diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 69949955aa..0000000000 --- a/.drone.yml +++ /dev/null @@ -1,18 +0,0 @@ -kind: pipeline -name: test on arm64 - -platform: - arch: arm64 - -steps: -- name: test - image: quay.io/condaforge/mambaforge:latest - environment: - MATPLOTLIB: agg - commands: - - lscpu - - mamba install hyperspy-base --only-deps - - mamba install scikit-learn pytest pytest-xdist - - pip install -e . - - pytest -n 4 - diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..7b663dd1dc --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# Run ruff format +922f3594ab343a49cbab22073fa79f073932e13d +8f05320b5419a482ac485f352f352ac4504d08f0 +614ccb9895f8e2aaa9a722712944b9eafa227c8c diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..035761c036 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'type: bug' + +--- + +#### Describe the bug +A clear and concise description of what the bug is. + +#### To Reproduce +Steps to reproduce the behavior: +``` +Minimum working example of code +``` + +#### Expected behavior +A clear and concise description of what you expected to happen. + +#### Python environment: + - HyperSpy version: 1.x.x + - Python version: 3.x + +#### Additional context +Add any other context about the problem here. If necessary, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000000..bdd4f85eda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,8 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..3f681faa25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'type: feature request' +assignees: '' + +--- + +#### Describe the functionality you would like to see. +A clear and concise description of what you would like to do. + +#### Describe the context +Do you want to extend existing functionalities, what types of signals should it apply to, etc. + +#### Additional information +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b8ba310070..8694f6df25 100755 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ ### Requirements -* Read the [developer guide](https://hyperspy.org/hyperspy-doc/current/dev_guide/index.html). +* Read the [developer guide](https://hyperspy.org/hyperspy-doc/current/dev_guide/intro.html). * Base your pull request on the [correct branch](https://hyperspy.org/hyperspy-doc/current/dev_guide/git.html#semantic-versioning-and-hyperspy-main-branches). * Filling out the template; it helps the review process and it is useful to summarise the PR. * This template can be updated during the progression of the PR to summarise its status. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..f77e937cfe --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + target-branch: "RELEASE_next_patch" diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 0000000000..a06a927e20 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,44 @@ +name: Documentation + +on: [push, pull_request] + +jobs: + Build: + # Use the "reusable workflow" from the hyperspy organisation + uses: hyperspy/.github/.github/workflows/doc.yml@main + with: + # install with speed extra to avoid warnings + pip_extra_doc: 'doc,speed' + # graphviz is required to build mermaid graph + # optipng is required to optimize thumbnail + install_package_ubuntu: graphviz optipng + + Push_dev: + needs: Build + # Push only on the "RELEASE_next_minor" and update the "dev" documentation + if: ${{ github.repository_owner == 'hyperspy' && github.ref_name == 'RELEASE_next_minor' }} + permissions: + # needs write permission to push the docs to gh-pages + contents: write + # Use the "reusable workflow" from the hyperspy organisation + uses: hyperspy/.github/.github/workflows/push_doc.yml@main + with: + repository: 'hyperspy/hyperspy-doc' + output_path: 'dev' + secrets: + access_token: ${{ secrets.PAT_DOCUMENTATION }} + + Push_tag: + needs: Build + # Push only on tag and update the "current" documentation + if: ${{ github.repository_owner == 'hyperspy' && startsWith(github.ref, 'refs/tags/') }} + permissions: + # needs write permission to push the docs to gh-pages + contents: write + # Use the "reusable workflow" from the hyperspy organisation + uses: hyperspy/.github/.github/workflows/push_doc.yml@main + with: + repository: 'hyperspy/hyperspy-doc' + output_path: 'current' + secrets: + access_token: ${{ secrets.PAT_DOCUMENTATION }} diff --git a/.github/workflows/nightly-merge.yml b/.github/workflows/nightly-merge.yml index fe6be8ecaf..f3772eae3d 100644 --- a/.github/workflows/nightly-merge.yml +++ b/.github/workflows/nightly-merge.yml @@ -12,10 +12,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Nightly Merge - uses: robotology/gh-action-nightly-merge@v1.3.2 + uses: robotology/gh-action-nightly-merge@81570ba03dd370f582bd3f52d47672d29191829f with: stable_branch: 'RELEASE_next_patch' development_branch: 'RELEASE_next_minor' diff --git a/.github/workflows/nightly-merge_non_uniform.yml b/.github/workflows/nightly-merge_non_uniform.yml deleted file mode 100644 index 1ece45add9..0000000000 --- a/.github/workflows/nightly-merge_non_uniform.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: 'Nightly Merge' - -on: - schedule: - - cron: '0 1 * * *' - -jobs: - nightly-merge: - permissions: - contents: write - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Nightly Merge - uses: robotology/gh-action-nightly-merge@v1.3.2 - with: - stable_branch: 'RELEASE_next_minor' - development_branch: 'non_uniform_axes' - allow_ff: true - allow_forks: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/package_and_test.yml b/.github/workflows/package_and_test.yml new file mode 100644 index 0000000000..4e1a195d43 --- /dev/null +++ b/.github/workflows/package_and_test.yml @@ -0,0 +1,9 @@ +name: Package & Test + +on: [push, pull_request] + +jobs: + package_and_test: + name: Package and Test + # Use the "reusable workflow" from the hyperspy organisation + uses: hyperspy/.github/.github/workflows/package_and_test.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21dd3daf24..068008d3b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,9 @@ name: Release +# Reusable workflow are not supported with trusted publisher +# https://github.com/pypa/gh-action-pypi-publish/issues/166 +# copy and paste +# https://github.com/hyperspy/.github/blob/main/.github/workflows/release_pure_python.yml + # This workflow builds the wheels "on tag". # If run from the hyperspy/hyperspy repository, the wheels will be uploaded to pypi ; # otherwise, the wheels will be available as a github artifact. @@ -9,140 +14,42 @@ on: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: - create_release: + package_and_test: + name: Package and Test + # Use the "reusable workflow" from the hyperspy organisation + uses: hyperspy/.github/.github/workflows/package_and_test.yml@main + + upload_to_pypi: + needs: [package_and_test] + runs-on: ubuntu-latest + name: Upload to pypi + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - name: Download dist + uses: actions/download-artifact@v4 + + - name: Display downloaded files + run: | + ls -shR + working-directory: dist + + - uses: pypa/gh-action-pypi-publish@release/v1 + if: ${{ startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'hyperspy' }} + # See https://docs.pypi.org/trusted-publishers/using-a-publisher/ + + create_github_release: + # If zenodo is setup to create a DOI automatically on a GitHub release, + # this step will trigger the mining of the DOI + needs: upload_to_pypi permissions: contents: write - name: Create Release + name: Create GitHub Release runs-on: ubuntu-latest - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: false - - build_wheels: - name: Wheels on ${{ matrix.os }}/py${{ matrix.python-version }} - needs: create_release - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-latest, macos-latest] - python-version: [3.6, 3.7, 3.8, 3.9] - - steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-python@v2 - name: Install Python - with: - python-version: ${{ matrix.python-version }} - - - name: What OS and Python version - run: | - uname -a - python --version - which python - - - name: install pep517 and twine - run: | - python -m pip install --upgrade pip - python -m pip install pep517 twine - - - name: Build wheels - run: | - python -m pep517.build --binary --out-dir dist/ . - - - name: Display content dist folder - run: | - ls dist/ - - - name: Install and test distribution - env: - MPLBACKEND: agg - run: | - pip install --pre --find-links dist hyperspy[tests] - pytest --pyargs hyperspy - - - uses: actions/upload-artifact@v2 - with: - path: ./dist/*.whl - - - name: Publish wheels to PyPI - if: github.repository_owner == 'hyperspy' - env: - # Github secret set in the hyperspy/hyperspy repository - # Not available from fork or pull request - # Secrets are not passed to workflows that are triggered by a pull request from a fork - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - twine upload dist/*.whl --verbose - - build_wheels_linux: - name: Wheels on ubuntu-latest - needs: create_release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: install twine - run: | - python -m pip install twine - - - name: Build manylinux Python wheels - uses: RalfG/python-wheels-manylinux-build@v0.3.3 - with: - python-versions: 'cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39' - build-requirements: 'cython' - - - name: Build source distribution - run: | - pip install pep517 - python -m pep517.build --source --out-dir sdist/ . - - - name: Display content dist folder - run: | - ls dist/ - ls sdist/ - - - name: Install and test distribution - env: - MPLBACKEND: agg - run: | - pip install --pre --find-links dist hyperspy[tests] - pytest --pyargs hyperspy - - - uses: actions/upload-artifact@v2 - with: - path: | - ./dist/*-manylinux*.whl - ./sdist/*.tar.gz - - - name: Publish wheels to PyPI - if: github.repository_owner == 'hyperspy' - env: - # Github secret set in the hyperspy/hyperspy repository - # Not available from fork or pull request - # Secrets are not passed to workflows that are triggered by a pull request from a fork - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - twine upload dist/*-manylinux*.whl --verbose - twine upload sdist/*.tar.gz --verbose - + if: ${{ startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'hyperspy' }} + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 260ca1a495..c9725fee43 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,113 +4,110 @@ on: [push, pull_request] jobs: run_test_site: - name: ${{ matrix.os }}-py${{ matrix.PYTHON_VERSION }}${{ matrix.LABEL }} - runs-on: ${{ matrix.os }}-latest + name: ${{ matrix.os }}-${{ matrix.os_version }}-py${{ matrix.PYTHON_VERSION }}${{ matrix.LABEL }} + runs-on: ${{ matrix.os }}-${{ matrix.os_version }} timeout-minutes: 30 env: MPLBACKEND: agg PIP_ARGS: --upgrade -e - PYTEST_ARGS: --pyargs hyperspy --reruns 3 -n 2 --instafail - PYTEST_ARGS_COVERAGE: + PYTEST_ARGS: --pyargs hyperspy --reruns 3 --instafail strategy: fail-fast: false matrix: os: [ubuntu, windows, macos] - PYTHON_VERSION: [3.8, 3.9] - PIP_SELECTOR: ['[all, tests]'] - DEPENDENCIES_DEV: [false] + os_version: [latest] + PYTHON_VERSION: ['3.9', '3.10', ] + PIP_SELECTOR: ['[all, tests, coverage]'] include: - # test oldest supported version of main dependencies on python 3.6 + # test oldest supported version of main dependencies on python 3.8 - os: ubuntu - PYTHON_VERSION: 3.6 - OLDEST_SUPPORTED_VERSION: true - DEPENDENCIES: matplotlib==3.1.0 numpy==1.17.1 scipy==1.1 imagecodecs==2019.12.3 dask==2.1.0 + os_version: latest + PYTHON_VERSION: '3.8' PIP_SELECTOR: '[all, tests, coverage]' - PYTEST_ARGS_COVERAGE: --cov=. --cov-report=xml + OLDEST_SUPPORTED_VERSION: true + # Don't install pillow 10.4.0 because of its incompatibility with numpy 1.20.x + # https://github.com/python-pillow/Pillow/pull/8187 + DEPENDENCIES: dask[array]==2021.5.1 matplotlib==3.1.3 numba==0.52 numpy==1.20.0 scipy==1.6 scikit-image==0.18 scikit-learn==1.0.1 pillow!=10.4.0 LABEL: -oldest # test minimum requirement - os: ubuntu - PYTHON_VERSION: 3.8 - PIP_SELECTOR: '[tests]' + os_version: latest + PYTHON_VERSION: '3.12' + PIP_SELECTOR: '[tests, coverage]' LABEL: -minimum - # Run coverage - os: ubuntu - PYTHON_VERSION: 3.8 + os_version: latest + PYTHON_VERSION: '3.8' PIP_SELECTOR: '[all, tests, coverage]' - PYTEST_ARGS_COVERAGE: --cov=. --cov-report=xml - LABEL: -coverage - # Run test suite against dependencies development version - - os: ubuntu - PYTHON_VERSION: 3.8 - PIP_SELECTOR: '[all, tests]' - LABEL: -dependencies_dev - DEPENDENCIES_DEV: true - DEPENDENCIES: numpy scipy scikit-learn scikit-image - - os: ubuntu - PYTHON_VERSION: 3.7 - PIP_SELECTOR: '[all, tests]' - exclude: - # redundant build (same as coverage) - os: ubuntu - PYTHON_VERSION: 3.8 + os_version: latest + PYTHON_VERSION: '3.11' + PIP_SELECTOR: '[all, tests, coverage]' + - os: macos + os_version: '13' + PYTHON_VERSION: '3.11' + PIP_SELECTOR: '[all, tests, coverage]' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Fetch tags upstream + if: ${{ github.repository_owner != 'hyperspy' }} + # Needs to fetch the tags from upstream to get the + # correct version with setuptools_scm + run: | + git remote add upstream https://github.com/hyperspy/hyperspy.git + git fetch upstream --tags - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v5 name: Install Python with: python-version: ${{ matrix.PYTHON_VERSION }} + - name: Get the number of CPUs + id: cpus + run: | + import os, platform + num_cpus = os.cpu_count() + print(f"Number of CPU: {num_cpus}") + print(f"Architecture: {platform.machine()}") + output_file = os.environ["GITHUB_OUTPUT"] + with open(output_file, "a", encoding="utf-8") as output_stream: + output_stream.write(f"count={num_cpus}\n") + shell: python + - name: Display version run: | python --version pip --version + - name: Install oldest supported version + if: ${{ matrix.OLDEST_SUPPORTED_VERSION }} + run: | + pip install ${{ matrix.DEPENDENCIES }} -v + - name: Install shell: bash run: | pip install ${{ env.PIP_ARGS }} .'${{ matrix.PIP_SELECTOR }}' - - name: Install oldest supported version - if: ${{ matrix.OLDEST_SUPPORTED_VERSION }} + - name: Pip list run: | - pip install ${{ matrix.DEPENDENCIES }} + pip list - - name: Install dependencies development version - if: ${{ matrix.DEPENDENCIES_DEV }} + - name: Run test suite run: | - pip install --upgrade --no-deps --pre \ - -i https://pypi.anaconda.org/scipy-wheels-nightly/simple \ - ${{ matrix.DEPENDENCIES }} + pytest ${{ env.PYTEST_ARGS }} -n ${{ steps.cpus.outputs.count }} --cov=. --cov-report=xml - - name: Run test suite + - name: Run doctest (Docstring) run: | - pytest ${{ env.PYTEST_ARGS }} ${{ matrix.PYTEST_ARGS_COVERAGE }} + pytest ${{ env.PYTEST_ARGS }} --doctest-modules --ignore=hyperspy/tests - name: Upload coverage to Codecov - if: ${{ always() }} && ${{ matrix.PYTEST_ARGS_COVERAGE }} - uses: codecov/codecov-action@v1 - - build_doc: - name: Build doc - runs-on: ubuntu-latest - env: - BUILD_DEPS: python3-dev build-essential graphviz - LATEX_DEPS: dvipng latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended - - steps: - - uses: actions/checkout@v2 - - - uses: ammaraskar/sphinx-action@master - with: - - pre-build-command: "apt-get update -y && apt-get install -y ${{ env.BUILD_DEPS }} ${{ env.LATEX_DEPS }} && pip install -e .[all,build-doc]" - build-command: make html - docs-folder: doc/ - - - uses: actions/upload-artifact@v2 - with: - path: ./doc/_build/html/ - name: doc_build - + if: ${{ github.repository_owner == 'hyperspy' }} + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/tests_extension.yml b/.github/workflows/tests_extension.yml new file mode 100644 index 0000000000..a24c320936 --- /dev/null +++ b/.github/workflows/tests_extension.yml @@ -0,0 +1,107 @@ +name: Integration tests + +on: + workflow_dispatch: + workflow: "*" + pull_request_review: + types: [submitted, edited] + pull_request: + types: [labeled, ready_for_review, reopened] + +jobs: + integration_test: + if: ${{ contains(github.event.pull_request.labels.*.name, 'run-extension-tests') || github.event_name == 'workflow_dispatch' }} + name: Extension_${{ matrix.EXTENSION_VERSION }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + #EXTENSION_VERSION: ['release', 'dev'] + EXTENSION_VERSION: ['dev'] + + env: + MPLBACKEND: agg + EXTENSION: kikuchipy lumispy pyxem atomap + TEST_DEPS: pytest pytest-xdist pytest-rerunfailures pytest-instafail + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v4 + + - uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-variant: Mambaforge + miniforge-version: latest + # use base environment, so that when using pip, this is from the + # mambaforge distribution + auto-activate-base: true + activate-environment: "" + + - name: Conda info + run: | + conda info + conda list + + - name: Install extensions and Test dependencies + run: | + mamba install hyperspy-base ${{ env.EXTENSION }} ${{ env.TEST_DEPS }} + + - name: Conda list + run: | + conda list + + - name: Install HyperSpy + run: | + pip install . + + - name: Install Extension Dev + if: contains(matrix.EXTENSION_VERSION, 'dev') + run: | + pip install https://github.com/lumispy/lumispy/archive/main.zip + pip install https://github.com/pyxem/kikuchipy/archive/develop.zip + pip install https://github.com/pyxem/pyxem/archive/main.zip + pip install https://gitlab.com/atomap/atomap/-/archive/master/atomap-master.zip + pip install https://github.com/hyperspy/holospy/archive/main.zip + pip install https://github.com/hyperspy/exspy/archive/main.zip + + - name: Install xvfb + run: | + # required for running kikuchipy test suite + sudo apt-get install xvfb + + - name: Run Kikuchipy Test Suite + if: ${{ always() }} + run: | + # Virtual buffer (xvfb) required for tests using PyVista + sudo apt-get install xvfb + xvfb-run python -m pytest --pyargs kikuchipy + + - name: Run LumiSpy Test Suite + if: ${{ always() }} + run: | + python -m pytest --pyargs lumispy + + - name: Run Pyxem Test Suite + if: ${{ always() }} + run: | + python -m pytest --pyargs pyxem + + - name: Run holospy Test Suite + if: ${{ always() }} + run: | + python -m pytest --pyargs holospy + + - name: Run exSpy Test Suite + if: ${{ always() }} + run: | + python -m pytest --pyargs exspy + + # The currently released version of Atomap (0.3.1) does not work with this test + # environment. Thus, only the dev version is currently tested. If a newer version + # of Atomap is released, the "if" can be changed to always(). + - name: Run atomap Test Suite + if: contains(matrix.EXTENSION_VERSION, 'dev') + run: | + python -m pytest --pyargs atomap diff --git a/.gitignore b/.gitignore index 609a760e7f..3d1527e456 100755 --- a/.gitignore +++ b/.gitignore @@ -1,51 +1,56 @@ syntax: glob -.spyderworkspace -.coverage -/MANIFEST -.hook_ignore* -*.so -*.pyc -*.pyd -*.swp *~ *.bak +*.deb *.mex +*nbc +*nbi *.o -dist -*.deb +*.pyc +*.pyd +*.pyproj +*.so *.swo +*.swp +*.DS_Store +*__pycache__ +*Thumbs.db +/.cache/ +/.spyproject/ +.coverage +.hook_ignore* +.idea/* +.python-version +.ropeproject/* +.spyderproject +.spyderworkspace +~.vimrc +desktop.ini +dist +/MANIFEST build/* bin/*.bat PortableInstall/* hyperspy.egg-info/* -deb_dist/* -~.vimrc data/octave-core -.spyderproject +deb_dist/* doc/_build/* doc/.build/* doc/api/* -doc/user_guide/_build/* -doc/user_guide/.build/* -setup/windows/requires/* -setup/windows/reccomends/* -doc/examples +doc/auto_examples doc/gh-pages doc/log.txt -*Thumbs.db -*.DS_Store -.ropeproject/* -.idea/* -desktop.ini -*.pyproj -hyperspy/tests/misc/cython/test_cython_integration.c +doc/user_guide/_build/* +doc/user_guide/.build/* +examples/io/*.msa +examples/io/rgb*.jpg +examples/region_of_interest/*.msa hyperspy/io_plugins/unbcf_fast.c hyperspy/misc/etc/test_compilers.obj -/.cache/ -/.spyproject/ -*nbc -*nbi hyperspy/tests/io/edax_files.zip +hyperspy/tests/misc/cython/test_cython_integration.c +setup/windows/requires/* +setup/windows/reccomends/* ### Code ### # Visual Studio Code - https://code.visualstudio.com/ @@ -53,4 +58,7 @@ hyperspy/tests/io/edax_files.zip .vscode/ tsconfig.json jsconfig.json -*egg-info* \ No newline at end of file +*egg-info* +*__pycache__* + +*sg_execution_times.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..93c89e2c4a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.5.0 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format +ci: + # Don't run automatically on PRs, instead add the comment + # "pre-commit.ci autofix" on a pull request to manually trigger auto-fixing + autofix_prs: false + autoupdate_branch: 'RELEASE_next_patch' diff --git a/AUTHORS.txt b/AUTHORS.txt index 8b3e079a99..de5f10e58b 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,5 +1,3 @@ -HyperSpy is maintened by `an active community of developers `_. +HyperSpy is maintained by `an active community of developers `_. The HyperSpy logo was created by Stefano Mazzucco. It is a modified version of `this figure `_ and the same GFDL license applies. - -The website is a fork of the Ipython website. diff --git a/CHANGES.rst b/CHANGES.rst index 7f64ea039a..1e5879dcd9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,15 +1,818 @@ +.. _changelog: + Changelog ********* Changelog entries for the development version are available at -https://hyperspy.readthedocs.io/en/latest/user_guide/changes.html +https://hyperspy.readthedocs.io/en/latest/changes.html .. towncrier-draft-entries:: |release| [UNRELEASED] .. towncrier release notes start -Hyperspy 1.6.4 (2021-07-08) -=========================== +2.1.1 (2024-07-11) +================== + +Enhancements +------------ + +- Only import gui widget when the toolkit is enabled. (`#3366 `_) +- Tweak documention on dask scheduler. (`#3380 `_) + + +Bug Fixes +--------- + +- Fix updating residual line in model plot when changing components. (`#3355 `_) +- Avoid changing ``data`` object when using :meth:`~.api.signals.BaseSignal.add_gaussian_noise` and :meth:`~.api.signals.BaseSignal.add_poissonian_noise`. (`#3379 `_) +- Avoid replacing the original mask in :meth:`~.api.signals.BaseSignal.blind_source_separation` (`#3384 `_) +- Fix gradients in some expression based components (`#3388 `_) + + +Improved Documentation +---------------------- + +- Add old version warning banner in documentation. (`#3397 `_) + + +Maintenance +----------- + +- Add support for numpy 2.0. (`#3386 `_) + + +2.1.0 (2024-05-08) +================== + +Enhancements +------------ + +- Add a dynamic navigator which updates when the number of navigation dimensions is greater than 3 (`#3199 `_) +- Add an example to the gallery to show how to extract a line profile from an image using a :class:`~.api.roi.Line2DROI` (`#3227 `_) +- Add an example showing how to handle RGB images. (`#3346 `_) +- Use :func:`pint.get_application_registry` to get :class:`pint.UnitRegistry` and facilitate interoperability of pint quantity operation with other modules. (`#3357 `_) +- :func:`~.api.plot.plot_roi_map` improvement: + + - Add ROIs on signal plot instead of adding them on a sum signal. + - Add support for more than 3 ROIs. + - Add ``color`` and ``cmap`` parameters to specify color and colormap. + - Add ``single_figure`` parameter to enable plotting on a single figure using :func:`~.api.plot.plot_images` or :func:`~.api.plot.plot_spectra`. + - Improve performance of updating ROI maps by a factor 2-4 when moving ROI. + - Update images in :func:`~.api.plot.plot_images` on data change event. + - Remove ROI widgets and disconnect interactive function when closing ROI map figures. (`#3364 `_) +- Documentation improvement. (`#3365 `_) + + +Bug Fixes +--------- + +- Fix ROI slicing of non-uniform axis (`#3328 `_) +- Add the ability to save and load :class:`~.axes.BaseDataAxis` objects to a hyperspy file. (`#3342 `_) +- Fix navigator event disconnection and fix plot when changing dtype from/to rbgx. (`#3346 `_) +- Fix :func:`~.api.get_configuration_directory_path` function. (`#3349 `_) +- Fix :func:`~.api.plot.plot_images` axis ticks discrepancy. (`#3361 `_) +- Fixes in :func:`~.api.plot.plot_roi_map`: + + - Fix slicing signal when using CircleROI (`#3358 `_). + - Fix redundant events when using :class:`~.api.roi.SpanROI`, which was causing flickering of the colorbar in the ROI map figures (`#3364 `_) +- Fix development version on ``RELEASE_next_minor`` branch. (`#3368 `_) + + +API changes +----------- + +- :func:`~.api.plot.plot_roi_map` doesn't return the sum of all ROI maps (``all_sum``) and the signals sliced by the ROIs (``roi_signals``), these can be obtained separately using the ``rois`` returned by :func:`~.api.plot.plot_roi_map` and :func:`~.api.interactive`. (`#3364 `_) + + +Maintenance +----------- + +- Ruff update: + + - Set the ``RELEASE_next_patch`` branch as target for the ``pre-commit.ci`` update to keep all branches in synchronisation. + - Update ruff to version 0.3.3 and run ruff check/format on source code. (`#3335 `_) +- Replace deprecated ``np.string_`` by ``np.bytes_``. (`#3338 `_) +- Enable ruff isort and all pyflakes/Pycodestyle rules, except E501 to avoid confict with black formatting. (`#3348 `_) +- Merge ``hyperspy.api.no`` and ``hyperspy.api.no_gui`` modules since the latter is not necessary anymore. (`#3349 `_) +- Convert projet readme to markdown, fixes badges on github (`#3351 `_) +- Simplify Azure Pipeline CI by removing build and uploading wheels, since this is now done on GitHub CI. (`#3356 `_) +- Fix duplicated test and occasional test failure. (`#3365 `_) +- Use lower case when checking matplotlib backend in the test suite. (`#3367 `_) +- Add ``percentile_range`` traitsui attribute to ``ImageContrastEditor`` necessary for `hyperspy/hyperspy_gui_traitsui#76 `_. (`#3368 `_) + + +2.0.1 (2024-02-26) +================== + +Bug Fixes +--------- + +- Fix bug with side by side plotting of signal containing navigation dimension only. (`#3304 `_) +- Fix getting release on some linux system wide install, e.g. Debian or Google colab (`#3318 `_) +- Fix incorrect position of ``Texts`` marker when using mathtext. (`#3319 `_) + + +Maintenance +----------- + +- Update version switcher. (`#3291 `_) +- Fix readme badges and fix broken web links. (`#3298 `_) +- Use ruff to lint code. (`#3299 `_) +- Use ruff to format code. (`#3300 `_) +- Run test suite on osx arm64 on GitHub CI and speed running test suite using all available CPUs (3 or 4) instead of only 2. (`#3305 `_) +- Fix API changes in scipy (:func:`scipy.signal.windows.tukey`) and scikit-image (:func:`skimage.restoration.unwrap_phase`). (`#3306 `_) +- Fix deprecation warnings and warnings in the test suite (`#3320 `_) +- Add documentation on how the documentation is updated and the required manual changes for minor and major releases. (`#3321 `_) +- Add Google Analytics ID to learn more about documentation usage. (`#3322 `_) +- Setup workflow to push development documentation automatically. (`#3297 `_) + +.. _changes_2.0: + +2.0 (2023-12-20) +================ + +Release Highlights +------------------ +- Hyperspy has split off some of the file reading/writing and domain specific functionalities into separate libraries! + + - `RosettaSciIO `_: A library for reading and writing scientific data files. + See `RosettaSciIO release notes `_ for new features and supported formats. + - `exSpy `_: A library for EELS and EDS analysis. + See `exSpy release notes `_ for new features. + - `holoSpy `_: A library for analysis of (off-axis) electron holography data. + See `holoSpy release notes `_ for new features. + +- The :py:mod:`~.api.plot.markers` API has been refactored + + - Lazy markers are now supported + - Plotting many markers is now `much` faster + - Added :py:class:`~.api.plot.markers.Polygons` marker + +- The documentation has been restructured and improved! + + - Short example scripts are now included in the documentation + - Improved guides for lazy computing as well as an improved developer guide + +- Plotting is easier and more consistent: + + - Added horizontal figure layout choice when using the ``ipympl`` backend + - Changing navigation coordinates using the keyboard arrow-keys has been removed. + Use ``Crtl`` + ``Arrow`` instead. + - Jump to navigation position using ``shift`` + click in the navigator figure. + +- HyperSpy now works with Pyodide/Jupyterlite, checkout `hyperspy.org/jupyterlite-hyperspy `_! +- The deprecated API has removed: see the list of API changes and removal in the :ref:`sections below `. + +New features +------------ + +- :py:meth:`~._signals.lazy.LazySignal.compute` will now pass keyword arguments to the dask :meth:`dask.array.Array.compute` method. This enables setting the scheduler and the number of computational workers. (`#2971 `_) +- Changes to :meth:`~.api.signals.BaseSignal.plot`: + + - Added horizontal figure layout choice when using the ``ipympl`` backend. The default layour can be set in the plot section of the preferences GUI. (`#3140 `_) + +- Changes to :meth:`~.api.signals.Signal2D.find_peaks`: + + - Lazy signals return lazy peak signals + - ``get_intensity`` argument added to get the intensity of the peaks + - The signal axes are now stored in the ``metadata.Peaks.signal_axes`` attribute of the peaks' signal. (`#3142 `_) + +- Change the logging output so that logging messages are not displayed in red, to avoid confusion with errors. (`#3173 `_) +- Added ``hyperspy.decorators.deprecated`` and ``hyperspy.decorators.deprecated_argument``: + + - Provide consistent and clean deprecation + - Added a guide for deprecating code (`#3174 `_) + +- Add functionality to select navigation position using ``shift`` + click in the navigator. (`#3175 `_) +- Added a ``plot_residual`` to :py:meth:`~.models.model1d.Model1D.plot`. When ``True``, a residual line (Signal - Model) appears in the model figure. (`#3186 `_) +- Switch to :meth:`matplotlib.axes.Axes.pcolormesh` for image plots involving non-uniform axes. + The following cases are covered: 2D-signal with arbitrary navigation-dimension, 1D-navigation and 1D-signal (linescan). + Not covered are 2D-navigation images (still uses sliders). (`#3192 `_) +- New :meth:`~.api.signals.BaseSignal.interpolate_on_axis` method to switch one axis of a signal. The data is interpolated in the process. (`#3214 `_) +- Added :func:`~.api.plot.plot_roi_map`. Allows interactively using a set of ROIs to select regions of the signal axes of a signal and visualise how the signal varies in this range spatially. (`#3224 `_) + + +Bug Fixes +--------- + +- Improve syntax in the `io` module. (`#3091 `_) +- Fix behaviour of :py:class:`~.misc.utils.DictionaryTreeBrowser` setter with value of dictionary type (`#3094 `_) +- Avoid slowing down fitting by optimising attribute access of model. (`#3155 `_) +- Fix harmless error message when using multiple :class:`~.api.roi.RectangularROI`: check if resizer patches are drawn before removing them. Don't display resizers when adding the widget to the figure (widget in unselected state) for consistency with unselected state (`#3222 `_) +- Fix keeping dtype in :py:meth:`~.api.signals.BaseSignal.rebin` when the endianess is specified in the dtype (`#3237 `_) +- Fix serialization error due to ``traits.api.Property`` not being serializable if a dtype is specified. + See #3261 for more details. (`#3262 `_) +- Fix setting bounds for ``"trf"``, ``"dogbox"`` optimizer (`#3244 `_) +- Fix bugs in new marker implementation: + + - Markers str representation fails if the marker isn't added to a signal + - make :meth:`~.api.plot.markers.Markers.from_signal` to work with all markers - it was only working with :class:`~.api.plot.markers.Points` (`#3270 `_) +- Documentation fixes: + + - Fix cross-references in documentation and enable sphinx "nitpicky" when building documentation to check for broken links. + - Fix using mutable objects as default argument. + - Change some :class:`~.component.Component` attributes to properties in order to include their docstrings in the API reference. (`#3273 `_) + + + + +Improved Documentation +---------------------- + +- Restructure documentation: + + - Improve structure of the API reference + - Improve introduction and overall structure of documentation + - Add gallery of examples (`#3050 `_) + +- Add examples to the gallery to show how to use SpanROI and slice signal interactively (`#3221 `_) +- Add a section on keeping a clean and sensible commit history to the developer guide. (`#3064 `_) +- Replace ``sphinx.ext.imgmath`` by ``sphinx.ext.mathjax`` to fix the math rendering in the *ReadTheDocs* build (`#3084 `_) +- Fix docstring examples in :class:`~.api.signals.BaseSignal` class. + Describe how to test docstring examples in developer guide. (`#3095 `_) +- Update intersphinx_mapping links of matplotlib, numpy and scipy. (`#3218 `_) +- Add examples on creating signal from tabular data or reading from a simple text file (`#3246 `_) +- Activate checking of example code in docstring and user guide using ``doctest`` and fix errors in the code. (`#3281 `_) +- Update warning of "beta" state in big data section to be more specific. (`#3282 `_) + + +Enhancements +------------ + +- Add support for passing ``**kwargs`` to :py:meth:`~.api.signals.Signal2D.plot` when using heatmap style in :py:func:`~.api.plot.plot_spectra` . (`#3219 `_) +- Add support for pep 660 on editable installs for pyproject.toml based builds of extension (`#3252 `_) +- Make HyperSpy compatible with pyodide (hence JupyterLite): + + - Set ``numba`` and ``numexpr`` as optional dependencies. + - Replace ``dill`` by ``cloudpickle``. + - Fallback to dask synchronous scheduler when running on pyodide. + - Reduce packaging size to less than 1MB. + - Add packaging test on GitHub CI. (`#3255 `_) + +.. _hyperspy_2.0_api_changes: + +API changes +----------- + +- RosettaSciIO was split out of the `HyperSpy repository `_ on July 23, 2022. The IO-plugins and related functions so far developed in HyperSpy were moved to the `RosettaSciIO repository `__. (`#2972 `_) +- Extend the IO functions to accept alias names for format ``name`` as defined in RosettaSciIO. (`#3009 `_) +- Fix behaviour of :meth:`~hyperspy.model.BaseModel.print_current_values`, :meth:`~.component.Component.print_current_values` + and :func:`~.api.print_known_signal_types`, which were not printing when running from a script - they were only printing when running in notebook or qtconsole. Now all print_* functions behave consistently: they all print the output instead of returning an object (string or html). The :func:`IPython.display.display` will pick a suitable rendering when running in an "ipython" context, for example notebook, qtconsole. (`#3145 `_) +- The markers have been refactored - see the new :py:mod:`~.api.plot.markers` API and the :ref:`gallery of examples ` for usage. The new :py:class:`~.api.plot.markers.Markers` uses :py:class:`matplotlib.collections.Collection`, is faster and more generic than the previous implementation and also supports lazy markers. Markers saved in HyperSpy files (``hspy``, ``zspy``) with HyperSpy < 2.0 are converted automatically when loading the file. (`#3148 `_) +- For all functions with the ``rechunk`` parameter, the default has been changed from ``True`` to ``False``. This means HyperSpy will not automatically try to change the chunking for lazy signals. The old behaviour could lead to a reduction in performance when working with large lazy datasets, for example 4D-STEM data. (`#3166 `_) +- Renamed ``Signal2D.crop_image`` to :meth:`~.api.signals.Signal2D.crop_signal` (`#3197 `_) +- Changes and improvement of the map function: + + - Removes the ``parallel`` argument + - Replace the ``max_workers`` with the ``num_workers`` argument to be consistent with ``dask`` + - Adds more documentation on setting the dask backend and how to use multiple cores + - Adds ``navigation_chunk`` argument for setting the chunks with a non-lazy signal + - Fix axes handling when the function to be mapped can be applied to the whole dataset - typically when it has the ``axis`` or ``axes`` keyword argument. (`#3198 `_) + +- Remove ``physics_tools`` since it is not used and doesn't fit in the scope of HyperSpy. (`#3235 `_) +- Improve the readability of the code by replacing the ``__call__`` method of some objects with the more explicit ``_get_current_data``. + + - Rename ``__call__`` method of :py:class:`~.api.signals.BaseSignal` to ``_get_current_data``. + - Rename ``__call__`` method of :py:class:`hyperspy.model.BaseModel` to ``_get_current_data``. + - Remove ``__call__`` method of the :py:class:`hyperspy.component.Component` class. (`#3238 `_) + +- Rename ``hyperspy.api.datasets`` to :mod:`hyperspy.api.data` and simplify submodule structure: + + - ``hyperspy.api.datasets.artificial_data.get_atomic_resolution_tem_signal2d`` is renamed to :func:`hyperspy.api.data.atomic_resolution_image` + - ``hyperspy.api.datasets.artificial_data.get_luminescence_signal`` is renamed to :func:`hyperspy.api.data.luminescence_signal` + - ``hyperspy.api.datasets.artificial_data.get_wave_image`` is renamed to :func:`hyperspy.api.data.wave_image` (`#3253 `_) + + +API Removal +----------- + +As the HyperSpy API evolves, some of its parts are occasionally reorganized or removed. +When APIs evolve, the old API is deprecated and eventually removed in a major +release. The functions and methods removed in HyperSpy 2.0 are listed below along +with migration advises: + +Axes +^^^^ + +- ``AxesManager.show`` has been removed, use :py:meth:`~.axes.AxesManager.gui` instead. +- ``AxesManager.set_signal_dimension`` has been removed, use :py:meth:`~.api.signals.BaseSignal.as_signal1D`, + :py:meth:`~.api.signals.BaseSignal.as_signal2D` or :py:meth:`~.api.signals.BaseSignal.transpose` of the signal instance instead. + +Components +^^^^^^^^^^ + +- The API of the :py:class:`~.api.model.components1D.Polynomial` has changed (it was deprecated in HyperSpy 1.5). The old API had a single parameters ``coefficients``, which has been replaced by ``a0``, ``a1``, etc. +- The ``legacy`` option (introduced in HyperSpy 1.6) for :class:`~.api.model.components1D.Arctan` has been removed, use :class:`exspy.components.EELSArctan` to use the old API. +- The ``legacy`` option (introduced in HyperSpy 1.6) for :class:`~.api.model.components1D.Voigt` has been removed, use :class:`exspy.components.PESVoigt` to use the old API. + +Data Visualization +^^^^^^^^^^^^^^^^^^ + +- The ``saturated_pixels`` keyword argument of :py:meth:`~.api.signals.Signal2D.plot` has been removed, use ``vmin`` and/or ``vmax`` instead. +- The ``get_complex`` property of ``hyperspy.drawing.signal1d.Signal1DLine`` has been removed. +- The keyword argument ``line_style`` of :py:func:`~.api.plot.plot_spectra` has been renamed to ``linestyle``. +- Changing navigation coordinates using keyboard ``Arrow`` has been removed, use + ``Crtl`` + ``Arrow`` instead. +- The ``markers`` submodules can not be imported from the :py:mod:`~.api` anymore, use :py:mod:`hyperspy.api.plot.markers` + directly, i.e. :class:`hyperspy.api.plot.markers.Arrows`, instead. +- The creation of markers has changed to use their class name instead of aliases, for example, + use ``m = hs.plot.markers.Lines`` instead of ``m = hs.plot.markers.line_segment``. + +Loading and Saving data +^^^^^^^^^^^^^^^^^^^^^^^ + +The following deprecated keyword arguments have been removed during the +migration of the IO plugins to the `RosettaSciIO library +`_: + +- The arguments ``mmap_dir`` and ``load_to_memory`` of the :py:func:`~.api.load` + function have been removed, use the ``lazy`` argument instead. +- :ref:`Bruker composite file (BCF) `: The ``'spectrum'`` option for the + ``select_type`` parameter was removed. Use ``'spectrum_image'`` instead. +- :ref:`Electron Microscopy Dataset (EMD) NCEM `: Using the + keyword ``dataset_name`` was removed, use ``dataset_path`` instead. +- :ref:`NeXus data format `: The ``dataset_keys``, ``dataset_paths`` + and ``metadata_keys`` keywords were removed. Use ``dataset_key``, ``dataset_path`` + and ``metadata_key`` instead. + +Machine Learning +^^^^^^^^^^^^^^^^ + +- The ``polyfit`` keyword argument has been removed. Use ``var_func`` instead. +- The list of possible values for the ``algorithm`` argument of the :py:meth:`~.api.signals.BaseSignal.decomposition` method + has been changed according to the following table: + + .. list-table:: Change of the ``algorithm`` argument + :widths: 25 75 + :header-rows: 1 + + * - hyperspy < 2.0 + - hyperspy >= 2.0 + * - fast_svd + - SVD along with the argument svd_solver="randomized" + * - svd + - SVD + * - fast_mlpca + - MLPCA along with the argument svd_solver="randomized + * - mlpca + - MLPCA + * - nmf + - NMF + * - RPCA_GoDec + - RPCA + +- The argument ``learning_rate`` of the ``ORPCA`` algorithm has been renamed to ``subspace_learning_rate``. +- The argument ``momentum`` of the ``ORPCA`` algorithm has been renamed to ``subspace_momentum``. +- The list of possible values for the ``centre`` keyword argument of the :py:meth:`~.api.signals.BaseSignal.decomposition` method + when using the ``SVD`` algorithm has been changed according to the following table: + + .. list-table:: Change of the ``centre`` argument + :widths: 50 50 + :header-rows: 1 + + * - hyperspy < 2.0 + - hyperspy >= 2.0 + * - trials + - navigation + * - variables + - signal +- For lazy signals, a possible value of the ``algorithm`` keyword argument of the + :py:meth:`~._signals.lazy.LazySignal.decomposition` method has been changed + from ``"ONMF"`` to ``"ORNMF"``. +- Setting the ``metadata`` and ``original_metadata`` attribute of signals is removed, use + the :py:meth:`~.misc.utils.DictionaryTreeBrowser.set_item` and + :py:meth:`~.misc.utils.DictionaryTreeBrowser.add_dictionary` methods of the + ``metadata`` and ``original_metadata`` attribute instead. + + +Model fitting +^^^^^^^^^^^^^ + +- The ``iterpath`` default value has changed from ``'flyback'`` to ``'serpentine'``. +- Changes in the arguments of the :py:meth:`~hyperspy.model.BaseModel.fit` and :py:meth:`~hyperspy.model.BaseModel.multifit` methods: + + - The ``fitter`` argument has been renamed to ``optimizer``. + - The list of possible values for the ``optimizer`` argument has been renamed according to the following table: + + .. list-table:: Renaming of the ``optimizer`` argument + :widths: 50 50 + :header-rows: 1 + + * - hyperspy < 2.0 + - hyperspy >= 2.0 + * - fmin + - Nelder-Mead + * - fmin_cg + - CG + * - fmin_ncg + - Newton-CG + * - fmin_bfgs + - Newton-BFGS + * - fmin_l_bfgs_b + - L-BFGS-B + * - fmin_tnc + - TNC + * - fmin_powell + - Powell + * - mpfit + - lm + * - leastsq + - lm + + - ``loss_function="ml"`` has been renamed to ``loss_function="ML-poisson"``. + - ``grad=True`` has been changed to ``grad="analytical"``. + - The ``ext_bounding`` argument has been renamed to ``bounded``. + - The ``min_function`` argument has been removed, use the ``loss_function`` argument instead. + - The ``min_function_grad`` argument has been removed, use the ``grad`` argument instead. + +- The following :py:class:`~hyperspy.model.BaseModel` methods have been removed: + + - ``hyperspy.model.BaseModel.set_boundaries`` + - ``hyperspy.model.BaseModel.set_mpfit_parameters_info`` + +- The arguments ``parallel`` and ``max_workers`` have been removed from the :py:meth:`~hyperspy.model.BaseModel.as_signal` methods. + +- Setting the ``metadata`` attribute of a :py:class:`~.samfire.Samfire` has been removed, use + the :py:meth:`~.misc.utils.DictionaryTreeBrowser.set_item` and + :py:meth:`~.misc.utils.DictionaryTreeBrowser.add_dictionary` methods of the + ``metadata`` attribute instead. + +- The deprecated ``twin_function`` and ``twin_inverse_function`` have been privatized. +- Remove ``fancy`` argument of :meth:`~hyperspy.model.BaseModel.print_current_values` and :meth:`~.component.Component.print_current_values`, + which wasn't changing the output rendering. +- The attribute ``channel_switches`` of :py:class:`~hyperspy.model.BaseModel` have been privatized, instead + use the :py:meth:`~hyperspy.model.BaseModel.set_signal_range_from_mask` or any other methods to + set the signal range, such as :py:meth:`~.models.model1d.Model1D.set_signal_range`, + :py:meth:`~.models.model1d.Model1D.add_signal_range` or :py:meth:`~.models.model1d.Model1D.remove_signal_range` + and their :py:class:`~.models.model2d.Model2D` counterparts. + + +Signal +^^^^^^ + +- ``metadata.Signal.binned`` is removed, use the ``is_binned`` axis attribute + instead, e. g. ``s.axes_manager[-1].is_binned``. +- Some possible values for the ``bins`` argument of the :py:meth:`~.api.signals.BaseSignal.get_histogram` + method have been changed according to the following table: + + .. list-table:: Change of the ``bins`` argument + :widths: 50 50 + :header-rows: 1 + + * - hyperspy < 2.0 + - hyperspy >= 2.0 + * - scotts + - scott + * - freedman + - fd + +- The ``integrate_in_range`` method has been removed, use :py:class:`~.roi.SpanROI` + followed by :py:meth:`~.api.signals.BaseSignal.integrate1D` instead. +- The ``progressbar`` keyword argument of the :py:meth:`~._signals.lazy.LazySignal.compute` method + has been removed, use ``show_progressbar`` instead. +- The deprecated ``comp_label`` argument of the methods :py:meth:`~.api.signals.BaseSignal.plot_decomposition_loadings`, + :py:meth:`~.api.signals.BaseSignal.plot_decomposition_factors`, :py:meth:`~.api.signals.BaseSignal.plot_bss_loadings`, + :py:meth:`~.api.signals.BaseSignal.plot_bss_factors`, :py:meth:`~.api.signals.BaseSignal.plot_cluster_distances`, + :py:meth:`~.api.signals.BaseSignal.plot_cluster_labels` has been removed, use the ``title`` argument instead. +- The :py:meth:`~.api.signals.BaseSignal.set_signal_type` now raises an error when passing + ``None`` to the ``signal_type`` argument. Use ``signal_type=""`` instead. +- Passing an "iterating over navigation argument" to the :py:meth:`~.api.signals.BaseSignal.map` + method is removed, pass a HyperSpy signal with suitable navigation and signal shape instead. + + +Signal2D +^^^^^^^^ + +- :meth:`~.api.signals.Signal2D.find_peaks` now returns lazy signals in case of lazy input signal. + + +Preferences +^^^^^^^^^^^ + +- The ``warn_if_guis_are_missing`` HyperSpy preferences setting has been removed, + as it is not necessary anymore. + + +Maintenance +----------- + +- Pin third party GitHub actions and add maintenance guidelines on how to update them (`#3027 `_) +- Drop support for python 3.7, update oldest supported dependencies and simplify code accordingly (`#3144 `_) +- IPython and IParallel are now optional dependencies (`#3145 `_) +- Fix Numpy 1.25 deprecation: implicit array to scalar conversion in :py:meth:`~.signals.Signal2D.align2D` (`#3189 `_) +- Replace deprecated :mod:`scipy.misc` by :mod:`scipy.datasets` in documentation (`#3225 `_) +- Fix documentation version switcher (`#3228 `_) +- Replace deprecated :py:class:`scipy.interpolate.interp1d` with :py:func:`scipy.interpolate.make_interp_spline` (`#3233 `_) +- Add support for python 3.12 (`#3256 `_) +- Consolidate package metadata: + + - use ``pyproject.toml`` only + - clean up unmaintained packaging files + - use ``setuptools_scm`` to define version + - add python 3.12 to test matrix (`#3268 `_) +- Pin pytest-xdist to 3.5 as a workaround for test suite failure on Azure Pipeline (`#3274 `_) + + + +.. _changes_1.7.6: + +1.7.6 (2023-11-17) +=================== + +Bug Fixes +--------- + +- Allows for loading of ``.hspy`` files saved with version 2.0.0 and greater and no unit or name set + for some axis. (`#3241 `_) + + +Maintenance +----------- + +- Backport of 3189: fix Numpy1.25 deprecation: implicite array to scalar conversion in :py:meth:`~.api.signals.Signal2D.align2D` (`#3243 `_) +- Pin pillow to <10.1 to avoid imageio error. (`#3251 `_) + + +.. _changes_1.7.5: + +1.7.5 (2023-05-04) +=================== + +Bug Fixes +--------- + +- Fix plotting boolean array with :py:func:`~.api.plot.plot_images` (`#3118 `_) +- Fix test with scipy1.11 and update deprecated ``scipy.interpolate.interp2d`` in the test suite (`#3124 `_) +- Use intersphinx links to fix links to scikit-image documentation (`#3125 `_) + + +Enhancements +------------ + +- Improve performance of `model.multifit` by avoiding `axes.is_binned` repeated evaluation (`#3126 `_) + + +Maintenance +----------- + +- Simplify release workflow and replace deprecated ``actions/create-release`` action with ``softprops/action-gh-release``. (`#3117 `_) +- Add support for python 3.11 (`#3134 `_) +- Pin ``imageio`` to <2.28 (`#3138 `_) + + +.. _changes_1.7.4: + +1.7.4 (2023-03-16) +=================== + +Bug Fixes +--------- + +- Fixes an array indexing bug when loading a .sur file format spectra series. (`#3060 `_) +- Speed up ``to_numpy`` function to avoid slow down when used repeatedly, typically during fitting (`#3109 `_) + + +Improved Documentation +---------------------- + +- Replace ``sphinx.ext.imgmath`` by ``sphinx.ext.mathjax`` to fix the math rendering in the *ReadTheDocs* build (`#3084 `_) + + +Enhancements +------------ + +- Add support for Phenom .elid revision 3 and 4 formats (`#3073 `_) + + +Maintenance +----------- + +- Add pooch as test dependency, as it is required to use scipy.dataset in latest scipy (1.10) and update plotting test. Fix warning when plotting non-uniform axis (`#3079 `_) +- Fix matplotlib 3.7 and scikit-learn 1.4 deprecations (`#3102 `_) +- Add support for new pattern to generate random numbers introduced in dask 2023.2.1. Deprecate usage of :py:class:`numpy.random.RandomState` in favour of :py:func:`numpy.random.default_rng`. Bump scipy minimum requirement to 1.4.0. (`#3103 `_) +- Fix checking links in documentation for domain, which aren't compatible with sphinx linkcheck (`#3108 `_) + + +.. _changes_1.7.3: + +1.7.3 (2022-10-29) +=================== + +Bug Fixes +--------- + +- Fix error when reading Velox containing FFT with odd number of pixels (`#3040 `_) +- Fix pint Unit for pint>=0.20 (`#3052 `_) + + +Maintenance +----------- + +- Fix deprecated import of scipy ``ascent`` in docstrings and the test suite (`#3032 `_) +- Fix error handling when trying to convert a ragged signal to non-ragged for numpy >=1.24 (`#3033 `_) +- Fix getting random state dask for dask>=2022.10.0 (`#3049 `_) + + +.. _changes_1.7.2: + +1.7.2 (2022-09-17) +=================== + +Bug Fixes +--------- + +- Fix some errors and remove unnecessary code identified by `LGTM + `_. (`#2977 `_) +- Fix error which occurs when guessing output size in the :py:meth:`~.api.signals.BaseSignal.map` function and using dask newer than 2022.7.1 (`#2981 `_) +- Fix display of x-ray lines when using log norm and the intensity at the line is 0 (`#2995 `_) +- Fix handling constant derivative in :py:meth:`~.api.signals.Signal1D.spikes_removal_tool` (`#3005 `_) +- Fix removing horizontal or vertical line widget; regression introduced in hyperspy 1.7.0 (`#3008 `_) + + +Improved Documentation +---------------------- + +- Add a note in the user guide to explain that when a file contains several datasets, :py:func:`~.api.load` returns a list of signals instead of a single signal and that list indexation can be used to access a single signal. (`#2975 `_) + + +Maintenance +----------- + +- Fix extension test suite CI workflow. Enable workflow manual trigger (`#2982 `_) +- Fix deprecation warning and time zone test failing on windows (locale dependent) (`#2984 `_) +- Fix external links in the documentation and add CI build to check external links (`#3001 `_) +- Fix hyperlink in bibliography (`#3015 `_) +- Fix matplotlib ``SpanSelector`` import for matplotlib 3.6 (`#3016 `_) + + +.. _changes_1.7.1: + +1.7.1 (2022-06-18) +=================== + +Bug Fixes +--------- + +- Fixes invalid file chunks when saving some signals to hspy/zspy formats. (`#2940 `_) +- Fix issue where a TIFF image from an FEI FIB/SEM navigation camera image would not be read due to missing metadata (`#2941 `_) +- Respect ``show_progressbar`` parameter in :py:meth:`~.api.signals.BaseSignal.map` (`#2946 `_) +- Fix regression in :py:meth:`~hyperspy.models.model1d.Model1D.set_signal_range` which was raising an error when used interactively (`#2948 `_) +- Fix :py:class:`~.api.roi.SpanROI` regression: the output of :py:meth:`~.roi.BaseInteractiveROI.interactive` was not updated when the ROI was changed. Fix errors with updating limits when plotting empty slice of data. Improve docstrings and test coverage. (`#2952 `_) +- Fix stacking signals that contain their variance in metadata. Previously it was raising an error when specifying the stacking axis. (`#2954 `_) +- Fix missing API documentation of several signal classes. (`#2957 `_) +- Fix two bugs in :py:meth:`~.api.signals.BaseSignal.decomposition`: + + * The poisson noise normalization was not applied when giving a `signal_mask` + * An error was raised when applying a ``signal_mask`` on a signal with signal dimension larger than 1. (`#2964 `_) + + +Improved Documentation +---------------------- + +- Fix and complete docstrings of :py:meth:`~.api.signals.Signal2D.align2D` and :py:meth:`~.api.signals.Signal2D.estimate_shift2D`. (`#2961 `_) + + +Maintenance +----------- + +- Minor refactor of the EELS subshells in the ``elements`` dictionary. (`#2868 `_) +- Fix packaging of test suite and tweak tests to pass on different platform of blas implementation (`#2933 `_) + + +.. _changes_1.7.0: + +1.7.0 (2022-04-26) +=================== + +New features +------------ + +- Add ``filter_zero_loss_peak`` argument to the ``hyperspy._signals.eels.EELSSpectrum.spikes_removal_tool`` method (`#1412 `_) +- Add :py:meth:`~.api.signals.Signal2D.calibrate` method to :py:class:`~.api.signals.Signal2D` signal, which allows for interactive calibration (`#1791 `_) +- Add ``hyperspy._signals.eels.EELSSpectrum.vacuum_mask`` method to: ``hyperspy._signals.eels.EELSSpectrum`` signal (`#2183 `_) +- Support for :ref:`relative slicing ` (`#2386 `_) +- Implement non-uniform axes, not all hyperspy functionalities support non-uniform axes, see this `tracking issue `_ for progress. (`#2399 `_) +- Add (weighted) :ref:`linear least square fitting `. Close `#488 `_ and `#574 `_. (`#2422 `_) +- Support for reading :external+rsciio:ref:`JEOL EDS data` (`#2488 `_) +- Plot overlayed images - see :ref:`plotting several images` (`#2599 `_) +- Add initial support for :ref:`GPU computation` using cupy (`#2670 `_) +- Add ``height`` property to the :py:class:`~._components.gaussian2d.Gaussian2D` component (`#2688 `_) +- Support for reading and writing :external+rsciio:ref:`TVIPS image stream data` (`#2780 `_) +- Add in :external+rsciio:ref:`zspy format`: hspy specification with the zarr format. Particularly useful to speed up loading and :ref:`saving large datasets` by using concurrency. (`#2825 `_) +- Support for reading :external+rsciio:ref:`DENSsolutions Impulse data` (`#2828 `_) +- Add lazy loading for :external+rsciio:ref:`JEOL EDS data` (`#2846 `_) +- Add :ref:`html representation` for lazy signals and the + :py:meth:`~._signals.lazy.LazySignal.get_chunk_size` method to get the chunk size + of given axes (`#2855 `_) +- Add support for Hamamatsu HPD-TA Streak Camera tiff files, + with axes and metadata parsing. (`#2908 `_) + + +Bug Fixes +--------- + +- Signals with 1 value in the signal dimension will now be :py:class:`~.api.signals.BaseSignal` (`#2773 `_) +- :py:func:`exspy.material.density_of_mixture` now throws a Value error when the density of an element is unknown (`#2775 `_) +- Improve error message when performing Cliff-Lorimer quantification with a single line intensity (`#2822 `_) +- Fix bug for the hydrogenic gdos k edge (`#2859 `_) +- Fix bug in axes.UnitConversion: the offset value was initialized by units. (`#2864 `_) +- Fix bug where the :py:meth:`~.api.signals.BaseSignal.map` function wasn't operating properly when an iterating signal was larger than the input signal. (`#2878 `_) +- In case the Bruker defined XML element node at SpectrumRegion contains no information on the + specific selected X-ray line (if there is only single line available), suppose it is 'Ka' line. (`#2881 `_) +- When loading Bruker Bcf, ``cutoff_at_kV=None`` does no cutoff (`#2898 `_) +- Fix bug where the :py:meth:`~.api.signals.BaseSignal.map` function wasn't operating properly when an iterating signal was not an array. (`#2903 `_) +- Fix bug for not saving ragged arrays with dimensions larger than 2 in the ragged dimension. (`#2906 `_) +- Fix bug with importing some spectra from eelsdb and add progress bar (`#2916 `_) +- Fix bug when the spikes_removal_tool would not work interactively for signal with 0-dimension navigation space. (`#2918 `_) + + +Deprecations +------------ + +- Deprecate ``hyperspy.axes.AxesManager.set_signal_dimension`` in favour of using :py:meth:`~.api.signals.BaseSignal.as_signal1D`, :py:meth:`~.api.signals.BaseSignal.as_signal2D` or :py:meth:`~.api.signals.BaseSignal.transpose` of the signal instance instead. (`#2830 `_) + + +Enhancements +------------ + +- :ref:`Region of Interest (ROI)` can now be created without specifying values (`#2341 `_) +- mpfit cleanup (`#2494 `_) +- Document reading Attolight data with the sur/pro format reader (`#2559 `_) +- Lazy signals now caches the current data chunk when using multifit and when plotting, improving performance. (`#2568 `_) +- Read cathodoluminescence metadata from digital micrograph files, amended in `PR #2894 `_ (`#2590 `_) +- Add possibility to search/access nested items in DictionaryTreeBrowser (metadata) without providing full path to item. (`#2633 `_) +- Improve :py:meth:`~.api.signals.BaseSignal.map` function in :py:class:`~.api.signals.BaseSignal` by utilizing dask for both lazy and non-lazy signals. This includes adding a `lazy_output` parameter, meaning non-lazy signals now can output lazy results. See the :ref:`user guide` for more information. (`#2703 `_) +- :external+rsciio:ref:`NeXus` file with more options when reading and writing (`#2725 `_) +- Add ``dtype`` argument to :py:meth:`~.api.signals.BaseSignal.rebin` (`#2764 `_) +- Add option to set output size when :external+rsciio:ref:`exporting images` (`#2791 `_) +- Add :py:meth:`~.axes.AxesManager.switch_iterpath` context manager to switch iterpath (`#2795 `_) +- Add options not to close file (lazy signal only) and not to write dataset for hspy file format, see :external+rsciio:ref:`hspy-format` for details (`#2797 `_) +- Add Github workflow to run test suite of extension from a pull request. (`#2824 `_) +- Add :py:attr:`~.api.signals.BaseSignal.ragged` attribute to :py:class:`~.api.signals.BaseSignal` to clarify when a signal contains a ragged array. Fix inconsistency caused by ragged array and add a :ref:`ragged array` section to the user guide (`#2842 `_) +- Import hyperspy submodules lazily to speed up importing hyperspy. Fix autocompletion `signals` submodule (`#2850 `_) +- Add support for JEOL SightX tiff file (`#2862 `_) +- Add new markers ``hyperspy.drawing._markers.arrow``, ``hyperspy.drawing._markers.ellipse`` and filled ``hyperspy.drawing._markers.rectangle``. (`#2871 `_) +- Add metadata about the file-reading and saving operations to the Signals + produced by :py:func:`~.api.load` and :py:meth:`~.api.signals.BaseSignal.save` + (see the :ref:`metadata structure ` section of the user guide) (`#2873 `_) +- expose Stage coordinates and rotation angle in metada for sem images in bcf reader. (`#2911 `_) + + +API changes +----------- + +- ``metadata.Signal.binned`` is replaced by an axis parameter, e. g. ``axes_manager[-1].is_binned`` (`#2652 `_) +- * when loading Bruker bcf, ``cutoff_at_kV=None`` (default) applies no more automatic cutoff. + * New acceptable values ``"zealous"`` and ``"auto"`` do automatic cutoff. (`#2910 `_) +- Deprecate the ability to directly set ``metadata`` and ``original_metadata`` Signal + attributes in favor of using :py:meth:`~.misc.utils.DictionaryTreeBrowser.set_item` + and :py:meth:`~.misc.utils.DictionaryTreeBrowser.add_dictionary` methods or + specifying metadata when creating signals (`#2913 `_) + + +Maintenance +----------- + +- Fix warning when build doc and formatting user guide (`#2762 `_) +- Drop support for python 3.6 (`#2839 `_) +- Continuous integration fixes and improvements; Bump minimal version requirement of dask to 2.11.0 and matplotlib to 3.1.3 (`#2866 `_) +- Tweak tests tolerance to fix tests failure on aarch64 platform; Add python 3.10 build. (`#2914 `_) +- Add support for matplotlib 3.5, simplify maintenance of ``RangeWidget`` and some signal tools. (`#2922 `_) +- Compress some tiff tests files to reduce package size (`#2926 `_) + + +.. _changes_1.6.5: + +1.6.5 (2021-10-28) +=================== + +Bug Fixes +--------- + +- Suspend plotting during :meth:`exspy.models.EELSModel.smart_fit` call (`#2796 `_) +- make :py:meth:`~.api.signals.BaseSignal.add_marker` also check if the plot is not active before plotting signal (`#2799 `_) +- Fix irresponsive ROI added to a signal plot with a right hand side axis (`#2809 `_) +- Fix :py:func:`~.api.plot.plot_histograms` drawstyle following matplotlib API change (`#2810 `_) +- Fix incorrect :py:meth:`~.api.signals.BaseSignal.map` output size of lazy signal when input and output axes do not match (`#2837 `_) +- Add support for latest h5py release (3.5) (`#2843 `_) + + +Deprecations +------------ + +- Rename ``line_style`` to ``linestyle`` in :py:func:`~.api.plot.plot_spectra` to match matplotlib argument name (`#2810 `_) + + +Enhancements +------------ + +- :py:meth:`~.roi.BaseInteractiveROI.add_widget` can now take a string or integer instead of tuple of string or integer (`#2809 `_) + + +.. _changes_1.6.4: + +1.6.4 (2021-07-08) +=================== Bug Fixes --------- @@ -17,7 +820,7 @@ Bug Fixes - Fix parsing EELS aperture label with unexpected value, for example 'Imaging' instead of '5 mm' (`#2772 `_) - Lazy datasets can now be saved out as blockfiles (blo) (`#2774 `_) - ComplexSignals can now be rebinned without error (`#2789 `_) -- Method `estimate_parameters` in `Polynomial` component now supports order +- Method :py:meth:`~.api.model.components1D.Polynomial.estimate_parameters` of the :py:class:`~._components.polynomial.Polynomial` component now supports order greater than 10 (`#2790 `_) - Update minimal requirement of dependency importlib_metadata from >= 1.6.0 to >= 3.6 (`#2793 `_) @@ -38,17 +841,19 @@ Maintenance - Fix image comparison failure with numpy 1.21.0 (`#2774 `_) -Hyperspy 1.6.3 (2021-06-10) -=========================== +.. _changes_1.6.3: + +1.6.3 (2021-06-10) +=================== Bug Fixes --------- - Fix ROI snapping regression (`#2720 `_) -- Fix :py:meth:`~._signals.signal1d.Signal1D.shift1D`, :py:meth:`~._signals.signal1d.Signal1D.align1D` and :py:meth:`~._signals.eels.EELSSpectrum.align_zero_loss_peak` regression with navigation dimension larger than one (`#2729 `_) -- Fix disconnecting events when closing figure and :py:meth:`~._signals.signal1d.Signal1D.remove_background` is active (`#2734 `_) -- Fix :py:meth:`~.signal.BaseSignal.map` regression of lazy signal with navigation chunks of size of 1 (`#2748 `_) -- Fix unclear error message when reading a hspy file saved using blosc compression and `hdf5plugin` hasn't been imported previously (`#2760 `_) +- Fix :py:meth:`~.api.signals.Signal1D.shift1D`, :py:meth:`~.api.signals.Signal1D.align1D` and ``hyperspy._signals.eels.EELSSpectrum.align_zero_loss_peak`` regression with navigation dimension larger than one (`#2729 `_) +- Fix disconnecting events when closing figure and :py:meth:`~.api.signals.Signal1D.remove_background` is active (`#2734 `_) +- Fix :py:meth:`~.api.signals.BaseSignal.map` regression of lazy signal with navigation chunks of size of 1 (`#2748 `_) +- Fix unclear error message when reading a hspy file saved using blosc compression and ``hdf5plugin`` hasn't been imported previously (`#2760 `_) - Fix saving ``navigator`` of lazy signal (`#2763 `_) @@ -60,7 +865,7 @@ Enhancements extensions and use them without restarting the python session (`#2709 `_) - Don't import hyperspy extensions when registering extensions (`#2711 `_) - Improve docstrings of various fitting methods (`#2724 `_) -- Improve speed of :py:meth:`~._signals.signal1d.Signal1D.shift1D` (`#2750 `_) +- Improve speed of :py:meth:`~.api.signals.Signal1D.shift1D` (`#2750 `_) - Add support for recent EMPAD file; scanning size wasn't parsed. (`#2757 `_) @@ -77,8 +882,8 @@ Maintenance .. _changes_1.6.2: -v1.6.2 -====== +1.6.2 (2021-04-13) +=================== This is a maintenance release that adds support for python 3.9 and includes numerous bug fixes and enhancements. @@ -91,19 +896,19 @@ Bug Fixes * Fix disconnect event when closing navigator only plot (fixes `#996 `_), (`#2631 `_) * Fix incorrect chunksize when saving EMD NCEM file and specifying chunks (`#2629 `_) -* Fix :py:meth:`~._signals.signal2d.Signal2D.find_peaks` GUIs call with laplacian/difference of gaussian methods (`#2622 `_ and `#2647 `_) +* Fix :py:meth:`~.api.signals.Signal2D.find_peaks` GUIs call with laplacian/difference of gaussian methods (`#2622 `_ and `#2647 `_) * Fix various bugs with ``CircleWidget`` and ``Line2DWidget`` (`#2625 `_) * Fix setting signal range of model with negative axis scales (`#2656 `_) * Fix and improve mask handling in lazy decomposition; Close `#2605 `_ (`#2657 `_) * Plot scalebar when the axis scales have different sign, fixes `#2557 `_ (`#2657 `_) -* Fix :py:meth:`~._signals.signal1d.Signal1D.align1D` returning zeros shifts (`#2675 `_) +* Fix :py:meth:`~.api.signals.Signal1D.align1D` returning zeros shifts (`#2675 `_) * Fix finding dataset path for EMD NCEM file containing more than one dataset in a group (`#2673 `_) * Fix squeeze function for multiple zero-dimensional entries, improved docstring, added to user guide. (`#2676 `_) * Fix error in Cliff-Lorimer quantification using absorption correction (`#2681 `_) * Fix ``navigation_mask`` bug in decomposition when provided as numpy array (`#2679 `_) * Fix closing image contrast tool and setting vmin/vmax values (`#2684 `_) * Fix range widget with matplotlib 3.4 (`#2684 `_) -* Fix bug in :py:func:`~.interactive.interactive` with function returning `None`. Improve user guide example. (`#2686 `_) +* Fix bug in :py:func:`~.api.interactive` with function returning `None`. Improve user guide example. (`#2686 `_) * Fix broken events when changing signal type `#2683 `_ * Fix setting offset in rebin: the offset was changed in the wrong axis (`#2690 `_) * Fix reading XRF bruker file, close `#2689 `_ (`#2694 `_) @@ -146,8 +951,8 @@ Maintenance .. _changes_1.6.1: -v1.6.1 -====== +1.6.1 (2020-11-28) +=================== This is a maintenance release that adds compatibility with h5py 3.0 and includes numerous bug fixes and enhancements. @@ -158,30 +963,30 @@ for details. .. _changes_1.6: -v1.6 -==== +1.6.0 (2020-08-05) +=================== NEW --- * Support for the following file formats: - * :ref:`sur-format` - * :ref:`elid_format-label` - * :ref:`nexus-format` - * :ref:`usid-format` - * :ref:`empad-format` - * Prismatic EMD format, see :ref:`emd-format` -* :meth:`~._signals.eels.EELSSpectrum.print_edges_near_energy` method + * :external+rsciio:ref:`digitalsurf-format` + * :external+rsciio:ref:`elid-format` + * :external+rsciio:ref:`nexus-format` + * :external+rsciio:ref:`usid-format` + * :external+rsciio:ref:`empad-format` + * Prismatic EMD format, see :external+rsciio:ref:`emd-format` +* ``hyperspy._signals.eels.EELSSpectrum.print_edges_near_energy`` method that, if the `hyperspy-gui-ipywidgets package `_ is installed, includes an - awesome interactive mode. See :ref:`eels_elemental_composition-label`. + awesome interactive mode. See :external+exspy:ref:`eels_elemental_composition-label`. * Model asymmetric line shape components: * :py:class:`~._components.doniach.Doniach` * :py:class:`~._components.split_voigt.SplitVoigt` -* :ref:`EDS absorption correction `. +* :external+exspy:ref:`EDS absorption correction `. * :ref:`Argand diagram for complex signals `. * :ref:`Multiple peak finding algorithms for 2D signals `. * :ref:`cluster_analysis-label`. @@ -189,7 +994,7 @@ NEW Enhancements ------------ -* The :py:meth:`~.signal.BaseSignal.get_histogram` now uses numpy's +* The :py:meth:`~.api.signals.BaseSignal.get_histogram` now uses numpy's `np.histogram_bin_edges() `_ and supports all of its ``bins`` keyword values. @@ -202,27 +1007,27 @@ Enhancements * :py:class:`~._components.arctan.Arctan` * :py:class:`~._components.voigt.Voigt` * :py:class:`~._components.heaviside.HeavisideStep` -* The model fitting :py:meth:`~.model.BaseModel.fit` and - :py:meth:`~.model.BaseModel.multifit` methods have been vastly improved. See +* The model fitting :py:meth:`~hyperspy.model.BaseModel.fit` and + :py:meth:`~hyperspy.model.BaseModel.multifit` methods have been vastly improved. See :ref:`model.fitting` and the API changes section below. * New serpentine iteration path for multi-dimensional fitting. See :ref:`model.multidimensional-label`. -* The :py:func:`~.drawing.utils.plot_spectra` function now listens to +* The :py:func:`~.api.plot.plot_spectra` function now listens to events to update the figure automatically. - See :ref:`this example `. + See :ref:`this example `. * Improve thread-based parallelism. Add ``max_workers`` argument to the - :py:meth:`~.signal.BaseSignal.map` method, such that the user can directly + :py:meth:`~.api.signals.BaseSignal.map` method, such that the user can directly control how many threads they launch. -* Many improvements to the :py:meth:`~.mva.MVA.decomposition` and - :py:meth:`~.mva.MVA.blind_source_separation` methods, including support for +* Many improvements to the :py:meth:`~.api.signals.BaseSignal.decomposition` and + :py:meth:`~.api.signals.BaseSignal.blind_source_separation` methods, including support for scikit-learn like algorithms, better API and much improved documentation. See :ref:`ml-label` and the API changes section below. * Add option to calculate the absolute thickness to the EELS - :meth:`~._signals.eels.EELSSpectrum.estimate_thickness` method. - See :ref:`eels_thickness-label`. + ``hyperspy._signals.eels.EELSSpectrum.estimate_thickness`` method. + See :external+exspy:ref:`eels_thickness-label`. * Vastly improved performance and memory footprint of the - :py:meth:`~._signals.signal2d.Signal2D.estimate_shift2D` method. -* The :py:meth:`~._signals.signal1d.Signal1D.remove_background` method can + :py:meth:`~.api.signals.Signal2D.estimate_shift2D` method. +* The :py:meth:`~.api.signals.Signal1D.remove_background` method can now remove Doniach, exponential, Lorentzian, skew normal, split Voigt and Voigt functions. Furthermore, it can return the background model that includes an estimation of the reduced chi-squared. @@ -233,8 +1038,8 @@ Enhancements ``vmin`` and ``vmax`` keywords now take values like ``vmin="30th"`` to clip the minimum value to the 30th percentile. See :ref:`signal.fft` for an example. -* The :py:meth:`~._signals.signal1d.Signal1D.plot` and - :py:meth:`~._signals.signal2d.Signal2D.plot` methods take a new keyword +* The :py:meth:`~.api.signals.Signal1D.plot` and + :py:meth:`~.api.signals.Signal2D.plot` methods take a new keyword argument ``autoscale``. See :ref:`plot.customize_images` for details. * The contrast editor and the decomposition methods can now operate on complex signals. @@ -245,20 +1050,20 @@ Enhancements API changes ----------- -* The :py:meth:`~._signals.signal2d.Signal2D.plot` keyword argument +* The :py:meth:`~.api.signals.Signal2D.plot` keyword argument ``saturated_pixels`` is deprecated. Please use ``vmin`` and/or ``vmax`` instead. -* The :py:func:`~.io.load` keyword argument ``dataset_name`` has been +* The :py:func:`~.api.load` keyword argument ``dataset_name`` has been renamed to ``dataset_path``. -* The :py:meth:`~.signal.BaseSignal.set_signal_type` method no longer takes +* The :py:meth:`~.api.signals.BaseSignal.set_signal_type` method no longer takes ``None``. Use the empty string ``""`` instead. -* The :py:meth:`~.signal.BaseSignal.get_histogram` ``bins`` keyword values +* The :py:meth:`~.api.signals.BaseSignal.get_histogram` ``bins`` keyword values have been renamed as follows for consistency with numpy: - * ``"scotts"`` -> ``"scott"``, - * ``"freedman"`` -> ``"fd"`` -* Multiple changes to the syntax of the :py:meth:`~.model.BaseModel.fit` - and :py:meth:`~.model.BaseModel.multifit` methods: + * ``"scotts"`` -> ``"scott"``, + * ``"freedman"`` -> ``"fd"`` +* Multiple changes to the syntax of the :py:meth:`~hyperspy.model.BaseModel.fit` + and :py:meth:`~hyperspy.model.BaseModel.multifit` methods: * The ``fitter`` keyword has been renamed to ``optimizer``. * The values that the ``optimizer`` keyword take have been renamed @@ -290,26 +1095,25 @@ API changes * The ``iterpath`` default will change from ``'flyback'`` to ``'serpentine'`` in HyperSpy version 2.0. -* The following :py:class:`~.model.BaseModel` methods are now private: +* The following :py:class:`~hyperspy.model.BaseModel` methods are now private: - * :py:meth:`~.model.BaseModel.set_boundaries` - * :py:meth:`~.model.BaseModel.set_mpfit_parameters_info` - * :py:meth:`~.model.BaseModel.set_boundaries` + * ``hyperspy.model.BaseModel.set_boundaries`` + * ``hyperspy.model.BaseModel.set_mpfit_parameters_info`` * The ``comp_label`` keyword of the machine learning plotting functions has been renamed to ``title``. -* The :py:class:`~.learn.rpca.orpca` constructor's ``learning_rate`` +* The :py:class:`~hyperspy.learn.rpca.orpca` constructor's ``learning_rate`` keyword has been renamed to ``subspace_learning_rate`` -* The :py:class:`~.learn.rpca.orpca` constructor's ``momentum`` +* The :py:class:`~hyperspy.learn.rpca.orpca` constructor's ``momentum`` keyword has been renamed to ``subspace_momentum`` -* The :py:class:`~.learn.svd_pca.svd_pca` constructor's ``centre`` keyword +* The :py:class:`~hyperspy.learn.svd_pca.svd_pca` constructor's ``centre`` keyword values have been renamed as follows: - * ``"trials"`` -> ``"navigation"`` - * ``"variables"`` -> ``"signal"`` + * ``"trials"`` -> ``"navigation"`` + * ``"variables"`` -> ``"signal"`` * The ``bounds`` keyword argument of the - :py:meth:`~._signals.lazy.decomposition` is deprecated and will be removed. -* Several syntax changes in the :py:meth:`~.learn.mva.decomposition` method: + :py:meth:`~._signals.lazy.LazySignal.decomposition` is deprecated and will be removed. +* Several syntax changes in the :py:meth:`~.api.signals.BaseSignal.decomposition` method: * Several ``algorithm`` keyword values have been renamed as follows: @@ -326,8 +1130,8 @@ API changes .. _changes_1.5.2: -v1.5.2 -====== +1.5.2 (2019-09-06) +=================== This is a maintenance release that adds compatibility with Numpy 1.17 and Dask 2.3.0 and fixes a bug in the Bruker reader. See `the issue tracker @@ -337,8 +1141,8 @@ for details. .. _changes_1.5.1: -v1.5.1 -====== +1.5.1 (2019-07-28) +=================== This is a maintenance release that fixes some regressions introduced in v1.5. Follow the following links for details on all the `bugs fixed @@ -347,8 +1151,8 @@ Follow the following links for details on all the `bugs fixed .. _changes_1.5: -v1.5 -==== +1.5.0 (2019-07-27) +=================== NEW --- @@ -356,8 +1160,8 @@ NEW * New method :py:meth:`hyperspy.component.Component.print_current_values`. See :ref:`the User Guide for details `. * New :py:class:`hyperspy._components.skew_normal.SkewNormal` component. -* New :py:meth:`hyperspy.signal.BaseSignal.apply_apodization` method and - ``apodization`` keyword for :py:meth:`hyperspy.signal.BaseSignal.fft`. See +* New :py:meth:`hyperspy.api.signals.BaseSignal.apply_apodization` method and + ``apodization`` keyword for :py:meth:`hyperspy.api.signals.BaseSignal.fft`. See :ref:`signal.fft` for details. * Estimation of number of significant components by the elbow method. See :ref:`mva.scree_plot`. @@ -366,14 +1170,14 @@ Enhancements ------------ * The contrast adjustment tool has been hugely improved. Test it by pressing the ``h`` key on any image. -* The :ref:`Developer Guide ` has been extended, enhanced and divided into +* The :ref:`Developer Guide ` has been extended, enhanced and divided into chapters. * Signals with signal dimension equal to 0 and navigation dimension 1 or 2 are automatically transposed when using - :py:func:`hyperspy.drawing.utils.plot_images` - or :py:func:`hyperspy.drawing.utils.plot_spectra` respectively. This is + :py:func:`hyperspy.api.plot.plot_images` + or :py:func:`hyperspy.api.plot.plot_spectra` respectively. This is specially relevant when plotting the result of EDS quantification. See - :ref:`eds-label` for examples. + :external+exspy:ref:`eds-label` for examples. * The following components have been rewritten using :py:class:`hyperspy._components.expression.Expression`, boosting their speeds among other benefits. Multiple issues have been fixed on the way. @@ -385,14 +1189,14 @@ Enhancements * :py:class:`hyperspy._components.logistic.Logistic` * :py:class:`hyperspy._components.error_function.Erf` * :py:class:`hyperspy._components.gaussian2d.Gaussian2D` - * :py:class:`hyperspy._components.volume_plasmon_drude.VolumePlasmonDrude` - * :py:class:`hyperspy._components.eels_double_power_law.DoublePowerLaw` - * The :py:class:`hyperspy._components.polynomial_deprecated.Polynomial` + * :py:class:`exspy.components.VolumePlasmonDrude` + * :py:class:`exspy.components.DoublePowerLaw` + * The ``hyperspy._components.polynomial_deprecated.Polynomial`` component will be deprecated in HyperSpy 2.0 in favour of the new :py:class:`hyperspy._components.polynomial.Polynomial` component, that is based on :py:class:`hyperspy._components.expression.Expression` and has an improved API. To start using the new component pass the ``legacy=False`` keyword to the - the :py:class:`hyperspy._components.polynomial_deprecated.Polynomial` component + the ``hyperspy._components.polynomial_deprecated.Polynomial`` component constructor. @@ -405,8 +1209,8 @@ For developers .. _changes_1.4.2: -v1.4.2 -====== +1.4.2 (2019-06-19) +=================== This is a maintenance release. Among many other fixes and enhancements, this release fixes compatibility issues with Matplotlib v 3.1. Follow the @@ -418,8 +1222,8 @@ and `enhancements .. _changes_1.4.1: -v1.4.1 -====== +1.4.1 (2018-10-23) +=================== This is a maintenance release. Follow the following links for details on all the `bugs fixed @@ -432,8 +1236,8 @@ This release fixes compatibility issues with Python 3.7. .. _changes_1.4: -v1.4 -==== +1.4.0 (2018-09-02) +=================== This is a minor release. Follow the following links for details on all the `bugs fixed @@ -448,14 +1252,14 @@ NEW * Support for three new file formats: - * Reading FEI's Velox EMD file format based on the HDF5 open standard. See :ref:`emd_fei-format`. - * Reading Bruker's SPX format. See :ref:`spx-format`. - * Reading and writing the mrcz open format. See :ref:`mrcz-format`. -* New :mod:`~.datasets.artificial_data` module which contains functions for generating + * Reading FEI's Velox EMD file format based on the HDF5 open standard. See :external+rsciio:ref:`emd_fei-format`. + * Reading Bruker's SPX format. See :external+rsciio:ref:`bruker-format`. + * Reading and writing the mrcz open format. See :external+rsciio:ref:`mrcz-format`. +* New ``hyperspy.datasets.artificial_data`` module which contains functions for generating artificial data, for use in things like docstrings or for people to test HyperSpy functionalities. See :ref:`example-data-label`. -* New :meth:`~.signal.BaseSignal.fft` and :meth:`~.signal.BaseSignal.ifft` signal methods. See :ref:`signal.fft`. -* New :meth:`~._signals.hologram_image.HologramImage.statistics` method to compute useful hologram parameters. See :ref:`holography.stats-label`. +* New :meth:`~.api.signals.BaseSignal.fft` and :meth:`~.api.signals.BaseSignal.ifft` signal methods. See :ref:`signal.fft`. +* New :meth:`holospy.signals.HologramImage.statistics` method to compute useful hologram parameters. See :external+holospy:ref:`holography.stats-label`. * Automatic axes units conversion and better units handling using `pint `__. See :ref:`quantity_and_converting_units`. * New :class:`~.roi.Line2DROI` :meth:`~.roi.Line2DROI.angle` method. See :ref:`roi-label` for details. @@ -463,30 +1267,30 @@ NEW Enhancements ------------ -* :py:func:`~.drawing.utils.plot_images` improvements (see :ref:`plot.images` for details): +* :py:func:`~.api.plot.plot_images` improvements (see :ref:`plot.images` for details): - * The ``cmap`` option of :py:func:`~.drawing.utils.plot_images` - supports iterable types, allowing the user to specify different colormaps - for the different images that are plotted by providing a list or other - generator. - * Clicking on an individual image updates it. + * The ``cmap`` option of :py:func:`~.api.plot.plot_images` + supports iterable types, allowing the user to specify different colormaps + for the different images that are plotted by providing a list or other + generator. + * Clicking on an individual image updates it. * New customizable keyboard shortcuts to navigate multi-dimensional datasets. See :ref:`visualization-label`. -* The :py:meth:`~._signals.signal1d.Signal1D.remove_background` method now operates much faster +* The :py:meth:`~.api.signals.Signal1D.remove_background` method now operates much faster in multi-dimensional datasets and adds the options to interatively plot the remainder of the operation and to set the removed background to zero. See :ref:`signal1D.remove_background` for details. -* The :py:meth:`~._signals.Signal2D.plot` method now takes a ``norm`` keyword that can be "linear", "log", +* The :py:meth:`~.api.signals.Signal2D.plot` method now takes a ``norm`` keyword that can be "linear", "log", "auto" or a matplotlib norm. See :ref:`plot.customize_images` for details. Moreover, there are three new extra keyword arguments, ``fft_shift`` and ``power_spectrum``, that are useful when plotting fourier transforms. See :ref:`signal.fft`. -* The :py:meth:`~._signals.signal2d.Signal2D.align2D` and :py:meth:`~._signals.signal2d.Signal2D.estimate_shift2D` +* The :py:meth:`~.api.signals.Signal2D.align2D` and :py:meth:`~.api.signals.Signal2D.estimate_shift2D` can operate with sub-pixel accuracy using skimage's upsampled matrix-multiplication DFT. See :ref:`signal2D.align`. .. _changes_1.3.2: -v1.3.2 -====== +1.3.2 (2018-07-03) +=================== This is a maintenance release. Follow the following links for details on all the `bugs fixed @@ -496,8 +1300,8 @@ and `enhancements `__ is installed. * :py:attr:`~.axes.AxesManager.signal_extent` and :py:attr:`~.axes.AxesManager.navigation_extent` properties to easily get the extent of each space. * New IPywidgets Graphical User Interface (GUI) elements for the `Jupyter Notebook `__. See the new `hyperspy_gui_ipywidgets `__ package. It is not installed by default, see :ref:`install-label` for details. -* All the :ref:`roi-label` now have a :meth:`gui` method to display a GUI if +* All the :ref:`roi-label` now have a ``gui`` method to display a GUI if at least one of HyperSpy's GUI packgages are installed. Enhancements ------------ * Creating many markers is now much faster. * New "Stage" metadata node. See :ref:`metadata_structure` for details. -* The Brucker file reader now supports the new version of the format. See :ref:`bcf-format`. +* The Brucker file reader now supports the new version of the format. See :external+rsciio:ref:`bruker-format`. * HyperSpy is now compatible with all matplotlib backends, including the nbagg which is particularly convenient for interactive data analysis in the `Jupyter Notebook `__ in combination with the new `hyperspy_gui_ipywidgets `__ package. See :ref:`importing_hyperspy-label`. * The ``vmin`` and ``vmax`` arguments of the - :py:func:`~.drawing.utils.plot_images` function now accept lists to enable + :py:func:`~.api.plot.plot_images` function now accept lists to enable setting these parameters for each plot individually. -* The :py:meth:`~.signal.MVATools.plot_decomposition_results` and - :py:meth:`~.signal.MVATools.plot_bss_results` methods now makes a better +* The :py:meth:`~.api.signals.BaseSignal.plot_decomposition_results` and + :py:meth:`~.api.signals.BaseSignal.plot_bss_results` methods now makes a better guess of the number of navigators (if any) required to visualise the components. (Previously they were always plotting four figures by default.) * All functions that take a signal range can now take a :py:class:`~.roi.SpanROI`. * The following ROIs can now be used for indexing or slicing (see :ref:`here ` for details): - * :py:class:`~.roi.Point1DROI` - * :py:class:`~.roi.Point2DROI` - * :py:class:`~.roi.SpanROI` - * :py:class:`~.roi.RectangularROI` + * :py:class:`~.api.roi.Point1DROI` + * :py:class:`~.api.roi.Point2DROI` + * :py:class:`~.api.roi.SpanROI` + * :py:class:`~.api.roi.RectangularROI` API changes @@ -572,38 +1376,38 @@ API changes performing a decomposition. (Previously they would plot as many figures as available components, usually resulting in memory saturation): - * :py:meth:`~.signal.MVATools.plot_decomposition_results`. - * :py:meth:`~.signal.MVATools.plot_decomposition_factors`. + * :py:meth:`~.api.signals.BaseSignal.plot_decomposition_results`. + * :py:meth:`~.api.signals.BaseSignal.plot_decomposition_factors`. * The default extension when saving to HDF5 following HyperSpy's specification - is now ``hspy`` instead of ``hdf5``. See :ref:`hspy-format`. + is now ``hspy`` instead of ``hdf5``. See :external+rsciio:ref:`hspy-format`. * The following methods are deprecated and will be removed in HyperSpy 2.0 - * :py:meth:`~.axes.AxesManager.show`. Use :py:meth:`~.axes.AxesManager.gui` - instead. - * All :meth:`notebook_interaction` method. Use the equivalent :meth:`gui` method - instead. - * :py:meth:`~._signals.signal1d.Signal1D.integrate_in_range`. - Use :py:meth:`~._signals.signal1d.Signal1D.integrate1D` instead. + * ``.axes.AxesManager.show``. Use :py:meth:`~.axes.AxesManager.gui` + instead. + * All ``notebook_interaction`` method. Use the equivalent ``gui`` method + instead. + * ``hyperspy.api.signals.Signal1D.integrate_in_range``. + Use :py:meth:`~.api.signals.BaseSignal.integrate1D` instead. * The following items have been removed from :ref:`preferences `: - * ``General.default_export_format`` - * ``General.lazy`` - * ``Model.default_fitter`` - * ``Machine_learning.multiple_files`` - * ``Machine_learning.same_window`` - * ``Plot.default_style_to_compare_spectra`` - * ``Plot.plot_on_load`` - * ``Plot.pylab_inline`` - * ``EELS.fine_structure_width`` - * ``EELS.fine_structure_active`` - * ``EELS.fine_structure_smoothing`` - * ``EELS.synchronize_cl_with_ll`` - * ``EELS.preedge_safe_window_width`` - * ``EELS.min_distance_between_edges_for_fine_structure`` + * ``General.default_export_format`` + * ``General.lazy`` + * ``Model.default_fitter`` + * ``Machine_learning.multiple_files`` + * ``Machine_learning.same_window`` + * ``Plot.default_style_to_compare_spectra`` + * ``Plot.plot_on_load`` + * ``Plot.pylab_inline`` + * ``EELS.fine_structure_width`` + * ``EELS.fine_structure_active`` + * ``EELS.fine_structure_smoothing`` + * ``EELS.synchronize_cl_with_ll`` + * ``EELS.preedge_safe_window_width`` + * ``EELS.min_distance_between_edges_for_fine_structure`` * New ``Preferences.GUIs`` section to enable/disable the installed GUI toolkits. @@ -613,15 +1417,15 @@ For developers been splitted into a separate package. See the new `hyperspy_gui_traitsui `__ package. -* The new :py:mod:`~.ui_registry` enables easy connection of external +* The new ``hyperspy.ui_registry`` enables easy connection of external GUI elements to HyperSpy. This is the mechanism used to split the traitsui and ipywidgets GUI elements. .. _changes_1.2: -v1.2 -==== +1.2.0 (2017-02-02) +=================== This is a minor release. Follow the following links for details on all the `bugs fixed @@ -635,13 +1439,13 @@ NEW --- * Lazy loading and evaluation. See :ref:`big-data-label`. -* Parallel :py:meth:`~.signal.BaseSignal.map` and all the functions that use +* Parallel :py:meth:`~.api.signals.BaseSignal.map` and all the functions that use it internally (a good fraction of HyperSpy's functionaly). See :ref:`map-label`. -* :ref:`electron-holography-label` reconstruction. -* Support for reading :ref:`edax-format` files. -* New signal methods :py:meth:`~.signal.BaseSignal.indexmin` and - :py:meth:`~.signal.BaseSignal.valuemin`. +* :external+holospy:ref:`electron-holography-label` reconstruction. +* Support for reading :external+rsciio:ref:`edax-format` files. +* New signal methods :py:meth:`~.api.signals.BaseSignal.indexmin` and + :py:meth:`~.api.signals.BaseSignal.valuemin`. Enhancements ------------ @@ -654,13 +1458,13 @@ Enhancements * Better support for EMD files. * The scree plot got a beauty treatment and some extra features. See :ref:`mva.scree_plot`. -* :py:meth:`~.signal.BaseSignal.map` can now take functions that return +* :py:meth:`~.api.signals.BaseSignal.map` can now take functions that return differently-shaped arrays or arbitrary objects, see :ref:`map-label`. * Add support for stacking multi-signal files. See :ref:`load-multiple-label`. * Markers can now be saved to hdf5 and creating many markers is easier and faster. See :ref:`plot.markers`. * Add option to save to HDF5 file using the ".hspy" extension instead of - ".hdf5". See :ref:`hspy-format`. This will be the default extension in + ".hdf5". See :external+rsciio:ref:`hspy-format`. This will be the default extension in HyperSpy 1.3. For developers @@ -672,8 +1476,8 @@ For developers .. _changes_1.1.2: -v1.1.2 -====== +1.1.2 (2079-01-12) +=================== This is a maintenance release. Follow the following links for details on all the `bugs fixed @@ -683,8 +1487,8 @@ and `enhancements = 0.17). -* :py:meth:`~.model.BaseModel.fit` algorithm names changed to be consistent +* :py:meth:`~hyperspy.model.BaseModel.fit` algorithm names changed to be consistent `scipy.optimze.minimize()` notation. -v1.0.1 -====== +1.0.1 (2016-07-27) +=================== This is a maintenance release. Follow the following links for details on all the `bugs fixed `__. -v1.0 -==== +1.0.0 (2016-07-14) +=================== This is a major release. Here we only list the highlist. A detailed list of changes `is available in github @@ -754,7 +1558,7 @@ NEW * Numpy ufuncs can now :ref:`operate on HyperSpy's signals `. * ComplexSignal and specialised subclasses to :ref:`operate on complex data `. * Events :ref:`logging `. -* Query and :ref:`fetch spectra ` from `The EELS Database `__. +* Query and fetch spectr from :func:`exspy.data.eelsdb` from `The EELS Database `__. * :ref:`interactive-label`. * :ref:`events-label`. @@ -769,25 +1573,25 @@ Model EDS ^^^ -* :ref:`Z-factors quantification `. -* :ref:`Cross section quantification `. -* :ref:`EDS curve fitting `. -* X-ray :ref:`absorption coefficient database `. +* :external+exspy:ref:`Z-factors quantification `. +* :external+exspy:ref:`Cross section quantification `. +* :external+exspy:ref:`EDS curve fitting `. +* X-ray :external+exspy:ref:`absorption coefficient database `. IO ^^ * Support for reading certain files without :ref:`loading them to memory `. -* :ref:`Bruker's composite file (bcf) ` reading support. -* :ref:`Electron Microscopy Datasets (EMD) ` read and write support. -* :ref:`SEMPER unf ` read and write support. -* :ref:`DENS heat log ` read support. -* :ref:`NanoMegas blockfile ` read and write support. +* :external+rsciio:ref:`Bruker's composite file (bcf) ` reading support. +* :external+rsciio:ref:`Electron Microscopy Datasets (EMD) ` read and write support. +* :external+rsciio:ref:`SEMPER unf ` read and write support. +* :external+rsciio:ref:`DENS heat log ` read support. +* :external+rsciio:ref:`NanoMegas blockfile ` read and write support. Enhancements ------------ * More useful ``AxesManager`` repr string with html repr for Jupyter Notebook. * Better progress bar (`tqdm `__). -* Add support for :ref:`writing/reading scale and unit to tif files +* Add support for :external+rsciio:ref:`writing/reading scale and unit to tif files ` to be read with ImageJ or DigitalMicrograph. Documentation @@ -797,24 +1601,24 @@ Documentation * :ref:`install-label`. * :ref:`ml-label`. - * :ref:`eds-label`. -* New :ref:`dev_guide-label`. + * :external+exspy:ref:`eds-label`. +* New :ref:`dev_guide`. API changes ----------- -* Split :ref:`components ` into `components1D` and `components2D`. -* Remove `record_by` from metadata. +* Split :ref:`components ` into ``components1D`` and ``components2D``. +* Remove ``record_by`` from metadata. * Remove simulation classes. -* The :py:class:`~._signals.signal1D.Signal1D`, - :py:class:`~._signals.image.Signal2D` and :py:class:`~.signal.BaseSignal` +* The :py:class:`~.api.signals.Signal1D`, + ``hyperspy._signals.image.Signal2D`` and :py:class:`~.api.signals.BaseSignal` classes deprecated the old `Spectrum` `Image` and `Signal` classes. -v0.8.5 -====== +0.8.5 (2016-07-02) +=================== This is a maintenance release. Follow the following links for details on all @@ -838,24 +1642,24 @@ New feature API changes ----------- -The new :py:class:`~.signal.BaseSignal`, -:py:class:`~._signals.signal1d.Signal1D` and -:py:class:`~._signals.signal2d.Signal2D` deprecate :py:class:`~.signal.Signal`, -:py:class:`~._signals.signal1D.Signal1D` and :py:class:`~._signals.image.Signal2D` -respectively. Also `as_signal1D`, `as_signal2D`, `to_signal1D` and `to_signal2D` -deprecate `as_signal1D`, `as_signal2D`, `to_spectrum` and `to_image`. See `#963 +The new :py:class:`~.api.signals.BaseSignal`, +:py:class:`~.api.signals.Signal1D` and +:py:class:`~.api.signals.Signal2D` deprecate ``hyperspy.signal.Signal``, +:py:class:`~.api.signals.Signal1D` and :py:class:`~.api.signals.Signal2D` +respectively. Also ``as_signal1D``, ``as_signal2D```, ``to_signal1D`` and ``to_signal2D`` +deprecate ``as_signal1D``, ``as_signal2D``, ``to_spectrum`` and ``to_image``. See `#963 `__ and `#943 `__ for details. -v0.8.4 -====== +0.8.4 (2016-03-04) +=================== This release adds support for Python 3 and drops support for Python 2. In all -other respects it is identical to v0.8.3. +other respects it is identical to 0.8.3. -v0.8.3 -====== +0.8.3 (2016-03-04) +=================== This is a maintenance release that includes fixes for multiple bugs, some enhancements, new features and API changes. This is set to be the last HyperSpy @@ -881,15 +1685,15 @@ Follow the following links for details on all the `bugs fixed .. _changes_0.8.2: -v0.8.2 -====== +0.8.2 (2015-08-13) +=================== -This is a maintenance release that fixes an issue with the Python installers. Those who have successfully installed v0.8.1 do not need to upgrade. +This is a maintenance release that fixes an issue with the Python installers. Those who have successfully installed 0.8.1 do not need to upgrade. .. _changes_0.8.1: -v0.8.1 -====== +0.8.1 (2015-08-12) +=================== This is a maintenance release. Follow the following links for details on all the `bugs fixed @@ -910,41 +1714,41 @@ New features * ``%hyperspy`` IPython magic to easily and transparently import HyperSpy, matplotlib and numpy when using IPython. * :py:class:`~._components.expression.Expression` model component to easily create analytical function components. More details :ref:`here `. -* :py:meth:`~.signal.Signal.unfolded` context manager. -* :py:meth:`~.signal.Signal.derivative` method. +* ``hyperspy.signal.Signal.unfolded`` context manager. +* ``hyperspy.signal.Signal.derivative`` method. * :ref:`syntax to access the components in the model ` that includes pretty printing of the components. API changes ----------- -* :py:mod:`~.hyperspy.hspy` is now deprecated in favour of the new - :py:mod:`~.hyperspy.api`. The new API renames and/or move several modules as +* ``hyperspy.hspy`` is now deprecated in favour of the new + :py:mod:`hyperspy.api`. The new API renames and/or move several modules as folows: - * ``hspy.components`` -> ``api.model.components`` - * ``hspy.utils``-> ``api`` - * ``hspy.utils.markers`` ``api.plot.markers`` - * ``hspy.utils.example_signals`` -> ``api.datasets.example_signals`` + * ``hspy.components`` -> ``hyperspy.api.model.components`` + * ``hspy.utils``-> ``hyperspy.api`` + * ``hspy.utils.markers`` ``hyperspy.api.plot.markers`` + * ``hspy.utils.example_signals`` -> ``hyperspy.api.datasets.example_signals`` - In HyperSpy 0.8.1 the full content of :py:mod:`~.hyperspy.hspy` is still + In HyperSpy 0.8.1 the full content of ``hyperspy.hspy`` is still imported in the user namespace, but this can now be disabled in ``hs.preferences.General.import_hspy``. In Hyperspy 1.0 it will be - disabled by default and the :py:mod:`~.hyperspy.hspy` module will be fully + disabled by default and the ``hyperspy.hspy`` module will be fully removed in HyperSpy 0.10. We encourage all users to migrate to the new syntax. For more details see :ref:`importing_hyperspy-label`. -* Indexing the :py:class:`~.signal.Signal` class is now deprecated. We encourage +* Indexing the ``hyperspy.signal.Signal`` class is now deprecated. We encourage all users to use ``isig`` and ``inav`` instead for indexing. -* :py:func:`~.hyperspy.hspy.create_model` is now deprecated in favour of the new - equivalent :py:meth:`~.signal.Signal.create_model` ``Signal`` method. -* :py:meth:`~.signal.Signal.unfold_if_multidim` is deprecated. +* ``hyperspy.hspy.create_model`` is now deprecated in favour of the new + equivalent ``hyperspy.signal.Signal.create_model`` ``Signal`` method. +* ``hyperspy.signal.Signal.unfold_if_multidim`` is deprecated. .. _changes_0.8: -v0.8 -==== +0.8.0 (2015-04-07) +=================== New features ------------ @@ -952,7 +1756,7 @@ New features Core ^^^^ -* :py:meth:`~._signals.signal1D.Signal1D.spikes_removal_tool` displays derivative max value when used with +* :py:meth:`~.api.signals.Signal1D.spikes_removal_tool` displays derivative max value when used with GUI. * Progress-bar can now be suppressed by passing ``show_progressbar`` argument to all functions that generate it. @@ -966,30 +1770,30 @@ IO Plotting ^^^^^^^^ -* New class, :py:class:`~.drawing.marker.MarkerBase`, to plot markers with ``hspy.utils.plot.markers`` module. See :ref:`plot.markers`. -* New method to plot images with the :py:func:`~.drawing.utils.plot_images` function in ``hspy.utils.plot.plot_images``. See :ref:`plot.images`. -* Improved :py:meth:`~._signals.image.Signal2D.plot` method to customize the image. See :ref:`plot.customize_images`. +* New class, ``hyperspy.drawing.marker.MarkerBase``, to plot markers with ``hspy.utils.plot.markers`` module. See :ref:`plot.markers`. +* New method to plot images with the :py:func:`~.api.plot.plot_images` function in ``hspy.utils.plot.plot_images``. See :ref:`plot.images`. +* Improved ``hyperspy._signals.image.Signal2D.plot`` method to customize the image. See :ref:`plot.customize_images`. EDS ^^^ -* New method for quantifying EDS TEM spectra using Cliff-Lorimer method, :py:meth:`~._signals.eds_tem.EDSTEMSpectrum.quantification`. See :ref:`eds_quantification-label`. -* New method to estimate for background subtraction, :py:meth:`~._signals.eds.EDSSpectrum.estimate_background_windows`. See :ref:`eds_background_subtraction-label`. -* New method to estimate the windows of integration, :py:meth:`~._signals.eds.EDSSpectrum.estimate_integration_windows`. -* New specific :py:meth:`~._signals.eds.EDSSpectrum.plot` method, with markers to indicate the X-ray lines, the window of integration or/and the windows for background subtraction. See :ref:`eds_plot_markers-label`. +* New method for quantifying EDS TEM spectra using Cliff-Lorimer method, ``hyperspy._signals.eds_tem.EDSTEMSpectrum.quantification``. See :external+exspy:ref:`eds_quantification-label`. +* New method to estimate for background subtraction, ``hyperspy._signals.eds.EDSSpectrum.estimate_background_windows``. See :external+exspy:ref:`eds_background_subtraction-label`. +* New method to estimate the windows of integration, ``hyperspy._signals.eds.EDSSpectrum.estimate_integration_windows``. +* New specific ``hyperspy._signals.eds.EDSSpectrum.plot`` method, with markers to indicate the X-ray lines, the window of integration or/and the windows for background subtraction. See :external+exspy:ref:`eds_plot_markers-label`. * New examples of signal in the ``hspy.utils.example_signals`` module. - + :py:func:`~.misc.example_signals_loading.load_1D_EDS_SEM_spectrum` - + :py:func:`~.misc.example_signals_loading.load_1D_EDS_TEM_spectrum` + + ``hyperspy.misc.example_signals_loading.load_1D_EDS_SEM_spectrum`` + + ``hyperspy.misc.example_signals_loading.load_1D_EDS_TEM_spectrum`` -* New method to mask the vaccum, :py:meth:`~._signals.eds_tem.EDSTEMSpectrum.vacuum_mask` and a specific :py:meth:`~._signals.eds_tem.EDSTEMSpectrum.decomposition` method that incoroporate the vacuum mask +* New method to mask the vaccum, ``hyperspy._signals.eds_tem.EDSTEMSpectrum.vacuum_mask`` and a specific ``hyperspy._signals.eds_tem.EDSTEMSpectrum.decomposition`` method that incoroporate the vacuum mask API changes ----------- -* :py:class:`~.component.Component` and :py:class:`~.component.Parameter` now inherit ``traits.api.HasTraits`` +* :py:class:`~hyperspy.component.Component` and :py:class:`~hyperspy.component.Parameter` now inherit ``traits.api.HasTraits`` that enable ``traitsui`` to modify these objects. -* :py:meth:`~.misc.utils.attrsetter` is added, behaving as the default python :py:meth:`setattr` with nested +* ``hyperspy.misc.utils.attrsetter`` is added, behaving as the default python ``setattr`` with nested attributes. * Several widget functions were made internal and/or renamed: + ``add_patch_to`` -> ``_add_patch_to`` @@ -999,8 +1803,8 @@ API changes + ``update_patch_size`` -> ``_update_patch_size`` + ``add_axes`` -> ``set_mpl_ax`` -v0.7.3 -====== +0.7.3 (2015-08-22) +=================== This is a maintenance release. A list of fixed issues is available in the `0.7.3 milestone @@ -1009,8 +1813,8 @@ in the github repository. .. _changes_0.7.2: -v0.7.2 -====== +0.7.2 (2015-08-22) +=================== This is a maintenance release. A list of fixed issues is available in the `0.7.2 milestone @@ -1019,8 +1823,8 @@ in the github repository. .. _changes_0.7.1: -v0.7.1 -====== +0.7.1 (2015-06-17) +=================== This is a maintenance release. A list of fixed issues is available in the `0.7.1 milestone @@ -1033,8 +1837,8 @@ New features * Add suspend/resume model plot updating. See :ref:`model.visualization`. -v0.7 -==== +0.7.0 (2014-04-03) +=================== New features ------------ @@ -1046,10 +1850,10 @@ Core * New Signal methods to transform between Signal subclasses. More information :ref:`here `. - + :py:meth:`~.signal.Signal.set_signal_type` - + :py:meth:`~.signal.Signal.set_signal_origin` - + :py:meth:`~.signal.Signal.as_signal2D` - + :py:meth:`~.signal.Signal.as_signal1D` + + ``hyperspy.signal.Signal.set_signal_type`` + + ``hyperspy.signal.Signal.set_signal_origin`` + + ``hyperspy.signal.Signal.as_signal2D`` + + ``hyperspy.signal.Signal.as_signal1D`` * The string representation of the Signal class now prints the shape of the data and includes a separator between the navigation and the signal axes e.g @@ -1061,23 +1865,23 @@ Core * Added compatibility witn the the GTK and TK toolkits, although with no GUI features. * It is now possible to run HyperSpy in a headless system. -* Added a CLI to :py:meth:`~.signal.Signal1DTools.remove_background`. -* New :py:meth:`~.signal.Signal1DTools.estimate_peak_width` method to estimate +* Added a CLI to ``hyperspy.signal.Signal1DTools.remove_background``. +* New ``hyperspy.signal.Signal1DTools.estimate_peak_width`` method to estimate peak width. * New methods to integrate over one axis: - :py:meth:`~.signal.Signal.integrate1D` and - :py:meth:`~.signal.Signal1DTools.integrate_in_range`. -* New :attr:`~signal.Signal.metadata` attribute, ``Signal.binned``. Several + ``hyperspy.signal.Signal.integrate1D`` and + ``hyperspy.signal.Signal1DTools.integrate_in_range``. +* New ``hyperspy.signal.Signal.metadata`` attribute, ``Signal.binned``. Several methods behave differently on binned and unbinned signals. See :ref:`signal.binned`. -* New :py:meth:`~.signal.Signal.map` method to easily transform the +* New ``hyperspy.signal.Signal.map`` method to easily transform the data using a function that operates on individual signals. See :ref:`signal.iterator`. -* New :py:meth:`~.signal.Signal.get_histogram` and - :py:meth:`~.signal.Signal.print_summary_statistics` methods. -* The spikes removal tool has been moved to the :class:`~._signal.Signal1D` +* New ``hyperspy.signal.Signal.get_histogram`` and + ``hyperspy.signal.Signal.print_summary_statistics`` methods. +* The spikes removal tool has been moved to the :class:`~.api.signals.Signal1D` class so that it is available for all its subclasses. -* The :py:meth:`~.signal.Signal.split` method now can automatically split back +* The ``hyperspy.signal.Signal.split``` method now can automatically split back stacked signals into its original part. See :ref:`signal.stack_split`. IO @@ -1092,14 +1896,14 @@ Plotting * Use the blitting capabilities of the different toolkits to speed up the plotting of images. -* Added several extra options to the Signal :py:meth:`~.signal.Signal.plot` +* Added several extra options to the Signal ``hyperspy.signal.Signal.plot`` method to customize the navigator. See :ref:`visualization-label`. * Add compatibility with IPython's matplotlib inline plotting. -* New function, :py:func:`~.drawing.utils.plot_spectra`, to plot several +* New function, :py:func:`~.api.plot.plot_spectra`, to plot several spectra in the same figure. See :ref:`plot.spectra`. -* New function, :py:func:`~.drawing.utils.plot_signals`, to plot several +* New function, :py:func:`~.api.plot.plot_signals`, to plot several signals at the same time. See :ref:`plot.signals`. -* New function, :py:func:`~.drawing.utils.plot_histograms`, to plot the histrograms +* New function, :py:func:`~.api.plot.plot_histograms`, to plot the histrograms of several signals at the same time. See :ref:`plot.signals`. Curve fitting @@ -1109,7 +1913,7 @@ Curve fitting computed automatically when fitting. See :ref:`model.fitting`. * New functionality to plot the individual components of a model. See :ref:`model.visualization`. -* New method, :py:meth:`~.model.Model.fit_component`, to help setting the +* New method, ``hyperspy.model.Model.fit_component``, to help setting the starting parameters. See :ref:`model.starting`. Machine learning @@ -1118,49 +1922,49 @@ Machine learning * The PCA scree plot can now be easily obtained as a Signal. See :ref:`mva.scree_plot`. * The decomposition and blind source separation components can now be obtained - as :py:class:`~.signal.Signal` instances. See :ref:`mva.get_results`. + as ``hyperspy.signal.Signal`` instances. See :ref:`mva.get_results`. * New methods to plot the decomposition and blind source separation results that support n-dimensional loadings. See :ref:`mva.visualization`. Dielectric function ^^^^^^^^^^^^^^^^^^^ -* New :class:`~.signal.Signal` subclass, - :class:`~._signals.dielectric_function.DielectricFunction`. +* New ``hyperspy.signal.Signal`` subclass, + ``hyperspy._signals.dielectric_function.DielectricFunction``. EELS ^^^^ * New method, - :meth:`~._signals.eels.EELSSpectrum.kramers_kronig_analysis` to calculate + ``hyperspy._signals.eels.EELSSpectrum.kramers_kronig_analysis`` to calculate the dielectric function from low-loss electron energy-loss spectra based on - the Kramers-Kronig relations. See :ref:`eels.kk`. + the Kramers-Kronig relations. See :external+exspy:ref:`eels.kk`. * New method to align the zero-loss peak, - :meth:`~._signals.eels.EELSSpectrum.align_zero_loss_peak`. + ``hyperspy._signals.eels.EELSSpectrum.align_zero_loss_peak``. EDS ^^^ * New signal, EDSSpectrum especialized in EDS data analysis, with subsignal for EDS with SEM and with TEM: EDSSEMSpectrum and EDSTEMSpectrum. See - :ref:`eds-label`. + :external+exspy:ref:`eds-label`. * New database of EDS lines available in the ``elements`` attribute of the ``hspy.utils.material`` module. * Adapted methods to calibrate the spectrum, the detector and the microscope. - See :ref:`eds_calibration-label`. + See :external+exspy:ref:`eds_calibration-label`. * Specific methods to describe the sample, - :py:meth:`~._signals.eds.EDSSpectrum.add_elements` and - :py:meth:`~._signals.eds.EDSSpectrum.add_lines`. See :ref:`eds_sample-label` + ``hyperspy._signals.eds.EDSSpectrum.add_elements`` and + ``hyperspy._signals.eds.EDSSpectrum.add_lines``. See :external+exspy:ref:`eds_sample-label` * New method to get the intensity of specific X-ray lines: - :py:meth:`~._signals.eds.EDSSpectrum.get_lines_intensity`. See - :ref:`eds_plot-label` + ``hyperspy._signals.eds.EDSSpectrum.get_lines_intensity``. See + :external+exspy:ref:`eds_sample-label` API changes ----------- * hyperspy.misc has been reorganized. Most of the functions in misc.utils has been rellocated to specialized modules. misc.utils is no longer imported in - hyperspy.hspy. A new hyperspy.utils module is imported instead. + ``hyperspy.hspy``. A new ``hyperspy.utils`` module is imported instead. * Objects that have been renamed + ``hspy.elements`` -> ``utils.material.elements``. @@ -1171,13 +1975,13 @@ API changes * The metadata has been reorganized. See :ref:`metadata_structure`. * The following signal methods now operate out-of-place: - + :py:meth:`~.signal.Signal.swap_axes` - + :py:meth:`~.signal.Signal.rebin` + + ``hyperspy.signal.Signal.swap_axes`` + + ``hyperspy.signal.Signal.rebin`` .. _changes_0.6: -v0.6 -==== +0.6.0 (2013-05-25) +=================== New features ------------ @@ -1186,9 +1990,9 @@ New features * Most arithmetic and rich arithmetic operators work with signal. See :ref:`signal.operations`. * Much improved EELSSpectrum methods: - :py:meth:`~._signals.eels.EELSSpectrum.estimate_zero_loss_peak_centre`, - :py:meth:`~._signals.eels.EELSSpectrum.estimate_elastic_scattering_intensity` and - :py:meth:`~._signals.eels.EELSSpectrum.estimate_elastic_scattering_threshold`. + ``hyperspy._signals.eels.EELSSpectrum.estimate_zero_loss_peak_centre``, + ``hyperspy._signals.eels.EELSSpectrum.estimate_elastic_scattering_intensity`` and + ``hyperspy._signals.eels.EELSSpectrum.estimate_elastic_scattering_threshold``. * The axes can now be given using their name e.g. ``s.crop("x", 1,10)`` * New syntax to specify position over axes: an integer specifies the indexes @@ -1201,27 +2005,27 @@ New features * Add padding to fourier-log and fourier-ratio deconvolution to fix the wrap-around problem and increase its performance. * New - :py:meth:`~.components.eels_cl_edge.EELSCLEdge.get_fine_structure_as_spectrum` + ``hyperspy.components.eels_cl_edge.EELSCLEdge.get_fine_structure_as_spectrum`` EELSCLEdge method. -* New :py:class:`~.components.arctan.Arctan` model component. +* New ``hyperspy.components.arctan.Arctan`` model component. * New - :py:meth:`~.model.Model.enable_adjust_position` - and :py:meth:`~.model.Model.disable_adjust_position` + ``hyperspy.model.Model.enable_adjust_position`` + and ``hyperspy.model.Model.disable_adjust_position`` to easily change the position of components using the mouse on the plot. * New Model methods - :py:meth:`~.model.Model.set_parameters_value`, - :py:meth:`~.model.Model.set_parameters_free` and - :py:meth:`~.model.Model.set_parameters_not_free` + ``hyperspy.model.Model.set_parameters_value``, + ``hyperspy.model.Model.set_parameters_free`` and + ``hyperspy.model.Model.set_parameters_not_free`` to easily set several important component attributes of a list of components at once. * New - :py:func:`~.misc.utils.stack` function to stack signals. + :py:func:`~.api.stack` function to stack signals. * New Signal methods: - :py:meth:`~.signal.Signal.integrate_simpson`, - :py:meth:`~.signal.Signal.max`, - :py:meth:`~.signal.Signal.min`, - :py:meth:`~.signal.Signal.var`, and - :py:meth:`~.signal.Signal.std`. + ``hyperspy.signal.Signal.integrate_simpson``, + ``hyperspy.signal.Signal.max``, + ``hyperspy.signal.Signal.min``, + ``hyperspy.signal.Signal.var``, and + ``hyperspy.signal.Signal.std``. * New sliders window to easily navigate signals with navigation_dimension > 2. * The Ripple (rpl) reader can now read rpl files produced by INCA. @@ -1260,8 +2064,8 @@ API changes .. _changes_0.5.1: -v0.5.1 -====== +0.5.1 (2012-09-28) +=================== New features ------------ @@ -1293,8 +2097,8 @@ API changes .. _changes_0.5: -v0.5 -==== +0.5.0 (2012-09-07) +=================== New features ------------ @@ -1355,13 +2159,13 @@ API changes .. _changes_0.4.1: -v0.4.1 -====== +0.4.1 (2012-04-16) +=================== New features ------------ - * Added TIFF 16, 32 and 64 bits support by using (and distributing) Christoph Gohlke's `tifffile library `__. + * Added TIFF 16, 32 and 64 bits support by using (and distributing) Christoph Gohlke's `tifffile library `_. * Improved UTF8 support. * Reduce the number of required libraries by making mdp and hdf5 not mandatory. * Improve the information returned by __repr__ of several objects. @@ -1386,8 +2190,8 @@ Syntax changes .. _changes_0.4: -v0.4 -==== +0.4.0 (2012-02-29) +=================== New features ------------ diff --git a/COPYING.txt b/COPYING.txt index 94a9ed024d..1f3afe18a3 100644 --- a/COPYING.txt +++ b/COPYING.txt @@ -641,11 +641,11 @@ the "copyright" line and a pointer to where the full notice is found. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -664,7 +664,7 @@ might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see -. +. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 14b5297f34..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -include *.txt -include *.rst -recursive-include hyperspy *.pyx -recursive-include bin *.py -recursive-include doc *.txt *.rst -recursive-include examples *.py *.rst *.txt -include hyperspy/misc/etc/test_compilers.c - diff --git a/README.md b/README.md new file mode 100644 index 0000000000..b5ab67e321 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ + +[![Azure pipelines status](https://dev.azure.com/franciscode-la-pena-manchon/hyperspy/_apis/build/status/hyperspy.hyperspy?branchName=RELEASE_next_minor)](https://dev.azure.com/franciscode-la-pena-manchon/hyperspy/_build?definitionId=1&_a=summary&view=branches) +[![Tests status](https://github.com/hyperspy/hyperspy/actions/workflows/tests.yml/badge.svg)](https://github.com/hyperspy/hyperspy/actions/workflows/tests.yml) +[![Documentation status](https://readthedocs.org/projects/hyperspy/badge/?version=latest)](https://hyperspy.org/hyperspy-doc/current) +[![Codecov coverage](https://codecov.io/gh/hyperspy/hyperspy/graph/badge.svg?token=X7T3LE121Q)](https://codecov.io/gh/hyperspy/hyperspy) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/hyperspy/hyperspy/RELEASE_next_minor.svg)](https://results.pre-commit.ci/latest/github/hyperspy/hyperspy/RELEASE_next_minor) + +[![Python Version](https://img.shields.io/pypi/pyversions/hyperspy.svg?style=flat)](https://pypi.python.org/pypi/hyperspy) +[![PyPI Version](http://img.shields.io/pypi/v/hyperspy.svg?style=flat)](https://pypi.python.org/pypi/hyperspy) +[![Anaconda Version](https://anaconda.org/conda-forge/hyperspy/badges/version.svg)](https://anaconda.org/conda-forge/hyperspy) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + +[![gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hyperspy/hyperspy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![DOI](https://zenodo.org/badge/2233998.svg)](https://zenodo.org/badge/latestdoi/2233998) + + +**HyperSpy** is an open source Python library for the interactive analysis of +multidimensional datasets that can be described as multidimensional arrays +of a given signal (for example, a 2D array of spectra, also known as a +spectrum image). + +**HyperSpy** makes it straightforward to apply analytical procedures that +operate on an individual signal to multidimensional arrays, as well as +providing easy access to analytical tools that exploit the multidimensionality +of the dataset. + +Its modular structure makes it easy to add features to analyze many different +types of signals. Visit the [HyperSpy Website](https://hyperspy.org) to learn more about HyperSpy and +its extension packages and for instructions on how to install the package and get +started in analysing your data. + +HyperSpy is released under the GPL v3 license. + +**Since version 0.8.4, HyperSpy only supports Python 3.** If you need to install +HyperSpy in Python 2.7, please install version 0.8.3. + +Contributing +------------ + +Everyone is welcome to contribute. Please read our [contributing +guidelines](https://github.com/hyperspy/hyperspy/blob/RELEASE_next_minor/.github/CONTRIBUTING.md) +and get started! diff --git a/README.rst b/README.rst deleted file mode 100644 index 09277fee3c..0000000000 --- a/README.rst +++ /dev/null @@ -1,64 +0,0 @@ -.. -*- mode: rst -*- - -|Azure|_ |Github|_ |Drone|_ |rtd|_ |Codecov|_ - -|python_version|_ |pypi_version|_ |anaconda_cloud|_ - -|gitter|_ |DOI|_ - -.. |Azure| image:: https://dev.azure.com/franciscode-la-pena-manchon/hyperspy/_apis/build/status/hyperspy.hyperspy?branchName=RELEASE_next_minor -.. _Azure: https://dev.azure.com/franciscode-la-pena-manchon/hyperspy/_build?definitionId=1&_a=summary&view=branches - -.. |Github| image:: https://github.com/hyperspy/hyperspy/workflows/Tests/badge.svg -.. _Github: https://github.com/hyperspy/hyperspy/actions?query=workflow%3ATests - -.. |Drone| image:: https://cloud.drone.io/api/badges/hyperspy/hyperspy/status.svg -.. _Drone: https://cloud.drone.io/hyperspy/hyperspy - -.. |Codecov| image:: https://codecov.io/gh/hyperspy/hyperspy/branch/RELEASE_next_minor/graph/badge.svg -.. _Codecov: https://codecov.io/gh/hyperspy/hyperspy - -.. |rtd| image:: https://readthedocs.org/projects/hyperspy/badge/?version=latest -.. _rtd: https://readthedocs.org/projects/hyperspy/?badge=latest - -.. |pypi_version| image:: http://img.shields.io/pypi/v/hyperspy.svg?style=flat -.. _pypi_version: https://pypi.python.org/pypi/hyperspy - -.. |anaconda_cloud| image:: https://anaconda.org/conda-forge/hyperspy/badges/version.svg -.. _anaconda_cloud: https://anaconda.org/conda-forge/hyperspy - -.. |python_version| image:: https://img.shields.io/pypi/pyversions/hyperspy.svg?style=flat -.. _python_version: https://pypi.python.org/pypi/hyperspy - -.. |gitter| image:: https://badges.gitter.im/Join%20Chat.svg -.. _gitter: https://gitter.im/hyperspy/hyperspy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge - -.. |DOI| image:: https://zenodo.org/badge/2233998.svg -.. _DOI: https://zenodo.org/badge/latestdoi/2233998 - - -HyperSpy is an open source Python library for the interactive analysis of -multidimensional datasets that can be described as multidimensional arrays -of a given signal (for example, a 2D array of spectra, also known as a -spectrum image). - -HyperSpy makes it straightforward to apply analytical procedures that -operate on an individual signal to multidimensional arrays, as well as -providing easy access to analytical tools that exploit the multidimensionality -of the dataset. - -Its modular structure makes it easy to add features to analyze many different -types of signals. - -HyperSpy is released under the GPL v3 license. - -**Since version 0.8.4, HyperSpy only supports Python 3.** If you need to install -HyperSpy in Python 2.7, please install version 0.8.3. - -Contributing ------------- - -Everyone is welcome to contribute. Please read our -`contributing guidelines `_ and get started! - - diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f2d76b4cdb..9569df24f7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,39 +25,37 @@ resources: strategy: matrix: - Linux_Python38: + Linux_Python39: vmImage: 'ubuntu-latest' - PYTHON_VERSION: '3.8' + PYTHON_VERSION: '3.9' MAMBAFORGE_PATH: $(Agent.BuildDirectory)/mambaforge - Linux_Python37: + Linux_Python310: vmImage: 'ubuntu-latest' - PYTHON_VERSION: '3.7' + PYTHON_VERSION: '3.10' MAMBAFORGE_PATH: $(Agent.BuildDirectory)/mambaforge - Linux_Python36: - vmImage: 'ubuntu-latest' - PYTHON_VERSION: '3.6' - MAMBAFORGE_PATH: $(Agent.BuildDirectory)/mambaforge - MacOS_Python38: + MacOS_Python39: vmImage: 'macOS-latest' - PYTHON_VERSION: '3.8' + PYTHON_VERSION: '3.9' MAMBAFORGE_PATH: $(Agent.BuildDirectory)/mambaforge - MacOS_Python37: + MacOS_Python310: vmImage: 'macOS-latest' - PYTHON_VERSION: '3.7' + PYTHON_VERSION: '3.10' MAMBAFORGE_PATH: $(Agent.BuildDirectory)/mambaforge - Windows_Python38: + Windows_Python39: vmImage: 'windows-latest' - PYTHON_VERSION: '3.8' + PYTHON_VERSION: '3.9' MAMBAFORGE_PATH: $(Agent.BuildDirectory)\mambaforge - Windows_Python36: + Windows_Python310: vmImage: 'windows-latest' - PYTHON_VERSION: '3.6' + PYTHON_VERSION: '3.10' MAMBAFORGE_PATH: $(Agent.BuildDirectory)\mambaforge pool: vmImage: '$(vmImage)' steps: +- checkout: self + fetchDepth: '1' # Fetch only one commit - template: azure_pipelines/clone_ci-scripts_repo.yml@templates - template: azure_pipelines/install_mambaforge.yml@templates - template: azure_pipelines/activate_conda.yml@templates @@ -69,6 +67,11 @@ steps: conda list displayName: Install package +- bash: | + source activate $ENV_NAME + conda clean --all -y + displayName: Clean conda cache + # Note we must use `-n 2` argument for pytest-xdist due to # https://github.com/pytest-dev/pytest-xdist/issues/9. - bash: | @@ -80,12 +83,3 @@ steps: artifact: $(Agent.JobName)-result_images displayName: Publish Image Comparison condition: failed() - -- template: azure_pipelines/generate_distribution.yml@templates -- template: azure_pipelines/publish_distribution.yml@templates -# - template: azure_pipelines/update_github_release.yml@templates -# parameters: -# # Set the token generated with github from the developer settings/personal -# # access tokens menu in azure pipeline -# github_token_name: '' - diff --git a/conda_environment.yml b/conda_environment.yml index 0ef9dc73fc..1495a6a26c 100644 --- a/conda_environment.yml +++ b/conda_environment.yml @@ -2,31 +2,26 @@ name: test_env channels: - conda-forge dependencies: -- dask-core >2.0 +- dask-core >=2021.3.1 - dill +- importlib-metadata - h5py -- imageio -- ipyparallel -- matplotlib-base -- matplotlib-scalebar -- mrcz +- jinja2 +- matplotlib-base >=3.1.3 - natsort -- numba +- numba >=0.52 - numexpr -- numpy -- pint -- prettytable -- python-dateutil +- numpy >=1.20 +- packaging +- pint >=0.10 +- prettytable >=2.3 +- python-dateutil >=2.5.0 - pyyaml - requests -- scikit-image -- scikit-learn -- scipy -- sparse -- sympy -- tifffile >=2019.07.26,!=2021.3.31 -- toolz -- tqdm -- traits - - +- rosettasciio-base +- scikit-image >=0.18 +- scikit-learn >=1.0.1 +- scipy >=1.6.0 +- sympy >=1.6.0 +- tqdm >=4.9.0 +- traits >=4.5.0 diff --git a/conda_environment_dev.yml b/conda_environment_dev.yml index cd61f0cb8a..a8f003721d 100644 --- a/conda_environment_dev.yml +++ b/conda_environment_dev.yml @@ -3,12 +3,13 @@ channels: - conda-forge dependencies: # We pin freetype and matplotlib for the image comparison -- freetype=2.9 -- matplotlib-base=3.1 -- cython +- freetype=2.10 +- matplotlib-base=3.5.1 - pytest - pytest-mpl -- pytest-xdist +# Regression introduced in https://github.com/pytest-dev/pytest-xdist/pull/778 +- pytest-xdist <3.5 - pytest-rerunfailures - pytest-instafail - +- pooch +- setuptools-scm diff --git a/doc/Makefile b/doc/Makefile index ac27553837..64ab80934f 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -34,8 +34,8 @@ help: @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: - -rm -rf $(BUILDDIR)/* - -rm -rf $(BUILDDIR)/../api/* + rm -rf $(BUILDDIR)/* + rm -rf auto_examples/ html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/doc/_static/custom-styles.css b/doc/_static/custom-styles.css new file mode 100644 index 0000000000..686bee6c4a --- /dev/null +++ b/doc/_static/custom-styles.css @@ -0,0 +1,31 @@ +/* Styling for the download buttons */ + +.downloadbutton { + min-width: 200px; + margin-top: 25px; + background-color:rgb(80, 157, 185); + -moz-border-radius:28px; + -webkit-border-radius:28px; + border-radius:28px; + border:1px solid rgb(80, 157, 185); + display:inline-block; + cursor:pointer; + color:#ffffff; + font-size:17px; + padding:16px 31px; + text-decoration:none; + text-align: center; + text-shadow:0px 1px 0px rgb(32, 157, 185); +} +.downloadbutton:hover { + background-color:rgb(32, 157, 185); +} +.downloadbutton:active { + position:relative; + top:1px; +} + +a.downloadbutton:hover, a.downloadbutton:focus { + color: white; + text-decoration: none; +} diff --git a/doc/user_guide/images/interactive_profiles.gif b/doc/_static/interactive_profiles.gif similarity index 100% rename from doc/user_guide/images/interactive_profiles.gif rename to doc/_static/interactive_profiles.gif diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json new file mode 100644 index 0000000000..9d44f87568 --- /dev/null +++ b/doc/_static/switcher.json @@ -0,0 +1,53 @@ +[ + { + "name": "dev", + "version": "dev", + "url": "https://hyperspy.org/hyperspy-doc/dev/" + }, + { + "version": "2.1.x", + "name": "2.1", + "url": "https://hyperspy.org/hyperspy-doc/current/", + "preferred": true + }, + { + "version": "2.0", + "url": "https://hyperspy.org/hyperspy-doc/v2.0/" + }, + { + "version": "1.7", + "url": "https://hyperspy.org/hyperspy-doc/v1.7/" + }, + { + "version": "1.6", + "url": "https://hyperspy.org/hyperspy-doc/v1.6/" + }, + { + "version": "1.5", + "url": "https://hyperspy.org/hyperspy-doc/v1.5/" + }, + { + "version": "1.4", + "url": "https://hyperspy.org/hyperspy-doc/v1.4/" + }, + { + "version": "1.3", + "url": "https://hyperspy.org/hyperspy-doc/v1.3/" + }, + { + "version": "1.2", + "url": "https://hyperspy.org/hyperspy-doc/v1.2/" + }, + { + "version": "1.1", + "url": "https://hyperspy.org/hyperspy-doc/v1.1/" + }, + { + "version": "1.0", + "url": "https://hyperspy.org/hyperspy-doc/v1.0/" + }, + { + "version": "0.8", + "url": "https://hyperspy.org/hyperspy-doc/v0.8/" + } +] diff --git a/doc/changes.rst b/doc/changes.rst new file mode 100644 index 0000000000..d9e113ec68 --- /dev/null +++ b/doc/changes.rst @@ -0,0 +1 @@ +.. include:: ../CHANGES.rst diff --git a/doc/citing.rst b/doc/citing.rst deleted file mode 100644 index afb9369c69..0000000000 --- a/doc/citing.rst +++ /dev/null @@ -1,14 +0,0 @@ -================ - Citing HyperSpy -================ - -If HyperSpy has been significant to a project that leads to an academic -publication, please acknowledge that fact by citing it. The DOI in the -badge below is the `Concept DOI `_ of -HyperSpy. It can be used to cite the project without referring to a specific -version. If you are citing HyperSpy because you have used it to process data, -please use the DOI of the specific version that you have employed. You can -find iy by clicking on the DOI badge below. - -.. image:: https://zenodo.org/badge/doi/10.5281/zenodo.592838.svg - :target: https://doi.org/10.5281/zenodo.592838 diff --git a/doc/conf.py b/doc/conf.py index bc68cd8aa1..bb367a00c0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -11,105 +11,130 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from hyperspy import Release import sys from datetime import datetime -sys.path.append('../') +import hyperspy + +sys.path.append("../") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.intersphinx', - 'sphinx.ext.imgmath', - 'sphinx.ext.graphviz', - 'sphinx.ext.autosummary', - 'sphinx_toggleprompt', - 'sphinxcontrib.towncrier', + "IPython.sphinxext.ipython_directive", # Needed in basic_usage.rst + "numpydoc", + "sphinxcontrib.towncrier", + "sphinx_design", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.githubpages", + "sphinx.ext.graphviz", + "sphinx.ext.mathjax", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx_gallery.gen_gallery", + "sphinx_copybutton", + "sphinx_favicon", +] + +linkcheck_ignore = [ + "https://anaconda.org", # 403 Client Error: Forbidden for url + "https://doi.org/10.1021/acs.nanolett.5b00449", # 403 Client Error: Forbidden for url + "https://onlinelibrary.wiley.com", # 403 Client Error: Forbidden for url + "https://www.jstor.org/stable/24307705", # 403 Client Error: Forbidden for url + "https://software.opensuse.org", # 400 Client Error: Bad Request for url ] +linkcheck_exclude_documents = [] + +# Specify a standard user agent, as Sphinx default is blocked on some sites +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54" + try: - import sphinxcontrib.spelling - extensions.append('sphinxcontrib.spelling') + import sphinxcontrib.spelling # noqa: F401 + + extensions.append("sphinxcontrib.spelling") except BaseException: pass # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] + +autosummary_generate = True # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'HyperSpy' -copyright = f'2011-{datetime.today().year}, The HyperSpy development team' +project = "HyperSpy" +copyright = f"2011-{datetime.today().year}, The HyperSpy development team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = Release.version # The full version, including alpha/beta/rc tags. -release = Release.version +release = hyperspy.__version__ +# The short X.Y version. +version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for +# The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -117,164 +142,308 @@ # html_theme_options = {'collapsiblesidebar': True} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_static/hyperspy_logo.png' - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = '_static/hyperspy.ico' +html_logo = "_static/hyperspy_logo.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] +favicons = [ + "hyperspy.ico", +] + +# For version switcher: +# For development, we match to the dev version in `switcher.json` +# for release version, we match to the minor increment + +# The old version banner used `release` to compare to the "prefered" version +# using https://www.npmjs.com/package/compare-versions +# To play well with our documentation structure (the preferred version point +# to the latest minor or patch release without having to update on patch release), +# we add a ".x". +# See https://github.com/pydata/pydata-sphinx-theme/issues/1552 for more context +# On a minor release, the version switcher json is updated. +# In the version switcher json, version needs to be defined with a `x`, e.g. 2.1.x +# in as it is done here to make sure that they match! +version_match = "dev" if "dev" in release else ".".join(release.split(".")[:2] + ["x"]) + +print("version_match:", version_match) + +html_theme_options = { + "analytics": { + "google_analytics_id": "G-B0XD0GTW1M", + }, + "show_toc_level": 2, + "github_url": "https://github.com/hyperspy/hyperspy", + "icon_links": [ + { + "name": "Gitter", + "url": "https://gitter.im/hyperspy/hyperspy", + "icon": "fab fa-gitter", + }, + { + "name": "HyperSpy", + "url": "https://hyperspy.org", + "icon": "_static/hyperspy.ico", + "type": "local", + }, + ], + "logo": { + "text": "HyperSpy", + }, + "external_links": [ + { + "url": "https://github.com/hyperspy/hyperspy-demos", + "name": "Tutorial", + }, + ], + "header_links_before_dropdown": 7, + "show_version_warning_banner": True, + "switcher": { + # Update when merged and released + "json_url": "https://hyperspy.org/hyperspy-doc/dev/_static/switcher.json", + "version_match": version_match, + }, + "navbar_start": ["navbar-logo", "version-switcher"], + "announcement": "HyperSpy API has changed in version 2.0, see the release notes!", +} # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'HyperSpydoc' +htmlhelp_basename = "HyperSpydoc" # Add the documentation for __init__() methods and the class docstring to the # built documentation -autoclass_content = 'both' +autoclass_content = "both" # -- Options for LaTeX output -------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'HyperSpy.tex', 'HyperSpy Documentation', - 'The HyperSpy Developers', 'manual'), + ( + "index", + "HyperSpy.tex", + "HyperSpy Documentation", + "The HyperSpy Developers", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'hyperspy', 'HyperSpy Documentation', - ['The HyperSpy developers'], 1) + ("index", "hyperspy", "HyperSpy Documentation", ["The HyperSpy developers"], 1) ] # -- Options for towncrier_draft extension ----------------------------------- # Options: draft/sphinx-version/sphinx-release -towncrier_draft_autoversion_mode = 'draft' +towncrier_draft_autoversion_mode = "draft" towncrier_draft_include_empty = False towncrier_draft_working_directory = ".." - # Add the hyperspy website to the intersphinx domains -intersphinx_mapping = {'python': ('https://docs.python.org/3', None), - 'hyperspyweb': ('https://hyperspy.org/', None), - 'matplotlib': ('https://matplotlib.org', None), - 'numpy': ('https://docs.scipy.org/doc/numpy', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'dask': ('https://docs.dask.org/en/latest', None), - 'astroML': ('https://www.astroml.org/', None), - 'sklearn': ('https://scikit-learn.org/stable', None), - 'skimage': ('https://scikit-image.org/docs/stable', None), - } +intersphinx_mapping = { + "cupy": ("https://docs.cupy.dev/en/stable", None), + "dask": ("https://docs.dask.org/en/latest", None), + "exspy": ("https://exspy.readthedocs.io/en/latest", None), + "h5py": ("https://docs.h5py.org/en/stable", None), + "holospy": ("https://holospy.readthedocs.io/en/latest", None), + "IPython": ("https://ipython.readthedocs.io/en/stable", None), + "ipyparallel": ("https://ipyparallel.readthedocs.io/en/latest", None), + "mdp": ("https://mdp-toolkit.github.io/", None), + "matplotlib": ("https://matplotlib.org/stable", None), + "numpy": ("https://numpy.org/doc/stable", None), + "pint": ("https://pint.readthedocs.io/en/stable", None), + "python": ("https://docs.python.org/3", None), + "rsciio": ("https://hyperspy.org/rosettasciio/", None), + "scipy": ("https://docs.scipy.org/doc/scipy", None), + "skimage": ("https://scikit-image.org/docs/stable", None), + "sklearn": ("https://scikit-learn.org/stable", None), + "traits": ("https://docs.enthought.com/traits/", None), + "zarr": ("https://zarr.readthedocs.io/en/stable", None), +} + +# Check links to API when building documentation +nitpicky = True +# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-nitpick_ignore +nitpick_ignore_regex = ( + # No need to be added to the API: documented in subclass + ("py:class", "hyperspy.misc.slicing.FancySlicing"), + ("py:class", "hyperspy.learn.mva.MVA"), + ("py:class", "hyperspy.signal.MVATools"), + ("py:class", "hyperspy.samfire_utils.strategy.SamfireStrategy"), + ("py:class", ".*goodness_test"), + ("py:class", "hyperspy.roi.BasePointROI"), + # Add exception to API + ("py:obj", "SignalDimensionError"), + ("py:obj", "DataDimensionError"), + # Need to be made a property + ("py:attr", "api.signals.BaseSignal.learning_results"), + ("py:attr", "api.signals.BaseSignal.axes_manager"), + ("py:attr", "hyperspy._signals.lazy.LazySignal.navigator"), + # Skip for now + ("py:attr", "axes.BaseDataAxis.is_binned.*"), + ("py:attr", "api.model.components1D.ScalableFixedPattern.*"), + ("py:class", ".*HistogramTilePlot"), + ("py:class", ".*SquareCollection"), + ("py:class", ".*RectangleCollection"), + # Traits property not playing well + ("py:attr", "component.Parameter.*"), + # Adding to the API reference not useful + ("py:.*", "api.preferences.*"), + # Unknown + ("py:.*", "has_pool"), +) + +# -- Options for numpydoc extension ----------------------------------- + +numpydoc_show_class_members = False +numpydoc_xref_param_type = True +numpydoc_xref_ignore = { + "type", + "optional", + "default", + "of", + "or", + "auto", + "from_elements", + "all_alpha", + "subclass", + "dask", + "scheduler", + "matplotlib", + "color", + "line", + "style", + "hyperspy", + "widget", + "strategy", + "module", + "prettytable", +} + +# if Version(numpydoc.__version__) >= Version("1.6.0rc0"): +# numpydoc_validation_checks = {"all", "ES01", "EX01", "GL02", "GL03", "SA01", "SS06"} + +autoclass_content = "both" + +autodoc_default_options = { + "show-inheritance": True, +} +toc_object_entries_show_parents = "hide" +numpydoc_class_members_toctree = False + +# -- Sphinx-Gallery--------------- + +# https://sphinx-gallery.github.io +sphinx_gallery_conf = { + "examples_dirs": "../examples", # path to your example scripts + "gallery_dirs": "auto_examples", # path to where to save gallery generated output + "filename_pattern": ".py", # pattern to define which will be executed + "ignore_pattern": "_sgskip.py", # pattern to define which will not be executed + "compress_images": ( + "images", + "thumbnails", + ), # use optipng to reduce image file size + "notebook_images": "https://hyperspy.org/hyperspy-doc/current/", # folder for loading images in gallery + "reference_url": {"hyperspy": None}, +} graphviz_output_format = "svg" +# -- Sphinx-copybutton ----------- -def run_apidoc(_): - # https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html - # https://www.sphinx-doc.org/es/1.2/ext/autodoc.html - import os - os.environ['SPHINX_APIDOC_OPTIONS'] = 'members,private-members,no-undoc-members,show-inheritance,ignore-module-all' - from sphinx.ext.apidoc import main +copybutton_prompt_text = r">>> |\.\.\. " +copybutton_prompt_is_regexp = True - cur_dir = os.path.normpath(os.path.dirname(__file__)) - output_path = os.path.join(cur_dir, 'api') - modules = os.path.normpath(os.path.join(cur_dir, "../hyperspy")) - exclude_pattern = ["../hyperspy/tests", - "../hyperspy/external", - "../hyperspy/io_plugins/unbcf_fast.pyx"] - main(['-e', '-f', '-P', '-o', output_path, modules, *exclude_pattern]) +tls_verify = True def setup(app): - app.connect('builder-inited', run_apidoc) + app.add_css_file("custom-styles.css") diff --git a/doc/conftest.py b/doc/conftest.py new file mode 100644 index 0000000000..88966382ba --- /dev/null +++ b/doc/conftest.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import matplotlib + +matplotlib.use("agg") + +import pytest + +import hyperspy.api as hs +import matplotlib.pyplot as plt +import numpy as np + + +@pytest.fixture(autouse=True) +def add_np(doctest_namespace): + doctest_namespace["np"] = np + doctest_namespace["plt"] = plt + doctest_namespace["hs"] = hs + + +# Don't show progressbar since it contains the runtime which +# will make the doctest fail +hs.preferences.General.show_progressbar = False diff --git a/doc/credits.rst b/doc/credits.rst deleted file mode 100644 index ec6ad83854..0000000000 --- a/doc/credits.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _credits: - -******* -Credits -******* - -.. include:: ../AUTHORS.txt - - - diff --git a/doc/dev_guide/coding_style.rst b/doc/dev_guide/coding_style.rst index 8c52b539ee..d75643151e 100644 --- a/doc/dev_guide/coding_style.rst +++ b/doc/dev_guide/coding_style.rst @@ -1,5 +1,4 @@ - - +.. _ruff: https://docs.astral.sh/ruff .. _coding_style-label: Coding style @@ -8,30 +7,46 @@ Coding style HyperSpy follows the Style Guide for Python Code - these are rules for code consistency that you can read all about in the `Python Style Guide `_. You can use the -`black `_ code formatter to automatically -fix the style of your code. You can install and run ``black`` by: - -.. code:: bash - - pip install black - black /path/to/your/file.py - -In Linux and MacOS you can run ``black`` automatically after each commit by -adding a ``post-commit`` file to ``.git/hook`` with the following content: - -.. code-block:: bash - - #!/bin/sh - # From https://gist.github.com/temoto/6183235 - FILES=$(git diff HEAD^ HEAD --name-only --diff-filter=ACM | grep -e '\.py$') - if [ -n "$FILES" ]; then - for f in $FILES - do - # black correction - black -v $f - git add $f - done - #git commit -m "Automatic style corrections courtesy of black" - GIT_COMMITTER_NAME="black" GIT_COMMITTER_EMAIL="black@email.com" git - commit --author="black " -m "Automatic style - corrections courtesy of black" +`black `_ or `ruff`_ code formatter to automatically +fix the style of your code using pre-commit hooks. + +Linting error can be suppressed in the code using the ``# noqa`` marker, +more information in the `ruff documentation `_. + +Pre-commit hooks +================ +Code linting and formatting is checked continuously using `ruff`_ pre-commit hooks. + +These can be run locally by using `pre-commit `__. +Alternatively, the comment ``pre-commit.ci autofix`` can be added to a PR to fix the formatting +using `pre-commit.ci `_. + +Deprecations +============ +HyperSpy follows `semantic versioning `_ where changes follow such that: + +1. MAJOR version when you make incompatible API changes +2. MINOR version when you add functionality in a backward compatible manner +3. PATCH version when you make backward compatible bug fixes + +This means that as little, ideally no, functionality should break between minor releases. +Deprecation warnings are raised whenever possible and feasible for functions/methods/properties/arguments, +so that users get a heads-up one (minor) release before something is removed or changes, with a possible +alternative to be used. + +A deprecation decorator should be placed right above the object signature to be deprecated: + +.. code-block:: python + + @deprecated(since=1.7.0, removal=2.0.0, alternative="bar") + def foo(self, n): + return n + 1 + + @deprecated_argument(since=1.7.0, removal=2.0.0,name="x", alternative="y") + def this_property(y): + return y + +This will update the docstring, and print a visible deprecation warning telling +the user to use the alternative function or argument. + +These deprecation wrappers are inspired by those in ``kikuchipy``. diff --git a/doc/dev_guide/git.rst b/doc/dev_guide/git.rst index 532db33af5..b97a72a03c 100644 --- a/doc/dev_guide/git.rst +++ b/doc/dev_guide/git.rst @@ -7,11 +7,11 @@ Using Git and GitHub For developing the code, the home of HyperSpy is on `GitHub `_, and you'll see that -a lot of this guide boils down to properly use that platform. So, visit the +a lot of this guide boils down to properly using that platform. So, visit the following link and poke around the code, issues, and pull requests: `HyperSpy on GitHub `_. -It is probably also worth visiting the `github.com `_ +It is probably also worth to visit `github.com `_ and to go through the `"boot camp" `_ to get a feel for the terminology. @@ -37,7 +37,7 @@ PS: If you choose to develop in Windows/Mac you may find `Github Desktop Use Git and work in manageable branches ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -By now you will have had a look around GitHub -- but why is it so important? +By now, you will have had a look around GitHub -- but why is it so important? Well, GitHub is the public forum in which we manage and discuss development of the code. More importantly, it enables every developer to use `Git`, which is @@ -47,15 +47,15 @@ branches) and switch between them easily. Later, you can choose which version you want to have integrated into HyperSpy. You can learn all about Git at `git-scm `_! -It is very important to separate your contributions so +It is very important to separate your contributions, so that each branch is a small advancement on the "master" code or on another branch. In the end, each branch will have to be checked and reviewed by someone else before it can be included -- so if it is too big, you will be asked to split it up! For personal use, before integrating things into the main HyperSpy code, you -can merge some together for your personal use. However, make sure each new -feature has it's own branch that is contributed through a separate pull +can merge some branches for your personal use. However, make sure each new +feature has its own branch that is contributed through a separate pull request! Diagrammatically, you should be aiming for something like this: @@ -75,13 +75,13 @@ Each number will change depending on the type of changes according to the follow The git repository of HyperSpy has 3 main branches matching the above pattern and depending on the type of pull request, you will need to base your pull request -one one of the following branch: +on one of the following branch: - ``RELEASE_next_major`` to change the API in a not backward-compatible fashion, - ``RELEASE_next_minor`` to add new features and improvement, - ``RELEASE_next_patch`` for bug fixes. -The ``RELEASE_next_patch`` is merged daily in the ``RELEASE_next_minor`` by the github action +The ``RELEASE_next_patch`` branch is merged daily into ``RELEASE_next_minor`` by the github action `Nightly Merge `_. @@ -102,3 +102,39 @@ For example, to rebase the ``bug_fix_branch`` branch from ``RELEASE_next_minor`` $ git rebase --onto RELEASE_next_patch RELEASE_next_minor bug_fix_branch + +Keeping the git history clean +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For review, and for revisiting changes at a later point, it is advisable to keep a "clean" git history, i.e. a meaningful succession of commits. In some cases, it is useful to rewrite the git history to keep it more readable: + +* it is not always possible to keep a clean history and quite often the code development follows an exploratory process with code changes going back and forth, etc. +* Commits that only fix typographic mistakes, formatting or failing tests usually can be *squashed* (merged) into the previous commits. + +When using a GUI for interaction with *git*, check out its features for joining and reordering commits. + +When using git in the command line, use ``git rebase`` with the *interactive* option. For example, to rearrange the last five commits: + +.. code:: bash + + $ git rebase -i HEAD~5 + +In a text editor, you can then edit the commit history. If you have commits ``a...e`` and want to merge ``b`` and ``e`` into ``a`` and ``d``, respectively, while moving ``c`` to the end of the hisotry, your file would look the following: + +.. code:: text + + pick a ... + squash b ... + pick d ... + squash e ... + pick c ... + +Afterwards, you get a chance to edit the commit messages. + +Finally, to push the changes, use a ``+`` in front of the branch name, to override commits you have already pushed to github previously: + +.. code:: bash + + git push origin +lumberjack-branch + +See, for example, `How (and why!) to keep your Git commit history clean `_ for a more detailed blog post on this subject. diff --git a/doc/dev_guide/index.rst b/doc/dev_guide/index.rst new file mode 100644 index 0000000000..a52440278c --- /dev/null +++ b/doc/dev_guide/index.rst @@ -0,0 +1,27 @@ +.. _development: + +########## +Contribute +########## + +HyperSpy is a community project maintained for and by its users. There are many ways you can help! + +- Help other users on `gitter `_ +- report a bug or request a feature on `GitHub `_ +- or improve the :ref:`documentation and code` + +.. toctree:: + :maxdepth: 2 + :caption: Contributing Guide + + intro.rst + git.rst + testing.rst + writing_docs.rst + coding_style.rst + lazy_computations.rst + plotting.rst + speeding_up_code.rst + writing_extensions.rst + useful_information.rst + maintenance.rst diff --git a/doc/dev_guide/intro.rst b/doc/dev_guide/intro.rst index 3479598436..e249752265 100644 --- a/doc/dev_guide/intro.rst +++ b/doc/dev_guide/intro.rst @@ -1,6 +1,6 @@  -.. _dev_guide-label: +.. _dev_guide: Introduction ============= @@ -21,13 +21,13 @@ Getting started 1. Start using HyperSpy and understand it ----------------------------------------- -Probably you would not be interested in contributing to HyperSpy, if you were +Probably you would not be interested in contributing to HyperSpy, if you were not already a user, but, just in case: the best way to start understanding how HyperSpy works and to build a broad overview of the code as it stands is to use it -- so what are you waiting for? :ref:`Install HyperSpy `! -The HyperSpy :ref:`User Guide ` also provides a good overview -of all the parts of the code that are currently implemented as well as much +The HyperSpy :ref:`User Guide ` also provides a good overview +of all the parts of the code that are currently implemented as well as much information about how everything works -- so read it well. @@ -62,7 +62,7 @@ Indeed, there are many ways to contribute: 1. Just by asking a question in our `Gitter chat room `_ instead of sending a private email to the developers you are contributing to - HyperSpy. Once you get more familiar with HyperSpy, it will be awesome if + HyperSpy. Once you get more familiar with HyperSpy, it will be awesome if you could help others with their questions. 2. Issues reported in the `issues tracker `_ @@ -85,7 +85,7 @@ You may have a very clear idea of what you want to contribute, but if you're not sure where to start, you can always look through the issues and pull requests on the `GitHub Page `_. You'll find that there are many known areas for development in the issues -and a number of pull-requests are partially finished projects just sitting +and a number of pull-requests are partially finished projects just sitting there waiting for a keen new contributor to come and learn by finishing. The documentation (let it be the docstrings, @@ -105,6 +105,16 @@ There are 3 key points to get right when starting out as a contributor: chunks. Use :ref:`Git branches ` to keep work separated in manageable sections. 2. Make sure that your :ref:`code style ` is good. -3. Bear in mind that every new function you write will need +3. Bear in mind that every new function you write will need :ref:`tests ` and :ref:`user documentation `! + +.. note:: + + The IO plugins formerly developed within HyperSpy have now been moved to + the separate `RosettaSciIO repository `_ + in order to facilitate a wider use also by other packages. Plugins supporting + additional formats or corrections/enhancements to existing plugins should now + be contributed to the `RosettaSciIO repository `_ + and file format specific issues should be reported to the `RosettaSciIO issue + tracker `_. diff --git a/doc/dev_guide/lazy_computations.rst b/doc/dev_guide/lazy_computations.rst index 1f75888f0c..3b5b43207e 100644 --- a/doc/dev_guide/lazy_computations.rst +++ b/doc/dev_guide/lazy_computations.rst @@ -11,21 +11,21 @@ have attempted to streamline it as much as possible. ``LazySignals`` use the two arrays are indeed almost identical, the most important differences are (``da`` being ``dask.array.Array`` in the examples): - - **Dask arrays are immutable**: ``da[3] = 2`` does not work. ``da += 2`` - does, but it's actually a new object -- you might as well use ``da = da + 2`` - for a better distinction. - - **Unknown shapes are problematic**: ``res = da[da>0.3]`` works, but the - shape of the result depends on the values and cannot be inferred without - execution. Hence, few operations can be run on ``res`` lazily, and it should - be avoided if possible. - - **Computations in Dask are Lazy**: Dask only preforms a computation when it has to. For example - the sum function isn't run until compute is called. This also means that some function can be - applied to only some portion of the data. +- **Dask arrays are immutable**: ``da[3] = 2`` does not work. ``da += 2`` + does, but it's actually a new object -- you might as well use ``da = da + 2`` + for a better distinction. +- **Unknown shapes are problematic**: ``res = da[da>0.3]`` works, but the + shape of the result depends on the values and cannot be inferred without + execution. Hence, few operations can be run on ``res`` lazily, and it should + be avoided if possible. +- **Computations in Dask are Lazy**: Dask only preforms a computation when it has to. For example + the sum function isn't run until compute is called. This also means that some function can be + applied to only some portion of the data. - .. code-block::python - summed_lazy_signal = lazy_signal.sum(axis=lazy_signal.axes_manager.signal_axes) # Dask sets up tasks but does not compute - summed_lazy_signal.inav[0:10].compute() # computes sum for only 0:10 - summed_lazy_signal.compute() # runs sum function + .. code-block::python + summed_lazy_signal = lazy_signal.sum(axis=lazy_signal.axes_manager.signal_axes) # Dask sets up tasks but does not compute + summed_lazy_signal.inav[0:10].compute() # computes sum for only 0:10 + summed_lazy_signal.compute() # runs sum function @@ -38,7 +38,7 @@ code (assignment, etc.) you wish. The ``map`` function is flexible and should be able to handle most operations that operate on some signal. If you add a ``BaseSignal`` with the same navigation size -as the signal it will be iterated alongside the mapped signal otherwise a keyword +as the signal, it will be iterated alongside the mapped signal, otherwise a keyword argument is assumed to be constant and is applied to every signal. If the new method cannot be coerced into a shape suitable for ``map``, separate diff --git a/doc/dev_guide/maintenance.rst b/doc/dev_guide/maintenance.rst new file mode 100644 index 0000000000..8be478c2dc --- /dev/null +++ b/doc/dev_guide/maintenance.rst @@ -0,0 +1,35 @@ +.. _maintenance-label: + +Maintenance +=========== + +GitHub Workflows +^^^^^^^^^^^^^^^^ + +`GitHub workflows `_ are used to: + +- run the test suite +- build packages and upload to pypi and GitHub release +- build the documentation and check the links (external and cross-references) +- push the development documentation to the ``dev`` folder of https://github.com/hyperspy/hyperspy-doc + +Some of these workflow need to access `GitHub "secrets" `_, +which are private to the HyperSpy repository. The personal access token ``PAT_DOCUMENTATION`` is set to be able to push +the documentation built to the https://github.com/hyperspy/hyperspy-doc repository. + +To reduce the risk that these "secrets" are made accessible publicly, for example, through the +injection of malicious code by third parties in one of the GitHub workflows used in the HyperSpy +organisation, the third party actions (those that are not provided by established trusted parties) +are pinned to the ``SHA`` of a specific commit, which is trusted not to contain malicious code. + +Updating GitHub Actions +^^^^^^^^^^^^^^^^^^^^^^^ + +The workflows in the HyperSpy repository use GitHub actions provided by established trusted parties +and third parties. They are updated regularly by the +`dependabot `_ +in pull requests. + +When updating a third party action, the action has to be pinned using the ``SHA`` of the commit of +the updated version and the corresponding code changes will need to be reviewed to verify that it +doesn't include malicious code. diff --git a/doc/dev_guide/plotting.rst b/doc/dev_guide/plotting.rst new file mode 100644 index 0000000000..4aedc797c5 --- /dev/null +++ b/doc/dev_guide/plotting.rst @@ -0,0 +1,14 @@ +.. _plotting-label: + +Interactive Plotting +==================== +Interactive plotting in hyperspy is handled through ``matplotlib`` and is primarily driven though +event handling. + +Specifically, for some signal ``s``, when the ``index`` value for some :class:`~.axes.BaseDataAxis` +is changed, then the signal plot is updated to reflect the data at that index. Each signal has a +``_get_current_data`` function, which will return the data at the current navigation index. + +For lazy signals, the ``_get_current_data`` function works slightly differently as the current chunk is cached. As a result, +the ``_get_current_data`` function first checks if the current chunk is cached and then either computes the chunk where the +navigation index resides or just pulls the value from the cached chunk. diff --git a/doc/dev_guide/speeding_up_code.rst b/doc/dev_guide/speeding_up_code.rst index 77c4dbfc74..e037d94fea 100644 --- a/doc/dev_guide/speeding_up_code.rst +++ b/doc/dev_guide/speeding_up_code.rst @@ -17,7 +17,7 @@ Writing Numba code If you need to improve the speed of a given part of the code your first choice should be `Numba `_. The motivation is that Numba -code is very similar (when not identical) to Python code, and, therefore, it is +code is very similar (when not identical) to Python code, and therefore, it is a lot easier to maintain than Cython code (see below). Numba is also a required dependency for HyperSpy, unlike Cython which diff --git a/doc/dev_guide/testing.rst b/doc/dev_guide/testing.rst index e959367363..eba142abac 100644 --- a/doc/dev_guide/testing.rst +++ b/doc/dev_guide/testing.rst @@ -14,9 +14,9 @@ for testing. The tests reside in the ``hyperspy.tests`` module. Tests are short functions, found in ``hyperspy/tests``, that call your functions under some known conditions and check the outputs against known values. They -should depend on as few other features as possible so that when they break +should depend on as few other features as possible so that when they break, we know exactly what caused it. Ideally, the tests should be written at the -same time than the code itself, as they are very convenient to run to check +same time as the code itself, as they are very convenient to run to check outputs when coding. Writing tests can seem laborious but you'll probably soon find that they are very important, as they force you to sanity-check all the work that you do. @@ -24,9 +24,9 @@ all the work that you do. **Useful testing hints:** * When comparing integers, it's fine to use ``==`` -* When comparing floats, be sure to use :py:meth:`np.testing.assert_allclose` - or :py:meth:`np.testing.assert_almost_equal` -* :py:meth:`np.testing.assert_allclose` is also convenient for comparing +* When comparing floats, be sure to use :func:`numpy.testing.assert_allclose` + or :func:`numpy.testing.assert_almost_equal` +* :func:`numpy.testing.assert_allclose` is also convenient for comparing numpy arrays * The ``hyperspy.misc.test_utils.py`` contains a few useful functions for testing @@ -34,21 +34,21 @@ all the work that you do. parameters of the same function without having to write to much repetitive code, which is often error-prone. See `pytest documentation `__ for more details. -* It is good to check that the tests does not use too much memory after +* It is good to check that the tests do not use too much memory after creating new tests. If you need to explicitly delete your objects and free memory, you can do the following to release the memory associated with the ``s`` object: -.. code:: python + .. code:: python - >>> import gc + >>> import gc - >>> # Do some work with the object - >>> s = ... + >>> # Do some work with the object + >>> s = ... - >>> # Clear the memory - >>> del s - >>> gc.collect() + >>> # Clear the memory + >>> del s + >>> gc.collect() # doctest: +SKIP Running tests @@ -86,7 +86,7 @@ Or, from HyperSpy's project folder, simply: `_ for more details. The HyperSpy test suite can also be run in parallel if you have multiple CPUs -available, using the ```pytest-xdist`` plugin `_. +available, using the `pytest-xdist plugin `_. If you have the plugin installed, HyperSpy will automatically run the test suite in parallel on your machine. @@ -106,9 +106,20 @@ that all tests in a file run in the same worker. Running tests in parallel using ``pytest-xdist`` will change the content and format of the output of ``pytest`` to the console. We recommend installing - ```pytest-sugar`` `_ to produce + `pytest-sugar `_ to produce nicer-looking output including an animated progressbar. +To test docstring examples, assuming the current location is the HyperSpy root +directory: + +.. code:: bash + + # All + $ pytest --doctest-modules --ignore-glob=hyperspy/tests --pyargs hyperspy + + # In a single file, like the signal.py file + $ pytest --doctest-modules hyperspy/signal.py + Flaky tests ^^^^^^^^^^^ @@ -118,7 +129,7 @@ random or non-deterministic behaviour. They may sometimes pass or sometimes fail it won't always be clear why. These are usually known as "flaky" tests. One way to approach flaky tests is to rerun them, to see if the failure was a one-off. -This can be achieved using the ```pytest-rerunfailures`` plugin `_. +This can be achieved using the `pytest-rerunfailures plugin `_. .. code:: bash @@ -172,13 +183,15 @@ In case of Azure Pipelines, CI helper scripts are pulled from the The testing matrix is as follows: -- **Github Actions**: test a range of Pythons version on Linux, MacOS and Windows; +- **Github Actions**: test a range of Python versions on Linux, MacOS and Windows; all dependencies are installed from `PyPI `_. See ``.github/workflows/tests.yml`` in the HyperSpy repository for further details. - **Azure Pipeline**: test a range of Python versions on Linux, MacOS and Windows; all dependencies are installed from `Anaconda Cloud `_ using the `"conda-forge" `_ channel. See ``azure-pipelines.yml`` in the HyperSpy repository for further details. +- The testing of **HyperSpy extensions** is described in the + :ref:`integration test suite ` section. This testing matrix has been designed to be simple and easy to maintain, whilst ensuring that packages from PyPI and Anaconda cloud are not mixed in order to @@ -220,7 +233,7 @@ tests. The reference images are located in the folder defined by the argument To run plot tests, you simply need to add the option ``--mpl``: -:: code:: bash +.. code:: bash $ pytest --mpl @@ -229,13 +242,13 @@ images will not be compared to the reference images. If you need to add or change some plots, follow the workflow below: - 1. Write the tests using appropriate decorators such as - ``@pytest.mark.mpl_image_compare``. - 2. If you need to generate a new reference image in the folder - ``plot_test_dir``, for example, run: ``pytest - --mpl-generate-path=plot_test_dir`` - 3. Run again the tests and this time they should pass. - 4. Use ``git add`` to put the new file in the git repository. +1. Write the tests using appropriate decorators such as + ``@pytest.mark.mpl_image_compare``. +2. If you need to generate a new reference image in the folder + ``plot_test_dir``, for example, run: ``pytest + --mpl-generate-path=plot_test_dir`` +3. Run again the tests and this time they should pass. +4. Use ``git add`` to put the new file in the git repository. When the plotting tests fail, it is possible to download the figure comparison images generated by ``pytest-mpl`` in the artifacts tabs of the @@ -243,10 +256,16 @@ corresponding build on Azure Pipeliness: .. figure:: ../user_guide/images/azure_pipeline_artifacts.png +.. note:: + + To generate the baseline images, the version of matplotlib defined in + `conda_environment_dev.yml `__ + is required. + The plotting tests are tested on Azure Pipelines against a specific version of -matplotlib defined in ``conda_environment_dev.yml``. This is because small -changes in the way matplotlib generates the figure between versions can sometimes -make the tests fail. +matplotlib defined in `conda_environment_dev.yml `__. +This is because small changes in the way matplotlib generates the figure between +versions can sometimes make the tests fail. For plotting tests, the matplotlib backend is set to ``agg`` by setting the ``MPLBACKEND`` environment variable to ``agg``. At the first import of @@ -256,7 +275,7 @@ variable and accordingly set the backend. Exporting pytest results as HTML ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -With ``pytest-html`` it is possible to export the results of running pytest +With ``pytest-html``, it is possible to export the results of running pytest for easier viewing. It can be installed by conda: .. code:: bash diff --git a/doc/dev_guide/useful_information.rst b/doc/dev_guide/useful_information.rst index 5272cba70a..9b48501311 100644 --- a/doc/dev_guide/useful_information.rst +++ b/doc/dev_guide/useful_information.rst @@ -43,7 +43,7 @@ github: `conda-forge/hyperspy-feedstock `_ are available online: * `pepy.tech `_ * `libraries.io `_ * `pypistats.org `_ + +HTML Representations +-------------------- + +For use inside of jupyter notebooks, *html* representations are functions which allow for +more detailed data representations using snippets of populated HTML. + +Hyperspy uses *jinja* and extends *dask's* *html* representations in many cases in +line with this PR: https://github.com/dask/dask/pull/8019 diff --git a/doc/dev_guide/writing_docs.rst b/doc/dev_guide/writing_docs.rst index b270a159a7..a9e53e7689 100644 --- a/doc/dev_guide/writing_docs.rst +++ b/doc/dev_guide/writing_docs.rst @@ -1,5 +1,4 @@ - .. _writing_documentation-label: Writing documentation @@ -10,12 +9,12 @@ Documentation comes in two parts: docstrings and user-guide documentation. Docstrings ^^^^^^^^^^ -Written at the start of a function and give essential information +Written at the start of a function, they give essential information about how it should be used, such as which arguments can be passed to it and what the syntax should be. The docstrings need to follow the `numpy -specification `_, as shown in `this example -`_. +specification `_, +as shown in `this example +`_. As a general rule, any code that is part of the public API (i.e. any function or class that an end-user might access) should have a clear and comprehensive @@ -24,7 +23,14 @@ be exposed to the end-user (usually a function or class starting with an undersc should still be documented to the extent that future developers can understand what the function does. -You can check your docstrings follow the convention by using the +To test code of "examples" section in the docstring, run: + +.. code:: bash + + pytest --doctest-modules --ignore=hyperspy/tests + + +You can check whether your docstrings follow the convention by using the ``flake8-docstrings`` `extension `_, like this: @@ -49,7 +55,15 @@ how to use it with examples and links to the relevant code. When writing both the docstrings and user guide documentation, it is useful to have some data which the users can use themselves. Artificial -datasets for this purpose can be found in `hyperspy.datasets.artificial_data`. +datasets for this purpose can be found in :mod:`~.api.data`. + +Example codes in the user guide can be tested using +`doctest `_: + +.. code:: bash + + pytest doc --doctest-modules --doctest-glob="*.rst" -v + Build the documentation ^^^^^^^^^^^^^^^^^^^^^^^ @@ -57,11 +71,11 @@ Build the documentation To check the output of what you wrote, you can build the documentation by running the ``make`` command in the ``hyperspy/doc`` directory. For example ``make html`` will build the whole documentation in -html format. See the make command documentation for more details. +html format. See the ``make`` command documentation for more details. To install the documentation dependencies, run either -.. code-block:: bash +.. code-block:: bash $ conda install hyperspy-dev @@ -69,13 +83,13 @@ or .. code-block:: bash - $ pip install hyperspy[build-doc] + $ pip install hyperspy[doc] When writing documentation, the Python package `sphobjinv `_ can be useful for writing cross-references. For example, to find how to write a cross-reference to -:py:meth:`hyperspy.signal.BaseSignal.set_signal_type`, use: +:meth:`~.api.signals.BaseSignal.set_signal_type`, use: .. code-block:: bash @@ -85,4 +99,23 @@ cross-references. For example, to find how to write a cross-reference to Name Score --------------------------------------------------------- ------- - :py:meth:`hyperspy.signal.BaseSignal.set_signal_type` 90 + :meth:`hyperspy.signal.BaseSignal.set_signal_type` 90 + +.. _versioned_documentation: + +Hosting versioned documentation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Builds of the documentation for each minor and major release are hosted in the https://github.com/hyperspy/hyperspy-doc +repository and are used by the `version switcher `_ +of the documentation. + +The ``"dev"`` version is updated automatically when pushing on the ``RELEASE_next_minor`` branch and the `"current"` (stable) +version is updated automatically when a tag is pushed. +When releasing a minor and major release, two manual steps are required: + +1. in https://github.com/hyperspy/hyperspy-doc, copy the "current" stable documentation to a separate folder named with the corresponding version +2. update the documentation version switch, in ``doc/_static/switcher.json``: + + - copy and paste the `"current"`` documentation entry + - update the version in the "current" entry to match the version to be released, e.g. increment the minor or major digit + - in the newly created entry, update the link to the folder created in step 1. diff --git a/doc/dev_guide/writing_extensions.rst b/doc/dev_guide/writing_extensions.rst index e9ba0fd366..4761fbe1aa 100644 --- a/doc/dev_guide/writing_extensions.rst +++ b/doc/dev_guide/writing_extensions.rst @@ -25,9 +25,9 @@ data. Models can also be provided by external packages, but don't need to be registered. Instead, they are returned by the ``create_model`` method of -the relevant :py:class:`hyperspy.signal.BaseSignal` subclass, see for example, -the :py:meth:`hyperspy._signals.eds_tem.EDSTEM_mixin.create_model` of the -:py:class:`~._signals.eds_tem.EDSTEMSpectrum`. +the relevant :class:`~.api.signals.BaseSignal` subclass, see for example, +the :meth:`exspy.signals.EDSTEMSpectrum.create_model` of the +:class:`exspy.signals.EDSTEMSpectrum`. It is good practice to add all packages that extend HyperSpy `to the list of known extensions @@ -54,10 +54,9 @@ In order to register HyperSpy extensions, you need to: 1. Add the following line to your package's ``setup.py``: - .. code-block:: python + .. code-block:: python - entry_points={'hyperspy.extensions': 'your_package_name = - your_package_name'}, + entry_points={'hyperspy.extensions': 'your_package_name = your_package_name'}, 2. Create a ``hyperspy_extension.yaml`` configuration file in your module's root directory. 3. Declare all new HyperSpy objects provided by your package in the @@ -75,11 +74,11 @@ When and where to create a new ``BaseSignal`` subclass ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ HyperSpy provides most of its functionality through the different -:py:class:`hyperspy.signal.BaseSignal` +:class:`~.api.signals.BaseSignal` subclasses. A HyperSpy "signal" is a class that contains data for analysis and functions to perform the analysis in the form of class methods. Functions that are useful for the analysis of most datasets are in the -:py:class:`hyperspy.signal.BaseSignal` class. All other functions are in +:class:`~.api.signals.BaseSignal` class. All other functions are in specialized subclasses. The flowchart below can help you decide where to add @@ -146,7 +145,7 @@ for your function, you should consider creating your own. Registering a new BaseSignal subclass ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To register a new :py:class:`hyperspy.signal.BaseSignal` subclass you must add it to the +To register a new :class:`~.api.signals.BaseSignal` subclass you must add it to the ``hyperspy_extension.yaml`` file, as in the following example: .. code-block:: yaml @@ -181,7 +180,7 @@ match for each sort of data. The optional ``signal_type_aliases`` are used to determine the most appropriate signal subclass when using -:py:meth:`hyperspy.signal.BaseSignal.set_signal_type`. +:meth:`~.api.signals.BaseSignal.set_signal_type`. For example, if the ``signal_type`` ``Electron Energy Loss Spectroscopy`` has an ``EELS`` alias, setting the signal type to ``EELS`` will correctly assign the signal subclass with ``Electron Energy Loss Spectroscopy`` signal type. @@ -194,7 +193,7 @@ Creating new HyperSpy model components When and where to create a new component ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -HyperSpy provides the :py:class:`hyperspy._components.expression.Expression` +HyperSpy provides the :class:`hyperspy._components.expression.Expression` component that enables easy creation of 1D and 2D components from mathematical expressions. Therefore, strictly speaking, we only need to create new components when they cannot be expressed as simple mathematical @@ -205,7 +204,7 @@ HyperSpy itself (if they are of common interest) or to specialized external packages extending HyperSpy. The flowchart below can help you decide when and where to add -a new hyperspy model :py:class:`hyperspy.component.Component` +a new hyperspy model :class:`hyperspy.component.Component` for your function, should you consider creating your own. .. This is the original mermaid code. It produces a nicer looking diagram @@ -277,7 +276,7 @@ Registering new components ^^^^^^^^^^^^^^^^^^^^^^^^^^ All new components must be a subclass of -:py:class:`hyperspy._components.expression.Expression`. To register a new +:class:`hyperspy._components.expression.Expression`. To register a new 1D component add it to the ``hyperspy_extension.yaml`` file as in the following example: @@ -319,12 +318,12 @@ Equivalently, to add a new component 2D: Creating and registering new widgets and toolkeys ------------------------------------------------- -To generate GUIs of specific methods and functions, HyperSpy use widgets and +To generate GUIs of specific methods and functions, HyperSpy uses widgets and toolkeys: * *widgets* (typically ipywidgets or traitsui objects) generate GUIs, -* *toolkeys* are functions using which it is possible to associate widgets to - a signal method or to a module function. +* *toolkeys* are functions which associate widgets to a signal method + or to a module function. An extension can declare new toolkeys and widgets. For example, the `hyperspy-gui-traitsui `_ and @@ -335,7 +334,7 @@ Registering toolkeys ^^^^^^^^^^^^^^^^^^^^ To register a new toolkey: -1. Declare a new toolkey, *e. g.* by adding the :py:func:`~.ui_registry.add_gui_method` +1. Declare a new toolkey, *e. g.* by adding the ``hyperspy.ui_registry.add_gui_method`` decorator to the function you want to assign a widget to. 2. Register a new toolkey that you have declared in your package by adding it to the ``hyperspy_extension.yaml`` file, as in the following example: @@ -379,7 +378,7 @@ Integration test suite ---------------------- The `integration test suite `__ -runs the test suite of hyperspy and hyperspy extension on a daily basis against both the +runs the test suite of HyperSpy and of all registered HyperSpy extensions on a daily basis against both the release and development versions. The build matrix is as follows: .. list-table:: Build matrix of the integration test suite @@ -418,6 +417,11 @@ The development packages of the dependencies are provided by the `scipy-wheels-nightly `_ repository, which provides ``scipy``, ``numpy``, ``scikit-learn`` and ``scikit-image`` at the time of writing. -The pre-release packages are obtained from `pypi `_ and these -will be used for any dependency which provides a pre-release package on pypi. +The pre-release packages are obtained from `PyPI `_ and these +will be used for any dependency which provides a pre-release package on PyPI. + +A similar `Integration test `__ +workflow can run from pull requests (PR) to the +`hyperspy `_ repository when the label +``run-extension-tests`` is added to a PR or when a PR review is edited. diff --git a/doc/gh-pages.py b/doc/gh-pages.py index 32df6d5e14..2fe2dd8e6f 100755 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -9,31 +9,30 @@ that is how the resulting directory will be named. In practice, you should use either actual clean tags from a current build or -something like 'current' as a stable URL for the most current version of the """ +something like 'current' as a stable URL for the most current version of the""" -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Imports -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- import os import shutil import sys from os import chdir as cd from os.path import join as pjoin +from subprocess import PIPE, CalledProcessError, Popen, check_call -from subprocess import Popen, PIPE, CalledProcessError, check_call - -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Globals -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- -pages_dir = 'gh-pages' -html_dir = '_build/html' -pdf_dir = '_build/latex' -pages_repo = 'https://github.com/hyperspy/hyperspy-doc.git' +pages_dir = "gh-pages" +html_dir = "_build/html" +pdf_dir = "_build/latex" +pages_repo = "https://github.com/hyperspy/hyperspy-doc.git" -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Functions -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def sh(cmd): @@ -72,14 +71,14 @@ def init_repo(path): sh("git clone %s %s" % (pages_repo, path)) here = os.getcwd() cd(path) - sh('git checkout gh-pages') + sh("git checkout gh-pages") cd(here) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Script starts -#----------------------------------------------------------------------------- -if __name__ == '__main__': +# ----------------------------------------------------------------------------- +if __name__ == "__main__": # The tag can be given as a positional argument try: tag = sys.argv[1] @@ -93,44 +92,43 @@ def init_repo(path): else: # ensure up-to-date before operating cd(pages_dir) - sh('git checkout gh-pages') - sh('git pull') + sh("git checkout gh-pages") + sh("git pull") cd(startdir) dest = pjoin(pages_dir, tag) # don't `make html` here, because gh-pages already depends on html in Makefile # sh('make html') - if tag != 'dev': + if tag != "dev": # only build pdf for non-dev targets - #sh2('make pdf') + # sh2('make pdf') pass # This is pretty unforgiving: we unconditionally nuke the destination # directory, and then copy the html tree in there shutil.rmtree(dest, ignore_errors=True) shutil.copytree(html_dir, dest) - if tag != 'dev': - #shutil.copy(pjoin(pdf_dir, 'ipython.pdf'), pjoin(dest, 'ipython.pdf')) + if tag != "dev": + # shutil.copy(pjoin(pdf_dir, 'ipython.pdf'), pjoin(dest, 'ipython.pdf')) pass try: cd(pages_dir) - branch = sh2('git rev-parse --abbrev-ref HEAD').strip().decode() - if branch != 'gh-pages': - e = 'On %r, git branch is %r, MUST be "gh-pages"' % (pages_dir, - branch) + branch = sh2("git rev-parse --abbrev-ref HEAD").strip().decode() + if branch != "gh-pages": + e = 'On %r, git branch is %r, MUST be "gh-pages"' % (pages_dir, branch) raise RuntimeError(e) - sh('git add -A %s' % tag) + sh("git add -A %s" % tag) sh('git commit -m"Updated doc release: %s"' % tag) print() - print('Most recent 3 commits:') + print("Most recent 3 commits:") sys.stdout.flush() - sh('git --no-pager log --oneline HEAD~3..') + sh("git --no-pager log --oneline HEAD~3..") finally: cd(startdir) print() - print('Now verify the build in: %r' % dest) + print("Now verify the build in: %r" % dest) print("If everything looks good, 'git push'") diff --git a/doc/help.rst b/doc/help.rst new file mode 100644 index 0000000000..bd36c9408f --- /dev/null +++ b/doc/help.rst @@ -0,0 +1,13 @@ +.. _get_help: + +Getting help +============ + +There are several places to obtain help with HyperSpy: + +- The `HyperSpy Gitter `_ chat is a good placed to go for both troubleshooting and general questions. +- Issue with installation? There are some troubleshooting tips built into the :ref:`installation page `, +- If you want to request new features or if you’re confident that you have found a bug, please + create a new issue on the `HyperSpy GitHub issues `_ page. + When reporting bugs, please try to replicate the bug with the HyperSpy sample data, and make every effort + to simplify your example script to only the elements necessary to replicate the bug. diff --git a/doc/hspy_examples b/doc/hspy_examples deleted file mode 120000 index 785887f7fb..0000000000 --- a/doc/hspy_examples +++ /dev/null @@ -1 +0,0 @@ -../examples/ \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst index a0e25edf72..e8fc37357e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,71 +1,12 @@ -.. HyperSpy documentation master file, created by - sphinx-quickstart on Mon Oct 18 11:10:55 2010. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -.. _user_guide-label: - -Welcome to HyperSpy's documentation! -==================================== - -.. toctree:: - :maxdepth: 1 - :caption: User Guide - - user_guide/intro.rst - user_guide/changes.rst - user_guide/install.rst - user_guide/getting_started.rst - user_guide/signal.rst - user_guide/axes.rst - user_guide/interactive_operations_ROIs.rst - user_guide/signal1d.rst - user_guide/signal2d.rst - user_guide/visualisation.rst - user_guide/mva.rst - user_guide/model.rst - user_guide/eels.rst - user_guide/eds.rst - user_guide/dielectric_function.rst - user_guide/electron_holography.rst - user_guide/io.rst - user_guide/events.rst - user_guide/big_data.rst - user_guide/metadata_structure.rst - user_guide/bibliography.rst - .. toctree:: - :maxdepth: 1 - :caption: Developer Guide - - dev_guide/intro.rst - dev_guide/git.rst - dev_guide/testing.rst - dev_guide/writing_docs.rst - dev_guide/coding_style.rst - dev_guide/lazy_computations.rst - dev_guide/speeding_up_code.rst - dev_guide/writing_extensions.rst - dev_guide/useful_information.rst - -.. toctree:: - :maxdepth: 2 - :caption: API References - - api/modules.rst - -.. toctree:: - :maxdepth: 1 - :caption: Credits and citation - - credits.rst - citing.rst - - -Indices and tables -================== + :maxdepth: 1 + :hidden: -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + User Guide + Examples + Reference + Get Help + Release Notes + Contribute +.. include:: intro.rst diff --git a/doc/intro.rst b/doc/intro.rst new file mode 100644 index 0000000000..e5e2be3cb2 --- /dev/null +++ b/doc/intro.rst @@ -0,0 +1,163 @@ + +What is HyperSpy +================ + +HyperSpy is an open source Python library which provides tools to facilitate +the interactive data analysis of multidimensional datasets that can be +described as multidimensional arrays of a given signal (e.g. a 2D array of +spectra a.k.a spectrum image). + +HyperSpy aims at making it easy and natural to apply analytical procedures +that operate on an individual signal to multidimensional datasets of any +size, as well as providing easy access to analytical tools that exploit their +multidimensionality. + +.. versionadded:: 1.5 + External packages can extend HyperSpy by registering signals, + components and widgets. + + +The functionality of HyperSpy can be extended by external packages, e.g. to +implement features for analyzing a particular sort of data (usually related to a +specific set of experimental methods). A `list of packages that extend HyperSpy +`_ is curated in a +dedicated repository. For details on how to register extensions see +:ref:`writing_extensions-label`. + + +.. versionchanged:: 2.0 + HyperSpy was split into a core package (HyperSpy) that provides the common + infrastructure for multidimensional datasets and the dedicated IO package + :external+rsciio:doc:`RosettaSciIO `. Signal classes focused on + specific types of data previously included in HyperSpy (EELS, EDS, Holography) + were moved to specialized `HyperSpy extensions + `_. + + +HyperSpy's character +==================== + +HyperSpy has been written by a subset of the people who use it, a particularity +that sets its character: + +* To us, this program is a research tool, much like a screwdriver or a Green's + function. We believe that the better our tools are, the better our research + will be. We also think that it is beneficial for the advancement of knowledge + to share our research tools and to forge them in a collaborative way. This is + because by collaborating we advance faster, mainly by avoiding reinventing the + wheel. Idealistic as it may sound, many other people think like this and it is + thanks to them that this project exists. + +* Not surprisingly, we care about making it easy for others to contribute to + HyperSpy. In other words, + we aim at minimising the “user becomes developer” threshold. + Do you want to contribute already? No problem, see the :ref:`dev_guide` + for details. + +* The main way of interacting with the program is through scripting. + This is because `Jupyter `_ exists, making your + interactive data analysis productive, scalable, reproducible and, + most importantly, fun. That said, widgets to interact with HyperSpy + elements are provided where there + is a clear productivity advantage in doing so. See the + `hyperspy-gui-ipywidgets `_ + and + `hyperspy-gui-traitsui `_ + packages for details. Not enough? If you + need a full, standalone GUI, `HyperSpyUI `_ + is for you. + +Learning resources +================== + +.. grid:: 2 3 3 3 + :gutter: 2 + + .. grid-item-card:: + :link: user_guide/install + :link-type: doc + + :octicon:`rocket;2em;sd-text-info` Getting Started + ^^^ + + New to HyperSpy or Python? The getting started guide provides an + introduction on basic usage of HyperSpy and how to install it. + + .. grid-item-card:: + :link: user_guide/index + :link-type: doc + + :octicon:`book;2em;sd-text-info` User Guide + ^^^ + + The user guide provides in-depth information on key concepts of HyperSpy + and how to use it along with background information and explanations. + + .. grid-item-card:: + :link: reference/index + :link-type: doc + + :octicon:`code-square;2em;sd-text-info` Reference + ^^^ + + Documentation of the metadata specification and of the Application Progamming Interface (API), + which describe how HyperSpy functions work and which parameters can be used. + + .. grid-item-card:: + :link: auto_examples/index + :link-type: doc + + :octicon:`zap;2em;sd-text-info` Examples + ^^^ + + Gallery of short examples illustrating simple tasks that can be performed with HyperSpy. + + .. grid-item-card:: + :link: https://github.com/hyperspy/hyperspy-demos + + :octicon:`workflow;2em;sd-text-info` Tutorials + ^^^ + + Tutorials in form of Jupyter Notebooks to learn how to + process multi-dimensional data using HyperSpy. + + .. grid-item-card:: + :link: dev_guide/index + :link-type: doc + + :octicon:`people;2em;sd-text-info` Contributing + ^^^ + + HyperSpy is a community project maintained for and by its users. + There are many ways you can help! + +Citing HyperSpy +================ + +If HyperSpy has been significant to a project that leads to an academic +publication, please acknowledge that fact by citing it. The DOI in the +badge below is the `Concept DOI `_ of +HyperSpy. It can be used to cite the project without referring to a specific +version. If you are citing HyperSpy because you have used it to process data, +please use the DOI of the specific version that you have employed. You can +find iy by clicking on the DOI badge below. + +.. image:: https://zenodo.org/badge/doi/10.5281/zenodo.592838.svg + :target: https://doi.org/10.5281/zenodo.592838 + +HyperSpy's citation in the scientific literature +------------------------------------------------ + +Given the increasing number of articles that cite HyperSpy we do not maintain a list of +articles citing HyperSpy. For an up to date list search for +HyperSpy in a scientific database e.g. `Google Scholar +`_. + +.. Note:: + Articles published before 2012 may mention the HyperSpy project under + its old name, `EELSLab`. + +Credits +======= + +.. include:: ../AUTHORS.txt diff --git a/doc/make.bat b/doc/make.bat index f0bdd19e5e..67e4d9cc21 100755 --- a/doc/make.bat +++ b/doc/make.bat @@ -37,7 +37,7 @@ if "%1" == "help" ( if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* - del /q /s %BUILDDIR%\..\api\* + rmdir /q /s auto_examples goto end ) diff --git a/doc/package_lists/arch_linux_detailed_package_list.txt b/doc/package_lists/arch_linux_detailed_package_list.txt deleted file mode 100644 index 1097ed624a..0000000000 --- a/doc/package_lists/arch_linux_detailed_package_list.txt +++ /dev/null @@ -1,26 +0,0 @@ -Package Required by ----------------- -------------- -python2 hyperspy -python2-numpy hyperspy -python2-matplotlib hyperspy -python2-pip hyperspy (for installation) -python2-traits hyperspy -python2-traitsui hyperspy -python2-h5py hyperspy -python2-scikit-learn hyperspy -python2-nose hyperspy -python2-statsmodels hyperspy -python2-pillow python2-matplotlib (optional for tiff export) -python2-pyqt4 python2-matplotlib -python2-pyqt5 python2-matplotlib -python2-scipy python2-statsmodels and python2-scikit-learn -python2-pandas python2-statsmodels -python2-setuptools python2-pip (and others) - -ipython2 hyperspy -python2-jinja ipython2 (for nb) -python2-pyzmq ipython2 (for nb and qtConsole) -python2-pyqt4 ipython2 (qtConsole) -python2-tornado ipython2 (for nb) -python2-sip ipython2 (for qtConsole) -python2-pygments ipython2 (for qtConsole) \ No newline at end of file diff --git a/doc/package_lists/arch_linux_package_list.txt b/doc/package_lists/arch_linux_package_list.txt deleted file mode 100644 index 1b18a7d462..0000000000 --- a/doc/package_lists/arch_linux_package_list.txt +++ /dev/null @@ -1,23 +0,0 @@ -python2 -python2-numpy -python2-matplotlib -python2-pip -python2-traits -python2-traitsui -python2-h5py -python2-scikit-learn -python2-nose -python2-statsmodels -python2-pillow -python2-pyqt4 -python2-pyqt5 -python2-scipy -python2-pandas -python2-setuptools -ipython2 -python2-jinja -python2-pyzmq -python2-pyqt4 -python2-tornado -python2-sip -python2-pygments \ No newline at end of file diff --git a/doc/reference/api.data.rst b/doc/reference/api.data.rst new file mode 100644 index 0000000000..574363c419 --- /dev/null +++ b/doc/reference/api.data.rst @@ -0,0 +1,7 @@ +:mod:`hyperspy.api.data` +------------------------ + +.. autosummary:: + +.. automodule:: hyperspy.api.data + :members: diff --git a/doc/reference/api.model/components1D.rst b/doc/reference/api.model/components1D.rst new file mode 100644 index 0000000000..48aa2c73be --- /dev/null +++ b/doc/reference/api.model/components1D.rst @@ -0,0 +1,5 @@ +:mod:`~hyperspy.api.model.components1D` +--------------------------------------- + +.. automodule:: hyperspy.api.model.components1D + :members: \ No newline at end of file diff --git a/doc/reference/api.model/components2D.rst b/doc/reference/api.model/components2D.rst new file mode 100644 index 0000000000..d544fa285b --- /dev/null +++ b/doc/reference/api.model/components2D.rst @@ -0,0 +1,8 @@ +:mod:`~hyperspy.api.model.components2D` +--------------------------------------- + +.. currentmodule:: hyperspy.api.model + +.. automodule:: hyperspy.api.model.components2D + + .. autoclass:: Gaussian2D diff --git a/doc/reference/api.model/index.rst b/doc/reference/api.model/index.rst new file mode 100644 index 0000000000..10c26d0bbe --- /dev/null +++ b/doc/reference/api.model/index.rst @@ -0,0 +1,19 @@ +:mod:`hyperspy.api.model` +------------------------- + +.. automodule:: hyperspy.api.model + +.. currentmodule:: hyperspy.api.model + +.. autosummary:: + :nosignatures: + + components1D + components2D + +.. toctree:: + :caption: Submodules + :maxdepth: 2 + + components1D + components2D diff --git a/doc/reference/api.plot/index.rst b/doc/reference/api.plot/index.rst new file mode 100644 index 0000000000..7be5b6ed92 --- /dev/null +++ b/doc/reference/api.plot/index.rst @@ -0,0 +1,19 @@ +:mod:`hyperspy.api.plot` +------------------------ + +.. autosummary:: + hyperspy.api.plot.markers + hyperspy.api.plot.plot_histograms + hyperspy.api.plot.plot_images + hyperspy.api.plot.plot_roi_map + hyperspy.api.plot.plot_signals + hyperspy.api.plot.plot_spectra + +.. toctree:: + :caption: Submodules + :maxdepth: 2 + + markers + +.. automodule:: hyperspy.api.plot + :members: diff --git a/doc/reference/api.plot/markers.rst b/doc/reference/api.plot/markers.rst new file mode 100644 index 0000000000..b934ea262e --- /dev/null +++ b/doc/reference/api.plot/markers.rst @@ -0,0 +1,19 @@ +:mod:`hyperspy.api.plot.markers` +-------------------------------- + +.. autosummary:: + hyperspy.api.plot.markers.Arrows + hyperspy.api.plot.markers.Circles + hyperspy.api.plot.markers.Ellipses + hyperspy.api.plot.markers.HorizontalLines + hyperspy.api.plot.markers.Lines + hyperspy.api.plot.markers.Markers + hyperspy.api.plot.markers.Points + hyperspy.api.plot.markers.Polygons + hyperspy.api.plot.markers.Rectangles + hyperspy.api.plot.markers.Squares + hyperspy.api.plot.markers.Texts + hyperspy.api.plot.markers.VerticalLines + +.. automodule:: hyperspy.api.plot.markers + :members: diff --git a/doc/reference/api.roi.rst b/doc/reference/api.roi.rst new file mode 100644 index 0000000000..c0db75a089 --- /dev/null +++ b/doc/reference/api.roi.rst @@ -0,0 +1,13 @@ +:mod:`hyperspy.api.roi` +----------------------- + +.. autosummary:: + hyperspy.api.roi.CircleROI + hyperspy.api.roi.Line2DROI + hyperspy.api.roi.Point1DROI + hyperspy.api.roi.Point2DROI + hyperspy.api.roi.RectangularROI + hyperspy.api.roi.SpanROI + +.. automodule:: hyperspy.api.roi + :members: diff --git a/doc/reference/api.rst b/doc/reference/api.rst new file mode 100644 index 0000000000..ce8ccb1d51 --- /dev/null +++ b/doc/reference/api.rst @@ -0,0 +1,14 @@ +:mod:`hyperspy.api` +------------------- + +.. autosummary:: + hyperspy.api.get_configuration_directory_path + hyperspy.api.interactive + hyperspy.api.load + hyperspy.api.print_known_signal_types + hyperspy.api.set_log_level + hyperspy.api.stack + hyperspy.api.transpose + +.. automodule:: hyperspy.api + :members: diff --git a/doc/reference/api.samfire/fit_tests.rst b/doc/reference/api.samfire/fit_tests.rst new file mode 100644 index 0000000000..308f124d81 --- /dev/null +++ b/doc/reference/api.samfire/fit_tests.rst @@ -0,0 +1,13 @@ +:mod:`hyperspy.api.samfire.fit_tests` +------------------------------------- + +.. autosummary:: + hyperspy.api.samfire.fit_tests.AIC_test + hyperspy.api.samfire.fit_tests.AICc_test + hyperspy.api.samfire.fit_tests.BIC_test + hyperspy.api.samfire.fit_tests.red_chisq_test + +.. automodule:: hyperspy.api.samfire.fit_tests + :members: + :undoc-members: + :inherited-members: diff --git a/doc/reference/api.samfire/global_strategies.rst b/doc/reference/api.samfire/global_strategies.rst new file mode 100644 index 0000000000..8769c7fa29 --- /dev/null +++ b/doc/reference/api.samfire/global_strategies.rst @@ -0,0 +1,11 @@ +:mod:`hyperspy.api.samfire.global_strategies` +--------------------------------------------- + +.. autosummary:: + hyperspy.api.samfire.global_strategies.GlobalStrategy + hyperspy.api.samfire.global_strategies.HistogramStrategy + +.. automodule:: hyperspy.api.samfire.global_strategies + :members: + :inherited-members: + diff --git a/doc/reference/api.samfire/index.rst b/doc/reference/api.samfire/index.rst new file mode 100644 index 0000000000..a59da869cc --- /dev/null +++ b/doc/reference/api.samfire/index.rst @@ -0,0 +1,19 @@ +:mod:`hyperspy.api.samfire` +--------------------------- + +.. autosummary:: + hyperspy.api.samfire.fit_tests + hyperspy.api.samfire.global_strategies + hyperspy.api.samfire.local_strategies + hyperspy.api.samfire.SamfirePool + +.. toctree:: + :caption: Submodules + :maxdepth: 2 + + fit_tests + global_strategies + local_strategies + +.. automodule:: hyperspy.api.samfire + :members: diff --git a/doc/reference/api.samfire/local_strategies.rst b/doc/reference/api.samfire/local_strategies.rst new file mode 100644 index 0000000000..7b7ae52237 --- /dev/null +++ b/doc/reference/api.samfire/local_strategies.rst @@ -0,0 +1,10 @@ +:mod:`hyperspy.api.samfire.local_strategies` +--------------------------------------------- + +.. autosummary:: + hyperspy.api.samfire.local_strategies.LocalStrategy + hyperspy.api.samfire.local_strategies.ReducedChiSquaredStrategy + +.. automodule:: hyperspy.api.samfire.local_strategies + :members: + :inherited-members: diff --git a/doc/reference/api.signals/BaseSignal.rst b/doc/reference/api.signals/BaseSignal.rst new file mode 100644 index 0000000000..90536abb62 --- /dev/null +++ b/doc/reference/api.signals/BaseSignal.rst @@ -0,0 +1,11 @@ +.. currentmodule:: hyperspy.api.signals + +:class:`BaseSignal` +------------------- + +.. autoclass:: BaseSignal + :members: + :inherited-members: + + .. autoattribute:: isig + .. autoattribute:: inav diff --git a/doc/reference/api.signals/ComplexSignal.rst b/doc/reference/api.signals/ComplexSignal.rst new file mode 100644 index 0000000000..3aef59d44c --- /dev/null +++ b/doc/reference/api.signals/ComplexSignal.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy.api.signals + +:class:`ComplexSignal` +---------------------- + +.. autoclass:: ComplexSignal + :members: diff --git a/doc/reference/api.signals/ComplexSignal1D.rst b/doc/reference/api.signals/ComplexSignal1D.rst new file mode 100644 index 0000000000..54a7e2bb2c --- /dev/null +++ b/doc/reference/api.signals/ComplexSignal1D.rst @@ -0,0 +1,8 @@ +.. currentmodule:: hyperspy.api.signals + +:class:`ComplexSignal1D` +------------------------ + +.. autoclass:: ComplexSignal1D + :members: + \ No newline at end of file diff --git a/doc/reference/api.signals/ComplexSignal2D.rst b/doc/reference/api.signals/ComplexSignal2D.rst new file mode 100644 index 0000000000..e18076b413 --- /dev/null +++ b/doc/reference/api.signals/ComplexSignal2D.rst @@ -0,0 +1,8 @@ +.. currentmodule:: hyperspy.api.signals + +:class:`ComplexSignal2D` +------------------------ + +.. autoclass:: ComplexSignal2D + :members: + \ No newline at end of file diff --git a/doc/reference/api.signals/Signal1D.rst b/doc/reference/api.signals/Signal1D.rst new file mode 100644 index 0000000000..aa32055735 --- /dev/null +++ b/doc/reference/api.signals/Signal1D.rst @@ -0,0 +1,8 @@ +.. currentmodule:: hyperspy.api.signals + +:class:`Signal1D` +----------------- + +.. autoclass:: Signal1D + :members: + \ No newline at end of file diff --git a/doc/reference/api.signals/Signal2D.rst b/doc/reference/api.signals/Signal2D.rst new file mode 100644 index 0000000000..2c9d3ff18d --- /dev/null +++ b/doc/reference/api.signals/Signal2D.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy.api.signals + +:class:`Signal2D` +----------------- + +.. autoclass:: Signal2D + :members: diff --git a/doc/reference/api.signals/index.rst b/doc/reference/api.signals/index.rst new file mode 100644 index 0000000000..f4d2e96d11 --- /dev/null +++ b/doc/reference/api.signals/index.rst @@ -0,0 +1,28 @@ +:mod:`hyperspy.api.signals` +--------------------------- + +.. automodule:: hyperspy.api.signals + +.. currentmodule:: hyperspy.api.signals + +.. autosummary:: + :nosignatures: + + BaseSignal + ComplexSignal + ComplexSignal1D + ComplexSignal2D + Signal1D + Signal2D + +.. toctree:: + :caption: Submodules + :maxdepth: 2 + :hidden: + + BaseSignal + ComplexSignal + ComplexSignal1D + ComplexSignal2D + Signal1D + Signal2D \ No newline at end of file diff --git a/doc/reference/base_classes/axes.rst b/doc/reference/base_classes/axes.rst new file mode 100644 index 0000000000..2e75339cf1 --- /dev/null +++ b/doc/reference/base_classes/axes.rst @@ -0,0 +1,15 @@ +Axes +---- + +.. currentmodule:: hyperspy.axes + +.. autosummary:: + BaseDataAxis + DataAxis + FunctionalDataAxis + UniformDataAxis + AxesManager + UnitConversion + +.. automodule:: hyperspy.axes + :members: BaseDataAxis, DataAxis, FunctionalDataAxis, UniformDataAxis, AxesManager, UnitConversion diff --git a/doc/reference/base_classes/events.rst b/doc/reference/base_classes/events.rst new file mode 100644 index 0000000000..c271156ae7 --- /dev/null +++ b/doc/reference/base_classes/events.rst @@ -0,0 +1,5 @@ +Events +------ + +.. automodule:: hyperspy.events + :members: diff --git a/doc/reference/base_classes/index.rst b/doc/reference/base_classes/index.rst new file mode 100644 index 0000000000..97c3fd72b8 --- /dev/null +++ b/doc/reference/base_classes/index.rst @@ -0,0 +1,17 @@ +Base Classes +------------ + +API of classes, which are not part of the :mod:`hyperspy.api` namespace but are inherited in +HyperSpy classes. The signal classes are not expected to be instantiated by users but their methods, +which are used by other classes, are documented here. + + +.. toctree:: + :maxdepth: 2 + + axes + events + machine_learning + model/index + signal/index + roi diff --git a/doc/reference/base_classes/machine_learning.rst b/doc/reference/base_classes/machine_learning.rst new file mode 100644 index 0000000000..f392e24777 --- /dev/null +++ b/doc/reference/base_classes/machine_learning.rst @@ -0,0 +1,23 @@ +Machine Learning +---------------- + +.. automodule:: hyperspy.learn.mva + :members: LearningResults + +.. automodule:: hyperspy.learn.mlpca + :members: + +.. automodule:: hyperspy.learn.ornmf + :members: + +.. automodule:: hyperspy.learn.orthomax + :members: + +.. automodule:: hyperspy.learn.rpca + :members: + +.. automodule:: hyperspy.learn.svd_pca + :members: + +.. automodule:: hyperspy.learn.whitening + :members: diff --git a/doc/reference/base_classes/model/basemodel.rst b/doc/reference/base_classes/model/basemodel.rst new file mode 100644 index 0000000000..415d29fb2c --- /dev/null +++ b/doc/reference/base_classes/model/basemodel.rst @@ -0,0 +1,11 @@ + +BaseModel +""""""""" + +.. currentmodule:: hyperspy.model + +.. autoclass:: BaseModel + :members: + +.. autoclass:: ModelComponents + :members: diff --git a/doc/reference/base_classes/model/component.rst b/doc/reference/base_classes/model/component.rst new file mode 100644 index 0000000000..b7424bcfee --- /dev/null +++ b/doc/reference/base_classes/model/component.rst @@ -0,0 +1,7 @@ +Component +""""""""" + +.. currentmodule:: hyperspy.component + +.. autoclass:: Component + :members: diff --git a/doc/reference/base_classes/model/index.rst b/doc/reference/base_classes/model/index.rst new file mode 100644 index 0000000000..a6dd600c8a --- /dev/null +++ b/doc/reference/base_classes/model/index.rst @@ -0,0 +1,12 @@ +Model +----- + +.. toctree:: + :maxdepth: 2 + + basemodel + model1d + model2d + component + parameter + samfire diff --git a/doc/reference/base_classes/model/model1d.rst b/doc/reference/base_classes/model/model1d.rst new file mode 100644 index 0000000000..e34723dc19 --- /dev/null +++ b/doc/reference/base_classes/model/model1d.rst @@ -0,0 +1,6 @@ + +Model1D +""""""" + +.. automodule:: hyperspy.models.model1d + :members: Model1D diff --git a/doc/reference/base_classes/model/model2d.rst b/doc/reference/base_classes/model/model2d.rst new file mode 100644 index 0000000000..ff50855ec7 --- /dev/null +++ b/doc/reference/base_classes/model/model2d.rst @@ -0,0 +1,6 @@ + +Model2D +""""""" + +.. automodule:: hyperspy.models.model2d + :members: diff --git a/doc/reference/base_classes/model/parameter.rst b/doc/reference/base_classes/model/parameter.rst new file mode 100644 index 0000000000..0e97cbbd4f --- /dev/null +++ b/doc/reference/base_classes/model/parameter.rst @@ -0,0 +1,11 @@ +Parameter +""""""""" + +.. currentmodule:: hyperspy.component + +.. autoclass:: Parameter + :members: + + .. autoattribute:: value + .. autoattribute:: free + .. autoattribute:: map diff --git a/doc/reference/base_classes/model/samfire.rst b/doc/reference/base_classes/model/samfire.rst new file mode 100644 index 0000000000..7350697760 --- /dev/null +++ b/doc/reference/base_classes/model/samfire.rst @@ -0,0 +1,14 @@ + +SamFire +""""""" + +.. currentmodule:: hyperspy.samfire + +.. autoclass:: Samfire + :members: + + +.. currentmodule:: hyperspy.utils.parallel_pool + +.. autoclass:: ParallelPool + :members: \ No newline at end of file diff --git a/doc/reference/base_classes/roi.rst b/doc/reference/base_classes/roi.rst new file mode 100644 index 0000000000..5317e5b0be --- /dev/null +++ b/doc/reference/base_classes/roi.rst @@ -0,0 +1,11 @@ +ROI +--- + +.. currentmodule:: hyperspy.roi + +.. autosummary:: + BaseROI + BaseInteractiveROI + +.. automodule:: hyperspy.roi + :members: BaseROI, BaseInteractiveROI diff --git a/doc/reference/base_classes/signal/CommonSignal1D.rst b/doc/reference/base_classes/signal/CommonSignal1D.rst new file mode 100644 index 0000000000..ec2cf5e188 --- /dev/null +++ b/doc/reference/base_classes/signal/CommonSignal1D.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._signals.common_signal1d + +:class:`CommonSignal1D` +----------------------- + +.. autoclass:: CommonSignal1D + :members: diff --git a/doc/reference/base_classes/signal/CommonSignal2D.rst b/doc/reference/base_classes/signal/CommonSignal2D.rst new file mode 100644 index 0000000000..0e356ce3bd --- /dev/null +++ b/doc/reference/base_classes/signal/CommonSignal2D.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._signals.common_signal2d + +:class:`CommonSignal2D` +----------------------- + +.. autoclass:: CommonSignal2D + :members: \ No newline at end of file diff --git a/doc/reference/base_classes/signal/LazyComplexSignal.rst b/doc/reference/base_classes/signal/LazyComplexSignal.rst new file mode 100644 index 0000000000..94280a821a --- /dev/null +++ b/doc/reference/base_classes/signal/LazyComplexSignal.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._lazy_signals + +:class:`LazyComplexSignal` +-------------------------- + +.. autoclass:: LazyComplexSignal + :members: diff --git a/doc/reference/base_classes/signal/LazyComplexSignal1D.rst b/doc/reference/base_classes/signal/LazyComplexSignal1D.rst new file mode 100644 index 0000000000..7377c017f2 --- /dev/null +++ b/doc/reference/base_classes/signal/LazyComplexSignal1D.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._lazy_signals + +:class:`LazyComplexSignal1D` +---------------------------- + +.. autoclass:: LazyComplexSignal1D + :members: diff --git a/doc/reference/base_classes/signal/LazyComplexSignal2D.rst b/doc/reference/base_classes/signal/LazyComplexSignal2D.rst new file mode 100644 index 0000000000..c852cdadca --- /dev/null +++ b/doc/reference/base_classes/signal/LazyComplexSignal2D.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._lazy_signals + +:class:`LazyComplexSignal2D` +---------------------------- + +.. autoclass:: LazyComplexSignal2D + :members: diff --git a/doc/reference/base_classes/signal/LazySignal.rst b/doc/reference/base_classes/signal/LazySignal.rst new file mode 100644 index 0000000000..c37ea6bb93 --- /dev/null +++ b/doc/reference/base_classes/signal/LazySignal.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._signals.lazy + +:class:`LazySignal` +------------------- + +.. autoclass:: LazySignal + :members: diff --git a/doc/reference/base_classes/signal/LazySignal1D.rst b/doc/reference/base_classes/signal/LazySignal1D.rst new file mode 100644 index 0000000000..c2ee25b63b --- /dev/null +++ b/doc/reference/base_classes/signal/LazySignal1D.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._lazy_signals + +:class:`LazySignal1D` +--------------------- + +.. autoclass:: LazySignal1D + :members: diff --git a/doc/reference/base_classes/signal/LazySignal2D.rst b/doc/reference/base_classes/signal/LazySignal2D.rst new file mode 100644 index 0000000000..e3578b4a6f --- /dev/null +++ b/doc/reference/base_classes/signal/LazySignal2D.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy._lazy_signals + +:class:`LazySignal2D` +--------------------- + +.. autoclass:: LazySignal2D + :members: diff --git a/doc/reference/base_classes/signal/ModelManager.rst b/doc/reference/base_classes/signal/ModelManager.rst new file mode 100644 index 0000000000..a0553320b5 --- /dev/null +++ b/doc/reference/base_classes/signal/ModelManager.rst @@ -0,0 +1,7 @@ +.. currentmodule:: hyperspy.signal + +:class:`ModelManager` +--------------------- + +.. autoclass:: ModelManager + :members: diff --git a/doc/reference/base_classes/signal/index.rst b/doc/reference/base_classes/signal/index.rst new file mode 100644 index 0000000000..3690a24cf1 --- /dev/null +++ b/doc/reference/base_classes/signal/index.rst @@ -0,0 +1,69 @@ +Signal +------ + +API of signal classes, which are not part of the user-facing :mod:`hyperspy.api` namespace but are +inherited in HyperSpy signals classes or used as attributes of signals. These classes are not +expected to be instantiated by users but their methods, which are used by other classes, +are documented here. + +ModelManager +^^^^^^^^^^^^ + +.. currentmodule:: hyperspy.signal + +.. autosummary:: + :nosignatures: + + ModelManager + +Common Signals +^^^^^^^^^^^^^^ + +.. currentmodule:: hyperspy._signals.common_signal1d + +.. autosummary:: + :nosignatures: + + CommonSignal1D + +.. currentmodule:: hyperspy._signals.common_signal2d + +.. autosummary:: + :nosignatures: + + CommonSignal2D + + +Lazy Signals +^^^^^^^^^^^^ +.. currentmodule:: hyperspy._signals.lazy + +.. autosummary:: + :nosignatures: + + LazySignal + +.. currentmodule:: hyperspy._lazy_signals + +.. autosummary:: + :nosignatures: + + LazyComplexSignal + LazyComplexSignal1D + LazyComplexSignal2D + LazySignal1D + LazySignal2D + +.. toctree:: + :maxdepth: 2 + :hidden: + + ModelManager + CommonSignal1D + CommonSignal2D + LazyComplexSignal + LazyComplexSignal1D + LazyComplexSignal2D + LazySignal + LazySignal1D + LazySignal2D diff --git a/doc/reference/index.rst b/doc/reference/index.rst new file mode 100644 index 0000000000..ed268d788a --- /dev/null +++ b/doc/reference/index.rst @@ -0,0 +1,26 @@ + +.. _reference: + +######### +Reference +######### + +.. toctree:: + :maxdepth: 2 + :caption: Metadata + + metadata + +.. toctree:: + :caption: API Reference + :maxdepth: 2 + + api + api.data + api.model/index + api.plot/index + api.roi + api.samfire/index + api.signals/index + base_classes/index + utils/index diff --git a/doc/reference/metadata.rst b/doc/reference/metadata.rst new file mode 100644 index 0000000000..95fa0fe4aa --- /dev/null +++ b/doc/reference/metadata.rst @@ -0,0 +1,357 @@ +.. _metadata_structure: + + +Metadata structure +****************** + +The :class:`~.api.signals.BaseSignal` class stores metadata in the +:attr:`~.api.signals.BaseSignal.metadata` attribute, which has a tree structure. By +convention, the node labels are capitalized and the leaves are not +capitalized. + +When a leaf contains a quantity that is not dimensionless, the units can be +given in an extra leaf with the same label followed by the "_units" suffix. +For example, an "energy" leaf should be accompanied by an "energy_units" leaf. + +The metadata structure is represented in the following tree diagram. The +default units are given in parentheses. Details about the leaves can be found +in the following sections of this chapter. + +:: + + metadata + ├── General + | |── FileIO + | | ├── 0 + | | | ├── operation + | | | ├── hyperspy_version + | | | ├── io_plugin + | │ | └── timestamp + | | ├── 1 + | | | ├── operation + | | | ├── hyperspy_version + | | | ├── io_plugin + | │ | └── timestamp + | | └── ... + │ ├── authors + │ ├── date + │ ├── doi + │ ├── original_filename + │ ├── notes + │ ├── time + │ ├── time_zone + │ └── title + ├── Sample + │ ├── credits + │ ├── description + │ └── thickness + └── Signal + ├── FFT + │ └── shifted + ├── Noise_properties + │ ├── Variance_linear_model + │ │ ├── correlation_factor + │ │ ├── gain_factor + │ │ ├── gain_offset + │ │ └── parameters_estimation_method + │ └── variance + ├── quantity + ├── signal_type + └── signal_origin + + +.. _general-metadata: + +General +======= + +title + type: Str + + A title for the signal, e.g. "Sample overview" + +original_filename + type: Str + + If the signal was loaded from a file this key stores the name of the + original file. + +time_zone + type: Str + + The time zone as supported by the python-dateutil library, e.g. "UTC", + "Europe/London", etc. It can also be a time offset, e.g. "+03:00" or + "-05:00". + +time + type: Str + + The acquisition or creation time in ISO 8601 time format, e.g. '13:29:10'. + +date + type: Str + + The acquisition or creation date in ISO 8601 date format, e.g. + '2018-01-28'. + + +authors + type: Str + + The authors of the data, in Latex format: Surname1, Name1 and Surname2, + Name2, etc. + +doi + type: Str + + Digital object identifier of the data, e. g. doi:10.5281/zenodo.58841. + +notes + type: Str + + Notes about the data. + +.. _general-file-metadata: + +FileIO +------ + +Contains information about the software packages and versions used any time the +Signal was created by reading the original data format (added in HyperSpy +v1.7) or saved by one of HyperSpy's IO tools. If the signal is saved to one +of the ``hspy``, ``zspy`` or ``nxs`` formats, the metadata within the ``FileIO`` +node will represent a history of the software configurations used when the +conversion was made from the proprietary/original format to HyperSpy's +format, as well as any time the signal was subsequently loaded from and saved +to disk. Under the ``FileIO`` node will be one or more nodes named ``0``, +``1``, ``2``, etc., each with the following structure: + +operation + type: Str + + This value will be either ``"load"`` or ``"save"`` to indicate whether + this node represents a load from, or save to disk operation, respectively. + +hyperspy_version + type: Str + + The version number of the HyperSpy software used to extract a Signal from + this data file or save this Signal to disk + +io_plugin + type: Str + + The specific input/output plugin used to originally extract this data file + into a HyperSpy Signal or save it to disk -- will be of the form + ``rsciio.``. + +timestamp + type: Str + + The timestamp of the computer running the data loading/saving process (in a + timezone-aware format). The timestamp will be in ISO 8601 format, as + produced by the :meth:`datetime.date.isoformat`. + + +.. _sample-metadata: + +Sample +====== + +credits + type: Str + + Acknowledgment of sample supplier, e.g. Prepared by Putin, Vladimir V. + +description + type: Str + + A brief description of the sample + +thickness + type: Float + + The thickness of the sample in m. + + +.. _signal-metadata: + +Signal +====== + +signal_type + type: Str + + A term that describes the signal type, e.g. EDS, PES... This information + can be used by HyperSpy to load the file as a specific signal class and + therefore the naming should be standardised. Currently, HyperSpy provides + special signal class for photoemission spectroscopy, electron energy + loss spectroscopy and energy dispersive spectroscopy. The signal_type in + these cases should be respectively PES, EELS and EDS_TEM (EDS_SEM). + +signal_origin + type: Str + + Describes the origin of the signal e.g. 'simulation' or 'experiment'. + + +record_by + .. deprecated:: 1.2 + + type: Str + + One of 'spectrum' or 'image'. It describes how the data is stored in memory. + If 'spectrum', the spectral data is stored in the faster index. + +quantity + type: Str + + The name of the quantity of the "intensity axis" with the units in round + brackets if required, for example Temperature (K). + + +FFT +--- + +shifted + type: bool. + + Specify if the FFT has the zero-frequency component shifted to the center of + the signal. + + +Noise_properties +---------------- + +variance + type: float or BaseSignal instance. + + The variance of the data. It can be a float when the noise is Gaussian or a + :class:`~.api.signals.BaseSignal` instance if the noise is heteroscedastic, + in which case it must have the same dimensions as + :attr:`~.api.signals.BaseSignal.data`. + +Variance_linear_model +^^^^^^^^^^^^^^^^^^^^^ + +In some cases the variance can be calculated from the data using a simple +linear model: ``variance = (gain_factor * data + gain_offset) * +correlation_factor``. + +gain_factor + type: Float + +gain_offset + type: Float + +correlation_factor + type: Float + +parameters_estimation_method + type: Str + + +_Internal_parameters +==================== + +This node is "private" and therefore is not displayed when printing the +:attr:`~.api.signals.BaseSignal.metadata` attribute. + +Stacking_history +---------------- + +Generated when using :func:`~.api.stack`. Used by +:meth:`~.api.signals.BaseSignal.split`, to retrieve the former list of signal. + +step_sizes + type: list of int + + Step sizes used that can be used in split. + +axis + type: int + + The axis index in axes manager on which the dataset were stacked. + +Folding +------- + +Constains parameters that related to the folding/unfolding of signals. + + +.. _metadata_handling: + +Functions to handle the metadata +================================ + +Existing nodes can be directly read out or set by adding the path in the +metadata tree: + +:: + + s.metadata.General.title = 'FlyingCircus' + s.metadata.General.title + + +The following functions can operate on the metadata tree. An example with the +same functionality as the above would be: + +:: + + s.metadata.set_item('General.title', 'FlyingCircus') + s.metadata.get_item('General.title') + + +Adding items +------------ + +:meth:`~.misc.utils.DictionaryTreeBrowser.set_item` + Given a ``path`` and ``value``, easily set metadata items, creating any + necessary nodes on the way. + +:meth:`~.misc.utils.DictionaryTreeBrowser.add_dictionary` + Add new items from a given ``dictionary``. + + +Output metadata +--------------- + +:meth:`~.misc.utils.DictionaryTreeBrowser.get_item` + Given an ``item_path``, return the ``value`` of the metadata item. + +:meth:`~.misc.utils.DictionaryTreeBrowser.as_dictionary` + Returns a dictionary representation of the metadata tree. + +:meth:`~.misc.utils.DictionaryTreeBrowser.export` + Saves the metadata tree in pretty tree printing format in a text file. + Takes ``filename`` as parameter. + + +Searching for keys +------------------ + +:meth:`~.misc.utils.DictionaryTreeBrowser.has_item` + Given an ``item_path``, returns ``True`` if the item exists anywhere + in the metadata tree. + +Using the option ``full_path=False``, the functions +:meth:`~.misc.utils.DictionaryTreeBrowser.has_item` and +:meth:`~.misc.utils.DictionaryTreeBrowser.get_item` can also find items by +their key in the metadata when the exact path is not known. By default, only +an exact match of the search string with the item key counts. The additional +setting ``wild=True`` allows to search for a case-insensitive substring of the +item key. The search functionality also accepts item keys preceded by one or +several nodes of the path (separated by the usual full stop). + +:meth:`~.misc.utils.DictionaryTreeBrowser.has_item` + For ``full_path=False``, given a ``item_key``, returns ``True`` if the item + exists anywhere in the metadata tree. + +:meth:`~.misc.utils.DictionaryTreeBrowser.has_item` + For ``full_path=False, return_path=True``, returns the path or list of + paths to any matching item(s). + +:meth:`~.misc.utils.DictionaryTreeBrowser.get_item` + For ``full_path=False``, returns the value or list of values for any + matching item(s). Setting ``return_path=True``, a tuple (value, path) is + returned -- or lists of tuples for multiple occurences. diff --git a/doc/reference/utils/index.rst b/doc/reference/utils/index.rst new file mode 100644 index 0000000000..4732182581 --- /dev/null +++ b/doc/reference/utils/index.rst @@ -0,0 +1,24 @@ +Utils +----- + +API of classes, which are not part of the :mod:`hyperspy.api` namespace but used by HyperSpy +signals. + + +.. currentmodule:: hyperspy.misc.utils + +.. autoclass:: DictionaryTreeBrowser + :members: + +.. autoclass:: TupleSA + :members: + +.. currentmodule:: hyperspy.misc.export_dictionary + +.. autofunction:: export_to_dictionary + +:mod:`~hyperspy.utils.peakfinders2D` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: hyperspy.utils.peakfinders2D + :members: diff --git a/doc/user_guide/axes.rst b/doc/user_guide/axes.rst index 9c4d70ed76..cbb8c170be 100644 --- a/doc/user_guide/axes.rst +++ b/doc/user_guide/axes.rst @@ -1,6 +1,9 @@ +.. _axes-handling: + Axes handling ************* +.. _dimensions-label: The navigation and signal dimensions ------------------------------------ @@ -10,7 +13,7 @@ functions operate on the *signal* axes and iterate over the *navigation* axes. Take an EELS spectrum image as specific example. It is a 2D array of spectra and has three dimensions: X, Y and energy-loss. In HyperSpy, X and Y are the *navigation* dimensions and the energy-loss is the *signal* dimension. To make -this distinction more explicit, the representation of the object includes a +this distinction more explicit, the representation of the object includes a separator ``|`` between the navigation and signal dimensions. In analogy, the *signal* dimension in EDX would be the X-ray energy, in optical spectra the wavelength axis, etc. However, HyperSpy can also handle data with more than one @@ -52,10 +55,10 @@ in the Signal2D subclass. Setting axis properties ----------------------- -The axes are managed and stored by the :py:class:`~.axes.AxesManager` class -that is stored in the :py:attr:`~.signal.BaseSignal.axes_manager` attribute of +The axes are managed and stored by the :class:`~.axes.AxesManager` class +that is stored in the :attr:`~.api.signals.BaseSignal.axes_manager` attribute of the signal class. The individual axes can be accessed by indexing the -:py:class:`~.axes.AxesManager`, e.g.: +:class:`~.axes.AxesManager`, e.g.: .. code-block:: python @@ -63,12 +66,17 @@ the signal class. The individual axes can be accessed by indexing the >>> s >>> s.axes_manager - , |)> + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + | 20 | 0 | 0 | 1 | + | 10 | 0 | 0 | 1 | + ---------------- | ------ | ------ | ------- | ------- | ------ + | 100 | 0 | 0 | 1 | >>> s.axes_manager[0] -The navigation axes come first, followed by the signal axes. Alternatively, +The navigation axes come first, followed by the signal axes. Alternatively, it is possible to selectively access the navigation or signal dimensions: .. code-block:: python @@ -84,10 +92,13 @@ following commands will access the same axis: .. code-block:: python >>> s.axes_manager[2] + >>> s.axes_manager[-1] + >>> s.axes_manager.signal_axes[0] + -The axis properties can be set by setting the :py:class:`~.axes.BaseDataAxis` +The axis properties can be set by setting the :class:`~.axes.BaseDataAxis` attributes, e.g.: .. code-block:: python @@ -96,6 +107,20 @@ attributes, e.g.: >>> s.axes_manager[0] +.. versionadded:: 2.2 + :meth:`~.misc.utils.TupleSA.set` and :meth:`~.misc.utils.TupleSA.get` methods for :attr:`~.axes.AxesManager.navigation_axes` + and :attr:`~.axes.AxesManager.signal_axes`. + +It is also possible to set multiple attributes of multiple axes at once, using the :meth:`~.misc.utils.TupleSA.set` +of the :attr:`~.axes.AxesManager.navigation_axes` and :attr:`~.axes.AxesManager.signal_axes` attributes. +The :meth:`~.misc.utils.TupleSA.get` returns a dictionary of the attributes. For example: + +.. code-block:: python + + >>> s.axes_manager.navigation_axes.set(name=("X", "Y"), offset=10, units="nm") + >>> s.axes_manager.navigation_axes.get("name", "offset", "units") + {"name" : ("X", "Y"), "offset" : (10, 10), "units" : ("nm", "nm")} + Once the name of an axis has been defined it is possible to request it by its name e.g.: @@ -109,12 +134,13 @@ name e.g.: >>> s.axes_manager["X"].offset = 100 + It is also possible to set the axes properties using a GUI by calling the -:py:meth:`~.axes.AxesManager.gui` method of the :py:class:`~.axes.AxesManager` +:meth:`~.axes.AxesManager.gui` method of the :class:`~.axes.AxesManager` .. code-block:: python - >>> s.axes_manager.gui() + >>> s.axes_manager.gui() # doctest: +SKIP .. _axes_manager_gui_image: @@ -124,11 +150,11 @@ It is also possible to set the axes properties using a GUI by calling the AxesManager ipywidgets GUI. or, for a specific axis, the respective method of e.g. -:py:class:`~.axes.UniformDataAxis`: +:class:`~.axes.UniformDataAxis`: .. code-block:: python - >>> s.axes_manager["X"].gui() + >>> s.axes_manager["X"].gui() # doctest: +SKIP .. _data_axis_gui_image: @@ -142,7 +168,7 @@ axes) you could use the navigation sliders: .. code-block:: python - >>> s.axes_manager.gui_navigation_sliders() + >>> s.axes_manager.gui_navigation_sliders() # doctest: +SKIP .. _navigation_sliders_image: @@ -153,7 +179,7 @@ axes) you could use the navigation sliders: Alternatively, the "current position" can be changed programmatically by directly accessing the ``indices`` attribute of a signal's -:py:class:`~.axes.AxesManager` or the ``index`` attribute of an individual +:class:`~.axes.AxesManager` or the ``index`` attribute of an individual axis. This is particularly useful when trying to set a specific location at which to initialize a model's parameters to sensible values before performing a fit over an entire spectrum image. The @@ -164,6 +190,7 @@ navigation dimensions: >>> s.axes_manager.indices = (5, 4) +.. _Axes_properties: Summary of axis properties ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -189,11 +216,11 @@ Types of data axes ------------------ HyperSpy supports different *data axis types*, which differ in how the axis is -defined: +defined: -* :py:class:`~.axes.DataAxis` defined by an array ``axis``, -* :py:class:`~.axes.FunctionalDataAxis` defined by a function ``expression`` or -* :py:class:`~.axes.UniformDataAxis` defined by the initial value ``offset`` +* :class:`~.axes.DataAxis` defined by an array ``axis``, +* :class:`~.axes.FunctionalDataAxis` defined by a function ``expression`` or +* :class:`~.axes.UniformDataAxis` defined by the initial value ``offset`` and spacing ``scale``. The main disambiguation is whether the @@ -202,7 +229,7 @@ axis is **uniform**, where the data points are equidistantly spaced, or when, e.g., a spectrum recorded over a *wavelength* axis is converted to a *wavenumber* or *energy* scale, where the conversion is based on a ``1/x`` dependence so that the axis spacing of the new axis varies along the length -of the axis. Whether an axis is uniform or not can be queried through the +of the axis. Whether an axis is uniform or not can be queried through the property ``is_uniform`` (bool) of the axis. Every axis of a signal object may be of a different type. For example, it is @@ -215,62 +242,67 @@ following table. .. table:: BaseDataAxis subclasses. - +-------------------------------------------------------------------+------------------------+-------------+ - | BaseDataAxis subclass | defining attributes | is_uniform | - +===================================================================+========================+=============+ - | :py:class:`~.axes.DataAxis` | axis | False | - +-------------------------------------------------------------------+------------------------+-------------+ - | :py:class:`~.axes.FunctionalDataAxis` | expression | False | - +-------------------------------------------------------------------+------------------------+-------------+ - | :py:class:`~.axes.UniformDataAxis` | offset, scale | True | - +-------------------------------------------------------------------+------------------------+-------------+ + +----------------------------------------+------------------------+----------------+ + | BaseDataAxis subclass | Defining attributes | ``is_uniform`` | + +========================================+========================+================+ + | :class:`~.axes.DataAxis` | axis | False | + +----------------------------------------+------------------------+----------------+ + | :class:`~.axes.FunctionalDataAxis` | expression | False | + +----------------------------------------+------------------------+----------------+ + | :class:`~.axes.UniformDataAxis` | offset, scale | True | + +----------------------------------------+------------------------+----------------+ .. NOTE:: Not all features are implemented for non-uniform axes. +.. warning:: + + Non-uniform axes are in beta state and its API may change in a minor release. + Not all hyperspy features are compatible with non-uniform axes and support + will be added in future releases. + + +.. _uniform-data-axis: + Uniform data axis ^^^^^^^^^^^^^^^^^ -The most common case is the :py:class:`~.axes.UniformDataAxis`. Here, the axis +The most common case is the :class:`~.axes.UniformDataAxis`. Here, the axis is defined by the ``offset``, ``scale`` and ``size`` parameters, which determine the `initial value`, `spacing` and `length`, respectively. The actual ``axis`` array is automatically calculated from these three values. The ``UniformDataAxis`` is a special case of the ``FunctionalDataAxis`` defined by the function ``scale * x + offset``. -Sample dictionary for a :py:class:`~.axes.UniformDataAxis`: +Sample dictionary for a :class:`~.axes.UniformDataAxis`: .. code-block:: python >>> dict0 = {'offset': 300, 'scale': 1, 'size': 500} >>> s = hs.signals.Signal1D(np.ones(500), axes=[dict0]) >>> s.axes_manager[0].get_axis_dictionary() - {'_type': 'UniformDataAxis', - 'name': , - 'units': , - 'navigate': False, - 'size': 500, - 'scale': 1.0, - 'offset': 300.0} + {'_type': 'UniformDataAxis', 'name': None, 'units': None, 'navigate': False, 'is_binned': False, 'size': 500, 'scale': 1.0, 'offset': 300.0} -Corresponding output of :py:class:`~.axes.AxesManager`: +Corresponding output of :class:`~.axes.AxesManager`: .. code-block:: python >>> s.axes_manager - < Axes manager, axes: (|500) > - Name | size | offset | scale | units - ================ | ====== | ======= | ======= | ====== - ---------------- | ------ | ------- | ------- | ------ - | 500 | 300 | 1 | + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + | 500 | 0 | 3e+02 | 1 | +.. _functional-data-axis: + Functional data axis ^^^^^^^^^^^^^^^^^^^^ -Alternatively, a :py:class:`~.axes.FunctionalDataAxis` is defined based on an +Alternatively, a :class:`~.axes.FunctionalDataAxis` is defined based on an ``expression`` that is evaluated to yield the axis points. The `expression` is a function defined as a ``string`` using the `SymPy `_ text expression @@ -279,45 +311,31 @@ expression, in this case ``a`` and ``b`` must be defined as additional attributes of the axis. The property ``is_uniform`` is automatically set to ``False``. -``x`` itself is an instance of :py:class:`~.axes.BaseDataAxis`. By default, -it will be a :py:class:`~.axes.UniformDataAxis` with ``offset = 0`` and +``x`` itself is an instance of :class:`~.axes.BaseDataAxis`. By default, +it will be a :class:`~.axes.UniformDataAxis` with ``offset = 0`` and ``scale = 1`` of the given ``size``. However, it can also be initialized with custom ``offset`` and ``scale`` values. Alternatively, it can be a non -uniform :py:class:`~.axes.DataAxis`. +uniform :class:`~.axes.DataAxis`. -Sample dictionary for a :py:class:`~.axes.FunctionalDataAxis`: +Sample dictionary for a :class:`~.axes.FunctionalDataAxis`: .. code-block:: python >>> dict0 = {'expression': 'a / (x + 1) + b', 'a': 100, 'b': 10, 'size': 500} >>> s = hs.signals.Signal1D(np.ones(500), axes=[dict0]) >>> s.axes_manager[0].get_axis_dictionary() - {'_type': 'FunctionalDataAxis', - 'name': , - 'units': , - 'navigate': False, - 'expression': 'a / (x + 1) + b', - 'size': 500, - 'x': {'_type': 'UniformDataAxis', - 'name': , - 'units': , - 'navigate': , - 'size': 500, - 'scale': 1.0, - 'offset': 0.0}, - 'a': 100, - 'b': 10} - -Corresponding output of :py:class:`~.axes.AxesManager`: + {'_type': 'FunctionalDataAxis', 'name': None, 'units': None, 'navigate': False, 'is_binned': False, 'expression': 'a / (x + 1) + b', 'size': 500, 'x': {'_type': 'UniformDataAxis', 'name': None, 'units': None, 'navigate': False, 'is_binned': False, 'size': 500, 'scale': 1.0, 'offset': 0.0}, 'a': 100, 'b': 10} + +Corresponding output of :class:`~.axes.AxesManager`: .. code-block:: python >>> s.axes_manager - < Axes manager, axes: (|1000) > - Name | size | offset | scale | units - ================ | ====== | ================ | ================ | ====== - ---------------- | ------ | ---------------- | ---------------- | ------ - | 500 | non-uniform axis | non-uniform axis | + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + | 500 | 0 | non-uniform axis | Initializing ``x`` with ``offset`` and ``scale``: @@ -336,7 +354,7 @@ Initializing ``x`` with ``offset`` and ``scale``: 19.52380952, 19.43396226, 19.34579439, 19.25925926, 19.17431193]) -Initializing ``x`` as non-uniform :py:class:`~.axes.DataAxis`: +Initializing ``x`` as non-uniform :class:`~.axes.DataAxis`: .. code-block:: python @@ -349,45 +367,45 @@ Initializing ``x`` as non-uniform :py:class:`~.axes.DataAxis`: >>> # the actual axis array >>> s.axes_manager[0].axis array([110. , 35. , 21.11111111, 16.25 , - 14. , 12.77777778, 12.04081633, 11.5625 , - 11.2345679 ]) + 14. , 12.77777778, 12.04081633, 11.5625 , + 11.2345679 ]) Initializing ``x`` with ``offset`` and ``scale``: +.. _data-axis: + (non-uniform) Data axis ^^^^^^^^^^^^^^^^^^^^^^^ -A :py:class:`~.axes.DataAxis` is the most flexible type of axis. The axis +A :class:`~.axes.DataAxis` is the most flexible type of axis. The axis points are directly given by an array named ``axis``. As this can be any array, the property ``is_uniform`` is automatically set to ``False``. -Sample dictionary for a :py:class:`~.axes.DataAxis`: +Sample dictionary for a :class:`~.axes.DataAxis`: .. code-block:: python >>> dict0 = {'axis': np.arange(12)**2} >>> s = hs.signals.Signal1D(np.ones(12), axes=[dict0]) >>> s.axes_manager[0].get_axis_dictionary() - {'_type': 'DataAxis', - 'name': , - 'units': , - 'navigate': False, - 'axis': array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121])} + {'_type': 'DataAxis', 'name': None, 'units': None, 'navigate': False, 'is_binned': False, 'axis': array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121])} -Corresponding output of :py:class:`~.axes.AxesManager`: +Corresponding output of :class:`~.axes.AxesManager`: .. code-block:: python >>> s.axes_manager - < Axes manager, axes: (|1000) > - Name | size | offset | scale | units - ================ | ====== | ================ | ================ | ====== - ---------------- | ------ | ---------------- | ---------------- | ------ - | 12 | non-uniform axis | non-uniform axis | + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + | 12 | 0 | non-uniform axis | +.. _defining-axes: + Defining a new axis ------------------- @@ -400,7 +418,7 @@ automatically determines the type of axis by the given attributes: >>> axis = axes.create_axis(offset=10,scale=0.5,size=20) >>> axis - + Alternatively, the creator of the different types of axes can be called directly: @@ -410,20 +428,14 @@ directly: >>> axis = axes.UniformDataAxis(offset=10,scale=0.5,size=20) >>> axis - + The dictionary defining the axis is returned by the ``get_axis_dictionary()`` method: .. code-block:: python >>> axis.get_axis_dictionary() - {'_type': 'UniformDataAxis', - 'name': , - 'units': , - 'navigate': , - 'size': 20, - 'scale': 0.5, - 'offset': 10.0} + {'_type': 'UniformDataAxis', 'name': None, 'units': None, 'navigate': False, 'is_binned': False, 'size': 20, 'scale': 0.5, 'offset': 10.0} This dictionary can be used, for example, in the :ref:`initilization of a new signal`. @@ -432,16 +444,16 @@ signal`. Adding/Removing axes to/from a signal ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Usually, the axes are directly added to a signal during :ref:`signal +Usually, the axes are directly added to a signal during :ref:`signal initialization`. However, you may wish to add/remove -axes from the :py:class:`~.axes.AxesManager` of a signal. +axes from the :class:`~.axes.AxesManager` of a signal. Note that there is currently no consistency check whether a signal object has the right number of axes of the right dimensions. Most functions will however -fail if you pass a signal object where the axes do not match the data +fail if you pass a signal object where the axes do not match the data dimensions and shape. -You can *add a set of axes* to the :py:class:`~.axes.AxesManager` by passing either a list of +You can *add a set of axes* to the :class:`~.axes.AxesManager` by passing either a list of axes dictionaries to ``axes_manager.create_axes()``: .. code-block:: python @@ -459,7 +471,7 @@ or a list of axes objects: >>> axis1 = DataAxis(axis=np.arange(12)**2) >>> s.axes_manager.create_axes([axis0,axis1]) -*Remove an axis* from the :py:class:`~.axes.AxesManager` using ``remove()``, e.g. for the last axis: +*Remove an axis* from the :class:`~.axes.AxesManager` using ``remove()``, e.g. for the last axis: .. code-block:: python @@ -471,30 +483,25 @@ or a list of axes objects: Using quantity and converting units ----------------------------------- -The ``scale`` and the ``offset`` of each :py:class:`~.axes.UniformDataAxis` axis +The ``scale`` and the ``offset`` of each :class:`~.axes.UniformDataAxis` axis can be set and retrieved as quantity. .. code-block:: python >>> s = hs.signals.Signal1D(np.arange(10)) >>> s.axes_manager[0].scale_as_quantity - 1.0 dimensionless + >>> s.axes_manager[0].scale_as_quantity = '2.5 µm' >>> s.axes_manager Name | size | index | offset | scale | units ================ | ====== | ====== | ======= | ======= | ====== ---------------- | ------ | ------ | ------- | ------- | ------ - | 10 | | 0 | 2.5 | µm + | 10 | 0 | 0 | 2.5 | µm >>> s.axes_manager[0].offset_as_quantity = '2.5 nm' - - Name | size | index | offset | scale | units - ================ | ====== | ====== | ======= | ======= | ====== - ---------------- | ------ | ------ | ------- | ------- | ------ - | 10 | | 2.5 | 2.5e+03 | nm -Internally, HyperSpy uses the `pint `_ library to +Internally, HyperSpy uses the `pint `_ library to manage the scale and offset quantities. The ``scale_as_quantity`` and ``offset_as_quantity`` attributes return pint object: @@ -502,26 +509,26 @@ manage the scale and offset quantities. The ``scale_as_quantity`` and >>> q = s.axes_manager[0].offset_as_quantity >>> type(q) # q is a pint quantity object - pint.quantity.build_quantity_class..Quantity + >>> q - 2.5 nanometer + -The ``convert_units`` method of the :py:class:`~.axes.AxesManager` converts +The ``convert_units`` method of the :class:`~.axes.AxesManager` converts units, which by default (no parameters provided) converts all axis units to an optimal unit to avoid using too large or small numbers. Each axis can also be converted individually using the ``convert_to_units`` -method of the :py:class:`~.axes.UniformDataAxis`: +method of the :class:`~.axes.UniformDataAxis`: .. code-block:: python - >>> axis = hs.hyperspy.axes.DataAxis(size=10, scale=0.1, offset=10, units='mm') + >>> axis = hs.hyperspy.axes.UniformDataAxis(size=10, scale=0.1, offset=10, units='mm') >>> axis.scale_as_quantity - 0.1 millimeter + >>> axis.convert_to_units('µm') >>> axis.scale_as_quantity - 100.0 micrometer + .. _Axes_storage_ordering: @@ -586,10 +593,10 @@ fast. Iterating over the AxesManager ------------------------------ -One can iterate over the :py:class:`~.axes.AxesManager` to produce indices to +One can iterate over the :class:`~.axes.AxesManager` to produce indices to the navigation axes. Each iteration will yield a new tuple of indices, sorted -according to the iteration path specified in :py:attr:`~.axes.AxesManager.iterpath`. -Setting the :py:attr:`~.axes.AxesManager.indices` property to a new index will +according to the iteration path specified in :attr:`~.axes.AxesManager.iterpath`. +Setting the :attr:`~.axes.AxesManager.indices` property to a new index will update the accompanying signal so that signal methods that operate at a specific navigation index will now use that index, like ``s.plot()``. @@ -597,48 +604,59 @@ navigation index will now use that index, like ``s.plot()``. >>> s = hs.signals.Signal1D(np.zeros((2,3,10))) >>> s.axes_manager.iterpath # check current iteration path - 'flyback' # default until Hyperspy 2.0, then 'serpentine' + 'serpentine' >>> for index in s.axes_manager: - ... s.axes_manager.indices = i # s.plot() will change with this ... print(index) (0, 0) (1, 0) (2, 0) - (0, 1) - (1, 1) (2, 1) + (1, 1) + (0, 1) - -The :py:attr:`~.axes.AxesManager.iterpath` attribute specifies the strategy that -the :py:class:`~.axes.AxesManager` should use to iterate over the navigation axes. +The :attr:`~.axes.AxesManager.iterpath` attribute specifies the strategy that +the :class:`~.axes.AxesManager` should use to iterate over the navigation axes. Two built-in strategies exist: -- ``'flyback'``: starts at (0, 0), continues down the row until the final - column, "flies back" to the first column, and continues from (1, 0) -- ``'serpentine'``: starts at (0, 0), but when it reaches the final column +- ``'serpentine'`` (default): starts at (0, 0), but when it reaches the final column (of index N), it continues from (1, N) along the next row, in the same way that a snake might slither, left and right. +- ``'flyback'``: starts at (0, 0), continues down the row until the final + column, "flies back" to the first column, and continues from (1, 0). + .. code-block:: python >>> s = hs.signals.Signal1D(np.zeros((2,3,10))) - >>> s.axes_manager.iterpath = 'serpentine' + >>> s.axes_manager.iterpath = 'flyback' >>> for index in s.axes_manager: ... print(index) + (0, 0) + (1, 0) + (2, 0) + (0, 1) + (1, 1) + (2, 1) -The :py:attr:`~.axes.AxesManager.iterpath` can also be set using the -:py:meth:`~.axes.AxesManager.switch_iterpath` context manager: + +The :attr:`~.axes.AxesManager.iterpath` can also be set using the +:meth:`~.axes.AxesManager.switch_iterpath` context manager: .. code-block:: python >>> s = hs.signals.Signal1D(np.zeros((2,3,10))) - >>> with s.axes_manager.switch_iterpath('serpentine'): - >>> for index in s.axes_manager: + >>> with s.axes_manager.switch_iterpath('flyback'): + ... for index in s.axes_manager: ... print(index) + (0, 0) + (1, 0) + (2, 0) + (0, 1) + (1, 1) + (2, 1) - -The :py:attr:`~.axes.AxesManager.iterpath` can also be set to be a specific list of indices, like [(0,0), (0,1)], +The :attr:`~.axes.AxesManager.iterpath` can also be set to be a specific list of indices, like [(0,0), (0,1)], but can also be any generator of indices. Storing a high-dimensional set of indices as a list or array can take a significant amount of memory. By using a generator instead, one almost entirely removes such a memory footprint: @@ -653,13 +671,13 @@ generator instead, one almost entirely removes such a memory footprint: (0, 1) >>> def reverse_flyback_generator(): - >>> for i in reversed(range(3)): + ... for i in reversed(range(3)): ... for j in reversed(range(2)): ... yield (i,j) >>> s.axes_manager.iterpath = reverse_flyback_generator() >>> for index in s.axes_manager: - ... print(index) + ... print(index) (2, 1) (2, 0) (1, 1) @@ -678,4 +696,3 @@ and a manually specified length as inputs: >>> from hyperspy.axes import GeneratorLen >>> gen = GeneratorLen(reverse_flyback_generator(), 6) >>> s.axes_manager.iterpath = gen - diff --git a/doc/user_guide/basic_usage.rst b/doc/user_guide/basic_usage.rst new file mode 100644 index 0000000000..96828f33ad --- /dev/null +++ b/doc/user_guide/basic_usage.rst @@ -0,0 +1,460 @@ +.. _basic-usage: + +Basic Usage +=========== + + +.. _importing_hyperspy-label: + +Starting Python in Windows +---------------------------- +If you used the bundle installation you should be able to use the context menus +to get started. Right-click on the folder containing the data you wish to +analyse and select "Jupyter notebook here" or "Jupyter qtconsole here". We +recommend the former, since notebooks have many advantages over conventional +consoles, as will be illustrated in later sections. The examples in some later +sections assume Notebook operation. A new tab should appear in your default +browser listing the files in the selected folder. To start a python notebook +choose "Python 3" in the "New" drop-down menu at the top right of the page. +Another new tab will open which is your Notebook. + +Starting Python in Linux and MacOS +------------------------------------ + +You can start IPython by opening a system terminal and executing ``ipython``, +(optionally followed by the "frontend": "qtconsole" for example). However, in +most cases, **the most agreeable way** to work with HyperSpy interactively +is using the `Jupyter Notebook `_ (previously known as +the IPython Notebook), which can be started as follows: + +.. code-block:: bash + + $ jupyter notebook + +Linux users may find it more convenient to start Jupyter/IPython from the +`file manager context menu `_. +In either OS you can also start by `double-clicking a notebook file +`_ if one already exists. + +Starting HyperSpy in the notebook (or terminal) +----------------------------------------------- +Typically you will need to `set up IPython for interactive plotting with +matplotlib +`_ using +``%matplotlib`` (which is known as a 'Jupyter magic') +*before executing any plotting command*. So, typically, after starting +IPython, you can import HyperSpy and set up interactive matplotlib plotting by +executing the following two lines in the IPython terminal (In these docs we +normally use the general Python prompt symbol ``>>>`` but you will probably +see ``In [1]:`` etc.): + +.. code-block:: python + + >>> %matplotlib qt # doctest: +SKIP + >>> import hyperspy.api as hs + +Note that to execute lines of code in the notebook you must press +``Shift+Return``. (For details about notebooks and their functionality try +the help menu in the notebook). Next, import two useful modules: numpy and +matplotlib.pyplot, as follows: + +.. code-block:: python + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + +The rest of the documentation will assume you have done this. It also assumes +that you have installed at least one of HyperSpy's GUI packages: +`jupyter widgets GUI `_ +and the +`traitsui GUI `_. + +Possible warnings when importing HyperSpy? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HyperSpy supports different GUIs and +`matplotlib backends `_ +which in specific cases can lead to warnings when importing HyperSpy. Most of the time +there is nothing to worry about — the warnings simply inform you of several choices you have. +There may be several causes for a warning, for example: + +- not all the GUIs packages are installed. If none is installed, we reccomend you to install + at least the ``hyperspy-gui-ipywidgets`` package is your are planning to perform interactive + data analysis in the Jupyter Notebook. Otherwise, you can simply disable the warning in + :ref:`preferences ` as explained below. +- the ``hyperspy-gui-traitsui`` package is installed and you are using an incompatible matplotlib + backend (e.g. ``notebook``, ``nbagg`` or ``widget``). + + - If you want to use the traitsui GUI, use the ``qt`` matplotlib backend instead. + - Alternatively, if you prefer to use the ``notebook`` or ``widget`` matplotlib backend, + and if you don't want to see the (harmless) warning, make sure that you have the + ``hyperspy-gui-ipywidgets`` installed and disable the traitsui + GUI in the :ref:`preferences `. + +.. versionchanged:: v1.3 + HyperSpy works with all matplotlib backends, including the ``notebook`` + (also called ``nbAgg``) backend that enables interactive plotting embedded + in the jupyter notebook. + + +.. NOTE:: + + When running in a headless system it is necessary to set the matplotlib + backend appropiately to avoid a `cannot connect to X server` error, for + example as follows: + + .. code-block:: python + + >>> import matplotlib + >>> matplotlib.rcParams["backend"] = "Agg" + >>> import hyperspy.api as hs + + +Getting help +------------ + +When using IPython, the documentation (docstring in Python jargon) can be +accessed by adding a question mark to the name of a function. e.g.: + +.. ipython:: + :okexcept: + + In [1]: import hyperspy.api as hs + In [2]: hs? + In [3]: hs.load? + In [4]: hs.signals? + +This syntax is a shortcut to the standard way one of displaying the help +associated to a given functions (docstring in Python jargon) and it is one of +the many features of `IPython `_, which is the +interactive python shell that HyperSpy uses under the hood. + + +Autocompletion +-------------- + +Another useful IPython feature is the +`autocompletion `_ +of commands and filenames using the tab and arrow keys. It is highly recommended +to read the `Ipython introduction `_ for many more useful features that will +boost your efficiency when working with HyperSpy/Python interactively. + +Creating signal from a numpy array +---------------------------------- + +HyperSpy can operate on any numpy array by assigning it to a BaseSignal class. +This is useful e.g. for loading data stored in a format that is not yet +supported by HyperSpy—supposing that they can be read with another Python +library—or to explore numpy arrays generated by other Python +libraries. Simply select the most appropriate signal from the +:mod:`~.api.signals` module and create a new instance by passing a numpy array +to the constructor e.g. + +.. code-block:: python + + >>> my_np_array = np.random.random((10, 20, 100)) + >>> s = hs.signals.Signal1D(my_np_array) + >>> s + + +The numpy array is stored in the :attr:`~.api.signals.BaseSignal.data` attribute +of the signal class: + +.. code-block:: python + + >>> s.data # doctest: +SKIP + + +.. _navigation-signal-dimensions: + +The navigation and signal dimensions +------------------------------------ + +In HyperSpy the data is interpreted as a signal array and, therefore, the data +axes are not equivalent. HyperSpy distinguishes between *signal* and +*navigation* axes and most functions operate on the *signal* axes and +iterate on the *navigation* axes. For example, an EELS spectrum image (i.e. +a 2D array of spectra) has three dimensions X, Y and energy-loss. In +HyperSpy, X and Y are the *navigation* dimensions and the energy-loss is the +*signal* dimension. To make this distinction more explicit the +representation of the object includes a separator ``|`` between the +navigation and signal dimensions e.g. + +In HyperSpy a spectrum image has signal dimension 1 and navigation dimension 2 +and is stored in the Signal1D subclass. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.zeros((10, 20, 30))) + >>> s + + + +An image stack has signal dimension 2 and navigation dimension 1 and is stored +in the Signal2D subclass. + +.. code-block:: python + + >>> im = hs.signals.Signal2D(np.zeros((30, 10, 20))) + >>> im + + +Note that HyperSpy rearranges the axes when compared to the array order. The +following few paragraphs explain how and why it does it. + +Depending how the array is arranged, some axes are faster to iterate than +others. Consider an example of a book as the dataset in question. It is +trivially simple to look at letters in a line, and then lines down the page, +and finally pages in the whole book. However if your words are written +vertically, it can be inconvenient to read top-down (the lines are still +horizontal, it's just the meaning that's vertical!). It's very time-consuming +if every letter is on a different page, and for every word you have to turn 5-6 +pages. Exactly the same idea applies here - in order to iterate through the +data (most often for plotting, but applies for any other operation too), you +want to keep it ordered for "fast access". + +In Python (more explicitly `numpy`) the "fast axes order" is C order (also +called row-major order). This means that the **last** axis of a numpy array is +fastest to iterate over (i.e. the lines in the book). An alternative ordering +convention is F order (column-major), where it is the reverse - the first axis +of an array is the fastest to iterate over. In both cases, the further an axis +is from the `fast axis` the slower it is to iterate over it. In the book +analogy you could think, for example, think about reading the first lines of +all pages, then the second and so on. + +When data is acquired sequentially it is usually stored in acquisition order. +When a dataset is loaded, HyperSpy generally stores it in memory in the same +order, which is good for the computer. However, HyperSpy will reorder and +classify the axes to make it easier for humans. Let's imagine a single numpy +array that contains pictures of a scene acquired with different exposure times +on different days. In numpy the array dimensions are ``(D, E, Y, X)``. This +order makes it fast to iterate over the images in the order in which they were +acquired. From a human point of view, this dataset is just a collection of +images, so HyperSpy first classifies the image axes (``X`` and ``Y``) as +`signal axes` and the remaining axes the `navigation axes`. Then it reverses +the order of each sets of axes because many humans are used to get the ``X`` +axis first and, more generally the axes in acquisition order from left to +right. So, the same axes in HyperSpy are displayed like this: ``(E, D | X, +Y)``. + +Extending this to arbitrary dimensions, by default, we reverse the numpy axes, +chop it into two chunks (signal and navigation), and then swap those chunks, at +least when printing. As an example: + +.. code-block:: bash + + (a1, a2, a3, a4, a5, a6) # original (numpy) + (a6, a5, a4, a3, a2, a1) # reverse + (a6, a5) (a4, a3, a2, a1) # chop + (a4, a3, a2, a1) (a6, a5) # swap (HyperSpy) + +In the background, HyperSpy also takes care of storing the data in memory in +a "machine-friendly" way, so that iterating over the navigation axes is always +fast. + + +.. _saving: + +Saving Files +------------ + +The data can be saved to several file formats. The format is specified by +the extension of the filename. + +.. code-block:: python + + >>> # load the data + >>> d = hs.load("example.tif") # doctest: +SKIP + >>> # save the data as a tiff + >>> d.save("example_processed.tif") # doctest: +SKIP + >>> # save the data as a png + >>> d.save("example_processed.png") # doctest: +SKIP + >>> # save the data as an hspy file + >>> d.save("example_processed.hspy") # doctest: +SKIP + +Some file formats are much better at maintaining the information about +how you processed your data. The preferred formats are +:external+rsciio:ref:`hspy ` and :external+rsciio:ref:`zspy `, +because they are open formats and keep most information possible. + +There are optional flags that may be passed to the save function. See +:ref:`saving_files` for more details. + +Accessing and setting the metadata +---------------------------------- + +When loading a file HyperSpy stores all metadata in the BaseSignal +:attr:`~.api.signals.BaseSignal.original_metadata` attribute. In addition, +some of those metadata and any new metadata generated by HyperSpy are stored in +:attr:`~.api.signals.BaseSignal.metadata` attribute. + + +.. code-block:: python + + >>> import exspy # doctest: +SKIP + >>> s = exspy.data.eelsdb(formula="NbO2", edge="M2,3")[0] # doctest: +SKIP + >>> s.metadata # doctest: +SKIP + ├── Acquisition_instrument + │ └── TEM + │ ├── Detector + │ │ └── EELS + │ │ └── collection_angle = 6.5 + │ ├── beam_energy = 100.0 + │ ├── convergence_angle = 10.0 + │ └── microscope = VG HB501UX + ├── General + │ ├── author = Wilfried Sigle + │ └── title = Niobium oxide NbO2 + ├── Sample + │ ├── chemical_formula = NbO2 + │ ├── description = Analyst: David Bach, Wilfried Sigle. Temperature: Room. + │ └── elements = ['Nb', 'O'] + └── Signal + ├── quantity = Electrons () + └── signal_type = EELS + + >>> s.original_metadata # doctest: +SKIP + ├── emsa + │ ├── DATATYPE = XY + │ ├── DATE = + │ ├── FORMAT = EMSA/MAS Spectral Data File + │ ├── NCOLUMNS = 1.0 + │ ├── NPOINTS = 1340.0 + │ ├── OFFSET = 120.0003 + │ ├── OWNER = eelsdatabase.net + │ ├── SIGNALTYPE = ELS + │ ├── TIME = + │ ├── TITLE = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217 + │ ├── VERSION = 1.0 + │ ├── XPERCHAN = 0.5 + │ ├── XUNITS = eV + │ └── YUNITS = + └── json + ├── api_permalink = https://api.eelsdb.eu/spectra/niobium-oxide-nbo2-2/ + ├── associated_spectra = [{'name': 'Niobium oxide NbO2', 'link': 'https://eelsdb.eu/spectra/niobium-oxide-nbo2/', 'type': 'Low Loss'}] + ├── author + │ ├── name = Wilfried Sigle + │ ├── profile_api_url = https://api.eelsdb.eu/author/wsigle/ + │ └── profile_url = https://eelsdb.eu/author/wsigle/ + ├── beamenergy = 100 kV + ├── collection = 6.5 mrad + ├── comment_count = 0 + ├── convergence = 10 mrad + ├── darkcurrent = Yes + ├── description = Analyst: David Bach, Wilfried Sigle. Temperature: Room. + ├── detector = Parallel: Gatan ENFINA + ├── download_link = https://eelsdb.eu/wp-content/uploads/2015/09/DspecYB7EbW.msa + ├── edges = ['Nb_M2,3', 'Nb_M4,5', 'O_K'] + ├── elements = ['Nb', 'O'] + ├── formula = NbO2 + ├── gainvariation = Yes + ├── guntype = cold field emission + ├── id = 21727 + ├── integratetime = 5 secs + ├── keywords = ['imported from old site'] + ├── max_energy = 789.5 eV + ├── microscope = VG HB501UX + ├── min_energy = 120 eV + ├── monochromated = No + ├── other_links = [{'url': 'http://pc-web.cemes.fr/eelsdb/index.php?page=displayspec.php&id=217', 'title': 'Old EELS DB'}] + ├── permalink = https://eelsdb.eu/spectra/niobium-oxide-nbo2-2/ + ├── published = 2008-02-15 00:00:00 + ├── readouts = 10 + ├── resolution = 1.3 eV + ├── stepSize = 0.5 eV/pixel + ├── thickness = 0.58 t/λ + ├── title = Niobium oxide NbO2 + └── type = Core Loss + + >>> s.metadata.General.title = "NbO2 Nb_M edge" # doctest: +SKIP + >>> s.metadata # doctest: +SKIP + ├── Acquisition_instrument + │ └── TEM + │ ├── Detector + │ │ └── EELS + │ │ └── collection_angle = 6.5 + │ ├── beam_energy = 100.0 + │ ├── convergence_angle = 10.0 + │ └── microscope = VG HB501UX + ├── General + │ ├── author = Wilfried Sigle + │ └── title = NbO2 Nb_M edge + ├── Sample + │ ├── chemical_formula = NbO2 + │ ├── description = Analyst: David Bach, Wilfried Sigle. Temperature: Room. + │ └── elements = ['Nb', 'O'] + └── Signal + ├── quantity = Electrons () + └── signal_type = EELS + + +.. _configuring-hyperspy-label: + +Configuring HyperSpy +-------------------- + +The behaviour of HyperSpy can be customised using the +:attr:`~.api.preferences`. The easiest way to do it is by calling +the :meth:`~.api.preferences.gui` method: + +.. code-block:: python + + >>> hs.preferences.gui() # doctest: +SKIP + +This command should raise the Preferences user interface if one of the +hyperspy gui packages are installed and enabled: + +.. _preferences_image: + +.. figure:: images/preferences.png + :align: center + + Preferences user interface. + +.. versionadded:: 1.3 + Possibility to enable/disable GUIs in the preferences. + +It is also possible to set the preferences programmatically. For example, +to disable the traitsui GUI elements and save the changes to disk: + +.. code-block:: python + + >>> hs.preferences.GUIs.enable_traitsui_gui = False + >>> hs.preferences.save() + >>> # if not saved, this setting will be used until the next jupyter kernel shutdown + +.. versionchanged:: 1.3 + + The following items were removed from preferences: + ``General.default_export_format``, ``General.lazy``, + ``Model.default_fitter``, ``Machine_learning.multiple_files``, + ``Machine_learning.same_window``, ``Plot.default_style_to_compare_spectra``, + ``Plot.plot_on_load``, ``Plot.pylab_inline``, ``EELS.fine_structure_width``, + ``EELS.fine_structure_active``, ``EELS.fine_structure_smoothing``, + ``EELS.synchronize_cl_with_ll``, ``EELS.preedge_safe_window_width``, + ``EELS.min_distance_between_edges_for_fine_structure``. + + + +.. _logger-label: + +Messages log +------------ + +HyperSpy writes messages to the `Python logger +`_. The +default log level is "WARNING", meaning that only warnings and more severe +event messages will be displayed. The default can be set in the +:ref:`preferences `. Alternatively, it can be set +using :func:`~.api.set_log_level` e.g.: + +.. code-block:: python + + >>> import hyperspy.api as hs + >>> hs.set_log_level('INFO') + >>> hs.load('my_file.dm3') # doctest: +SKIP + INFO:hyperspy.io_plugins.digital_micrograph:DM version: 3 + INFO:hyperspy.io_plugins.digital_micrograph:size 4796607 B + INFO:hyperspy.io_plugins.digital_micrograph:Is file Little endian? True + INFO:hyperspy.io_plugins.digital_micrograph:Total tags in root group: 15 + `_]. -.. _Chantler2005: - -:ref:`[Chantler2005] ` - C. Chantler, K. Olsen, R. Dragoset, - J. Chang, A. Kishore, S. Kotochigova, and D. Zucker, "Detailed Tabulation - of Atomic Form Factors, Photoelectric Absorption and Scattering Cross - Section, and Mass Attenuation Coefficients for Z = 1-92 from E = 1-10 eV - to E = 0.4-1.0 MeV" *NIST Standard Reference Data* - [``_]. - .. _Egerton2011: :ref:`[Egerton2011] ` @@ -44,12 +35,6 @@ Bibliography via Stochastic Optimization," *NIPS 2013*, 2013 [``_]. -.. _Gabor1948: - -:ref:`[Gabor1948] ` - D. Gabor, "A new microscopic principle," - *Nature* 161 (1948): 777-778 [``_]. - .. _Herraez: :ref:`[Herraez] ` @@ -65,38 +50,13 @@ Bibliography component analysis: algorithms and applications," *Neural Networks* 13 (2000): 411–430 [``_]. -.. _Joy1993: - -:ref:`[Joy1993] ` - D.Joy, Y.-S. Zhang, X. Zhang, T.Hashimoto, R. Bunn, - L.Allard, and T. Nolan, "Practical aspects of electron holography," - *Ultramicroscopy* 51.1-4 (1993): 1-14 - [``_]. - .. _Keenan2004: :ref:`[Keenan2004] ` M. Keenan and P. Kotula, "Accounting for Poisson noise in the multivariate analysis of ToF-SIMS spectrum images," *Surf. Interface Anal* 36(3) (2004): 203–212 - [``_]. - -.. _MacArthur2016: - -:ref:`[MacArthur2016] ` - K. MacArthur, T. Slater, S. Haigh, - D. Ozkaya, P. Nellist, and S. Lozano-Perez, "Quantitative Energy-Dispersive - X-Ray Analysis of Catalyst Nanoparticles Using a Partial Cross Section - Approach," *Microsc. Microanal.* 22 (2016): 71–81 - [``_]. - -.. _McCartney2007: - -:ref:`[McCartney2007] ` - M. McCartney and D. Smith, "Electron - holography: phase imaging with nanometer resolution," *Annu. Rev. Mater. - Res.* 37 (2007): 729-767 - [``_]. + [``_]. .. _[Nicoletti2013]: @@ -115,46 +75,6 @@ Bibliography *Ultramicroscopy* 111 (2010): 169–176 [``_]. -.. _Rossouw2015: - -:ref:`[Rossouw2015] ` - D. Rossouw, P. Burdet, F. de la Peña, - C. Ducati, B. Knappett, A. Wheatley, and P. Midgley, "Multicomponent Signal - Unmixing from Nanoheterostructures: Overcoming the Traditional Challenges of - Nanoscale X-ray Analysis via Machine Learning," *Nano Lett.* 15(4) (2015): - 2716–2720 [``_]. - -.. _Tonomura1999: - -:ref:`[Tonomura1999] ` - A. Tonomura, "Electron Holography," - Springer Berlin Heidelberg, 1999. 78-132 - [``_]. - -.. _Watanabe1996: - -:ref:`[Watanabe1996] ` - M. Watanabe, Z. Horita, and M. Nemoto, - "Absorption correction and thickness determination using the zeta factor in - quantitative X-ray microanalysis," *Ultramicroscopy* 65 (1996): 187–198 - [``_]. - -.. _Watanabe2006: - -:ref:`[Watanabe2006] ` - M. Watanabe and D. Williams, "The - quantitative analysis of thin specimens: a review of progress from the - Cliff-Lorimer to the new zeta-factor methods," *J. Microsc.* 221 (2006): - 89–109 [``_]. - -.. _Williams2009: - -:ref:`[Williams2009] ` - D. Williams and B Carter, "Transmission - Electron Microscopy: A Textbook for Materials Science (Part 4)," Second Ed., - Springer, New York, 2009 - [``_]. - .. _Zhao2016: :ref:`[Zhao2016] ` @@ -169,7 +89,7 @@ Bibliography :ref:`[Zhou2011] ` T. Zhou and D. Tao, "GoDec: Randomized Low-rank & Sparse Matrix Decomposition in Noisy Case", *ICML-11* (2011): 33–40 - [``_]. + [``_]. .. _Schaffer2004: @@ -199,44 +119,38 @@ Bibliography .. _Lerotic2004: :ref:`[Lerotic2004] ` - M Lerotic, C Jacobsen, T Schafer, S Vogt + M Lerotic, C Jacobsen, T Schafer, S Vogt "Cluster analysis of soft X-ray spectromicroscopy data". - Ultramicroscopy 100 (2004) 35–57 + Ultramicroscopy 100 (2004) 35–57 [``_] .. _Iakoubovskii2008: :ref:`[Iakoubovskii2008] ` - - Iakoubovskii, K., K. Mitsuishi, Y. Nakayama, and K. Furuya. - ‘Thickness Measurements with Electron Energy Loss Spectroscopy’. - Microscopy Research and Technique 71, no. 8 (2008): 626–31. - [``_]. + Iakoubovskii, K., K. Mitsuishi, Y. Nakayama, and K. Furuya. + ‘Thickness Measurements with Electron Energy Loss Spectroscopy’. + Microscopy Research and Technique 71, no. 8 (2008): 626–31. + [``_]. + +.. _Rossouw2015: + +:ref:`[Rossouw2015] ` + D. Rossouw, P. Burdet, F. de la Peña, + C. Ducati, B. Knappett, A. Wheatley, and P. Midgley, "Multicomponent Signal + Unmixing from Nanoheterostructures: Overcoming the Traditional Challenges of + Nanoscale X-ray Analysis via Machine Learning," *Nano Lett.* 15(4) (2015): + 2716–2720 [``_]. .. _White2009: :ref:`[White2009] ` - - T.A. White, “Structure solution using precession electron diffraction and - diffraction tomography” PhD Thesis, University of Cambridge, 2009. + T.A. White, “Structure solution using precession electron diffraction and + diffraction tomography” PhD Thesis, University of Cambridge, 2009. .. _Zaefferer2000: :ref:`[Zaefferer2000] ` - - S. Zaefferer, “New developments of computer-aided crystallographic - analysis in transmission electron microscopy” J. Appl. Crystallogr., - vol. 33, no. v, pp. 10–25, 2000. - [``_]. - - -Peer-review articles with results obtained using HyperSpy ---------------------------------------------------------- - -.. note:: - - Given the incresing number of articles that cite HyperSpy we no longer - maintain a list of articles here. For an up to date list search for - HyperSpy in a scientific database e.g. `Google Scholar - `_. - -.. Warning:: - The articles published before 2012 may mention the HyperSpy project under - its old name, EELSLab + S. Zaefferer, “New developments of computer-aided crystallographic + analysis in transmission electron microscopy” J. Appl. Crystallogr., + vol. 33, no. v, pp. 10–25, 2000. + [``_]. diff --git a/doc/user_guide/big_data.rst b/doc/user_guide/big_data.rst index 768aa91fed..a30510233f 100644 --- a/doc/user_guide/big_data.rst +++ b/doc/user_guide/big_data.rst @@ -3,22 +3,12 @@ Working with big data ********************* -.. warning:: All the features described in this chapter are in beta state. - - Although most of them work as described, their operation may not always - be optimal, well-documented and/or consistent with their in-memory counterparts. - - Therefore, although efforts will be taken to minimise major disruptions, - the syntax and features described here may change in patch and minor - HyperSpy releases. If you experience issues with HyperSpy's lazy features - please report them to the developers. - .. versionadded:: 1.2 HyperSpy makes it possible to analyse data larger than the available memory by providing "lazy" versions of most of its signals and functions. In most cases the syntax remains the same. This chapter describes how to work with data -larger than memory using the :py:class:`~._signals.lazy.LazySignal` class and +larger than memory using the :class:`~._signals.lazy.LazySignal` class and its derivatives. @@ -34,14 +24,19 @@ or similar), first wrap it in ``dask.array.Array`` as shown `here as normal and call ``as_lazy()``: .. code-block:: python + + >>> import h5py # doctest: +SKIP + >>> f = h5py.File("myfile.hdf5") # doctest: +SKIP + >>> data = f['/data/path'] # doctest: +SKIP + + Wrap the data in dask and chunk as appropriate - >>> import h5py - >>> f = h5py.File("myfile.hdf5") # Load the file - >>> data = f['/data/path'] # Get the data - >>> import dask.array as da # Import dask to wrap - >>> chunks = (1000,100) # Chunk as appropriate - >>> x = da.from_array(data, chunks=chunks) # Wrap the data in dask - >>> s = hs.signals.Signal1D(x).as_lazy() # Create the lazy signal + >>> import dask.array as da # doctest: +SKIP + >>> x = da.from_array(data, chunks=(1000, 100)) # doctest: +SKIP + + Create the lazy signal + + >>> s = hs.signals.Signal1D(x).as_lazy() # doctest: +SKIP Loading lazily @@ -52,15 +47,18 @@ loading a 34.9 GB ``.blo`` file on a regular laptop might look like: .. code-block:: python - >>> s = hs.load("shish26.02-6.blo", lazy=True) - >>> s + >>> s = hs.load("shish26.02-6.blo", lazy=True) # doctest: +SKIP + >>> s # doctest: +SKIP - >>> s.data + >>> s.data # doctest: +SKIP dask.array - >>> print(s.data.dtype, s.data.nbytes / 1e9) + >>> print(s.data.dtype, s.data.nbytes / 1e9) # doctest: +SKIP uint8 34.9175808 - >>> s.change_dtype("float") # To be able to perform decomposition, etc. - >>> print(s.data.dtype, s.data.nbytes / 1e9) + + Change dtype to perform decomposition, etc. + + >>> s.change_dtype("float") # doctest: +SKIP + >>> print(s.data.dtype, s.data.nbytes / 1e9) # doctest: +SKIP float64 279.3406464 Loading the dataset in the original unsigned integer format would require @@ -69,19 +67,22 @@ almost 280GB of memory. However, with the lazy processing both of these steps are near-instantaneous and require very little computational resources. .. versionadded:: 1.4 - :py:meth:`~._signals.lazy.LazySignal.close_file` + :meth:`~._signals.lazy.LazySignal.close_file` Currently when loading an hdf5 file lazily the file remains open at least while the signal exists. In order to close it explicitly, use the -:py:meth:`~._signals.lazy.LazySignal.close_file` method. Alternatively, -you could close it on calling :py:meth:`~._signals.lazy.LazySignal.compute` +:meth:`~._signals.lazy.LazySignal.close_file` method. Alternatively, +you could close it on calling :meth:`~._signals.lazy.LazySignal.compute` by passing the keyword argument ``close_file=True`` e.g.: .. code-block:: python - >>> s = hs.load("file.hspy", lazy=True) - >>> ssum = s.sum(axis=0) - >>> ssum.compute(close_file=True) # closes the file.hspy file + >>> s = hs.load("file.hspy", lazy=True) # doctest: +SKIP + >>> ssum = s.sum(axis=0) # doctest: +SKIP + + Close the file + + >>> ssum.compute(close_file=True) # doctest: +SKIP Lazy stacking @@ -93,20 +94,27 @@ lazily (both when loading or afterwards): .. code-block:: python - >>> siglist = hs.load("*.hdf5") - >>> s = hs.stack(siglist, lazy=True) - >>> # Or load lazily and stack afterwards: - >>> siglist = hs.load("*.hdf5", lazy=True) - >>> s = hs.stack(siglist) # no need to pass 'lazy', as signals already lazy - >>> # Or do everything in one go: - >>> s = hs.load("*.hdf5", lazy=True, stack=True) + >>> siglist = hs.load("*.hdf5") # doctest: +SKIP + >>> s = hs.stack(siglist, lazy=True) # doctest: +SKIP + + Or load lazily and stack afterwards: + + >>> siglist = hs.load("*.hdf5", lazy=True) # doctest: +SKIP + + Make a stack, no need to pass 'lazy', as signals are already lazy + + >>> s = hs.stack(siglist) # doctest: +SKIP + + Or do everything in one go: + + >>> s = hs.load("*.hdf5", lazy=True, stack=True) # doctest: +SKIP Casting signals as lazy ^^^^^^^^^^^^^^^^^^^^^^^ To convert a regular HyperSpy signal to a lazy one such that any future operations are only performed lazily, use the -:py:meth:`~.signal.BaseSignal.as_lazy` method: +:meth:`~.api.signals.BaseSignal.as_lazy` method: .. code-block:: python @@ -123,34 +131,39 @@ operations are only performed lazily, use the Machine learning ---------------- +.. warning:: The machine learning features are in beta state. + + Although most of them work as described, their operation may not always + be optimal, well-documented and/or consistent with their in-memory counterparts. + :ref:`mva.decomposition` algorithms for machine learning often perform large matrix manipulations, requiring significantly more memory than the data size. To perform decomposition operation lazily, HyperSpy provides access to several "online" algorithms as well as `dask `_'s lazy SVD algorithm. Online algorithms perform the decomposition by operating serially on chunks of data, enabling the lazy decomposition of large datasets. In line with the -standard HyperSpy signals, lazy :py:meth:`~._signals.lazy.LazySignal.decomposition` +standard HyperSpy signals, lazy :meth:`~._signals.lazy.LazySignal.decomposition` offers the following online algorithms: .. _lazy_decomposition-table: .. table:: Available lazy decomposition algorithms in HyperSpy - +--------------------------+----------------------------------------------------------------+ - | Algorithm | Method | - +==========================+================================================================+ - | "SVD" (default) | :py:func:`dask.array.linalg.svd` | - +--------------------------+----------------------------------------------------------------+ - | "PCA" | :py:class:`sklearn.decomposition.IncrementalPCA` | - +--------------------------+----------------------------------------------------------------+ - | "ORPCA" | :py:class:`~.learn.rpca.ORPCA` | - +--------------------------+----------------------------------------------------------------+ - | "ORNMF" | :py:class:`~.learn.ornmf.ORNMF` | - +--------------------------+----------------------------------------------------------------+ + +--------------------------+---------------------------------------------------+ + | Algorithm | Method | + +==========================+===================================================+ + | "SVD" (default) | :func:`dask.array.linalg.svd` | + +--------------------------+---------------------------------------------------+ + | "PCA" | :class:`sklearn.decomposition.IncrementalPCA` | + +--------------------------+---------------------------------------------------+ + | "ORPCA" | :class:`~.learn.rpca.ORPCA` | + +--------------------------+---------------------------------------------------+ + | "ORNMF" | :class:`~.learn.ornmf.ORNMF` | + +--------------------------+---------------------------------------------------+ .. seealso:: - :py:meth:`~.learn.mva.MVA.decomposition` for more details on decomposition + :meth:`~.api.signals.BaseSignal.decomposition` for more details on decomposition with non-lazy signals. @@ -174,7 +187,7 @@ of 25: >>> import hyperspy.api as hs >>> data = da.random.random((100, 100, 1000, 1000), chunks=('auto', 'auto', 200, 200)) >>> s = hs.signals.Signal2D(data).as_lazy() - >>> s.plot() + >>> s.plot() # doctest: +SKIP In the example above, the calculation of the navigation is fast but the actual visualisation of the dataset is slow, each for each navigation index change, @@ -189,13 +202,13 @@ harddrive when changing navigation indices: >>> data = da.random.random((100, 100, 1000, 1000), chunks=('auto', 'auto', 1000, 1000)) >>> s = hs.signals.Signal2D(data).as_lazy() - >>> s.plot() + >>> s.plot() # doctest: +SKIP This approach depends heavily on the chunking of the data and may not be -always suitable. The :py:meth:`~hyperspy._signals.lazy.LazySignal.compute_navigator` +always suitable. The :meth:`~hyperspy._signals.lazy.LazySignal.compute_navigator` can be used to calculate the navigator efficient and store the navigator, so that it can be used when plotting and saved for the later loading of the dataset. -The :py:meth:`~hyperspy._signals.lazy.LazySignal.compute_navigator` has optional +The :meth:`~hyperspy._signals.lazy.LazySignal.compute_navigator` has optional argument to specify the index where the sum needs to be calculated and how to rechunk the dataset when calculating the navigator. This allows to efficiently calculate the navigator without changing the actual chunking of the @@ -205,8 +218,8 @@ dataset, since the rechunking only takes during the computation of the navigator >>> data = da.random.random((100, 100, 1000, 1000), chunks=('auto', 'auto', 100, 100)) >>> s = hs.signals.Signal2D(data).as_lazy() - >>> s.compute_navigator(chunks_number=5) - >>> s.plot() + >>> s.compute_navigator(chunks_number=5) # doctest: +SKIP + >>> s.plot() # doctest: +SKIP .. code-block:: python @@ -234,7 +247,7 @@ interger. └── sum_from = [slice(0, 200, None), slice(0, 200, None)] An alternative is to calculate the navigator separately and store it in the -signal using the :py:attr:`~hyperspy._signals.lazy.LazySignal.navigator` setter. +signal using the :attr:`~hyperspy._signals.lazy.LazySignal.navigator` setter. .. code-block:: python @@ -243,27 +256,92 @@ signal using the :py:attr:`~hyperspy._signals.lazy.LazySignal.navigator` setter. >>> s = hs.signals.Signal2D(data).as_lazy() >>> s - >>> # for fastest results, just pick one signal space pixel + +For fastest results, just pick one signal space pixel + +.. code-block:: python + >>> nav = s.isig[500, 500] - >>> # Alternatively, sum as per default behaviour of non-lazy signal - >>> nav = s.sum(s.axes_manager.signal_axes) - >>> nav + +Alternatively, sum as per default behaviour of non-lazy signal + +.. code-block:: python + + >>> nav = s.sum(s.axes_manager.signal_axes) # doctest: +SKIP + >>> nav # doctest: +SKIP - >>> # Compute the result - >>> nav.compute() + >>> nav.compute() # doctest: +SKIP [########################################] | 100% Completed | 13.1s - >>> s.navigator = nav - >>> s.plot() + >>> s.navigator = nav # doctest: +SKIP + >>> s.plot() # doctest: +SKIP -Alternatively, it is possible to not have a navigator, and use sliders -instead: +Alternatively, it is possible to not have a navigator, and use sliders instead .. code-block:: python >>> s - - >>> s.plot(navigator='slider') + + >>> s.plot(navigator='slider') # doctest: +SKIP + +.. versionadded:: 1.7 +.. _big_data.gpu: + +GPU support +----------- + +Lazy data processing on GPUs requires explicitly transferring the data to the +GPU. + +On linux, it is recommended to use the +`dask_cuda `_ library +(not supported on windows) to manage the dask scheduler. As for CPU lazy +processing, if the dask scheduler is not specified, the default scheduler +will be used. + +.. code-block:: python + + >>> from dask_cuda import LocalCUDACluster # doctest: +SKIP + >>> from dask.distributed import Client # doctest: +SKIP + >>> cluster = LocalCUDACluster() # doctest: +SKIP + >>> client = Client(cluster) # doctest: +SKIP + +.. code-block:: python + + >>> import cupy as cp # doctest: +SKIP + >>> import dask.array as da + + Create a dask array + + >>> data = da.random.random(size=(20, 20, 100, 100)) + >>> data + dask.array + + Convert the dask chunks from numpy array to cupy array + + >>> data = data.map_blocks(cp.asarray) # doctest: +SKIP + >>> data # doctest: +SKIP + dask.array + + Create the signal + + >>> s = hs.signals.Signal2D(data).as_lazy() # doctest: +SKIP + +.. note:: + See the dask blog on `Richardson Lucy (RL) deconvolution `_ + for an example of lazy processing on GPUs using dask and cupy + + +.. _FitBigData-label: + +Model fitting +------------- +Most curve-fitting functionality will automatically work on models created from +lazily loaded signals. HyperSpy extracts the relevant chunk from the signal and fits to that. + +The linear ``'lstsq'`` optimizer supports fitting the entire dataset in a vectorised manner +using :func:`dask.array.linalg.lstsq`. This can give potentially enormous performance benefits over fitting +with a nonlinear optimizer, but comes with the restrictions explained in the :ref:`linear fitting` section. Practical tips -------------- @@ -271,6 +349,12 @@ Practical tips Despite the limitations detailed below, most HyperSpy operations can be performed lazily. Important points are: +- :ref:`big_data.chunking` +- :ref:`compute_lazy_signals` +- :ref:`lazy_operations_axes` + +.. _big_data.chunking: + Chunking ^^^^^^^^ @@ -288,45 +372,88 @@ The following example shows how to chunk one of the two navigation dimensions in .. code-block:: python >>> import dask.array as da - >>> data = da.random.random((10,200,300)) + >>> data = da.random.random((10, 200, 300)) >>> data.chunksize (10, 200, 300) - >>> s = hs.signals.Signal1D(data) - >>> s # Note the reversed order of navigation dimensions - + >>> s = hs.signals.Signal1D(data).as_lazy() + + Note the reversed order of navigation dimensions + + >>> s + - >>> s.save('chunked_signal.hspy', chunks=(10, 100, 300)) # Chunking first hyperspy dimension (second array dimension) - >>> s2 = hs.load('chunked_signal.hspy', lazy=True) - >>> s2.data.chunksize + Save data with chunking first hyperspy dimension (second array dimension) + + >>> s.save('chunked_signal.zspy', chunks=(10, 100, 300)) # doctest: +SKIP + >>> s2 = hs.load('chunked_signal.zspy', lazy=True) # doctest: +SKIP + >>> s2.data.chunksize # doctest: +SKIP (10, 100, 300) -.. versionadded:: 1.3.2 +To get the chunk size of given axes, the :meth:`~._signals.lazy.LazySignal.get_chunk_size` +method can be used: + +.. code-block:: python + + >>> import dask.array as da + >>> data = da.random.random((10, 200, 300)) + >>> data.chunksize + (10, 200, 300) + >>> s = hs.signals.Signal1D(data).as_lazy() + >>> s.get_chunk_size() # All navigation axes + ((10,), (200,)) + >>> s.get_chunk_size(0) # The first navigation axis + ((200,),) + +.. versionadded:: 2.0.0 + +Starting in version 2.0.0 HyperSpy does not automatically rechunk datasets as +this can lead to reduced performance. The ``rechunk`` or ``optimize`` keyword argument +can be set to ``True`` to let HyperSpy automatically change the chunking which +could potentially speed up operations. + +.. versionadded:: 1.7.0 + +.. _lazy._repr_html_: + +For more recent versions of dask (dask>2021.11) when using hyperspy in a jupyter +notebook a helpful html representation is available. + +.. code-block:: python + + >>> import dask.array as da + >>> data = da.zeros((20, 20, 10, 10, 10)) + >>> s = hs.signals.Signal2D(data).as_lazy() + >>> s # doctest: +SKIP + +.. figure:: images/chunks.png -By default, HyperSpy tries to optimize the chunking for most operations. However, -it is sometimes possible to manually set a more optimal chunking manually. Therefore, -many operations take a ``rechunk`` or ``optimize`` keyword argument to disable -automatic rechunking. +This helps to visualize the chunk structure and identify axes where the chunk spans the entire +axis (bolded axes). +.. _compute_lazy_signals: + Computing lazy signals ^^^^^^^^^^^^^^^^^^^^^^ Upon saving lazy signals, the result of computations is stored on disk. In order to store the lazy signal in memory (i.e. make it a normal HyperSpy -signal) it has a :py:meth:`~._signals.lazy.LazySignal.compute` method: +signal) it has a :meth:`~._signals.lazy.LazySignal.compute` method: .. code-block:: python >>> s - - >>> s.compute() + + >>> s.compute() # doctest: +SKIP [########################################] | 100% Completed | 0.1s - >>> s - + >>> s # doctest: +SKIP + +.. _lazy_operations_axes: + Lazy operations that affect the axes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -339,7 +466,141 @@ axes parameters that *may have changed* before the computation is requested. Therefore, in order to avoid such issues, it is reccomended to explicitly compute the result of all functions that are affected by the axes parameters. This is the reason why e.g. the result of -:py:meth:`~._signals.signal1d.Signal1D.shift1D` is not lazy. +:meth:`~.api.signals.Signal1D.shift1D` is not lazy. + +.. _dask_scheduler: + +Dask Scheduler +-------------- + +Dask is a flexible library for parallel computing in Python. All of the lazy operations (and many of the non lazy operations) in +hyperspy run through dask. Dask can be used to run computations on a single machine or +scaled to a cluster. This section introduces the different schedulers and how to use them +in HyperSpy - for more details, see the dask documention on +`scheduling `_. + +.. Note:: + + To scale on multiple machines, e.g. a computer cluster, the distributed scheduler is required. + + +Single Threaded Scheduler +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The single threaded scheduler in dask is useful for debugging and testing. It is not +recommended for general use. + +.. code-block:: python + + >>> import dask + >>> import hyperspy.api as hs + >>> import numpy as np + >>> import dask.array as da + + Set the scheduler to single-threaded globally + + >>> dask.config.set(scheduler='single-threaded') # doctest: +SKIP + +Alternatively, you can set the scheduler to single-threaded for a single function call by +setting the ``scheduler`` keyword argument to ``'single-threaded'``. + +Or for something like plotting you can set the scheduler to single-threaded for the +duration of the plotting call by using the ``with dask.config.set`` context manager. + +.. code-block:: python + + >>> s.compute(scheduler="single-threaded") # doctest: +SKIP + + >>> with dask.config.set(scheduler='single-threaded'): + ... s.plot() # doctest: +SKIP + +Single Machine Schedulers +^^^^^^^^^^^^^^^^^^^^^^^^^ +Dask has two schedulers available for single machines. + +1. Threaded Scheduler: + Fastest to set up but only provides parallelism through threads so only non python functions will be parallelized. + This is good if you have largely numpy code and not too many cores. +2. Processes Scheduler: + Each task (and all of the necessary dependencies) are shipped to different processes. As such it has a larger set + up time. This preforms well for python dominated code. + +.. code-block:: python + + >>> import dask + >>> dask.config.set(scheduler='processes') # doctest: +SKIP + + Any hyperspy code will now use the multiprocessing scheduler + + >>> s.compute() # doctest: +SKIP + + Change to threaded Scheduler, overwrite default + + >>> dask.config.set(scheduler='threads') # doctest: +SKIP + >>> s.compute() # doctest: +SKIP + + +Distributed Scheduler +^^^^^^^^^^^^^^^^^^^^^ + +.. warning:: Distributed computing is not supported for all file formats. + + Distributed computing is limited to a few file formats, see the list of + :external+rsciio:ref:`supported file format ` in + RosettaSciIO documentation. If the format you are using is not supported, + it is recommended to convert the file to :external+rsciio:ref:`zspy ` + by reading with a single machine scheduler and saving it as a ``zspy`` file. + +The recommended way to use dask is with the distributed scheduler. This allows you to scale your computations +to a cluster of machines. The distributed scheduler can be used on a single machine as well. ``dask-distributed`` +also gives you access to the dask dashboard which allows you to monitor your computations. + +Some operations such as the matrix decomposition algorithms in hyperspy don't currently work with +the distributed scheduler. + +.. code-block:: python + + >>> from dask.distributed import Client # doctest: +SKIP + >>> from dask.distributed import LocalCluster # doctest: +SKIP + >>> import dask.array as da + >>> import hyperspy.api as hs + + >>> cluster = LocalCluster() # doctest: +SKIP + >>> client = Client(cluster) # doctest: +SKIP + >>> client # doctest: +SKIP + + Any calculation will now use the distributed scheduler + + >>> s # doctest: +SKIP + >>> s.plot() # doctest: +SKIP + >>> s.compute() # doctest: +SKIP + +Running computation on remote cluster can be done easily using ``dask_jobqueue`` + +.. code-block:: python + + >>> from dask_jobqueue import SLURMCluster # doctest: +SKIP + >>> from dask.distributed import Client # doctest: +SKIP + >>> cluster = SLURMCluster(cores=48, + ... memory='120Gb', + ... walltime="01:00:00", + ... queue='research') # doctest: +SKIP + + Get 3 nodes + + >>> cluster.scale(jobs=3) # doctest: +SKIP + >>> client = Client(cluster) # doctest: +SKIP + >>> client # doctest: +SKIP + +Any calculation will now use the distributed scheduler + +.. code-block:: python + + >>> s = hs.data.two_gaussians() + >>> repeated_data = da.repeat(da.array(s.data[np.newaxis, :]),10, axis=0) + >>> s = hs.signals.Signal1D(repeated_data).as_lazy() + >>> summed = s.map(np.sum, inplace=False) + >>> s.compute() # doctest: +SKIP Limitations @@ -363,7 +624,7 @@ practical terms the following fails with lazy signals: .. code-block:: python >>> s = hs.signals.BaseSignal([0]).as_lazy() - >>> s += 1 + >>> s += 1 # doctest: +SKIP Traceback (most recent call last): File "", line 1, in s += 1 @@ -397,11 +658,20 @@ Other minor differences convenience, ``nansum``, ``nanmean`` and other ``nan*`` signal methods were added to mimic the workflow as closely as possible. +.. _big_data.saving: + +Saving Big Data +^^^^^^^^^^^^^^^ + +The most efficient format supported by HyperSpy to write data is the +:external+rsciio:ref:`ZSpy format `, +mainly because it supports writing concurrently from multiple threads or processes. +This also allows for smooth interaction with dask-distributed for efficient scaling. .. _lazy_details: -Behind the scenes --technical details -------------------------------------- +Behind the scenes -- technical details +-------------------------------------- Standard HyperSpy signals load the data into memory for fast access and processing. While this behaviour gives good performance in terms of speed, it @@ -411,7 +681,7 @@ significant problem when processing very large datasets on consumer-oriented hardware. HyperSpy offers a solution for this problem by including -:py:class:`~._signals.lazy.LazySignal` and its derivatives. The main idea of +:class:`~._signals.lazy.LazySignal` and its derivatives. The main idea of these classes is to perform any operation (as the name suggests) `lazily `_ (delaying the execution until the result is requested (e.g. saved, plotted)) and in a @@ -436,7 +706,7 @@ offers a couple of advantages: not required for the final result, it will not be loaded at all, saving time and resources. * **Able to extend to a distributed computing environment (clusters)**. - :py:``dask.distributed`` (see + :``dask.distributed`` (see `the dask documentation `_) offers a straightforward way to expand the effective memory for computations to that of a cluster, which allows performing the operations significantly faster diff --git a/doc/user_guide/changes.rst b/doc/user_guide/changes.rst deleted file mode 100644 index d76c92b6f8..0000000000 --- a/doc/user_guide/changes.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../CHANGES.rst diff --git a/doc/user_guide/cluster.rst b/doc/user_guide/cluster.rst deleted file mode 100644 index feb88b1520..0000000000 --- a/doc/user_guide/cluster.rst +++ /dev/null @@ -1,439 +0,0 @@ - -Cluster analysis -================ - -.. versionadded:: 1.6 - -Introduction ------------- - -`Cluster analysis `__ or clustering -is the task of grouping a set of measurements such that measurements in the same -group (called a cluster) are more similar (in some sense) to each other than to -those in other groups (clusters). -A HyperSpy signal can represent a number of large arrays of different measurements -which can represent spectra, images or sets of paramaters. -Identifying and extracting trends from large datasets is often difficult and -decomposition methods, blind source separation and cluster analysis play an important role in this process. - -Cluster analysis, in essence, compares the "distances" (or similar metric) -between different sets of measurements and groups those that are closest together. -The features it groups can be raw data points, for example, comparing for -every navigation dimension all points of a spectrum. However, if the -dataset is large, the process of clustering can be computationally intensive so -clustering is more commonly used on an extracted set of features or parameters. -For example, extraction of two peak positions of interest via a fitting process -rather than clustering all spectra points. - -In favourable cases, matrix decomposition and related methods can decompose the -data into a (ideally small) set of significant loadings and factors. -The factors capture a core representation of the features in the data and the loadings -provide the mixing ratios of these factors that best describe the original data. -Overall, this usually represents a much smaller data volume compared to the original data -and can helps to identify correlations. - -A detailed description of the application of cluster analysis in x-ray -spectro-microscopy and further details on the theory and implementation can -be found in :ref:`[Lerotic2004] `. - -Nomenclature ------------- - -Taking the example of a 1D Signal of dimensions ``(20, 10|4)`` containing the -dataset, we say there are 200 *samples*. The four measured parameters are the -*features*. If we choose to search for 3 clusters within this dataset, we -derive three main values: - -1. The `labels`, of dimensions ``(3| 20, 10)``. Each navigation position is - assigned to a cluster. The `labels` of each cluster are boolean arrays - that mark the data that has been assigned to the cluster with `True`. -2. The `cluster_distances`, of dimensions ``(3| 20, 10)``, which are the - distances of all the data points to the centroid of each cluster. -3. The "*cluster signals*", which are signals that are representative of - their clusters. In HyperSpy two are computer: - `cluster_sum_signals` and `cluster_centroid_signals`, - of dimensions ``(3| 4)``, which are the sum of all the cluster signals - that belong to each cluster or the signal closest to each cluster - centroid respectively. - - -Clustering functions HyperSpy ------------------------------ - -All HyperSpy signals have the following methods for clustering analysis: - -* :py:meth:`~.learn.mva.MVA.cluster_analysis` -* :py:meth:`~.signal.MVATools.plot_cluster_results` -* :py:meth:`~.signal.MVATools.plot_cluster_labels` -* :py:meth:`~.signal.MVATools.plot_cluster_signals` -* :py:meth:`~.signal.MVATools.plot_cluster_distances` -* :py:meth:`~.signal.MVATools.get_cluster_signals` -* :py:meth:`~.signal.MVATools.get_cluster_labels` -* :py:meth:`~.signal.MVATools.get_cluster_distances` -* :py:meth:`~.learn.mva.MVA.estimate_number_of_clusters` -* :py:meth:`~.learn.mva.MVA.plot_cluster_metric` - -The :py:meth:`~.learn.mva.MVA.cluster_analysis` method can perform cluster -analysis using any `sklearn.clustering -`_ clustering -algorithms or any other object with a compatible API. This involves importing -the relevant algorithm class from scikit-learn. - -.. code-block:: python - - >>> from sklearn.cluster import KMeans - >>> s.cluster_analysis(cluster_source="signal", algorithm=KMeans(n_clusters=3, n_init=8)) - - -For convenience, the default algorithm is ``kmeans`` algorithm and is imported -internally. All extra keyword arguments are passed to the algorithm when -present. Therefore the following code is equivalent to the previous one: - -For example: - -.. code-block:: python - - >>> s.cluster_analysis(cluster_source="signal", n_clusters=3, preprocessing="norm", algorithm="kmeans", n_init=8) - -is equivalent to: - -:py:meth:`~.learn.mva.MVA.cluster_analysis` computes the cluster labels. The -clusters areas with identical label are averaged to create a set of cluster -centres. This averaging can be performed on the ``signal`` itself, the -``bss`` or ``decomposition`` results or a user supplied signal. - -Pre-processing --------------- - -Cluster analysis measures the distances between features and groups them. It -is often necessary to pre-process the features in order to obtain meaningful -results. - -For example, pre-processing can be useful to reveal clusters when -performing cluster analysis of decomposition results. Decomposition methods -decompose data into a set of factors and a set of loadings defining the -mixing needed to represent the data. If signal 1 is reduced to three -components with mixing 0.1 0.5 2.0, and signal 2 is reduced to a mixing of 0.2 -1.0 4.0, it should be clear that these represent the same signal but with a -scaling difference. Normalization of the data can again be used to remove -scaling effects. - -Therefore, the pre-processing step -will highly influence the results and should be evaluated for the problem -under investigation. - -All pre-processing methods from (or compatible with) `sklearn.preprocessing -`_ can be passed -to the ``scaling`` keyword of the :py:meth:`~.learn.mva.MVA.cluster_analysis` -method. For convenience, the following methods from scikit-learn are -available as standard: ``standard`` , ``minmax`` and ``norm`` as -standard. Briefly, ``norm`` treats the features as a vector and normalizes the -vector length. ``standard`` re-scales each feature by removing the mean and -scaling to unit variance. ``minmax`` normalizes each feature between the -minimum and maximum range of that feature. - -Cluster signals -^^^^^^^^^^^^^^^ - -In HyperSpy *cluster signals* are signals that somehow represent their clusters. -The concept is ill-defined, since cluster algorithms only assign data points to -clusters. HyperSpy computers 2 cluster signals, - -1. ``cluster_sum_signals``, which are the sum of all the cluster signals - that belong to each cluster. -2. ``cluster_centroid_signals``, which is the signal closest to each cluster - centroid. - - -When plotting the "*cluster signals*" we can select any of those -above using the ``signal`` keyword argument: - -.. code-block:: python - - >>> s.plot_cluster_labels(signal="centroid") - -In addition, it is possible to plot the mean signal over the different -clusters: - -.. code-block:: python - - >>> s.plot_cluster_labels(signal="mean") - - -Clustering with user defined algorithms -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -User developed preprocessing or cluster algorithms can be -used in place of the sklearn methods. -A preprocessing object needs a ``fit_transform`` which -appropriately scales the data. -The example below defines a preprocessing class which normalizes -the data then applies a square root to enhances weaker features. - -.. code-block:: python - - >>> class PowerScaling(object): - >>> - >>> def __init__(self,power=0.5): - >>> self.power = power - >>> - >>> def fit_transform(self,data): - >>> norm = np.amax(data,axis=1) - >>> scaled_data = data/norm[:,None] - >>> scaled_data = scaled_data - np.min(scaled_data)+1.0e-8 - >>> scaled_data = scaled_data ** self.power - >>> return scaled_data - -The PowerScaling class can then be passed to the cluster_analysis method for use. - -.. code-block:: python - - >>> ps = PowerScaling() - >>> s.cluster_analysis(cluster_source="decomposition", number_of_components=3, preprocessing=ps) - -For user defined clustering algorithms the class must implementation -``fit`` and have a ``label_`` attribute that contains the clustering labels. -An example template would be: - -.. code-block:: python - - - >>> class MyClustering(object): - >>> - >>> def __init__(self): - >>> self.labels_ = None - >>> - >>> def fit_(self,X): - >>> self.labels_ = do_something(X) - - - -Examples --------- - -Clustering using decomposition results -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Let's use the `make_blobs -`_ -function supplied by `scikit-learn` to make dummy data to see how clustering -might work in practice. - -.. code-block:: python - - >>> import hyperspy.api as hs - >>> from sklearn.datasets import make_blobs - >>> data = make_blobs( - >>> n_samples=1000, - >>> n_features=100, - >>> centers=3, - >>> shuffle=False, - >>> random_state=1)[0].reshape(50, 20, 100) - >>> s = hs.signals.Signal1D(data) - -.. code-block:: python - - >>> hs.plot.plot_images(data.T) - - -.. image:: images/clustering_data.png - - -To see how cluster analysis works it's best to first examine the signal. -Moving around the image you should be able to see 3 distinct regions in which -the 1D signal modulates slightly. - -.. code-block:: python - - >>> s.plot() - - -Let's perform SVD to reduce the dimensionality of the dataset by exploiting -redundancies: - - -.. code-block:: python - - >>> s.decomposition() - >>> s.plot_explained_variance_ratio() - -.. image:: images/clustering_scree_plot.png - -From the scree plot we deduce that, as expected, that the dataset can be reduce -to 3 components. Let's plot their loadings: - -.. code-block:: python - - >>> s.plot_decomposition_loadings(comp_ids=3, axes_decor="off") - -.. image:: images/clustering_decomposition_loadings.png - -In the SVD loading we can identify 3 regions, but they are mixed in the components. -Let's perform cluster analysis of decomposition results, to find similar regions -and the representative features in those regions. Notice that this dataset does -not require any pre-processing for cluster analysis. - -.. code-block:: python - - >>> s.cluster_analysis(cluster_source="decomposition", number_of_components=3, preprocessing=None) - >>> s.plot_cluster_labels(axes_decor="off") - -.. image:: images/clustering_labels.png - -To see what the labels the cluster algorithm has assigned you can inspect -the ``cluster_labels``: - -.. code-block:: python - - >>> s.learning_results.cluster_labels[0] - array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - - -In this case we know there are 3 cluster, but for real examples the number of -clusters is not known *a priori*. A number of metrics, such as elbow, -Silhouette and Gap can be used to estimate the optimal number of clusters. -The elbow method measures the sum-of-squares of the distances within a -cluster and, as for the PCA decomposition, an "elbow" or point where the gains -diminish with increasing number of clusters indicates the ideal number of -clusters. Silhouette analysis measures how well separated clusters are and -can be used to determine the most likely number of clusters. As the scoring -is a measure of separation of clusters a number of solutions may occur and -maxima in the scores are used to indicate possible solutions. Gap analysis -is similar but compares the “gap” between the clustered data results and -those from a randomly data set of the same size. The largest gap indicates -the best clustering. The metric results can be plotted to check how -well-defined the clustering is. - -.. code-block:: python - - >>> s.estimate_number_of_clusters(cluster_source="decomposition", metric="gap") - >>> s.plot_cluster_metric() - -.. image:: images/clustering_Gap.png - -The optimal number of clusters can be set or accessed from the learning -results - -.. code-block:: python - - >>> s.learning_results.number_of_clusters - 3 - - - -Clustering using another signal as source -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In this example we will perform clustering analysis on the position of two -peaks. The signals containing the position of the peaks can be computed for -example using :ref:`curve fitting `. Given an existing fitted -model, the parameters can be extracted as signals and stacked. Clustering can -then be applied as described previously to identify trends in the fitted -results. - -Let's start by creating a suitable synthetic dataset. - -.. code-block:: python - - >>> import hyperspy.api as hs - >>> import numpy as np - >>> s_dummy = hs.signals.Signal1D(np.zeros((64, 64, 1000))) - >>> s_dummy.axes_manager.signal_axes[0].scale = 2e-3 - >>> s_dummy.axes_manager.signal_axes[0].units = "eV" - >>> s_dummy.axes_manager.signal_axes[0].name = "energy" - >>> m = s_dummy.create_model() - >>> m.append(hs.model.components1D.GaussianHF(fwhm=0.2)) - >>> m.append(hs.model.components1D.GaussianHF(fwhm=0.3)) - >>> m.components.GaussianHF.centre.map["values"][:32, :] = .3 + .1 - >>> m.components.GaussianHF.centre.map["values"][32:, :] = .7 + .1 - >>> m.components.GaussianHF_0.centre.map["values"][:, 32:] = m.components.GaussianHF.centre.map["values"][:, 32:] * 2 - >>> m.components.GaussianHF_0.centre.map["values"][:, :32] = m.components.GaussianHF.centre.map["values"][:, :32] * 0.5 - >>> for component in m: - ... component.centre.map["is_set"][:] = True - ... component.centre.map["values"][:] += np.random.normal(size=(64, 64)) * 0.01 - >>> s = m.as_signal() - >>> stack = hs.stack([m.components.GaussianHF.centre.as_signal(), - >>> hs.plot.plot_images(stack, axes_decor="off", colorbar="single", - suptitle="") - -.. image:: images/clustering_gaussian_centres.png - -Let's now perform cluster analysis on the stack and calculate the centres using -the spectrum image. Notice that we don't need to fit the model to the data -because this is a synthetic dataset. When analysing experimental data you will -need to fit the model first. Also notice that here we need to pre-process the -dataset by normalization in order to reveal the clusters due to the -proportionality relationship between the position of the peaks. - -.. code-block:: python - - >>> stack = hs.stack([m.components.GaussianHF.centre.as_signal(), - m.components.GaussianHF_0.centre.as_signal()]) - >>> s.estimate_number_of_clusters(cluster_source=stack.T, preprocessing="norm") - 2 - >>> s.cluster_analysis(cluster_source=stack.T, source_for_centers=s, n_clusters=2, preprocessing="norm") - >>> s.plot_cluster_labels() - -.. image:: images/clustering_gaussian_centres_labels.png - -.. code-block:: python - - >>> s.plot_cluster_signals(signal="mean") - -.. image:: images/clustering_gaussian_centres_mean.png - - -Notice that in this case averaging or summing the signals of -each cluster is not appropriate, since the clustering criterium -is the ratio between the peaks positions. A better alternative -is to plot the signals closest to the centroids: - - >>> s.plot_cluster_signals(signal="centroid") - -.. image:: images/clustering_gaussian_centres_centroid.png - - diff --git a/doc/user_guide/dielectric_function.rst b/doc/user_guide/dielectric_function.rst deleted file mode 100644 index 84bf8dce3c..0000000000 --- a/doc/user_guide/dielectric_function.rst +++ /dev/null @@ -1,56 +0,0 @@ -Dielectric function tools -------------------------- - -The :py:class:`~._signals.dielectric_function.DielectricFunction` class -inherits from :py:class:`~._signals.complex_signal.ComplexSignal` and can -thus access complex properties. To convert a -:py:class:`~._signals.complex_signal.ComplexSignal` to a -:py:class:`~._signals.dielectric_function.DielectricFunction`, -make sure that the signal dimension and signal type are properly set: - - .. code-block:: python - - >>> s.set_signal_type('DielectricFunction') - -Note that :py:class:`~._signals.dielectric_function.DielectricFunction` is -complex and therefore is a subclass of -:py:class:`~._signals.complex_signal1d.ComplexSignal1D`. - - -Number of effective electrons -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The Bethe f-sum rule gives rise to two definitions of the effective number (see -:ref:`[Egerton2011] `): - -.. math:: - - n_{\mathrm{eff1}}\left(-\Im\left(\epsilon^{-1}\right)\right)=\frac{2\epsilon_{0}m_{0}}{\pi\hbar^{2}e^{2}n_{a}}\int_{0}^{E}E'\Im\left(\frac{-1}{\epsilon}\right)dE' - - n_{\mathrm{eff2}}\left(\epsilon_{2}\right)=\frac{2\epsilon_{0}m_{0}}{\pi\hbar^{2}e^{2}n_{a}}\int_{0}^{E}E'\epsilon_{2}\left(E'\right)dE' - -where :math:`n_a` is the number of atoms (or molecules) per unit volume of the -sample, :math:`\epsilon_0` is the vacuum permittivity, :math:`m_0` is the -electron mass and :math:`e` is the electron charge. - -The -:py:meth:`~._signals.dielectric_function.DielectricFunction.get_number_of_effective_electrons` -method computes both. - -Compute the electron energy-loss signal -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The -:py:meth:`~._signals.dielectric_function.DielectricFunction.get_electron_energy_loss_spectrum` -"naively" computes the single-scattering electron-energy loss spectrum from the -dielectric function given the zero-loss peak (or its integral) and the sample -thickness using: - -.. math:: - - S\left(E\right)=\frac{2I_{0}t}{\pi - a_{0}m_{0}v^{2}}\ln\left[1+\left(\frac{\beta}{\theta(E)}\right)^{2}\right]\Im\left[\frac{-1}{\epsilon\left(E\right)}\right] - -where :math:`I_0` is the zero-loss peak integral, :math:`t` the sample -thickness, :math:`\beta` the collection semi-angle and :math:`\theta(E)` the -characteristic scattering angle. diff --git a/doc/user_guide/eds.rst b/doc/user_guide/eds.rst deleted file mode 100644 index 735c85b80c..0000000000 --- a/doc/user_guide/eds.rst +++ /dev/null @@ -1,873 +0,0 @@ -.. _eds-label: - -Energy-Dispersive X-ray Spectrometry (EDS) -****************************************** - -The methods described in this chapter are specific to the following signals: - -* :py:class:`~._signals.eds_tem.EDSTEM` -* :py:class:`~._signals.eds_sem.EDSSEMSpectrum` - -This chapter describes step-by-step the analysis of an EDS -spectrum (SEM or TEM). - -.. NOTE:: - See also the `EDS tutorials `_. - -Spectrum loading and parameters -------------------------------- - -The sample and data used in this section are described in -:ref:`[Burdet2013] <[Burdet2013]>`, -and can be downloaded using: - -.. code-block:: python - - >>> #Download the data (130MB) - >>> from urllib.request import urlretrieve, urlopen - >>> from zipfile import ZipFile - >>> files = urlretrieve("https://www.dropbox.com/s/s7cx92mfh2zvt3x/" - ... "HyperSpy_demos_EDX_SEM_files.zip?raw=1", - ... "./HyperSpy_demos_EDX_SEM_files.zip") - >>> with ZipFile("HyperSpy_demos_EDX_SEM_files.zip") as z: - >>> z.extractall() - -Loading data -^^^^^^^^^^^^ - -All data are loaded with the :py:func:`~.io.load` function, as described in -detail in :ref:`Loading files`. HyperSpy is able to import -different formats, among them ".msa" and ".rpl" (the raw format of Oxford -Instruments and Brucker). - -Here are three example for files exported by Oxford Instruments software -(INCA). For a single spectrum: - -.. code-block:: python - - >>> s = hs.load("Ni_superalloy_1pix.msa") - >>> s - - -For a spectrum image (The .rpl file is recorded as an image in this example, -The method :py:meth:`~.signal.BaseSignal.as_signal1D` set it back to a one -dimensional signal with the energy axis in first position): - -.. code-block:: python - - >>> si = hs.load("Ni_superalloy_010.rpl").as_signal1D(0) - >>> si - - -Finally, for a stack of spectrum images, using "*" as a wildcard character: - -.. code-block:: python - - >>> si4D = hs.load("Ni_superalloy_0*.rpl", stack=True) - >>> si4D = si4D.as_signal1D(0) - >>> si4D - - -.. _eds_calibration-label: - -Microscope and detector parameters -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -First, the signal type ("EDS_TEM" or "EDS_SEM") needs to be set with the -:py:meth:`~.signal.BaseSignal.set_signal_type` method. By assigning the -class of the object, specific EDS methods are made available. - -.. code-block:: python - - >>> s = hs.load("Ni_superalloy_1pix.msa") - >>> s.set_signal_type("EDS_SEM") - >>> s - - -You can also specify the signal type as an argument of -the :py:func:`~.io.load` function: - -.. code-block:: python - - >>> s = hs.load("Ni_superalloy_1pix.msa", signal_type="EDS_SEM") - >>> s - - -HyperSpy will automatically load any existing microscope parameters from the -file, and store them in the :py:attr:`~.signal.BaseSignal.metadata` -attribute (see :ref:`metadata_structure`). These parameters can be displayed -as follows: - -.. code-block:: python - - >>> s = hs.load("Ni_superalloy_1pix.msa", signal_type="EDS_SEM") - >>> s.metadata.Acquisition_instrument.SEM - ├── Detector - │ └── EDS - │ ├── azimuth_angle = 63.0 - │ ├── elevation_angle = 35.0 - │ ├── energy_resolution_MnKa = 130.0 - │ ├── live_time = 0.006855 - │ └── real_time = 0.0 - ├── beam_current = 0.0 - ├── beam_energy = 15.0 - └── tilt_stage = 38.0 - - -You can also set these parameters directly: - -.. code-block:: python - - >>> s = hs.load("Ni_superalloy_1pix.msa", signal_type="EDS_SEM") - >>> s.metadata.Acquisition_instrument.SEM.beam_energy = 30 - -or by using the -:py:meth:`~._signals.eds_tem.EDSTEMSpectrum.set_microscope_parameters` method: - -.. code-block:: python - - >>> s = hs.load("Ni_superalloy_1pix.msa", signal_type="EDS_SEM") - >>> s.set_microscope_parameters(beam_energy = 30) - -or through the GUI: - -.. code-block:: python - - >>> s = hs.load("Ni_superalloy_1pix.msa", signal_type="EDS_SEM") - >>> s.set_microscope_parameters() - -.. figure:: images/EDS_microscope_parameters_gui.png - :align: center - :width: 350 - - EDS microscope parameters preferences window - -Any microscope and detector parameters that are not found in the imported file -will be set by default. These default values can be changed in the -:py:class:`~.defaults_parser.Preferences` class (see :ref:`preferences -`). - -.. code-block:: python - - >>> hs.preferences.EDS.eds_detector_elevation = 37 - -or through the GUI: - -.. code-block:: python - - >>> hs.preferences.gui() - -.. figure:: images/EDS_preferences_gui.png - :align: center - :width: 400 - - EDS preferences window - -Energy axis -^^^^^^^^^^^ - -The size, scale and units of the energy axis are automatically imported from -the imported file, where they exist. These properties can also be set -or adjusted manually with the :py:class:`~.axes.AxesManager` -(see :ref:`Axis properties` for more info): - -.. code-block:: python - - >>> si = hs.load("Ni_superalloy_010.rpl", - ... signal_type="EDS_TEM").as_signal1D(0) - >>> si.axes_manager[-1].name = 'E' - >>> si.axes_manager['E'].units = 'keV' - >>> si.axes_manager['E'].scale = 0.01 - >>> si.axes_manager['E'].offset = -0.1 - -or through the GUI: - -.. code-block:: python - - >>> si.axes_manager.gui() - -.. figure:: images/EDS_energy_axis_gui.png - :align: center - :width: 280 - - Axis properties window - - -Copying spectrum calibration -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -All of the above parameters can be copied from one spectrum to another -with the :py:meth:`~._signals.eds_tem.EDSTEMSpectrum.get_calibration_from` -method. - -.. code-block:: python - - >>> # s1pixel contains all the parameters - >>> s1pixel = hs.load("Ni_superalloy_1pix.msa", signal_type="EDS_TEM") - >>> - >>> # si contains no parameters - >>> si = hs.load("Ni_superalloy_010.rpl", - ... signal_type="EDS_TEM").as_signal1D(0) - >>> - >>> # Copy all the properties of s1pixel to si - >>> si.get_calibration_from(s1pixel) - -.. _eds_sample-label: - -Describing the sample ---------------------- - -The description of the sample is also stored in the -:py:attr:`~.signal.BaseSignal.metadata` attribute. It can be displayed using: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.add_lines() - >>> s.metadata.Sample.thickness = 100 - >>> s.metadata.Sample - ├── description = FePt bimetallic nanoparticles - ├── elements = ['Fe', 'Pt'] - ├── thickness = 100 - └── xray_lines = ['Fe_Ka', 'Pt_La'] - - -The following methods are either called "set" or "add". - -* "set" methods overwrite previously defined values -* "add" methods add to the previously defined values - -Elements -^^^^^^^^ - -The elements present in the sample can be defined using the -:py:meth:`~._signals.eds.EDSSpectrum.set_elements` and -:py:meth:`~._signals.eds.EDSSpectrum.add_elements` methods. Only element -abbreviations are accepted: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.set_elements(['Fe', 'Pt']) - >>> s.add_elements(['Cu']) - >>> s.metadata.Sample - └── elements = ['Cu', 'Fe', 'Pt'] - -X-ray lines -^^^^^^^^^^^ - -Similarly, the X-ray lines can be defined using the -:py:meth:`~._signals.eds.EDSSpectrum.set_lines` and -:py:meth:`~._signals.eds.EDSSpectrum.add_lines` methods. The corresponding -elements will be added automatically. -Several lines per element can be defined at once. - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.set_elements(['Fe', 'Pt']) - >>> s.set_lines(['Fe_Ka', 'Pt_La']) - >>> s.add_lines(['Fe_La']) - >>> s.metadata.Sample - ├── elements = ['Fe', 'Pt'] - └── xray_lines = ['Fe_Ka', 'Fe_La', 'Pt_La'] - -The X-ray lines can also be defined automatically, if the beam energy is set. -The most excited X-ray line is selected per element (highest energy above an -overvoltage of 2 (< beam energy / 2)): - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.set_elements(['Al', 'Cu', 'Mn']) - >>> s.set_microscope_parameters(beam_energy=30) - >>> s.add_lines() - >>> s.metadata.Sample - ├── elements = ['Al', 'Cu', 'Mn'] - └── xray_lines = ['Al_Ka', 'Cu_Ka', 'Mn_Ka'] - -.. code-block:: python - - - >>> s.set_microscope_parameters(beam_energy=10) - >>> s.set_lines([]) - >>> s.metadata.Sample - ├── elements = ['Al', 'Cu', 'Mn'] - └── xray_lines = ['Al_Ka', 'Cu_La', 'Mn_La'] - -A warning is raised if you try to set an X-ray line higher than the beam -energy: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.set_elements(['Mn']) - >>> s.set_microscope_parameters(beam_energy=5) - >>> s.add_lines(['Mn_Ka']) - Warning: Mn Ka is above the data energy range. - - -Elemental database -^^^^^^^^^^^^^^^^^^ - -HyperSpy includes an elemental database, which contains the energy of the -X-ray lines. - -.. code-block:: python - - >>> hs.material.elements.Fe.General_properties - ├── Z = 26 - ├── atomic_weight = 55.845 - └── name = iron - >>> hs.material.elements.Fe.Physical_properties - └── density (g/cm^3) = 7.874 - >>> hs.material.elements.Fe.Atomic_properties.Xray_lines - ├── Ka - │ ├── energy (keV) = 6.404 - │ └── weight = 1.0 - ├── Kb - │ ├── energy (keV) = 7.0568 - │ └── weight = 0.1272 - ├── La - │ ├── energy (keV) = 0.705 - │ └── weight = 1.0 - ├── Lb3 - │ ├── energy (keV) = 0.792 - │ └── weight = 0.02448 - ├── Ll - │ ├── energy (keV) = 0.615 - │ └── weight = 0.3086 - └── Ln - ├── energy (keV) = 0.62799 - └── weight = 0.12525 - -Finding elements from energy -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To find the nearest X-ray line for a given energy, use the utility function -:py:func:`~.misc.eds.utils.get_xray_lines_near_energy` to search the elemental -database: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> P = s.find_peaks1D_ohaver(maxpeakn=1)[0] - >>> hs.eds.get_xray_lines_near_energy(P['position'], only_lines=['a', 'b']) - ['C_Ka', 'Ca_La', 'B_Ka'] - -The lines are returned in order of distance from the specified energy, and can -be limited by additional, optional arguments. - -.. _eds_plot-label: - -Plotting --------- - -You can visualize an EDS spectrum using the -:py:meth:`~._signals.eds.EDSSpectrum.plot` method (see -:ref:`visualisation`). For example: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.plot() - -.. figure:: images/EDS_plot_spectrum.png - :align: center - :width: 500 - - EDS spectrum - -An example of multi-dimensional EDS data (e.g. 3D SEM-EDS) is given in -:ref:`visualisation multi-dimension`. - - -.. _eds_plot_markers-label: - -Plotting X-ray lines -^^^^^^^^^^^^^^^^^^^^ - -X-ray lines can be added as plot labels with -:py:meth:`~._signals.eds.EDSSpectrum.plot`. The lines are either retrieved -from `metadata.Sample.Xray_lines`, or selected with the same method as -:py:meth:`~._signals.eds.EDSSpectrum.add_lines` using the elements in -`metadata.Sample.elements`. - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.add_elements(['C','Mn','Cu','Al','Zr']) - >>> s.plot(True) - -.. figure:: images/EDS_plot_Xray_default.png - :align: center - :width: 500 - - EDS spectrum plot with line markers - -You can also select a subset of lines to label: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.add_elements(['C','Mn','Cu','Al','Zr']) - >>> s.plot(True, only_lines=['Ka','b']) - -.. figure:: images/EDS_plot_Xray_a.png - :align: center - :width: 500 - - EDS spectrum plot with a selection of line markers - -.. _get_lines_intensity: - - -Getting the intensity of an X-ray line --------------------------------------- - -The sample and data used in this section are described in -:ref:`[Rossouw2015] `, and can be downloaded using: - -.. code-block:: python - - >>> #Download the data (1MB) - >>> from urllib.request import urlretrieve, urlopen - >>> from zipfile import ZipFile - >>> files = urlretrieve("https://www.dropbox.com/s/ecdlgwxjq04m5mx/" - ... "HyperSpy_demos_EDS_TEM_files.zip?raw=1", - ... "./HyperSpy_demos_EDX_TEM_files.zip") - >>> with ZipFile("HyperSpy_demos_EDX_TEM_files.zip") as z: - >>> z.extractall() - -The width of integration is defined by extending the energy resolution of -Mn Ka to the peak energy (`energy_resolution_MnKa` in the metadata): - -.. code-block:: python - - >>> s = hs.load('core_shell.hdf5') - >>> s.get_lines_intensity(['Fe_Ka'], plot_result=True) - -.. figure:: images/EDS_get_lines_intensity.png - :align: center - :width: 500 - - Iron map as computed and displayed by ``get_lines_intensity`` - -The X-ray lines defined in `metadata.Sample.Xray_lines` are used by default. -The EDS maps can be plotted using :py:func:`~.drawing.utils.plot_images`, see :ref:`plotting several images` -for more information in setting plotting parameters. - -.. code-block:: python - - >>> s = hs.load('core_shell.hdf5') - >>> s.metadata.Sample - ├── elements = ['Fe', 'Pt'] - └── xray_lines =['Fe_Ka', 'Pt_La'] - >>> eds_maps = s.get_lines_intensity() - >>> hs.plot.plot_images(eds_maps, axes_decor='off', scalebar='all') - -.. figure:: images/EDS_get_lines_intensity_all.png - :align: center - :width: 500 - -Finally, the windows of integration can be visualised using -:py:meth:`~._signals.eds.EDSSpectrum.plot` method: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum().isig[5.:13.] - >>> s.add_lines() - >>> s.plot(integration_windows='auto') - -.. figure:: images/EDS_integration_windows.png - :align: center - :width: 500 - - EDS spectrum with integration windows markers - -.. _eds_background_subtraction-label: - -Background subtraction -^^^^^^^^^^^^^^^^^^^^^^ - -The background can be subtracted from the X-ray intensities with -:py:meth:`~._signals.eds.EDSSpectrum.get_lines_intensity`. -The background value is obtained by averaging the intensity in two -windows on each side of the X-ray line. -The position of the windows can be estimated using -:py:meth:`~._signals.eds.EDSSpectrum.estimate_background_windows`, and -can be plotted using :py:meth:`~._signals.eds.EDSSpectrum.plot`: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum().isig[5.:13.] - >>> s.add_lines() - >>> bw = s.estimate_background_windows(line_width=[5.0, 2.0]) - >>> s.plot(background_windows=bw) - >>> s.get_lines_intensity(background_windows=bw, plot_result=True) - -.. figure:: images/EDS_background_subtraction.png - :align: center - :width: 500 - - EDS spectrum with background subtraction markers. - -.. _eds_fitting-label: - -EDS curve fitting ------------------ - -The intensity of X-ray lines can be extracted using curve-fitting in HyperSpy. -This example uses an EDS-SEM spectrum of a a test material (EDS-TM001) provided -by `BAM `_. - -First, we load the spectrum, define the chemical composition of the sample and -set the beam energy: - -.. code-block:: python - - >>> s = hs.load('bam.msa') - >>> s.add_elements(['Al', 'Ar', 'C', 'Cu', 'Mn', 'Zr']) - >>> s.set_microscope_parameters(beam_energy=10) - -Next, the model is created with -:py:meth:`~._signals.eds_sem.EDSSEMSpectrum.create_model`. One Gaussian is -automatically created per X-ray line, along with a polynomial for the -background. - -.. code-block:: python - - >>> m = s.create_model() - >>> m.print_current_values() - - Components Parameter Value - Al_Ka - A 65241.4 - Al_Kb - Ar_Ka - A 3136.88 - Ar_Kb - C_Ka - A 79258.9 - Cu_Ka - A 1640.8 - Cu_Kb - Cu_La - A 74032.6 - Cu_Lb1 - Cu_Ln - Cu_Ll - Cu_Lb3 - Mn_Ka - A 47796.6 - Mn_Kb - Mn_La - A 73665.7 - Mn_Ln - Mn_Ll - Mn_Lb3 - Zr_La - A 68703.8 - Zr_Lb1 - Zr_Lb2 - Zr_Ln - Zr_Lg3 - Zr_Ll - Zr_Lg1 - Zr_Lb3 - background_order_6 - -The width and the energies are fixed, while the heights of the sub-X-ray -lines are linked to the main X-ray lines (alpha lines). The model can now be -fitted: - -.. code-block:: python - - >>> m.fit() - -The background fitting can be improved with -:py:meth:`~.models.edsmodel.EDSModel.fit_background` by enabling only energy -ranges containing no X-ray lines: - -.. code-block:: python - - >>> m.fit_background() - -The width of the X-ray lines is defined from the energy resolution (FWHM at -Mn Ka) provided by `energy_resolution_MnKa` in `metadata`. This parameters -can be calibrated by fitting with -:py:meth:`~.models.edsmodel.EDSModel.calibrate_energy_axis`: - -.. code-block:: python - - >>> m.calibrate_energy_axis(calibrate='resolution') - Energy resolution (FWHM at Mn Ka) changed from 130.000000 to 131.927922 eV - -Fine-tuning of specific X-ray lines can be achieved using -:py:meth:`~.models.edsmodel.EDSModel.calibrate_xray_lines`: - -.. code-block:: python - - >>> m.calibrate_xray_lines('energy', ['Ar_Ka'], bound=10) - >>> m.calibrate_xray_lines('width', ['Ar_Ka'], bound=10) - >>> m.calibrate_xray_lines('sub_weight', ['Mn_La'], bound=10) - -The result of the fit is obtained with the -:py:meth:`~.models.edsmodel.EDSModel.get_lines_intensity` method. - -.. code-block:: python - - >>> result = m.get_lines_intensity(plot_result=True) - Al_Ka at 1.4865 keV : Intensity = 65241.42 - Ar_Ka at 2.9577 keV : Intensity = 3136.88 - C_Ka at 0.2774 keV : Intensity = 79258.95 - Cu_Ka at 8.0478 keV : Intensity = 1640.80 - Cu_La at 0.9295 keV : Intensity = 74032.56 - Mn_Ka at 5.8987 keV : Intensity = 47796.57 - Mn_La at 0.63316 keV : Intensity = 73665.70 - Zr_La at 2.0423 keV : Intensity = 68703.75 - -Finally, we visualize the result: - -.. code-block:: python - - >>> m.plot() - -.. figure:: images/EDS_fitting.png - :align: center - :width: 500 - -The following methods can be used to enable/disable different -functionalities of X-ray lines when fitting: - -* :py:meth:`~.models.edsmodel.EDSModel.free_background` -* :py:meth:`~.models.edsmodel.EDSModel.fix_background` -* :py:meth:`~.models.edsmodel.EDSModel.enable_xray_lines` -* :py:meth:`~.models.edsmodel.EDSModel.disable_xray_lines` -* :py:meth:`~.models.edsmodel.EDSModel.free_sub_xray_lines_weight` -* :py:meth:`~.models.edsmodel.EDSModel.fix_sub_xray_lines_weight` -* :py:meth:`~.models.edsmodel.EDSModel.free_xray_lines_energy` -* :py:meth:`~.models.edsmodel.EDSModel.fix_xray_lines_energy` -* :py:meth:`~.models.edsmodel.EDSModel.free_xray_lines_width` -* :py:meth:`~.models.edsmodel.EDSModel.fix_xray_lines_width` - -.. _eds_quantification-label: - -EDS Quantification ------------------- - -HyperSpy includes three methods for EDS quantification with or without -absorption correction: - -* Cliff-Lorimer -* Zeta-factors -* Ionization cross sections - -Quantification must be applied to the background-subtracted intensities, which -can be found using :py:meth:`~._signals.eds.EDSSpectrum.get_lines_intensity`. -The quantification of these intensities can then be calculated using -:py:meth:`~._signals.eds_tem.EDSTEMSpectrum.quantification`. - -The quantification method needs be specified as either 'CL', 'zeta', or -'cross_section'. If no method is specified, the function will raise an -exception. - -A list of factors or cross sections should be supplied in the same order as -the listed intensities (please note that HyperSpy intensities in -:py:meth:`~._signals.eds.EDSSpectrum.get_lines_intensity` are in alphabetical -order). - -A set of k-factors can be usually found in the EDS manufacturer software -although determination from standard samples for the particular instrument used -is usually preferable. In the case of zeta-factors and cross sections, these -must be determined experimentally using standards. - -Zeta-factors should be provided in units of kg/m^2. The method is described -further in :ref:`[Watanabe1996] ` -and :ref:`[Watanabe2006] `. Cross sections should be -provided in units of barns (b). Further details on the cross section method can -be found in :ref:`[MacArthur2016] `. Conversion between -zeta-factors and cross sections is possible using -:py:func:`~.misc.eds.utils.edx_cross_section_to_zeta` or -:py:func:`~.misc.eds.utils.zeta_to_edx_cross_section`. - -Using the Cliff-Lorimer method as an example, quantification can be carried -out as follows: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.add_lines() - >>> kfactors = [1.450226, 5.075602] #For Fe Ka and Pt La - >>> bw = s.estimate_background_windows(line_width=[5.0, 2.0]) - >>> intensities = s.get_lines_intensity(background_windows=bw) - >>> atomic_percent = s.quantification(intensities, method='CL', - ... factors=kfactors) - Fe (Fe_Ka): Composition = 15.41 atomic percent - Pt (Pt_La): Composition = 84.59 atomic percent - -The obtained composition is in atomic percent, by default. However, it can be -transformed into weight percent either with the option -:py:meth:`~._signals.eds_tem.EDSTEMSpectrum.quantification`: - -.. code-block:: python - - >>> # With s, intensities and kfactors from before - >>> s.quantification(intensities, method='CL', factors=kfactors, - >>> composition_units='weight') - Fe (Fe_Ka): Composition = 4.96 weight percent - Pt (Pt_La): Composition = 95.04 weight percent - -or using :py:func:`~.misc.material.atomic_to_weight`: - -.. code-block:: python - - >>> # With atomic_percent from before - >>> weight_percent = hs.material.atomic_to_weight(atomic_percent) - -The reverse method is :py:func:`~.misc.material.weight_to_atomic`. - -The zeta-factor method needs both the 'beam_current' (in nA) and the -acquisition or dwell time (referred to as 'real_time' in seconds) in order -to obtain an accurate quantification. Both of the these parameters can be -assigned to the metadata using: - -.. code-block:: python - - >>> s.set_microscope_parameters(beam_current=0.5) - >>> s.set_microscope_parameters(real_time=1.5) - -If these parameters are not set, the code will produce an error. -The zeta-factor method will produce two sets of results. Index [0] contains the -composition maps for each element in atomic percent, and index [1] contains the -mass-thickness map. - -The cross section method needs the 'beam_current', dwell time ('real_time') and -probe area in order to obtain an accurate quantification. The 'beam_current' -and 'real_time' can be set as shown above. The 'probe_area' (in nm^2) can -be defined in two different ways. - -If the probe diameter is narrower than the pixel width, then the probe is being -under-sampled and an estimation of the probe area needs to be used. This can -be added to the metadata with: - -.. code-block:: python - - >>> s.set_microscope_parameters(probe_area=0.00125) - -Alternatively, if sub-pixel scanning is used (or the spectrum map was recorded -at a high spatial sampling and subsequently binned into much larger pixels) -then the illumination area becomes the pixel area of the spectrum image. -This is a much more accurate approach for quantitative EDS and should be -used where possible. The pixel width could either be added to the metadata -by putting the pixel area in as the 'probe_area' (above) or by calibrating -the spectrum image (see :ref:`Setting_axis_properties`). - -Either approach will provide an illumination area for the cross_section -quantification. If the pixel width is not set, the code will still run with the -default value of 1 nm with a warning message to remind the user that this is -the case. - -The cross section method will produce two sets of results. Index [0] contains -the composition maps for each element in atomic percent and index [1] is the -number of atoms per pixel for each element. - -.. NOTE:: - - Please note that the function does not assume square pixels, so both the - x and y pixel dimensions must be set. For quantification of line scans, - rather than spectrum images, the pixel area should be added to the - metadata as above. - -.. _eds_absorption-label: - -Absorption Correction -^^^^^^^^^^^^^^^^^^^^^ - -Absorption correction can be included into any of the three quantification -methods by adding the parameter absorption_correction=True to the function. -By default the function iterates the quantification function until of -tolerance value of 0.5% up to a maximum number of iterations. The maximum -number of iterations is set to 30 by default but can be increased by -specifying max_interations= in the function call. However, typically for TEM -experiments convergence is witness after less then 5 iterations. - -For example: - -.. code-block:: python - - >>> s.quantification(intensities, method='cross_section', - >>> factors=factors, absorption_correction=True) - -However for the kfactor method the user must additionally provide a sample -thickness (in nm) either as a single float value or as a numpy array with the -same dimensions as the navigation axes. If this is done the calculated -mass_thickness is additionally outputted from the function as well as the -composition maps for each element. - -.. code-block:: python - - >>> s.quantification(intensities, method='CL', - >>> factors=factors, absorption_correction=True - >>> thickness = 100.) - -At this stage absorption correction is only applicable for parallel-sided, -thin-film samples. Absorption correction is calculated on a pixel by pixel -basis after having determined a sample mass-thickness map. It therefore may -be a source of error in particularly inhomogeneous specimens. - -Absorption correction can also only be applied to spectra from a single EDS -detector. For systems that consist of multiple detectors, such as the Thermo -Fisher Super-X, it is therefore necessary to load the spectra from each -detector separately. - -Utils ------ - -.. _eds_absorption_db-label: - -Mass absorption coefficient database -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A mass absorption coefficient database :ref:`[Chantler2005] ` -is available: - -.. code-block:: python - - >>> hs.material.mass_absorption_coefficient( - >>> element='Al', energies=['C_Ka','Al_Ka']) - array([ 26330.38933818, 372.02616732]) - -.. code-block:: python - - >>> hs.material.mass_absorption_mixture( - >>> elements=['Al','Zn'], weight_percent=[50,50], energies='Al_Ka') - 2587.4161643905127 - -Electron and X-ray range -^^^^^^^^^^^^^^^^^^^^^^^^ - -The electron and X-ray range in a bulk material can be estimated with -:py:meth:`hs.eds.electron_range` and :py:meth:`hs.eds.xray_range` - -To calculate the X-ray range of Cu Ka in pure Copper at 30 kV in micron: - -.. code-block:: python - - >>> hs.eds.xray_range('Cu_Ka', 30.) - 1.9361716759499248 - -To calculate the X-ray range of Cu Ka in pure Carbon at 30kV in micron: - -.. code-block:: python - - >>> hs.eds.xray_range('Cu_Ka', 30., hs.material.elements.C. - >>> Physical_properties.density_gcm3) - 7.6418811280855454 - -To calculate the electron range in pure Copper at 30 kV in micron - -.. code-block:: python - - >>> hs.eds.electron_range('Cu', 30.) - 2.8766744984001607 diff --git a/doc/user_guide/eels.rst b/doc/user_guide/eels.rst deleted file mode 100644 index afd9a9af08..0000000000 --- a/doc/user_guide/eels.rst +++ /dev/null @@ -1,360 +0,0 @@ - -Electron Energy Loss Spectroscopy -********************************* - -.. _eels_tools-label: - -Tools for EELS data analysis ----------------------------- - -The functions described in this chapter are only available for the -:py:class:`~._signals.eels.EELSSpectrum` class. To transform a -:py:class:`~.signal.BaseSignal` (or subclass) into a -:py:class:`~._signals.eels.EELSSpectrum`: - -.. code-block:: python - - >>> s.set_signal_type("EELS") - -Note these chapter discusses features that are available only for -:py:class:`~._signals.eels.EELSSpectrum` class. However, this class inherits -many useful feature from its parent class that are documented in previous -chapters. - - -.. _eels_elemental_composition-label: - -Elemental composition of the sample -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -It can be useful to define the elemental composition of the sample for -archiving purposes or to use some feature (e.g. curve fitting) that requires -this information. The elemental composition of the sample can be declared -using :py:meth:`~._signals.eels.EELSSpectrum.add_elements`. The -information is stored in the :py:attr:`~.signal.BaseSignal.metadata` -attribute (see :ref:`metadata_structure`). This information is saved to file -when saving in the hspy format (HyperSpy's HDF5 specification). - -An utility function :py:meth:`~.misc.eels.tools.get_edges_near_energy` can be -helpful to identify possible elements in the sample. -:py:meth:`~.misc.eels.tools.get_edges_near_energy` returns a list of edges -arranged in the order closest to the specified energy within a window, both -measured in eV. The size of the window can be controlled by the argument -`width` (default as 10)--- If the specified energy is 849 eV and the width is -6 eV, it returns a list of edges with onset energy between 846 eV to 852 eV and -they are arranged in the order closest to 849 eV. - -.. code-block:: python - - >>> from hyperspy.misc.eels.tools import get_edges_near_energy - >>> get_edges_near_energy(532) - ['O_K', 'Pd_M3', 'Sb_M5', 'Sb_M4'] - >>> get_edges_near_energy(849, width=6) - ['La_M4', 'Fe_L1'] - - -` -The static method :py:meth:`~._signals.eels.EELSSpectrum.print_edges_near_energy` -in :py:class:`~._signals.eels.EELSSpectrum` will print out a table containing -more information about the edges. - -.. code-block:: python - - >>> s = hs.datasets.artificial_data.get_core_loss_eels_signal() - >>> s.print_edges_near_energy(401, width=20) - +-------+-------------------+-----------+-----------------------------+ - | edge | onset energy (eV) | relevance | description | - +-------+-------------------+-----------+-----------------------------+ - | N_K | 401.0 | Major | Abrupt onset | - | Sc_L3 | 402.0 | Major | Sharp peak. Delayed maximum | - | Cd_M5 | 404.0 | Major | Delayed maximum | - | Sc_L2 | 407.0 | Major | Sharp peak. Delayed maximum | - | Mo_M2 | 410.0 | Minor | Sharp peak | - | Mo_M3 | 392.0 | Minor | Sharp peak | - | Cd_M4 | 411.0 | Major | Delayed maximum | - +-------+-------------------+-----------+-----------------------------+ - -The method :py:meth:`~._signals.eels.EELSSpectrum.edges_at_energy` allows -inspecting different sections of the signal for interactive edge -identification (the default). A region can be selected by dragging the mouse -across the signal and after clicking the `Update` button, edges with onset -energies within the selected energy range will be displayed. By toggling the -edge buttons, it will put or remove the corresponding edges on the signal. When -the `Complementary edge` box is ticked, edges outside the selected range with -the same element of edges within the selected energy range will be shown as well -to aid identification of edges. - -.. code-block:: python - - >>> s = hs.datasets.artificial_data.get_core_loss_eels_signal() - >>> s.edges_at_energy() - -.. figure:: images/EELS_edges_at_energy.png - :align: center - :width: 500 - - Labels of edges can be put or remove by toggling the edge buttons. - - -.. _eels_thickness-label: - -Thickness estimation -^^^^^^^^^^^^^^^^^^^^ - -.. versionadded:: 1.6 - Option to compute the absolute thickness, including the angular corrections - and mean free path estimation. - -The :py:meth:`~._signals.eels.EELSSpectrum.estimate_thickness` method can -estimate the thickness from a low-loss EELS spectrum using the log-ratio -method. If the beam energy, collection angle, convergence angle and sample -density are known, the absolute thickness is computed using the method in -:ref:`[Iakoubovskii2008] `. This includes the estimation of -the inelastic mean free path (iMFP). For more accurate results, it is possible -to input the iMFP of the material if known. If the density and/or the iMFP are -not known, the output is the thickness relative to the (unknown) iMFP without -any angular corrections. - -Zero-loss peak centre and alignment -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The -:py:meth:`~._signals.eels.EELSSpectrum.estimate_zero_loss_peak_centre` -can be used to estimate the position of the zero-loss peak. The method assumes -that the ZLP is the most intense feature in the spectra. For a more general -approach see :py:meth:`~.signal.Signal1DTools.find_peaks1D_ohaver`. - -The :py:meth:`~._signals.eels.EELSSpectrum.align_zero_loss_peak` can -align the ZLP with subpixel accuracy. It is more robust and easy to use than -:py:meth:`~.signal.Signal1DTools.align1D` for the task. Note that it is -possible to apply the same alignment to other spectra using the `also_align` -argument. This can be useful e.g. to align core-loss spectra acquired -quasi-simultaneously. If there are other features in the low loss signal -which are more intense than the ZLP, the `signal_range` argument can narrow -down the energy range for searching for the ZLP. - -Deconvolutions -^^^^^^^^^^^^^^ - -Three deconvolution methods are currently available: - -* :py:meth:`~._signals.eels.EELSSpectrum.fourier_log_deconvolution` -* :py:meth:`~._signals.eels.EELSSpectrum.fourier_ratio_deconvolution` -* :py:meth:`~._signals.eels.EELSSpectrum.richardson_lucy_deconvolution` - -Estimate elastic scattering intensity -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The -:py:meth:`~._signals.eels.EELSSpectrum.estimate_elastic_scattering_intensity` -can be used to calculate the integral of the zero loss peak (elastic intensity) -from EELS low-loss spectra containing the zero loss peak using the -(rudimentary) threshold method. The threshold can be global or spectrum-wise. -If no threshold is provided it is automatically calculated using -:py:meth:`~._signals.eels.EELSSpectrum.estimate_elastic_scattering_threshold` -with default values. - -:py:meth:`~._signals.eels.EELSSpectrum.estimate_elastic_scattering_threshold` -can be used to calculate separation point between elastic and inelastic -scattering on EELS low-loss spectra. This algorithm calculates the derivative -of the signal and assigns the inflexion point to the first point below a -certain tolerance. This tolerance value can be set using the `tol` keyword. -Currently, the method uses smoothing to reduce the impact of the noise in the -measure. The number of points used for the smoothing window can be specified by -the npoints keyword. - - -.. _eels.kk: - -Kramers-Kronig Analysis -^^^^^^^^^^^^^^^^^^^^^^^ - -The single-scattering EEL spectrum is approximately related to the complex -permittivity of the sample and can be estimated by Kramers-Kronig analysis. -The :py:meth:`~._signals.eels.EELSSpectrum.kramers_kronig_analysis` -method implements the Kramers-Kronig FFT method as in -:ref:`[Egerton2011] ` to estimate the complex dielectric function -from a low-loss EELS spectrum. In addition, it can estimate the thickness if -the refractive index is known and approximately correct for surface -plasmon excitations in layers. - - -.. _eels.fitting: - -EELS curve fitting ------------------- - -HyperSpy makes it really easy to quantify EELS core-loss spectra by curve -fitting as it is shown in the next example of quantification of a boron nitride -EELS spectrum from the `EELS Data Base -`__ (see :ref:`example-data-label`). - -Load the core-loss and low-loss spectra - - -.. code-block:: python - - >>> s = hs.datasets.eelsdb(title="Hexagonal Boron Nitride", - ... spectrum_type="coreloss")[0] - >>> ll = hs.datasets.eelsdb(title="Hexagonal Boron Nitride", - ... spectrum_type="lowloss")[0] - - -Set some important experimental information that is missing from the original -core-loss file - -.. code-block:: python - - >>> s.set_microscope_parameters(beam_energy=100, - ... convergence_angle=0.2, - ... collection_angle=2.55) - -.. warning:: - - `convergence_angle` and `collection_angle` are actually semi-angles and are - given in mrad. `beam_energy` is in keV. - - -Define the chemical composition of the sample - -.. code-block:: python - - >>> s.add_elements(('B', 'N')) - - -In order to include the effect of plural scattering, the model is convolved with the loss loss spectrum in which case the low loss spectrum needs to be provided to :py:meth:`~._signals.eels.EELSSpectrum.create_model`: - -.. code-block:: python - - >>> m = s.create_model(ll=ll) - - -HyperSpy has created the model and configured it automatically: - -.. code-block:: python - - >>> m.components - # | Attribute Name | Component Name | Component Type - ---- | -------------------- | -------------------- | -------------------- - 0 | PowerLaw | PowerLaw | PowerLaw - 1 | N_K | N_K | EELSCLEdge - 2 | B_K | B_K | EELSCLEdge - -Conveniently, all the EELS core-loss components of the added elements are added -automatically, names after its element symbol. - -.. code-block:: python - - >>> m.components.N_K - - >>> m.components.B_K - - -By default the fine structure features are disabled (although -the default value can be configured (see :ref:`configuring-hyperspy-label`). -We must enable them to accurately fit this spectrum. - -.. code-block:: python - - >>> m.enable_fine_structure() - - -We use smart_fit instead of standard fit method because smart_fit is optimized -to fit EELS core-loss spectra - -.. code-block:: python - - >>> m.smart_fit() - - -This fit can also be applied over the entire signal to fit a whole spectrum -image - -.. code-block:: python - - >>> m.multifit(kind='smart') - -.. NOTE:: - - `m.smart_fit()` and `m.multifit(kind='smart')` are methods specific to the EELS model. - The fitting procedure acts in iterative manner along the energy-loss-axis. - First it fits only the background up to the first edge. It continues by deactivating all edges except the first one, then performs the fit. - Then it only activates the the first two, fits, and repeats this until all edges are fitted simultanously. - - Other, non-EELSCLEdge components, are never deactivated, and fitted on every iteration. - -Print the result of the fit - -.. code-block:: python - - >>> m.quantify() - Absolute quantification: - Elem. Intensity - B 0.045648 - N 0.048061 - - -Visualize the result - -.. code-block:: python - - >>> m.plot() - - -.. figure:: images/curve_fitting_BN.png - :align: center - :width: 500 - - Curve fitting quantification of a boron nitride EELS core-loss spectrum - from the `EELS Data Base `__. - - -There are several methods that are only available in -:py:class:`~.models.eelsmodel.EELSModel`: - -* :py:meth:`~.models.eelsmodel.EELSModel.smart_fit` is a fit method that is - more robust than the standard routine when fitting EELS data. -* :py:meth:`~.models.eelsmodel.EELSModel.quantify` prints the intensity at - the current locations of all the EELS ionisation edges in the model. -* :py:meth:`~.models.eelsmodel.EELSModel.remove_fine_structure_data` removes - the fine structure spectral data range (as defined by the - :py:attr:`~._components.eels_cl_edge.EELSCLEdge.fine_structure_width)` - ionisation edge components. It is specially useful when fitting without - convolving with a zero-loss peak. - -The following methods permit to easily enable/disable background and ionisation -edges components: - -* :py:meth:`~.models.eelsmodel.EELSModel.enable_edges` -* :py:meth:`~.models.eelsmodel.EELSModel.enable_background` -* :py:meth:`~.models.eelsmodel.EELSModel.disable_background` -* :py:meth:`~.models.eelsmodel.EELSModel.enable_fine_structure` -* :py:meth:`~.models.eelsmodel.EELSModel.disable_fine_structure` - -The following methods permit to easily enable/disable several ionisation -edge functionalities: - -* :py:meth:`~.models.eelsmodel.EELSModel.set_all_edges_intensities_positive` -* :py:meth:`~.models.eelsmodel.EELSModel.unset_all_edges_intensities_positive` -* :py:meth:`~.models.eelsmodel.EELSModel.enable_free_onset_energy` -* :py:meth:`~.models.eelsmodel.EELSModel.disable_free_onset_energy` -* :py:meth:`~.models.eelsmodel.EELSModel.fix_edges` -* :py:meth:`~.models.eelsmodel.EELSModel.free_edges` -* :py:meth:`~.models.eelsmodel.EELSModel.fix_fine_structure` -* :py:meth:`~.models.eelsmodel.EELSModel.free_fine_structure` - - -When fitting edges with fine structure enabled it is often desirable that the -fine structure region of nearby ionization edges does not overlap. HyperSpy -provides a method, -:py:meth:`~.models.eelsmodel.EELSModel.resolve_fine_structure`, to -automatically adjust the fine structure to prevent fine structure to avoid -overlapping. This method is executed automatically when e.g. components are -added or removed from the model, but sometimes is necessary to call it -manually. - -Sometimes it is desirable to disable the automatic adjustment of the fine -structure width. It is possible to suspend this feature by calling -:py:meth:`~.models.eelsmodel.EELSModel.suspend_auto_fine_structure_width`. -To resume it use -:py:meth:`~.models.eelsmodel.EELSModel.suspend_auto_fine_structure_width` diff --git a/doc/user_guide/electron_holography.rst b/doc/user_guide/electron_holography.rst deleted file mode 100644 index f252bbac0c..0000000000 --- a/doc/user_guide/electron_holography.rst +++ /dev/null @@ -1,196 +0,0 @@ -.. _electron-holography-label: - -Electron Holography -******************* - -HyperSpy provides the user with a signal class which can be used to process -electron holography data: - -* :py:class:`~._signals.hologram_image.HologramImage` - -It inherits from :py:class:`~._signals.signal2d.Signal2D` class and thus can -use all of its functionality. The usage of the class is explained in the -following sections. - - -The HologramImage class -======================= - -The :py:class:`~._signals.hologram_image.HologramImage` class is designed to -contain images acquired via electron holography. - -To transform a :py:class:`~._signals.signal2d.Signal2D` (or subclass) into a -:py:class:`~._signals.hologram_image.HologramImage` use: - -.. code-block:: python - - >>> im.set_signal_type('hologram') - - -Reconstruction of holograms ---------------------------- -The detailed description of electron holography and reconstruction of holograms -can be found in literature :ref:`[Gabor1948] `, -:ref:`[Tonomura1999] `, -:ref:`[McCartney2007] `, -and :ref:`[Joy1993] `. -Fourier based reconstruction of off-axis holograms (includes -finding a side band in FFT, isolating and filtering it, recenter and -calculate inverse Fourier transform) can be performed using the -:meth:`~._signals.hologram_image.HologramImage.reconstruct_phase` method -which returns a :py:class:`~._signals.complex_signal2d.ComplexSignal2D` class, -containing the reconstructed electron wave. -The :meth:`~._signals.hologram_image.HologramImage.reconstruct_phase` method -takes sideband position and size as parameters: - -.. code-block:: python - - >>> import hyperspy.api as hs - >>> im = hs.datasets.example_signals.object_hologram() - >>> wave_image = im.reconstruct_phase(sb_position=(, ), - ... sb_size=sb_radius) - -The parameters can be found automatically by calling following methods: - -.. code-block:: python - - >>> sb_position = im.estimate_sideband_position(ap_cb_radius=None, - ... sb='lower') - >>> sb_size = im.estimate_sideband_size(sb_position) - -:meth:`~._signals.hologram_image.HologramImage.estimate_sideband_position` -method searches for maximum of intensity in upper or lower part of FFT pattern -(parameter ``sb``) excluding the middle area defined by ``ap_cb_radius``. -:meth:`~._signals.hologram_image.HologramImage.estimate_sideband_size` method -calculates the radius of the sideband filter as half of the distance to the -central band which is commonly used for strong phase objects. Alternatively, -the sideband filter radius can be recalculate as 1/3 of the distance -(often used for weak phase objects) for example: - -.. code-block:: python - - >>> sb_size = sb_size * 2 / 3 - - -To reconstruct the hologram with a vacuum reference wave, the reference -hologram should be provided to the method either as Hyperspy's -:py:class:`~._signals.hologram_image.HologramImage` or as a nparray: - -.. code-block:: python - - >>> reference_hologram = hs.datasets.example_signals.reference_hologram() - >>> wave_image = im.reconstruct_phase(reference_hologram, - ... sb_position=sb_position, - ... sb_size=sb_size) - -Using the reconstructed wave, one can access its amplitude and phase (also -unwrapped phase) using -``amplitude`` and ``phase`` properties -(also the :meth:`~._signals.complex_signal.ComplexSignal.unwrapped_phase` -method): - -.. code-block:: python - - >>> wave_image.unwrapped_phase().plot() - -.. figure:: images/holography_unwrapped_phase.png - :align: center - - Unwrapped phase image. - -Additionally, it is possible to change the smoothness of the sideband filter -edge (which is by default set to 5% of the filter radius) using parameter -``sb_smoothness``. - -Both ``sb_size`` and ``sb_smoothness`` can be provided in desired units rather -than pixels (by default) by setting ``sb_unit`` value either to ``mrad`` or -``nm`` for milliradians or inverse nanometers respectively. For example: - -.. code-block:: python - - >>> wave_image = im.reconstruct_phase(reference_hologram, - ... sb_position=sb_position, sb_size=30, - ... sb_smoothness=0.05*30,sb_unit='mrad') - -Also the :py:meth:`~._signals.hologram_image.HologramImage.reconstruct_phase` -method can output wave images with desired size (shape). By default the shape -of the original hologram is preserved. Though this leads to oversampling of the -output wave images, since the information is limited by the size of the -sideband filter. To avoid oversampling the output shape can be set to the -diameter of the sideband as follows: - -.. code-block:: python - - >>> out_size = int(2*sb_size.data) - >>> wave_image = im.reconstruct_phase(reference_hologram, - ... sb_position=sb_position, - ... sb_size=sb_size, - ... output_shape=(out_size, out_size)) - -Note that the -:py:meth:`~._signals.hologram_image.HologramImage.reconstruct_phase` -method can be called without parameters, which will cause their automatic -assignment by -:py:meth:`~._signals.hologram_image.HologramImage.estimate_sideband_position` -and :py:meth:`~._signals.hologram_image.HologramImage.estimate_sideband_size` -methods. This, however, is not recommended for not experienced users. - -.. _holography.stats-label: - -Further processing of complex wave and phase --------------------------------------------- -Once the complex electron wave reconstructed it :ref:`can be processed the same way as any other complex signal -`. A useful tool to explore the complex data is :ref:`Argand plot `, which can be -calculated and displayed as follows: - -.. _holo.argand-example: - -.. code-block:: python - - >>> ad = wave_image.argand_diagram(display_range=[-3, 3]) - >>> ad.plot(scalebar=False) - -.. figure:: images/holography_argand_diagram.png - :align: center - - Argand diagram of the reconstructed complex wave. - -Getting hologram statistics ---------------------------- -There are many reasons to have an access to some parameters of holograms which describe the quality of the data. -:meth:`~._signals.hologram_image.HologramImage.statistics` can be used to calculate carrier frequency, -fringe spacing and estimate fringe contrast. The method outputs dictionary with the values listed above calculated also -in different units. In particular fringe spacing is calculated in pixels (fringe sampling) as well as in -calibrated units. Carrier frequency is calculated in inverse pixels or calibrated units as well as radians. -Estimation of fringe contrast is either performed by division of standard deviation by mean value of hologram or -in Fourier space as twice the fraction of amplitude of sideband centre and amplitude of center band (i.e. FFT origin). -The first method is default and using it requires the fringe field to cover entire field of view; the method is -highly sensitive to any artifacts in holograms like dud pixels, -fresnel fringes and etc. The second method is less sensitive to the artifacts listed above and gives -reasonable estimation of fringe contrast even if the hologram is not covering entire field of view, but it is highly -sensitive to precise calculation of sideband position and therefore sometimes may underestimate the contrast. -The selection between to algorithms can be done using parameter ``fringe_contrast_algorithm`` setting it to -``'statistical'`` or to ``'fourier'``. The side band position typically provided by a ``sb_position``. -The statistics can be accessed as follows: - -.. code-block:: python - - >>> statistics = im.statistics(sb_position=sb_position) - -Note that by default the ``single_value`` parameter is ``True`` which forces the output of single values for each -entry of statistics dictionary calculated from first navigation pixel. (I.e. for image stacks only first image -will be used for calculating the statistics.) Otherwise: - -.. code-block:: python - - >>> statistics = im.statistics(sb_position=sb_position, single_value=False) - -Entries of ``statistics`` are Hyperspy signals containing the hologram parameters for each image in a stack. - -The estimation of fringe spacing using ``'fourier'`` method applies apodization in real space prior calculating FFT. -By default ``apodization`` parameter is set to ``hanning`` which applies Hanning window. Other options are using either -``None`` or ``hamming`` for no apodization or Hamming window. Please note that for experimental conditions -especially with extreme sampling of fringes and strong contrast variation due to Fresnel effects -the calculated fringe contrast provides only an estimate and the values may differ strongly depending on apodization. - -For further information see documentation of :meth:`~._signals.hologram_image.HologramImage.statistics`. diff --git a/doc/user_guide/events.rst b/doc/user_guide/events.rst index 2861d0f113..57b9b138c5 100644 --- a/doc/user_guide/events.rst +++ b/doc/user_guide/events.rst @@ -6,88 +6,88 @@ Events Events are a mechanism to send notifications. HyperSpy events are decentralised, meaning that there is not a central events dispatcher. -Instead, each object that can emit events has an :py:attr:`events` -attribute that is an instance of :py:class:`~.events.Events` and that contains -instances of :py:class:`~.events.Event` as attributes. When triggered the +Instead, each object that can emit events has an ``events`` +attribute that is an instance of :class:`~.events.Events` and that contains +instances of :class:`~.events.Event` as attributes. When triggered the first keyword argument, `obj` contains the object that the events belongs to. Different events may be triggered by other keyword arguments too. Connecting to events -------------------- -The following example shows how to connect to the `index_changed` event of -:py:class:`~.axes.DataAxis` that is triggered with `obj` and `index` keywords: - - .. code-block:: python - - >>> s = hs.signals.Signal1D(np.random.random((10,100))) - >>> nav_axis = s.axes_manager.navigation_axes[0] - >>> nav_axis.name = "x" - >>> def on_index_changed(obj, index): - >>> print("on_index_changed_called") - >>> print("Axis name: ", obj.name) - >>> print("Index: ", index) - ... - >>> nav_axis.events.index_changed.connect(on_index_changed) - >>> s.axes_manager.indices = (3,) - on_index_changed_called - ('Axis name: ', 'x') - ('Index: ', 3) - >>> s.axes_manager.indices = (9,) - on_index_changed_called - ('Axis name: ', 'x') - ('Index: ', 9) +The following example shows how to connect to the ``index_changed`` event of +:class:`~.axes.DataAxis` that is triggered with ``obj`` and ``index`` keywords: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.random((10,100))) + >>> nav_axis = s.axes_manager.navigation_axes[0] + >>> nav_axis.name = "x" + >>> def on_index_changed(obj, index): + ... print("on_index_changed_called") + ... print("Axis name: ", obj.name) + ... print("Index: ", index) + + >>> nav_axis.events.index_changed.connect(on_index_changed) + >>> s.axes_manager.indices = (3,) + on_index_changed_called + Axis name: x + Index: 3 + >>> s.axes_manager.indices = (9,) + on_index_changed_called + Axis name: x + Index: 9 + It is possible to select the keyword arguments that are passed to the -connected. For example, in the following only the `index` keyword argument is -passed to `on_index_changed2` and none to `on_index_changed3`: +connected. For example, in the following only the ``index`` keyword argument is +passed to ``on_index_changed2`` and none to ``on_index_changed3``: - .. code-block:: python +.. code-block:: python >>> def on_index_changed2(index): - >>> print("on_index_changed2_called") - >>> print("Index: ", index) - ... + ... print("on_index_changed2_called") + ... print("Index: ", index) + >>> nav_axis.events.index_changed.connect(on_index_changed2, ["index"]) >>> s.axes_manager.indices = (0,) on_index_changed_called - ('Axis name: ', 'x') - ('Index: ', 0) + Axis name: x + Index: 0 on_index_changed2_called - ('Index: ', 0) + Index: 0 >>> def on_index_changed3(): - >>> print("on_index_changed3_called") - ... + ... print("on_index_changed3_called") + >>> nav_axis.events.index_changed.connect(on_index_changed3, []) >>> s.axes_manager.indices = (1,) on_index_changed_called - ('Axis name: ', 'x') - ('Index: ', 1) + Axis name: x + Index: 1 on_index_changed2_called - ('Index: ', 1) + Index: 1 on_index_changed3_called + It is also possible to map trigger keyword arguments to connected function keyword arguments as follows: - .. code-block:: python >>> def on_index_changed4(arg): - >>> print("on_index_changed4_called") - >>> print("Index: ", arg) - ... - >>> nav_axis.events.index_changed.connect(on_index_changed4, - ... {"index" : "arg"}) + ... print("on_index_changed4_called") + ... print("Index: ", arg) + + >>> nav_axis.events.index_changed.connect(on_index_changed4, {"index" : "arg"}) >>> s.axes_manager.indices = (4,) on_index_changed_called - ('Axis name: ', 'x') - ('Index: ', 4) + Axis name: x + Index: 4 on_index_changed2_called - ('Index: ', 4) + Index: 4 on_index_changed3_called on_index_changed4_called - ('Index: ', 4) + Index: 4 Suppressing events ------------------ @@ -98,20 +98,19 @@ a given event and all callbacks of all events of an object. .. code-block:: python >>> with nav_axis.events.index_changed.suppress_callback(on_index_changed2): - >>> s.axes_manager.indices = (7,) - ... + ... s.axes_manager.indices = (7,) on_index_changed_called - ('Axis name: ', 'x') - ('Index: ', 7) + Axis name: x + Index: 7 on_index_changed3_called on_index_changed4_called - ('Index: ', 7) + Index: 7 >>> with nav_axis.events.index_changed.suppress(): - >>> s.axes_manager.indices = (6,) - ... + ... s.axes_manager.indices = (6,) + >>> with nav_axis.events.suppress(): - >>> s.axes_manager.indices = (5,) - ... + ... s.axes_manager.indices = (5,) + Triggering events ----------------- @@ -119,8 +118,8 @@ Triggering events Although usually there is no need to trigger events manually, there are cases where it is required. When triggering events manually it is important to pass the right keywords as specified in the event docstring. In the -following example we change the :py:attr:`data` attribute of a -:py:class:`~.signal.BaseSignal` manually and we then trigger the `data_changed` +following example we change the :attr:`~.api.signals.BaseSignal.data` attribute of a +:class:`~.api.signals.BaseSignal` manually and we then trigger the ``data_changed`` event. .. code-block:: python diff --git a/doc/user_guide/getting_started.rst b/doc/user_guide/getting_started.rst deleted file mode 100644 index eab091a722..0000000000 --- a/doc/user_guide/getting_started.rst +++ /dev/null @@ -1,488 +0,0 @@ -Getting started -*************** - - -.. _importing_hyperspy-label: - -Starting Python in Windows ----------------------------- -If you used the bundle installation you should be able to use the context menus -to get started. Right-click on the folder containing the data you wish to -analyse and select "Jupyter notebook here" or "Jupyter qtconsole here". We -recommend the former, since notebooks have many advantages over conventional -consoles, as will be illustrated in later sections. The examples in some later -sections assume Notebook operation. A new tab should appear in your default -browser listing the files in the selected folder. To start a python notebook -choose "Python 3" in the "New" drop-down menu at the top right of the page. -Another new tab will open which is your Notebook. - -Starting Python in Linux and MacOS ------------------------------------- - -You can start IPython by opening a system terminal and executing ``ipython``, -(optionally followed by the "frontend": "qtconsole" for example). However, in -most cases, **the most agreeable way** to work with HyperSpy interactively -is using the `Jupyter Notebook `_ (previously known as -the IPython Notebook), which can be started as follows: - -.. code-block:: bash - - $ jupyter notebook - -Linux users may find it more convenient to start Jupyter/IPython from the -`file manager context menu `_. -In either OS you can also start by `double-clicking a notebook file -`_ if one already exists. - -Starting HyperSpy in the notebook (or terminal) ------------------------------------------------ -Typically you will need to `set up IPython for interactive plotting with -matplotlib -`_ using -``%matplotlib`` (which is known as a 'Jupyter magic') -*before executing any plotting command*. So, typically, after starting -IPython, you can import HyperSpy and set up interactive matplotlib plotting by -executing the following two lines in the IPython terminal (In these docs we -normally use the general Python prompt symbol ``>>>`` but you will probably -see ``In [1]:`` etc.): - -.. code-block:: python - - >>> %matplotlib qt - >>> import hyperspy.api as hs - -Note that to execute lines of code in the notebook you must press -``Shift+Return``. (For details about notebooks and their functionality try -the help menu in the notebook). Next, import two useful modules: numpy and -matplotlib.pyplot, as follows: - -.. code-block:: python - - >>> import numpy as np - >>> import matplotlib.pyplot as plt - -The rest of the documentation will assume you have done this. It also assumes -that you have installed at least one of HyperSpy's GUI packages: -`jupyter widgets GUI `_ -and the -`traitsui GUI `_. - -Possible warnings when importing HyperSpy? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HyperSpy supports different GUIs and -`matplotlib backends `_ -which in specific cases can lead to warnings when importing HyperSpy. Most of the time -there is nothing to worry about — the warnings simply inform you of several choices you have. -There may be several causes for a warning, for example: - -- not all the GUIs packages are installed. If none is installed, we reccomend you to install - at least the ``hyperspy-gui-ipywidgets`` package is your are planning to perform interactive - data analysis in the Jupyter Notebook. Otherwise, you can simply disable the warning in - :ref:`preferences ` as explained below. -- the ``hyperspy-gui-traitsui`` package is installed and you are using an incompatible matplotlib - backend (e.g. ``notebook``, ``nbagg`` or ``widget``). - - - If you want to use the traitsui GUI, use the ``qt`` matplotlib backend instead. - - Alternatively, if you prefer to use the ``notebook`` or ``widget`` matplotlib backend, - and if you don't want to see the (harmless) warning, make sure that you have the - ``hyperspy-gui-ipywidgets`` installed and disable the traitsui - GUI in the :ref:`preferences `. - - -By default, HyperSpy warns the user if one of the GUI packages is not installed. -These warnings can be turned off using the -:ref:`preferences ` GUI or programmatically as follows: - - .. code-block:: python - - >>> import hyperspy.api as hs - >>> hs.preferences.GUIs.warn_if_guis_are_missing = False - >>> hs.preferences.save() - - -.. versionchanged:: v1.3 - HyperSpy works with all matplotlib backends, including the ``notebook`` - (also called ``nbAgg``) backend that enables interactive plotting embedded - in the jupyter notebook. - - -.. NOTE:: - - When running in a headless system it is necessary to set the matplotlib - backend appropiately to avoid a `cannot connect to X server` error, for - example as follows: - - .. code-block:: python - - >>> import matplotlib - >>> matplotlib.rcParams["backend"] = "Agg" - >>> import hyperspy.api as hs - - -Getting help ------------- - -When using IPython, the documentation (docstring in Python jargon) can be -accessed by adding a question mark to the name of a function. e.g.: - -.. code-block:: none - - >>> hs? - >>> hs.load? - >>> hs.signals? - -This syntax is a shortcut to the standard way one of displaying the help -associated to a given functions (docstring in Python jargon) and it is one of -the many features of `IPython `_, which is the -interactive python shell that HyperSpy uses under the hood. - -Please note that the documentation of the code is a work in progress, so not -all the objects are documented yet. - -Up-to-date documentation is always available in `the HyperSpy website. -`_ - - -Autocompletion --------------- - -Another useful `IPython `_ feature is the -autocompletion of commands and filenames using the tab and arrow keys. It is -highly recommended to read the `Ipython documentation -`_ (specially their `Getting -started `_ -section) for many more useful features that will boost your efficiency when -working with HyperSpy/Python interactively. - - -Loading data ------------- - -Once HyperSpy is running, to load from a supported file format (see -:ref:`supported-formats`) simply type: - -.. code-block:: python - - >>> s = hs.load("filename") - -.. HINT:: - - The load function returns an object that contains data read from the file. - We assign this object to the variable ``s`` but you can choose any (valid) - variable name you like. for the filename, don\'t forget to include the - quotation marks and the file extension. - -If no argument is passed to the load function, a window will be raised that -allows to select a single file through your OS file manager, e.g.: - -.. code-block:: python - - >>> # This raises the load user interface - >>> s = hs.load() - -It is also possible to load multiple files at once or even stack multiple -files. For more details read :ref:`loading_files` - -"Loading" data from a numpy array ---------------------------------- - -HyperSpy can operate on any numpy array by assigning it to a BaseSignal class. -This is useful e.g. for loading data stored in a format that is not yet -supported by HyperSpy—supposing that they can be read with another Python -library—or to explore numpy arrays generated by other Python -libraries. Simply select the most appropriate signal from the -:py:mod:`~.signals` module and create a new instance by passing a numpy array -to the constructor e.g. - -.. code-block:: python - - >>> my_np_array = np.random.random((10,20,100)) - >>> s = hs.signals.Signal1D(my_np_array) - >>> s - - -The numpy array is stored in the :py:attr:`~.signal.BaseSignal.data` attribute -of the signal class. - -.. _example-data-label: - -Loading example data and data from online databases ---------------------------------------------------- - -HyperSpy is distributed with some example data that can be found in -`hs.datasets.example_signals`. The following example plots one of the example -signals: - -.. code-block:: python - - >>> hs.datasets.example_signals.EDS_TEM_Spectrum().plot() - -.. versionadded:: 1.4 - :py:mod:`~.datasets.artificial_data` - -There are also artificial datasets, which are made to resemble real -experimental data. - -.. code-block:: python - - >>> s = hs.datasets.artificial_data.get_core_loss_eels_signal() - >>> s.plot() - -.. _eelsdb-label: - -The :py:func:`~.misc.eels.eelsdb.eelsdb` function in `hs.datasets` can -directly load spectra from `The EELS Database `_. For -example, the following loads all the boron trioxide spectra currently -available in the database: - -.. code-block:: python - - >>> hs.datasets.eelsdb(formula="B2O3") - [, - ] - - -The navigation and signal dimensions ------------------------------------- - -In HyperSpy the data is interpreted as a signal array and, therefore, the data -axes are not equivalent. HyperSpy distinguishes between *signal* and -*navigation* axes and most functions operate on the *signal* axes and -iterate on the *navigation* axes. For example, an EELS spectrum image (i.e. -a 2D array of spectra) has three dimensions X, Y and energy-loss. In -HyperSpy, X and Y are the *navigation* dimensions and the energy-loss is the -*signal* dimension. To make this distinction more explicit the -representation of the object includes a separator ``|`` between the -navigation and signal dimensions e.g. - -In HyperSpy a spectrum image has signal dimension 1 and navigation dimension 2 -and is stored in the Signal1D subclass. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.zeros((10, 20, 30))) - >>> s - - - -An image stack has signal dimension 2 and navigation dimension 1 and is stored -in the Signal2D subclass. - -.. code-block:: python - - >>> im = hs.signals.Signal2D(np.zeros((30, 10, 20))) - >>> im - - -Note that HyperSpy rearranges the axes when compared to the array order. The -following few paragraphs explain how and why it does it. - -Depending how the array is arranged, some axes are faster to iterate than -others. Consider an example of a book as the dataset in question. It is -trivially simple to look at letters in a line, and then lines down the page, -and finally pages in the whole book. However if your words are written -vertically, it can be inconvenient to read top-down (the lines are still -horizontal, it's just the meaning that's vertical!). It's very time-consuming -if every letter is on a different page, and for every word you have to turn 5-6 -pages. Exactly the same idea applies here - in order to iterate through the -data (most often for plotting, but applies for any other operation too), you -want to keep it ordered for "fast access". - -In Python (more explicitly `numpy`) the "fast axes order" is C order (also -called row-major order). This means that the **last** axis of a numpy array is -fastest to iterate over (i.e. the lines in the book). An alternative ordering -convention is F order (column-major), where it is the reverse - the first axis -of an array is the fastest to iterate over. In both cases, the further an axis -is from the `fast axis` the slower it is to iterate over it. In the book -analogy you could think, for example, think about reading the first lines of -all pages, then the second and so on. - -When data is acquired sequentially it is usually stored in acquisition order. -When a dataset is loaded, HyperSpy generally stores it in memory in the same -order, which is good for the computer. However, HyperSpy will reorder and -classify the axes to make it easier for humans. Let's imagine a single numpy -array that contains pictures of a scene acquired with different exposure times -on different days. In numpy the array dimensions are ``(D, E, Y, X)``. This -order makes it fast to iterate over the images in the order in which they were -acquired. From a human point of view, this dataset is just a collection of -images, so HyperSpy first classifies the image axes (``X`` and ``Y``) as -`signal axes` and the remaining axes the `navigation axes`. Then it reverses -the order of each sets of axes because many humans are used to get the ``X`` -axis first and, more generally the axes in acquisition order from left to -right. So, the same axes in HyperSpy are displayed like this: ``(E, D | X, -Y)``. - -Extending this to arbitrary dimensions, by default, we reverse the numpy axes, -chop it into two chunks (signal and navigation), and then swap those chunks, at -least when printing. As an example: - -.. code-block:: bash - - (a1, a2, a3, a4, a5, a6) # original (numpy) - (a6, a5, a4, a3, a2, a1) # reverse - (a6, a5) (a4, a3, a2, a1) # chop - (a4, a3, a2, a1) (a6, a5) # swap (HyperSpy) - -In the background, HyperSpy also takes care of storing the data in memory in -a "machine-friendly" way, so that iterating over the navigation axes is always -fast. - - -.. _saving: - -Saving Files ------------- - -The data can be saved to several file formats. The format is specified by -the extension of the filename. - -.. code-block:: python - - >>> # load the data - >>> d = hs.load("example.tif") - >>> # save the data as a tiff - >>> d.save("example_processed.tif") - >>> # save the data as a png - >>> d.save("example_processed.png") - >>> # save the data as an hspy file - >>> d.save("example_processed.hspy") - -Some file formats are much better at maintaining the information about -how you processed your data. The preferred format in HyperSpy is hspy, -which is based on the HDF5 format. This format keeps the most information -possible. - -There are optional flags that may be passed to the save function. See -:ref:`saving_files` for more details. - -Accessing and setting the metadata ----------------------------------- - -When loading a file HyperSpy stores all metadata in the BaseSignal -:py:attr:`~.signal.BaseSignal.original_metadata` attribute. In addition, -some of those metadata and any new metadata generated by HyperSpy are stored in -:py:attr:`~.signal.BaseSignal.metadata` attribute. - - -.. code-block:: python - - >>> s = hs.load("NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217.msa") - >>> s.metadata - ├── original_filename = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217.msa - ├── record_by = spectrum - ├── signal_type = EELS - └── title = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217 - - >>> s.original_metadata - ├── DATATYPE = XY - ├── DATE = - ├── FORMAT = EMSA/MAS Spectral Data File - ├── NCOLUMNS = 1.0 - ├── NPOINTS = 1340.0 - ├── OFFSET = 120.0003 - ├── OWNER = eelsdatabase.net - ├── SIGNALTYPE = ELS - ├── TIME = - ├── TITLE = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217 - ├── VERSION = 1.0 - ├── XPERCHAN = 0.5 - ├── XUNITS = eV - └── YUNITS = - - >>> s.set_microscope_parameters(100, 10, 20) - >>> s.metadata - ├── TEM - │ ├── EELS - │ │ └── collection_angle = 20 - │ ├── beam_energy = 100 - │ └── convergence_angle = 10 - ├── original_filename = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217.msa - ├── record_by = spectrum - ├── signal_type = EELS - └── title = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217 - - >>> s.metadata.TEM.microscope = "STEM VG" - >>> s.metadata - ├── TEM - │ ├── EELS - │ │ └── collection_angle = 20 - │ ├── beam_energy = 100 - │ ├── convergence_angle = 10 - │ └── microscope = STEM VG - ├── original_filename = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217.msa - ├── record_by = spectrum - ├── signal_type = EELS - └── title = NbO2_Nb_M_David_Bach,_Wilfried_Sigle_217 - - -.. _configuring-hyperspy-label: - -Configuring HyperSpy --------------------- - -The behaviour of HyperSpy can be customised using the -:py:class:`~.defaults_parser.Preferences` class. The easiest way to do it is by -calling the :meth:`gui` method: - -.. code-block:: python - - >>> hs.preferences.gui() - -This command should raise the Preferences user interface if one of the -hyperspy gui packages are installed and enabled: - -.. _preferences_image: - -.. figure:: images/preferences.png - :align: center - - Preferences user interface. - -.. versionadded:: 1.3 - Possibility to enable/disable GUIs in the preferences. - -It is also possible to set the preferences programmatically. For example, -to disable the traitsui GUI elements and save the changes to disk: - -.. code-block:: python - - >>> hs.preferences.GUIs.enable_traitsui_gui = False - >>> hs.preferences.save() - >>> # if not saved, this setting will be used until the next jupyter kernel shutdown - -.. versionchanged:: 1.3 - - The following items were removed from preferences: - ``General.default_export_format``, ``General.lazy``, - ``Model.default_fitter``, ``Machine_learning.multiple_files``, - ``Machine_learning.same_window``, ``Plot.default_style_to_compare_spectra``, - ``Plot.plot_on_load``, ``Plot.pylab_inline``, ``EELS.fine_structure_width``, - ``EELS.fine_structure_active``, ``EELS.fine_structure_smoothing``, - ``EELS.synchronize_cl_with_ll``, ``EELS.preedge_safe_window_width``, - ``EELS.min_distance_between_edges_for_fine_structure``. - - - -.. _logger-label: - -Messages log ------------- - -HyperSpy writes messages to the `Python logger -`_. The -default log level is "WARNING", meaning that only warnings and more severe -event messages will be displayed. The default can be set in the -:ref:`preferences `. Alternatively, it can be set -using :py:func:`~.logger.set_log_level` e.g.: - -.. code-block:: python - - >>> import hyperspy.api as hs - >>> hs.set_log_level('INFO') - >>> hs.load(r'my_file.dm3') - INFO:hyperspy.io_plugins.digital_micrograph:DM version: 3 - INFO:hyperspy.io_plugins.digital_micrograph:size 4796607 B - INFO:hyperspy.io_plugins.digital_micrograph:Is file Little endian? True - INFO:hyperspy.io_plugins.digital_micrograph:Total tags in root group: 15 - `_ to Jupyter Notebook, Qtconsole or JupyterLab -.. image:: images/download_hyperspy_button.png - :width: 350 - :align: center - :target: https://github.com/hyperspy/hyperspy-bundle/releases +.. raw:: html + +
For instructions, see the `HyperSpy bundle `__ repository. @@ -70,16 +74,16 @@ it can easily be installed using conda. To install HyperSpy run the following from the Anaconda Prompt on Windows or from a Terminal on Linux and Mac. - .. code-block:: bash +.. code-block:: bash - $ conda install hyperspy -c conda-forge + $ conda install hyperspy -c conda-forge This will also install the optional GUI packages ``hyperspy_gui_ipywidgets`` and ``hyperspy_gui_traitsui``. To install HyperSpy without the GUI packages, use: - .. code-block:: bash +.. code-block:: bash - $ conda install hyperspy-base -c conda-forge + $ conda install hyperspy-base -c conda-forge .. note:: @@ -119,29 +123,29 @@ solutions are: The following example illustrates how to create a new environment named ``hspy_environment``, activate it and install HyperSpy in the new environment. - .. code-block:: bash +.. code-block:: bash - $ conda create -n hspy_environment - $ conda activate hspy_environment - $ conda install hyperspy -c conda-forge + $ conda create -n hspy_environment + $ conda activate hspy_environment + $ conda install hyperspy -c conda-forge - .. note:: +.. note:: - A consequence of installing hyperspy in a new environment is that you need - to activate this environment using ``conda activate environment_name`` where - ``environment_name`` is the name of the environment, however `shortcuts` can - be created using different approaches: + A consequence of installing hyperspy in a new environment is that you need + to activate this environment using ``conda activate environment_name`` where + ``environment_name`` is the name of the environment, however `shortcuts` can + be created using different approaches: - - Install `start_jupyter_cm `_ - in the hyperspy environment. - - Install `nb_conda_kernels `_. - - Create `IPython kernels for different environment `_. + - Install `start_jupyter_cm `_ + in the hyperspy environment. + - Install `nb_conda_kernels `_. + - Create `IPython kernels for different environment `_. To learn more about the Anaconda eco-system: -- Choose between `Anaconda or Miniconda `_? +- Choose between `Anaconda or Miniconda `_? - Understanding `conda and pip `_. -- What is `conda-forge `__. +- What is `conda-forge `__. .. _install-with-pip: @@ -149,38 +153,38 @@ Installation using pip ---------------------- HyperSpy is listed in the `Python Package Index -`_. Therefore, it can be automatically downloaded -and installed `pip `__. You may need to +`_. Therefore, it can be automatically downloaded +and installed `pip `__. You may need to install pip for the following commands to run. To install all of HyperSpy's functionalities, run: - .. code-block:: bash +.. code-block:: bash - $ pip install hyperspy[all] + $ pip install hyperspy[all] To install only the strictly required dependencies and limited functionalities, use: - .. code-block:: bash +.. code-block:: bash - $ pip install hyperspy + $ pip install hyperspy See the following list of selectors to select the installation of optional dependencies required by specific functionalities: +* ``ipython`` for integration with the `ipython` terminal and parallel processing using `ipyparallel`, * ``learning`` for some machine learning features, -* ``gui-jupyter`` to use the `Jupyter widgets `_ +* ``gui-jupyter`` to use the `Jupyter widgets `_ GUI elements, -* ``gui-traitsui`` to use the GUI elements based on `traitsui `_, -* ``mrcz`` to read mrcz file, -* ``speed`` to speed up some functionalities, -* ``usid`` to read usid file, +* ``gui-traitsui`` to use the GUI elements based on `traitsui `_, +* ``speed`` install numba and numexpr to speed up some functionalities, * ``tests`` to install required libraries to run HyperSpy's unit tests, -* ``build-doc`` to install required libraries to build HyperSpy's documentation, +* ``coverage`` to coverage statistics when running the tests, +* ``doc`` to install required libraries to build HyperSpy's documentation, * ``dev`` to install all the above, * ``all`` to install all the above except the development requirements - (``tests``, ``build-doc`` and ``dev``). + (``tests``, ``doc`` and ``dev``). For example: @@ -204,18 +208,18 @@ Using conda To update hyperspy to the latest release using conda: - .. code-block:: bash +.. code-block:: bash - $ conda update hyperspy -c conda-forge + $ conda update hyperspy -c conda-forge Using pip ^^^^^^^^^ To update hyperspy to the latest release using pip: - .. code-block:: bash +.. code-block:: bash - $ pip install hyperspy --upgrade + $ pip install hyperspy --upgrade Install specific version ------------------------ @@ -225,21 +229,21 @@ Using conda To install a specific version of hyperspy (for example ``1.6.1``) using conda: - .. code-block:: bash +.. code-block:: bash - $ conda install hyperspy=1.6.1 -c conda-forge + $ conda install hyperspy=1.6.1 -c conda-forge Using pip ^^^^^^^^^ To install a specific version of hyperspy (for example ``1.6.1``) using pip: - .. code-block:: bash +.. code-block:: bash - $ pip install hyperspy==1.6.1 + $ pip install hyperspy==1.6.1 -.. _install-dev: +.. _install-rolling: Rolling release Linux distributions ----------------------------------- @@ -264,6 +268,8 @@ the stable release) is also available in the AUR as |python-hyperspy-git|_. .. |python-hyperspy-git| replace:: ``python-hyperspy-git`` .. _python-hyperspy-git: https://aur.archlinux.org/packages/python-hyperspy-git +.. _install-dev: + Install development version --------------------------- @@ -271,7 +277,7 @@ Clone the hyperspy repository ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To get the development version from our git repository you need to install `git -`_. Then just do: +`_. Then just do: .. code-block:: bash @@ -287,29 +293,37 @@ To get the development version from our git repository you need to install `git Installation in a Anaconda/Miniconda distribution ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Optionally, create an environment to separate your hyperspy installation from +other anaconda environments (`read more about environments here +`_): + +.. code-block:: bash + + $ conda create -n hspy_dev python # create an empty environment with latest python + $ conda activate hspy_dev # activate environment Install the runtime and development dependencies requirements using conda: .. code-block:: bash - $ conda install hyperspy-base -c conda-forge --only-deps - $ conda install hyperspy-dev -c conda-forge + $ conda install hyperspy-base -c conda-forge --only-deps # install hyperspy dependencies + $ conda install hyperspy-dev -c conda-forge # install developer dependencies The package ``hyperspy-dev`` will install the development dependencies required for testing and building the documentation. From the root folder of your hyperspy repository (folder containing the -``setup.py`` file) run `pip `_ in development mode: +``setup.py`` file) run `pip `_ in development mode: .. code-block:: bash - $ pip install -e . --no-deps + $ pip install -e . --no-deps # install the currently checked-out branch of hyperspy Installation in other (non-system) Python distribution ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From the root folder of your hyperspy repository (folder containing the -``setup.py`` file) run `pip `_ in development mode: +``setup.py`` file) run `pip `_ in development mode: .. code-block:: bash @@ -324,8 +338,6 @@ use the corresponding selector as explained in the :ref:`install-with-pip` secti can be installed through the AUR by installing the `hyperspy-git package `_ -.. _create-debian-binary: - Installation in a system Python distribution ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -333,12 +345,14 @@ When using a system Python distribution, it is recommended to install the dependencies using your system package manager. From the root folder of your hyperspy repository (folder containing the -``setup.py`` file) run `pip `_ in development mode. +``setup.py`` file) run `pip `_ in development mode. .. code-block:: bash $ pip install -e --user .[dev] +.. _create-debian-binary: + Creating Debian/Ubuntu binaries ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -353,4 +367,3 @@ You can create binaries for Debian/Ubuntu from the source by running the For this to work, the following packages must be installed in your system python-stdeb, debhelper, dpkg-dev and python-argparser are required. - diff --git a/doc/user_guide/interactive_operations.rst b/doc/user_guide/interactive_operations.rst new file mode 100644 index 0000000000..74dc996dc0 --- /dev/null +++ b/doc/user_guide/interactive_operations.rst @@ -0,0 +1,42 @@ +.. _interactive-label: + +Interactive Operations +********************** + +The function :func:`~.api.interactive` simplifies the definition of +operations that are automatically updated when an event is triggered. By +default the operation is recomputed when the data or the axes of the original +signal is changed. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(10.)) + >>> ssum = hs.interactive(s.sum, axis=0) + >>> ssum.data + array([45.]) + >>> s.data /= 10 + >>> s.events.data_changed.trigger(s) + >>> ssum.data + array([4.5]) + +Interactive operations can be performed in a chain. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(2 * 3 * 4).reshape((2, 3, 4))) + >>> ssum = hs.interactive(s.sum, axis=0) + >>> ssum_mean = hs.interactive(ssum.mean, axis=0) + >>> ssum_mean.data + array([30., 33., 36., 39.]) + >>> s.data # doctest: +SKIP + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]]]) + >>> s.data *= 10 + >>> s.events.data_changed.trigger(obj=s) + >>> ssum_mean.data + array([300., 330., 360., 390.]) diff --git a/doc/user_guide/interactive_operations_ROIs.rst b/doc/user_guide/interactive_operations_ROIs.rst deleted file mode 100644 index ac5fb3e761..0000000000 --- a/doc/user_guide/interactive_operations_ROIs.rst +++ /dev/null @@ -1,276 +0,0 @@ - - -Interactive Operations and Region of Interest (ROI) -*************************************************** - -.. _interactive-label: - -Interactive operations ----------------------- - - -The function :py:func:`~.interactive.interactive` simplifies the definition of -operations that are automatically updated when an event is triggered. By -default the operation is recomputed when the data or the axes of the original -signal is changed. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(10.)) - >>> ssum = hs.interactive(s.sum, axis=0) - >>> ssum.data - array([45.0]) - >>> s.data /= 10 - >>> s.events.data_changed.trigger(s) - >>> ssum.data - array([ 4.5]) - -Interactive operations can be performed in a chain. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(2 * 3 * 4).reshape((2, 3, 4))) - >>> ssum = hs.interactive(s.sum, axis=0) - >>> ssum_mean = hs.interactive(ssum.mean, axis=0) - >>> ssum_mean.data - array([ 30., 33., 36., 39.]) - >>> s.data - array([[[ 0, 1, 2, 3], - [ 4, 5, 6, 7], - [ 8, 9, 10, 11]], - - [[12, 13, 14, 15], - [16, 17, 18, 19], - [20, 21, 22, 23]]]) - >>> s.data *= 10 - >>> s.events.data_changed.trigger(obj=s) - >>> ssum_mean.data - array([ 300., 330., 360., 390.]) - -.. _roi-label: - -Region Of Interest (ROI) ------------------------- - -ROIs can be defined to select part of any compatible signal and may be applied -either to the navigation or to the signal axes. A number of different ROIs are -available: - -* :py:class:`~.roi.Point1DROI` -* :py:class:`~.roi.Point2DROI` -* :py:class:`~.roi.SpanROI` -* :py:class:`~.roi.RectangularROI` -* :py:class:`~.roi.CircleROI` -* :py:class:`~.roi.Line2DROI` - -Once created, an ROI can be applied to the signal: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(2000).reshape((20,10,10))) - >>> im = hs.signals.Signal2D(np.arange(100).reshape((10,10))) - >>> roi = hs.roi.RectangularROI(left=3, right=7, top=2, bottom=5) - >>> sr = roi(s) - >>> sr - - >>> imr = roi(im) - >>> imr - - -ROIs can also be used :ref:`interactively ` with widgets. -The following example shows how to interactively apply ROIs to an image. Note -that *it is necessary* to plot the signal onto which the widgets will be -added before calling :py:meth:`~.roi.BaseInteractiveROI.interactive`. - -.. code-block:: python - - >>> import scipy.misc - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) - >>> rectangular_roi = hs.roi.RectangularROI(left=30, right=500, - ... top=200, bottom=400) - >>> line_roi = hs.roi.Line2DROI(0, 0, 512, 512, 1) - >>> point_roi = hs.roi.Point2DROI(256, 256) - >>> im.plot() - >>> roi2D = rectangular_roi.interactive(im, color="blue") - >>> roi1D = line_roi.interactive(im, color="yellow") - >>> roi0D = point_roi.interactive(im, color="red") - - -.. figure:: images/image_with_rois.png - :align: center - :width: 500 - -.. figure:: images/roi1d.png - :align: center - :width: 500 - -.. figure:: images/roi2d.png - :align: center - :width: 500 - -.. NOTE:: - - Depending on your screen and display settings, it can be difficult to `pick` - or manipulate widgets and you can try to change the pick tolerance in - the :ref:`HyperSpy plot preferences `. - Typically, using a 4K resolution with a small scaling factor (<150 %), setting - the pick tolerance to 15 instead of 7.5 makes the widgets easier to manipulate. - -If instantiated without arguments, (i.e. ``rect = RectangularROI()`` the roi -will automatically determine sensible values to center it when -interactively adding it to a signal. This provides a conventient starting point -to further manipulate the ROI, either by hand or using the gui (i.e. ``rect.gui``). - -Notably, since ROIs are independent from the signals they sub-select, the widget -can be plotted on a different signal altogether. - -.. code-block:: python - - >>> import scipy.misc - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) - >>> s = hs.signals.Signal1D(np.random.rand(512, 512, 512)) - >>> roi = hs.roi.RectangularROI(left=30, right=77, top=20, bottom=50) - >>> s.plot() # plot signal to have where to display the widget - >>> imr = roi.interactive(im, navigation_signal=s, color="red") - >>> roi(im).plot() - -ROIs are implemented in terms of physical coordinates and not pixels, so with -proper calibration will always point to the same region. - -.. figure:: images/random_image_with_rect_roi.png - :align: center - :width: 500 - -.. figure:: images/random_image_with_rect_roi_spectrum.png - :align: center - :width: 500 - -.. figure:: images/roi2d.png - :align: center - :width: 500 - - -And of course, as all interactive operations, interactive ROIs are chainable. -The following example shows how to display interactively the histogram of a -rectangular ROI. Notice how we customise the default event connections in -order to increase responsiveness. - - -.. code-block:: python - - >>> import scipy.misc - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) - >>> im.plot() - >>> roi = hs.roi.RectangularROI(left=30, right=500, top=200, bottom=400) - >>> im_roi = roi.interactive(im, color="red") - >>> roi_hist = hs.interactive(im_roi.get_histogram, - ... event=roi.events.changed, - bins=150, # Set number of bins for `get_histogram` - ... recompute_out_event=None) - >>> roi_hist.plot() - - -.. figure:: images/image_with_rect_roi.gif - :align: center - :width: 100% - -.. versionadded:: 1.3 - ROIs can be used in place of slices when indexing and to define a - signal range in functions taken a ``signal_range`` argument. - - -All ROIs have a :meth:`gui` method that displays an user interface if -a hyperspy GUI is installed (currently only works with the -``hyperspy_gui_ipywidgets`` GUI), enabling precise control of the ROI -parameters: - -.. code-block:: python - - >>> # continuing from above: - >>> roi.gui() - -.. figure:: images/roi_gui_control.gif - :align: center - :width: 100% - -.. versionadded:: 1.4 - :meth:`~.roi.Line2DROI.angle` can be used to calculate an angle between - ROI line and one of the axes providing its name through optional argument ``axis``: - -.. code-block:: python - - >>> import scipy - >>> holo = hs.datasets.example_signals.object_hologram() - >>> roi = hs.roi.Line2DROI(x1=465.577, y1=445.15, x2=169.4, y2=387.731, linewidth=0) - >>> holo.plot() - >>> ss = roi.interactive(holo) - -.. figure:: images/roi_line2d_holo.png - :align: center - :width: 500 - -.. code-block:: python - - >>> roi.angle(axis='y') - -100.97166759025453 - -The default output of the method is in degrees, though radians can be selected -as follows: - -.. code-block:: python - - >>> roi.angle(axis='vertical', units='radians') - -1.7622880506791903 - -Conveniently, :meth:`~.roi.Line2DROI.angle` can be used to rotate an image to -align selected features with respect to vertical or horizontal axis: - -.. code-block:: python - ->>> holo.map(scipy.ndimage.rotate, angle=roi.angle(axis='horizontal'), inplace=False).plot() - -.. figure:: images/roi_line2d_rotate.png - :align: center - :width: 500 - - -.. _roi-slice-label: - -Slicing using ROIs ------------------- - -ROIs can be used in place of slices when indexing. For example: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> roi = hs.roi.SpanROI(left=5, right=15) - >>> sc = s.isig[roi] - >>> im = hs.datasets.example_signals.object_hologram() - >>> roi = hs.roi.RectangularROI(left=120, right=460., top=300, bottom=560) - >>> imc = im.isig[roi] - -.. versionadded:: 1.3 - :meth:`gui` method. - -.. versionadded:: 1.6 - New :meth:`__getitem__` method for all ROIs. - -In addition the following all ROIs have a py:meth:`__getitem__` method that enables -using them in place of tuples. -For example, the method :py:meth:`~._signals.signal2d.align2D` takes a ``roi`` -argument with the left, right, top, bottom coordinates of the ROI. -Handily, we can pass a :py:class:`~.roi.RectangularROI` ROI instead. - -.. code-block:: python - - >>> import hyperspy.api as hs - >>> import numpy as np - >>> im = hs.signals.Signal2D(np.random.random((10,30,30)) - >>> roi = hs.roi.RectangularROI(left=2, right=10, top=0, bottom=5)) - >>> tuple(roi) - (2.0, 10.0, 0.0, 5.0) - >>> im.align2D(roi=roi) - - - diff --git a/doc/user_guide/intro.rst b/doc/user_guide/intro.rst deleted file mode 100644 index 7940442c41..0000000000 --- a/doc/user_guide/intro.rst +++ /dev/null @@ -1,65 +0,0 @@ -Introduction -============ - -What is HyperSpy ----------------- - -HyperSpy is an open source Python library which provides tools to facilitate -the interactive data analysis of multidimensional datasets that can be -described as multidimensional arrays of a given signal (e.g. a 2D array of -spectra a.k.a spectrum image). - -HyperSpy aims at making it easy and natural to apply analytical procedures -that operate on an individual signal to multidimensional datasets of any -size, as well as providing easy access to analytical tools that exploit their -multidimensionality. - -.. versionadded:: 1.5 - External packages can extend HyperSpy by registering signals, - components and widgets. - - -.. _hyperspy_extensions-label: - -External packages can extend HyperSpy to e.g. implement features to analyse a -particular sort of data. For details on how to register extensions see -:ref:`writing_extensions-label`. For a list of packages that extend HyperSpy -follow `this link `_. - -.. note:: - From version 2.0, HyperSpy will be split into a core package (HyperSpy) - that will provide the common infrastructure and a number of HyperSpy - extensions specialized in the analysis of different types of data. - -HyperSpy's character --------------------- - -HyperSpy has been written by a subset of the people who use it, a particularity -that sets its character: - -* To us this program is a research tool, much like a screwdriver or a Green's - function. We believe that the better our tools are, the better our research - will be. We also think that it is beneficial for the advancement of knowledge - to share our research tools and to forge them in a collaborative way. This is - because by collaborating we advance faster, mainly by avoiding reinventing the - wheel. Idealistic as it may sound, many other people think like this and it is - thanks to them that this project exists. - -* Not surprisingly, we care about making it easy for others to contribute to - HyperSpy. In other words, - we aim at minimising the “user becomes developer” threshold. - Do you want to contribute already? No problem, see the :ref:`dev_guide-label` - for details. - -* The main way of interacting with the program is through scripting. - This is because `Jupyter `_ exists, making your - interactive data analysis productive, scalable, reproducible and, - most importantly, fun. That said, widgets to interact with HyperSpy - elements are provided where there - is a clear productivity advantage in doing so. See the - `hyperspy-gui-ipywidgets `_ - and - `hyperspy-gui-traitsui `_ - packages for details. Not enough? If you - need a full, standalone GUI, `HyperSpyUI `_ - is for you. diff --git a/doc/user_guide/io.rst b/doc/user_guide/io.rst index e2499192d7..a39bdead2f 100644 --- a/doc/user_guide/io.rst +++ b/doc/user_guide/io.rst @@ -1,50 +1,119 @@ .. _io: -*********************** Loading and saving data *********************** -.. contents:: - :depth: 3 +.. versionchanged:: 2.0 + + The IO plugins formerly developed within HyperSpy have been moved to + the separate package :external+rsciio:doc:`RosettaSciIO ` + in order to facilitate a wider use also by other packages. Plugins supporting + additional formats or corrections/enhancements to existing plugins should now + be contributed to the `RosettaSciIO repository `_ + and file format specific issues should be reported to the `RosettaSciIO issue + tracker `_. .. _loading_files: -Loading files: the load function -================================ +Loading +======= -HyperSpy can read and write to multiple formats (see :ref:`supported-formats`). -To load data use the :py:func:`~.load` command. For example, to load the -image spam.jpg you can type: +Basic usage +----------- + +HyperSpy can read and write to multiple formats (see :external+rsciio:ref:`supported-formats`). +To load data use the :func:`~.load` command. For example, to load the +image ``spam.jpg``, you can type: .. code-block:: python - >>> s = hs.load("spam.jpg") + >>> s = hs.load("spam.jpg") # doctest: +SKIP If loading was successful, the variable ``s`` contains a HyperSpy signal or any -type of signal defined in on of the :ref:`HyperSpy extensions ` -- see available :ref:`signal subclasses ` for more -information. To list the signal types available on your local installation use: +type of signal defined in one of the :ref:`HyperSpy extensions `, +see :ref:`load_specify_signal_type-label` for more details. + +.. note:: + + When the file contains several datasets, the :func:`~.api.load` function + will return a list of HyperSpy signals, instead of a single HyperSpy signal. + Each signal can then be accessed using list indexation. + + .. code-block:: python + + >>> s = hs.load("spameggsandham.hspy") # doctest: +SKIP + >>> s # doctest: +SKIP + [, + , + ] + + Using indexation to access the first signal (index 0): + + .. code-block:: python + + >>> s[0] # doctest: +SKIP + + + +.. HINT:: + + The load function returns an object that contains data read from the file. + We assign this object to the variable ``s`` but you can choose any (valid) + variable name you like. for the filename, don\'t forget to include the + quotation marks and the file extension. + +If no argument is passed to the load function, a window will be raised that +allows to select a single file through your OS file manager, e.g.: + +.. code-block:: python + + >>> # This raises the load user interface + >>> s = hs.load() # doctest: +SKIP + +It is also possible to load multiple files at once or even stack multiple +files. For more details read :ref:`load-multiple-label`. + +Specifying reader +----------------- + +HyperSpy will attempt to infer the appropriate file reader to use based on +the file extension (for example. ``.hspy``, ``.emd`` and so on). You can +override this using the ``reader`` keyword: + +.. code-block:: python + + # Load a .hspy file with an unknown extension + >>> s = hs.load("filename.some_extension", reader="hspy") # doctest: +SKIP + +.. _load_specify_signal_type-label: + +Specifying signal type +---------------------- + +HyperSpy will attempt to infer the most suitable signal type for the data being +loaded. Domain specific signal types are provided by :ref:`extension libraries +`. To list the signal types +available on your local installation use: .. code-block:: python - >>> hs.print_known_signal_types() + >>> hs.print_known_signal_types() # doctest: +SKIP -HyperSpy will try to guess the most likely data type for the corresponding -file. However, you can force it to read the data as a particular data type by -providing the ``signal_type`` keyword, which has to correspond to one of the -available subclasses of signal, e.g.: +When loading data, the signal type can be specified by providing the ``signal_type`` +keyword, which has to correspond to one of the available subclasses of signal +(The ``EELS`` signal type is provided by the extension :external+exspy:ref:`eXSpy `): .. code-block:: python - >>> s = hs.load("filename", signal_type="EELS") + >>> s = hs.load("filename", signal_type="EELS") # doctest: +SKIP -If the loaded file contains several datasets, the :py:func:`~.io.load` -functions will return a list of the corresponding signals: +If the loaded file contains several datasets, the :func:`~.api.load` +function will return a list of the corresponding signals: .. code-block:: python - >>> s = hs.load("spameggsandham.hspy") - >>> s + >>> s = hs.load("spameggsandham.hspy") # doctest: +SKIP + >>> s # doctest: +SKIP [, , ] @@ -52,69 +121,61 @@ functions will return a list of the corresponding signals: .. note:: Note for python programmers: the data is stored in a numpy array - in the :py:attr:`~.signal.BaseSignal.data` attribute, but you will not + in the :attr:`~.api.signals.BaseSignal.data` attribute, but you will not normally need to access it there. -HyperSpy will attempt to infer the appropriate file reader to use based on -the file extension (for example. ``.hspy``, ``.emd`` and so on). You can -override this using the ``reader`` keyword: - -.. code-block:: python +Metadata +-------- - # Load a .hspy file with an unknown extension - >>> s = hs.load("filename.some_extension", reader="hspy") - -Some file formats store some extra information about the data (metadata) and -HyperSpy reads most of them and stores them in the -:py:attr:`~.signal.BaseSignal.original_metadata` attribute. Also, depending on -the file format, a part of this information will be mapped by HyperSpy to the -:py:attr:`~.signal.BaseSignal.metadata` attribute, where it can be used by -e.g. routines operating on the signal. See :ref:`metadata structure +Most scientific file formats store some extra information about the data and the +conditions under which it was acquired (metadata). HyperSpy reads most of them and +stores them in the :attr:`~.api.signals.BaseSignal.original_metadata` attribute. +Also, depending on the file format, a part of this information will be mapped by +HyperSpy to the :attr:`~.api.signals.BaseSignal.metadata` attribute, where it can +for example be used by routines operating on the signal. See the :ref:`metadata structure ` for details. .. note:: Extensive metadata can slow down loading and processing, and - loading the :py:attr:`~.signal.BaseSignal.original_metadata` can be disabled - using the ``load_original_metadata`` argument of the :py:func:`~.load` - function; in this case, the :py:attr:`~.signal.BaseSignal.metadata` will - still be populated. + loading the :attr:`~.api.signals.BaseSignal.original_metadata` can be disabled + using the ``load_original_metadata`` argument of the :func:`~.load` + function. If this argument is set to `False`, the + :attr:`~.api.signals.BaseSignal.metadata` will still be populated. To print the content of the attributes simply use: .. code-block:: python - >>> s.original_metadata - >>> s.metadata + >>> s.original_metadata # doctest: +SKIP + >>> s.metadata # doctest: +SKIP -The :py:attr:`~.signal.BaseSignal.original_metadata` and -:py:attr:`~.signal.BaseSignal.metadata` can be exported to text files -using the :py:meth:`~.misc.utils.DictionaryTreeBrowser.export` method, e.g.: +The :attr:`~.api.signals.BaseSignal.original_metadata` and +:attr:`~.api.signals.BaseSignal.metadata` can be exported to text files +using the :meth:`~.misc.utils.DictionaryTreeBrowser.export` method, e.g.: .. code-block:: python - >>> s.original_metadata.export('parameters') + >>> s.original_metadata.export('parameters') # doctest: +SKIP .. _load_to_memory-label: -.. deprecated:: 1.2 - ``memmap_dir`` and ``load_to_memory`` :py:func:`~.io.load` keyword - arguments. Use ``lazy`` instead of ``load_to_memory``. ``lazy`` makes - ``memmap_dir`` unnecessary. +Lazy loading of large datasets +------------------------------ -.. versionadd: 1.2 +.. versionadded:: 1.2 ``lazy`` keyword argument. Almost all file readers support `lazy` loading, which means accessing the data -without loading it to memory (see :ref:`supported-formats` for a list). This -feature can be useful when analysing large files. To use this feature set -``lazy`` to ``True`` e.g.: +without loading it to memory (see :external+rsciio:ref:`supported-formats` for a +list). This feature can be useful when analysing large files. To use this feature, +set ``lazy`` to ``True`` e.g.: .. code-block:: python - >>> s = hs.load("filename.hspy", lazy=True) + >>> s = hs.load("filename.hspy", lazy=True) # doctest: +SKIP -More details on lazy evaluation support in :ref:`big-data-label`. +More details on lazy evaluation support can be found in :ref:`big-data-label`. The units of the navigation and signal axes can be converted automatically during loading using the ``convert_units`` parameter. If `True`, the @@ -132,20 +193,20 @@ functions, e.g.: .. code-block:: python - >>> s = hs.load(["file1.hspy", "file2.hspy"]) + >>> s = hs.load(["file1.hspy", "file2.hspy"]) # doctest: +SKIP -or by using `shell-style wildcards `_: +or by using `shell-style wildcards `_: .. code-block:: python - >>> s = hs.load("file*.hspy") + >>> s = hs.load("file*.hspy") # doctest: +SKIP Alternatively, regular expression type character classes can be used such as ``[a-z]`` for lowercase letters or ``[0-9]`` for one digit integers: .. code-block:: python - >>> s = hs.load('file[0-9].hspy') + >>> s = hs.load('file[0-9].hspy') # doctest: +SKIP .. note:: @@ -160,7 +221,7 @@ Alternatively, regular expression type character classes can be used such as >>> # /home/data/afile[1x1].hspy >>> # /home/data/afile[1x2].hspy - >>> s = hs.load("/home/data/afile[*].hspy", escape_square_brackets=True) + >>> s = hs.load("/home/data/afile[*].hspy", escape_square_brackets=True) # doctest: +SKIP HyperSpy also supports ```pathlib.Path`` `_ objects, for example: @@ -171,12 +232,12 @@ objects, for example: >>> from pathlib import Path >>> # Use pathlib.Path - >>> p = Path("/path/to/a/file.hspy") - >>> s = hs.load(p) + >>> p = Path("/path/to/a/file.hspy") # doctest: +SKIP + >>> s = hs.load(p) # doctest: +SKIP >>> # Use pathlib.Path.glob - >>> p = Path("/path/to/some/files/").glob("*.hspy") - >>> s = hs.load(p) + >>> p = Path("/path/to/some/files/").glob("*.hspy") # doctest: +SKIP + >>> s = hs.load(p) # doctest: +SKIP By default HyperSpy will return a list of all the files loaded. Alternatively, by setting ``stack=True``, HyperSpy can be instructed to stack the data - given @@ -187,1581 +248,66 @@ per file must also match, or an error will be raised. .. code-block:: python - >>> ls + >>> ls # doctest: +SKIP CL1.raw CL1.rpl CL2.raw CL2.rpl CL3.raw CL3.rpl CL4.raw CL4.rpl LL3.raw LL3.rpl shift_map-SI3.npy hdf5/ - >>> s = hs.load('*.rpl') - >>> s + >>> s = hs.load('*.rpl') # doctest: +SKIP + >>> s # doctest: +SKIP [, , , , ] - >>> s = hs.load('*.rpl', stack=True) - >>> s + >>> s = hs.load('*.rpl', stack=True) # doctest: +SKIP + >>> s # doctest: +SKIP +.. _example-data-label: -.. _saving_files: +Loading example data and data from online databases +--------------------------------------------------- -Saving data to files -==================== - -To save data to a file use the :py:meth:`~.signal.BaseSignal.save` method. The -first argument is the filename and the format is defined by the filename -extension. If the filename does not contain the extension, the default format -(:ref:`hspy-format`) is used. For example, if the :py:const:`s` variable -contains the :py:class:`~.signal.BaseSignal` that you want to write to a file, -the following will write the data to a file called :file:`spectrum.hspy` in the -default :ref:`hspy-format` format: - -.. code-block:: python - - >>> s.save('spectrum') - -If you want to save to the :ref:`ripple format ` instead, write: - -.. code-block:: python - - >>> s.save('spectrum.rpl') - -Some formats take extra arguments. See the relevant subsections of -:ref:`supported-formats` for more information. - - -.. _supported-formats: - -Supported formats -================= - -Here is a summary of the different formats that are currently supported by -HyperSpy. The "lazy" column specifies if lazy evaluation is supported. - - -.. table:: Supported file formats - - +-----------------------------------+--------+--------+--------+ - | Format | Read | Write | lazy | - +===================================+========+========+========+ - | Gatan's dm3 | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | Gatan's dm4 | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | FEI's emi and ser | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | hspy | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | Image: e.g. jpg, png, tif, ... | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | TIFF | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | MRC | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | MRCZ | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | EMSA/MSA | Yes | Yes | No | - +-----------------------------------+--------+--------+--------+ - | NetCDF | Yes | No | No | - +-----------------------------------+--------+--------+--------+ - | Ripple | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | SEMPER unf | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | Blockfile | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | DENS heater log | Yes | No | No | - +-----------------------------------+--------+--------+--------+ - | Bruker's bcf | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | Bruker's spx | Yes | No | No | - +-----------------------------------+--------+--------+--------+ - | EMD (NCEM) | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | EMD (Velox) | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | Protochips log | Yes | No | No | - +-----------------------------------+--------+--------+--------+ - | EDAX spc and spd | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | h5USID h5 | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | Phenom elid | Yes | No | No | - +-----------------------------------+--------+--------+--------+ - | DigitalSurf's sur and pro | Yes | No | No | - +-----------------------------------+--------+--------+--------+ - | Nexus nxs | Yes | Yes | Yes | - +-----------------------------------+--------+--------+--------+ - | EMPAD xml | Yes | No | Yes | - +-----------------------------------+--------+--------+--------+ - | JEOL asw, map, img, pts, eds | Yes | No | No | - +-----------------------------------+--------+--------+--------+ - -.. _hspy-format: - -HSpy - HyperSpy's HDF5 Specification ------------------------------------- - -This is the default format and it is the only one that guarantees that no -information will be lost in the writing process and that supports saving data -of arbitrary dimensions. It is based on the `HDF5 open standard -`_. The HDF5 file format is supported by `many -applications -`_. -Part of the specification is documented in :ref:`metadata_structure`. - -.. versionadded:: 1.2 - Enable saving HSpy files with the ``.hspy`` extension. Previously only the - ``.hdf5`` extension was recognised. - -.. versionchanged:: 1.3 - The default extension for the HyperSpy HDF5 specification is now ``.hspy``. - The option to change the default is no longer present in ``preferences``. - -Only loading of HDF5 files following the HyperSpy specification are supported. -Usually their extension is ``.hspy`` extension, but older versions of HyperSpy -would save them with the ``.hdf5`` extension. Both extensions are recognised -by HyperSpy since version 1.2. However, HyperSpy versions older than 1.2 -won't recognise the ``.hspy`` extension. To -workaround the issue when using old HyperSpy installations simply change the -extension manually to ``.hdf5`` or -save directly the file using this extension by explicitly adding it to the -filename e.g.: +HyperSpy is distributed with some example data that can be found in +:mod:`~.api.data`: .. code-block:: python - >>> s = hs.signals.BaseSignal([0]) - >>> s.save('test.hdf5') + >>> s = hs.data.two_gaussians() + >>> s.plot() +.. versionadded:: 1.4 + :mod:`~.api.data` (formerly ``hyperspy.api.datasets.artificial_data``) -When saving to ``hspy``, all supported objects in the signal's -:py:attr:`~.signal.BaseSignal.metadata` is stored. This includes lists, tuples and signals. -Please note that in order to increase saving efficiency and speed, if possible, -the inner-most structures are converted to numpy arrays when saved. This -procedure homogenizes any types of the objects inside, most notably casting -numbers as strings if any other strings are present: +There are also artificial datasets, which are made to resemble real +experimental data. .. code-block:: python - >>> # before saving: - >>> somelist - [1, 2.0, 'a name'] - >>> # after saving: - ['1', '2.0', 'a name'] - -The change of type is done using numpy "safe" rules, so no information is lost, -as numbers are represented to full machine precision. - -This feature is particularly useful when using -:py:meth:`~hyperspy._signals.eds.EDSSpectrum.get_lines_intensity`: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.metadata.Sample.intensities = s.get_lines_intensity() - >>> s.save('EDS_spectrum.hspy') - - >>> s_new = hs.load('EDS_spectrum.hspy') - >>> s_new.metadata.Sample.intensities - [, - , - , - , - ] - -.. versionadded:: 1.3.1 - ``chunks`` keyword argument - -The hyperspy HDF5 format supports chunking the data into smaller pieces to make it possible to load only part -of a dataset at a time. By default, the data is saved in chunks that are optimised to contain at least one -full signal shape for non-lazy signal, while for lazy signal, the chunking of the dask is used. It is possible to -customise the chunk shape using the ``chunks`` keyword. -For example, to save the data with ``(20, 20, 256)`` chunks instead of the default ``(7, 7, 2048)`` chunks -for this signal: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.random.random((100, 100, 2048))) - >>> s.save("test_chunks", chunks=(20, 20, 256)) - -Note that currently it is not possible to pass different customised chunk shapes to all signals and -arrays contained in a signal and its metadata. Therefore, the value of ``chunks`` provided on saving -will be applied to all arrays contained in the signal. - -By passing ``True`` to ``chunks`` the chunk shape is guessed using ``h5py``'s ``guess_chunk`` function -what, for large signal spaces usually leads to smaller chunks as ``guess_chunk`` does not impose the -constrain of storing at least one signal per chunks. For example, for the signal in the example above -passing ``chunks=True`` results in ``(7, 7, 256)`` chunks. - -Choosing the correct chunk-size can significantly affect the speed of reading, writing and performance of many hyperspy algorithms. -See the `chunking section `__ under `Working with big data `__ for more information. - -Extra saving arguments -^^^^^^^^^^^^^^^^^^^^^^ - -- ``compression``: One of ``None``, ``'gzip'``, ``'szip'``, ``'lzf'`` (default is ``'gzip'``). - ``'szip'`` may be unavailable as it depends on the HDF5 installation including it. - -.. note:: - - HyperSpy uses h5py for reading and writing HDF5 files and, therefore, it - supports all `compression filters supported by h5py `_. - The default is ``'gzip'``. It is possible to enable other compression filters - such as ``blosc`` by installing e.g. `hdf5plugin `_. - However, be aware that loading those files will require installing the package - providing the compression filter. If not available an error will be raised. - - Compression can significantly increase the saving speed. If file size is not - an issue, it can be disabled by setting ``compression=None``. Notice that only - ``compression=None`` and ``compression='gzip'`` are available in all platforms, - see the `h5py documentation `_ - for more details. Therefore, if you choose any other compression filter for - saving a file, be aware that it may not be possible to load it in some platforms. - - -.. _netcdf-format: - -NetCDF ------- - -This was the default format in HyperSpy's predecessor, EELSLab, but it has been -superseded by :ref:`hspy-format` in HyperSpy. We provide only reading capabilities -but we do not support writing to this format. - -Note that only NetCDF files written by EELSLab are supported. - -To use this format a python netcdf interface must be installed manually because -it is not installed by default when using the automatic installers. - - -.. _mrc-format: - -MRC ---- - -This is a format widely used for tomographic data. Our implementation is based -on `this specification -`_. We also -partly support FEI's custom header. We do not provide writing features for this -format, but, as it is an open format, we may implement this feature in the -future on demand. - -For mrc files ``load`` takes the ``mmap_mode`` keyword argument enabling -loading the file using a different mode (default is copy-on-write) . However, -note that lazy loading does not support in-place writing (i.e lazy loading and -the "r+" mode are incompatible). - -.. _mrcz-format: - -MRCZ ----- - -MRCZ is an extension of the CCP-EM MRC2014 file format. `CCP-EM MRC2014 -`_ file format. It uses the -`blosc` meta-compression library to bitshuffle and compress files in a blocked, -multi-threaded environment. The supported data types are: - -[`float32`,`int8`,`uint16`,`int16`,`complex64`] - -It supports arbitrary meta-data, which is serialized into JSON. - -MRCZ also supports asynchronous reads and writes. - -Repository: https://github.com/em-MRCZ -PyPI: https://pypi.python.org/pypi/mrcz -Citation: Submitted. -Preprint: http://www.biorxiv.org/content/early/2017/03/13/116533 - -Support for this format is not enabled by default. In order to enable it -install the `mrcz` and optionally the `blosc` Python packages. - -Extra saving arguments -^^^^^^^^^^^^^^^^^^^^^^ - -- ``do_async``: currently supported within HyperSpy for writing only, this will - save the file in a background thread and return immediately. Defaults - to `False`. - -.. Warning:: - - There is no method currently implemented within Hyperspy to tell if an - asychronous write has finished. - - -- ``compressor``: The compression codec, one of [`None`,`'zlib`',`'zstd'`, `'lz4'`]. - Defaults to `None`. -- ``clevel``: The compression level, an `int` from 1 to 9. Defaults to 1. -- ``n_threads``: The number of threads to use for 'blosc' compression. Defaults to - the maximum number of virtual cores (including Intel Hyperthreading) - on your system, which is recommended for best performance. If \ - ``do_async = True`` you may wish to leave one thread free for the - Python GIL. - -The recommended compression codec is 'zstd' (zStandard) with `clevel=1` for -general use. If speed is critical, use 'lz4' (LZ4) with `clevel=9`. Integer data -compresses more redably than floating-point data, and in general the histogram -of values in the data reflects how compressible it is. - -To save files that are compatible with other programs that can use MRC such as -GMS, IMOD, Relion, MotionCorr, etc. save with `compressor=None`, extension `.mrc`. -JSON metadata will not be recognized by other MRC-supporting software but should -not cause crashes. - -Example Usage -^^^^^^^^^^^^^ - -.. code-block:: python - - >>> s.save('file.mrcz', do_async=True, compressor='zstd', clevel=1) - - >>> new_signal = hs.load('file.mrcz') - + >>> s = hs.data.atomic_resolution_image() + >>> s.plot() -.. _msa-format: - -EMSA/MSA --------- - -This `open standard format -`__ -is widely used to exchange single spectrum data, but it does not support -multidimensional data. It can be used to exchange single spectra with Gatan's -Digital Micrograph. - -.. WARNING:: - If several spectra are loaded and stacked (``hs.load('pattern', stack_signals=True``) - the calibration read from the first spectrum and applied to all other spectra. - -Extra saving arguments -^^^^^^^^^^^^^^^^^^^^^^ - -For the MSA format the ``format`` argument is used to specify whether the -energy axis should also be saved with the data. The default, 'Y' omits the -energy axis in the file. The alternative, 'XY', saves a second column with the -calibrated energy data. It is possible to personalise the separator with the -`separator` keyword. - -.. Warning:: - - However, if a different separator is chosen the resulting file will not - comply with the MSA/EMSA standard and HyperSpy and other software may not - be able to read it. - -The default encoding is `latin-1`. It is possible to set a different encoding -using the `encoding` argument, e.g.: - -.. code-block:: python - - >>> s.save('file.msa', encoding = 'utf8') - - -.. _ripple-format: - -Ripple ------- - -This *open standard format* developed at NIST as native format for -`Lispix `_ is widely used to exchange -multidimensional data. However, it only supports data of up to three -dimensions. It can also be used to exchange data with Bruker and used in -combination with the :ref:`import-rpl` it is very useful for exporting data -to Gatan's Digital Micrograph. - -The default encoding is latin-1. It is possible to set a different encoding -using the encoding argument, e.g.: - -.. code-block:: python - - >>> s.save('file.rpl', encoding = 'utf8') - - -For mrc files ``load`` takes the ``mmap_mode`` keyword argument enabling -loading the file using a different mode (default is copy-on-write) . However, -note that lazy loading does not support in-place writing (i.e lazy loading and -the "r+" mode are incompatible). - -.. _image-format: - -Images ------- - -HyperSpy can read and write data to `all the image formats -`_ supported by -`imageio`, which uses the Python Image Library (PIL/pillow). -This includes png, pdf, gif, etc. - -When saving an image, a scalebar can be added to the image and the formatting, -location, etc. of the scalebar can be set using the ``scalebar_kwds`` arguments -- see the `matplotlib-scalebar `_ -documentation for more information. - -.. code-block:: python - - >>> s.save('file.jpg', scalebar=True) - >>> s.save('file.jpg', scalebar=True, scalebar_kwds={'location':'lower right'}) - -When saving an image, keyword arguments can be passed to the corresponding -pillow file writer. - -It is important to note that these image formats only support 8-bit files, and -therefore have an insufficient dynamic range for most scientific applications. -It is therefore highly discouraged to use any general image format (with the -exception of :ref:`tiff-format` which uses another library) to store data for -analysis purposes. - -.. _tiff-format: - -TIFF ----- - -HyperSpy can read and write 2D and 3D TIFF files using using -Christoph Gohlke's ``tifffile`` library. In particular, it supports reading and -writing of TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH, and FluoView files. Most of -these are uncompressed or losslessly compressed 2**(0 to 6) bit integer, 16, 32 -and 64-bit float, grayscale and RGB(A) images, which are commonly used in -bio-scientific imaging. See `the library webpage -`_ for more details. - -.. versionadded: 1.0 - Add support for writing/reading scale and unit to tif files to be read with - ImageJ or DigitalMicrograph - -Currently HyperSpy has limited support for reading and saving the TIFF tags. -However, the way that HyperSpy reads and saves the scale and the units of TIFF -files is compatible with ImageJ/Fiji and Gatan Digital Micrograph software. -HyperSpy can also import the scale and the units from TIFF files saved using -FEI, Zeiss SEM and Olympus SIS software. - -.. code-block:: python - - >>> # Force read image resolution using the x_resolution, y_resolution and - >>> # the resolution_unit of the TIFF tags. Be aware, that most of the - >>> # software doesn't (properly) use these tags when saving TIFF files. - >>> s = hs.load('file.tif', force_read_resolution=True) - -HyperSpy can also read and save custom tags through the ``tifffile`` -library. - -.. code-block:: python - - >>> # Saving the string 'Random metadata' in a custom tag (ID 65000) - >>> extratag = [(65000, 's', 1, "Random metadata", False)] - >>> s.save('file.tif', extratags=extratag) - - >>> # Saving the string 'Random metadata' from a custom tag (ID 65000) - >>> s2 = hs.load('file.tif') - >>> s2.original_metadata['Number_65000'] - b'Random metadata' - -.. warning:: - - The file will be saved with the same bit depth as the signal. Since - most processing operations in HyperSpy and numpy will result in 64-bit - floats, this can result in 64-bit ``.tiff`` files, which are not always - compatible with other imaging software. - - You can first change the dtype of the signal before saving: - - .. code-block:: python - - >>> s.data.dtype - dtype('float64') - >>> s.change_dtype('float32') - >>> s.data.dtype - dtype('float32') - >>> s.save('file.tif') - -.. _dm3-format: - -Gatan Digital Micrograph ------------------------- - -HyperSpy can read both dm3 and dm4 files but the reading features are not -complete (and probably they will remain so unless Gatan releases the -specifications of the format). That said, we understand that this is an -important feature and if loading a particular Digital Micrograph file fails for -you, please report it as an issue in the `issues tracker -`__ to make -us aware of the problem. - -Some of the tags in the DM-files are added to the metadata of the signal -object. This includes, microscope information and certain parameters for EELS, -EDS and CL signals. - -Extra loading arguments -^^^^^^^^^^^^^^^^^^^^^^^ - -- ``optimize``: bool, default is True. During loading, the data is replaced by its - :ref:`optimized copy ` to speed up operations, - e. g. iteration over navigation axes. The cost of this speed improvement is to - double the memory requirement during data loading. - -.. warning:: - - It has been reported that in some versions of Gatan Digital Micrograph, - any binned data stores the _averages_ of the binned channels or pixels, - rather than the _sum_, which would be required for proper statistical - analysis. We therefore strongly recommend that all binning is performed - using Hyperspy where possible. - - See the original `bug report here `_. - - -.. _edax-format: - -EDAX TEAM/Genesis SPD and SPC ------------------------------ - -HyperSpy can read both ``.spd`` (spectrum image) and ``.spc`` (single spectra) -files from the EDAX TEAM software and its predecessor EDAX Genesis. -If reading an ``.spd`` file, the calibration of the -spectrum image is loaded from the corresponding ``.ipr`` and ``.spc`` files -stored in the same directory, or from specific files indicated by the user. -If these calibration files are not available, the data from the ``.spd`` -file will still be loaded, but with no spatial or energy calibration. -If elemental information has been defined in the spectrum image, those -elements will automatically be added to the signal loaded by HyperSpy. - -Currently, loading an EDAX TEAM spectrum or spectrum image will load an -``EDSSEMSpectrum`` Signal. If support for TEM EDS data is needed, please -open an issue in the `issues tracker `__ to -alert the developers of the need. - -For further reference, file specifications for the formats are -available publicly available from EDAX and are on Github -(`.spc `_, -`.spd `_, and -`.ipr `_). - -Extra loading arguments for SPD file -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- ``spc_fname``: {None, str}, name of file from which to read the spectral calibration. If data was exported fully from EDAX TEAM software, an .spc file with the same name as the .spd should be present. If `None`, the default filename will be searched for. Otherwise, the name of the ``.spc`` file to use for calibration can be explicitly given as a string. -- ``ipr_fname``: {None, str}, name of file from which to read the spatial calibration. If data was exported fully from EDAX TEAM software, an ``.ipr`` file with the same name as the ``.spd`` (plus a "_Img" suffix) should be present. If `None`, the default filename will be searched for. Otherwise, the name of the ``.ipr`` file to use for spatial calibration can be explicitly given as a string. -- ``**kwargs``: remaining arguments are passed to the Numpy ``memmap`` function. - -Extra loading arguments for SPD and SPC files -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- ``load_all_spc`` : bool, switch to control if all of the ``.spc`` header is - read, or just the important parts for import into HyperSpy. - - -.. _fei-format: - -FEI TIA SER and EMI -------------------- - -HyperSpy can read ``ser`` and ``emi`` files but the reading features are not -complete (and probably they will be unless FEI releases the specifications of -the format). That said we know that this is an important feature and if loading -a particular ser or emi file fails for you, please report it as an issue in the -`issues tracker `__ to make us -aware of the problem. - -HyperSpy (unlike TIA) can read data directly from the ``.ser`` files. However, -by doing so, the information that is stored in the emi file is lost. -Therefore strongly recommend to load using the ``.emi`` file instead. - -When reading an ``.emi`` file if there are several ``.ser`` files associated -with it, all of them will be read and returned as a list. - - -Extra loading arguments -^^^^^^^^^^^^^^^^^^^^^^^ - -- ``only_valid_data`` : bool, in case of series or linescan data with the - acquisition stopped before the end: if True, load only the acquired data. - If False, the empty data are filled with zeros. The default is False and this - default value will change to True in version 2.0. - -.. _unf-format: - -SEMPER UNF binary format ------------------------- - -SEMPER is a fully portable system of programs for image processing, particularly -suitable for applications in electron microscopy developed by Owen Saxton (see -DOI: 10.1016/S0304-3991(79)80044-3 for more information). The unf format is a -binary format with an extensive header for up to 3 dimensional data. -HyperSpy can read and write unf-files and will try to convert the data into a -fitting BaseSignal subclass, based on the information stored in the label. -Currently version 7 of the format should be fully supported. - -.. _blockfile-format: - -Blockfile ---------- - -HyperSpy can read and write the blockfile format from NanoMegas ASTAR software. -It is used to store a series of diffraction patterns from scanning precession -electron diffraction (SPED) measurements, with a limited set of metadata. The -header of the blockfile contains information about centering and distortions -of the diffraction patterns, but is not applied to the signal during reading. -Blockfiles only support data values of type -`np.uint8 `_ (integers -in range 0-255). - -.. warning:: - - While Blockfiles are supported, it is a proprietary format, and future - versions of the format might therefore not be readable. Complete - interoperability with the official software can neither be guaranteed. - -Blockfiles are by default loaded in a "copy-on-write" manner using -`numpy.memmap -`_ . -For blockfiles ``load`` takes the ``mmap_mode`` keyword argument enabling -loading the file using a different mode. However, note that lazy loading -does not support in-place writing (i.e lazy loading and the "r+" mode -are incompatible). - -Extra saving arguments -^^^^^^^^^^^^^^^^^^^^^^ - -- ``intensity_scaling`` : in case the dataset that needs to be saved does not - have the `np.uint8` data type, casting to this datatype without intensity - rescaling results in overflow errors (default behavior). This option allows - you to perform linear intensity scaling of the images prior to saving the - data. The options are: - - - `'dtype'`: the limits of the datatype of the dataset, e.g. 0-65535 for - `np.uint16`, are mapped onto 0-255 respectively. Does not work for `float` - data types. - - `'minmax'`: the minimum and maximum in the dataset are mapped to 0-255. - - `'crop'`: everything below 0 and above 255 is set to 0 and 255 respectively - - 2-tuple of `floats` or `ints`: the intensities between these values are - scaled between 0-255, everything below is 0 and everything above is 255. -- ``navigator_signal``: the BLO file also stores a virtual bright field (VBF) image which - behaves like a navigation signal in the ASTAR software. By default this is - set to `'navigator'`, which results in the default :py:attr:`navigator` signal to be used. - If this signal was not calculated before (e.g. by calling :py:meth:`~.signal.BaseSignal.plot`), it is - calculated when :py:meth:`~.signal.BaseSignal.save` is called, which can be time consuming. - Alternatively, setting the argument to `None` will result in a correctly sized - zero array to be used. Finally, a custom ``Signal2D`` object can be passed, - but the shape must match the navigation dimensions. - -.. _dens-format: - -DENS heater log ---------------- - -HyperSpy can read heater log format for DENS solution's heating holder. The -format stores all the captured data for each timestamp, together with a small -header in a plain-text format. The reader extracts the measured temperature -along the time axis, as well as the date and calibration constants stored in -the header. - -Bruker's formats ----------------- -Bruker's Esprit(TM) software and hardware allows to acquire and save the data -in different kind of formats. Hyperspy can read two main basic formats: bcf -and spx. - -.. _bcf-format: - -Bruker composite file -^^^^^^^^^^^^^^^^^^^^^ - -HyperSpy can read "hypermaps" saved with Bruker's Esprit v1.x or v2.x in bcf -hybrid (virtual file system/container with xml and binary data, optionally -compressed) format. Most bcf import functionality is implemented. Both -high-resolution 16-bit SEM images and hyperspectral EDX data can be retrieved -simultaneously. - -BCF can look as all inclusive format, however it does not save some key EDX -parameters: any of dead/live/real times, FWHM at Mn_Ka line. However, real time -for whole map is calculated from pixelAverage, lineAverage, pixelTime, -lineCounter and map height parameters. - -Note that Bruker Esprit uses a similar format for EBSD data, but it is not -currently supported by HyperSpy. - -Extra loading arguments -+++++++++++++++++++++++ - -- ``select_type`` : one of (None, 'spectrum', 'image'). If specified, only the - corresponding type of data, either spectrum or image, is returned. - By default (None), all data are loaded. -- ``index`` : one of (None, int, "all"). Allow to select the index of the dataset - in the bcf file, which can contains several datasets. Default None value - result in loading the first dataset. When set to 'all', all available datasets - will be loaded and returned as separate signals. -- ``downsample`` : the downsample ratio of hyperspectral array (height and width - only), can be integer >=1, where '1' results in no downsampling (default 1). - The underlying method of downsampling is unchangeable: sum. Differently than - ``block_reduce`` from skimage.measure it is memory efficient (does not creates - intermediate arrays, works inplace). -- ``cutoff_at_kV`` : if set (can be int or float >= 0) can be used either to crop - or enlarge energy (or channels) range at max values (default None). - -Example of loading reduced (downsampled, and with energy range cropped) -"spectrum only" data from bcf (original shape: 80keV EDS range (4096 channels), -100x75 pixels): - -.. code-block:: python - - >>> hs.load("sample80kv.bcf", select_type='spectrum', downsample=2, cutoff_at_kV=10) - - -load the same file without extra arguments: - -.. code-block:: python - - >>> hs.load("sample80kv.bcf") - [, - , - ] - -The loaded array energy dimension can by forced to be larger than the data -recorded by setting the 'cutoff_at_kV' kwarg to higher value: - -.. code-block:: python - - >>> hs.load("sample80kv.bcf", cutoff_at_kV=80) - [, - , - ] - -Note that setting downsample to >1 currently locks out using SEM imagery -as navigator in the plotting. - -.. _spx-format: - -SPX format -^^^^^^^^^^ - -Hyperspy can read Bruker's spx format (single spectra format based on XML). -The format contains extensive list of details and parameters of EDS analyses -which are mapped in hyperspy to metadata and original_metadata dictionaries. - -.. _emd-format: - -EMD ---- - -EMD stands for “Electron Microscopy Dataset.” It is a subset of the open source -HDF5 wrapper format. N-dimensional data arrays of any standard type can be -stored in an HDF5 file, as well as tags and other metadata. - -EMD (NCEM) -^^^^^^^^^^ - -This `EMD format `_ was developed by Colin Ophus at the -National Center for Electron Microscopy (NCEM). -This format is used by the `prismatic software `_ -to save the simulation outputs. - -Extra loading arguments -+++++++++++++++++++++++ - -- ``dataset_path`` : None, str or list of str. Path of the dataset. If None, - load all supported datasets, otherwise the specified dataset(s). -- ``stack_group`` : bool, default is True. Stack datasets of groups with common - path. Relevant for emd file version >= 0.5 where groups can be named - 'group0000', 'group0001', etc. -- ``chunks`` : None, True or tuple. Determine the chunking of the dataset to save. - See the ``chunks`` arguments of the ``hspy`` file format for more details. - - -For files containing several datasets, the `dataset_name` argument can be -used to select a specific one: - -.. code-block:: python - - >>> s = hs.load("adatafile.emd", dataset_name="/experimental/science_data_1/data") - - -Or several by using a list: - -.. code-block:: python - - >>> s = hs.load("adatafile.emd", - ... dataset_name=[ - ... "/experimental/science_data_1/data", - ... "/experimental/science_data_2/data"]) - - -.. _emd_fei-format: - -EMD (Velox) -^^^^^^^^^^^ - -This is a non-compliant variant of the standard EMD format developed by -Thermo-Fisher (former FEI). HyperSpy supports importing images, EDS spectrum and EDS -spectrum streams (spectrum images stored in a sparse format). For spectrum -streams, there are several loading options (described below) to control the frames -and detectors to load and if to sum them on loading. The default is -to import the sum over all frames and over all detectors in order to decrease -the data size in memory. - - -.. note:: - - Pruned Velox EMD files only contain the spectrum image in a proprietary - format that HyperSpy cannot read. Therefore, don't prune Velox EMD files - if you intend to read them with HyperSpy. - -.. code-block:: python - - >>> hs.load("sample.emd") - [, - ] - -.. note:: - - FFTs made in Velox are loaded in as-is as a HyperSpy ComplexSignal2D object. - The FFT is not centered and only positive frequencies are stored in the file. - Making FFTs with HyperSpy from the respective image datasets is recommended. - -.. note:: - - DPC data is loaded in as a HyperSpy ComplexSignal2D object. - -.. note:: - - Currently only lazy uncompression rather than lazy loading is implemented. - This means that it is not currently possible to read EDS SI Velox EMD files - with size bigger than the available memory. - - -.. warning:: - - This format is still not stable and files generated with the most recent - version of Velox may not be supported. If you experience issues loading - a file, please report it to the HyperSpy developers so that they can - add support for newer versions of the format. - - -.. _Extra-loading-arguments-fei-emd: - -Extra loading arguments -+++++++++++++++++++++++ - -- ``select_type`` : one of {None, 'image', 'single_spectrum', 'spectrum_image'} (default is None). -- ``first_frame`` : integer (default is 0). -- ``last_frame`` : integer (default is None) -- ``sum_frames`` : boolean (default is True) -- ``sum_EDS_detectors`` : boolean (default is True) -- ``rebin_energy`` : integer (default is 1) -- ``SI_dtype`` : numpy dtype (default is None) -- ``load_SI_image_stack`` : boolean (default is False) - -The ``select_type`` parameter specifies the type of data to load: if `image` is selected, -only images (including EDS maps) are loaded, if `single_spectrum` is selected, only -single spectra are loaded and if `spectrum_image` is selected, only the spectrum -image will be loaded. The ``first_frame`` and ``last_frame`` parameters can be used -to select the frame range of the EDS spectrum image to load. To load each individual -EDS frame, use ``sum_frames=False`` and the EDS spectrum image will be loaded -with an extra navigation dimension corresponding to the frame index -(time axis). Use the ``sum_EDS_detectors=True`` parameter to load the signal of -each individual EDS detector. In such a case, a corresponding number of distinct -EDS signal is returned. The default is ``sum_EDS_detectors=True``, which loads the -EDS signal as a sum over the signals from each EDS detectors. The ``rebin_energy`` -and ``SI_dtype`` parameters are particularly useful in combination with -``sum_frames=False`` to reduce the data size when one want to read the -individual frames of the spectrum image. If ``SI_dtype=None`` (default), the dtype -of the data in the emd file is used. The ``load_SI_image_stack`` parameter allows -loading the stack of STEM images acquired simultaneously as the EDS spectrum image. -This can be useful to monitor any specimen changes during the acquisition or to -correct the spatial drift in the spectrum image by using the STEM images. - -.. code-block:: python - - >>> hs.load("sample.emd", sum_EDS_detectors=False) - [, - , - , - , - ] - - >>> hs.load("sample.emd", sum_frames=False, load_SI_image_stack=True, SI_dtype=np.int8, rebin_energy=4) - [, - ] - - - -.. _protochips-format: - -Protochips log --------------- - -HyperSpy can read heater, biasing and gas cell log files for Protochips holder. -The format stores all the captured data together with a small header in a csv -file. The reader extracts the measured quantity (e. g. temperature, pressure, -current, voltage) along the time axis, as well as the notes saved during the -experiment. The reader returns a list of signal with each signal corresponding -to a quantity. Since there is a small fluctuation in the step of the time axis, -the reader assumes that the step is constant and takes its mean, which is a -good approximation. Further release of HyperSpy will read the time axis more -precisely by supporting non-uniform axis. - - -.. _usid-format: - -USID ----- - -Background -^^^^^^^^^^ - -`Universal Spectroscopy and Imaging Data `_ -(USID) is an open, community-driven, self-describing, and standardized schema for -representing imaging and spectroscopy data of any size, dimensionality, precision, -instrument of origin, or modality. USID data is typically stored in -Hierarchical Data Format Files (HDF5) and the combination of USID within HDF5 files is -referred to as h5USID. - -`pyUSID `_ -provides a convenient interface to I/O operations on such h5USID files. USID -(via pyUSID) forms the foundation for other materials microscopy scientific -python package called `pycroscopy `_. -If you have any questions regarding this module, please consider -`contacting `_ -the developers of pyUSID. - -Requirements -^^^^^^^^^^^^ - -1. Reading and writing h5USID files require the - `installation of pyUSID `_. -2. Files must use the ``.h5`` file extension in order to use this io plugin. - Using the ``.hdf5`` extension will default to HyperSpy's own plugin. - -Reading -^^^^^^^ - -h5USID files can contain multiple USID datasets within the same file. -HyperSpy supports reading in one or more USID datasets. - -Extra loading arguments -+++++++++++++++++++++++ - -- ``dataset_path``: str. Absolute path of USID Main HDF5 dataset. - (default is ``None`` - all USID Main Datasets will be read) -- ``ignore_non_linear_dims``: bool, default is True. If True, parameters that - were varied non-linearly in the desired dataset will result in Exceptions. - Else, all such non-linearly varied parameters will be treated as - linearly varied parameters and a Signal object will be generated. - - -Reading the sole dataset within a h5USID file: - -.. code-block:: python - - >>> hs.load("sample.h5") - - -If multiple datasets are present within the h5USID file and you try the same command again, -**all** available datasets will be loaded. - -.. note:: - - Given that HDF5 files can accommodate very large datasets, setting ``lazy=True`` - is strongly recommended if the contents of the HDF5 file are not known apriori. - This prevents issues with regard to loading datasets far larger than memory. - - Also note that setting ``lazy=True`` leaves the file handle to the HDF5 file open. - If it is important that the files be closed after reading, set ``lazy=False``. - -.. code-block:: python - - >>> hs.load("sample.h5") - [, - ] - -We can load a specific dataset using the ``dataset_path`` keyword argument. Setting it to the -absolute path of the desired dataset will cause the single dataset to be loaded. - -.. code-block:: python - - >>> # Loading a specific dataset - >>> hs.load("sample.h5", dataset_path='/Measurement_004/Channel_003/Main_Data') - - -h5USID files support the storage of HDF5 dataset with -`compound data types `_. -As an (*oversimplified*) example, one could store a color image using a compound data type that allows -each color channel to be accessed by name rather than an index. -Naturally, reading in such a compound dataset into HyperSpy will result in a separate -signal for each named component in the dataset: - -.. code-block:: python - - >>> hs.load("file_with_a_compound_dataset.h5") - [, - Signal2D, title: blue, dimensions: (|128, 128)>, - Signal2D, title: green, dimensions: (|128, 128)>] - -h5USID files also support parameters or dimensions that have been varied non-uniformly. -This capability is important in several spectroscopy techniques where the bias is varied as a -`bi-polar triangular waveform `_ -rather than uniformly from the minimum value to the maximum value. -Since HyperSpy Signals expect uniform variation of parameters / axes, such non-uniform information -would be lost in the axes manager. The USID plugin will default to a warning -when it encounters a parameter that has been varied non-uniformly: - -.. code-block:: python - - >>> hs.load("sample.h5") - UserWarning: Ignoring non-uniformity of dimension: Bias - - -Obviously, the -In order to prevent accidental misinterpretation of information downstream, the keyword argument -``ignore_non_uniform_dims`` can be set to ``False`` which will result in a ``ValueError`` instead. - -.. code-block:: python - - >>> hs.load("sample.h5") - ValueError: Cannot load provided dataset. Parameter: Bias was varied non-uniformly. - Supply keyword argument "ignore_non_uniform_dims=True" to ignore this error - -Writing -^^^^^^^ - -Signals can be written to new h5USID files using the standard :py:meth:`~.signal.BaseSignal.save` function. -Setting the ``overwrite`` keyword argument to ``True`` will append to the specified -HDF5 file. All other keyword arguments will be passed to -`pyUSID.hdf_utils.write_main_dataset() `_ - -.. code-block:: python - - >>> sig.save("USID.h5") - -Note that the model and other secondary data artifacts linked to the signal are not -written to the file but these can be implemented at a later stage. - -.. _nexus-format: - -Nexus ------ - -Background -^^^^^^^^^^ - -`NeXus `_ is a common data format originally -developed by the neutron and x-ray science x-ray communities. It is still being -developed as an international standard by scientists and programmers -representing major scientific facilities in order to facilitate greater -cooperation in the analysis and visualization of data. -Nexus uses a variety of classes to record data, values, -units and other experimental metadata associated with an experiment. -For specific types of experiments an Application Definition may exist, which -defines an agreed common layout that facilities can adhere to. - -Nexus metadata and data are stored in Hierarchical Data Format Files (HDF5) with -a .nxs extension although standards HDF5 extensions are sometimes used. -Files must use the ``.nxs`` file extension in order to use this io plugin. -Using the ``.nxs`` extension will default to the Nexus loader. If your file has -a HDF5 extension, you can also explicitly set the Nexus file reader: - -.. code-block:: python - - # Load a NeXus file with a .h5 extension - >>> s = hs.load("filename.h5", reader="nxs") - -The loader will follow version 3 of the -`Nexus data rules `_. -The signal type, Signal1D or Signal2D, will be inferred by the ``interpretation`` -attribute, if this is set to ``spectrum`` or ``image``, in the ``NXdata`` -description. If the `interpretation -`_ attribute is -not set, the loader will return a ``BaseSignal``, which must then be converted -to the appropriate signal type. Following the Nexus data rules, if a ``default`` -dataset is not defined, the loader will load NXdata -and HDF datasets according to the keyword options in the reader. -A number of the `Nexus examples `_ -from large facilties do not use NXdata or use older versions of the Nexus -implementation. Data can still be loaded from these files but information or -associations may be missing. However, this missing information can be recovered -from within the ``original_metadata`` which contains the overall structure of -the entry. - -As the Nexus format uses the HDF5 format and needs to read both data and -metadata structured in different ways, the loader is written to be quite -flexible and can also be used to inspect any hdf5 based file. - - -Differences with respect to hspy -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HyperSpy metadata structure stores arrays as hdf datasets without attributes -and stores floats, ints and strings as attributes. -Nexus formats typically use hdf datasets attributes to store additional -information such as an indication of the units for an axis or the NX_class which -the dataset structure follows. The metadata, hyperspy or original_metadata, -therefore needs to be able to indicate the values and attributes of a dataset. -To implement this structure the ``value`` and ``attrs`` of a dataset can also be -defined. The value of a dataset is set using a ``value`` key. -The attributes of a dataset are defined by an ``attrs`` key. - -For example, to store an array called ``axis_x``, with a units attribute within -original_metadata, the following structure would be used: - -:: - - ├──original_metadata - │ ├── axis_x - │ │ ├── value : array([1.0,2.0,3.0,4.0,5.0]) - │ │ ├── attrs - │ │ │ ├── units : mm - - -.. code-block:: python - - >>> original_metadata.set_item(axis_x.value,[1.0,2.0,3.0,4.0,5.0]) - >>> original_metadata.set_item(axis_x.attrs.units,"mm") - -To access the axis information: - -.. code-block:: python - - >>> original_metadata.axis_x.value - >>> original_metadata.axis_x.attrs.units - -To modify the axis information: - -.. code-block:: python - - >>> original_metadata.axis_x.value = [2.0,3.0,4.0,5.0,6.0] - >>> original_metadata.axis_x.attrs.units = "um" - -To store data in a Nexus monochromator format, ``value`` -and ``attrs`` keys can define additional attributes: - -:: - - ├── monochromator - │ ├── energy - │ │ ├── value : 12.0 - │ │ ├── attrs - │ │ │ ├── units : keV - │ │ │ ├── NXclass : NXmonochromator - - -The ``attrs`` key can also be used to define Nexus structures for the definition -of structures and relationships between data: - -:: - - ├── mydata - │ ├── attrs - │ │ ├── NX_class : "NXdata" - │ │ ├── axes : ["x","."] - │ ├── data - │ │ ├──value : [[30,23...110] - │ ├── x - │ │ ├──value : [1,2.....100] - │ │ ├── attrs - │ │ │ ├── unit : "mm" - - -The use of ``attrs`` or ``value`` to set values within the metadata is optional -and metadata values can also be set, read or modified in the normal way. - - -.. code-block:: python - - >>> original_metadata.monochromator.energy = 12.5 - -HyperSpy metadata is stored within the Nexus file and should be automatically -restored when a signal is loaded from a previously saved Nexus file. - -.. note:: - - Altering the standard metadata structure of a signal - using ``attrs`` or ``value`` keywords is not recommended. - -Reading -^^^^^^^ -Nexus files can contain multiple datasets within the same file, but the -ordering of datasets can vary depending on the setup of an experiment or -processing step when the data was collected. -For example, in one experiment Fe, Ca, P, Pb were collected but in the next experiment -Ca, P, K, Fe, Pb were collected. HyperSpy supports reading in one or more datasets -and returns a list of signals but in this example case the indexing is different. -To control which data or metadata is loaded and in what order -some additional loading arguments are provided. - -Extra loading arguments -+++++++++++++++++++++++ - -- ``dataset_key``: ``None``, ``str`` or ``list`` of strings - Default is ``None`` . String(s) to search for in the path to find one or more datasets. -- ``dataset_path``: ``None``, ``str`` or ``list`` of strings - Default is ``None`` . Absolute path(s) to search for in the path to find one or more datasets. -- ``metadata_key``: ``None``, ``str`` or ``list`` of strings - Default is ``None`` . Absolute path(s) or string(s) to search for in the path to find metadata. -- ``skip_array_metadata``: ``bool`` - Default is False. Option to skip loading metadata that are arrays to avoid duplicating loading of data. -- ``nxdata_only``: ``bool`` - Default is False. Option to only convert NXdata formatted data to signals. -- ``hardlinks_only``: ``bool`` - Default is False. Option to ignore soft or External links in the file. -- ``use_default``: ``bool`` - Default is False. Only load the ``default`` dataset, if defined, from the file. Otherwise load according to the other keyword options. - -.. note:: - - Given that HDF5 files can accommodate very large datasets, setting ``lazy=True`` - is strongly recommended if the content of the HDF5 file is not known apriori. - This prevents issues with regard to loading datasets far larger than memory. - - Also note that setting ``lazy=True`` leaves the file handle to the HDF5 file open - and it can be closed with :py:meth:`~._signals.lazy.LazySignal.close_file` - or when using :py:meth:`~._signals.lazy.LazySignal.compute` with ``close_file=True``. - - -Reading a Nexus file (a single Nexus dataset): - -.. code-block:: python - - >>> sig = hs.load("sample.nxs") - -By default, the loader will look for stored NXdata objects. -If there are hdf datasets which are not stored as NXdata, but which -should be loaded as signals, set the ``nxdata_only`` keyword to False and all -hdf datasets will be returned as signals: - -.. code-block:: python - - >>> sig = hs.load("sample.nxs", nxdata_only=False) - -We can load a specific dataset using the ``dataset_path`` keyword argument. -Setting it to the absolute path of the desired dataset will cause -the single dataset to be loaded: - -.. code-block:: python - - >>> # Loading a specific dataset - >>> hs.load("sample.nxs", dataset_path="/entry/experiment/EDS/data") - -We can also choose to load datasets based on a search key using the -``dataset_key`` keyword argument. This can also be used to load NXdata not -outside of the ``default`` version 3 rules. Instead of providing an absolute -path, a string can be provided as well, and datasets with this key will be -returned. The previous example could also be written as: - -.. code-block:: python - - >>> # Loading datasets containing the string "EDS" - >>> hs.load("sample.nxs", dataset_key="EDS") - -The difference between ``dataset_path`` and ``dataset_key`` is illustrated -here: - -.. code-block:: python - - >>> # Only the dataset /entry/experiment/EDS/data will be loaded - >>> hs.load("sample.nxs", dataset_path="/entry/experiment/EDS/data") - >>> # All datasets contain the entire string "/entry/experiment/EDS/data" will be loaded - >>> hs.load("sample.nxs", dataset_key="/entry/experiment/EDS/data") - -Multiple datasets can be loaded by providing a number of keys: - -.. code-block:: python - - >>> # Loading a specific dataset - >>> hs.load("sample.nxs", dataset_key=["EDS", "Fe", "Ca"]) - -Metadata can also be filtered in the same way using ``metadata_key``: - -.. code-block:: python - - >>> # Load data with metadata matching metadata_key - >>> hs.load("sample.nxs", metadata_key="entry/instrument") - -.. note:: - - The Nexus loader removes any NXdata blocks from the metadata. - -Metadata that are arrays can be skipped by using ``skip_array_metadata``: - -.. code-block:: python - - >>> # Load data while skipping metadata that are arrays - >>> hs.load("sample.nxs", skip_array_metadata=True) - -Nexus files also support parameters or dimensions that have been varied -non-linearly. Since HyperSpy Signals expect linear variation of parameters / -axes, such non-linear information would be lost in the axes manager and -replaced with indices. -Nexus and HDF can result in large metadata structures with large datasets within the loaded -original_metadata. If lazy loading is used this may not be a concern but care must be taken -when saving the data. To control whether large datasets are loaded or saved, -use the ``metadata_key`` to load only the most relevant information. Alternatively, -set ``skip_array_metadata`` to ``True`` to avoid loading those large datasets in original_metadata. - - -Writing -^^^^^^^ -Signals can be written to new Nexus files using the standard :py:meth:`~.signal.BaseSignal.save` -function. - -Extra saving arguments -++++++++++++++++++++++ -- ``save_original_metadata``: ``bool`` - Default is True, option to save the original_metadata when storing to file. -- ``skip_metadata_key``: ``bool`` - ``None``, ``str`` or ``list`` of strings - Default is ``None``. Option to skip certain metadata keys when storing to file. -- ``use_default``: ``bool`` - Default is False. Set the ``default`` attribute for the Nexus file. - -.. code-block:: python - - >>> sig.save("output.nxs") - -Using the save method will store the nexus file with the following structure: - -:: - - ├── entry1 - │ ├── signal_name - │ │ ├── auxiliary - │ │ │ ├── original_metadata - │ │ │ ├── hyperspy_metadata - │ │ │ ├── learning_results - │ │ ├── signal_data - │ │ │ ├── data and axes (NXdata format) - - -The original_metadata can include hdf datasets which you may not wish to store. -The original_metadata can be omitted using ``save_original_metadata``. - -.. code-block:: python - - >>> sig.save("output.nxs", save_original_metadata=False) - -If only certain metadata are to be ignored, use ``skip_metadata_key``: - -.. code-block:: python - - >>> sig.save("output.nxs", skip_metadata_key=['xsp3', 'solstice_scan']) - -To save multiple signals, the file_writer method can be called directly. - -.. code-block:: python - - >>> from hyperspy.io_plugins.nexus import file_writer - >>> file_writer("test.nxs",[signal1,signal2]) - -When saving multiple signals, a default signal can be defined. This can be used when storing -associated data or processing steps along with a final result. All signals can be saved but -a single signal can be marked as the default for easier loading in HyperSpy or plotting with Nexus tools. -The default signal is selected as the first signal in the list: - -.. code-block:: python - - >>> from hyperspy.io_plugins.nexus import file_writer - >>> import hyperspy.api as hs - >>> file_writer("test.nxs", [signal1, signal2], use_default = True) - >>> hs.load("test.nxs", use_default = True) - -The output will be arranged by signal name: - -:: - - ├── entry1 (NXentry) - │ ├── signal_name (NXentry) - │ │ ├── auxiliary (NXentry) - │ │ │ ├── original_metadata (NXcollection) - │ │ │ ├── hyperspy_metadata (NXcollection) - │ │ │ ├── learning_results (NXcollection) - │ │ ├── signal_data (NXdata format) - │ │ │ ├── data and axes - ├── entry2 (NXentry) - │ ├── signal_name (NXentry) - │ │ ├── auxiliary (NXentry) - │ │ │ ├── original_metadata (NXcollection) - │ │ │ ├── hyperspy_metadata (NXcollection) - │ │ │ ├── learning_results (NXcollection) - │ │ ├── signal_data (NXdata) - │ │ │ ├── data and axes - - -.. note:: - - Signals saved as nxs by this plugin can be loaded normally and the - original_metadata, signal data, axes, metadata and learning_results - will be restored. Model information is not currently stored. - Nexus does not store how the data should be displayed. - To preserve the signal details an additional navigation attribute - is added to each axis to indicate if it is a navigation axis. - - -Inspecting -^^^^^^^^^^ -Looking in a Nexus or HDF file for specific metadata is often useful - e.g. to find -what position a specific stage was at. The methods ``read_metadata_from_file`` -and ``list_datasets_in_file`` can be used to load the file contents or -list the hdf datasets contained in a file. The inspection methods use the same ``metadata_key`` or ``dataset_key`` as when loading. -For example to search for metadata in a file: - - >>> from hyperspy.io_plugins.nexus import read_metadata_from_file - >>> read_metadata_from_file("sample.hdf5",metadata_key=["stage1_z"]) - {'entry': {'instrument': {'scannables': {'stage1': {'stage1_z': {'value': -9.871700000000002, - 'attrs': {'gda_field_name': 'stage1_z', - 'local_name': 'stage1.stage1_z', - 'target': '/entry/instrument/scannables/stage1/stage1_z', - 'units': 'mm'}}}}}}} - -To list the datasets stored in the file: - - >>> from hyperspy.io_plugins.nexus import read_datasets_from_file - >>> list_datasets_in_file("sample.nxs") - NXdata found - /entry/xsp3_addetector - /entry/xsp3_addetector_total - HDF datasets found - /entry/solstice_scan/keys/uniqueKeys - /entry/solstice_scan/scan_shape - Out[3]: - (['/entry/xsp3_addetector', '/entry/xsp3_addetector_total'], - ['/entry/solstice_scan/keys/uniqueKeys', '/entry/solstice_scan/scan_shape']) - - -.. _sur-format: - -SUR and PRO format ------------------- - -This is a format developed by the digitalsurf company to handle various types of -scientific measurements data such as profilometer, SEM, AFM, RGB(A) images, multilayer -surfaces and profiles. Even though it is essentially a surfaces format, 1D signals -are supported for spectra and spectral maps. Specifically, this file format is used -by Attolight SA for its scanning electron microscope cathodoluminescence -(SEM-CL) hyperspectral maps. Metadata parsing is supported, including user-specific -metadata, as well as the loading of files containing multiple objects packed together. - -The plugin was developed based on the MountainsMap software documentation, which -contains a description of the binary format. - -.. _empad-format: - -EMPAD format ------------- - -This is the file format used by the Electron Microscope Pixel Array -Detector (EMPAD). It is used to store a series of diffraction patterns from -scanning transmission electron diffraction measurements, with a limited set of -metadata. Similarly, to the :ref:`ripple format `, the raw data -and metadata are saved in two different files and for the EMPAD reader, these -are saved in the ``raw`` and ``xml`` files, respectively. To read EMPAD data, -use the ``xml`` file: - -.. code-block:: python - - >>> sig = hs.load("file.xml") - - -which will automatically read the raw data from the ``raw`` file too. The -filename of the ``raw`` file is defined in the ``xml`` file, which implies -changing the file name of the ``raw`` file will break reading the file. - - -.. _elid_format-label: - -Phenom ELID format ------------------- - -This is the file format used by the software package Element Identification for the Thermo -Fisher Scientific Phenom desktop SEM. It is a proprietary binary format which can contain -images, single EDS spectra, 1D line scan EDS spectra and 2D EDS spectrum maps. The reader -will convert all signals and its metadata into hyperspy signals. - -The current implementation supports ELID files created with Element Identification version -3.8.0 and later. You can convert older ELID files by loading the file into a recent Element -Identification release and then save the ELID file into the newer file format. - -.. _jeol_format-label: - -JEOL ASW format ---------------- - -This is the file format used by the `JEOL Analysist Station software` for which -hyperspy can read the ``asw``, ``pts``, ``map`` and ``eds`` format. To read the -calibration, it is required to load the ``asw`` file, which will load all others -files automatically. - -Extra loading arguments -^^^^^^^^^^^^^^^^^^^^^^^ +.. _saving_files: -- ``rebin_energy`` : Factor used to rebin the energy dimension. It must be a - multiple of the number of channels, typically 4096. (default 1) -- ``sum_frames`` : If False, each individual frame (sweep in JEOL software jargon) - is loaded. Be aware that loading each individual will use a lot of memory, - however, it can be used in combination with ``rebin_energy``, ``cutoff_at_kV`` - and ``downsample`` to reduce memory usage. - (default True). -- ``SI_dtype`` : set dtype of the eds dataset. Useful to adjust memory usage - and maximum number of X-rays per channel. (default np.unit8) -- ``cutoff_at_kV`` : if set (can be int or float >= 0), use to crop the energy - range up the specified energy. If ``None``, the whole energy range is loaded. - Useful to reduce memory usage. (default None). -- ``downsample`` : the downsample ratio of the navigation dimension of EDS - dataset, it can be integer or a tuple of length 2 to define ``x`` and ``y`` - separetely and it must be a mutiple of the size of the navigation dimension. - (default 1). +Saving +====== -Example of loading data downsampled, and with energy range cropped with the -original navigation dimension 512 x 512 and the EDS range 40 keV over 4096 -channels: +To save data to a file use the :meth:`~.api.signals.BaseSignal.save` method. The +first argument is the filename and the format is defined by the filename +extension. If the filename does not contain the extension, the default format +(:external+rsciio:ref:`HSpy-HDF5 `) is used. For example, if the ``s`` variable +contains the :class:`~.api.signals.BaseSignal` that you want to write to a file, +the following will write the data to a file called :file:`spectrum.hspy` in the +default :external+rsciio:ref:`HSpy-HDF5 ` format: .. code-block:: python - >>> hs.load("sample40kv.asw", downsample=8, cutoff_at_kV=10) - [, - , - , - ] + >>> s.save('spectrum') # doctest: +SKIP -load the same file without extra arguments: +If you want to save to the :external+rsciio:ref:`ripple format ` instead, write: .. code-block:: python - >>> hs.load("sample40kv.asw") - [, - , - , - ] - - -Reading data generated by HyperSpy using other software packages -================================================================ - -The following scripts may help reading data generated by HyperSpy using -other software packages. - - -.. _import-rpl: - -ImportRPL Digital Micrograph plugin ------------------------------------ - -This Digital Micrograph plugin is designed to import Ripple files into Digital Micrograph. -It is used to ease data transit between DigitalMicrograph and HyperSpy without losing -the calibration using the extra keywords that HyperSpy adds to the standard format. - -When executed it will ask for 2 files: - -#. The riple file with the data format and calibrations -#. The data itself in raw format. - -If a file with the same name and path as the riple file exits -with raw or bin extension it is opened directly without prompting. -ImportRPL was written by Luiz Fernando Zagonel. - -`Download ImportRPL `_ - - -HDF5 reader plugin for Digital Micrograph ------------------------------------------ - -This Digital Micrograph plugin is designed to import HDF5 files and like the -`ImportRPL` script above, it can be used to easily transfer data from HyperSpy to -Digital Micrograph by using the HDF5 hyperspy format (``hspy`` extension). - -Download ``gms_plugin_hdf5`` from its `Github repository `__. - - -.. _hyperspy-matlab: - -readHyperSpyH5 MATLAB Plugin ----------------------------- - -This MATLAB script is designed to import HyperSpy's saved HDF5 files (``.hspy`` extension). -Like the Digital Micrograph script above, it is used to easily transfer data -from HyperSpy to MATLAB, while retaining spatial calibration information. + >>> s.save('spectrum.rpl') # doctest: +SKIP -Download ``readHyperSpyH5`` from its `Github repository `__. +Some formats take extra arguments. See the corresponding pages at +:external+rsciio:ref:`supported-formats` for more information. diff --git a/doc/user_guide/metadata_structure.rst b/doc/user_guide/metadata_structure.rst deleted file mode 100644 index 8ad5406846..0000000000 --- a/doc/user_guide/metadata_structure.rst +++ /dev/null @@ -1,525 +0,0 @@ -.. _metadata_structure: - - -Metadata structure -****************** - -The :class:`~.signal.BaseSignal` class stores metadata in the -:attr:`~.signal.BaseSignal.metadata` attribute that has a tree structure. By -convention, the nodes labels are capitalized and the leaves are not -capitalized. - -When a leaf contains a quantity that is not dimensionless, the units can be -given in an extra leaf with the same label followed by the "_units" suffix. - -The metadata structure is represented in the following tree diagram. The -default units are given in parentheses. Details about the leaves can be found -in the following sections of this chapter. - -:: - - ├── Acquisition_instrument - │ ├── SEM - │ │ ├── Detector - │ │ │ ├── detector_type - │ │ │ └── EDS - │ │ │ ├── azimuth_angle (º) - │ │ │ ├── elevation_angle (º) - │ │ │ ├── energy_resolution_MnKa (eV) - │ │ │ ├── live_time (s) - │ │ │ └── real_time (s) - │ │ ├── beam_current (nA) - │ │ ├── beam_energy (keV) - │ │ ├── probe_area (nm²) - │ │ ├── convergence_angle (mrad) - │ │ ├── magnification - │ │ ├── microscope - │ │ ├── Stage - │ │ │ ├── rotation (º) - │ │ │ ├── tilt_alpha (º) - │ │ │ ├── tilt_beta (º) - │ │ │ ├── x (mm) - │ │ │ ├── y (mm) - │ │ │ └── z (mm) - │ │ └── working_distance (mm) - │ └── TEM - │ ├── Detector - │ │ ├── EDS - │ │ │ ├── azimuth_angle (º) - │ │ │ ├── elevation_angle (º) - │ │ │ ├── energy_resolution_MnKa (eV) - │ │ │ ├── live_time (s) - │ │ │ └── real_time (s) - │ │ └── EELS - │ │ ├── aperture (mm) - │ │ ├── collection_angle (mrad) - │ │ ├── dwell_time (s) - │ │ ├── exposure (s) - │ │ ├── frame_number - │ │ └── spectrometer - │ ├── Biprism - │ │ ├── azimuth_angle (º) - │ │ ├── position - │ │ └── voltage (V) - │ ├── acquisition_mode - │ ├── beam_current (nA) - │ ├── beam_energy (keV) - │ ├── probe_area (nm²) - │ ├── camera_length (mm) - │ ├── convergence_angle (mrad) - │ ├── magnification - │ ├── microscope - │ └── Stage - │ ├── rotation (º) - │ ├── tilt_alpha (º) - │ ├── tilt_beta (º) - │ ├── x (mm) - │ ├── y (mm) - │ └── z (mm) - ├── General - │ ├── authors - │ ├── date - │ ├── doi - │ ├── original_filename - │ ├── notes - │ ├── time - │ ├── time_zone - │ └── title - ├── Sample - │ ├── credits - │ ├── description - │ ├── elements - │ ├── thickness - │ └── xray_lines - └── Signal - ├── FFT - │ └── shifted - ├── Noise_properties - │ ├── Variance_linear_model - │ │ ├── correlation_factor - │ │ ├── gain_factor - │ │ ├── gain_offset - │ │ └── parameters_estimation_method - │ └── variance - ├── quantity - ├── signal_type - └── signal_origin - -General -======= - -title - type: Str - - A title for the signal, e.g. "Sample overview" - -original_filename - type: Str - - If the signal was loaded from a file this key stores the name of the - original file. - -time_zone - type: Str - - The time zone as supported by the python-dateutil library, e.g. "UTC", - "Europe/London", etc. It can also be a time offset, e.g. "+03:00" or - "-05:00". - -time - type: Str - - The acquisition or creation time in ISO 8601 time format, e.g. '13:29:10'. - -date - type: Str - - The acquisition or creation date in ISO 8601 date format, e.g. - '2018-01-28'. - - -authors - type: Str - - The authors of the data, in Latex format: Surname1, Name1 and Surname2, - Name2, etc. - -doi - type: Str - - Digital object identifier of the data, e. g. doi:10.5281/zenodo.58841. - -notes - type: Str - - Notes about the data. - -Acquisition_instrument -====================== - -TEM ---- - -Contain information relevant to transmission electron microscope signals. - -microscope - type: Str - - The microscope model, e.g. VG 501 - -acquisition_mode - type: Str - - Either 'TEM' or 'STEM' - -camera_length - type: Float - - The camera length in mm. - -convergence_angle - type: Float - - The beam convergence semi-angle in mrad. - -beam_energy - type: Float - - The energy of the electron beam in keV - -beam_current - type: Float - - The beam current in nA. - -probe_area - type: Float - - The illumination area of the electron beam in nm\ :sup:`2`. - -dwell_time - type: Float - - The dwell time in seconds. This is relevant for STEM acquisition - -exposure - type: Float - - The exposure time in seconds. This is relevant for TEM acquisition. - -magnification - type: Float - - The magnification. - -SEM ---- - -Contain information relevant to scanning electron microscope signals. - -microscope - type: Str - - The microscope model, e.g. VG 501 - -convergence_angle - type: Float - - The beam convergence semi-angle in mrad. - -beam_energy - type: Float - - The energy of the electron beam in keV - -beam_current - type: Float - - The beam current in nA. - -probe_area - type: Float - - The illumination area of the electron beam in nm\ :sup:`2`. - -magnification - type: Float - - The magnification. - -working_distance - type: Float - - The working distance in mm. - -Stage ------ -tilt_alpha - type: Float - - A tilt of the stage in degree. - -tilt_beta - type: Float - - Another tilt of the stage in degree. - -rotation - type: Float - - The rotation of the stage in degree. - -x - type: Float - - The position of the stage in mm along the x axis. - -y - type: Float - - The position of the stage in mm along the y axis. - -z - type: Float - - The position of the stage in mm along the z axis. - -Detector --------- - -All instruments can contain a "Detector" node with information about the -detector used to acquire the signal. EDX and EELS detectors should follow the -following structure: - -detector_type - type: Str - - The type of the detector, e.g. SE for SEM - -EELS -^^^^ - -This node stores parameters relevant to electron energy loss spectroscopy -signals. - -aperture_size - type: Float - - The entrance aperture size of the spectrometer in mm. - -collection_angle - type: Float - - The collection semi-angle in mrad. - -dwell_time - type: Float - - The dwell time in seconds. This is relevant for STEM acquisition - -exposure - type: Float - - The exposure time in seconds. This is relevant for TEM acquisition. - -frame_number - type: int - - The number of frames/spectra integrated during the acquisition. - -spectrometer - type: Str - - The spectrometer model, e.g. Gatan Enfinium ER (Model 977). - -EDS -^^^ - -This node stores parameters relevant to electron X-ray energy dispersive -spectroscopy data. - - -azimuth_angle - type: Float - - The azimuth angle of the detector in degree. If the azimuth is zero, - the detector is perpendicular to the tilt axis. - -elevation_angle - type: Float - - The elevation angle of the detector in degree. The detector is - perpendicular to the surface with an angle of 90. - -energy_resolution_MnKa - type: Float - - The full width at half maximum (FWHM) of the manganese K alpha - (Mn Ka) peak in eV. This value is used as a first approximation - of the energy resolution of the detector. - -real_time - type: Float - - The time spent to record the spectrum in second. - -live_time - type: Float - - The time spent to record the spectrum in second, compensated for the - dead time of the detector. - -Biprism -------- - -This node stores parameters of biprism used in off-axis electron holography - -azimuth_angle (º) - type: Float - - Rotation angle of the biprism in degree - -position - type: Str - - Position of the biprism in microscope column, e.g. Selected area aperture - plane - -voltage - type: Float - - Voltage of electrostatic biprism in volts - -Sample -====== - -credits - type: Str - - Acknowledgment of sample supplier, e.g. Prepared by Putin, Vladimir V. - -description - type: Str - - A brief description of the sample - -elements - type: list - - A list of the symbols of the elements composing the sample, e.g. ['B', 'N'] - for a sample composed of Boron and Nitrogen. - -xray_lines - type: list - - A list of the symbols of the X-ray lines to be used for processing, - e.g. ['Al_Ka', 'Ni_Lb'] for the K alpha line of Aluminum - and the L beta line of Nickel. - -thickness - type: Float - - The thickness of the sample in m. - - -Signal -====== - -signal_type - type: Str - - A term that describes the signal type, e.g. EDS, PES... This information - can be used by HyperSpy to load the file as a specific signal class and - therefore the naming should be standarised. Currently HyperSpy provides - special signal class for photoemission spectroscopy, electron energy - loss spectroscopy and energy dispersive spectroscopy. The signal_type in - these cases should be respectively PES, EELS and EDS_TEM (EDS_SEM). - -signal_origin - type: Str - - Describes the origin of the signal e.g. 'simulation' or 'experiment'. - - -record_by - .. deprecated:: 1.2 - - type: Str - - One of 'spectrum' or 'image'. It describes how the data is stored in memory. - If 'spectrum' the spectral data is stored in the faster index. - -quantity - type: Str - - The name of the quantity of the "intensity axis" with the units in round - brackets if required, for example Temperature (K). - - -FFT ---- - -shifted - type: bool. - - Specify if the FFT has the zero-frequency component shifted to the center of - the signal. - - -Noise_properties ----------------- - -variance - type: float or BaseSignal instance. - - The variance of the data. It can be a float when the noise is Gaussian or a - :class:`~.signal.BaseSignal` instance if the noise is heteroscedastic, - in which case it must have the same dimensions as - :attr:`~.signal.BaseSignal.data`. - -Variance_linear_model -^^^^^^^^^^^^^^^^^^^^^ - -In some cases the variance can be calculated from the data using a simple -linear model: ``variance = (gain_factor * data + gain_offset) * -correlation_factor``. - -gain_factor - type: Float - -gain_offset - type: Float - -correlation_factor - type: Float - -parameters_estimation_method - type: Str - -_Internal_parameters -==================== - -This node is "private" and therefore is not displayed when printing the -:attr:`~.signal.BaseSignal.metadata` attribute. For example, an "energy" leaf -should be accompanied by an "energy_units" leaf. - -Stacking_history ----------------- - -Generated when using :py:meth:`~.utils.stack`. Used by -:py:meth:`~.signal.BaseSignal.split`, to retrieve the former list of signal. - -step_sizes - type: list of int - - Step sizes used that can be used in split. - -axis - type: int - - The axis index in axes manager on which the dataset were stacked. - -Folding -------- - -Constains parameters that related to the folding/unfolding of signals. diff --git a/doc/user_guide/model.rst b/doc/user_guide/model.rst deleted file mode 100644 index 9e0728fe5b..0000000000 --- a/doc/user_guide/model.rst +++ /dev/null @@ -1,1578 +0,0 @@ -.. _model-label: - -Model fitting -************* - -HyperSpy can perform curve fitting of one-dimensional signals (spectra) and -two-dimensional signals (images) in `n`-dimensional data sets. -Models are defined by adding individual functions (components in HyperSpy's -terminology) to a :py:class:`~.model.BaseModel` instance. Those individual -components are then summed to create the final model function that can be -fitted to the data, by adjusting the free parameters of the individual -components. - - -Models can be created and fit to experimental data in both one and two -dimensions i.e. spectra and images respectively. Most of the syntax is -identical in either case. A one-dimensional model is created when a model -is created for a :py:class:`~._signals.signal1d.Signal1D` whereas a two- -dimensional model is created for a :py:class:`~._signals.signal2d.Signal2D`. - -.. note:: - - Plotting and analytical gradient-based fitting methods are not yet - implemented for the :py:class:`~.models.model2d.Model2D` class. - - -Caveats -------- - -* Before creating a model verify that the - :py:attr:`~.signal.BaseSignal.axes_manager.signal_axes[0].is_binned` attribute - of the signal axis is set to the correct value because the resulting - model depends on this parameter. See :ref:`signal.binned` for more details. -* When importing data that has been binned using other software, in - particular Gatan's DM, the stored values may be the averages of the - binned channels or pixels, instead of their sum, as would be required - for proper statistical analysis. We therefore cannot guarantee that - the statistics will be valid, and so strongly recommend that all - pre-fitting binning is performed using Hyperspy. - -Creating a model ----------------- - -A :py:class:`~.models.model1d.Model1D` can be created for data in the -:py:class:`~._signals.signal1d.Signal1D` class using the -:py:meth:`~._signals.signal1d.Signal1D.create_model` method: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(300).reshape(30, 10)) - >>> m = s.create_model() # Creates the 1D-Model and assign it to m - -Similarly, a :py:class:`~.models.model2d.Model2D` can be created for data -in the :py:class:`~._signals.signal2d.Signal2D` class using the -:py:meth:`~._signals.signal2d.Signal2D.create_model` method: - -.. code-block:: python - - >>> im = hs.signals.Signal2D(np.arange(300).reshape(3, 10, 10)) - >>> mod = im.create_model() # Create the 2D-Model and assign it to mod - -The syntax for creating both one-dimensional and two-dimensional models is thus -identical for the user in practice. When a model is created you may be -prompted to provide important information not already included in the -datafile, `e.g.` if ``s`` is EELS data, you may be asked for the accelerating -voltage, convergence and collection semi-angles etc. - - -Model components ----------------- - -In HyperSpy a model consists of a sum of individual components. For convenience, -HyperSpy provides a number of pre-defined model components as well as mechanisms -to create your own components. - -.. _model_components-label: - -Pre-defined model components -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Various components are available in one (:py:mod:`~.components1d`) and -two-dimensions (:py:mod:`~.components2d`) to construct a -model. - -The following general components are currently available for one-dimensional models: - -* :py:class:`~._components.arctan.Arctan` -* :py:class:`~._components.bleasdale.Bleasdale` -* :py:class:`~._components.doniach.Doniach` -* :py:class:`~._components.error_function.Erf` -* :py:class:`~._components.exponential.Exponential` -* :py:class:`~._components.expression.Expression` -* :py:class:`~._components.gaussian.Gaussian` -* :py:class:`~._components.gaussianhf.GaussianHF` -* :py:class:`~._components.heaviside.HeavisideStep` -* :py:class:`~._components.logistic.Logistic` -* :py:class:`~._components.lorentzian.Lorentzian` -* :py:class:`~._components.offset.Offset` -* :py:class:`~._components.polynomial.Polynomial` -* :py:class:`~._components.power_law.PowerLaw` -* :py:class:`~._components.pes_see.SEE` -* :py:class:`~._components.scalable_fixed_pattern.ScalableFixedPattern` -* :py:class:`~._components.skew_normal.SkewNormal` -* :py:class:`~._components.voigt.Voigt` -* :py:class:`~._components.split_voigt.SplitVoigt` -* :py:class:`~._components.volume_plasmon_drude.VolumePlasmonDrude` - -The following components developed with specific signal types in mind are -currently available for one-dimensional models: - -* :py:class:`~._components.eels_arctan.EELSArctan` -* :py:class:`~._components.eels_double_power_law.DoublePowerLaw` -* :py:class:`~._components.eels_cl_edge.EELSCLEdge` -* :py:class:`~._components.pes_core_line_shape.PESCoreLineShape` -* :py:class:`~._components.pes_voigt.PESVoigt` -* :py:class:`~._components.pes_see.SEE` -* :py:class:`~._components.eels_vignetting.Vignetting` - -The following components are currently available for two-dimensional models: - -* :py:class:`~._components.expression.Expression` -* :py:class:`~._components.gaussian2d.Gaussian2D` - -However, this doesn't mean that you have to limit yourself to this meagre -list of functions. As discussed below, it is very easy to turn a -mathematical, fixed-pattern or Python function into a component. - -.. _expression_component-label: - -Define components from a mathematical expression -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - -The easiest way to turn a mathematical expression into a component is using the -:py:class:`~._components.expression.Expression` component. For example, the -following is all you need to create a -:py:class:`~._components.gaussian.Gaussian` component with more sensible -parameters for spectroscopy than the one that ships with HyperSpy: - -.. code-block:: python - - >>> g = hs.model.components1D.Expression( - ... expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2)", - ... name="Gaussian", - ... position="x0", - ... height=1, - ... fwhm=1, - ... x0=0, - ... module="numpy") - -If the expression is inconvenient to write out in full (e.g. it's long and/or -complicated), multiple substitutions can be given, separated by semicolons. -Both symbolic and numerical substitutions are allowed: - -.. code-block:: python - - >>> expression = "h / sqrt(p2) ; p2 = 2 * m0 * e1 * x * brackets;" - >>> expression += "brackets = 1 + (e1 * x) / (2 * m0 * c * c) ;" - >>> expression += "m0 = 9.1e-31 ; c = 3e8; e1 = 1.6e-19 ; h = 6.6e-34" - >>> wavelength = hs.model.components1D.Expression( - ... expression=expression, - ... name="Electron wavelength with voltage") - -:py:class:`~._components.expression.Expression` uses `Sympy -`_ internally to turn the string into -a function. By default it "translates" the expression using -numpy, but often it is possible to boost performance by using -`numexpr `_ instead. - -It can also create 2D components with optional rotation. In the following -example we create a 2D Gaussian that rotates around its center: - -.. code-block:: python - - >>> g = hs.model.components2D.Expression( - ... "k * exp(-((x-x0)**2 / (2 * sx ** 2) + (y-y0)**2 / (2 * sy ** 2)))", - ... "Gaussian2d", add_rotation=True, position=("x0", "y0"), - ... module="numpy", ) - -Define new components from a Python function -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Of course :py:class:`~._components.expression.Expression` is only useful for -analytical functions. You can define more general components modifying the -following template to suit your needs: - - -.. code-block:: python - - from hyperspy.component import Component - - class MyComponent(Component): - - """ - """ - - def __init__(self, parameter_1=1, parameter_2=2): - # Define the parameters - Component.__init__(self, ('parameter_1', 'parameter_2')) - - # Optionally we can set the initial values - self.parameter_1.value = parameter_1 - self.parameter_1.value = parameter_1 - - # The units (optional) - self.parameter_1.units = 'Tesla' - self.parameter_2.units = 'Kociak' - - # Once defined we can give default values to the attribute - # For example we fix the attribure_1 (optional) - self.parameter_1.attribute_1.free = False - - # And we set the boundaries (optional) - self.parameter_1.bmin = 0. - self.parameter_1.bmax = None - - # Optionally, to boost the optimization speed we can also define - # the gradients of the function we the syntax: - # self.parameter.grad = function - self.parameter_1.grad = self.grad_parameter_1 - self.parameter_2.grad = self.grad_parameter_2 - - # Define the function as a function of the already defined parameters, - # x being the independent variable value - def function(self, x): - p1 = self.parameter_1.value - p2 = self.parameter_2.value - return p1 + x * p2 - - # Optionally define the gradients of each parameter - def grad_parameter_1(self, x): - """ - Returns d(function)/d(parameter_1) - """ - return 0 - - def grad_parameter_2(self, x): - """ - Returns d(function)/d(parameter_2) - """ - return x - -Define components from a fixed-pattern -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:class:`~._components.scalable_fixed_pattern.ScalableFixedPattern` -component enables fitting a pattern (in the form of a -:py:class:`~._signals.signal1d.Signal1D` instance) to data by shifting -(:py:attr:`~._components.scalable_fixed_pattern.ScalableFixedPattern.shift`) -and -scaling it in the x and y directions using the -:py:attr:`~._components.scalable_fixed_pattern.ScalableFixedPattern.xscale` -and -:py:attr:`~._components.scalable_fixed_pattern.ScalableFixedPattern.yscale` -parameters respectively. - -Adding components to the model ------------------------------- - -To print the current components in a model use -:py:attr:`~.model.BaseModel.components`. A table with component number, -attribute name, component name and component type will be printed: - -.. code-block:: python - - >>> m - - >>> m.components # an empty model - # | Attribute Name | Component Name | Component Type - ---- | -------------------- | -------------------- | --------------------- - - -.. note:: Sometimes components may be created automatically. For example, if - the :py:class:`~._signals.signal1d.Signal1D` is recognised as EELS data, a - power-law background component may automatically be added to the model. - Therefore, the table above may not all may empty on model creation. - -To add a component to the model, first we have to create an instance of the -component. -Once the instance has been created we can add the component to the model -using the :py:meth:`~.model.BaseModel.append` and -:py:meth:`~.model.BaseModel.extend` methods for one or more components -respectively. - -As an example, let's add several :py:class:`~._components.gaussian.Gaussian` -components to the model: - -.. code-block:: python - - >>> gaussian = hs.model.components1D.Gaussian() # Create a Gaussian comp. - >>> m.append(gaussian) # Add it to the model - >>> m.components # Print the model components - # | Attribute Name | Component Name | Component Type - ---- | -------------------- | --------------------- | --------------------- - 0 | Gaussian | Gaussian | Gaussian - >>> gaussian2 = hs.model.components1D.Gaussian() # Create another gaussian - >>> gaussian3 = hs.model.components1D.Gaussian() # Create a third gaussian - - -We could use the :py:meth:`~.model.BaseModel.append` method twice to add the -two Gaussians, but when adding multiple components it is handier to use the -extend method that enables adding a list of components at once. - - -.. code-block:: python - - >>> m.extend((gaussian2, gaussian3)) # note the double parentheses! - >>> m.components - # | Attribute Name | Component Name | Component Type - ---- | -------------------- | ------------------- | --------------------- - 0 | Gaussian | Gaussian | Gaussian - 1 | Gaussian_0 | Gaussian_0 | Gaussian - 2 | Gaussian_1 | Gaussian_1 | Gaussian - - -We can customise the name of the components. - -.. code-block:: python - - >>> gaussian.name = 'Carbon' - >>> gaussian2.name = 'Long Hydrogen name' - >>> gaussian3.name = 'Nitrogen' - >>> m.components - # | Attribute Name | Component Name | Component Type - ---- | --------------------- | --------------------- | ------------------- - 0 | Carbon | Carbon | Gaussian - 1 | Long_Hydrogen_name | Long Hydrogen name | Gaussian - 2 | Nitrogen | Nitrogen | Gaussian - - -Notice that two components cannot have the same name: - -.. code-block:: python - - >>> gaussian2.name = 'Carbon' - Traceback (most recent call last): - File "", line 1, in - g2.name = "Carbon" - File "/home/fjd29/Python/hyperspy/hyperspy/component.py", line 466, in - name "the name " + str(value)) - ValueError: Another component already has the name Carbon - - -It is possible to access the components in the model by their name or by the -index in the model. - -.. code-block:: python - - >>> m - # | Attribute Name | Component Name | Component Type - ---- | --------------------- | -------------------- | ------------------- - 0 | Carbon | Carbon | Gaussian - 1 | Long_Hydrogen_name | Long Hydrogen name | Gaussian - 2 | Nitrogen | Nitrogen | Gaussian - >>> m[0] - - >>> m["Long Hydrogen name"] - - - -In addition, the components can be accessed in the -:py:attr:`~.model.BaseModel.components` `Model` attribute. This is specially -useful when working in interactive data analysis with IPython because it -enables tab completion. - -.. code-block:: python - - >>> m - # | Attribute Name | Component Name | Component Type - ---- | --------------------- | --------------------- | ------------------- - 0 | Carbon | Carbon | Gaussian - 1 | Long_Hydrogen_name | Long Hydrogen name | Gaussian - 2 | Nitrogen | Nitrogen | Gaussian - >>> m.components.Long_Hydrogen_name - - - -It is possible to "switch off" a component by setting its -``active`` attribute to ``False``. When a component is -switched off, to all effects it is as if it was not part of the model. To -switch it back on simply set the ``active`` attribute back to ``True``. - -In multi-dimensional signals it is possible to store the value of the -``active`` attribute at each navigation index. -To enable this feature for a given component set the -:py:attr:`~.component.Component.active_is_multidimensional` attribute to -`True`. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(100).reshape(10,10)) - >>> m = s.create_model() - >>> g1 = hs.model.components1D.Gaussian() - >>> g2 = hs.model.components1D.Gaussian() - >>> m.extend([g1,g2]) - >>> g1.active_is_multidimensional = True - >>> g1._active_array - array([ True, True, True, True, True, True, True, True, True, True], dtype=bool) - >>> g2._active_array is None - True - >>> m.set_component_active_value(False) - >>> g1._active_array - array([False, False, False, False, False, False, False, False, False, False], dtype=bool) - >>> m.set_component_active_value(True, only_current=True) - >>> g1._active_array - array([ True, False, False, False, False, False, False, False, False, False], dtype=bool) - >>> g1.active_is_multidimensional = False - >>> g1._active_array is None - True - - -.. _model_indexing-label: - -Indexing the model ------------------- - -Often it is useful to consider only part of the model - for example at -a particular location (i.e. a slice in the navigation space) or energy range -(i.e. a slice in the signal space). This can be done using exactly the same -syntax that we use for signal indexing (:ref:`signal.indexing`). -:py:attr:`~.model.BaseModel.red_chisq` and :py:attr:`~.model.BaseModel.dof` -are automatically recomputed for the resulting slices. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(100).reshape(10,10)) - >>> m = s.create_model() - >>> m.append(hs.model.components1D.Gaussian()) - >>> # select first three navigation pixels and last five signal channels - >>> m1 = m.inav[:3].isig[-5:] - >>> m1.signal - - - -Getting and setting parameter values and attributes ---------------------------------------------------- - -:py:meth:`~.model.BaseModel.print_current_values()` prints the properties of the -parameters of the components in the current coordinates. In the Jupyter Notebook, -the default view is HTML-formatted, which allows for formatted copying -into other software, such as Excel. This can be changed to a standard -terminal view with the argument ``fancy=False``. One can also filter for only active -components and only showing component with free parameters with the arguments -``only_active`` and ``only_free``, respectively. - -.. _Component.print_current_values: - -The current values of a particular component can be printed using the -:py:attr:`~.component.Component.print_current_values()` method. - -.. code-block:: python - - >>> m = s.create_model() - >>> m.fit() - >>> G = m[1] - >>> G.print_current_values(fancy=False) - Gaussian: Al_Ka - Active: True - Parameter Name | Free | Value | Std | Min - ============== | ===== | ========== | ========== | ========== - A | True | 62894.6824 | 1039.40944 | 0.0 - sigma | False | 0.03253440 | None | None - centre | False | 1.4865 | None | None - -The current coordinates can be either set by navigating the -:py:meth:`~.model.BaseModel.plot`, or specified by pixel indices in -``m.axes_manager.indices`` or as calibrated coordinates in -``m.axes_manager.coordinates``. - -:py:attr:`~.component.Component.parameters` contains a list of the parameters -of a component and :py:attr:`~.component.Component.free_parameters` lists only -the free parameters. - -The value of a particular parameter in the current coordinates can be -accessed by :py:attr:`component.Parameter.value` (e.g. ``Gaussian.A.value``). -To access an array of the value of the parameter across all navigation -pixels, :py:attr:`component.Parameter.map['values']` (e.g. -``Gaussian.A.map["values"]``) can be used. On its own, -:py:attr:`component.Parameter.map` returns a NumPy array with three elements: -``'values'``, ``'std'`` and ``'is_set'``. The first two give the value and -standard error for each index. The last element shows whether the value has -been set in a given index, either by a fitting procedure or manually. - -If a model contains several components with the same parameters, it is possible -to change them all by using :py:meth:`~.model.BaseModel.set_parameters_value`. -Example: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(100).reshape(10,10)) - >>> m = s.create_model() - >>> g1 = hs.model.components1D.Gaussian() - >>> g2 = hs.model.components1D.Gaussian() - >>> m.extend([g1,g2]) - >>> m.set_parameters_value('A', 20) - >>> g1.A.map['values'] - array([ 20., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) - >>> g2.A.map['values'] - array([ 20., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) - >>> m.set_parameters_value('A', 40, only_current=True) - >>> g1.A.map['values'] - array([ 40., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) - >>> m.set_parameters_value('A',30, component_list=[g2]) - >>> g2.A.map['values'] - array([ 30., 30., 30., 30., 30., 30., 30., 30., 30., 30.]) - >>> g1.A.map['values'] - array([ 40., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) - - -To set the ``free`` state of a parameter change the -:py:attr:`~.component.Parameter.free` attribute. To change the ``free`` state -of all parameters in a component to `True` use -:py:meth:`~.component.Component.set_parameters_free`, and -:py:meth:`~.component.Component.set_parameters_not_free` for setting them to -``False``. Specific parameter-names can also be specified by using -``parameter_name_list``, shown in the example: - -.. code-block:: python - - >>> g = hs.model.components1D.Gaussian() - >>> g.free_parameters - [, - , - ] - >>> g.set_parameters_not_free() - >>> g.set_parameters_free(parameter_name_list=['A','centre']) - >>> g.free_parameters - [, - ] - -Similar functions exist for :py:class:`~.model.BaseModel`: -:py:meth:`~.model.BaseModel.set_parameters_free` and -:py:meth:`~.model.BaseModel.set_parameters_not_free`. Which sets the -``free`` states for the parameters in components in a model. Specific -components and parameter-names can also be specified. For example: - -.. code-block:: python - - >>> g1 = hs.model.components1D.Gaussian() - >>> g2 = hs.model.components1D.Gaussian() - >>> m.extend([g1,g2]) - >>> m.set_parameters_not_free() - >>> g1.free_parameters - [] - >>> g2.free_parameters - [] - >>> m.set_parameters_free(parameter_name_list=['A']) - >>> g1.free_parameters - [] - >>> g2.free_parameters - [] - >>> m.set_parameters_free([g1], parameter_name_list=['sigma']) - >>> g1.free_parameters - [, - ] - >>> g2.free_parameters - [] - - -The value of a parameter can be coupled to the value of another by setting the -:py:attr:`~.component.Parameter.twin` attribute: - -.. code-block:: python - - >>> gaussian.parameters # Print the parameters of the Gaussian components - (, - , - ) - >>> gaussian.centre.free = False # Fix the centre - >>> gaussian.free_parameters # Print the free parameters - [, ] - >>> m.print_current_values(only_free=True, fancy=False) # Print the values of all free parameters. - Model1D: - Gaussian: Carbon - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | True | 1.0 | None | 0.0 | None - sigma | True | 1.0 | None | None | None - - Gaussian: Long Hydrogen name - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | True | 1.0 | None | 0.0 | None - sigma | True | 1.0 | None | None | None - centre | True | 0.0 | None | None | None - - Gaussian: Nitrogen - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | True | 1.0 | None | 0.0 | None - sigma | True | 1.0 | None | None | None - centre | True | 0.0 | None | None | None - - >>> # Couple the A parameter of gaussian2 to the A parameter of gaussian 3: - >>> gaussian2.A.twin = gaussian3.A - >>> gaussian2.A.value = 10 # Set the gaussian2 A value to 10 - >>> gaussian3.print_current_values(fancy=False) - Gaussian: Nitrogen - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | True | 10.0 | None | 0.0 | None - sigma | True | 1.0 | None | None | None - centre | True | 0.0 | None | None | None - - >>> gaussian3.A.value = 5 # Set the gaussian1 centre value to 5 - >>> gaussian2.print_current_values(fancy=False) - Gaussian: Long Hydrogen name - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | False | 5.0 | None | 0.0 | None - sigma | True | 1.0 | None | None | None - centre | True | 0.0 | None | None | None - -.. deprecated:: 1.2.0 - Setting the :py:attr:`~.component.Parameter.twin_function` and - :py:attr:`~.component.Parameter.twin_inverse_function` attributes. Set the - :py:attr:`~.component.Parameter.twin_function_expr` and - :py:attr:`~.component.Parameter.twin_inverse_function_expr` attributes - instead. - -.. versionadded:: 1.2.0 - :py:attr:`~.component.Parameter.twin_function_expr` and - :py:attr:`~.component.Parameter.twin_inverse_function_expr`. - -By default the coupling function is the identity function. However it is -possible to set a different coupling function by setting the -:py:attr:`~.component.Parameter.twin_function_expr` and -:py:attr:`~.component.Parameter.twin_inverse_function_expr` attributes. For -example: - -.. code-block:: python - - >>> gaussian2.A.twin_function_expr = "x**2" - >>> gaussian2.A.twin_inverse_function_expr = "sqrt(abs(x))" - >>> gaussian2.A.value = 4 - >>> gaussian3.print_current_values(fancy=False) - Gaussian: Nitrogen - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | True | 2.0 | None | 0.0 | None - sigma | True | 1.0 | None | None | None - centre | True | 0.0 | None | None | None - -.. code-block:: python - - >>> gaussian3.A.value = 4 - >>> gaussian2.print_current_values(fancy=False) - Gaussian: Long Hydrogen name - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | False | 16.0 | None | 0.0 | None - sigma | True | 1.0 | None | None | None - centre | True | 0.0 | None | None | None - -.. _model.fitting: - -Fitting the model to the data ------------------------------ - -To fit the model to the data at the current coordinates (e.g. to fit one -spectrum at a particular point in a spectrum-image), use -:py:meth:`~.model.BaseModel.fit`. HyperSpy implements a number of -different optimization approaches, each of which can have particular -benefits and/or drawbacks depending on your specific application. -A good approach to choosing an optimization approach is to ask yourself -the question "Do you want to...": - -* Apply bounds to your model parameter values? -* Use gradient-based fitting algorithms to accelerate your fit? -* Estimate the standard deviations of the parameter values found by the fit? -* Fit your data in the least-squares sense, or use another loss function? -* Find the global optima for your parameters, or is a local optima acceptable? - -Optimization algorithms -^^^^^^^^^^^^^^^^^^^^^^^ - -The following table summarizes the features of some of the optimizers -currently available in HyperSpy, including whether they support parameter -bounds, gradients and parameter error estimation. The "Type" column indicates -whether the optimizers find a local or global optima. - -.. _optimizers-table: - -.. table:: Features of supported curve-fitting optimizers. - - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | Optimizer | Bounds | Gradients | Errors | Loss function | Type | - +======================================+========+===========+========+================+========+ - | ``"lm"`` (default) | Yes | Yes | Yes | Only ``"ls"`` | local | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | ``"trf"`` | Yes | Yes | Yes | Only ``"ls"`` | local | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | ``"dogbox"`` | Yes | Yes | Yes | Only ``"ls"`` | local | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | ``"odr"`` | No | Yes | Yes | Only ``"ls"`` | local | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | :py:func:`scipy.optimize.minimize` | Yes * | Yes * | No | All | local | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | ``"Differential Evolution"`` | Yes | No | No | All | global | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | ``"Dual Annealing"`` ** | Yes | No | No | All | global | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - | ``"SHGO"`` ** | Yes | No | No | All | global | - +--------------------------------------+--------+-----------+--------+----------------+--------+ - -.. note:: - - \* **All** of the fitting algorithms available in :py:func:`scipy.optimize.minimize` are currently - supported by HyperSpy; however, only some of them support bounds and/or gradients. For more information, - please see the `SciPy documentation `_. - - \*\* Requires ``scipy >= 1.2.0``. - -The default optimizer in HyperSpy is ``"lm"``, which stands for the `Levenberg-Marquardt -algorithm `_. In -earlier versions of HyperSpy (< 1.6) this was known as ``"leastsq"``. - -Loss functions -^^^^^^^^^^^^^^ - -HyperSpy supports a number of loss functions. The default is ``"ls"``, -i.e. the least-squares loss. For the vast majority of cases, this loss -function is appropriate, and has the additional benefit of supporting -parameter error estimation and :ref:`goodness-of-fit ` -testing. However, if your data contains very low counts per pixel, or -is corrupted by outliers, the ``"ML-poisson"`` and ``"huber"`` loss -functions may be worth investigating. - -Least squares with error estimation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following example shows how to perfom least squares optimization with -error estimation. First we create data consisting of a line -``y = a*x + b`` with ``a = 1`` and ``b = 100``, and we then add Gaussian -noise to it: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(100, 300)) - >>> s.add_gaussian_noise(std=100) - -To fit it, we create a model consisting of a -:class:`~._components.polynomial.Polynomial` component of order 1 and fit it -to the data. - -.. code-block:: python - - >>> m = s.create_model() - >>> line = hs.model.components1D.Polynomial(order=1) - >>> m.append(line) - >>> m.fit() - -Once the fit is complete, the optimized value of the parameters and their -estimated standard deviation are stored in the following line attributes: - -.. code-block:: python - - >>> line.a.value - 0.9924615648843765 - >>> line.b.value - 103.67507406125888 - >>> line.a.std - 0.11771053738516088 - >>> line.b.std - 13.541061301257537 - -.. warning:: - - When the noise is heteroscedastic, only if the - ``metadata.Signal.Noise_properties.variance`` attribute of the - :class:`~._signals.signal1d.Signal1D` instance is defined can - the parameter standard deviations be estimated accurately. - - If the variance is not defined, the standard deviations are still - computed, by setting variance equal to 1. However, this calculation - will not be correct unless an accurate value of the variance is - provided. See :ref:`signal.noise_properties` for more information. - -Weighted least squares with error estimation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the following example, we add Poisson noise to the data instead of -Gaussian noise, and proceed to fit as in the previous example. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(300)) - >>> s.add_poissonian_noise() - >>> m = s.create_model() - >>> line = hs.model.components1D.Polynomial(order=1) - >>> m.append(line) - >>> m.fit() - >>> line.coefficients.value - (1.0052331707848698, -1.0723588390873573) - >>> line.coefficients.std - (0.0081710549764721901, 1.4117294994070277) - -Because the noise is heteroscedastic, the least squares optimizer estimation is -biased. A more accurate result can be obtained with weighted least squares, -where the weights are proportional to the inverse of the noise variance. -Although this is still biased for Poisson noise, it is a good approximation -in most cases where there are a sufficient number of counts per pixel. - -.. code-block:: python - - >>> exp_val = hs.signals.Signal1D(np.arange(300)) - >>> s.estimate_poissonian_noise_variance(expected_value=exp_val) - >>> m.fit() - >>> line.coefficients.value - (1.0004224896604759, -0.46982916592391377) - >>> line.coefficients.std - (0.0055752036447948173, 0.46950832982673557) - -.. warning:: - - When the attribute ``metadata.Signal.Noise_properties.variance`` - is defined, the behaviour is to perform a weighted least-squares - fit using the inverse of the noise variance as the weights. - In this scenario, to then disable weighting, you will need to **unset** - the attribute. You can achieve this with - :meth:`~.signal.BaseSignal.set_noise_variance`: - - .. code-block:: python - - >>> m.signal.set_noise_variance(None) - >>> m.fit() # This will now be an unweighted fit - >>> line.coefficients.value - (1.0052331707848698, -1.0723588390873573) - -Poisson maximum likelihood estimation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To avoid biased estimation in the case of data corrupted by Poisson noise -with very few counts, we can use Poisson maximum likelihood estimation (MLE) instead. -This is an unbiased estimator for Poisson noise. To perform MLE, we must -use a general, non-linear optimizer from the :ref:`table above `, -such as Nelder-Mead or L-BFGS-B: - -.. code-block:: python - - >>> m.fit(optimizer="Nelder-Mead", loss_function="ML-poisson") - >>> line.coefficients.value - (1.0030718094185611, -0.63590210946134107) - -Estimation of the parameter errors is not currently supported for Poisson -maximum likelihood estimation. - -Huber loss function -~~~~~~~~~~~~~~~~~~~ - -HyperSpy also implements the -`Huber loss `_ function, -which is typically less sensitive to outliers in the data compared -to the least-squares loss. Again, we need to use one of the general -non-linear optimization algorithms: - -.. code-block:: python - - >>> m.fit(optimizer="Nelder-Mead", loss_function="huber") - -Estimation of the parameter errors is not currently supported -for the Huber loss function. - -Custom loss functions -~~~~~~~~~~~~~~~~~~~~~ - -As well as the built-in loss functions described above, -a custom loss function can be passed to the model: - -.. code-block:: python - - >>> def my_custom_function(model, values, data, weights=None): - ... """ - ... Parameters - ... ---------- - ... model : Model instance - ... the model that is fitted. - ... values : np.ndarray - ... A one-dimensional array with free parameter values suggested by the - ... optimizer (that are not yet stored in the model). - ... data : np.ndarray - ... A one-dimensional array with current data that is being fitted. - ... weights : {np.ndarray, None} - ... An optional one-dimensional array with parameter weights. - ... - ... Returns - ... ------- - ... score : float - ... A signle float value, representing a score of the fit, with - ... lower values corresponding to better fits. - ... """ - ... # Almost any operation can be performed, for example: - ... # First we store the suggested values in the model - ... model.fetch_values_from_array(values) - ... - ... # Evaluate the current model - ... cur_value = model(onlyactive=True) - ... - ... # Calculate the weighted difference with data - ... if weights is None: - ... weights = 1 - ... difference = (data - cur_value) * weights - ... - ... # Return squared and summed weighted difference - ... return (difference**2).sum() - - >>> # We must use a general non-linear optimizer - >>> m.fit(optimizer='Nelder-Mead', loss_function=my_custom_function) - -If the optimizer requires an analytical gradient function, it can be similarly -passed, using the following signature: - -.. code-block:: python - - >>> def my_custom_gradient_function(model, values, data, weights=None): - ... """ - ... Parameters - ... ---------- - ... model : Model instance - ... the model that is fitted. - ... values : np.ndarray - ... A one-dimensional array with free parameter values suggested by the - ... optimizer (that are not yet stored in the model). - ... data : np.ndarray - ... A one-dimensional array with current data that is being fitted. - ... weights : {np.ndarray, None} - ... An optional one-dimensional array with parameter weights. - ... - ... Returns - ... ------- - ... gradients : np.ndarray - ... a one-dimensional array of gradients, the size of `values`, - ... containing each parameter gradient with the given values - ... """ - ... # As an example, estimate maximum likelihood gradient: - ... model.fetch_values_from_array(values) - ... cur_value = model(onlyactive=True) - ... - ... # We use in-built jacobian estimation - ... jac = model._jacobian(values, data) - ... - ... return -(jac * (data / cur_value - 1)).sum(1) - - >>> # We must use a general non-linear optimizer again - >>> m.fit(optimizer='L-BFGS-B', - ... loss_function=my_custom_function, - ... grad=my_custom_gradient_function) - -Using gradient information -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. versionadded:: 1.6 ``grad="analytical"`` and ``grad="fd"`` keyword arguments - -Optimization algorithms that take into account the gradient of -the loss function will often out-perform so-called "derivative-free" -optimization algorithms in terms of how rapidly they converge to a -solution. HyperSpy can use analytical gradients for model-fitting, -as well as numerical estimates of the gradient based on finite differences. - -If all the components in the model support analytical gradients, -you can pass ``grad="analytical"`` in order to use this information -when fitting. The results are typically more accurate than an -estimated gradient, and the optimization often runs faster since -fewer function evaluations are required to calculate the gradient. - -Following the above examples: - -.. code-block:: python - - >>> m = s.create_model() - >>> line = hs.model.components1D.Polynomial(order=1) - >>> m.append(line) - - >>> # Use a 2-point finite-difference scheme to estimate the gradient - >>> m.fit(grad="fd", fd_scheme="2-point") - - >>> # Use the analytical gradient - >>> m.fit(grad="analytical") - - >>> # Huber loss and Poisson MLE functions - >>> # also support analytical gradients - >>> m.fit(grad="analytical", loss_function="huber") - >>> m.fit(grad="analytical", loss_function="ML-poisson") - -.. note:: - - Analytical gradients are not yet implemented for the - :py:class:`~.models.model2d.Model2D` class. - -Bounded optimization -^^^^^^^^^^^^^^^^^^^^ - -Non-linear optimization can sometimes fail to converge to a good optimum, -especially if poor starting values are provided. Problems of ill-conditioning -and non-convergence can be improved by using bounded optimization. - -All components' parameters have the attributes ``parameter.bmin`` and -``parameter.bmax`` ("bounded min" and "bounded max"). When fitting using the -``bounded=True`` argument by ``m.fit(bounded=True)`` or ``m.multifit(bounded=True)``, -these attributes set the minimum and maximum values allowed for ``parameter.value``. - -Currently, not all optimizers support bounds - see the -:ref:`table above `. In the following example, a Gaussian -histogram is fitted using a :class:`~._components.gaussian.Gaussian` -component using the Levenberg-Marquardt ("lm") optimizer and bounds -on the ``centre`` parameter. - -.. code-block:: python - - >>> s = hs.signals.BaseSignal(np.random.normal(loc=10, scale=0.01, - ... size=100000)).get_histogram() - >>> s.axes_manager[-1].is_binned = True - >>> m = s.create_model() - >>> g1 = hs.model.components1D.Gaussian() - >>> m.append(g1) - >>> g1.centre.value = 7 - >>> g1.centre.bmin = 7 - >>> g1.centre.bmax = 14 - >>> m.fit(optimizer="lm", bounded=True) - >>> m.print_current_values(fancy=False) - Model1D: histogram - Gaussian: Gaussian - Active: True - Parameter Name | Free | Value | Std | Min | Max - ============== | ===== | ========== | ========== | ========== | ========== - A | True | 99997.3481 | 232.333693 | 0.0 | None - sigma | True | 0.00999184 | 2.68064163 | None | None - centre | True | 9.99980788 | 2.68064070 | 7.0 | 14.0 - - -Optimization results -^^^^^^^^^^^^^^^^^^^^ - -After fitting the model, details about the optimization -procedure, including whether it finished successfully, -are returned as :py:class:`scipy.optimize.OptimizeResult` object, -according to the keyword argument ``return_info=True``. -These details are often useful for diagnosing problems such -as a poorly-fitted model or a convergence failure. -You can also access the object as the ``fit_output`` attribute: - -.. code-block:: python - - >>> m.fit() - - - >>> type(m.fit_output) - - -You can also print this information using the -``print_info`` keyword argument: - -.. code-block:: python - - # Print the info to stdout - >>> m.fit(optimizer="L-BFGS-B", print_info=True) - Fit info: - optimizer=L-BFGS-B - loss_function=ls - bounded=False - grad="fd" - Fit result: - hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64> - message: b'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH' - nfev: 168 - nit: 32 - njev: 42 - status: 0 - success: True - x: array([ 9.97614503e+03, -1.10610734e-01, 1.98380701e+00]) - - -.. _model.goodness_of_fit: - -Goodness of fit -^^^^^^^^^^^^^^^ - -The chi-squared, reduced chi-squared and the degrees of freedom are -computed automatically when fitting a (weighted) least-squares model -(i.e. only when ``loss_function="ls"``). They are stored as signals, in the -:attr:`~.model.BaseModel.chisq`, :attr:`~.model.BaseModel.red_chisq` and -:attr:`~.model.BaseModel.dof` attributes of the model respectively. - -.. warning:: - - Unless ``metadata.Signal.Noise_properties.variance`` contains - an accurate estimation of the variance of the data, the chi-squared and - reduced chi-squared will not be computed correctly. This is true for both - homocedastic and heteroscedastic noise. - -.. _model.visualization: - -Visualizing the model -^^^^^^^^^^^^^^^^^^^^^ - -To visualise the result use the :py:meth:`~.model.BaseModel.plot` method: - -.. code-block:: python - - >>> m.plot() # Visualise the results - -By default only the full model line is displayed in the plot. In addition, it -is possible to display the individual components by calling -:py:meth:`~.model.BaseModel.enable_plot_components` or directly using -:py:meth:`~.model.BaseModel.plot`: - -.. code-block:: python - - >>> m.plot(plot_components=True) # Visualise the results - -To disable this feature call -:py:meth:`~.model.BaseModel.disable_plot_components`. - -.. versionadded:: 1.4 ``Signal1D.plot`` keyword arguments - -All extra keyword argments are passes to the :meth:`plot` method of the -corresponing signal object. For example, the following plots the model signal -figure but not its navigator: - -.. code-block:: python - - >>> m.plot(navigator=False) - -By default the model plot is automatically updated when any parameter value -changes. It is possible to suspend this feature with -:py:meth:`~.model.BaseModel.suspend_update`. - -.. To resume it use :py:meth:`~.model.BaseModel.resume_update`. - -.. _model.starting: - -Setting the initial parameters -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Non-linear optimization often requires setting sensible starting parameters. -This can be done by plotting the model and adjusting the parameters by hand. - -.. versionchanged:: 1.3 - All :meth:`notebook_interaction` methods renamed to :meth:`gui`. The - :meth:`notebook_interaction` methods will be removed in 2.0 - -.. _notebook_interaction-label: - -If running in a Jupyter Notebook, interactive widgets can be used to -conveniently adjust the parameter values by running -:py:meth:`~.model.BaseModel.gui` for :py:class:`~.model.BaseModel`, -:py:class:`~.component.Component` and -:py:class:`~.component.Parameter`. - -.. figure:: images/notebook_widgets.png - :align: center - :width: 985 - - Interactive widgets for the full model in a Jupyter notebook. Drag the - sliders to adjust current parameter values. Typing different minimum and - maximum values changes the boundaries of the slider. - -Also, :py:meth:`~.models.model1d.Model1D.enable_adjust_position` provides an -interactive way of setting the position of the components with a -well-defined position. -:py:meth:`~.models.model1d.Model1D.disable_adjust_position` disables the tool. - -.. figure:: images/model_adjust_position.png - :align: center - :width: 500 - - Interactive component position adjustment tool. Drag the vertical lines - to set the initial value of the position parameter. - -Exclude data from the fitting process -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The following :py:class:`~.model.BaseModel` methods can be used to exclude -undesired spectral channels from the fitting process: - -* :py:meth:`~.models.model1d.Model1D.set_signal_range` -* :py:meth:`~.models.model1d.Model1D.remove_signal_range` -* :py:meth:`~.models.model1d.Model1D.reset_signal_range` - -In 2D models, those methods are not implemented and the -``m.channel_switches`` attribute of a model can be set using boolean arrays of the -same shape as the data's signal, where ``True`` means that the datapoint -will be used in the fitting routine. - -The example below shows how a boolean array can be easily created from the -signal and how the ``isig`` syntax can be used to define the signal range. - -.. code-block:: python - - >>> # Create a sample 2D gaussian dataset - >>> g = hs.model.components2D.Gaussian2D( - ... A=1, centre_x=-5.0, centre_y=-5.0, sigma_x=1.0, sigma_y=2.0,) - - >>> scale = 0.1 - >>> x = np.arange(-10, 10, scale) - >>> y = np.arange(-10, 10, scale) - >>> X, Y = np.meshgrid(x, y) - - >>> im = hs.signals.Signal2D(g.function(X, Y)) - >>> im.axes_manager[0].scale = scale - >>> im.axes_manager[0].offset = -10 - >>> im.axes_manager[1].scale = scale - >>> im.axes_manager[1].offset = -10 - - >>> m = im.create_model() # Model initialisation - >>> gt = hs.model.components2D.Gaussian2D() - >>> m.append(gt) - - >>> # Create a boolean signal of the same shape as the signal space of im - >>> # and with all values set to `False`. - >>> signal_mask = hs.signals.Signal2D(np.zeros_like(im(), dtype=bool)) - >>> # Specify the signal range using the isig syntax - >>> signal_mask.isig[-7.:-3.,-9.:-1.] = True - - >>> m.channel_switches = signal_mask.data # Set channel switches - >>> m.fit() - -.. _model.multidimensional-label: - -Fitting multidimensional datasets -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To fit the model to all the elements of a multidimensional dataset, use -:py:meth:`~.model.BaseModel.multifit`: - -.. code-block:: python - - >>> m.multifit() # warning: this can be a lengthy process on large datasets - -:py:meth:`~.model.BaseModel.multifit` fits the model at the first position, -stores the result of the fit internally and move to the next position until -reaching the end of the dataset. - -.. NOTE:: - - Sometimes this method can fail, especially in the case of a TEM spectrum - image of a particle surrounded by vacuum (since in that case the - top-left pixel will typically be an empty signal). - - To get sensible starting parameters, you can do a single - :py:meth:`~.model.BaseModel.fit` after changing the active position - within the spectrum image (either using the plotting GUI or by directly - modifying ``s.axes_manager.indices`` as in :ref:`Setting_axis_properties`). - - After doing this, you can initialize the model at every pixel to the - values from the single pixel fit using ``m.assign_current_values_to_all()``, - and then use :py:meth:`~.model.BaseModel.multifit` to perform the fit over - the entire spectrum image. - -.. versionadded:: 1.6 New optional fitting iteration path `"serpentine"` - -Typically, curve fitting on a multidimensional dataset happens in the following -manner: Pixels are fit along the row from the first index in the first row, and once the -last pixel in the row is reached, one proceeds from the first index in the second row. -Since the fitting procedure typically uses the fit of the previous pixel -as the starting point for the next, a common problem with this fitting iteration -path is that the fitting fails going from the end of one row to the beginning of -the next, as the spectrum can change abruptly. This kind of iteration path is -the default in HyperSpy (but will change to ``'serpentine'`` in HyperSpy version -2.0). It can be explicitly set using the :py:meth:`~.model.BaseModel.multifit` -``iterpath='flyback'`` argument. - -A simple solution to the flyback fitting problem is to iterate through the -signal indices in a horizontal serpentine pattern, as seen on the image below. -This alternate iteration method can be enabled by the :py:meth:`~.model.BaseModel.multifit` -``iterpath='serpentine'`` argument. The serpentine pattern supports n-dimensional -navigation space, so the first index in the second frame of a three-dimensional -navigation space will be at the last position of the previous frame. - -.. figure:: images/FlybackVsSerpentine.png - :align: center - :width: 500 - - Comparing the scan patterns generated by the ``'flyback'`` and ``'serpentine'`` - iterpath options for a 2D navigation space. The pixel intensity and number refers - to the order that the signal is fitted in. - -In addition to ``'serpentine'`` and ``'flyback'``, ``iterpath`` can take as argument any list -or array of indices, or a generator of such, as explained in the :ref:`Iterating AxesManager ` section. - -Sometimes one may like to store and fetch the value of the parameters at a -given position manually. This is possible using -:py:meth:`~.model.BaseModel.store_current_values` and -:py:meth:`~.model.BaseModel.fetch_stored_values`. - -Visualising the result of the fit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:class:`~.model.BaseModel` :py:meth:`~.model.BaseModel.plot_results`, -:py:class:`~.component.Component` :py:meth:`~.component.Component.plot` and -:py:class:`~.component.Parameter` :py:meth:`~.component.Parameter.plot` methods -can be used to visualise the result of the fit **when fitting multidimensional -datasets**. - -.. _storing_models-label: - -Storing models --------------- - -Multiple models can be stored in the same signal. In particular, when -:py:meth:`~.model.BaseModel.store` is called, a full "frozen" copy of the model -is stored in stored in the signal's :py:class:`~.signal.ModelManager`, -which can be accessed in the ``models`` attribute (i.e. ``s.models``) -The stored models can be recreated at any time by calling -:py:meth:`~.signal.ModelManager.restore` with the stored -model name as an argument. To remove a model from storage, simply call -:py:meth:`~.signal.ModelManager.remove`. - -The stored models can be either given a name, or assigned one automatically. -The automatic naming follows alphabetical scheme, with the sequence being (a, -b, ..., z, aa, ab, ..., az, ba, ...). - -.. NOTE:: - - If you want to slice a model, you have to perform the operation on the - model itself, not its stored version - -.. WARNING:: - - Modifying a signal in-place (e.g. :py:meth:`~.signal.BaseSignal.map`, - :py:meth:`~.signal.BaseSignal.crop`, - :py:meth:`~._signals.signal1d.Signal1D.align1D`, - :py:meth:`~._signals.signal2d.Signal2D.align2D` and similar) - will invalidate all stored models. This is done intentionally. - -Current stored models can be listed by calling ``s.models``: - -.. code-block:: python - - >>> m = s.create_model() - >>> m.append(hs.model.components1D.Lorentzian()) - >>> m.store('myname') - >>> s.models - └── myname - ├── components - │ └── Lorentzian - ├── date = 2015-09-07 12:01:50 - └── dimensions = (|100) - - >>> m.append(hs.model.components1D.Exponential()) - >>> m.store() # assign model name automatically - >>> s.models - ├── a - │ ├── components - │ │ ├── Exponential - │ │ └── Lorentzian - │ ├── date = 2015-09-07 12:01:57 - │ └── dimensions = (|100) - └── myname - ├── components - │ └── Lorentzian - ├── date = 2015-09-07 12:01:50 - └── dimensions = (|100) - >>> m1 = s.models.restore('myname') - >>> m1.components - # | Attribute Name | Component Name | Component Type - ---- | ------------------- | -------------------- | -------------------- - 0 | Lorentzian | Lorentzian | Lorentzian - - -Saving and loading the result of the fit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To save a model, a convenience function :py:meth:`~.model.BaseModel.save` is -provided, which stores the current model into its signal and saves the -signal. As described in :ref:`storing_models-label`, more than just one -model can be saved with one signal. - -.. code-block:: python - - >>> m = s.create_model() - >>> # analysis and fitting goes here - >>> m.save('my_filename', 'model_name') - >>> l = hs.load('my_filename.hspy') - >>> m = l.models.restore('model_name') # or l.models.model_name.restore() - -For older versions of HyperSpy (before 0.9), the instructions were as follows: - - Note that this method is known to be brittle i.e. there is no - guarantee that a version of HyperSpy different from the one used to save - the model will be able to load it successfully. Also, it is - advisable not to use this method in combination with functions that - alter the value of the parameters interactively (e.g. - `enable_adjust_position`) as the modifications made by this functions - are normally not stored in the IPython notebook or Python script. - - To save a model: - - 1. Save the parameter arrays to a file using - :py:meth:`~.model.BaseModel.save_parameters2file`. - - 2. Save all the commands that used to create the model to a file. This - can be done in the form of an IPython notebook or a Python script. - - 3. (Optional) Comment out or delete the fitting commands (e.g. - :py:meth:`~.model.BaseModel.multifit`). - - To recreate the model: - - 1. Execute the IPython notebook or Python script. - - 2. Use :py:meth:`~.model.BaseModel.load_parameters_from_file` to load - back the parameter values and arrays. - - -Exporting the result of the fit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:class:`~.model.BaseModel` :py:meth:`~.model.BaseModel.export_results`, -:py:class:`~.component.Component` :py:meth:`~.component.Component.export` and -:py:class:`~.component.Parameter` :py:meth:`~.component.Parameter.export` -methods can be used to export the result of the optimization in all supported -formats. - -Batch setting of parameter attributes -------------------------------------- - -The following model methods can be used to ease the task of setting some important -parameter attributes. These can also be used on a per-component basis, by calling them -on individual components. - -* :py:meth:`~.model.BaseModel.set_parameters_not_free` -* :py:meth:`~.model.BaseModel.set_parameters_free` -* :py:meth:`~.model.BaseModel.set_parameters_value` - -.. _SAMFire-label: - -Smart Adaptive Multi-dimensional Fitting (SAMFire) --------------------------------------------------- - -SAMFire (Smart Adaptive Multi-dimensional Fitting) is an algorithm created to -reduce the starting value (or local / false minima) problem, which often arises -when fitting multi-dimensional datasets. - -The algorithm will be described in full when accompanying paper is published, -but we are making the implementation available now, with additional details -available in the following `conference proceeding -`_. - -The idea -^^^^^^^^ - -The main idea of SAMFire is to change two things compared to the traditional -way of fitting datasets with many dimensions in the navigation space: - - #. Pick a more sensible pixel fitting order. - #. Calculate the pixel starting parameters from already fitted parts of the - dataset. - -Both of these aspects are linked one to another and are represented by two -different strategy families that SAMFfire uses while operating. - -Strategies -^^^^^^^^^^ - -During operation SAMFire uses a list of strategies to determine how to select -the next pixel and estimate its starting parameters. Only one strategy is used -at a time. Next strategy is chosen when no new pixels are can be fitted with -the current strategy. Once either the strategy list is exhausted or the full -dataset fitted, the algorithm terminates. - -There are two families of strategies. In each family there may be many -strategies, using different statistical or significance measures. - -As a rule of thumb, the first strategy in the list should always be from the -local family, followed by a strategy from the global family. - -Local strategy family -^^^^^^^^^^^^^^^^^^^^^ - -These strategies assume that locally neighbouring pixels are similar. As a -result, the pixel fitting order seems to follow data-suggested order, and the -starting values are computed from the surrounding already fitted pixels. - -More information about the exact procedure will be available once the -accompanying paper is published. - - -Global strategy family -^^^^^^^^^^^^^^^^^^^^^^ - -Global strategies assume that the navigation coordinates of each pixel bear no -relation to it's signal (i.e. the location of pixels is meaningless). As a -result, the pixels are selected at random to ensure uniform sampling of the -navigation space. - -A number of candidate starting values are computed form global statistical -measures. These values are all attempted in order until a satisfactory result -is found (not necessarily testing all available starting guesses). As a result, -on average each pixel requires significantly more computations when compared to -a local strategy. - -More information about the exact procedure will be available once the -accompanying paper is published. - -Seed points -^^^^^^^^^^^ - -Due to the strategies using already fitted pixels to estimate the starting -values, at least one pixel has to be fitted beforehand by the user. - -The seed pixel(s) should be selected to require the most complex model present -in the dataset, however in-built goodness of fit checks ensure that only -sufficiently well fitted values are allowed to propagate. - -If the dataset consists of regions (in the navigation space) of highly -dissimilar pixels, often called "domain structures", at least one seed pixel -should be given for each unique region. - -If the starting pixels were not optimal, only part of the dataset will be -fitted. In such cases it is best to allow the algorithm terminate, then provide -new (better) seed pixels by hand, and restart SAMFire. It will use the -new seed together with the already computed parts of the data. - -Usage -^^^^^ - -After creating a model and fitting suitable seed pixels, to fit the rest of -the multi-dimensional dataset using SAMFire we must create a SAMFire instance -as follows: - -.. code-block:: python - - >>> samf = m.create_samfire(workers=None, ipyparallel=False) - -By default SAMFire will look for an `ipyparallel -`_ cluster for the -workers for around 30 seconds. If none is available, it will use -multiprocessing instead. However, if you are not planning to use ipyparallel, -it's recommended specify it explicitly via the ``ipyparallel=False`` argument, -to use the fall-back option of `multiprocessing`. - -By default a new SAMFire object already has two (and currently only) strategies -added to its strategist list: - -.. code-block:: python - - >>> samf.strategies - A | # | Strategy - -- | ---- | ------------------------- - x | 0 | Reduced chi squared strategy - | 1 | Histogram global strategy - -The currently active strategy is marked by an 'x' in the first column. - -If a new datapoint (i.e. pixel) is added manually, the "database" of the -currently active strategy has to be refreshed using the -:py:meth:`~.samfire.Samfire.refresh_database` call. - -The current strategy "database" can be plotted using the -:py:meth:`~.samfire.Samfire.plot` method. - -Whilst SAMFire is running, each pixel is checked by a ``goodness_test``, -which is by default -:py:class:`~.samfire_utils.goodness_of_fit_tests.red_chisq.red_chisq_test`, -checking the reduced chi-squared to be in the bounds of [0, 2]. - -This tolerance can (and most likely should!) be changed appropriately for the -data as follows: - -.. code-block:: python - - >>> samf.metadata.goodness_test.tolerance = 0.3 # use a sensible value - -The SAMFire managed multi-dimensional fit can be started using the -:py:meth:`~.samfire.Samfire.start` method. All keyword arguments are passed to -the underlying (i.e. usual) :py:meth:`~.model.BaseModel.fit` call: - -.. code-block:: python - - >>> samf.start(optimizer='lm', bounded=True) diff --git a/doc/user_guide/model/SAMFire.rst b/doc/user_guide/model/SAMFire.rst new file mode 100644 index 0000000000..81d34a7201 --- /dev/null +++ b/doc/user_guide/model/SAMFire.rst @@ -0,0 +1,144 @@ +.. _SAMFire-label: + +Smart Adaptive Multi-dimensional Fitting (SAMFire) +-------------------------------------------------- + +SAMFire (Smart Adaptive Multi-dimensional Fitting) is an algorithm created to +reduce the starting value (or local / false minima) problem, which often arises +when fitting multi-dimensional datasets. + +The algorithm is described in `Tomas Ostasevicius' PhD thesis `_, entitled "Multi-dimensional Data Analysis in Electron Microscopy". + +The idea +^^^^^^^^ + +The main idea of SAMFire is to change two things compared to the traditional +way of fitting datasets with many dimensions in the navigation space: + + #. Pick a more sensible pixel fitting order. + #. Calculate the pixel starting parameters from already fitted parts of the + dataset. + +Both of these aspects are linked one to another and are represented by two +different strategy families that SAMFfire uses while operating. + +Strategies +^^^^^^^^^^ + +During operation SAMFire uses a list of strategies to determine how to select +the next pixel and estimate its starting parameters. Only one strategy is used +at a time. Next strategy is chosen when no new pixels can be fitted with +the current strategy. Once either the strategy list is exhausted or the full +dataset fitted, the algorithm terminates. + +There are two families of strategies. In each family there may be many +strategies, using different statistical or significance measures. + +As a rule of thumb, the first strategy in the list should always be from the +local family, followed by a strategy from the global family. + +Local strategy family +^^^^^^^^^^^^^^^^^^^^^ + +These strategies assume that locally neighbouring pixels are similar. As a +result, the pixel fitting order seems to follow data-suggested order, and the +starting values are computed from the surrounding already fitted pixels. + +More information about the exact procedure will be available once the +accompanying paper is published. + + +Global strategy family +^^^^^^^^^^^^^^^^^^^^^^ + +Global strategies assume that the navigation coordinates of each pixel bear no +relation to it's signal (i.e. the location of pixels is meaningless). As a +result, the pixels are selected at random to ensure uniform sampling of the +navigation space. + +A number of candidate starting values are computed form global statistical +measures. These values are all attempted in order until a satisfactory result +is found (not necessarily testing all available starting guesses). As a result, +on average each pixel requires significantly more computations when compared to +a local strategy. + +More information about the exact procedure will be available once the +accompanying paper is published. + +Seed points +^^^^^^^^^^^ + +Due to the strategies using already fitted pixels to estimate the starting +values, at least one pixel has to be fitted beforehand by the user. + +The seed pixel(s) should be selected to require the most complex model present +in the dataset, however in-built goodness of fit checks ensure that only +sufficiently well fitted values are allowed to propagate. + +If the dataset consists of regions (in the navigation space) of highly +dissimilar pixels, often called "domain structures", at least one seed pixel +should be given for each unique region. + +If the starting pixels were not optimal, only part of the dataset will be +fitted. In such cases it is best to allow the algorithm terminate, then provide +new (better) seed pixels by hand, and restart SAMFire. It will use the +new seed together with the already computed parts of the data. + +Usage +^^^^^ + +After creating a model and fitting suitable seed pixels, to fit the rest of +the multi-dimensional dataset using SAMFire we must create a SAMFire instance +as follows: + +.. code-block:: python + + >>> samf = m.create_samfire(workers=None, ipyparallel=False) # doctest: +SKIP + +By default SAMFire will look for an `ipyparallel +`_ cluster for the +workers for around 30 seconds. If none is available, it will use +multiprocessing instead. However, if you are not planning to use ipyparallel, +it's recommended specify it explicitly via the ``ipyparallel=False`` argument, +to use the fall-back option of `multiprocessing`. + +By default a new SAMFire object already has two (and currently only) strategies +added to its ``strategies`` list: + +.. code-block:: python + + >>> samf.strategies # doctest: +SKIP + A | # | Strategy + -- | ---- | ------------------------- + x | 0 | Reduced chi squared strategy + | 1 | Histogram global strategy + +The currently active strategy is marked by an 'x' in the first column. + +If a new datapoint (i.e. pixel) is added manually, the "database" of the +currently active strategy has to be refreshed using the +:meth:`~.samfire.Samfire.refresh_database` call. + +The current strategy "database" can be plotted using the +:meth:`~.samfire.Samfire.plot` method. + +Whilst SAMFire is running, each pixel is checked by a ``goodness_test``, +which is by default +:class:`~.api.samfire.fit_tests.red_chisq_test`, +checking the reduced chi-squared to be in the bounds of [0, 2]. + +This tolerance can (and most likely should!) be changed appropriately for the +data as follows: + +.. code-block:: python + + >>> # use a sensible value + >>> samf.metadata.goodness_test.tolerance = 0.3 # doctest: +SKIP + +The SAMFire managed multi-dimensional fit can be started using the +:meth:`~.samfire.Samfire.start` method. All keyword arguments are passed to +the underlying (i.e. usual) :meth:`~.model.BaseModel.fit` call: + +.. code-block:: python + + >>> samf.start(optimizer='lm', bounded=True) # doctest: +SKIP diff --git a/doc/user_guide/model/adding_components.rst b/doc/user_guide/model/adding_components.rst new file mode 100644 index 0000000000..469114ebcc --- /dev/null +++ b/doc/user_guide/model/adding_components.rst @@ -0,0 +1,144 @@ +.. _adding-components: + +Adding components to the model +------------------------------ + +To print the current components in a model use +:attr:`~.model.BaseModel.components`. A table with component number, +attribute name, component name and component type will be printed: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(100)) + >>> m = s.create_model() + >>> m + + >>> m.components + # | Attribute Name | Component Name | Component Type + ---- | ------------------- | ------------------- | ------------------- + + +.. note:: Sometimes components may be created automatically. For example, if + the :class:`~._signals.signal1d.Signal1D` is recognised as EELS data, a + power-law background component may automatically be added to the model. + Therefore, the table above may not all may empty on model creation. + +To add a component to the model, first we have to create an instance of the +component. +Once the instance has been created we can add the component to the model +using the :meth:`~.model.BaseModel.append` and +:meth:`~.model.BaseModel.extend` methods for one or more components +respectively. + +As an example, let's add several :class:`~._components.gaussian.Gaussian` +components to the model: + +.. code-block:: python + + >>> gaussian = hs.model.components1D.Gaussian() # Create a Gaussian comp. + >>> m.append(gaussian) # Add it to the model + >>> m.components # Print the model components + # | Attribute Name | Component Name | Component Type + ---- | ------------------- | ------------------- | ------------------- + 0 | Gaussian | Gaussian | Gaussian + >>> gaussian2 = hs.model.components1D.Gaussian() # Create another gaussian + >>> gaussian3 = hs.model.components1D.Gaussian() # Create a third gaussian + + +We could use the :meth:`~.model.BaseModel.append` method twice to add the +two Gaussians, but when adding multiple components it is handier to use the +extend method that enables adding a list of components at once. + + +.. code-block:: python + + >>> m.extend((gaussian2, gaussian3)) # note the double parentheses! + >>> m.components + # | Attribute Name | Component Name | Component Type + ---- | ------------------- | ------------------- | ------------------- + 0 | Gaussian | Gaussian | Gaussian + 1 | Gaussian_0 | Gaussian_0 | Gaussian + 2 | Gaussian_1 | Gaussian_1 | Gaussian + +We can customise the name of the components. + +.. code-block:: python + + >>> gaussian.name = 'Carbon' + >>> gaussian2.name = 'Long Hydrogen name' + >>> gaussian3.name = 'Nitrogen' + >>> m.components + # | Attribute Name | Component Name | Component Type + ---- | ------------------- | ------------------- | ------------------- + 0 | Carbon | Carbon | Gaussian + 1 | Long_Hydrogen_name | Long Hydrogen name | Gaussian + 2 | Nitrogen | Nitrogen | Gaussian + +Notice that two components cannot have the same name: + +.. code-block:: python + + >>> gaussian2.name = 'Carbon' + Traceback (most recent call last): + File "", line 1, in + g2.name = "Carbon" + File "/home/fjd29/Python/hyperspy/hyperspy/component.py", line 466, in + name "the name " + str(value)) + ValueError: Another component already has the name Carbon + + +It is possible to access the components in the model by their name or by the +index in the model. + +.. code-block:: python + + >>> m.components + # | Attribute Name | Component Name | Component Type + ---- | ------------------- | ------------------- | ------------------- + 0 | Carbon | Carbon | Gaussian + 1 | Long_Hydrogen_name | Long Hydrogen name | Gaussian + 2 | Nitrogen | Nitrogen | Gaussian + >>> m[0] + + >>> m["Long Hydrogen name"] + + + +In addition, the components can be accessed in the +:attr:`~.model.BaseModel.components` `Model` attribute. This is specially +useful when working in interactive data analysis with IPython because it +enables tab completion. + +.. code-block:: python + + >>> m.components + # | Attribute Name | Component Name | Component Type + ---- | ------------------- | ------------------- | ------------------- + 0 | Carbon | Carbon | Gaussian + 1 | Long_Hydrogen_name | Long Hydrogen name | Gaussian + 2 | Nitrogen | Nitrogen | Gaussian + >>> m.components.Long_Hydrogen_name + + + +It is possible to "switch off" a component by setting its +``active`` attribute to ``False``. When a component is +switched off, to all effects it is as if it was not part of the model. To +switch it back on simply set the ``active`` attribute back to ``True``. + +In multi-dimensional signals it is possible to store the value of the +``active`` attribute at each navigation index. +To enable this feature for a given component set the +:attr:`~.component.Component.active_is_multidimensional` attribute to +`True`. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(100).reshape(10,10)) + >>> m = s.create_model() + >>> g1 = hs.model.components1D.Gaussian() + >>> g2 = hs.model.components1D.Gaussian() + >>> m.extend([g1,g2]) + >>> g1.active_is_multidimensional = True + >>> m.set_component_active_value(False) + >>> m.set_component_active_value(True, only_current=True) diff --git a/doc/user_guide/model/creating_model.rst b/doc/user_guide/model/creating_model.rst new file mode 100644 index 0000000000..9f348f297e --- /dev/null +++ b/doc/user_guide/model/creating_model.rst @@ -0,0 +1,41 @@ +.. _creating_model: + +Creating a model +---------------- + +A :class:`~.models.model1d.Model1D` can be created for data in the +:class:`~.api.signals.Signal1D` class using the +:meth:`~.api.signals.Signal1D.create_model` method: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(300).reshape(30, 10)) + >>> m = s.create_model() # Creates the 1D-Model and assign it to m + +Similarly, a :class:`~.models.model2d.Model2D` can be created for data +in the :class:`~.api.signals.Signal2D` class using the +:meth:`~.api.signals.Signal2D.create_model` method: + +.. code-block:: python + + >>> im = hs.signals.Signal2D(np.arange(300).reshape(3, 10, 10)) + >>> mod = im.create_model() # Create the 2D-Model and assign it to mod + +The syntax for creating both one-dimensional and two-dimensional models is thus +identical for the user in practice. When a model is created you may be +prompted to provide important information not already included in the +datafile, `e.g.` if ``s`` is EELS data, you may be asked for the accelerating +voltage, convergence and collection semi-angles etc. + +.. note:: + + * Before creating a model verify that the + :attr:`~.axes.BaseDataAxis.is_binned` attribute + of the signal axis is set to the correct value because the resulting + model depends on this parameter. See :ref:`signal.binned` for more details. + * When importing data that has been binned using other software, in + particular Gatan's DM, the stored values may be the averages of the + binned channels or pixels, instead of their sum, as would be required + for proper statistical analysis. We therefore cannot guarantee that + the statistics will be valid, and so strongly recommend that all + pre-fitting binning is performed using Hyperspy. diff --git a/doc/user_guide/model/fitting_big_data.rst b/doc/user_guide/model/fitting_big_data.rst new file mode 100644 index 0000000000..469ffd583c --- /dev/null +++ b/doc/user_guide/model/fitting_big_data.rst @@ -0,0 +1,3 @@ +Fitting big data +---------------- +See the section in :ref:`FitBigData-label` working with big data. diff --git a/doc/user_guide/model/fitting_model.rst b/doc/user_guide/model/fitting_model.rst new file mode 100644 index 0000000000..3bed5aa48b --- /dev/null +++ b/doc/user_guide/model/fitting_model.rst @@ -0,0 +1,766 @@ + +.. _model.fitting: + +Fitting the model to the data +----------------------------- + +To fit the model to the data at the current coordinates (e.g. to fit one +spectrum at a particular point in a spectrum-image), use +:meth:`~.model.BaseModel.fit`. HyperSpy implements a number of +different optimization approaches, each of which can have particular +benefits and/or drawbacks depending on your specific application. +A good approach to choosing an optimization approach is to ask yourself +the question "Do you want to...": + +* Apply bounds to your model parameter values? +* Use gradient-based fitting algorithms to accelerate your fit? +* Estimate the standard deviations of the parameter values found by the fit? +* Fit your data in the least-squares sense, or use another loss function? +* Find the global optima for your parameters, or is a local optima acceptable? + +Optimization algorithms +^^^^^^^^^^^^^^^^^^^^^^^ + +The following table summarizes the features of some of the optimizers +currently available in HyperSpy, including whether they support parameter +bounds, gradients and parameter error estimation. The "Type" column indicates +whether the optimizers find a local or global optima. + +.. _optimizers-table: + +.. table:: Features of supported curve-fitting optimizers. + + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | Optimizer | Bounds | Gradients | Errors | Loss function | Type | Linear | + +=================================+==========+===========+==========+===============+========+========+ + | ``"lm"`` (default) | Yes | Yes | Yes | Only ``"ls"`` | local | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"trf"`` | Yes | Yes | Yes | Only ``"ls"`` | local | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"dogbox"`` | Yes | Yes | Yes | Only ``"ls"`` | local | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"odr"`` | No | Yes | Yes | Only ``"ls"`` | local | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"lstsq"`` | No | No | Yes [1]_ | Only ``"ls"`` | global | Yes | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"ols"`` | No | No | Yes [1]_ | Only ``"ls"`` | global | Yes | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"nnls"`` | No | No | Yes [1]_ | Only ``"ls"`` | global | Yes | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"ridge"`` | No | No | Yes [1]_ | Only ``"ls"`` | global | Yes | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | :func:`scipy.optimize.minimize` | Yes [2]_ | Yes [2]_ | No | All | local | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"Differential Evolution"`` | Yes | No | No | All | global | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"Dual Annealing"`` [3]_ | Yes | No | No | All | global | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + | ``"SHGO"`` [3]_ | Yes | No | No | All | global | No | + +---------------------------------+----------+-----------+----------+---------------+--------+--------+ + +.. rubric:: Footnotes + +.. [1] Requires the :meth:`~hyperspy.model.BaseModel.multifit` ``calculate_errors = True`` argument + in most cases. See the documentation below on :ref:`linear least square fitting ` + for more info. + +.. [2] **All** of the fitting algorithms available in :func:`scipy.optimize.minimize` are currently + supported by HyperSpy; however, only some of them support bounds and/or gradients. For more information, + please see the `SciPy documentation `_. + +.. [3] Requires ``scipy >= 1.2.0``. + + + +The default optimizer in HyperSpy is ``"lm"``, which stands for the `Levenberg-Marquardt +algorithm `_. In +earlier versions of HyperSpy (< 1.6) this was known as ``"leastsq"``. + +Loss functions +^^^^^^^^^^^^^^ + +HyperSpy supports a number of loss functions. The default is ``"ls"``, +i.e. the least-squares loss. For the vast majority of cases, this loss +function is appropriate, and has the additional benefit of supporting +parameter error estimation and :ref:`goodness-of-fit ` +testing. However, if your data contains very low counts per pixel, or +is corrupted by outliers, the ``"ML-poisson"`` and ``"huber"`` loss +functions may be worth investigating. + +Least squares with error estimation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following example shows how to perfom least squares optimization with +error estimation. First we create data consisting of a line +``y = a*x + b`` with ``a = 1`` and ``b = 100``, and we then add Gaussian +noise to it: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(100, 300, dtype='float32')) + >>> s.add_gaussian_noise(std=100) + +To fit it, we create a model consisting of a +:class:`~._components.polynomial.Polynomial` component of order 1 and fit it +to the data. + +.. code-block:: python + + >>> m = s.create_model() + >>> line = hs.model.components1D.Polynomial(order=1) + >>> m.append(line) + >>> m.fit() # doctest: +SKIP + +Once the fit is complete, the optimized value of the parameters and their +estimated standard deviation are stored in the following line attributes: + +.. code-block:: python + + >>> line.a0.value # doctest: +SKIP + 0.9924615648843765 + >>> line.a1.value # doctest: +SKIP + 103.67507406125888 + >>> line.a0.std # doctest: +SKIP + 0.11771053738516088 + >>> line.a1.std # doctest: +SKIP + 13.541061301257537 + +.. warning:: + + When the noise is heteroscedastic, only if the + ``metadata.Signal.Noise_properties.variance`` attribute of the + :class:`~._signals.signal1d.Signal1D` instance is defined can + the parameter standard deviations be estimated accurately. + + If the variance is not defined, the standard deviations are still + computed, by setting variance equal to 1. However, this calculation + will not be correct unless an accurate value of the variance is + provided. See :ref:`signal.noise_properties` for more information. + +.. _weighted_least_squares-label: + +Weighted least squares with error estimation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the following example, we add Poisson noise to the data instead of +Gaussian noise, and proceed to fit as in the previous example. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(300)) + >>> s.add_poissonian_noise() + >>> m = s.create_model() + >>> line = hs.model.components1D.Polynomial(order=1) + >>> m.append(line) + >>> m.fit() # doctest: +SKIP + >>> line.a0.value # doctest: +SKIP + -0.7262000522775925 + >>> line.a1.value # doctest: +SKIP + 1.0086925334859176 + >>> line.a0.std # doctest: +SKIP + 1.4141418570079 + >>> line.a1.std # doctest: +SKIP + 0.008185019194679451 + +Because the noise is heteroscedastic, the least squares optimizer estimation is +biased. A more accurate result can be obtained with weighted least squares, +where the weights are proportional to the inverse of the noise variance. +Although this is still biased for Poisson noise, it is a good approximation +in most cases where there are a sufficient number of counts per pixel. + +.. code-block:: python + + >>> exp_val = hs.signals.Signal1D(np.arange(300)+1) + >>> s.estimate_poissonian_noise_variance(expected_value=exp_val) + >>> line.estimate_parameters(s, 10, 250) + True + >>> m.fit() # doctest: +SKIP + >>> line.a0.value # doctest: +SKIP + -0.6666008600519397 + >>> line.a1.value # doctest: +SKIP + 1.017145603577098 + >>> line.a0.std # doctest: +SKIP + 0.8681360488613021 + >>> line.a1.std # doctest: +SKIP + 0.010308732161043038 + + +.. warning:: + + When the attribute ``metadata.Signal.Noise_properties.variance`` + is defined, the behaviour is to perform a weighted least-squares + fit using the inverse of the noise variance as the weights. + In this scenario, to then disable weighting, you will need to **unset** + the attribute. You can achieve this with + :meth:`~.api.signals.BaseSignal.set_noise_variance`: + + .. code-block:: python + + >>> m.signal.set_noise_variance(None) # This will now be an unweighted fit + >>> m.fit() # doctest: +SKIP + >>> line.a0.value # doctest: +SKIP + -1.9711403542163477 + >>> line.a1.value # doctest: +SKIP + 1.0258716193502546 + +Poisson maximum likelihood estimation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To avoid biased estimation in the case of data corrupted by Poisson noise +with very few counts, we can use Poisson maximum likelihood estimation (MLE) instead. +This is an unbiased estimator for Poisson noise. To perform MLE, we must +use a general, non-linear optimizer from the :ref:`table above `, +such as Nelder-Mead or L-BFGS-B: + +.. code-block:: python + + >>> m.fit(optimizer="Nelder-Mead", loss_function="ML-poisson") # doctest: +SKIP + >>> line.a0.value # doctest: +SKIP + 0.00025567973144090695 + >>> line.a1.value # doctest: +SKIP + 1.0036866523183754 + +Estimation of the parameter errors is not currently supported for Poisson +maximum likelihood estimation. + +Huber loss function +~~~~~~~~~~~~~~~~~~~ + +HyperSpy also implements the +`Huber loss `_ function, +which is typically less sensitive to outliers in the data compared +to the least-squares loss. Again, we need to use one of the general +non-linear optimization algorithms: + +.. code-block:: python + + >>> m.fit(optimizer="Nelder-Mead", loss_function="huber") # doctest: +SKIP + +Estimation of the parameter errors is not currently supported +for the Huber loss function. + +Custom loss functions +~~~~~~~~~~~~~~~~~~~~~ + +As well as the built-in loss functions described above, +a custom loss function can be passed to the model: + +.. code-block:: python + + >>> def my_custom_function(model, values, data, weights=None): + ... """ + ... Parameters + ... ---------- + ... model : Model instance + ... the model that is fitted. + ... values : np.ndarray + ... A one-dimensional array with free parameter values suggested by the + ... optimizer (that are not yet stored in the model). + ... data : np.ndarray + ... A one-dimensional array with current data that is being fitted. + ... weights : {np.ndarray, None} + ... An optional one-dimensional array with parameter weights. + ... + ... Returns + ... ------- + ... score : float + ... A signle float value, representing a score of the fit, with + ... lower values corresponding to better fits. + ... """ + ... # Almost any operation can be performed, for example: + ... # First we store the suggested values in the model + ... model.fetch_values_from_array(values) + ... + ... # Evaluate the current model + ... cur_value = model(onlyactive=True) + ... + ... # Calculate the weighted difference with data + ... if weights is None: + ... weights = 1 + ... difference = (data - cur_value) * weights + ... + ... # Return squared and summed weighted difference + ... return (difference**2).sum() + + >>> # We must use a general non-linear optimizer + >>> m.fit(optimizer='Nelder-Mead', loss_function=my_custom_function) # doctest: +SKIP + +If the optimizer requires an analytical gradient function, it can be similarly +passed, using the following signature: + +.. code-block:: python + + >>> def my_custom_gradient_function(model, values, data, weights=None): + ... """ + ... Parameters + ... ---------- + ... model : Model instance + ... the model that is fitted. + ... values : np.ndarray + ... A one-dimensional array with free parameter values suggested by the + ... optimizer (that are not yet stored in the model). + ... data : np.ndarray + ... A one-dimensional array with current data that is being fitted. + ... weights : {np.ndarray, None} + ... An optional one-dimensional array with parameter weights. + ... + ... Returns + ... ------- + ... gradients : np.ndarray + ... a one-dimensional array of gradients, the size of `values`, + ... containing each parameter gradient with the given values + ... """ + ... # As an example, estimate maximum likelihood gradient: + ... model.fetch_values_from_array(values) + ... cur_value = model(onlyactive=True) + ... + ... # We use in-built jacobian estimation + ... jac = model._jacobian(values, data) + ... + ... return -(jac * (data / cur_value - 1)).sum(1) + + >>> # We must use a general non-linear optimizer again + >>> m.fit(optimizer='L-BFGS-B', + ... loss_function=my_custom_function, + ... grad=my_custom_gradient_function) # doctest: +SKIP + +Using gradient information +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 1.6 ``grad="analytical"`` and ``grad="fd"`` keyword arguments + +Optimization algorithms that take into account the gradient of +the loss function will often out-perform so-called "derivative-free" +optimization algorithms in terms of how rapidly they converge to a +solution. HyperSpy can use analytical gradients for model-fitting, +as well as numerical estimates of the gradient based on finite differences. + +If all the components in the model support analytical gradients, +you can pass ``grad="analytical"`` in order to use this information +when fitting. The results are typically more accurate than an +estimated gradient, and the optimization often runs faster since +fewer function evaluations are required to calculate the gradient. + +Following the above examples: + +.. code-block:: python + + >>> m = s.create_model() + >>> line = hs.model.components1D.Polynomial(order=1) + >>> m.append(line) + + >>> # Use a 2-point finite-difference scheme to estimate the gradient + >>> m.fit(grad="fd", fd_scheme="2-point") # doctest: +SKIP + + >>> # Use the analytical gradient + >>> m.fit(grad="analytical") # doctest: +SKIP + + >>> # Huber loss and Poisson MLE functions + >>> # also support analytical gradients + >>> m.fit(grad="analytical", loss_function="huber") # doctest: +SKIP + >>> m.fit(grad="analytical", loss_function="ML-poisson") # doctest: +SKIP + +.. note:: + + Analytical gradients are not yet implemented for the + :class:`~.models.model2d.Model2D` class. + +Bounded optimization +^^^^^^^^^^^^^^^^^^^^ + +Non-linear optimization can sometimes fail to converge to a good optimum, +especially if poor starting values are provided. Problems of ill-conditioning +and non-convergence can be improved by using bounded optimization. + +All components' parameters have the attributes ``parameter.bmin`` and +``parameter.bmax`` ("bounded min" and "bounded max"). When fitting using the +``bounded=True`` argument by ``m.fit(bounded=True)`` or ``m.multifit(bounded=True)``, +these attributes set the minimum and maximum values allowed for ``parameter.value``. + +Currently, not all optimizers support bounds - see the +:ref:`table above `. In the following example, a Gaussian +histogram is fitted using a :class:`~._components.gaussian.Gaussian` +component using the Levenberg-Marquardt ("lm") optimizer and bounds +on the ``centre`` parameter. + +.. code-block:: python + + >>> s = hs.signals.BaseSignal(np.random.normal(loc=10, scale=0.01, + ... size=100000)).get_histogram() + >>> s.axes_manager[-1].is_binned = True + >>> m = s.create_model() + >>> g1 = hs.model.components1D.Gaussian() + >>> m.append(g1) + >>> g1.centre.value = 7 + >>> g1.centre.bmin = 7 + >>> g1.centre.bmax = 14 + >>> m.fit(optimizer="lm", bounded=True) # doctest: +SKIP + >>> m.print_current_values() # doctest: +SKIP + Model1D: histogram + Gaussian: Gaussian + Active: True + Parameter Name | Free | Value | Std | Min | Max + ============== | ===== | ========== | ========== | ========== | ========== + A | True | 99997.3481 | 232.333693 | 0.0 | None + sigma | True | 0.00999184 | 2.68064163 | None | None + centre | True | 9.99980788 | 2.68064070 | 7.0 | 14.0 + +.. _linear_fitting-label: + +Linear least squares +^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 1.7 + +Linear fitting can be used to address some of the drawbacks of non-linear optimization: + +- it doesn't suffer from the *starting parameters* issue, which can sometimes be problematic + with nonlinear fitting. Since linear fitting uses linear algebra to find the + solution (find the parameter values of the model), the solution is a unique solution, + while nonlinear optimization uses an iterative approach and therefore relies + on the initial values of the parameters. +- it is fast, because i) in favorable situations, the signal can be fitted in a vectorized + fashion, i.e. the signal is fitted in a single run instead of iterating over + the navigation dimension; ii) it is not iterative, `i.e.` it does the + calculation only one time instead of 10-100 iterations, depending on how + quickly the non-linear optimizer will converge. + +However, linear fitting can *only* fit linear models and will not be able to fit +parameters which vary *non-linearly*. + +A component is considered linear when its free parameters scale the component only +in the y-axis. For the exemplary function ``y = a*x**b``, ``a`` is a linear parameter, whilst ``b`` +is not. If ``b.free = False``, then the component is linear. +Components can also be made up of several linear parts. For instance, +the 2D-polynomial ``y = a*x**2+b*y**2+c*x+d*y+e`` is entirely linear. + +.. note:: + + After creating a model with values for the nonlinear parameters, a quick way to set + all nonlinear parameters to be ``free = False`` is to use ``m.set_parameters_not_free(only_nonlinear=True)`` + +To check if a parameter is linear, use the model or component method +:meth:`~hyperspy.model.BaseModel.print_current_values()`. For a component to be +considered linear, it can hold only one free parameter, and that parameter +must be linear. + +If all components in a model are linear, then a linear optimizer can be used to +solve the problem as a linear regression problem! This can be done using two approaches: + +- the standard pixel-by-pixel approach as used by the *nonlinear* optimizers +- fit the entire dataset in one *vectorised* operation, which will be much faster (up to 1000 times). + However, there is a caveat: all fixed parameters must have the same value across the dataset in + order to avoid creating a very large array whose size will scale with the number of different + values of the non-free parameters. + +.. note:: + + A good example of a linear model in the electron-microscopy field is an Energy-Dispersive + X-ray Spectroscopy (EDS) dataset, which typically consists of a polynomial background and + Gaussian peaks with well-defined energy (``Gaussian.centre``) and peak widths + (``Gaussian.sigma``). This dataset can be fit extremely fast with a linear optimizer. + +There are several implementations of linear least squares fitting in HyperSpy: + +- ``'lstsq'``: least squares using :func:`numpy.linalg.lstsq`, or + :func:`dask.array.linalg.lstsq` for lazy signals, +- ``'ols'``: ordinary least squares, using :class:`sklearn.linear_model.LinearRegression`, +- ``'nnls'``: least squares with positive constraints on the coefficients using + :class:`sklearn.linear_model.LinearRegression`, +- ``'ridge'``: least square supporting regularisation using + :class:`sklearn.linear_model.Ridge`. The parameter ``alpha`` controls the + regularization strength and can significantly affect the results. + +See the corresponding documentation in `scikit-learn `_ +or :func:`numpy.linalg.lstsq` for passing parameters to :meth:`~hyperspy.model.BaseModel.fit` or +:meth:`~hyperspy.model.BaseModel.multifit`. +Only the ``'lstsq'`` optimizer supports lazy signals. + +As for non-linear least squares fitting, :ref:`weighted least squares ` +are supported. + +In the following example, we first generate a 300x300 navigation signal of varying total intensity, +and then populate it with an EDS spectrum at each point. The signal can be fitted with a polynomial +background and a Gaussian for each peak. Hyperspy automatically adds these to the model, and fixes +the ``centre`` and ``sigma`` parameters to known values. Fitting this model with a non-linear optimizer +can about half an hour on a decent workstation. With a linear optimizer, it takes seconds. + +.. code-block:: python + + >>> nav = hs.signals.Signal2D(np.random.random((300, 300))).T + >>> s = exspy.data.EDS_TEM_FePt_nanoparticles() * nav # doctest: +SKIP + >>> m = s.create_model() # doctest: +SKIP + + >>> m.multifit(optimizer='lstsq') # doctest: +SKIP + +Standard errors for the parameters are by default not calculated when the dataset +is fitted in vectorized fashion, because it has large memory requirement. +If errors are required, either pass ``calculate_errors=True`` as an argument +to :meth:`~hyperspy.model.BaseModel.multifit`, or rerun +:meth:`~hyperspy.model.BaseModel.multifit` with a nonlinear optimizer, +which should run fast since the parameters are already optimized. + +None of the linear optimizers currently support bounds. + +Optimization results +^^^^^^^^^^^^^^^^^^^^ + +After fitting the model, details about the optimization +procedure, including whether it finished successfully, +are returned as :class:`scipy.optimize.OptimizeResult` object, +according to the keyword argument ``return_info=True``. +These details are often useful for diagnosing problems such +as a poorly-fitted model or a convergence failure. +You can also access the object as the ``fit_output`` attribute: + +.. code-block:: python + + >>> m.fit() # doctest: +SKIP + >>> type(m.fit_output) # doctest: +SKIP + + +You can also print this information using the +``print_info`` keyword argument: + +.. code-block:: python + + # Print the info to stdout + >>> m.fit(optimizer="L-BFGS-B", print_info=True) # doctest: +SKIP + Fit info: + optimizer=L-BFGS-B + loss_function=ls + bounded=False + grad="fd" + Fit result: + hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64> + message: b'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH' + nfev: 168 + nit: 32 + njev: 42 + status: 0 + success: True + x: array([ 9.97614503e+03, -1.10610734e-01, 1.98380701e+00]) + + +.. _model.goodness_of_fit: + +Goodness of fit +^^^^^^^^^^^^^^^ + +The chi-squared, reduced chi-squared and the degrees of freedom are +computed automatically when fitting a (weighted) least-squares model +(i.e. only when ``loss_function="ls"``). They are stored as signals, in the +:attr:`~.model.BaseModel.chisq`, :attr:`~.model.BaseModel.red_chisq` and +:attr:`~.model.BaseModel.dof` attributes of the model respectively. + +.. warning:: + + Unless ``metadata.Signal.Noise_properties.variance`` contains + an accurate estimation of the variance of the data, the chi-squared and + reduced chi-squared will not be computed correctly. This is true for both + homocedastic and heteroscedastic noise. + +.. _model.visualization: + +Visualizing the model +^^^^^^^^^^^^^^^^^^^^^ + +To visualise the result use the :meth:`~.models.model1d.Model1D.plot` method: + +.. code-block:: python + + >>> m.plot() # Visualise the results + +By default only the full model line is displayed in the plot. In addition, it +is possible to display the individual components by calling +:meth:`~.model.BaseModel.enable_plot_components` or directly using +:meth:`~.models.model1d.Model1D.plot`: + +.. code-block:: python + + >>> m.plot(plot_components=True) # Visualise the results + +To disable this feature call +:meth:`~.model.BaseModel.disable_plot_components`. + +.. versionadded:: 1.4 :meth:`~.api.signals.Signal1D.plot` keyword arguments + +All extra keyword arguments are passed to the :meth:`~.api.signals.Signal1D.plot` +method of the corresponding signal object. The following example plots the +model signal figure but not its navigator: + +.. code-block:: python + + >>> m.plot(navigator=False) + +By default the model plot is automatically updated when any parameter value +changes. It is possible to suspend this feature with +:meth:`~.model.BaseModel.suspend_update`. + +.. To resume it use :meth:`~.model.BaseModel.resume_update`. + +.. _model.starting: + +Setting the initial parameters +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Non-linear optimization often requires setting sensible starting parameters. +This can be done by plotting the model and adjusting the parameters by hand. + +.. versionchanged:: 1.3 + All ``notebook_interaction`` methods renamed to :meth:`~.model.BaseModel.gui`. + The ``notebook_interaction`` methods were removed in 2.0. + +.. _notebook_interaction-label: + +If running in a Jupyter Notebook, interactive widgets can be used to +conveniently adjust the parameter values by running +:meth:`~.model.BaseModel.gui` for :class:`~.model.BaseModel`, +:class:`~.component.Component` and +:class:`~.component.Parameter`. + +.. figure:: ../images/notebook_widgets.png + :align: center + :width: 985 + + Interactive widgets for the full model in a Jupyter notebook. Drag the + sliders to adjust current parameter values. Typing different minimum and + maximum values changes the boundaries of the slider. + +Also, :meth:`~.models.model1d.Model1D.enable_adjust_position` provides an +interactive way of setting the position of the components with a +well-defined position. +:meth:`~.models.model1d.Model1D.disable_adjust_position` disables the tool. + +.. figure:: ../images/model_adjust_position.png + :align: center + :width: 500 + + Interactive component position adjustment tool. Drag the vertical lines + to set the initial value of the position parameter. + +Exclude data from the fitting process +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following :class:`~.model.BaseModel` methods can be used to exclude +undesired spectral channels from the fitting process: + +* :meth:`~.models.model1d.Model1D.set_signal_range` +* :meth:`~.models.model1d.Model1D.add_signal_range` +* :meth:`~.model.BaseModel.set_signal_range_from_mask` +* :meth:`~.models.model1d.Model1D.remove_signal_range` +* :meth:`~.models.model1d.Model1D.reset_signal_range` + +The example below shows how a boolean array can be easily created from the +signal and how the ``isig`` syntax can be used to define the signal range. + +.. code-block:: python + + >>> # Create a sample 2D gaussian dataset + >>> g = hs.model.components2D.Gaussian2D( + ... A=1, centre_x=-5.0, centre_y=-5.0, sigma_x=1.0, sigma_y=2.0,) + + >>> scale = 0.1 + >>> x = np.arange(-10, 10, scale) + >>> y = np.arange(-10, 10, scale) + >>> X, Y = np.meshgrid(x, y) + + >>> im = hs.signals.Signal2D(g.function(X, Y)) + >>> im.axes_manager[0].scale = scale + >>> im.axes_manager[0].offset = -10 + >>> im.axes_manager[1].scale = scale + >>> im.axes_manager[1].offset = -10 + + >>> m = im.create_model() # Model initialisation + >>> gt = hs.model.components2D.Gaussian2D() + >>> m.append(gt) + + >>> m.set_signal_range(-7, -3, -9, -1) # Set signal range + >>> m.fit() # doctest: +SKIP + + Alternatively, create a boolean signal of the same shape + as the signal space of im + + >>> signal_mask = im > 0.01 + + >>> m.set_signal_range_from_mask(signal_mask.data) # Set signal range + >>> m.fit() # doctest: +SKIP + +.. _model.multidimensional-label: + +Fitting multidimensional datasets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To fit the model to all the elements of a multidimensional dataset, use +:meth:`~.model.BaseModel.multifit`: + +.. code-block:: python + + >>> m.multifit() # warning: this can be a lengthy process on large datasets + +:meth:`~.model.BaseModel.multifit` fits the model at the first position, +stores the result of the fit internally and move to the next position until +reaching the end of the dataset. + +.. NOTE:: + + Sometimes this method can fail, especially in the case of a TEM spectrum + image of a particle surrounded by vacuum (since in that case the + top-left pixel will typically be an empty signal). + + To get sensible starting parameters, you can do a single + :meth:`~.model.BaseModel.fit` after changing the active position + within the spectrum image (either using the plotting GUI or by directly + modifying ``s.axes_manager.indices`` as in :ref:`Setting_axis_properties`). + + After doing this, you can initialize the model at every pixel to the + values from the single pixel fit using ``m.assign_current_values_to_all()``, + and then use :meth:`~.model.BaseModel.multifit` to perform the fit over + the entire spectrum image. + +.. versionadded:: 1.6 New optional fitting iteration path `"serpentine"` +.. versionadded:: 2.0 New default iteration path for fitting is "serpentine"` + +In HyperSpy, curve fitting on a multidimensional dataset happens in the following +manner: Pixels are fit along the row from the first index in the first row, and +once the last pixel in the row is reached, one proceeds in reverse order from the +last index in the second row. This procedure leads to a serpentine pattern, as +seen on the image below. The serpentine pattern supports n-dimensional +navigation space, so the first index in the second frame of a three-dimensional +navigation space will be at the last position of the previous frame. + +An alternative scan pattern would be the ``'flyback'`` scheme, where the map is +iterated through row by row, always starting from the first index. This pattern +can be explicitly set using the :meth:`~.model.BaseModel.multifit` +``iterpath='flyback'`` argument. However, the ``'serpentine'`` strategy is +usually more robust, as it always moves on to a neighbouring pixel and the fitting +procedure uses the fit result of the previous pixel as the starting point for the +next. A common problem in the ``'flyback'`` pattern is that the fitting fails +going from the end of one row to the beginning of the next, as the spectrum can +change abruptly. + +.. figure:: ../images/FlybackVsSerpentine.png + :align: center + :width: 500 + + Comparing the scan patterns generated by the ``'flyback'`` and ``'serpentine'`` + iterpath options for a 2D navigation space. The pixel intensity and number + refers to the order that the signal is fitted in. + +In addition to ``'serpentine'`` and ``'flyback'``, ``iterpath`` can take as +argument any list or array of indices, or a generator of such, as explained in +the :ref:`Iterating AxesManager ` section. + +Sometimes one may like to store and fetch the value of the parameters at a +given position manually. This is possible using +:meth:`~.model.BaseModel.store_current_values` and +:meth:`~.model.BaseModel.fetch_stored_values`. + +Visualising the result of the fit +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`~.model.BaseModel` :meth:`~.model.BaseModel.plot_results`, +:class:`~.component.Component` :meth:`~.component.Component.plot` and +:class:`~.component.Parameter` :meth:`~.component.Parameter.plot` methods +can be used to visualise the result of the fit **when fitting multidimensional +datasets**. diff --git a/doc/user_guide/model/index.rst b/doc/user_guide/model/index.rst new file mode 100644 index 0000000000..d6f0a231a4 --- /dev/null +++ b/doc/user_guide/model/index.rst @@ -0,0 +1,37 @@ +.. _model-label: + +Model fitting +************* + +HyperSpy can perform curve fitting of one-dimensional signals (spectra) and +two-dimensional signals (images) in `n`-dimensional data sets. +Models are defined by adding individual functions (components in HyperSpy's +terminology) to a :class:`~.model.BaseModel` instance. Those individual +components are then summed to create the final model function that can be +fitted to the data, by adjusting the free parameters of the individual +components. + + +Models can be created and fit to experimental data in both one and two +dimensions i.e. spectra and images respectively. Most of the syntax is +identical in either case. A one-dimensional model is created when a model +is created for a :class:`~._signals.signal1d.Signal1D` whereas a two- +dimensional model is created for a :class:`~._signals.signal2d.Signal2D`. + +.. note:: + + Plotting and analytical gradient-based fitting methods are not yet + implemented for the :class:`~.models.model2d.Model2D` class. + +.. toctree:: + :maxdepth: 2 + + creating_model.rst + model_components.rst + adding_components.rst + indexing_model.rst + model_parameters.rst + fitting_model.rst + storing_models.rst + fitting_big_data.rst + SAMFire.rst diff --git a/doc/user_guide/model/indexing_model.rst b/doc/user_guide/model/indexing_model.rst new file mode 100644 index 0000000000..c0fe372877 --- /dev/null +++ b/doc/user_guide/model/indexing_model.rst @@ -0,0 +1,21 @@ +.. _model_indexing-label: + +Indexing the model +------------------ + +Often it is useful to consider only part of the model - for example at +a particular location (i.e. a slice in the navigation space) or energy range +(i.e. a slice in the signal space). This can be done using exactly the same +syntax that we use for signal :ref:`indexing `. +:attr:`~.model.BaseModel.red_chisq` and :attr:`~.model.BaseModel.dof` +are automatically recomputed for the resulting slices. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(100).reshape(10,10)) + >>> m = s.create_model() + >>> m.append(hs.model.components1D.Gaussian()) + >>> # select first three navigation pixels and last five signal channels + >>> m1 = m.inav[:3].isig[-5:] + >>> m1.signal + diff --git a/doc/user_guide/model/model_components.rst b/doc/user_guide/model/model_components.rst new file mode 100644 index 0000000000..b2aa707e3f --- /dev/null +++ b/doc/user_guide/model/model_components.rst @@ -0,0 +1,176 @@ +.. _model-components: + +Model components +---------------- + +In HyperSpy a model consists of a sum of individual components. For convenience, +HyperSpy provides a number of pre-defined model components as well as mechanisms +to create your own components. + +.. _model_components-label: + +Pre-defined model components +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Various components are available in one (:mod:`~.api.model.components1D`) and +two-dimensions (:mod:`~.api.model.components2D`) to construct a +model. + +The following general components are currently available for one-dimensional models: + +* :class:`~.api.model.components1D.Arctan` +* :class:`~.api.model.components1D.Bleasdale` +* :class:`~.api.model.components1D.Doniach` +* :class:`~.api.model.components1D.Erf` +* :class:`~.api.model.components1D.Exponential` +* :class:`~.api.model.components1D.Expression` +* :class:`~.api.model.components1D.Gaussian` +* :class:`~.api.model.components1D.GaussianHF` +* :class:`~.api.model.components1D.HeavisideStep` +* :class:`~.api.model.components1D.Logistic` +* :class:`~.api.model.components1D.Lorentzian` +* :class:`~.api.model.components1D.Offset` +* :class:`~.api.model.components1D.Polynomial` +* :class:`~.api.model.components1D.PowerLaw` +* :class:`~.api.model.components1D.ScalableFixedPattern` +* :class:`~.api.model.components1D.SkewNormal` +* :class:`~.api.model.components1D.Voigt` +* :class:`~.api.model.components1D.SplitVoigt` + +The following components are currently available for two-dimensional models: + +* :class:`~.api.model.components1D.Expression` +* :class:`~.api.model.components2D.Gaussian2D` + +However, this doesn't mean that you have to limit yourself to this meagre +list of functions. As discussed below, it is very easy to turn a +mathematical, fixed-pattern or Python function into a component. + +.. _expression_component-label: + +Define components from a mathematical expression +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +The easiest way to turn a mathematical expression into a component is using the +:class:`~._components.expression.Expression` component. For example, the +following is all you need to create a +:class:`~._components.gaussian.Gaussian` component with more sensible +parameters for spectroscopy than the one that ships with HyperSpy: + +.. code-block:: python + + >>> g = hs.model.components1D.Expression( + ... expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2)", + ... name="Gaussian", + ... position="x0", + ... height=1, + ... fwhm=1, + ... x0=0, + ... module="numpy") + +If the expression is inconvenient to write out in full (e.g. it's long and/or +complicated), multiple substitutions can be given, separated by semicolons. +Both symbolic and numerical substitutions are allowed: + +.. code-block:: python + + >>> expression = "h / sqrt(p2) ; p2 = 2 * m0 * e1 * x * brackets;" + >>> expression += "brackets = 1 + (e1 * x) / (2 * m0 * c * c) ;" + >>> expression += "m0 = 9.1e-31 ; c = 3e8; e1 = 1.6e-19 ; h = 6.6e-34" + >>> wavelength = hs.model.components1D.Expression( + ... expression=expression, + ... name="Electron wavelength with voltage") + +:class:`~._components.expression.Expression` uses `Sympy +`_ internally to turn the string into +a function. By default it "translates" the expression using +numpy, but often it is possible to boost performance by using +`numexpr `_ instead. + +It can also create 2D components with optional rotation. In the following +example we create a 2D Gaussian that rotates around its center: + +.. code-block:: python + + >>> g = hs.model.components2D.Expression( + ... "k * exp(-((x-x0)**2 / (2 * sx ** 2) + (y-y0)**2 / (2 * sy ** 2)))", + ... "Gaussian2d", add_rotation=True, position=("x0", "y0"), + ... module="numpy", ) + +Define new components from a Python function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Of course :class:`~._components.expression.Expression` is only useful for +analytical functions. You can define more general components modifying the +following template to suit your needs: + + +.. code-block:: python + + from hyperspy.component import Component + + class MyComponent(Component): + + """ + """ + + def __init__(self, parameter_1=1, parameter_2=2): + # Define the parameters + Component.__init__(self, ('parameter_1', 'parameter_2')) + + # Optionally we can set the initial values + self.parameter_1.value = parameter_1 + self.parameter_2.value = parameter_2 + + # The units (optional) + self.parameter_1.units = 'Tesla' + self.parameter_2.units = 'Kociak' + + # Once defined we can give default values to the attribute + # For example we fix the attribure_1 (optional) + self.parameter_1.attribute_1.free = False + + # And we set the boundaries (optional) + self.parameter_1.bmin = 0. + self.parameter_1.bmax = None + + # Optionally, to boost the optimization speed we can also define + # the gradients of the function we the syntax: + # self.parameter.grad = function + self.parameter_1.grad = self.grad_parameter_1 + self.parameter_2.grad = self.grad_parameter_2 + + # Define the function as a function of the already defined parameters, + # x being the independent variable value + def function(self, x): + p1 = self.parameter_1.value + p2 = self.parameter_2.value + return p1 + x * p2 + + # Optionally define the gradients of each parameter + def grad_parameter_1(self, x): + """ + Returns d(function)/d(parameter_1) + """ + return 0 + + def grad_parameter_2(self, x): + """ + Returns d(function)/d(parameter_2) + """ + return x + +Define components from a fixed-pattern +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`~.api.model.components1D.ScalableFixedPattern` +component enables fitting a pattern (in the form of a +:class:`~.api.signals.Signal1D` instance) to data by shifting +(:attr:`~.api.model.components1D.ScalableFixedPattern.shift`) +and +scaling it in the x and y directions using the +:attr:`~.api.model.components1D.ScalableFixedPattern.xscale` +and +:attr:`~.api.model.components1D.ScalableFixedPattern.yscale` +parameters respectively. diff --git a/doc/user_guide/model/model_parameters.rst b/doc/user_guide/model/model_parameters.rst new file mode 100644 index 0000000000..9384380093 --- /dev/null +++ b/doc/user_guide/model/model_parameters.rst @@ -0,0 +1,252 @@ +.. _model-intro: + +Getting and setting parameter values and attributes +--------------------------------------------------- + +Getting parameter values +^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`~.model.BaseModel.print_current_values()` prints the properties of the +parameters of the components in the current coordinates. In the Jupyter Notebook, +the default view is HTML-formatted, which allows for formatted copying +into other software, such as Excel. One can also filter for only active +components and only showing component with free parameters with the arguments +``only_active`` and ``only_free``, respectively. + +.. _Component.print_current_values: + +The current values of a particular component can be printed using the +:attr:`~.component.Component.print_current_values()` method. + +.. code-block:: python + + >>> s = exspy.data.EDS_SEM_TM002() # doctest: +SKIP + >>> m = s.create_model() # doctest: +SKIP + >>> m.fit() # doctest: +SKIP + >>> G = m[1] # doctest: +SKIP + >>> G.print_current_values() # doctest: +SKIP + Gaussian: Al_Ka + Active: True + Parameter Name | Free | Value | Std | Min + ============== | ===== | ========== | ========== | ========== + A | True | 62894.6824 | 1039.40944 | 0.0 + sigma | False | 0.03253440 | None | None + centre | False | 1.4865 | None | None + +The current coordinates can be either set by navigating the +:meth:`~.models.model1d.Model1D.plot`, or specified by pixel indices in +``m.axes_manager.indices`` or as calibrated coordinates in +``m.axes_manager.coordinates``. + +:attr:`~.component.Component.parameters` contains a list of the parameters +of a component and :attr:`~.component.Component.free_parameters` lists only +the free parameters. + +The value of a particular parameter in the current coordinates can be +accessed by :attr:`component.Parameter.value` (e.g. ``Gaussian.A.value``). +To access an array of the value of the parameter across all navigation +pixels, :attr:`component.Parameter.map['values']` (e.g. +``Gaussian.A.map["values"]``) can be used. On its own, +:attr:`component.Parameter.map` returns a NumPy array with three elements: +``'values'``, ``'std'`` and ``'is_set'``. The first two give the value and +standard error for each index. The last element shows whether the value has +been set in a given index, either by a fitting procedure or manually. + +If a model contains several components with the same parameters, it is possible +to change them all by using :meth:`~.model.BaseModel.set_parameters_value`: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(100).reshape(10,10)) + >>> m = s.create_model() + >>> g1 = hs.model.components1D.Gaussian() + >>> g2 = hs.model.components1D.Gaussian() + >>> m.extend([g1,g2]) + >>> m.set_parameters_value('A', 20) + >>> g1.A.map['values'] + array([20., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) + >>> g2.A.map['values'] + array([20., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) + >>> m.set_parameters_value('A', 40, only_current=True) + >>> g1.A.map['values'] + array([40., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) + >>> m.set_parameters_value('A',30, component_list=[g2]) + >>> g2.A.map['values'] + array([30., 30., 30., 30., 30., 30., 30., 30., 30., 30.]) + >>> g1.A.map['values'] + array([40., 20., 20., 20., 20., 20., 20., 20., 20., 20.]) + + +Setting Parameters free / not free +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To set the ``free`` state of a parameter change the +:attr:`~.component.Parameter.free` attribute. To change the ``free`` state +of all parameters in a component to `True` use +:meth:`~.component.Component.set_parameters_free`, and +:meth:`~.component.Component.set_parameters_not_free` for setting them to +``False``. Specific parameter-names can also be specified by using +``parameter_name_list``, shown in the example: + +.. code-block:: python + + >>> g = hs.model.components1D.Gaussian() + >>> g.free_parameters + (, , ) + >>> g.set_parameters_not_free() + >>> g.set_parameters_free(parameter_name_list=['A','centre']) + >>> g.free_parameters + (, ) + +Similar functions exist for :class:`~.model.BaseModel`: +:meth:`~.model.BaseModel.set_parameters_free` and +:meth:`~.model.BaseModel.set_parameters_not_free`. Which sets the +``free`` states for the parameters in components in a model. Specific +components and parameter-names can also be specified. For example: + +.. code-block:: python + + >>> g1 = hs.model.components1D.Gaussian() + >>> g2 = hs.model.components1D.Gaussian() + >>> m.extend([g1,g2]) + >>> m.set_parameters_not_free() + >>> g1.free_parameters + () + >>> g2.free_parameters + () + >>> m.set_parameters_free(parameter_name_list=['A']) + >>> g1.free_parameters + (,) + >>> g2.free_parameters + (,) + >>> m.set_parameters_free([g1], parameter_name_list=['sigma']) + >>> g1.free_parameters + (, ) + >>> g2.free_parameters + (,) + + +Setting twin parameters +^^^^^^^^^^^^^^^^^^^^^^^ + +The value of a parameter can be coupled to the value of another by setting the +:attr:`~.component.Parameter.twin` attribute: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(100)) + >>> m = s.create_model() + + >>> gaussian = hs.model.components1D.Gaussian() + >>> gaussian2 = hs.model.components1D.Gaussian() # Create another gaussian + >>> gaussian3 = hs.model.components1D.Gaussian() # Create a third gaussian + >>> gaussian.name = 'Carbon' + >>> gaussian2.name = 'Long Hydrogen name' + >>> gaussian3.name = 'Nitrogen' + >>> m.extend((gaussian, gaussian2, gaussian3)) + + >>> gaussian.parameters # Print the parameters of the Gaussian components + (, , ) + >>> gaussian.centre.free = False # Fix the centre + >>> gaussian.free_parameters # Print the free parameters + (, ) + >>> m.print_current_values(only_free=True) # Print the values of all free parameters. + Model1D: + CurrentComponentValues: Carbon + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | True | 1.0 | None | 0.0 | None | True + sigma | True | 1.0 | None | 0.0 | None | False + + CurrentComponentValues: Long Hydrogen name + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | True | 1.0 | None | 0.0 | None | True + centre | True | 0.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False + + CurrentComponentValues: Nitrogen + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | True | 1.0 | None | 0.0 | None | True + centre | True | 0.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False + + >>> # Couple the A parameter of gaussian2 to the A parameter of gaussian 3: + >>> gaussian2.A.twin = gaussian3.A + >>> gaussian2.A.value = 10 # Set the gaussian2 A value to 10 + >>> gaussian3.print_current_values() + CurrentComponentValues: Nitrogen + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | True | 10.0 | None | 0.0 | None | True + centre | True | 0.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False + >>> gaussian3.A.value = 5 # Set the gaussian1 centre value to 5 + >>> gaussian2.print_current_values() + CurrentComponentValues: Long Hydrogen name + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | Twinned | 5.0 | None | 0.0 | None | True + centre | True | 0.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False + + +.. deprecated:: 1.2.0 + Setting the ``twin_function`` and ``twin_inverse_function`` attributes, + set the :attr:`~.component.Parameter.twin_function_expr` and + :attr:`~.component.Parameter.twin_inverse_function_expr` attributes + instead. + +.. versionadded:: 1.2.0 + :attr:`~.component.Parameter.twin_function_expr` and + :attr:`~.component.Parameter.twin_inverse_function_expr`. + +By default the coupling function is the identity function. However it is +possible to set a different coupling function by setting the +:attr:`~.component.Parameter.twin_function_expr` and +:attr:`~.component.Parameter.twin_inverse_function_expr` attributes. For +example: + +.. code-block:: python + + >>> gaussian2.A.twin_function_expr = "x**2" + >>> gaussian2.A.twin_inverse_function_expr = "sqrt(abs(x))" + >>> gaussian2.A.value = 4 + >>> gaussian3.print_current_values() + CurrentComponentValues: Nitrogen + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | True | 2.0 | None | 0.0 | None | True + centre | True | 0.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False + +.. code-block:: python + + >>> gaussian3.A.value = 4 + >>> gaussian2.print_current_values() + CurrentComponentValues: Long Hydrogen name + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | Twinned | 16.0 | None | 0.0 | None | True + centre | True | 0.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False + + +Batch setting of parameter attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following model methods can be used to ease the task of setting some important +parameter attributes. These can also be used on a per-component basis, by calling them +on individual components. + +* :meth:`~.model.BaseModel.set_parameters_not_free` +* :meth:`~.model.BaseModel.set_parameters_free` +* :meth:`~.model.BaseModel.set_parameters_value` diff --git a/doc/user_guide/model/storing_models.rst b/doc/user_guide/model/storing_models.rst new file mode 100644 index 0000000000..892b7b951e --- /dev/null +++ b/doc/user_guide/model/storing_models.rst @@ -0,0 +1,124 @@ +.. _storing_models-label: + +Storing models +-------------- + +Multiple models can be stored in the same signal. In particular, when +:meth:`~.model.BaseModel.store` is called, a full "frozen" copy of the model +is stored in stored in the signal's :class:`~.signal.ModelManager`, +which can be accessed in the ``models`` attribute (i.e. ``s.models``) +The stored models can be recreated at any time by calling +:meth:`~.signal.ModelManager.restore` with the stored +model name as an argument. To remove a model from storage, simply call +:meth:`~.signal.ModelManager.remove`. + +The stored models can be either given a name, or assigned one automatically. +The automatic naming follows alphabetical scheme, with the sequence being (a, +b, ..., z, aa, ab, ..., az, ba, ...). + +.. NOTE:: + + If you want to slice a model, you have to perform the operation on the + model itself, not its stored version + +.. WARNING:: + + Modifying a signal in-place (e.g. :meth:`~.api.signals.BaseSignal.map`, + :meth:`~.api.signals.BaseSignal.crop`, + :meth:`~.api.signals.Signal1D.align1D`, + :meth:`~.api.signals.Signal2D.align2D` and similar) + will invalidate all stored models. This is done intentionally. + +Current stored models can be listed by calling ``s.models``: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(100)) + >>> m = s.create_model() + >>> m.append(hs.model.components1D.Lorentzian()) + >>> m.store('myname') + >>> s.models # doctest: +SKIP + └── myname + ├── components + │ └── Lorentzian + ├── date = 2015-09-07 12:01:50 + └── dimensions = (|100) + + >>> m.append(hs.model.components1D.Exponential()) + >>> m.store() # assign model name automatically + >>> s.models # doctest: +SKIP + ├── a + │ ├── components + │ │ ├── Exponential + │ │ └── Lorentzian + │ ├── date = 2015-09-07 12:01:57 + │ └── dimensions = (|100) + └── myname + ├── components + │ └── Lorentzian + ├── date = 2015-09-07 12:01:50 + └── dimensions = (|100) + >>> m1 = s.models.restore('myname') + >>> m1.components + # | Attribute Name | Component Name | Component Type + ---- | ------------------- | ------------------- | ------------------- + 0 | Lorentzian | Lorentzian | Lorentzian + + +Saving and loading the result of the fit +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To save a model, a convenience function :meth:`~.model.BaseModel.save` is +provided, which stores the current model into its signal and saves the +signal. As described in :ref:`storing_models-label`, more than just one +model can be saved with one signal. + +.. code-block:: python + + >>> m = s.create_model() + >>> # analysis and fitting goes here + >>> m.save('my_filename', 'model_name') # doctest: +SKIP + >>> l = hs.load('my_filename.hspy') # doctest: +SKIP + >>> m = l.models.restore('model_name') # doctest: +SKIP + + Alternatively + + >>> m = l.models.model_name.restore() # doctest: +SKIP + +For older versions of HyperSpy (before 0.9), the instructions were as follows: + + Note that this method is known to be brittle i.e. there is no + guarantee that a version of HyperSpy different from the one used to save + the model will be able to load it successfully. Also, it is + advisable not to use this method in combination with functions that + alter the value of the parameters interactively (e.g. + `enable_adjust_position`) as the modifications made by this functions + are normally not stored in the IPython notebook or Python script. + + To save a model: + + 1. Save the parameter arrays to a file using + :meth:`~.model.BaseModel.save_parameters2file`. + + 2. Save all the commands that used to create the model to a file. This + can be done in the form of an IPython notebook or a Python script. + + 3. (Optional) Comment out or delete the fitting commands (e.g. + :meth:`~.model.BaseModel.multifit`). + + To recreate the model: + + 1. Execute the IPython notebook or Python script. + + 2. Use :meth:`~.model.BaseModel.load_parameters_from_file` to load + back the parameter values and arrays. + + +Exporting the result of the fit +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`~.model.BaseModel` :meth:`~.model.BaseModel.export_results`, +:class:`~.component.Component` :meth:`~.component.Component.export` and +:class:`~.component.Parameter` :meth:`~.component.Parameter.export` +methods can be used to export the result of the optimization in all supported +formats. diff --git a/doc/user_guide/mva.rst b/doc/user_guide/mva.rst deleted file mode 100644 index 48bb6ad3c1..0000000000 --- a/doc/user_guide/mva.rst +++ /dev/null @@ -1,774 +0,0 @@ - -.. _ml-label: - -Machine learning -**************** - -Introduction -============ - -HyperSpy provides easy access to several "machine learning" algorithms that -can be useful when analysing multi-dimensional data. In particular, -decomposition algorithms, such as principal component analysis (PCA), or -blind source separation (BSS) algorithms, such as independent component -analysis (ICA), are available through the methods described in this section. - -.. hint:: - - HyperSpy will decompose a dataset, :math:`X`, into two new datasets: - one with the dimension of the signal space known as **factors** (:math:`A`), - and the other with the dimension of the navigation space known as **loadings** - (:math:`B`), such that :math:`X = A B^T`. - - For some of the algorithms listed below, the decomposition results in - an `approximation` of the dataset, i.e. :math:`X \approx A B^T`. - -.. _mva.decomposition: - -Decomposition -============= - -Decomposition techniques are most commonly used as a means of noise -reduction (or `denoising`) and dimensionality reduction. To apply a -decomposition to your dataset, run the :py:meth:`~.learn.mva.MVA.decomposition` -method, for example: - -.. code-block:: python - - >>> import numpy as np - >>> from hyperspy.signals import Signal1D - - >>> s = Signal1D(np.random.randn(10, 10, 200)) - >>> s.decomposition() - - >>> # Load data from a file, then decompose - >>> s = hs.load("my_file.hspy") - >>> s.decomposition() - -.. note:: - The signal ``s`` must be multi-dimensional, *i.e.* - ``s.axes_manager.navigation_size > 1`` - -One of the most popular uses of :py:meth:`~.learn.mva.MVA.decomposition` -is data denoising. This is achieved by using a limited set of components -to make a model of the original dataset, omitting the less significant components that -ideally contain only noise. - -To reconstruct your denoised or reduced model, run the -:py:meth:`~.learn.mva.MVA.get_decomposition_model` method. For example: - -.. code-block:: python - - >>> # Use all components to reconstruct the model - >>> sc = s.get_decomposition_model() - - >>> # Use first 3 components to reconstruct the model - >>> sc = s.get_decomposition_model(3) - - >>> # Use components [0, 2] to reconstruct the model - >>> sc = s.get_decomposition_model([0, 2]) - -Sometimes, it is useful to examine the residuals between your original data and -the decomposition model. You can easily calculate and display the residuals, -since :py:meth:`~.learn.mva.MVA.get_decomposition_model` returns a new -object, which in the example above we have called ``sc``: - -.. code-block:: python - - >>> (s - sc).plot() - -You can perform operations on this new object ``sc`` later. -It is a copy of the original ``s`` object, except that the data has -been replaced by the model constructed using the chosen components. - -If you provide the ``output_dimension`` argument, which takes an integer value, -the decomposition algorithm attempts to find the best approximation for the -dataset :math:`X` with only a limited set of factors :math:`A` and loadings :math:`B`, -such that :math:`X \approx A B^T`. - -.. code-block:: python - - >>> s.decomposition(output_dimension=3) - -Some of the algorithms described below require ``output_dimension`` to be provided. - -Available algorithms --------------------- - -HyperSpy implements a number of decomposition algorithms via the ``algorithm`` argument. -The table below lists the algorithms that are currently available, and includes -links to the appropriate documentation for more information on each one. - -.. note:: - - Choosing which algorithm to use is likely to depend heavily on the nature of your - dataset and the type of analysis you are trying to perform. We discuss some of the - reasons for choosing one algorithm over another below, but would encourage you to - do your own research as well. The `scikit-learn documentation - `_ is a - very good starting point. - -.. _decomposition-table: - -.. table:: Available decomposition algorithms in HyperSpy - - +--------------------------+----------------------------------------------------------------+ - | Algorithm | Method | - +==========================+================================================================+ - | "SVD" (default) | :py:func:`~.learn.svd_pca.svd_pca` | - +--------------------------+----------------------------------------------------------------+ - | "MLPCA" | :py:func:`~.learn.mlpca.mlpca` | - +--------------------------+----------------------------------------------------------------+ - | "sklearn_pca" | :py:class:`sklearn.decomposition.PCA` | - +--------------------------+----------------------------------------------------------------+ - | "NMF" | :py:class:`sklearn.decomposition.NMF` | - +--------------------------+----------------------------------------------------------------+ - | "sparse_pca" | :py:class:`sklearn.decomposition.SparsePCA` | - +--------------------------+----------------------------------------------------------------+ - | "mini_batch_sparse_pca" | :py:class:`sklearn.decomposition.MiniBatchSparsePCA` | - +--------------------------+----------------------------------------------------------------+ - | "RPCA" | :py:func:`~.learn.rpca.rpca_godec` | - +--------------------------+----------------------------------------------------------------+ - | "ORPCA" | :py:class:`~.learn.rpca.ORPCA` | - +--------------------------+----------------------------------------------------------------+ - | "ORNMF" | :py:class:`~.learn.ornmf.ORNMF` | - +--------------------------+----------------------------------------------------------------+ - | custom object | An object implementing ``fit()`` and ``transform()`` methods | - +--------------------------+----------------------------------------------------------------+ - -.. _mva.svd: - -Singular value decomposition (SVD) ----------------------------------- - -The default algorithm in HyperSpy is ``"SVD"``, which uses an approach called -"singular value decomposition" to decompose the data in the form -:math:`X = U \Sigma V^T`. The factors are given by :math:`U \Sigma`, and the -loadings are given by :math:`V^T`. For more information, please read the method -documentation for :py:func:`~.learn.svd_pca.svd_pca`. - -.. code-block:: python - - >>> import numpy as np - >>> from hyperspy.signals import Signal1D - - >>> s = Signal1D(np.random.randn(10, 10, 200)) - >>> s.decomposition() - -.. note:: - In some fields, including electron microscopy, this approach of applying an SVD - directly to the data :math:`X` is often called PCA :ref:`(see below) `. - - However, in the classical definition of PCA, the SVD should be applied to data that has - first been "centered" by subtracting the mean, i.e. :math:`\mathrm{SVD}(X - \bar X)`. - - The ``"SVD"`` algorithm in HyperSpy **does not** apply this - centering step by default. As a result, you may observe differences between - the output of the ``"SVD"`` algorithm and, for example, - :py:class:`sklearn.decomposition.PCA`, which **does** apply centering. - -.. _mva.pca: - -Principal component analysis (PCA) ----------------------------------- - -One of the most popular decomposition methods is `principal component analysis -`_ (PCA). -To perform PCA on your dataset, run the :py:meth:`~.learn.mva.MVA.decomposition` -method with any of following arguments. - -If you have `scikit-learn `_ installed: - -.. code-block:: python - - >>> s.decomposition(algorithm="sklearn_pca") - -You can also turn on centering with the default ``"SVD"`` algorithm via -the ``"centre"`` argument: - -.. code-block:: python - - # Subtract the mean along the navigation axis - >>> s.decomposition(algorithm="SVD", centre="navigation") - - # Subtract the mean along the signal axis - >>> s.decomposition(algorithm="SVD", centre="signal") - -You can also use :py:class:`sklearn.decomposition.PCA` directly: - -.. code-block:: python - - >>> from sklearn.decomposition import PCA - - >>> s.decomposition(algorithm=PCA()) - -.. _poissonian-noise-label: - -Poissonian noise ----------------- - -Most of the standard decomposition algorithms assume that the noise of the data -follows a Gaussian distribution (also known as "homoskedastic noise"). -In cases where your data is instead corrupted by Poisson noise, HyperSpy -can "normalize" the data by performing a scaling operation, which can greatly -enhance the result. More details about the normalization procedure can be -found in :ref:`[Keenan2004] `. - -To apply Poissonian noise normalization to your data: - -.. code-block:: python - - >>> s.decomposition(normalize_poissonian_noise=True) - - >>> # Because it is the first argument we could have simply written: - >>> s.decomposition(True) - -.. warning:: - Poisson noise normalization cannot be used in combination with data - centering using the ``'centre'`` argument. Attempting to do so will - raise an error. - -.. _mva.mlpca: - -Maximum likelihood principal component analysis (MLPCA) -------------------------------------------------------- - -Instead of applying Poisson noise normalization to your data, you can instead -use an approach known as Maximum Likelihood PCA (MLPCA), which provides a more -robust statistical treatment of non-Gaussian "heteroskedastic noise". - -.. code-block:: python - - >>> s.decomposition(algorithm="MLPCA") - -For more information, please read the method documentation for :py:func:`~.learn.mlpca.mlpca`. - -.. note:: - - You must set the ``output_dimension`` when using MLPCA. - -.. _mva.rpca: - -Robust principal component analysis (RPCA) ------------------------------------------- - -PCA is known to be very sensitive to the presence of outliers in data. These -outliers can be the result of missing or dead pixels, X-ray spikes, or very -low count data. If one assumes a dataset, :math:`X`, to consist of a low-rank -component :math:`L` corrupted by a sparse error component :math:`S`, such that -:math:`X=L+S`, then Robust PCA (RPCA) can be used to recover the low-rank -component for subsequent processing :ref:`[Candes2011] `. - -.. figure:: images/rpca_schematic.png - :align: center - :width: 425 - - Schematic diagram of the robust PCA problem, which combines a low-rank matrix - with sparse errors. Robust PCA aims to decompose the matrix back into these two - components. - -.. note:: - - You must set the ``output_dimension`` when using Robust PCA. - -The default RPCA algorithm is GoDec :ref:`[Zhou2011] `. In HyperSpy -it returns the factors and loadings of :math:`L`. RPCA solvers work by using -regularization, in a similar manner to lasso or ridge regression, to enforce -the low-rank constraint on the data. The low-rank regularization parameter, -``lambda1``, defaults to ``1/sqrt(n_features)``, but it is strongly recommended -that you explore the behaviour of different values. - -.. code-block:: python - - >>> s.decomposition(algorithm="RPCA", output_dimension=3, lambda1=0.1) - -HyperSpy also implements an *online* algorithm for RPCA developed by Feng et -al. :ref:`[Feng2013] `. This minimizes memory usage, making it -suitable for large datasets, and can often be faster than the default -algorithm. - -.. code-block:: python - - >>> s.decomposition(algorithm="ORPCA", output_dimension=3) - -The online RPCA implementation sets several default parameters that are -usually suitable for most datasets, including the regularization parameter -highlighted above. Again, it is strongly recommended that you explore the -behaviour of these parameters. To further improve the convergence, you can -"train" the algorithm with the first few samples of your dataset. For example, -the following code will train ORPCA using the first 32 samples of the data. - -.. code-block:: python - - >>> s.decomposition(algorithm="ORPCA", output_dimension=3, training_samples=32) - -Finally, online RPCA includes two alternatives methods to the default -block-coordinate descent solver, which can again improve both the convergence -and speed of the algorithm. These are particularly useful for very large datasets. - -The methods are based on stochastic gradient descent (SGD), and take an -additional parameter to set the learning rate. The learning rate dictates -the size of the steps taken by the gradient descent algorithm, and setting -it too large can lead to oscillations that prevent the algorithm from -finding the correct minima. Usually a value between 1 and 2 works well: - -.. code-block:: python - - >>> s.decomposition(algorithm="ORPCA", - ... output_dimension=3, - ... method="SGD", - ... subspace_learning_rate=1.1) - -You can also use Momentum Stochastic Gradient Descent (MomentumSGD), -which typically improves the convergence properties of stochastic gradient -descent. This takes the further parameter "momentum", which should be a -fraction between 0 and 1. - -.. code-block:: python - - >>> s.decomposition(algorithm="ORPCA", - ... output_dimension=3, - ... method="MomentumSGD", - ... subspace_learning_rate=1.1, - ... subspace_momentum=0.5) - -Using the "SGD" or "MomentumSGD" methods enables the subspace, -i.e. the underlying low-rank component, to be tracked as it changes -with each sample update. The default method instead assumes a fixed, -static subspace. - -.. _mva.nmf: - -Non-negative matrix factorization (NMF) ---------------------------------------- - -Another popular decomposition method is non-negative matrix factorization -(NMF), which can be accessed in HyperSpy with: - -.. code-block:: python - - >>> s.decomposition(algorithm='NMF') - -Unlike PCA, NMF forces the components to be strictly non-negative, which can -aid the physical interpretation of components for count data such as images, -EELS or EDS. For an example of NMF in EELS processing, see -:ref:`[Nicoletti2013] <[Nicoletti2013]>`. - -NMF takes the optional argument ``output_dimension``, which determines the number -of components to keep. Setting this to a small number is recommended to keep -the computation time small. Often it is useful to run a PCA decomposition first -and use the :ref:`scree plot ` to determine a suitable value -for ``output_dimension``. - -.. _mva.rnmf: - -Robust non-negative matrix factorization (RNMF) ------------------------------------------------ - -In a similar manner to the online, robust methods that complement PCA -:ref:`above `, HyperSpy includes an online robust NMF method. -This is based on the OPGD (Online Proximal Gradient Descent) algorithm -of :ref:`[Zhao2016] `. - -.. note:: - - You must set the ``output_dimension`` when using Robust NMF. - -As before, you can control the regularization applied via the parameter "lambda1": - -.. code-block:: python - - >>> s.decomposition(algorithm="ORNMF", output_dimension=3, lambda1=0.1) - -The MomentumSGD method is useful for scenarios where the subspace, i.e. the -underlying low-rank component, is changing over time. - -.. code-block:: python - - >>> s.decomposition(algorithm="ORNMF", - ... output_dimension=3, - ... method="MomentumSGD", - ... subspace_learning_rate=1.1, - ... subspace_momentum=0.5) - -Both the default and MomentumSGD solvers assume an *l2*-norm minimization problem, -which can still be sensitive to *very* heavily corrupted data. A more robust -alternative is available, although it is typically much slower. - -.. code-block:: python - - >>> s.decomposition(algorithm="ORNMF", output_dimension=3, method="RobustPGD") - -.. _mva.custom_decomposition: - -Custom decomposition algorithms -------------------------------- - -HyperSpy supports passing a custom decomposition algorithm, provided it follows the form of a -`scikit-learn estimator `_. -Any object that implements ``fit()`` and ``transform()`` methods is acceptable, including -:py:class:`sklearn.pipeline.Pipeline` and :py:class:`sklearn.model_selection.GridSearchCV`. -You can access the fitted estimator by passing ``return_info=True``. - -.. code-block:: python - - >>> # Passing a custom decomposition algorithm - >>> from sklearn.preprocessing import MinMaxScaler - >>> from sklearn.pipeline import Pipeline - >>> from sklearn.decomposition import PCA - - >>> pipe = Pipeline([("scaler", MinMaxScaler()), ("PCA", PCA())]) - >>> out = s.decomposition(algorithm=pipe, return_info=True) - - >>> out - Pipeline(memory=None, - steps=[('scaler', MinMaxScaler(copy=True, feature_range=(0, 1))), - ('PCA', PCA(copy=True, iterated_power='auto', n_components=None, - random_state=None, svd_solver='auto', tol=0.0, - whiten=False))], - verbose=False) - -.. _mva.blind_source_separation: - -Blind Source Separation -======================= - -In some cases it is possible to obtain more physically interpretable set of -components using a process called Blind Source Separation (BSS). This largely -depends on the particular application. For more information about blind source -separation please see :ref:`[Hyvarinen2000] `, and for an -example application to EELS analysis, see :ref:`[Pena2010] `. - -.. warning:: - - The BSS algorithms operate on the result of a previous - decomposition analysis. It is therefore necessary to perform a - :ref:`decomposition ` first before calling - :py:meth:`~.learn.mva.MVA.blind_source_separation`, otherwise it - will raise an error. - - You must provide an integer ``number_of_components`` argument, - or a list of components as the ``comp_list`` argument. This performs - BSS on the chosen number/list of components from the previous - decomposition. - -To perform blind source separation on the result of a previous decomposition, -run the :py:meth:`~.learn.mva.MVA.blind_source_separation` method, for example: - -.. code-block:: python - - >>> import numpy as np - >>> from hyperspy.signals import Signal1D - - >>> s = Signal1D(np.random.randn(10, 10, 200)) - >>> s.decomposition(output_dimension=3) - - >>> s.blind_source_separation(number_of_components=3) - - # Perform only on the first and third components - >>> s.blind_source_separation(comp_list=[0, 2]) - -Available algorithms --------------------- - -HyperSpy implements a number of BSS algorithms via the ``algorithm`` argument. -The table below lists the algorithms that are currently available, and includes -links to the appropriate documentation for more information on each one. - -.. _bss-table: - -.. table:: Available blind source separation algorithms in HyperSpy - - +-----------------------------+----------------------------------------------------------------+ - | Algorithm | Method | - +=============================+================================================================+ - | "sklearn_fastica" (default) | :py:class:`sklearn.decomposition.FastICA` | - +-----------------------------+----------------------------------------------------------------+ - | "orthomax" | :py:func:`~.learn.orthomax.orthomax` | - +-----------------------------+----------------------------------------------------------------+ - | "FastICA" | :py:class:`mdp.nodes.FastICANode` | - +-----------------------------+----------------------------------------------------------------+ - | "JADE" | :py:class:`mdp.nodes.JADENode` | - +-----------------------------+----------------------------------------------------------------+ - | "CuBICA" | :py:class:`mdp.nodes.CuBICANode` | - +-----------------------------+----------------------------------------------------------------+ - | "TDSEP" | :py:class:`mdp.nodes.TDSEPNode` | - +-----------------------------+----------------------------------------------------------------+ - | custom object | An object implementing ``fit()`` and ``transform()`` methods | - +-----------------------------+----------------------------------------------------------------+ - -.. note:: - - Except :py:func:`~.learn.orthomax.orthomax`, all of the implemented BSS algorithms listed above - rely on external packages being available on your system. ``sklearn_fastica``, requires - `scikit-learn `_ while ``FastICA, JADE, CuBICA, TDSEP`` - require the `Modular toolkit for Data Processing (MDP) `_. - -.. _mva.orthomax: - -Orthomax --------- - -Orthomax rotations are a statistical technique used to clarify and highlight the relationship among factors, -by adjusting the coordinates of PCA results. The most common approach is known as -`"varimax" `_, which intended to maximize the variance shared -among the components while preserving orthogonality. The results of an orthomax rotation following PCA are -often "simpler" to interpret than just PCA, since each componenthas a more discrete contribution to the data. - -.. code-block:: python - - >>> import numpy as np - >>> from hyperspy.signals import Signal1D - - >>> s = Signal1D(np.random.randn(10, 10, 200)) - >>> s.decomposition(output_dimension=3) - - >>> s.blind_source_separation(number_of_components=3, algorithm="orthomax") - -.. _mva.ica: - -Independent component analysis (ICA) ------------------------------------- - -One of the most common approaches for blind source separation is -`Independent Component Analysis (ICA) `_. -This separates a signal into subcomponents by assuming that the subcomponents are (a) non-Gaussian, -and (b) that they are statistically independent from each other. - -.. _mva.custom_bss: - -Custom BSS algorithms ---------------------- - -As with :ref:`decomposition `, HyperSpy supports passing a custom BSS algorithm, -provided it follows the form of a `scikit-learn estimator `_. -Any object that implements ``fit()`` and ``transform()`` methods is acceptable, including -:py:class:`sklearn.pipeline.Pipeline` and :py:class:`sklearn.model_selection.GridSearchCV`. -You can access the fitted estimator by passing ``return_info=True``. - -.. code-block:: python - - >>> # Passing a custom BSS algorithm - >>> from sklearn.preprocessing import MinMaxScaler - >>> from sklearn.pipeline import Pipeline - >>> from sklearn.decomposition import FastICA - - >>> pipe = Pipeline([("scaler", MinMaxScaler()), ("ica", FastICA())]) - >>> out = s.blind_source_separation(number_of_components=3, algorithm=pipe, return_info=True) - - >>> out - Pipeline(memory=None, - steps=[('scaler', MinMaxScaler(copy=True, feature_range=(0, 1))), - ('ica', FastICA(algorithm='parallel', fun='logcosh', fun_args=None, - max_iter=200, n_components=3, random_state=None, - tol=0.0001, w_init=None, whiten=True))], - verbose=False) - -.. _cluster_analysis-label: - -.. include:: cluster.rst - -.. _mva.visualization: - - -Visualizing results -=================== - -HyperSpy includes a number of plotting methods for visualizing the results -of decomposition and blind source separation analyses. All the methods -begin with ``plot_``. - -.. _mva.scree_plot: - -Scree plots ------------ - -.. note:: - Scree plots are only available for the ``"SVD"`` and ``"PCA"`` algorithms. - -PCA will sort the components in the dataset in order of decreasing -variance. It is often useful to estimate the dimensionality of the data by -plotting the explained variance against the component index. This plot is -sometimes called a scree plot. For most datasets, the values in a scree plot -will decay rapidly, eventually becoming a slowly descending line. - -To obtain a scree plot for your dataset, run the -:py:meth:`~.learn.mva.MVA.plot_explained_variance_ratio` method: - -.. code-block:: python - - >>> s.plot_explained_variance_ratio(n=20) - -.. figure:: images/screeplot.png - :align: center - :width: 500 - - PCA scree plot - -The point at which the scree plot becomes linear (often referred to as -the "elbow") is generally judged to be a good estimation of the dimensionality -of the data (or equivalently, the number of components that should be retained -- see below). Components to the left of the elbow are considered part of the "signal", -while components to the right are considered to be "noise", and thus do not explain -any significant features of the data. - -By specifying a ``threshold`` value, a cutoff line will be drawn at the total variance -specified, and the components above this value will be styled distinctly from the -remaining components to show which are considered signal, as opposed to noise. -Alternatively, by providing an integer value for ``threshold``, the line will -be drawn at the specified component (see below). - -Note that in the above scree plot, the first component has index 0. This is because -Python uses zero-based indexing. To switch to a "number-based" (rather than -"index-based") notation, specify the ``xaxis_type`` parameter: - -.. code-block:: python - - >>> s.plot_explained_variance_ratio(n=20, threshold=4, xaxis_type='number') - -.. figure:: images/screeplot2.png - :align: center - :width: 500 - - PCA scree plot with number-based axis labeling and a threshold value - specified - -The number of significant components can be estimated and a vertical line -drawn to represent this by specifying ``vline=True``. In this case, the "elbow" -is found in the variance plot by estimating the distance from each point in the -variance plot to a line joining the first and last points of the plot, and then -selecting the point where this distance is largest. - -If multiple maxima are found, the index corresponding to the first occurrence -is returned. As the index of the first component is zero, the number of -significant PCA components is the elbow index position + 1. More details -about the elbow-finding technique can be found in -:ref:`[Satopää2011] `, and in the documentation for -:py:meth:`~.learn.mva.MVA.estimate_elbow_position`. - -.. figure:: images/screeplot_elbow_method.png - :align: center - :width: 500 - -.. figure:: images/screeplot3.png - :align: center - :width: 500 - - PCA scree plot with number-based axis labeling and an estimate of the no of significant - positions based on the "elbow" position - -These options (together with many others), can be customized to -develop a figure of your liking. See the documentation of -:py:meth:`~.learn.mva.MVA.plot_explained_variance_ratio` for more details. - -Sometimes it can be useful to get the explained variance ratio as a spectrum. -For example, to plot several scree plots obtained with -different data pre-treatments in the same figure, you can combine -:py:func:`~.drawing.utils.plot_spectra` with -:py:meth:`~.learn.mva.MVA.get_explained_variance_ratio`. - -.. _mva.plot_decomposition: - -Decomposition plots -------------------- - -HyperSpy provides a number of methods for visualizing the factors and loadings -found by a decomposition analysis. To plot everything in a compact form, -use :py:meth:`~.signal.MVATools.plot_decomposition_results`. - -You can also plot the factors and loadings separately using the following -methods. It is recommended that you provide the number of factors or loadings -you wish to visualise, since the default is to plot all of them. - -* :py:meth:`~.signal.MVATools.plot_decomposition_factors` -* :py:meth:`~.signal.MVATools.plot_decomposition_loadings` - -.. _mva.plot_bss: - -Blind source separation plots ------------------------------ - -Visualizing blind source separation results is much the same as decomposition. -You can use :py:meth:`~.signal.MVATools.plot_bss_results` for a compact display, -or instead: - -* :py:meth:`~.signal.MVATools.plot_bss_factors` -* :py:meth:`~.signal.MVATools.plot_bss_loadings` - -.. _mva.get_results: - -Clustering plots ----------------- - -Visualizing cluster results is much the same as decomposition. -You can use :py:meth:`~.signal.MVATools.plot_bss_results` for a compact display, -or instead: - -* :py:meth:`~.signal.MVATools.plot_cluster_results`. -* :py:meth:`~.signal.MVATools.plot_cluster_signals`. -* :py:meth:`~.signal.MVATools.plot_cluster_labels`. - - -Obtaining the results as BaseSignal instances -============================================= - -The decomposition and BSS results are internally stored as numpy arrays in the -:py:class:`~.signal.BaseSignal` class. Frequently it is useful to obtain the -decomposition/BSS factors and loadings as HyperSpy signals, and HyperSpy -provides the following methods for that purpose: - -* :py:meth:`~.signal.MVATools.get_decomposition_loadings` -* :py:meth:`~.signal.MVATools.get_decomposition_factors` -* :py:meth:`~.signal.MVATools.get_bss_loadings` -* :py:meth:`~.signal.MVATools.get_bss_factors` - -.. _mva.saving-label: - -Saving and loading results -========================== - -Saving in the main file ------------------------ - -If you save the dataset on which you've performed machine learning analysis in -the :ref:`hspy-format` format (the default in HyperSpy) (see -:ref:`saving_files`), the result of the analysis is also saved in the same -file automatically, and it is loaded along with the rest of the data when you -next open the file. - -.. note:: - This approach currently supports storing one decomposition and one BSS - result, which may not be enough for your purposes. - -Saving to an external file --------------------------- - -Alternatively, you can save the results of the current machine learning -analysis to a separate file with the -:py:meth:`~.learn.mva.LearningResults.save` method: - -.. code-block:: python - - >>> # Save the result of the analysis - >>> s.learning_results.save('my_results.npz') - - >>> # Load back the results - >>> s.learning_results.load('my_results.npz') - -Exporting in different formats ------------------------------- - -You can also export the results of a machine learning analysis to any format -supported by HyperSpy with the following methods: - -* :py:meth:`~.signal.MVATools.export_decomposition_results` -* :py:meth:`~.signal.MVATools.export_bss_results` - -These methods accept many arguments to customise the way in which the -data is exported, so please consult the method documentation. The options -include the choice of file format, the prefixes for loadings and factors, -saving figures instead of data and more. - -.. warning:: - Data exported in this way cannot be easily loaded into HyperSpy's - machine learning structure. diff --git a/doc/user_guide/mva/bss.rst b/doc/user_guide/mva/bss.rst new file mode 100644 index 0000000000..0c67d30aa2 --- /dev/null +++ b/doc/user_guide/mva/bss.rst @@ -0,0 +1,153 @@ +.. _mva.blind_source_separation: + +Blind Source Separation +======================= + +In some cases it is possible to obtain more physically interpretable set of +components using a process called Blind Source Separation (BSS). This largely +depends on the particular application. For more information about blind source +separation please see :ref:`[Hyvarinen2000] `, and for an +example application to EELS analysis, see :ref:`[Pena2010] `. + +.. warning:: + + The BSS algorithms operate on the result of a previous + decomposition analysis. It is therefore necessary to perform a + :ref:`decomposition ` first before calling + :meth:`~.api.signals.BaseSignal.blind_source_separation`, otherwise it + will raise an error. + + You must provide an integer ``number_of_components`` argument, + or a list of components as the ``comp_list`` argument. This performs + BSS on the chosen number/list of components from the previous + decomposition. + +To perform blind source separation on the result of a previous decomposition, +run the :meth:`~.api.signals.BaseSignal.blind_source_separation` method, for example: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.randn(10, 10, 200)) + >>> s.decomposition(output_dimension=3) + Decomposition info: + normalize_poissonian_noise=False + algorithm=SVD + output_dimension=3 + centre=None + + >>> s.blind_source_separation(number_of_components=3) + Blind source separation info: + number_of_components=3 + algorithm=sklearn_fastica + diff_order=1 + reverse_component_criterion=factors + whiten_method=PCA + scikit-learn estimator: + FastICA(tol=1e-10, whiten=False) + + # Perform only on the first and third components + >>> s.blind_source_separation(comp_list=[0, 2]) + Blind source separation info: + number_of_components=2 + algorithm=sklearn_fastica + diff_order=1 + reverse_component_criterion=factors + whiten_method=PCA + scikit-learn estimator: + FastICA(tol=1e-10, whiten=False) + + +Available algorithms +-------------------- + +HyperSpy implements a number of BSS algorithms via the ``algorithm`` argument. +The table below lists the algorithms that are currently available, and includes +links to the appropriate documentation for more information on each one. + +.. _bss-table: + +.. table:: Available blind source separation algorithms in HyperSpy + + +-----------------------------+----------------------------------------------------------------+ + | Algorithm | Method | + +=============================+================================================================+ + | "sklearn_fastica" (default) | :class:`sklearn.decomposition.FastICA` | + +-----------------------------+----------------------------------------------------------------+ + | "orthomax" | :func:`~.learn.orthomax.orthomax` | + +-----------------------------+----------------------------------------------------------------+ + | "FastICA" | :class:`mdp.nodes.FastICANode` | + +-----------------------------+----------------------------------------------------------------+ + | "JADE" | :class:`mdp.nodes.JADENode` | + +-----------------------------+----------------------------------------------------------------+ + | "CuBICA" | :class:`mdp.nodes.CuBICANode` | + +-----------------------------+----------------------------------------------------------------+ + | "TDSEP" | :class:`mdp.nodes.TDSEPNode` | + +-----------------------------+----------------------------------------------------------------+ + | custom object | An object implementing ``fit()`` and ``transform()`` methods | + +-----------------------------+----------------------------------------------------------------+ + +.. note:: + + Except :func:`~.learn.orthomax.orthomax`, all of the implemented BSS algorithms listed above + rely on external packages being available on your system. ``sklearn_fastica``, requires + `scikit-learn `_ while ``FastICA, JADE, CuBICA, TDSEP`` + require the `Modular toolkit for Data Processing (MDP) `_. + +.. _mva.orthomax: + +Orthomax +-------- + +Orthomax rotations are a statistical technique used to clarify and highlight the relationship among factors, +by adjusting the coordinates of PCA results. The most common approach is known as +`"varimax" `_, which intended to maximize the variance shared +among the components while preserving orthogonality. The results of an orthomax rotation following PCA are +often "simpler" to interpret than just PCA, since each componenthas a more discrete contribution to the data. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.randn(10, 10, 200)) + >>> s.decomposition(output_dimension=3, print_info=False) + + >>> s.blind_source_separation(number_of_components=3, algorithm="orthomax") + Blind source separation info: + number_of_components=3 + algorithm=orthomax + diff_order=1 + reverse_component_criterion=factors + whiten_method=PCA + + +.. _mva.ica: + +Independent component analysis (ICA) +------------------------------------ + +One of the most common approaches for blind source separation is +`Independent Component Analysis (ICA) `_. +This separates a signal into subcomponents by assuming that the subcomponents are (a) non-Gaussian, +and (b) that they are statistically independent from each other. + +.. _mva.custom_bss: + +Custom BSS algorithms +--------------------- + +As with :ref:`decomposition `, HyperSpy supports passing a custom BSS algorithm, +provided it follows the form of a :external+sklearn:ref:`scikit-learn estimator `. +Any object that implements ``fit()`` and ``transform()`` methods is acceptable, including +:class:`sklearn.pipeline.Pipeline` and :class:`sklearn.model_selection.GridSearchCV`. +You can access the fitted estimator by passing ``return_info=True``. + +.. code-block:: python + + >>> # Passing a custom BSS algorithm + >>> from sklearn.preprocessing import MinMaxScaler + >>> from sklearn.pipeline import Pipeline + >>> from sklearn.decomposition import FastICA + + >>> pipe = Pipeline([("scaler", MinMaxScaler()), ("ica", FastICA())]) + >>> out = s.blind_source_separation(number_of_components=3, algorithm=pipe, return_info=True, print_info=False) + + >>> out + Pipeline(steps=[('scaler', MinMaxScaler()), ('ica', FastICA())]) diff --git a/doc/user_guide/mva/clustering.rst b/doc/user_guide/mva/clustering.rst new file mode 100644 index 0000000000..a5c107e8d7 --- /dev/null +++ b/doc/user_guide/mva/clustering.rst @@ -0,0 +1,449 @@ +.. _cluster_analysis-label: + +Cluster analysis +================ + +.. versionadded:: 1.6 + +Introduction +------------ + +`Cluster analysis `__ or clustering +is the task of grouping a set of measurements such that measurements in the same +group (called a cluster) are more similar (in some sense) to each other than to +those in other groups (clusters). +A HyperSpy signal can represent a number of large arrays of different measurements +which can represent spectra, images or sets of paramaters. +Identifying and extracting trends from large datasets is often difficult and +decomposition methods, blind source separation and cluster analysis play an important role in this process. + +Cluster analysis, in essence, compares the "distances" (or similar metric) +between different sets of measurements and groups those that are closest together. +The features it groups can be raw data points, for example, comparing for +every navigation dimension all points of a spectrum. However, if the +dataset is large, the process of clustering can be computationally intensive so +clustering is more commonly used on an extracted set of features or parameters. +For example, extraction of two peak positions of interest via a fitting process +rather than clustering all spectra points. + +In favourable cases, matrix decomposition and related methods can decompose the +data into a (ideally small) set of significant loadings and factors. +The factors capture a core representation of the features in the data and the loadings +provide the mixing ratios of these factors that best describe the original data. +Overall, this usually represents a much smaller data volume compared to the original data +and can helps to identify correlations. + +A detailed description of the application of cluster analysis in x-ray +spectro-microscopy and further details on the theory and implementation can +be found in :ref:`[Lerotic2004] `. + +Nomenclature +------------ + +Taking the example of a 1D Signal of dimensions ``(20, 10|4)`` containing the +dataset, we say there are 200 *samples*. The four measured parameters are the +*features*. If we choose to search for 3 clusters within this dataset, we +derive three main values: + +1. The `labels`, of dimensions ``(3| 20, 10)``. Each navigation position is + assigned to a cluster. The `labels` of each cluster are boolean arrays + that mark the data that has been assigned to the cluster with `True`. +2. The `cluster_distances`, of dimensions ``(3| 20, 10)``, which are the + distances of all the data points to the centroid of each cluster. +3. The "*cluster signals*", which are signals that are representative of + their clusters. In HyperSpy two are computer: + `cluster_sum_signals` and `cluster_centroid_signals`, + of dimensions ``(3| 4)``, which are the sum of all the cluster signals + that belong to each cluster or the signal closest to each cluster + centroid respectively. + + +Clustering functions HyperSpy +----------------------------- + +All HyperSpy signals have the following methods for clustering analysis: + +* :meth:`~.api.signals.BaseSignal.cluster_analysis` +* :meth:`~.api.signals.BaseSignal.plot_cluster_results` +* :meth:`~.api.signals.BaseSignal.plot_cluster_labels` +* :meth:`~.api.signals.BaseSignal.plot_cluster_signals` +* :meth:`~.api.signals.BaseSignal.plot_cluster_distances` +* :meth:`~.api.signals.BaseSignal.get_cluster_signals` +* :meth:`~.api.signals.BaseSignal.get_cluster_labels` +* :meth:`~.api.signals.BaseSignal.get_cluster_distances` +* :meth:`~.api.signals.BaseSignal.estimate_number_of_clusters` +* :meth:`~.api.signals.BaseSignal.plot_cluster_metric` + +The :meth:`~.api.signals.BaseSignal.cluster_analysis` method can perform cluster +analysis using any :external+sklearn:ref:`scikit-learn clustering ` +algorithms or any other object with a compatible API. This involves importing +the relevant algorithm class from scikit-learn. + +.. code-block:: python + + >>> from sklearn.cluster import KMeans # doctest: +SKIP + >>> s.cluster_analysis( + ... cluster_source="signal", algorithm=KMeans(n_clusters=3, n_init=8) + ... ) # doctest: +SKIP + + +For convenience, the default algorithm is the ``kmeans`` algorithm and is imported +internally. All extra keyword arguments are passed to the algorithm when +present. Therefore the following code is equivalent to the previous one: + +For example: + +.. code-block:: python + + >>> s.cluster_analysis( + ... cluster_source="signal", n_clusters=3, preprocessing="norm", algorithm="kmeans", n_init=8 + ... ) # doctest: +SKIP + +is equivalent to: + +:meth:`~.api.signals.BaseSignal.cluster_analysis` computes the cluster labels. The +clusters areas with identical label are averaged to create a set of cluster +centres. This averaging can be performed on the ``signal`` itself, the +:ref:`BSS ` or :ref:`decomposition ` results +or a user supplied signal. + +Pre-processing +-------------- + +Cluster analysis measures the distances between features and groups them. It +is often necessary to pre-process the features in order to obtain meaningful +results. + +For example, pre-processing can be useful to reveal clusters when +performing cluster analysis of decomposition results. Decomposition methods +decompose data into a set of factors and a set of loadings defining the +mixing needed to represent the data. If signal 1 is reduced to three +components with mixing 0.1 0.5 2.0, and signal 2 is reduced to a mixing of 0.2 +1.0 4.0, it should be clear that these represent the same signal but with a +scaling difference. Normalization of the data can again be used to remove +scaling effects. + +Therefore, the pre-processing step +will highly influence the results and should be evaluated for the problem +under investigation. + +All pre-processing methods from (or compatible with) the +:external+sklearn:ref:`scikit-learn pre-processing ` module can be passed +to the ``scaling`` keyword of the :meth:`~.api.signals.BaseSignal.cluster_analysis` +method. For convenience, the following methods from scikit-learn are +available as standard: ``standard`` , ``minmax`` and ``norm`` as +standard. Briefly, ``norm`` treats the features as a vector and normalizes the +vector length. ``standard`` re-scales each feature by removing the mean and +scaling to unit variance. ``minmax`` normalizes each feature between the +minimum and maximum range of that feature. + +Cluster signals +^^^^^^^^^^^^^^^ + +In HyperSpy *cluster signals* are signals that somehow represent their clusters. +The concept is ill-defined, since cluster algorithms only assign data points to +clusters. HyperSpy computes 2 cluster signals, + +1. ``cluster_sum_signals``, which are the sum of all the cluster signals + that belong to each cluster. +2. ``cluster_centroid_signals``, which is the signal closest to each cluster + centroid. + + +When plotting the "*cluster signals*" we can select any of those +above using the ``signal`` keyword argument: + +.. code-block:: python + + >>> s.plot_cluster_labels(signal="centroid") # doctest: +SKIP + +In addition, it is possible to plot the mean signal over the different +clusters: + +.. code-block:: python + + >>> s.plot_cluster_labels(signal="mean") # doctest: +SKIP + + +Clustering with user defined algorithms +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +User developed preprocessing or cluster algorithms can be +used in place of the sklearn methods. +A preprocessing object needs a ``fit_transform`` which +appropriately scales the data. +The example below defines a preprocessing class which normalizes +the data then applies a square root to enhances weaker features. + +.. code-block:: python + + >>> class PowerScaling(object): + ... + ... def __init__(self,power=0.5): + ... self.power = power + ... + ... def fit_transform(self,data): + ... norm = np.amax(data,axis=1) + ... scaled_data = data/norm[:,None] + ... scaled_data = scaled_data - np.min(scaled_data)+1.0e-8 + ... scaled_data = scaled_data ** self.power + ... return scaled_data + +The PowerScaling class can then be passed to the cluster_analysis method for use. + +.. code-block:: python + + >>> ps = PowerScaling() # doctest: +SKIP + >>> s.cluster_analysis( + ... cluster_source="decomposition", number_of_components=3, preprocessing=ps + ... ) # doctest: +SKIP + +For user defined clustering algorithms the class must implementation +``fit`` and have a ``label_`` attribute that contains the clustering labels. +An example template would be: + +.. code-block:: python + + + >>> class MyClustering(object): + ... + ... def __init__(self): + ... self.labels_ = None + ... + ... def fit_(self,X): + ... self.labels_ = do_something(X) + + + +Examples +-------- + +Clustering using decomposition results +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Let's use the :func:`sklearn.datasets.make_blobs` +function supplied by `scikit-learn` to make dummy data to see how clustering +might work in practice. + +.. code-block:: python + + >>> from sklearn.datasets import make_blobs + >>> data = make_blobs( + ... n_samples=1000, + ... n_features=100, + ... centers=3, + ... shuffle=False, + ... random_state=1)[0].reshape(50, 20, 100) + >>> s = hs.signals.Signal1D(data) + +.. code-block:: python + + >>> hs.plot.plot_images(s.T.inav[:9], axes_decor="off") # doctest: +SKIP + + +.. image:: ../images/clustering_data.png + + +To see how cluster analysis works it's best to first examine the signal. +Moving around the image you should be able to see 3 distinct regions in which +the 1D signal modulates slightly. + +.. code-block:: python + + >>> s.plot() + + +Let's perform SVD to reduce the dimensionality of the dataset by exploiting +redundancies: + + +.. code-block:: python + + >>> s.decomposition() + Decomposition info: + normalize_poissonian_noise=False + algorithm=SVD + output_dimension=None + centre=None + >>> s.plot_explained_variance_ratio() + + +.. image:: ../images/clustering_scree_plot.png + +From the scree plot we deduce that, as expected, that the dataset can be reduce +to 3 components. Let's plot their loadings: + +.. code-block:: python + + >>> s.plot_decomposition_loadings(comp_ids=3, axes_decor="off") # doctest: +SKIP + +.. image:: ../images/clustering_decomposition_loadings.png + +In the SVD loading we can identify 3 regions, but they are mixed in the components. +Let's perform cluster analysis of decomposition results, to find similar regions +and the representative features in those regions. Notice that this dataset does +not require any pre-processing for cluster analysis. + +.. code-block:: python + + >>> s.cluster_analysis(cluster_source="decomposition", number_of_components=3, preprocessing=None) + >>> s.plot_cluster_labels(axes_decor="off") # doctest: +SKIP + +.. image:: ../images/clustering_labels.png + +To see what the labels the cluster algorithm has assigned you can inspect +the ``cluster_labels``: + +.. code-block:: python + + >>> s.learning_results.cluster_labels[0] # doctest: +SKIP + array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + + +In this case we know there are 3 cluster, but for real examples the number of +clusters is not known *a priori*. A number of metrics, such as elbow, +Silhouette and Gap can be used to estimate the optimal number of clusters. +The elbow method measures the sum-of-squares of the distances within a +cluster and, as for the PCA decomposition, an "elbow" or point where the gains +diminish with increasing number of clusters indicates the ideal number of +clusters. Silhouette analysis measures how well separated clusters are and +can be used to determine the most likely number of clusters. As the scoring +is a measure of separation of clusters a number of solutions may occur and +maxima in the scores are used to indicate possible solutions. Gap analysis +is similar but compares the “gap” between the clustered data results and +those from a randomly data set of the same size. The largest gap indicates +the best clustering. The metric results can be plotted to check how +well-defined the clustering is. + +.. code-block:: python + + >>> s.estimate_number_of_clusters(cluster_source="decomposition", metric="gap") + 3 + >>> s.plot_cluster_metric() + + +.. image:: ../images/clustering_Gap.png + +The optimal number of clusters can be set or accessed from the learning +results + +.. code-block:: python + + >>> s.learning_results.number_of_clusters # doctest: +SKIP + 3 + + +Clustering using another signal as source +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In this example we will perform clustering analysis on the position of two +peaks. The signals containing the position of the peaks can be computed for +example using :ref:`curve fitting `. Given an existing fitted +model, the parameters can be extracted as signals and stacked. Clustering can +then be applied as described previously to identify trends in the fitted +results. + +Let's start by creating a suitable synthetic dataset. + +.. code-block:: python + + >>> import hyperspy.api as hs + >>> import numpy as np + >>> s_dummy = hs.signals.Signal1D(np.zeros((64, 64, 1000))) + >>> s_dummy.axes_manager.signal_axes[0].scale = 2e-3 + >>> s_dummy.axes_manager.signal_axes[0].units = "eV" + >>> s_dummy.axes_manager.signal_axes[0].name = "energy" + >>> m = s_dummy.create_model() + >>> m.append(hs.model.components1D.GaussianHF(fwhm=0.2)) + >>> m.append(hs.model.components1D.GaussianHF(fwhm=0.3)) + >>> m.components.GaussianHF.centre.map["values"][:32, :] = .3 + .1 + >>> m.components.GaussianHF.centre.map["values"][32:, :] = .7 + .1 + >>> m.components.GaussianHF_0.centre.map["values"][:, 32:] = m.components.GaussianHF.centre.map["values"][:, 32:] * 2 + >>> m.components.GaussianHF_0.centre.map["values"][:, :32] = m.components.GaussianHF.centre.map["values"][:, :32] * 0.5 + >>> for component in m: + ... component.centre.map["is_set"][:] = True + ... component.centre.map["values"][:] += np.random.normal(size=(64, 64)) * 0.01 + >>> s = m.as_signal() + >>> stack = [component.centre.as_signal() for component in m] + >>> hs.plot.plot_images(stack, axes_decor="off", colorbar="single", suptitle="") # doctest: +SKIP + +.. image:: ../images/clustering_gaussian_centres.png + +Let's now perform cluster analysis on the stack and calculate the centres using +the spectrum image. Notice that we don't need to fit the model to the data +because this is a synthetic dataset. When analysing experimental data you will +need to fit the model first. Also notice that here we need to pre-process the +dataset by normalization in order to reveal the clusters due to the +proportionality relationship between the position of the peaks. + +.. code-block:: python + + >>> stack = hs.stack([component.centre.as_signal() for component in m]) + >>> s.estimate_number_of_clusters(cluster_source=stack.T, preprocessing="norm") + 2 + >>> s.cluster_analysis(cluster_source=stack.T, source_for_centers=s, n_clusters=2, preprocessing="norm") + >>> s.plot_cluster_labels() # doctest: +SKIP + +.. image:: ../images/clustering_gaussian_centres_labels.png + +.. code-block:: python + + >>> s.plot_cluster_signals(signal="mean") # doctest: +SKIP + +.. image:: ../images/clustering_gaussian_centres_mean.png + + +Notice that in this case averaging or summing the signals of +each cluster is not appropriate, since the clustering criterium +is the ratio between the peaks positions. A better alternative +is to plot the signals closest to the centroids: + +.. code-block:: python + + >>> s.plot_cluster_signals(signal="centroid") # doctest: +SKIP + +.. image:: ../images/clustering_gaussian_centres_centroid.png diff --git a/doc/user_guide/mva/decomposition.rst b/doc/user_guide/mva/decomposition.rst new file mode 100644 index 0000000000..f4f882831f --- /dev/null +++ b/doc/user_guide/mva/decomposition.rst @@ -0,0 +1,439 @@ +.. _mva.decomposition: + +Decomposition +============= + +Decomposition techniques are most commonly used as a means of noise +reduction (or `denoising`) and dimensionality reduction. To apply a +decomposition to your dataset, run the :meth:`~.api.signals.BaseSignal.decomposition` +method, for example: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.randn(10, 10, 200)) + >>> s.decomposition() + Decomposition info: + normalize_poissonian_noise=False + algorithm=SVD + output_dimension=None + centre=None + + >>> # Load data from a file, then decompose + >>> s = hs.load("my_file.hspy") # doctest: +SKIP + >>> s.decomposition() # doctest: +SKIP + +.. note:: + The signal ``s`` must be multi-dimensional, *i.e.* + ``s.axes_manager.navigation_size > 1`` + +One of the most popular uses of :meth:`~.api.signals.BaseSignal.decomposition` +is data denoising. This is achieved by using a limited set of components +to make a model of the original dataset, omitting the less significant components that +ideally contain only noise. + +To reconstruct your denoised or reduced model, run the +:meth:`~.api.signals.BaseSignal.get_decomposition_model` method. For example: + +.. code-block:: python + + >>> # Use all components to reconstruct the model + >>> sc = s.get_decomposition_model() # doctest: +SKIP + + >>> # Use first 3 components to reconstruct the model + >>> sc = s.get_decomposition_model(3) # doctest: +SKIP + + >>> # Use components [0, 2] to reconstruct the model + >>> sc = s.get_decomposition_model([0, 2]) # doctest: +SKIP + +Sometimes, it is useful to examine the residuals between your original data and +the decomposition model. You can easily calculate and display the residuals, +since :meth:`~.api.signals.BaseSignal.get_decomposition_model` returns a new +object, which in the example above we have called ``sc``: + +.. code-block:: python + + >>> (s - sc).plot() # doctest: +SKIP + +You can perform operations on this new object ``sc`` later. +It is a copy of the original ``s`` object, except that the data has +been replaced by the model constructed using the chosen components. + +If you provide the ``output_dimension`` argument, which takes an integer value, +the decomposition algorithm attempts to find the best approximation for the +dataset :math:`X` with only a limited set of factors :math:`A` and loadings :math:`B`, +such that :math:`X \approx A B^T`. + +.. code-block:: python + + >>> s.decomposition(output_dimension=3) # doctest: +SKIP + +Some of the algorithms described below require ``output_dimension`` to be provided. + +Available algorithms +-------------------- + +HyperSpy implements a number of decomposition algorithms via the ``algorithm`` argument. +The table below lists the algorithms that are currently available, and includes +links to the appropriate documentation for more information on each one. + +.. note:: + + Choosing which algorithm to use is likely to depend heavily on the nature of your + dataset and the type of analysis you are trying to perform. We discuss some of the + reasons for choosing one algorithm over another below, but would encourage you to + do your own research as well. The `scikit-learn documentation + `_ is a + very good starting point. + +.. _decomposition-table: + +.. table:: Available decomposition algorithms in HyperSpy + + +--------------------------+----------------------------------------------------------------+ + | Algorithm | Method | + +==========================+================================================================+ + | "SVD" (default) | :func:`~.learn.svd_pca.svd_pca` | + +--------------------------+----------------------------------------------------------------+ + | "MLPCA" | :func:`~.learn.mlpca.mlpca` | + +--------------------------+----------------------------------------------------------------+ + | "sklearn_pca" | :class:`sklearn.decomposition.PCA` | + +--------------------------+----------------------------------------------------------------+ + | "NMF" | :class:`sklearn.decomposition.NMF` | + +--------------------------+----------------------------------------------------------------+ + | "sparse_pca" | :class:`sklearn.decomposition.SparsePCA` | + +--------------------------+----------------------------------------------------------------+ + | "mini_batch_sparse_pca" | :class:`sklearn.decomposition.MiniBatchSparsePCA` | + +--------------------------+----------------------------------------------------------------+ + | "RPCA" | :func:`~.learn.rpca.rpca_godec` | + +--------------------------+----------------------------------------------------------------+ + | "ORPCA" | :class:`~.learn.rpca.ORPCA` | + +--------------------------+----------------------------------------------------------------+ + | "ORNMF" | :class:`~.learn.ornmf.ORNMF` | + +--------------------------+----------------------------------------------------------------+ + | custom object | An object implementing ``fit()`` and ``transform()`` methods | + +--------------------------+----------------------------------------------------------------+ + +.. _mva.svd: + +Singular value decomposition (SVD) +---------------------------------- + +The default algorithm in HyperSpy is ``"SVD"``, which uses an approach called +"singular value decomposition" to decompose the data in the form +:math:`X = U \Sigma V^T`. The factors are given by :math:`U \Sigma`, and the +loadings are given by :math:`V^T`. For more information, please read the method +documentation for :func:`~.learn.svd_pca.svd_pca`. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.randn(10, 10, 200)) + >>> s.decomposition() + Decomposition info: + normalize_poissonian_noise=False + algorithm=SVD + output_dimension=None + centre=None + +.. note:: + In some fields, including electron microscopy, this approach of applying an SVD + directly to the data :math:`X` is often called PCA :ref:`(see below) `. + + However, in the classical definition of PCA, the SVD should be applied to data that has + first been "centered" by subtracting the mean, i.e. :math:`\mathrm{SVD}(X - \bar X)`. + + The ``"SVD"`` algorithm in HyperSpy **does not** apply this + centering step by default. As a result, you may observe differences between + the output of the ``"SVD"`` algorithm and, for example, + :class:`sklearn.decomposition.PCA`, which **does** apply centering. + +.. _mva.pca: + +Principal component analysis (PCA) +---------------------------------- + +One of the most popular decomposition methods is `principal component analysis +`_ (PCA). +To perform PCA on your dataset, run the :meth:`~.api.signals.BaseSignal.decomposition` +method with any of following arguments. + +If you have `scikit-learn `_ installed: + +.. code-block:: python + + >>> s.decomposition(algorithm="sklearn_pca") + Decomposition info: + normalize_poissonian_noise=False + algorithm=sklearn_pca + output_dimension=None + centre=None + scikit-learn estimator: + PCA() + +You can also turn on centering with the default ``"SVD"`` algorithm via +the ``"centre"`` argument: + +.. code-block:: python + + # Subtract the mean along the navigation axis + >>> s.decomposition(algorithm="SVD", centre="navigation") + Decomposition info: + normalize_poissonian_noise=False + algorithm=SVD + output_dimension=None + centre=navigation + + # Subtract the mean along the signal axis + >>> s.decomposition(algorithm="SVD", centre="signal") + Decomposition info: + normalize_poissonian_noise=False + algorithm=SVD + output_dimension=None + centre=signal + +You can also use :class:`sklearn.decomposition.PCA` directly: + +.. code-block:: python + + >>> from sklearn.decomposition import PCA + >>> s.decomposition(algorithm=PCA()) + Decomposition info: + normalize_poissonian_noise=False + algorithm=PCA() + output_dimension=None + centre=None + scikit-learn estimator: + PCA() + + +.. _poissonian-noise-label: + +Poissonian noise +---------------- + +Most of the standard decomposition algorithms assume that the noise of the data +follows a Gaussian distribution (also known as "homoskedastic noise"). +In cases where your data is instead corrupted by Poisson noise, HyperSpy +can "normalize" the data by performing a scaling operation, which can greatly +enhance the result. More details about the normalization procedure can be +found in :ref:`[Keenan2004] `. + +To apply Poissonian noise normalization to your data: + +.. code-block:: python + + >>> s.decomposition(normalize_poissonian_noise=True) # doctest: +SKIP + + >>> # Because it is the first argument we could have simply written: + >>> s.decomposition(True) # doctest: +SKIP + +.. warning:: + Poisson noise normalization cannot be used in combination with data + centering using the ``'centre'`` argument. Attempting to do so will + raise an error. + +.. _mva.mlpca: + +Maximum likelihood principal component analysis (MLPCA) +------------------------------------------------------- + +Instead of applying Poisson noise normalization to your data, you can instead +use an approach known as Maximum Likelihood PCA (MLPCA), which provides a more +robust statistical treatment of non-Gaussian "heteroskedastic noise". + +.. code-block:: python + + >>> s.decomposition(algorithm="MLPCA") # doctest: +SKIP + +For more information, please read the method documentation for :func:`~.learn.mlpca.mlpca`. + +.. note:: + + You must set the ``output_dimension`` when using MLPCA. + +.. _mva.rpca: + +Robust principal component analysis (RPCA) +------------------------------------------ + +PCA is known to be very sensitive to the presence of outliers in data. These +outliers can be the result of missing or dead pixels, X-ray spikes, or very +low count data. If one assumes a dataset, :math:`X`, to consist of a low-rank +component :math:`L` corrupted by a sparse error component :math:`S`, such that +:math:`X=L+S`, then Robust PCA (RPCA) can be used to recover the low-rank +component for subsequent processing :ref:`[Candes2011] `. + +.. figure:: ../images/rpca_schematic.png + :align: center + :width: 425 + + Schematic diagram of the robust PCA problem, which combines a low-rank matrix + with sparse errors. Robust PCA aims to decompose the matrix back into these two + components. + +.. note:: + + You must set the ``output_dimension`` when using Robust PCA. + +The default RPCA algorithm is GoDec :ref:`[Zhou2011] `. In HyperSpy +it returns the factors and loadings of :math:`L`. RPCA solvers work by using +regularization, in a similar manner to lasso or ridge regression, to enforce +the low-rank constraint on the data. The low-rank regularization parameter, +``lambda1``, defaults to ``1/sqrt(n_features)``, but it is strongly recommended +that you explore the behaviour of different values. + +.. code-block:: python + + >>> s.decomposition(algorithm="RPCA", output_dimension=3, lambda1=0.1) + Decomposition info: + normalize_poissonian_noise=False + algorithm=RPCA + output_dimension=3 + centre=None + +HyperSpy also implements an *online* algorithm for RPCA developed by Feng et +al. :ref:`[Feng2013] `. This minimizes memory usage, making it +suitable for large datasets, and can often be faster than the default +algorithm. + +.. code-block:: python + + >>> s.decomposition(algorithm="ORPCA", output_dimension=3) # doctest: +SKIP + +The online RPCA implementation sets several default parameters that are +usually suitable for most datasets, including the regularization parameter +highlighted above. Again, it is strongly recommended that you explore the +behaviour of these parameters. To further improve the convergence, you can +"train" the algorithm with the first few samples of your dataset. For example, +the following code will train ORPCA using the first 32 samples of the data. + +.. code-block:: python + + >>> s.decomposition(algorithm="ORPCA", output_dimension=3, training_samples=32) # doctest: +SKIP + +Finally, online RPCA includes two alternatives methods to the default +block-coordinate descent solver, which can again improve both the convergence +and speed of the algorithm. These are particularly useful for very large datasets. + +The methods are based on stochastic gradient descent (SGD), and take an +additional parameter to set the learning rate. The learning rate dictates +the size of the steps taken by the gradient descent algorithm, and setting +it too large can lead to oscillations that prevent the algorithm from +finding the correct minima. Usually a value between 1 and 2 works well: + +.. code-block:: python + + >>> s.decomposition(algorithm="ORPCA", + ... output_dimension=3, + ... method="SGD", + ... subspace_learning_rate=1.1) # doctest: +SKIP + +You can also use Momentum Stochastic Gradient Descent (MomentumSGD), +which typically improves the convergence properties of stochastic gradient +descent. This takes the further parameter ``subspace_momentum``, which should +be a fraction between 0 and 1. + +.. code-block:: python + + >>> s.decomposition(algorithm="ORPCA", + ... output_dimension=3, + ... method="MomentumSGD", + ... subspace_learning_rate=1.1, + ... subspace_momentum=0.5) # doctest: +SKIP + +Using the ``"SGD"`` or ``"MomentumSGD"`` methods enables the subspace, +i.e. the underlying low-rank component, to be tracked as it changes +with each sample update. The default method instead assumes a fixed, +static subspace. + +.. _mva.nmf: + +Non-negative matrix factorization (NMF) +--------------------------------------- + +Another popular decomposition method is non-negative matrix factorization +(NMF), which can be accessed in HyperSpy with: + +.. code-block:: python + + >>> s.decomposition(algorithm="NMF") # doctest: +SKIP + +Unlike PCA, NMF forces the components to be strictly non-negative, which can +aid the physical interpretation of components for count data such as images, +EELS or EDS. For an example of NMF in EELS processing, see +:ref:`[Nicoletti2013] <[Nicoletti2013]>`. + +NMF takes the optional argument ``output_dimension``, which determines the number +of components to keep. Setting this to a small number is recommended to keep +the computation time small. Often it is useful to run a PCA decomposition first +and use the :ref:`scree plot ` to determine a suitable value +for ``output_dimension``. + +.. _mva.rnmf: + +Robust non-negative matrix factorization (RNMF) +----------------------------------------------- + +In a similar manner to the online, robust methods that complement PCA +:ref:`above `, HyperSpy includes an online robust NMF method. +This is based on the OPGD (Online Proximal Gradient Descent) algorithm +of :ref:`[Zhao2016] `. + +.. note:: + + You must set the ``output_dimension`` when using Robust NMF. + +As before, you can control the regularization applied via the parameter "lambda1": + +.. code-block:: python + + >>> s.decomposition(algorithm="ORNMF", output_dimension=3, lambda1=0.1) # doctest: +SKIP + +The MomentumSGD method is useful for scenarios where the subspace, i.e. the +underlying low-rank component, is changing over time. + +.. code-block:: python + + >>> s.decomposition(algorithm="ORNMF", + ... output_dimension=3, + ... method="MomentumSGD", + ... subspace_learning_rate=1.1, + ... subspace_momentum=0.5) # doctest: +SKIP + +Both the default and MomentumSGD solvers assume an *l2*-norm minimization problem, +which can still be sensitive to *very* heavily corrupted data. A more robust +alternative is available, although it is typically much slower. + +.. code-block:: python + + >>> s.decomposition(algorithm="ORNMF", output_dimension=3, method="RobustPGD") # doctest: +SKIP + +.. _mva.custom_decomposition: + +Custom decomposition algorithms +------------------------------- + +HyperSpy supports passing a custom decomposition algorithm, provided it follows the form of a +`scikit-learn estimator `_. +Any object that implements ``fit`` and ``transform`` methods is acceptable, including +:class:`sklearn.pipeline.Pipeline` and :class:`sklearn.model_selection.GridSearchCV`. +You can access the fitted estimator by passing ``return_info=True``. + +.. code-block:: python + + >>> # Passing a custom decomposition algorithm + >>> from sklearn.preprocessing import MinMaxScaler + >>> from sklearn.pipeline import Pipeline + >>> from sklearn.decomposition import PCA + + >>> pipe = Pipeline([("scaler", MinMaxScaler()), ("PCA", PCA())]) + >>> out = s.decomposition(algorithm=pipe, return_info=True) + Decomposition info: + normalize_poissonian_noise=False + algorithm=Pipeline(steps=[('scaler', MinMaxScaler()), ('PCA', PCA())]) + output_dimension=None + centre=None + scikit-learn estimator: + Pipeline(steps=[('scaler', MinMaxScaler()), ('PCA', PCA())]) + + >>> out + Pipeline(steps=[('scaler', MinMaxScaler()), ('PCA', PCA())]) diff --git a/doc/user_guide/mva/export_results.rst b/doc/user_guide/mva/export_results.rst new file mode 100644 index 0000000000..362a3bdbcf --- /dev/null +++ b/doc/user_guide/mva/export_results.rst @@ -0,0 +1,68 @@ +.. _mva.export: + +Export results +============== + +Obtain the results as BaseSignal instances +------------------------------------------ + +The decomposition and BSS results are internally stored as numpy arrays in the +:class:`~.api.signals.BaseSignal` class. Frequently it is useful to obtain the +decomposition/BSS factors and loadings as HyperSpy signals, and HyperSpy +provides the following methods for that purpose: + +* :meth:`~.api.signals.BaseSignal.get_decomposition_loadings` +* :meth:`~.api.signals.BaseSignal.get_decomposition_factors` +* :meth:`~.api.signals.BaseSignal.get_bss_loadings` +* :meth:`~.api.signals.BaseSignal.get_bss_factors` + +.. _mva.saving-label: + +Save and load results +--------------------- + +Save in the main file +~~~~~~~~~~~~~~~~~~~~~ + +If you save the dataset on which you've performed machine learning analysis in +the :external+rsciio:ref:`HSpy-HDF5 ` format (the default in HyperSpy, see +:ref:`saving_files`), the result of the analysis is also saved in the same +file automatically, and it is loaded along with the rest of the data when you +next open the file. + +.. note:: + This approach currently supports storing one decomposition and one BSS + result, which may not be enough for your purposes. + +Save to an external file +~~~~~~~~~~~~~~~~~~~~~~~~ + +Alternatively, you can save the results of the current machine learning +analysis to a separate file with the +:meth:`~.learn.mva.LearningResults.save` method: + +.. code-block:: python + + >>> # Save the result of the analysis + >>> s.learning_results.save('my_results.npz') # doctest: +SKIP + + >>> # Load back the results + >>> s.learning_results.load('my_results.npz') # doctest: +SKIP + +Export in different formats +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also export the results of a machine learning analysis to any format +supported by HyperSpy with the following methods: + +* :meth:`~.api.signals.BaseSignal.export_decomposition_results` +* :meth:`~.api.signals.BaseSignal.export_bss_results` + +These methods accept many arguments to customise the way in which the +data is exported, so please consult the method documentation. The options +include the choice of file format, the prefixes for loadings and factors, +saving figures instead of data and more. + +.. warning:: + Data exported in this way cannot be easily loaded into HyperSpy's + machine learning structure. diff --git a/doc/user_guide/mva/index.rst b/doc/user_guide/mva/index.rst new file mode 100644 index 0000000000..cce0f2ad10 --- /dev/null +++ b/doc/user_guide/mva/index.rst @@ -0,0 +1,30 @@ + +.. _ml-label: + +Machine learning +**************** + +HyperSpy provides easy access to several "machine learning" algorithms that +can be useful when analysing multi-dimensional data. In particular, +decomposition algorithms, such as principal component analysis (PCA), or +blind source separation (BSS) algorithms, such as independent component +analysis (ICA), are available through the methods described in this section. + +.. hint:: + + HyperSpy will decompose a dataset, :math:`X`, into two new datasets: + one with the dimension of the signal space known as **factors** (:math:`A`), + and the other with the dimension of the navigation space known as **loadings** + (:math:`B`), such that :math:`X = A B^T`. + + For some of the algorithms listed below, the decomposition results in + an `approximation` of the dataset, i.e. :math:`X \approx A B^T`. + +.. toctree:: + :maxdepth: 2 + + decomposition.rst + bss.rst + clustering.rst + visualize_results.rst + export_results.rst diff --git a/doc/user_guide/mva/visualize_results.rst b/doc/user_guide/mva/visualize_results.rst new file mode 100644 index 0000000000..a9a0d3c76a --- /dev/null +++ b/doc/user_guide/mva/visualize_results.rst @@ -0,0 +1,138 @@ +.. _mva.visualization: + +Visualizing results +=================== + +HyperSpy includes a number of plotting methods for visualizing the results +of decomposition and blind source separation analyses. All the methods +begin with ``plot_``. + +.. _mva.scree_plot: + +Scree plots +----------- + +.. note:: + Scree plots are only available for the ``"SVD"`` and ``"PCA"`` algorithms. + +PCA will sort the components in the dataset in order of decreasing +variance. It is often useful to estimate the dimensionality of the data by +plotting the explained variance against the component index. This plot is +sometimes called a scree plot. For most datasets, the values in a scree plot +will decay rapidly, eventually becoming a slowly descending line. + +To obtain a scree plot for your dataset, run the +:meth:`~.api.signals.BaseSignal.plot_explained_variance_ratio` method: + +.. code-block:: python + + >>> s.plot_explained_variance_ratio(n=20) # doctest: +SKIP + +.. figure:: ../images/screeplot.png + :align: center + :width: 500 + + PCA scree plot + +The point at which the scree plot becomes linear (often referred to as +the "elbow") is generally judged to be a good estimation of the dimensionality +of the data (or equivalently, the number of components that should be retained +- see below). Components to the left of the elbow are considered part of the "signal", +while components to the right are considered to be "noise", and thus do not explain +any significant features of the data. + +By specifying a ``threshold`` value, a cutoff line will be drawn at the total variance +specified, and the components above this value will be styled distinctly from the +remaining components to show which are considered signal, as opposed to noise. +Alternatively, by providing an integer value for ``threshold``, the line will +be drawn at the specified component (see below). + +Note that in the above scree plot, the first component has index 0. This is because +Python uses zero-based indexing. To switch to a "number-based" (rather than +"index-based") notation, specify the ``xaxis_type`` parameter: + +.. code-block:: python + + >>> s.plot_explained_variance_ratio(n=20, threshold=4, xaxis_type='number') # doctest: +SKIP + +.. figure:: ../images/screeplot2.png + :align: center + :width: 500 + + PCA scree plot with number-based axis labeling and a threshold value + specified + +The number of significant components can be estimated and a vertical line +drawn to represent this by specifying ``vline=True``. In this case, the "elbow" +is found in the variance plot by estimating the distance from each point in the +variance plot to a line joining the first and last points of the plot, and then +selecting the point where this distance is largest. + +If multiple maxima are found, the index corresponding to the first occurrence +is returned. As the index of the first component is zero, the number of +significant PCA components is the elbow index position + 1. More details +about the elbow-finding technique can be found in +:ref:`[Satopää2011] `, and in the documentation for +:meth:`~.api.signals.BaseSignal.estimate_elbow_position`. + +.. figure:: ../images/screeplot_elbow_method.png + :align: center + :width: 500 + +.. figure:: ../images/screeplot3.png + :align: center + :width: 500 + + PCA scree plot with number-based axis labeling and an estimate of the no of significant + positions based on the "elbow" position + +These options (together with many others), can be customized to +develop a figure of your liking. See the documentation of +:meth:`~.api.signals.BaseSignal.plot_explained_variance_ratio` for more details. + +Sometimes it can be useful to get the explained variance ratio as a spectrum. +For example, to plot several scree plots obtained with +different data pre-treatments in the same figure, you can combine +:func:`~.api.plot.plot_spectra` with +:meth:`~.api.signals.BaseSignal.get_explained_variance_ratio`. + +.. _mva.plot_decomposition: + +Decomposition plots +------------------- + +HyperSpy provides a number of methods for visualizing the factors and loadings +found by a decomposition analysis. To plot everything in a compact form, +use :meth:`~.api.signals.BaseSignal.plot_decomposition_results`. + +You can also plot the factors and loadings separately using the following +methods. It is recommended that you provide the number of factors or loadings +you wish to visualise, since the default is to plot all of them. + +* :meth:`~.api.signals.BaseSignal.plot_decomposition_factors` +* :meth:`~.api.signals.BaseSignal.plot_decomposition_loadings` + +.. _mva.plot_bss: + +Blind source separation plots +----------------------------- + +Visualizing blind source separation results is much the same as decomposition. +You can use :meth:`~.api.signals.BaseSignal.plot_bss_results` for a compact display, +or instead: + +* :meth:`~.api.signals.BaseSignal.plot_bss_factors` +* :meth:`~.api.signals.BaseSignal.plot_bss_loadings` + +.. _mva.get_results: + +Clustering plots +---------------- + +Visualizing cluster results is much the same as decomposition. +You can use :meth:`~.api.signals.BaseSignal.plot_bss_results` for a compact display, +or instead: + +* :meth:`~.api.signals.BaseSignal.plot_cluster_results`. +* :meth:`~.api.signals.BaseSignal.plot_cluster_signals`. +* :meth:`~.api.signals.BaseSignal.plot_cluster_labels`. diff --git a/doc/user_guide/pint_unit_registry.rst b/doc/user_guide/pint_unit_registry.rst new file mode 100644 index 0000000000..c078902c27 --- /dev/null +++ b/doc/user_guide/pint_unit_registry.rst @@ -0,0 +1,33 @@ +.. _pint_unit_registry: + +Unit Handling with Pint Quantity +******************************** + +HyperSpy uses the `pint `_ library to handle unit conversion. +To be interoperatable with other modules, HyperSpy uses the default pint :class:`pint.UnitRegistry` +provided by the :func:`pint.get_application_registry` function as described in the sections +`having a shared registry `_ +and `serialization `_ +of the pint user guide. + +For example, in the case of the ``scale_as_quantify`` pint quantity object from :class:`~.axes.UniformDataAxis`, +the default pint :class:`pint.UnitRegistry` is used: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(10)) + >>> s.axes_manager[0].scale_as_quantity + + >>> s.axes_manager[0].scale_as_quantity = '2.5 µm' + >>> s.axes_manager[0].scale_as_quantity + + +Then, using :func:`pint.get_application_registry` get the handle of the same instance of :class:`pint.UnitRegistry` +used by HyperSpy and use it to operate on this pint quantity: + + >>> import pint + >>> ureg = pint.get_application_registry() + >>> scale = 2E-6 * ureg.meter + >>> s.axes_manager[0].scale_as_quantity += scale + >>> s.axes_manager[0].scale_as_quantity + diff --git a/doc/user_guide/region_of_interest.rst b/doc/user_guide/region_of_interest.rst new file mode 100644 index 0000000000..3923ae68bd --- /dev/null +++ b/doc/user_guide/region_of_interest.rst @@ -0,0 +1,264 @@ + +.. _roi-label: + +Region Of Interest (ROI) +************************ + +ROIs can be defined to select part of any compatible signal and may be applied +either to the navigation or to the signal axes. A number of different ROIs are +available: + +* :class:`~.roi.Point1DROI` +* :class:`~.roi.Point2DROI` +* :class:`~.roi.SpanROI` +* :class:`~.roi.RectangularROI` +* :class:`~.roi.CircleROI` +* :class:`~.roi.Line2DROI` + +Once created, an ROI can be applied to the signal: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(2000).reshape((20,10,10))) + >>> im = hs.signals.Signal2D(np.arange(100).reshape((10,10))) + >>> roi = hs.roi.RectangularROI(left=3, right=7, top=2, bottom=5) + >>> sr = roi(s) + >>> sr + + >>> imr = roi(im) + >>> imr + + +ROIs can also be used :ref:`interactively ` with widgets. +The following example shows how to interactively apply ROIs to an image. Note +that *it is necessary* to plot the signal onto which the widgets will be +added before calling :meth:`~.roi.BaseInteractiveROI.interactive`. + +.. code-block:: python + + >>> import scipy + >>> im = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> rectangular_roi = hs.roi.RectangularROI(left=30, right=500, + ... top=200, bottom=400) + >>> line_roi = hs.roi.Line2DROI(0, 0, 512, 512, 1) + >>> point_roi = hs.roi.Point2DROI(256, 256) + >>> im.plot() + >>> roi2D = rectangular_roi.interactive(im, color="blue") + >>> roi1D = line_roi.interactive(im, color="yellow") + >>> roi0D = point_roi.interactive(im, color="red") + + +.. figure:: images/image_with_rois.png + :align: center + :width: 500 + +.. figure:: images/roi1d.png + :align: center + :width: 500 + +.. figure:: images/roi2d.png + :align: center + :width: 500 + +.. NOTE:: + + Depending on your screen and display settings, it can be difficult to `pick` + or manipulate widgets and you can try to change the pick tolerance in + the :ref:`HyperSpy plot preferences `. + Typically, using a 4K resolution with a small scaling factor (<150 %), setting + the pick tolerance to 15 instead of 7.5 makes the widgets easier to manipulate. + +If instantiated without arguments, (i.e. ``rect = RectangularROI()`` the roi +will automatically determine sensible values to center it when +interactively adding it to a signal. This provides a conventient starting point +to further manipulate the ROI, either by hand or using the gui (i.e. ``rect.gui``). + +Notably, since ROIs are independent from the signals they sub-select, the widget +can be plotted on a different signal altogether. + +.. code-block:: python + + >>> import scipy + >>> im = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> s = hs.signals.Signal1D(np.random.rand(512, 512, 512)) + >>> roi = hs.roi.RectangularROI(left=30, right=77, top=20, bottom=50) + >>> s.plot() # plot signal to have where to display the widget + >>> imr = roi.interactive(im, navigation_signal=s, color="red") + >>> roi(im).plot() + +ROIs are implemented in terms of physical coordinates and not pixels, so with +proper calibration will always point to the same region. + +.. figure:: images/random_image_with_rect_roi.png + :align: center + :width: 500 + +.. figure:: images/random_image_with_rect_roi_spectrum.png + :align: center + :width: 500 + +.. figure:: images/roi2d.png + :align: center + :width: 500 + + +And of course, as all interactive operations, interactive ROIs are chainable. +The following example shows how to display interactively the histogram of a +rectangular ROI. Notice how we customise the default event connections in +order to increase responsiveness. + + +.. code-block:: python + + >>> import scipy + >>> im = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> im.plot() + >>> roi = hs.roi.RectangularROI(left=30, right=500, top=200, bottom=400) + >>> im_roi = roi.interactive(im, color="red") + >>> roi_hist = hs.interactive(im_roi.get_histogram, + ... event=roi.events.changed, + ... bins=150, # Set number of bins for `get_histogram` + ... recompute_out_event=None) + >>> roi_hist.plot() + + +.. figure:: images/image_with_rect_roi.gif + :align: center + :width: 100% + +.. versionadded:: 1.3 + ROIs can be used in place of slices when indexing and to define a + signal range in functions taken a ``signal_range`` argument. + + +All ROIs have a ``gui`` method that displays an user interface if +a hyperspy GUI is installed (currently only works with the +``hyperspy_gui_ipywidgets`` GUI), enabling precise control of the ROI +parameters: + +.. code-block:: python + + >>> # continuing from above: + >>> roi.gui() # doctest: +SKIP + +.. figure:: images/roi_gui_control.gif + :align: center + :width: 100% + +.. versionadded:: 1.4 + :meth:`~.roi.Line2DROI.angle` can be used to calculate an angle between + ROI line and one of the axes providing its name through optional argument ``axis``: + +.. code-block:: python + + >>> import scipy + >>> ima = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> roi = hs.roi.Line2DROI(x1=144, y1=240, x2=306, y2=178, linewidth=0) + >>> ima.plot() + >>> roi.interactive(ima, color='red') + + +.. figure:: images/roi_line2d.png + :align: center + :width: 500 + +.. code-block:: python + + >>> roi.angle(axis='vertical') + 110.94265054998827 + +The default output of the method is in degrees, though radians can be selected +as follows: + +.. code-block:: python + + >>> roi.angle(axis='vertical', units='radians') + 1.9363145329867932 + +Conveniently, :meth:`~.roi.Line2DROI.angle` can be used to rotate an image to +align selected features with respect to vertical or horizontal axis: + +.. code-block:: python + + >>> ima.map(scipy.ndimage.rotate, angle=roi.angle(axis='horizontal'), inplace=False).plot() + +.. figure:: images/roi_line2d_rotate.png + :align: center + :width: 500 + + +.. _roi-slice-label: + +Slicing using ROIs +------------------ + +ROIs can be used in place of slices when indexing. For example: + +.. code-block:: python + + >>> s = hs.data.two_gaussians() + >>> roi = hs.roi.SpanROI(left=5, right=15) + >>> sc = s.isig[roi] + >>> im = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> roi = hs.roi.RectangularROI(left=120, right=460., top=300, bottom=560) + >>> imc = im.isig[roi] + +.. versionadded:: 1.3 + ``gui`` method added, for example :meth:`~.api.roi.Point1DROI.gui`. + +.. versionadded:: 1.6 + New ``__getitem__`` method for all ROIs. + +In addition, all ROIs have a ``__getitem__`` method that enables +using them in place of tuples. +For example, the method :meth:`~.api.signals.Signal2D.align2D` takes a ``roi`` +argument with the left, right, top, bottom coordinates of the ROI. +Handily, we can pass a :class:`~.roi.RectangularROI` ROI instead. + +.. code-block:: python + + >>> import hyperspy.api as hs + >>> import numpy as np + >>> im = hs.signals.Signal2D(np.random.random((10,30,30))) + >>> roi = hs.roi.RectangularROI(left=2, right=10, top=0, bottom=5) + >>> tuple(roi) + (2.0, 10.0, 0.0, 5.0) + >>> im.align2D(roi=roi) # doctest: +SKIP + + +Interactively Slicing Signal Dimensions +--------------------------------------- + +:func:`~.api.plot.plot_roi_map` is a function that allows you to +interactively visualize the spatial variation of intensity in a Signal +within a ROI of its signal axes. In other words, it shows maps of +the integrated signal for custom ranges along the signal axis. + +To allow selection of the signal ROIs, a plot of the mean signal over all +spatial positions is generated. Interactive ROIs can then be adjusted to the +desired regions within this plot. + +For each ROI, a plot reflecting how the intensity of signal within this ROI +varies over the spatial dimensions of the Signal object is also plotted. + +For Signal objects with 1 signal dimension :py:class:`~.roi.SpanROI`\ s are used +and for 2 signal dimensions, :py:class:`~.roi.RectangularROI`\ s are used. + +In the example below, for a hyperspectral map with 2 navigation dimensions and +1 signal dimension (i.e. a spectrum at each position in a 2D map), +:py:class:`~.roi.SpanROI`\ s are used to select spectral regions of interest. +For each spectral region of interest a plot is generated displaying the +intensity within this region at each position in the map. + +.. code-block:: python + + >>> import hyperpsy.api as hs # doctest: +SKIP + >>> sig = hs.load('mydata.sur') # doctest: +SKIP + >>> sig # doctest: +SKIP + + >>> hs.plot.plot_roi_map(sig, rois=2) # doctest: +SKIP + + +.. image:: images/plot_roi_map_demo.gif + :width: 100% + :alt: Demo of plot_roi_map functionality. diff --git a/doc/user_guide/signal.rst b/doc/user_guide/signal.rst deleted file mode 100644 index ae6218f710..0000000000 --- a/doc/user_guide/signal.rst +++ /dev/null @@ -1,1722 +0,0 @@ - -The Signal class -**************** - -The Signal class and its subclasses ------------------------------------ - -.. WARNING:: - This subsection can be a bit confusing for beginners. - Do not worry if you do not understand it all. - - -HyperSpy stores the data in the :py:class:`~.signal.BaseSignal` class, that is -the object that you get when e.g. you load a single file using -:py:func:`~.io.load`. Most of the data analysis functions are also contained in -this class or its specialized subclasses. The :py:class:`~.signal.BaseSignal` -class contains general functionality that is available to all the subclasses. -The subclasses provide functionality that is normally specific to a particular -type of data, e.g. the :py:class:`~._signals.signal1d.Signal1D` class provides -common functionality to deal with one-dimensional (e.g. spectral) data and -:py:class:`~._signals.eels.EELSSpectrum` (which is a subclass of -:py:class:`~._signals.signal1d.Signal1D`) adds extra functionality to the -:py:class:`~._signals.signal1d.Signal1D` class for electron energy-loss -spectroscopy data analysis. - -The :ref:`table below ` summarises all the -specialised :py:class:`~.signal.BaseSignal` subclasses currently distributed -with HyperSpy. - - - -The :py:mod:`~.signals` module, which contains all available signal subclasses, -is imported in the user namespace when loading HyperSpy. In the following -example we create a Signal2D instance from a 2D numpy array: - -.. code-block:: python - - >>> im = hs.signals.Signal2D(np.random.random((64,64))) - >>> im - - - -The different signals store other objects in what are called attributes. For -examples, the data is stored in a numpy array in the -:py:attr:`~.signal.BaseSignal.data` attribute, the original parameters in the -:py:attr:`~.signal.BaseSignal.original_metadata` attribute, the mapped parameters -in the :py:attr:`~.signal.BaseSignal.metadata` attribute and the axes -information (including calibration) can be accessed (and modified) in the -:py:class:`~.axes.AxesManager` attribute. - - -.. _signal_initialization: - -Signal initialization ---------------------- - -Many of the values in the :py:class:`~.axes.AxesManager` can be -set when making the :py:class:`~.signal.BaseSignal` object. - -.. code-block:: python - - >>> dict0 = {'size': 10, 'name':'Axis0', 'units':'A', 'scale':0.2, 'offset':1} - >>> s = hs.signals.BaseSignal(np.random.random((10,20)), axes=[dict0, dict1]) - >>> s.axes_manager - - Name | size | index | offset | scale | units - ================ | ====== | ====== | ======= | ======= | ====== - ---------------- | ------ | ------ | ------- | ------- | ------ - Axis1 | 20 | | 2 | 0.1 | B - Axis0 | 10 | | 1 | 0.2 | A - -This also applies to the :py:attr:`~.signal.BaseSignal.metadata`. - -.. code-block:: python - - >>> metadata_dict = {'General':{'name':'A BaseSignal'}} - >>> metadata_dict['General']['title'] = 'A BaseSignal title' - >>> s = hs.signals.BaseSignal(np.arange(10), metadata=metadata_dict) - >>> s.metadata - ├── General - │ ├── name = A BaseSignal - │ └── title = A BaseSignal title - └── Signal - └── signal_type = - -Instead of using a list of *axes dictionaries* ``[dict0, dict1]`` during signal -initialization, you can also pass a list of *axes objects*: ``[axis0, axis1]``. - - -The navigation and signal dimensions ------------------------------------- - -HyperSpy can deal with data of arbitrary dimensions. Each dimension is -internally classified as either "navigation" or "signal" and the way this -classification is done determines the behaviour of the signal. - -The concept is probably best understood with an example: let's imagine a three -dimensional dataset e.g. a numpy array with dimensions `(10, 20, 30)`. This -dataset could be an spectrum image acquired by scanning over a sample in two -dimensions. As in this case the signal is one-dimensional we use a -:py:class:`~._signals.signal1d.Signal1D` subclass for this data e.g.: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.random.random((10, 20, 30))) - >>> s - - -In HyperSpy's terminology, the *signal dimension* of this dataset is 30 and -the navigation dimensions (20, 10). Notice the separator `|` between the -navigation and signal dimensions. - - -However, the same dataset could also be interpreted as an image -stack instead. Actually it could has been acquired by capturing two -dimensional images at different wavelengths. Then it would be natural to -identify the two spatial dimensions as the signal dimensions and the wavelength -dimension as the navigation dimension. To view the data in this way we could -have used a :py:class:`~._signals.signal2d.Signal2D` instead e.g.: - -.. code-block:: python - - >>> im = hs.signals.Signal2D(np.random.random((10, 20, 30))) - >>> im - - -Indeed, for data analysis purposes, -one may like to operate with an image stack as if it was a set of spectra or -viceversa. One can easily switch between these two alternative ways of -classifying the dimensions of a three-dimensional dataset by -:ref:`transforming between BaseSignal subclasses -`. - -The same dataset could be seen as a three-dimensional signal: - -.. code-block:: python - - >>> td = hs.signals.BaseSignal(np.random.random((10, 20, 30))) - >>> td - - -Notice that with use :py:class:`~.signal.BaseSignal` because there is -no specialised subclass for three-dimensional data. Also note that by default -:py:class:`~.signal.BaseSignal` interprets all dimensions as signal dimensions. -We could also configure it to operate on the dataset as a three-dimensional -array of scalars by changing the default *view* of -:py:class:`~.signal.BaseSignal` by taking the transpose of it: - -.. code-block:: python - - >>> scalar = td.T - >>> scalar - - -For more examples of manipulating signal axes in the "signal-navigation" space -can be found in :ref:`signal.transpose`. - -.. NOTE:: - - Although each dimension can be arbitrarily classified as "navigation - dimension" or "signal dimension", for most common tasks there is no need to - modify HyperSpy's default choice. - - - -.. _transforming_signal-label: - -Transforming between signal subclasses -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The different subclasses are characterized by the `signal_type` metadata attribute, -the data `dtype` and the signal dimension. See the table and diagram below. -`signal_type` describes the nature of the signal. It can be any string, normally the -acronym associated with a particular signal. In certain cases HyperSpy provides -features that are only available for a particular signal type through -:py:class:`~.signal.BaseSignal` subclasses. The :py:class:`~.signal.BaseSignal` method -:py:meth:`~.signal.BaseSignal.set_signal_type` changes the signal_type in place, which -may result in a :py:class:`~.signal.BaseSignal` subclass transformation. - - -Furthermore, the `dtype` of the signal data also affects the subclass assignment. There are -e.g. specialised signal subclasses to handle complex data (see the following diagram). - - -.. figure:: images/HyperSpySignalOverview.png - :align: center - :width: 500 - - Diagram showing the inheritance structure of the different subclasses - -.. _signal_subclasses_table-label: - - -.. table:: BaseSignal subclass :py:attr:`~.signal.BaseSignal.metadata` attributes. - - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | BaseSignal subclass | signal_dimension | signal_type | dtype | - +=========================================================================+==================+=======================+==========+ - | :py:class:`~.signal.BaseSignal` | - | - | real | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.signal1d.Signal1D` | 1 | - | real | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.eels.EELSSpectrum` | 1 | EELS | real | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.eds_sem.EDSSEMSpectrum` | 1 | EDS_SEM | real | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.eds_tem.EDSTEM` | 1 | EDS_TEM | real | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.signal2d.Signal2D` | 2 | - | real | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.hologram_image.HologramImage` | 2 | hologram | real | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.dielectric_function.DielectricFunction` | 1 | DielectricFunction | complex | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.complex_signal.ComplexSignal` | - | - | complex | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.complex_signal1d.ComplexSignal1D` | 1 | - | complex | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - | :py:class:`~._signals.complex_signal2d.Complex2D` | 2 | - | complex | - +-------------------------------------------------------------------------+------------------+-----------------------+----------+ - -.. versionchanged:: 1.0 ``Simulation``, ``SpectrumSimulation`` and ``ImageSimulation`` - classes removed. - -.. versionadded:: 1.5 - External packages can register extra :py:class:`~.signal.BaseSignal` - subclasses. - -Note that, if you have :ref:`packages that extend HyperSpy -` installed in your system, there may -be more specialised signals available to you. To print all available specialised -:py:class:`~.signal.BaseSignal` subclasses installed in your system call the -:py:func:`~.utils.print_known_signal_types` -function as in the following example: - -.. code-block:: python - - >>> hs.print_known_signal_types() - +--------------------+---------------------+--------------------+----------+ - | signal_type | aliases | class name | package | - +--------------------+---------------------+--------------------+----------+ - | DielectricFunction | dielectric function | DielectricFunction | hyperspy | - | EDS_SEM | | EDSSEMSpectrum | hyperspy | - | EDS_TEM | | EDSTEMSpectrum | hyperspy | - | EELS | TEM EELS | EELSSpectrum | hyperspy | - | hologram | | HologramImage | hyperspy | - | MySignal | | MySignal | hspy_ext | - +--------------------+---------------------+--------------------+----------+ - -.. warning:: - From version 2.0 HyperSpy will no longer ship - :py:class:`~.signal.BaseSignal` subclasses that are specific to a - particular type of data (i.e. with non-empty ``signal_type``). All those - signals currently distributed with HyperSpy will be moved to new - packages. - -The following example shows how to transform between different subclasses. - - .. code-block:: python - - >>> s = hs.signals.Signal1D(np.random.random((10,20,100))) - >>> s - - >>> s.metadata - ├── signal_type = - └── title = - >>> im = s.to_signal2D() - >>> im - - >>> im.metadata - ├── signal_type = - └── title = - >>> s.set_signal_type("EELS") - >>> s - - >>> s.change_dtype("complex") - >>> s - - - -.. _signal.binned: - -Binned and unbinned signals ---------------------------- - -Signals that are a histogram of a probability density function (pdf) should -have the ``is_binned`` attribute of the signal axis set to ``True``. The reason -is that some methods operate differently on signals that are *binned*. An -example of *binned* signals are EDS spectra, where the multichannel analyzer -integrates the signal counts in every channel (=bin). -Note that for 2D signals each signal axis has an ``is_binned`` -attribute that can be set independently. For example, for the first signal -axis: ``signal.axes_manager.signal_axes[0].is_binned``. - -The default value of the ``is_binned`` attribute is shown in the -following table: - -.. table:: Binned default values for the different subclasses. - - - +---------------------------------------------------------------+--------+ - | BaseSignal subclass | binned | - +===============================================================+========+ - | :py:class:`~.signal.BaseSignal` | False | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.signal1d.Signal1D` | False | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.eels.EELSSpectrum` | True | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.eds_sem.EDSSEMSpectrum` | True | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.eds_tem.EDSTEMSpectrum` | True | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.signal2d.Signal2D` | False | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.complex_signal.ComplexSignal` | False | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.complex_signal1d.ComplexSignal1D` | False | - +---------------------------------------------------------------+--------+ - | :py:class:`~._signals.complex_signal2d.ComplexSignal2D` | False | - +---------------------------------------------------------------+--------+ - - - -To change the default value: - -.. code-block:: python - - >>> s.axes_manager[-1].is_binned = True - -.. versionchanged:: 1.7 The ``binned`` attribute from the metadata has been - replaced by the axis attributes ``is_binned``. - -Integration of binned signals -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For binned axes, the detector already provides the per-channel integration of -the signal. Therefore, in this case, :py:meth:`~.signal.BaseSignal.integrate1D` -performs a simple summation along the given axis. In contrast, for unbinned -axes, :py:meth:`~.signal.BaseSignal.integrate1D` calls the -:py:meth:`~.signal.BaseSignal.integrate_simpson` method. - - -Generic tools -------------- - -Below we briefly introduce some of the most commonly used tools (methods). For -more details about a particular method click on its name. For a detailed list -of all the methods available see the :py:class:`~.signal.BaseSignal` documentation. - -The methods of this section are available to all the signals. In other chapters -methods that are only available in specialized -subclasses. - -Mathematical operations -^^^^^^^^^^^^^^^^^^^^^^^ - -A number of mathematical operations are available -in :py:class:`~.signal.BaseSignal`. Most of them are just wrapped numpy -functions. - -The methods that perform mathematical operation over one or more axis at a -time are: - -* :py:meth:`~.signal.BaseSignal.sum` -* :py:meth:`~.signal.BaseSignal.max` -* :py:meth:`~.signal.BaseSignal.min` -* :py:meth:`~.signal.BaseSignal.mean` -* :py:meth:`~.signal.BaseSignal.std` -* :py:meth:`~.signal.BaseSignal.var` -* :py:meth:`~.signal.BaseSignal.nansum` -* :py:meth:`~.signal.BaseSignal.nanmax` -* :py:meth:`~.signal.BaseSignal.nanmin` -* :py:meth:`~.signal.BaseSignal.nanmean` -* :py:meth:`~.signal.BaseSignal.nanstd` -* :py:meth:`~.signal.BaseSignal.nanvar` - -Note that by default all this methods perform the operation over *all* -navigation axes. - -Example: - -.. code-block:: python - - >>> s = hs.signals.BaseSignal(np.random.random((2,4,6))) - >>> s.axes_manager[0].name = 'E' - >>> s - - >>> # by default perform operation over all navigation axes - >>> s.sum() - - >>> # can also pass axes individually - >>> s.sum('E') - - >>> # or a tuple of axes to operate on, with duplicates, by index or directly - >>> ans = s.sum((-1, s.axes_manager[1], 'E', 0)) - >>> ans - - >>> ans.axes_manager[0] - - -The following methods operate only on one axis at a time: - -* :py:meth:`~.signal.BaseSignal.diff` -* :py:meth:`~.signal.BaseSignal.derivative` -* :py:meth:`~.signal.BaseSignal.integrate_simpson` -* :py:meth:`~.signal.BaseSignal.integrate1D` -* :py:meth:`~.signal.BaseSignal.indexmin` -* :py:meth:`~.signal.BaseSignal.indexmax` -* :py:meth:`~.signal.BaseSignal.valuemin` -* :py:meth:`~.signal.BaseSignal.valuemax` - -.. _ufunc-label: - -All numpy ufunc can operate on :py:class:`~.signal.BaseSignal` -instances, for example: - -.. code-block:: python - - >>> s = hs.signals.Signal1D([0, 1]) - >>> s.metadata.General.title = "A" - >>> s - - >>> np.exp(s) - - >>> np.exp(s).data - array([ 1. , 2.71828183]) - >>> np.power(s, 2) - - >>> np.add(s, s) - - >>> np.add(hs.signals.Signal1D([0, 1]), hs.signals.Signal1D([0, 1])) - - - -Notice that the title is automatically updated. When the signal has no title -a new title is automatically generated: - -.. code-block:: python - - >>> np.add(hs.signals.Signal1D([0, 1]), hs.signals.Signal1D([0, 1])) - - - -Functions (other than unfucs) that operate on numpy arrays can also operate -on :py:class:`~.signal.BaseSignal` instances, however they return a numpy -array instead of a :py:class:`~.signal.BaseSignal` instance e.g.: - -.. code-block:: python - - >>> np.angle(s) - array([ 0., 0.]) - -.. note:: - For numerical **differentiation** and **integration**, use the proper - methods :py:meth:`~.signal.BaseSignal.derivative` and - :py:meth:`~.signal.BaseSignal.integrate1D`. In certain cases, particularly - when operating on a non-uniform axis, the approximations using the - :py:meth:`~.signal.BaseSignal.diff` and :py:meth:`~.signal.BaseSignal.sum` - methods will lead to erroneous results. - - -.. _signal.indexing: - -Indexing -^^^^^^^^ - -Indexing a :py:class:`~.signal.BaseSignal` provides a powerful, convenient and -Pythonic way to access and modify its data. In HyperSpy indexing is achieved -using ``isig`` and ``inav``, which allow the navigation and signal dimensions -to be indexed independently. The idea is essentially to specify a subset of the -data based on its position in the array and it is therefore essential to know -the convention adopted for specifying that position, which is described here. - -Those new to Python may find indexing a somewhat esoteric concept but once -mastered it is one of the most powerful features of Python based code and -greatly simplifies many common tasks. HyperSpy's Signal indexing is similar -to numpy array indexing and those new to Python are encouraged to read the -associated `numpy documentation on the subject `_. - - -Key features of indexing in HyperSpy are as follows (note that some of these -features differ from numpy): - -* HyperSpy indexing does: - - + Allow independent indexing of signal and navigation dimensions - + Support indexing with decimal numbers. - + Support indexing with units. - + Support indexing with relative coordinates i.e. 'rel0.5' - + Use the image order for indexing i.e. [x, y, z,...] (HyperSpy) vs - [...,z,y,x] (numpy) - -* HyperSpy indexing does not: - - + Support indexing using arrays. - + Allow the addition of new axes using the newaxis object. - -The examples below illustrate a range of common indexing tasks. - -First consider indexing a single spectrum, which has only one signal dimension -(and no navigation dimensions) so we use ``isig``: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(10)) - >>> s - - >>> s.data - array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - >>> s.isig[0] - - >>> s.isig[0].data - array([0]) - >>> s.isig[9].data - array([9]) - >>> s.isig[-1].data - array([9]) - >>> s.isig[:5] - - >>> s.isig[:5].data - array([0, 1, 2, 3, 4]) - >>> s.isig[5::-1] - - >>> s.isig[5::-1] - - >>> s.isig[5::2] - - >>> s.isig[5::2].data - array([5, 7, 9]) - -Unlike numpy, HyperSpy supports indexing using decimal numbers or strings -(containing a decimal number and units), in which case -HyperSpy indexes using the axis scales instead of the indices. Additionally, -one can index using relative coordinates, for example 'rel0.5' to index the -middle of the axis. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(10)) - >>> s - - >>> s.data - array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - >>> s.axes_manager[0].scale = 0.5 - >>> s.axes_manager[0].axis - array([ 0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5]) - >>> s.isig[0.5:4.].data - array([1, 2, 3, 4, 5, 6, 7]) - >>> s.isig[0.5:4].data - array([1, 2, 3]) - >>> s.isig[0.5:4:2].data - array([1, 3]) - >>> s.axes_manager[0].units = 'µm' - >>> s.isig[:'2000 nm'].data - array([0, 1, 2, 3]) - >>> s.isig[:'rel0.5'].data - array([0, 1, 2, 3]) - -Importantly the original :py:class:`~.signal.BaseSignal` and its "indexed self" -share their data and, therefore, modifying the value of the data in one -modifies the same value in the other. Note also that in the example below -s.data is used to access the data as a numpy array directly and this array is -then indexed using numpy indexing. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(10)) - >>> s - - >>> s.data - array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - >>> si = s.isig[::2] - >>> si.data - array([0, 2, 4, 6, 8]) - >>> si.data[:] = 10 - >>> si.data - array([10, 10, 10, 10, 10]) - >>> s.data - array([10, 1, 10, 3, 10, 5, 10, 7, 10, 9]) - >>> s.data[:] = 0 - >>> si.data - array([0, 0, 0, 0, 0]) - -Of course it is also possible to use the same syntax to index multidimensional -data treating navigation axes using ``inav`` and signal axes using ``isig``. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(2*3*4).reshape((2,3,4))) - >>> s - - >>> s.data - array([[[ 0, 1, 2, 3], - [ 4, 5, 6, 7], - [ 8, 9, 10, 11]], - - [[12, 13, 14, 15], - [16, 17, 18, 19], - [20, 21, 22, 23]]]) - >>> s.axes_manager[0].name = 'x' - >>> s.axes_manager[1].name = 'y' - >>> s.axes_manager[2].name = 't' - >>> s.axes_manager.signal_axes - (,) - >>> s.axes_manager.navigation_axes - (, ) - >>> s.inav[0,0].data - array([0, 1, 2, 3]) - >>> s.inav[0,0].axes_manager - - Name | size | index | offset | scale | units - ================ | ====== | ====== | ======= | ======= | ====== - ---------------- | ------ | ------ | ------- | ------- | ------ - t | 4 | | 0 | 1 | - >>> s.inav[0,0].isig[::-1].data - array([3, 2, 1, 0]) - >>> s.isig[0] - - >>> s.isig[0].axes_manager - - Name | size | index | offset | scale | units - ================ | ====== | ====== | ======= | ======= | ====== - x | 3 | 0 | 0 | 1 | - y | 2 | 0 | 0 | 1 | - ---------------- | ------ | ------ | ------- | ------- | ------ - >>> s.isig[0].data - array([[ 0, 4, 8], - [12, 16, 20]]) - -Independent indexation of the signal and navigation dimensions is demonstrated -further in the following: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(2*3*4).reshape((2,3,4))) - >>> s - - >>> s.data - array([[[ 0, 1, 2, 3], - [ 4, 5, 6, 7], - [ 8, 9, 10, 11]], - - [[12, 13, 14, 15], - [16, 17, 18, 19], - [20, 21, 22, 23]]]) - >>> s.axes_manager[0].name = 'x' - >>> s.axes_manager[1].name = 'y' - >>> s.axes_manager[2].name = 't' - >>> s.axes_manager.signal_axes - (,) - >>> s.axes_manager.navigation_axes - (, ) - >>> s.inav[0,0].data - array([0, 1, 2, 3]) - >>> s.inav[0,0].axes_manager - - Name | size | index | offset | scale | units - ================ | ====== | ====== | ======= | ======= | ====== - ---------------- | ------ | ------ | ------- | ------- | ------ - t | 4 | | 0 | 1 | - >>> s.isig[0] - - >>> s.isig[0].axes_manager - - Name | size | index | offset | scale | units - ================ | ====== | ====== | ======= | ======= | ====== - x | 3 | 0 | 0 | 1 | - y | 2 | 0 | 0 | 1 | - ---------------- | ------ | ------ | ------- | ------- | ------ - >>> s.isig[0].data - array([[ 0, 4, 8], - [12, 16, 20]]) - - -The same syntax can be used to set the data values in signal and navigation -dimensions respectively: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(2*3*4).reshape((2,3,4))) - >>> s - - >>> s.data - array([[[ 0, 1, 2, 3], - [ 4, 5, 6, 7], - [ 8, 9, 10, 11]], - - [[12, 13, 14, 15], - [16, 17, 18, 19], - [20, 21, 22, 23]]]) - >>> s.inav[0,0].data - array([0, 1, 2, 3]) - >>> s.inav[0,0] = 1 - >>> s.inav[0,0].data - array([1, 1, 1, 1]) - >>> s.inav[0,0] = s.inav[1,1] - >>> s.inav[0,0].data - array([16, 17, 18, 19]) - - -.. _signal.operations: - -Signal operations -^^^^^^^^^^^^^^^^^ - -:py:class:`~.signal.BaseSignal` supports all the Python binary arithmetic -operations (+, -, \*, //, %, divmod(), pow(), \*\*, <<, >>, &, ^, \|), -augmented binary assignments (+=, -=, \*=, /=, //=, %=, \*\*=, <<=, >>=, &=, -^=, \|=), unary operations (-, +, abs() and ~) and rich comparisons operations -(<, <=, ==, x!=y, <>, >, >=). - -These operations are performed element-wise. When the dimensions of the signals -are not equal `numpy broadcasting rules apply -`_ independently -for the navigation and signal axes. - -.. WARNING:: - - Hyperspy does not check if the calibration of the signals matches. - -In the following example `s2` has only one navigation axis while `s` has two. -However, because the size of their first navigation axis is the same, their -dimensions are compatible and `s2` is -broadcasted to match `s`'s dimensions. - -.. code-block:: python - - >>> s = hs.signals.Signal2D(np.ones((3,2,5,4))) - >>> s2 = hs.signals.Signal2D(np.ones((2,5,4))) - >>> s - - >>> s2 - - >>> s + s2 - - -In the following example the dimensions are not compatible and an exception -is raised. - -.. code-block:: python - - >>> s = hs.signals.Signal2D(np.ones((3,2,5,4))) - >>> s2 = hs.signals.Signal2D(np.ones((3,5,4))) - >>> s - - >>> s2 - - >>> s + s2 - Traceback (most recent call last): - File "", line 1, in - s + s2 - File "", line 2, in __add__ - File "/home/fjd29/Python/hyperspy/hyperspy/signal.py", line 2686, in _binary_operator_ruler - raise ValueError(exception_message) - ValueError: Invalid dimensions for this operation - - -Broadcasting operates exactly in the same way for the signal axes: - -.. code-block:: python - - >>> s = hs.signals.Signal2D(np.ones((3,2,5,4))) - >>> s2 = hs.signals.Signal1D(np.ones((3, 2, 4))) - >>> s - - >>> s2 - - >>> s + s2 - - -In-place operators also support broadcasting, but only when broadcasting would -not change the left most signal dimensions: - -.. code-block:: python - - >>> s += s2 - >>> s - - >>> s2 += s - Traceback (most recent call last): - File "", line 1, in - s2 += s - File "", line 2, in __iadd__ - File "/home/fjd29/Python/hyperspy/hyperspy/signal.py", line 2737, in _binary_operator_ruler - self.data = getattr(sdata, op_name)(odata) - ValueError: non-broadcastable output operand with shape (3,2,1,4) doesn\'t match the broadcast shape (3,2,5,4) - - -.. _signal.iterator: - -Iterating over the navigation axes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -BaseSignal instances are iterables over the navigation axes. For example, the -following code creates a stack of 10 images and saves them in separate "png" -files by iterating over the signal instance: - -.. code-block:: python - - >>> image_stack = hs.signals.Signal2D(np.random.random((2, 5, 64,64))) - >>> for single_image in image_stack: - ... single_image.save("image %s.png" % str(image_stack.axes_manager.indices)) - The "image (0, 0).png" file was created. - The "image (1, 0).png" file was created. - The "image (2, 0).png" file was created. - The "image (3, 0).png" file was created. - The "image (4, 0).png" file was created. - The "image (0, 1).png" file was created. - The "image (1, 1).png" file was created. - The "image (2, 1).png" file was created. - The "image (3, 1).png" file was created. - The "image (4, 1).png" file was created. - -The data of the signal instance that is returned at each iteration is a view of -the original data, a property that we can use to perform operations on the -data. For example, the following code rotates the image at each coordinate by -a given angle and uses the :py:func:`~.utils.stack` function in combination -with `list comprehensions -`_ -to make a horizontal "collage" of the image stack: - -.. code-block:: python - - >>> import scipy.ndimage - >>> image_stack = hs.signals.Signal2D(np.array([scipy.misc.ascent()]*5)) - >>> image_stack.axes_manager[1].name = "x" - >>> image_stack.axes_manager[2].name = "y" - >>> for image, angle in zip(image_stack, (0, 45, 90, 135, 180)): - ... image.data[:] = scipy.ndimage.rotate(image.data, angle=angle, - ... reshape=False) - >>> # clip data to integer range: - >>> image_stack.data = np.clip(image_stack.data, 0, 255) - >>> collage = hs.stack([image for image in image_stack], axis=0) - >>> collage.plot(scalebar=False) - -.. figure:: images/rotate_ascent.png - :align: center - :width: 500 - - Rotation of images by iteration. - -.. _map-label: - -Iterating external functions with the map method -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Performing an operation on the data at each coordinate, as in the previous example, -using an external function can be more easily accomplished using the -:py:meth:`~.signal.BaseSignal.map` method: - -.. code-block:: python - - >>> import scipy.ndimage - >>> image_stack = hs.signals.Signal2D(np.array([scipy.misc.ascent()]*4)) - >>> image_stack.axes_manager[1].name = "x" - >>> image_stack.axes_manager[2].name = "y" - >>> image_stack.map(scipy.ndimage.rotate, - ... angle=45, - ... reshape=False) - >>> # clip data to integer range - >>> image_stack.data = np.clip(image_stack.data, 0, 255) - >>> collage = hs.stack([image for image in image_stack], axis=0) - >>> collage.plot() - -.. figure:: images/rotate_ascent_apply_simple.png - :align: center - :width: 500 - - Rotation of images by the same amount using :py:meth:`~.signal.BaseSignal.map`. - -The :py:meth:`~.signal.BaseSignal.map` method can also take variable -arguments as in the following example. - -.. code-block:: python - - >>> import scipy.ndimage - >>> image_stack = hs.signals.Signal2D(np.array([scipy.misc.ascent()]*4)) - >>> image_stack.axes_manager[1].name = "x" - >>> image_stack.axes_manager[2].name = "y" - >>> angles = hs.signals.BaseSignal(np.array([0, 45, 90, 135])) - >>> image_stack.map(scipy.ndimage.rotate, - ... angle=angles.T, - ... reshape=False) - -.. figure:: images/rotate_ascent_apply_ndkwargs.png - :align: center - :width: 500 - - Rotation of images using :py:meth:`~.signal.BaseSignal.map` with different - arguments for each image in the stack. - -.. versionadded:: 1.2.0 - ``inplace`` keyword and non-preserved output shapes - -If all function calls do not return identically-shaped results, only navigation -information is preserved, and the final result is an array where -each element corresponds to the result of the function (or arbitrary object -type). As such, most HyperSpy functions cannot operate on such Signal, and the -data should be accessed directly. - -The ``inplace`` keyword (by default ``True``) of the -:py:meth:`~.signal.BaseSignal.map` method allows either overwriting the current -data (default, ``True``) or storing it to a new signal (``False``). - -.. code-block:: python - - >>> import scipy.ndimage - >>> image_stack = hs.signals.Signal2D(np.array([scipy.misc.ascent()]*4)) - >>> angles = hs.signals.BaseSignal(np.array([0, 45, 90, 135])) - >>> result = image_stack.map(scipy.ndimage.rotate, - ... angle=angles.T, - ... inplace=False, - ... reshape=True) - 100%|████████████████████████████████████████████| 4/4 [00:00<00:00, 18.42it/s] - - >>> result - - >>> image_stack.data.dtype - dtype('O') - >>> for d in result.data.flat: - ... print(d.shape) - (512, 512) - (724, 724) - (512, 512) - (724, 724) - -.. _parallel-map-label: - -The execution can be sped up by passing ``parallel`` keyword to the -:py:meth:`~.signal.BaseSignal.map` method: - -.. code-block:: python - - >>> import time - >>> def slow_func(data): - ... time.sleep(1.) - ... return data + 1 - >>> s = hs.signals.Signal1D(np.arange(20).reshape((20,1))) - >>> s - - >>> s.map(slow_func, parallel=False) - 100%|██████████████████████████████████████| 20/20 [00:20<00:00, 1.00s/it] - >>> # some operations will be done in parallel: - >>> s.map(slow_func, parallel=True) - 100%|██████████████████████████████████████| 20/20 [00:02<00:00, 6.73it/s] - -.. note:: - - HyperSpy implements *thread-based* parallelism for the :py:meth:`~.signal.BaseSignal.map` - method. You can control the number of threads that are created by passing an integer value - to the ``max_workers`` keyword argument. By default, it will use ``min(32, os.cpu_count())``. - -.. versionadded:: 1.4 - Iterating over signal using a parameter with no navigation dimension. - -In this case, the parameter is cyclically iterated over the navigation -dimension of the input signal. In the example below, signal s is -multiplied by a cosine parameter d, which is repeated over the -navigation dimension of s. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.random.rand(10, 512)) - >>> d = hs.signals.Signal1D(np.cos(np.linspace(0., 2*np.pi, 512))) - >>> s.map(lambda A, B: A * B, B=d) - 100%|██████████| 10/10 [00:00<00:00, 2573.19it/s] - - -Cropping -^^^^^^^^ - -Cropping can be performed in a very compact and powerful way using -:ref:`signal.indexing` . In addition it can be performed using the following -method or GUIs if cropping :ref:`signal1D ` or :ref:`signal2D -`. There is also a general :py:meth:`~.signal.BaseSignal.crop` -method that operates *in place*. - - -.. _rebin-label: - -Rebinning -^^^^^^^^^ -.. versionadded:: 1.3 - :py:meth:`~.signal.BaseSignal.rebin` generalized to remove the constrain - of the ``new_shape`` needing to be a divisor of ``data.shape``. - - -The :py:meth:`~.signal.BaseSignal.rebin` methods supports rebinning the data to -arbitrary new shapes as long as the number of dimensions stays the same. -However, internally, it uses two different algorithms to perform the task. Only -when the new shape dimensions are divisors of the old shape's, the operation -supports :ref:`lazy-evaluation ` and is usually faster. -Otherwise, the operation requires linear interpolation. - -For example, the following two equivalent rebinning operations can be performed -lazily: - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum().as_lazy() - >>> print(s) - - >>> print(s.rebin(scale=[2])) - - - -.. code-block:: python - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum().as_lazy() - >>> print(s.rebin(new_shape=[512])) - - - -On the other hand, the following rebinning operation requires interpolation and -cannot be performed lazily: - -.. code-block:: python - - >>> spectrum = hs.signals.EDSTEMSpectrum(np.ones([4, 4, 10])) - >>> spectrum.data[1, 2, 9] = 5 - >>> print(spectrum) - - >>> print ('Sum = ', spectrum.data.sum()) - Sum = 164.0 - >>> scale = [0.5, 0.5, 5] - >>> test = spectrum.rebin(scale=scale) - >>> test2 = spectrum.rebin(new_shape=(8, 8, 2)) # Equivalent to the above - >>> print(test) - - >>> print(test2) - - >>> print('Sum =', test.data.sum()) - Sum = 164.0 - >>> print('Sum =', test2.data.sum()) - Sum = 164.0 - >>> spectrum.as_lazy().rebin(scale=scale) - Traceback (most recent call last): - File "", line 1, in - spectrum.as_lazy().rebin(scale=scale) - File "/home/fjd29/Python/hyperspy3/hyperspy/_signals/eds.py", line 184, in rebin - m = super().rebin(new_shape=new_shape, scale=scale, crop=crop, out=out) - File "/home/fjd29/Python/hyperspy3/hyperspy/_signals/lazy.py", line 246, in rebin - "Lazy rebin requires scale to be integer and divisor of the " - NotImplementedError: Lazy rebin requires scale to be integer and divisor of the original signal shape - - -The ``dtype`` argument can be used to specify the ``dtype`` of the returned -signal: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.ones((2, 5, 10), dtype=np.uint8) - >>> print(s) - - >>> print(s.data.dtype) - uint8 - - # Use dtype=np.unit16 to specify a dtype - >>> s2 = s.rebin(scale=(5, 2, 1), dtype=np.uint16) - >>> print(s2.data.dtype) - uint16 - - # Use dtype="same" to keep the same dtype - >>> s3 = s.rebin(scale=(5, 2, 1), dtype="same") - >>> print(s3.data.dtype) - uint8 - - # By default `dtype=None`, the dtype is determined by the behaviour of - # numpy.sum, in this case, unsigned integer of the same precision as the - # platform interger - >>> s4 = s.rebin(scale=(5, 2, 1)) - >>> print(s4.data.dtype) - uint64 - - -.. _squeeze-label: - -Squeezing -^^^^^^^^^ - -The :py:meth:`~.signal.BaseSignal.squeeze` method removes any zero-dimensional -axes, i.e. axes of ``size=1``, and the attributed data dimensions from a signal. -The method returns a reduced copy of the signal and does not operate in place. - -.. code-block:: python - - >>> s = hs.signals.Signal2D(np.random.random((2,1,1,6,8,8))) - - >>> s = s.squeeze() - >>> s - - -Squeezing can be particularly useful after a rebinning operation that leaves -one dimension with ``shape=1``: - - >>> s = hs.signals.Signal2D(np.random.random((5,5,5,10,10))) - >>> s.rebin(new_shape=(5,1,5,5,5)) - - >>> s.rebin(new_shape=(5,1,5,5,5)).squeeze() - - - -Folding and unfolding -^^^^^^^^^^^^^^^^^^^^^ - -When dealing with multidimensional datasets it is sometimes useful to transform -the data into a two dimensional dataset. This can be accomplished using the -following two methods: - -* :py:meth:`~.signal.BaseSignal.fold` -* :py:meth:`~.signal.BaseSignal.unfold` - -It is also possible to unfold only the navigation or only the signal space: - -* :py:meth:`~.signal.BaseSignal.unfold_navigation_space` -* :py:meth:`~.signal.BaseSignal.unfold_signal_space` - - -.. _signal.stack_split: - -Splitting and stacking -^^^^^^^^^^^^^^^^^^^^^^ - -Several objects can be stacked together over an existing axis or over a -new axis using the :py:func:`~.utils.stack` function, if they share axis -with same dimension. - -.. code-block:: python - - >>> image = hs.signals.Signal2D(scipy.misc.ascent()) - >>> image = hs.stack([hs.stack([image]*3,axis=0)]*3,axis=1) - >>> image.plot() - -.. figure:: images/stack_ascent_3_3.png - :align: center - :width: 500 - - Stacking example. - -.. note:: - - When stacking signals with large amount of - :py:attr:`~.signal.BaseSignal.original_metadata`, these metadata will be - stacked and this can lead to very large amount of metadata which can in - turn slow down processing. The ``stack_original_metadata`` argument can be - used to disable stacking :py:attr:`~.signal.BaseSignal.original_metadata`. - -An object can be split into several objects -with the :py:meth:`~.signal.BaseSignal.split` method. This function can be used -to reverse the :py:func:`~.utils.stack` function: - -.. code-block:: python - - >>> image = image.split()[0].split()[0] - >>> image.plot() - -.. figure:: images/split_ascent_3_3.png - :align: center - :width: 400 - - Splitting example. - - -.. _signal.fft: - -Fast Fourier Transform (FFT) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The `fast Fourier transform `_ -of a signal can be computed using the :py:meth:`~.signal.BaseSignal.fft` method. By default, -the FFT is calculated with the origin at (0, 0), which will be displayed at the -bottom left and not in the centre of the FFT. Conveniently, the ``shift`` argument of the -the :py:meth:`~.signal.BaseSignal.fft` method can be used to center the output of the FFT. -In the following example, the FFT of a hologram is computed using ``shift=True`` and its -output signal is displayed, which shows that the FFT results in a complex signal with a -real and an imaginary parts: - -.. code-block:: python - - >>> im = hs.datasets.example_signals.reference_hologram() - >>> fft_shifted = im.fft(shift=True) - >>> fft_shifted.plot() - -.. figure:: images/FFT_vacuum_reference_hologram.png - :align: center - :width: 800 - -The strong features in the real and imaginary parts correspond to the lattice fringes of the -hologram. - -For visual inspection of the FFT it is convenient to display its power spectrum -(i.e. the square of the absolute value of the FFT) rather than FFT itself as it is done -in the example above by using the ``power_spectum`` argument: - -.. code-block:: python - - >>> im = hs.datasets.example_signals.reference_hologram() - >>> fft = im.fft(True) - >>> fft.plot(True) - -Where ``power_spectum`` is set to ``True`` since it is the first argument of the -:py:meth:`~._signals.complex_signal.ComplexSignal.plot` method for complex signal. -When ``power_spectrum=True``, the plot will be displayed on a log scale by default. - - -.. figure:: images/FFT_vacuum_reference_hologram_power_spectrum.png - :align: center - :width: 400 - -The visualisation can be further improved by setting the minimum value to display to the 30-th -percentile; this can be done by using ``vmin="30th"`` in the plot function: - -.. code-block:: python - - >>> im = hs.datasets.example_signals.reference_hologram() - >>> fft = im.fft(True) - >>> fft.plot(True, vmin="30th") - -.. figure:: images/FFT_vacuum_reference_hologram_power_spectrum_vmin30th.png - :align: center - :width: 400 - -The streaks visible in the FFT come from the edge of the image and can be removed by -applying an `apodization `_ function to the original -signal before the computation of the FFT. This can be done using the ``apodization`` argument of -the :py:meth:`~.signal.BaseSignal.fft` method and it is usually used for visualising FFT patterns -rather than for quantitative analyses. By default, the so-called ``hann`` windows is -used but different type of windows such as the ``hamming`` and ``tukey`` windows. - -.. code-block:: python - - >>> im = hs.datasets.example_signals.reference_hologram() - >>> fft = im.fft(shift=True) - >>> fft_apodized = im.fft(shift=True, apodization=True) - >>> fft_apodized.plot(True, vmin="30th") - -.. figure:: images/FFT_vacuum_reference_hologram_power_spectrum_vmin30th-apodization.png - :align: center - :width: 400 - - - -Inverse Fast Fourier Transform (iFFT) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Inverse fast Fourier transform can be calculated from a complex signal by using the -:py:meth:`~.signal.BaseSignal.ifft` method. Similarly to the :py:meth:`~.signal.BaseSignal.fft` method, -the ``shift`` argument can be provided to shift the origin of the iFFT when necessary: - -.. code-block:: python - - >>> im_ifft = im.fft(shift=True).ifft(shift=True) - - -.. _signal.change_dtype: - -Changing the data type -^^^^^^^^^^^^^^^^^^^^^^ - -Even if the original data is recorded with a limited dynamic range, it is often -desirable to perform the analysis operations with a higher precision. -Conversely, if space is limited, storing in a shorter data type can decrease -the file size. The :py:meth:`~.signal.BaseSignal.change_dtype` changes the data -type in place, e.g.: - -.. code-block:: python - - >>> s = hs.load('EELS Signal1D Signal2D (high-loss).dm3') - Title: EELS Signal1D Signal2D (high-loss).dm3 - Signal type: EELS - Data dimensions: (21, 42, 2048) - Data representation: spectrum - Data type: float32 - >>> s.change_dtype('float64') - >>> print(s) - Title: EELS Signal1D Signal2D (high-loss).dm3 - Signal type: EELS - Data dimensions: (21, 42, 2048) - Data representation: spectrum - Data type: float64 - - -In addition to all standard numpy dtypes, HyperSpy supports four extra dtypes -for RGB images **for visualization purposes only**: ``rgb8``, ``rgba8``, -``rgb16`` and ``rgba16``. This includes of course multi-dimensional RGB images. - -The requirements for changing from and to any ``rgbx`` dtype are more strict -than for most other dtype conversions. To change to a ``rgbx`` dtype the -``signal_dimension`` must be 1 and its size 3 (4) 3(4) for ``rgb`` (or -``rgba``) dtypes and the dtype must be ``uint8`` (``uint16``) for -``rgbx8`` (``rgbx16``). After conversion the ``signal_dimension`` becomes 2. - -Most operations on signals with RGB dtypes will fail. For processing simply -change their dtype to ``uint8`` (``uint16``).The dtype of images of -dtype ``rgbx8`` (``rgbx16``) can only be changed to ``uint8`` (``uint16``) and -the ``signal_dimension`` becomes 1. - -In the following example we create a 1D signal with signal size 3 and with -dtype ``uint16`` and change its dtype to ``rgb16`` for plotting. - -.. code-block:: python - - >>> rgb_test = np.zeros((1024, 1024, 3)) - >>> ly, lx = rgb_test.shape[:2] - >>> offset_factor = 0.16 - >>> size_factor = 3 - >>> Y, X = np.ogrid[0:lx, 0:ly] - >>> rgb_test[:,:,0] = (X - lx / 2 - lx*offset_factor) ** 2 + \ - ... (Y - ly / 2 - ly*offset_factor) ** 2 < \ - ... lx * ly / size_factor **2 - >>> rgb_test[:,:,1] = (X - lx / 2 + lx*offset_factor) ** 2 + \ - ... (Y - ly / 2 - ly*offset_factor) ** 2 < \ - ... lx * ly / size_factor **2 - >>> rgb_test[:,:,2] = (X - lx / 2) ** 2 + \ - ... (Y - ly / 2 + ly*offset_factor) ** 2 \ - ... < lx * ly / size_factor **2 - >>> rgb_test *= 2**16 - 1 - >>> s = hs.signals.Signal1D(rgb_test) - >>> s.change_dtype("uint16") - >>> s - - >>> s.change_dtype("rgb16") - >>> s - - >>> s.plot() - - -.. figure:: images/rgb_example.png - :align: center - :width: 500 - - RGB data type example. - - -.. _signal.transpose: - -Transposing (changing signal spaces) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. versionadded:: 1.1 - -:py:meth:`~.signal.BaseSignal.transpose` method changes how the dataset -dimensions are interpreted (as signal or navigation axes). By default is -swaps the signal and navigation axes. For example: - - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.zeros((4,5,6))) - >>> s - - >>> s.transpose() - - -For :py:meth:`~.signal.BaseSignal.T` is a shortcut for the default behaviour: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.zeros((4,5,6))).T - - - -The method accepts both explicit axes to keep in either space, or just a number -of axes required in one space (just one number can be specified, as the other -is defined as "all other axes"). When axes order is not explicitly defined, -they are "rolled" from one space to the other as if the ```` wrap a circle. The example below should help clarifying this. - - -.. code-block:: python - - >>> # just create a signal with many distinct dimensions - >>> s = hs.signals.BaseSignal(np.random.rand(1,2,3,4,5,6,7,8,9)) - >>> s - - >>> s.transpose(signal_axes=5) # roll to leave 5 axes in signal space - - >>> s.transpose(navigation_axes=3) # roll leave 3 axes in navigation space - - >>> # 3 explicitly defined axes in signal space - >>> s.transpose(signal_axes=[0, 2, 6]) - - >>> # A mix of two lists, but specifying all axes explicitly - >>> # The order of axes is preserved in both lists - >>> s.transpose(navigation_axes=[1, 2, 3, 4, 5, 8], signal_axes=[0, 6, 7]) - - -A convenience functions :py:func:`~.utils.transpose` is available to operate on -many signals at once, for example enabling plotting any-dimension signals -trivially: - -.. code-block:: python - - >>> s2 = hs.signals.BaseSignal(np.random.rand(2, 2)) # 2D signal - >>> s3 = hs.signals.BaseSignal(np.random.rand(3, 3, 3)) # 3D signal - >>> s4 = hs.signals.BaseSignal(np.random.rand(4, 4, 4, 4)) # 4D signal - >>> hs.plot.plot_images(hs.transpose(s2, s3, s4, signal_axes=2)) - -.. _signal.transpose_optimize: - -The :py:meth:`~.signal.BaseSignal.transpose` method accepts keyword argument -``optimize``, which is ``False`` by default, meaning modifying the output -signal data **always modifies the original data** i.e. the data is just a view -of the original data. If ``True``, the method ensures the data in memory is -stored in the most efficient manner for iterating by making a copy of the data -if required, hence modifying the output signal data **not always modifies the -original data**. - -The convenience methods :py:meth:`~.signal.BaseSignal.as_signal1D` and -:py:meth:`~.signal.BaseSignal.as_signal2D` internally use -:py:meth:`~.signal.BaseSignal.transpose`, but always optimize the data -for iteration over the navigation axes if required. Hence, these methods do not -always return a view of the original data. If a copy of the data is required -use -:py:meth:`~.signal.BaseSignal.deepcopy` on the output of any of these -methods e.g.: - -.. code-block:: python - - >>> hs.signals.Signal1D(np.zeros((4,5,6))).T.deepcopy() - - - -Applying apodization window -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Apodization window (also known as apodization function) can be applied to a signal -using :py:meth:`~.signal.BaseSignal.apply_apodization` method. By default standard -Hann window is used: - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.ones(1000)) - >>> sa = s.apply_apodization() - >>> sa.metadata.General.title = 'Hann window' - >>> sa.plot() - - -.. figure:: images/hann_window.png - :align: center - :width: 400 - -Higher order Hann window can be used in order to keep larger fraction of intensity of original signal. -This can be done providing an integer number for the order of the window through -keyword argument ``hann_order``. (The last one works only together with default value of ``window`` argument -or with ``window='hann'``.) - -.. code-block:: python - - >>> im = hs.datasets.example_signals.reference_hologram().isig[:200, :200] - >>> ima = im.apply_apodization(window='hann', hann_order=3) - >>> hs.plot.plot_images([im, ima], vmax=3000, tight_layout=True) - - -.. figure:: images/hann_3d_order_ref_holo.png - :align: center - :width: 800 - -In addition to Hann window also Hamming or Tukey windows can be applied using ``window`` attribute -selecting ``'hamming'`` or ``'tukey'`` respectively. - -The shape of Tukey window can be adjusted using parameter alpha -provided through ``tukey_alpha`` keyword argument (only used when ``window='tukey'``). -The parameter represents the fraction of the window inside the cosine tapered region, -i.e. smaller is alpha larger is the middle flat region where the original signal -is preserved. If alpha is one, the Tukey window is equivalent to a Hann window. -(Default value is 0.5) - -Apodization can be applied in place by setting keyword argument ``inplace`` to ``True``. -In this case method will not return anything. - -Basic statistical analysis --------------------------- - -:py:meth:`~.signal.BaseSignal.get_histogram` computes the histogram and -conveniently returns it as signal instance. It provides methods to -calculate the bins. :py:meth:`~.signal.BaseSignal.print_summary_statistics` -prints the five-number summary statistics of the data. - -These two methods can be combined with -:py:meth:`~.signal.BaseSignal.get_current_signal` to compute the histogram or -print the summary statistics of the signal at the current coordinates, e.g: - -.. code-block:: python - - >>> s = hs.signals.EELSSpectrum(np.random.normal(size=(10,100))) - >>> s.print_summary_statistics() - Summary statistics - ------------------ - mean: 0.021 - std: 0.957 - min: -3.991 - Q1: -0.608 - median: 0.013 - Q3: 0.652 - max: 2.751 - - >>> s.get_current_signal().print_summary_statistics() - Summary statistics - ------------------ - mean: -0.019 - std: 0.855 - min: -2.803 - Q1: -0.451 - median: -0.038 - Q3: 0.484 - max: 1.992 - -Histogram of different objects can be compared with the functions -:py:func:`~.drawing.utils.plot_histograms` (see -:ref:`visualisation ` for the plotting options). For example, -with histograms of several random chi-square distributions: - - -.. code-block:: python - - >>> img = hs.signals.Signal2D([np.random.chisquare(i+1,[100,100]) for - ... i in range(5)]) - >>> hs.plot.plot_histograms(img,legend='auto') - -.. figure:: images/plot_histograms_chisquare.png - :align: center - :width: 500 - - Comparing histograms. - - -.. _signal.noise_properties: - -Setting the noise properties ----------------------------- - -Some data operations require the data variance. Those methods use the -``metadata.Signal.Noise_properties.variance`` attribute if it exists. You can -set this attribute as in the following example where we set the variance to be -10: - -.. code-block:: python - - >>> s.metadata.Signal.set_item("Noise_properties.variance", 10) - -You can also use the functions :meth:`~.signal.BaseSignal.set_noise_variance` -and :meth:`~.signal.BaseSignal.get_noise_variance` for convenience: - -.. code-block:: python - - >>> s.set_noise_variance(10) - >>> s.get_noise_variance() - 10 - -For heteroscedastic noise the ``variance`` attribute must be a -:class:`~.signal.BaseSignal`. Poissonian noise is a common case of -heteroscedastic noise where the variance is equal to the expected value. The -:meth:`~.signal.BaseSignal.estimate_poissonian_noise_variance` -method can help setting the variance of data with -semi-Poissonian noise. With the default arguments, this method simply sets the -variance attribute to the given ``expected_value``. However, more generally -(although the noise is not strictly Poissonian), the variance may be -proportional to the expected value. Moreover, when the noise is a mixture of -white (Gaussian) and Poissonian noise, the variance is described by the -following linear model: - - .. math:: - - \mathrm{Var}[X] = (a * \mathrm{E}[X] + b) * c - -Where `a` is the ``gain_factor``, `b` is the ``gain_offset`` (the Gaussian -noise variance) and `c` the ``correlation_factor``. The correlation -factor accounts for correlation of adjacent signal elements that can -be modelled as a convolution with a Gaussian point spread function. -:meth:`~.signal.BaseSignal.estimate_poissonian_noise_variance` can be used to -set the noise properties when the variance can be described by this linear -model, for example: - - -.. code-block:: python - - >>> s = hs.signals.Spectrum(np.ones(100)) - >>> s.add_poissonian_noise() - >>> s.metadata - ├── General - │ └── title = - └── Signal - └── signal_type = - - >>> s.estimate_poissonian_noise_variance() - >>> s.metadata - ├── General - │ └── title = - └── Signal - ├── Noise_properties - │ ├── Variance_linear_model - │ │ ├── correlation_factor = 1 - │ │ ├── gain_factor = 1 - │ │ └── gain_offset = 0 - │ └── variance = - └── signal_type = - -Speeding up operations ----------------------- - -Reusing a Signal for output -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Many signal methods create and return a new signal. For fast operations, the -new signal creation time is non-negligible. Also, when the operation is -repeated many times, for example in a loop, the cumulative creation time can -become significant. Therefore, many operations on -:py:class:`~.signal.BaseSignal` accept an optional argument `out`. If an -existing signal is passed to `out`, the function output will be placed into -that signal, instead of being returned in a new signal. The following example -shows how to use this feature to slice a :py:class:`~.signal.BaseSignal`. It is -important to know that the :py:class:`~.signal.BaseSignal` instance passed in -the `out` argument must be well-suited for the purpose. Often this means that -it must have the same axes and data shape as the -:py:class:`~.signal.BaseSignal` that would normally be returned by the -operation. - -.. code-block:: python - - >>> s = hs.signals.Signal1D(np.arange(10)) - >>> s_sum = s.sum(0) - >>> s_sum.data - array([45]) - >>> s.isig[:5].sum(0, out=s_sum) - >>> s_sum.data - array([10]) - >>> s_roi = s.isig[:3] - >>> s_roi - - >>> s.isig.__getitem__(slice(None, 5), out=s_roi) - >>> s_roi - - -.. _complex_data-label: - -Handling complex data ---------------------- - -The HyperSpy :py:class:`~._signals.complex_signal.ComplexSignal` signal class -and its subclasses for 1-dimensional and 2-dimensional data allow the user to -access complex properties like the ``real`` and ``imag`` parts of the data or the -``amplitude`` (also known as the modulus) and ``phase`` (also known as angle or -argument) directly. Getting and setting those properties can be done as -follows: - -.. code-block:: python - - >>> real = s.real # real is a new HS signal accessing the same data - >>> s.real = new_real # new_real can be an array or signal - >>> imag = s.imag # imag is a new HS signal accessing the same data - >>> s.imag = new_imag # new_imag can be an array or signal - -It is important to note that `data` passed to the constructor of a -:py:class:`~._signals.complex_signal.ComplexSignal` (or to a subclass), which -is not already complex, will be converted to the numpy standard of -`np.complex`/`np.complex128`. `data` which is already complex will be passed -as is. - -To transform a real signal into a complex one use: - -.. code-block:: python - - >>> s.change_dtype(complex) - -Changing the ``dtype`` of a complex signal to something real is not clearly -defined and thus not directly possible. Use the ``real``, ``imag``, -``amplitude`` or ``phase`` properties instead to extract the real data that is -desired. - - -Calculate the angle / phase / argument -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:meth:`~._signals.complex_signal.ComplexSignal.angle` function -can be used to calculate the angle, which is equivalent to using the ``phase`` -property if no argument is used. If the data is real, the angle will be 0 for -positive values and 2$\pi$ for negative values. If the `deg` parameter is set -to ``True``, the result will be given in degrees, otherwise in rad (default). -The underlying function is the :py:func:`numpy.angle` function. -:py:meth:`~._signals.complex_signal.ComplexSignal.angle` will return -an appropriate HyperSpy signal. - - -Phase unwrapping -^^^^^^^^^^^^^^^^ - -With the :py:meth:`~._signals.complex_signal.ComplexSignal.unwrapped_phase` -method the complex phase of a signal can be unwrapped and returned as a new signal. -The underlying method is :py:func:`skimage.restoration.unwrap_phase`, which -uses the algorithm described in :ref:`[Herraez] `. - - -.. _complex.argand: - -Calculate and display Argand diagram -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Sometimes it is convenient to visualize a complex signal as a plot of its -imaginary part versus real one. In this case so called Argand diagrams can -be calculated using :py:meth:`~hyperspy.signals.ComplexSignal.argand_diagram` -method, which returns the plot as a :py:class:`~._signals.signal2d.Signal2D`. -Optional arguments ``size`` and ``display_range`` can be used to change the -size (and therefore resolution) of the plot and to change the range for the -display of the plot respectively. The last one is especially useful in order to -zoom into specific regions of the plot or to limit the plot in case of noisy -data points. - -An example of calculation of Aragand diagram is :ref:`shown for electron -holography data `. - -Add a linear phase ramp -^^^^^^^^^^^^^^^^^^^^^^^ - -For 2-dimensional complex images, a linear phase ramp can be added to the -signal via the -:py:meth:`~._signals.complex_signal2d.ComplexSignal2D.add_phase_ramp` method. -The parameters ``ramp_x`` and ``ramp_y`` dictate the slope of the ramp in `x`- -and `y` direction, while the offset is determined by the ``offset`` parameter. -The fulcrum of the linear ramp is at the origin and the slopes are given in -units of the axis with the according scale taken into account. Both are -available via the :py:class:`~.axes.AxesManager` of the signal. diff --git a/doc/user_guide/signal/basic_statistical_analysis.rst b/doc/user_guide/signal/basic_statistical_analysis.rst new file mode 100644 index 0000000000..4e79d4feec --- /dev/null +++ b/doc/user_guide/signal/basic_statistical_analysis.rst @@ -0,0 +1,57 @@ +.. _signal.statistics: + +Basic statistical analysis +-------------------------- + +:meth:`~.api.signals.BaseSignal.get_histogram` computes the histogram and +conveniently returns it as signal instance. It provides methods to +calculate the bins. :meth:`~.api.signals.BaseSignal.print_summary_statistics` +prints the five-number summary statistics of the data. + +These two methods can be combined with +:meth:`~.api.signals.BaseSignal.get_current_signal` to compute the histogram or +print the summary statistics of the signal at the current coordinates, e.g: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.normal(size=(10, 100))) # doctest: +SKIP + >>> s.print_summary_statistics() # doctest: +SKIP + Summary statistics + ------------------ + mean: -0.0143 + std: 0.982 + min: -3.18 + Q1: -0.686 + median: 0.00987 + Q3: 0.653 + max: 2.57 + + >>> s.get_current_signal().print_summary_statistics() # doctest: +SKIP + Summary statistics + ------------------ + mean: -0.019 + std: 0.855 + min: -2.803 + Q1: -0.451 + median: -0.038 + Q3: 0.484 + max: 1.992 + +Histogram of different objects can be compared with the functions +:func:`~.api.plot.plot_histograms` (see +:ref:`visualisation ` for the plotting options). For example, +with histograms of several random chi-square distributions: + + +.. code-block:: python + + >>> img = hs.signals.Signal2D([np.random.chisquare(i+1,[100,100]) for + ... i in range(5)]) + >>> hs.plot.plot_histograms(img,legend='auto') + + +.. figure:: ../images/plot_histograms_chisquare.png + :align: center + :width: 500 + + Comparing histograms. diff --git a/doc/user_guide/signal/binned_signals.rst b/doc/user_guide/signal/binned_signals.rst new file mode 100644 index 0000000000..937bb453a6 --- /dev/null +++ b/doc/user_guide/signal/binned_signals.rst @@ -0,0 +1,61 @@ +.. _signal.binned: + +Binned and unbinned signals +--------------------------- + +Signals that are a histogram of a probability density function (pdf) should +have the ``is_binned`` attribute of the signal axis set to ``True``. The reason +is that some methods operate differently on signals that are *binned*. An +example of *binned* signals are EDS spectra, where the multichannel analyzer +integrates the signal counts in every channel (=bin). +Note that for 2D signals each signal axis has an ``is_binned`` +attribute that can be set independently. For example, for the first signal +axis: ``signal.axes_manager.signal_axes[0].is_binned``. + +The default value of the ``is_binned`` attribute is shown in the +following table: + +.. table:: Binned default values for the different subclasses. + + + +----------------------------------------+--------+----------+ + | BaseSignal subclass | binned | Library | + +========================================+========+==========+ + | :class:`~.api.signals.BaseSignal` | False | hyperspy | + +----------------------------------------+--------+----------+ + | :class:`~.api.signals.Signal1D` | False | hyperspy | + +----------------------------------------+--------+----------+ + | :class:`exspy.signals.EELSSpectrum` | True | exSpy | + +----------------------------------------+--------+----------+ + | :class:`exspy.signals.EDSSEMSpectrum` | True | exSpy | + +----------------------------------------+--------+----------+ + | :class:`exspy.signals.EDSTEMSpectrum` | True | exSpy | + +----------------------------------------+--------+----------+ + | :class:`~.api.signals.Signal2D` | False | hyperspy | + +----------------------------------------+--------+----------+ + | :class:`~.api.signals.ComplexSignal` | False | hyperspy | + +----------------------------------------+--------+----------+ + | :class:`~.api.signals.ComplexSignal1D` | False | hyperspy | + +----------------------------------------+--------+----------+ + | :class:`~.api.signals.ComplexSignal2D` | False | hyperspy | + +----------------------------------------+--------+----------+ + + + +To change the default value: + +.. code-block:: python + + >>> s.axes_manager[-1].is_binned = True # doctest: +SKIP + +.. versionchanged:: 1.7 The ``binned`` attribute from the metadata has been + replaced by the axis attributes ``is_binned``. + +Integration of binned signals +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For binned axes, the detector already provides the per-channel integration of +the signal. Therefore, in this case, :meth:`~.api.signals.BaseSignal.integrate1D` +performs a simple summation along the given axis. In contrast, for unbinned +axes, :meth:`~.api.signals.BaseSignal.integrate1D` calls the +:meth:`~.api.signals.BaseSignal.integrate_simpson` method. diff --git a/doc/user_guide/signal/complex_datatype.rst b/doc/user_guide/signal/complex_datatype.rst new file mode 100644 index 0000000000..8a08a14954 --- /dev/null +++ b/doc/user_guide/signal/complex_datatype.rst @@ -0,0 +1,89 @@ +.. _complex_data-label: + +Complex datatype +---------------- + +The HyperSpy :class:`~.api.signals.ComplexSignal` signal class +and its subclasses for 1-dimensional and 2-dimensional data allow the user to +access complex properties like the ``real`` and ``imag`` parts of the data or the +``amplitude`` (also known as the modulus) and ``phase`` (also known as angle or +argument) directly. Getting and setting those properties can be done as +follows: + +.. code-block:: python + + >>> s = hs.signals.ComplexSignal1D(np.arange(100) + 1j * np.arange(100)) + >>> real = s.real # real is a new HS signal accessing the same data + >>> s.real = np.random.random(100) # new_real can be an array or signal + >>> imag = s.imag # imag is a new HS signal accessing the same data + >>> s.imag = np.random.random(100) # new_imag can be an array or signal + +It is important to note that `data` passed to the constructor of a +:class:`~.api.signals.ComplexSignal` (or to a subclass), which +is not already complex, will be converted to the numpy standard of +`np.complex`/`np.complex128`. `data` which is already complex will be passed +as is. + +To transform a real signal into a complex one use: + +.. code-block:: python + + >>> s.change_dtype(complex) + +Changing the ``dtype`` of a complex signal to something real is not clearly +defined and thus not directly possible. Use the ``real``, ``imag``, +``amplitude`` or ``phase`` properties instead to extract the real data that is +desired. + + +Calculate the angle / phase / argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :meth:`~.api.signals.ComplexSignal.angle` function +can be used to calculate the angle, which is equivalent to using the ``phase`` +property if no argument is used. If the data is real, the angle will be 0 for +positive values and 2$\pi$ for negative values. If the `deg` parameter is set +to ``True``, the result will be given in degrees, otherwise in rad (default). +The underlying function is the :func:`numpy.angle` function. +:meth:`~.api.signals.ComplexSignal.angle` will return +an appropriate HyperSpy signal. + + +Phase unwrapping +^^^^^^^^^^^^^^^^ + +With the :meth:`~.api.signals.ComplexSignal.unwrapped_phase` +method the complex phase of a signal can be unwrapped and returned as a new signal. +The underlying method is :func:`skimage.restoration.unwrap_phase`, which +uses the algorithm described in :ref:`[Herraez] `. + + +.. _complex.argand: + +Calculate and display Argand diagram +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sometimes it is convenient to visualize a complex signal as a plot of its +imaginary part versus real one. In this case so called Argand diagrams can +be calculated using :meth:`~.api.signals.ComplexSignal.argand_diagram` +method, which returns the plot as a :class:`~.api.signals.Signal2D`. +Optional arguments ``size`` and ``display_range`` can be used to change the +size (and therefore resolution) of the plot and to change the range for the +display of the plot respectively. The last one is especially useful in order to +zoom into specific regions of the plot or to limit the plot in case of noisy +data points. + +An example of calculation of Aragand diagram is holospy:ref:`shown for electron +holography data `. + +Add a linear phase ramp +^^^^^^^^^^^^^^^^^^^^^^^ + +For 2-dimensional complex images, a linear phase ramp can be added to the +signal via the +:meth:`~.api.signals.ComplexSignal2D.add_phase_ramp` method. +The parameters ``ramp_x`` and ``ramp_y`` dictate the slope of the ramp in `x`- +and `y` direction, while the offset is determined by the ``offset`` parameter. +The fulcrum of the linear ramp is at the origin and the slopes are given in +units of the axis with the according scale taken into account. Both are +available via the :class:`~.axes.AxesManager` of the signal. diff --git a/doc/user_guide/signal/generic_tools.rst b/doc/user_guide/signal/generic_tools.rst new file mode 100644 index 0000000000..d491c4dcd6 --- /dev/null +++ b/doc/user_guide/signal/generic_tools.rst @@ -0,0 +1,932 @@ +Generic tools +------------- + +Below we briefly introduce some of the most commonly used tools (methods). For +more details about a particular method click on its name. For a detailed list +of all the methods available see the :class:`~.api.signals.BaseSignal` documentation. + +The methods of this section are available to all the signals. In other chapters +methods that are only available in specialized subclasses are listed. + +.. _math.operations: + +Mathematical operations +^^^^^^^^^^^^^^^^^^^^^^^ + +A number of mathematical operations are available +in :class:`~.api.signals.BaseSignal`. Most of them are just wrapped numpy +functions. + +The methods that perform mathematical operation over one or more axis at a +time are: + +* :meth:`~.api.signals.BaseSignal.sum` +* :meth:`~.api.signals.BaseSignal.max` +* :meth:`~.api.signals.BaseSignal.min` +* :meth:`~.api.signals.BaseSignal.mean` +* :meth:`~.api.signals.BaseSignal.std` +* :meth:`~.api.signals.BaseSignal.var` +* :meth:`~.api.signals.BaseSignal.nansum` +* :meth:`~.api.signals.BaseSignal.nanmax` +* :meth:`~.api.signals.BaseSignal.nanmin` +* :meth:`~.api.signals.BaseSignal.nanmean` +* :meth:`~.api.signals.BaseSignal.nanstd` +* :meth:`~.api.signals.BaseSignal.nanvar` + +Note that by default all this methods perform the operation over *all* +navigation axes. + +Example: + +.. code-block:: python + + >>> s = hs.signals.BaseSignal(np.random.random((2,4,6))) + >>> s.axes_manager[0].name = 'E' + >>> s + + >>> # by default perform operation over all navigation axes + >>> s.sum() + + >>> # can also pass axes individually + >>> s.sum('E') + + >>> # or a tuple of axes to operate on, with duplicates, by index or directly + >>> ans = s.sum((-1, s.axes_manager[1], 'E', 0)) + >>> ans + + >>> ans.axes_manager[0] + + +The following methods operate only on one axis at a time: + +* :meth:`~.api.signals.BaseSignal.diff` +* :meth:`~.api.signals.BaseSignal.derivative` +* :meth:`~.api.signals.BaseSignal.integrate_simpson` +* :meth:`~.api.signals.BaseSignal.integrate1D` +* :meth:`~.api.signals.BaseSignal.indexmin` +* :meth:`~.api.signals.BaseSignal.indexmax` +* :meth:`~.api.signals.BaseSignal.valuemin` +* :meth:`~.api.signals.BaseSignal.valuemax` + +.. _ufunc-label: + +All numpy ufunc can operate on :class:`~.api.signals.BaseSignal` +instances, for example: + +.. code-block:: python + + >>> s = hs.signals.Signal1D([0, 1]) + >>> s.metadata.General.title = "A" + >>> s + + >>> np.exp(s) + + >>> np.exp(s).data + array([1. , 2.71828183]) + >>> np.power(s, 2) + + >>> np.add(s, s) + + >>> np.add(hs.signals.Signal1D([0, 1]), hs.signals.Signal1D([0, 1])) + + + +Notice that the title is automatically updated. When the signal has no title +a new title is automatically generated: + +.. code-block:: python + + >>> np.add(hs.signals.Signal1D([0, 1]), hs.signals.Signal1D([0, 1])) + + + +Functions (other than unfucs) that operate on numpy arrays can also operate +on :class:`~.api.signals.BaseSignal` instances, however they return a numpy +array instead of a :class:`~.api.signals.BaseSignal` instance e.g.: + +.. code-block:: python + + >>> np.angle(s) + array([0., 0.]) + +.. note:: + For numerical **differentiation** and **integration**, use the proper + methods :meth:`~.api.signals.BaseSignal.derivative` and + :meth:`~.api.signals.BaseSignal.integrate1D`. In certain cases, particularly + when operating on a non-uniform axis, the approximations using the + :meth:`~.api.signals.BaseSignal.diff` and :meth:`~.api.signals.BaseSignal.sum` + methods will lead to erroneous results. + + +.. _signal.operations: + +Signal operations +^^^^^^^^^^^^^^^^^ + +:class:`~.api.signals.BaseSignal` supports all the Python binary arithmetic +operations (+, -, \*, //, %, divmod(), pow(), \*\*, <<, >>, &, ^, \|), +augmented binary assignments (+=, -=, \*=, /=, //=, %=, \*\*=, <<=, >>=, &=, +^=, \|=), unary operations (-, +, abs() and ~) and rich comparisons operations +(<, <=, ==, x!=y, <>, >, >=). + +These operations are performed element-wise. When the dimensions of the signals +are not equal `numpy broadcasting rules apply +`_ independently +for the navigation and signal axes. + +.. WARNING:: + + Hyperspy does not check if the calibration of the signals matches. + +In the following example `s2` has only one navigation axis while `s` has two. +However, because the size of their first navigation axis is the same, their +dimensions are compatible and `s2` is +broadcasted to match `s`'s dimensions. + +.. code-block:: python + + >>> s = hs.signals.Signal2D(np.ones((3,2,5,4))) + >>> s2 = hs.signals.Signal2D(np.ones((2,5,4))) + >>> s + + >>> s2 + + >>> s + s2 + + +In the following example the dimensions are not compatible and an exception +is raised. + +.. code-block:: python + + >>> s = hs.signals.Signal2D(np.ones((3,2,5,4))) + >>> s2 = hs.signals.Signal2D(np.ones((3,5,4))) + >>> s + + >>> s2 + + >>> s + s2 # doctest: +SKIP + Traceback (most recent call last): + File "", line 1, in + s + s2 + File "", line 2, in __add__ + File "/home/fjd29/Python/hyperspy/hyperspy/signal.py", line 2686, in _binary_operator_ruler + raise ValueError(exception_message) + ValueError: Invalid dimensions for this operation + + +Broadcasting operates exactly in the same way for the signal axes: + +.. code-block:: python + + >>> s = hs.signals.Signal2D(np.ones((3,2,5,4))) + >>> s2 = hs.signals.Signal1D(np.ones((3, 2, 4))) + >>> s + + >>> s2 + + >>> s + s2 + + +In-place operators also support broadcasting, but only when broadcasting would +not change the left most signal dimensions: + +.. code-block:: python + + >>> s += s2 + >>> s + + >>> s2 += s # doctest: +SKIP + Traceback (most recent call last): + File "", line 1, in + s2 += s + File "", line 2, in __iadd__ + File "/home/fjd29/Python/hyperspy/hyperspy/signal.py", line 2737, in _binary_operator_ruler + self.data = getattr(sdata, op_name)(odata) + ValueError: non-broadcastable output operand with shape (3,2,1,4) doesn\'t match the broadcast shape (3,2,5,4) + + +.. _signal.iterator: + +Iterating over the navigation axes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +BaseSignal instances are iterables over the navigation axes. For example, the +following code creates a stack of 10 images and saves them in separate "png" +files by iterating over the signal instance: + +.. code-block:: python + + >>> image_stack = hs.signals.Signal2D(np.random.randint(10, size=(2, 5, 64,64))) + >>> for single_image in image_stack: + ... single_image.save("image %s.png" % str(image_stack.axes_manager.indices)) # doctest: +SKIP + The "image (0, 0).png" file was created. + The "image (1, 0).png" file was created. + The "image (2, 0).png" file was created. + The "image (3, 0).png" file was created. + The "image (4, 0).png" file was created. + The "image (0, 1).png" file was created. + The "image (1, 1).png" file was created. + The "image (2, 1).png" file was created. + The "image (3, 1).png" file was created. + The "image (4, 1).png" file was created. + +The data of the signal instance that is returned at each iteration is a view of +the original data, a property that we can use to perform operations on the +data. For example, the following code rotates the image at each coordinate by +a given angle and uses the :func:`~.api.stack` function in combination +with `list comprehensions +`_ +to make a horizontal "collage" of the image stack: + +.. code-block:: python + + >>> import scipy.ndimage + >>> image_stack = hs.signals.Signal2D(np.array([scipy.datasets.ascent()]*5)) + >>> image_stack.axes_manager[1].name = "x" + >>> image_stack.axes_manager[2].name = "y" + >>> for image, angle in zip(image_stack, (0, 45, 90, 135, 180)): + ... image.data[:] = scipy.ndimage.rotate(image.data, angle=angle, + ... reshape=False) + >>> # clip data to integer range: + >>> image_stack.data = np.clip(image_stack.data, 0, 255) + >>> collage = hs.stack([image for image in image_stack], axis=0) + >>> collage.plot(scalebar=False) + +.. figure:: ../images/rotate_ascent.png + :align: center + :width: 500 + + Rotation of images by iteration. + +.. _map-label: + +Iterating external functions with the map method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Performing an operation on the data at each coordinate, as in the previous example, +using an external function can be more easily accomplished using the +:meth:`~.api.signals.BaseSignal.map` method: + +.. code-block:: python + + >>> import scipy.ndimage + >>> image_stack = hs.signals.Signal2D(np.array([scipy.datasets.ascent()]*4)) + >>> image_stack.axes_manager[1].name = "x" + >>> image_stack.axes_manager[2].name = "y" + >>> image_stack.map(scipy.ndimage.rotate, angle=45, reshape=False) + >>> # clip data to integer range + >>> image_stack.data = np.clip(image_stack.data, 0, 255) + >>> collage = hs.stack([image for image in image_stack], axis=0) + >>> collage.plot() + +.. figure:: ../images/rotate_ascent_apply_simple.png + :align: center + :width: 500 + + Rotation of images by the same amount using :meth:`~.api.signals.BaseSignal.map`. + +The :meth:`~.api.signals.BaseSignal.map` method can also take variable +arguments as in the following example. + +.. code-block:: python + + >>> import scipy.ndimage + >>> image_stack = hs.signals.Signal2D(np.array([scipy.datasets.ascent()]*4)) + >>> image_stack.axes_manager[1].name = "x" + >>> image_stack.axes_manager[2].name = "y" + >>> angles = hs.signals.BaseSignal(np.array([0, 45, 90, 135])) + >>> image_stack.map(scipy.ndimage.rotate, angle=angles.T, reshape=False) + +.. figure:: ../images/rotate_ascent_apply_ndkwargs.png + :align: center + :width: 500 + + Rotation of images using :meth:`~.api.signals.BaseSignal.map` with different + arguments for each image in the stack. + +.. versionadded:: 1.2.0 + ``inplace`` keyword and non-preserved output shapes + +If all function calls do not return identically-shaped results, only navigation +information is preserved, and the final result is an array where +each element corresponds to the result of the function (or arbitrary object +type). These are :ref:`ragged arrays ` and has the dtype `object`. +As such, most HyperSpy functions cannot operate on such signals, and the +data should be accessed directly. + +The ``inplace`` keyword (by default ``True``) of the +:meth:`~.api.signals.BaseSignal.map` method allows either overwriting the current +data (default, ``True``) or storing it to a new signal (``False``). + +.. code-block:: python + + >>> import scipy.ndimage + >>> image_stack = hs.signals.Signal2D(np.array([scipy.datasets.ascent()]*4)) + >>> angles = hs.signals.BaseSignal(np.array([0, 45, 90, 135])) + >>> result = image_stack.map(scipy.ndimage.rotate, + ... angle=angles.T, + ... inplace=False, + ... ragged=True, + ... reshape=True) + + >>> result + + >>> result.data.dtype + dtype('O') + >>> for d in result.data.flat: + ... print(d.shape) + (512, 512) + (724, 724) + (512, 512) + (724, 724) + +.. versionadded:: 1.4 + Iterating over signal using a parameter with no navigation dimension. + +In this case, the parameter is cyclically iterated over the navigation +dimension of the input signal. In the example below, signal s is +multiplied by a cosine parameter d, which is repeated over the +navigation dimension of s. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.rand(10, 512)) + >>> d = hs.signals.Signal1D(np.cos(np.linspace(0., 2*np.pi, 512))) + >>> s.map(lambda A, B: A * B, B=d) + + +.. _lazy_output-map-label: + +.. versionadded:: 1.7 + Get result as lazy signal + +Especially when working with very large datasets, it can be useful to +not do the computation immediately. For example if it would make you run +out of memory. In that case, the `lazy_output` parameter can be used. + +.. code-block:: python + + >>> from scipy.ndimage import gaussian_filter + >>> s = hs.signals.Signal2D(np.random.random((4, 4, 128, 128))) + >>> s_out = s.map(gaussian_filter, sigma=5, inplace=False, lazy_output=True) + >>> s_out + + +`s_out` can then be saved to a hard drive, to avoid it being loaded into memory. +Alternatively, it can be computed and loaded into memory using `s_out.compute()` + +.. code-block:: python + + >>> s_out.save("gaussian_filter_file.hspy") # doctest: +SKIP + +Another advantage of using `lazy_output=True` is the ability to "chain" operations, +by running :meth:`~.api.signals.BaseSignal.map` on the output from a previous +:meth:`~.api.signals.BaseSignal.map` operation. +For example, first running a Gaussian filter, followed by peak finding. This can +improve the computation time, and reduce the memory need. + +.. code-block:: python + + >>> s_out = s.map(scipy.ndimage.gaussian_filter, sigma=5, inplace=False, lazy_output=True) + >>> from skimage.feature import blob_dog + >>> s_out1 = s_out.map(blob_dog, threshold=0.05, inplace=False, ragged=True, lazy_output=False) + >>> s_out1 + + +This is especially relevant for very large datasets, where memory use can be a +limiting factor. + + +Cropping +^^^^^^^^ + +Cropping can be performed in a very compact and powerful way using +:ref:`signal.indexing` . In addition it can be performed using the following +method or GUIs if cropping :ref:`signal1D ` or :ref:`signal2D +`. There is also a general :meth:`~.api.signals.BaseSignal.crop` +method that operates *in place*. + + +.. _rebin-label: + +Rebinning +^^^^^^^^^ +.. versionadded:: 1.3 + :meth:`~.api.signals.BaseSignal.rebin` generalized to remove the constrain + of the ``new_shape`` needing to be a divisor of ``data.shape``. + + +The :meth:`~.api.signals.BaseSignal.rebin` methods supports rebinning the data to +arbitrary new shapes as long as the number of dimensions stays the same. +However, internally, it uses two different algorithms to perform the task. Only +when the new shape dimensions are divisors of the old shape's, the operation +supports :ref:`lazy-evaluation ` and is usually faster. +Otherwise, the operation requires linear interpolation. + +For example, the following two equivalent rebinning operations can be performed +lazily: + +.. code-block:: python + + >>> s = hs.data.two_gaussians().as_lazy() + >>> print(s) + + >>> print(s.rebin(scale=[1, 1, 2])) + + + +.. code-block:: python + + >>> s = hs.data.two_gaussians().as_lazy() + >>> print(s.rebin(new_shape=[32, 32, 512])) + + + +On the other hand, the following rebinning operation requires interpolation and +cannot be performed lazily: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.ones([4, 4, 10])) + >>> s.data[1, 2, 9] = 5 + >>> print(s) + + >>> print ('Sum = ', s.data.sum()) + Sum = 164.0 + >>> scale = [0.5, 0.5, 5] + >>> test = s.rebin(scale=scale) + >>> test2 = s.rebin(new_shape=(8, 8, 2)) # Equivalent to the above + >>> print(test) + + >>> print(test2) + + >>> print('Sum =', test.data.sum()) + Sum = 164.0 + >>> print('Sum =', test2.data.sum()) + Sum = 164.0 + >>> s.as_lazy().rebin(scale=scale) # doctest: +SKIP + Traceback (most recent call last): + File "", line 1, in + spectrum.as_lazy().rebin(scale=scale) + File "/home/fjd29/Python/hyperspy3/hyperspy/_signals/eds.py", line 184, in rebin + m = super().rebin(new_shape=new_shape, scale=scale, crop=crop, out=out) + File "/home/fjd29/Python/hyperspy3/hyperspy/_signals/lazy.py", line 246, in rebin + "Lazy rebin requires scale to be integer and divisor of the " + NotImplementedError: Lazy rebin requires scale to be integer and divisor of the original signal shape + + +The ``dtype`` argument can be used to specify the ``dtype`` of the returned +signal:: + + >>> s = hs.signals.Signal1D(np.ones((2, 5, 10), dtype=np.uint8)) + >>> print(s) + + >>> print(s.data.dtype) + uint8 + +Use ``dtype=np.unit16`` to specify a dtype:: + + >>> s2 = s.rebin(scale=(5, 2, 1), dtype=np.uint16) + >>> print(s2.data.dtype) + uint16 + +Use ``dtype="same"`` to keep the same dtype:: + + >>> s3 = s.rebin(scale=(5, 2, 1), dtype="same") + >>> print(s3.data.dtype) + uint8 + +By default ``dtype=None``, the dtype is determined by the behaviour of +numpy.sum, in this case, unsigned integer of the same precision as the +platform interger:: + + >>> s4 = s.rebin(scale=(5, 2, 1)) + >>> print(s4.data.dtype) + uint32 + + +Interpolate to a different axis +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :meth:`~.api.signals.BaseSignal.interpolate_on_axis` method makes it possible to +exchange any existing axis of a signal with a new axis, +regardless of the signals dimension or the axes types. +This is achieved by interpolating the data using :func:`scipy.interpolate.make_interp_spline` +from the old axis to the new axis. Replacing multiple axes can be done iteratively. + +.. code-block:: python + + >>> from hyperspy.axes import UniformDataAxis, DataAxis + >>> x = {"offset": 0, "scale": 1, "size": 10, "name": "X", "navigate": True} + >>> e = {"offset": 0, "scale": 1, "size": 50, "name": "E", "navigate": False} + >>> s = hs.signals.Signal1D(np.random.random((10, 50)), axes=[x, e]) + >>> s + + >>> x_new = UniformDataAxis(offset=1.5, scale=0.8, size=7, name="X_NEW", navigate=True) + >>> e_new = DataAxis(axis=np.arange(8)**2, name="E_NEW", navigate=False) + >>> s2 = s.interpolate_on_axis(x_new, 0, inplace=False) + >>> s2 + + >>> s2.interpolate_on_axis(e_new, "E", inplace=True) + >>> s2 + + + +.. _squeeze-label: + +Squeezing +^^^^^^^^^ + +The :meth:`~.api.signals.BaseSignal.squeeze` method removes any zero-dimensional +axes, i.e. axes of ``size=1``, and the attributed data dimensions from a signal. +The method returns a reduced copy of the signal and does not operate in place. + +.. code-block:: python + + >>> s = hs.signals.Signal2D(np.random.random((2, 1, 1, 6, 8, 8))) + >>> s + + >>> s = s.squeeze() + >>> s + + +Squeezing can be particularly useful after a rebinning operation that leaves +one dimension with ``shape=1``: + + >>> s = hs.signals.Signal2D(np.random.random((5,5,5,10,10))) + >>> s.rebin(new_shape=(5,1,5,5,5)) + + >>> s.rebin(new_shape=(5,1,5,5,5)).squeeze() + + + +Folding and unfolding +^^^^^^^^^^^^^^^^^^^^^ + +When dealing with multidimensional datasets it is sometimes useful to transform +the data into a two dimensional dataset. This can be accomplished using the +following two methods: + +* :meth:`~.api.signals.BaseSignal.fold` +* :meth:`~.api.signals.BaseSignal.unfold` + +It is also possible to unfold only the navigation or only the signal space: + +* :meth:`~.api.signals.BaseSignal.unfold_navigation_space` +* :meth:`~.api.signals.BaseSignal.unfold_signal_space` + + +.. _signal.stack_split: + +Splitting and stacking +^^^^^^^^^^^^^^^^^^^^^^ + +Several objects can be stacked together over an existing axis or over a +new axis using the :func:`~.api.stack` function, if they share axis +with same dimension. + +.. code-block:: python + + >>> image = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> image = hs.stack([hs.stack([image]*3,axis=0)]*3,axis=1) + >>> image.plot() + +.. figure:: ../images/stack_ascent_3_3.png + :align: center + :width: 500 + + Stacking example. + +.. note:: + + When stacking signals with large amount of + :attr:`~.api.signals.BaseSignal.original_metadata`, these metadata will be + stacked and this can lead to very large amount of metadata which can in + turn slow down processing. The ``stack_original_metadata`` argument can be + used to disable stacking :attr:`~.api.signals.BaseSignal.original_metadata`. + +An object can be split into several objects +with the :meth:`~.api.signals.BaseSignal.split` method. This function can be used +to reverse the :func:`~.api.stack` function: + +.. code-block:: python + + >>> image = image.split()[0].split()[0] + >>> image.plot() + +.. figure:: ../images/split_ascent_3_3.png + :align: center + :width: 400 + + Splitting example. + + +.. _signal.fft: + +Fast Fourier Transform (FFT) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The `fast Fourier transform `_ +of a signal can be computed using the :meth:`~.api.signals.BaseSignal.fft` method. By default, +the FFT is calculated with the origin at (0, 0), which will be displayed at the +bottom left and not in the centre of the FFT. Conveniently, the ``shift`` argument of the +the :meth:`~.api.signals.BaseSignal.fft` method can be used to center the output of the FFT. +In the following example, the FFT of a hologram is computed using ``shift=True`` and its +output signal is displayed, which shows that the FFT results in a complex signal with a +real and an imaginary parts: + +.. code-block:: python + + >>> im = hs.data.wave_image() + >>> fft_shifted = im.fft(shift=True) + >>> fft_shifted.plot() + +.. figure:: ../images/FFT_vacuum_reference_hologram.png + :align: center + :width: 800 + +The strong features in the real and imaginary parts correspond to the lattice fringes of the +hologram. + +For visual inspection of the FFT it is convenient to display its power spectrum +(i.e. the square of the absolute value of the FFT) rather than FFT itself as it is done +in the example above by using the ``power_spectum`` argument: + +.. code-block:: python + + >>> im = hs.data.wave_image() + >>> fft = im.fft(True) + >>> fft.plot(True) + +Where ``power_spectum`` is set to ``True`` since it is the first argument of the +:meth:`~.api.signals.ComplexSignal.plot` method for complex signal. +When ``power_spectrum=True``, the plot will be displayed on a log scale by default. + + +.. figure:: ../images/FFT_vacuum_reference_hologram_power_spectrum.png + :align: center + :width: 400 + +The visualisation can be further improved by setting the minimum value to display to the 30-th +percentile; this can be done by using ``vmin="30th"`` in the plot function: + +.. code-block:: python + + >>> im = hs.data.wave_image() + >>> fft = im.fft(True) + >>> fft.plot(True, vmin="30th") + +.. figure:: ../images/FFT_vacuum_reference_hologram_power_spectrum_vmin30th.png + :align: center + :width: 400 + +The streaks visible in the FFT come from the edge of the image and can be removed by +applying an `apodization `_ function to the original +signal before the computation of the FFT. This can be done using the ``apodization`` argument of +the :meth:`~.api.signals.BaseSignal.fft` method and it is usually used for visualising FFT patterns +rather than for quantitative analyses. By default, the so-called ``hann`` windows is +used but different type of windows such as the ``hamming`` and ``tukey`` windows. + +.. code-block:: python + + >>> im = hs.data.wave_image() + >>> fft = im.fft(shift=True) + >>> fft_apodized = im.fft(shift=True, apodization=True) + >>> fft_apodized.plot(True, vmin="30th") + +.. figure:: ../images/FFT_vacuum_reference_hologram_power_spectrum_vmin30th-apodization.png + :align: center + :width: 400 + + + +Inverse Fast Fourier Transform (iFFT) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Inverse fast Fourier transform can be calculated from a complex signal by using the +:meth:`~.api.signals.BaseSignal.ifft` method. Similarly to the :meth:`~.api.signals.BaseSignal.fft` method, +the ``shift`` argument can be provided to shift the origin of the iFFT when necessary: + +.. code-block:: python + + >>> im_ifft = im.fft(shift=True).ifft(shift=True) + + +.. _signal.change_dtype: + +Changing the data type +^^^^^^^^^^^^^^^^^^^^^^ + +Even if the original data is recorded with a limited dynamic range, it is often +desirable to perform the analysis operations with a higher precision. +Conversely, if space is limited, storing in a shorter data type can decrease +the file size. The :meth:`~.api.signals.BaseSignal.change_dtype` changes the data +type in place, e.g.: + +.. code-block:: python + + >>> s = hs.load('EELS Signal1D Signal2D (high-loss).dm3') # doctest: +SKIP + Title: EELS Signal1D Signal2D (high-loss).dm3 + Signal type: EELS + Data dimensions: (21, 42, 2048) + Data representation: spectrum + Data type: float32 + >>> s.change_dtype('float64') # doctest: +SKIP + >>> print(s) # doctest: +SKIP + Title: EELS Signal1D Signal2D (high-loss).dm3 + Signal type: EELS + Data dimensions: (21, 42, 2048) + Data representation: spectrum + Data type: float64 + + +In addition to all standard numpy dtypes, HyperSpy supports four extra dtypes +for RGB images **for visualization purposes only**: ``rgb8``, ``rgba8``, +``rgb16`` and ``rgba16``. This includes of course multi-dimensional RGB images. + +The requirements for changing from and to any ``rgbx`` dtype are more strict +than for most other dtype conversions. To change to a ``rgbx`` dtype the +``signal_dimension`` must be 1 and its size 3 (4) 3(4) for ``rgb`` (or +``rgba``) dtypes and the dtype must be ``uint8`` (``uint16``) for +``rgbx8`` (``rgbx16``). After conversion the ``signal_dimension`` becomes 2. + +Most operations on signals with RGB dtypes will fail. For processing simply +change their dtype to ``uint8`` (``uint16``).The dtype of images of +dtype ``rgbx8`` (``rgbx16``) can only be changed to ``uint8`` (``uint16``) and +the ``signal_dimension`` becomes 1. + +In the following example we create a 1D signal with signal size 3 and with +dtype ``uint16`` and change its dtype to ``rgb16`` for plotting. + +.. code-block:: python + + >>> rgb_test = np.zeros((1024, 1024, 3)) + >>> ly, lx = rgb_test.shape[:2] + >>> offset_factor = 0.16 + >>> size_factor = 3 + >>> Y, X = np.ogrid[0:lx, 0:ly] + >>> rgb_test[:,:,0] = (X - lx / 2 - lx*offset_factor) ** 2 + \ + ... (Y - ly / 2 - ly*offset_factor) ** 2 < \ + ... lx * ly / size_factor **2 + >>> rgb_test[:,:,1] = (X - lx / 2 + lx*offset_factor) ** 2 + \ + ... (Y - ly / 2 - ly*offset_factor) ** 2 < \ + ... lx * ly / size_factor **2 + >>> rgb_test[:,:,2] = (X - lx / 2) ** 2 + \ + ... (Y - ly / 2 + ly*offset_factor) ** 2 \ + ... < lx * ly / size_factor **2 + >>> rgb_test *= 2**16 - 1 + >>> s = hs.signals.Signal1D(rgb_test) + >>> s.change_dtype("uint16") + >>> s + + >>> s.change_dtype("rgb16") + >>> s + + >>> s.plot() + + +.. figure:: ../images/rgb_example.png + :align: center + :width: 500 + + RGB data type example. + + +.. _signal.transpose: + +Transposing (changing signal spaces) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. versionadded:: 1.1 + +:meth:`~.api.signals.BaseSignal.transpose` method changes how the dataset +dimensions are interpreted (as signal or navigation axes). By default is +swaps the signal and navigation axes. For example: + + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.zeros((4,5,6))) + >>> s + + >>> s.transpose() + + +For :meth:`~.api.signals.BaseSignal.T` is a shortcut for the default behaviour: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.zeros((4,5,6))).T + >>> s + + + +The method accepts both explicit axes to keep in either space, or just a number +of axes required in one space (just one number can be specified, as the other +is defined as "all other axes"). When axes order is not explicitly defined, +they are "rolled" from one space to the other as if the ```` wrap a circle. The example below should help clarifying this. + + +.. code-block:: python + + >>> # just create a signal with many distinct dimensions + >>> s = hs.signals.BaseSignal(np.random.rand(1, 2, 3, 4, 5, 6, 7, 8, 9)) + >>> s + + >>> s.transpose(signal_axes=5) # roll to leave 5 axes in signal space + + >>> s.transpose(navigation_axes=3) # roll leave 3 axes in navigation space + + >>> # 3 explicitly defined axes in signal space + >>> s.transpose(signal_axes=[0, 2, 6]) + + >>> # A mix of two lists, but specifying all axes explicitly + >>> # The order of axes is preserved in both lists + >>> s.transpose(navigation_axes=[1, 2, 3, 4, 5, 8], signal_axes=[0, 6, 7]) + + +A convenience functions :func:`~.api.transpose` is available to operate on +many signals at once, for example enabling plotting any-dimension signals +trivially: + +.. code-block:: python + + >>> s2 = hs.signals.BaseSignal(np.random.rand(2, 2)) # 2D signal + >>> s3 = hs.signals.BaseSignal(np.random.rand(3, 3, 3)) # 3D signal + >>> s4 = hs.signals.BaseSignal(np.random.rand(4, 4, 4, 4)) # 4D signal + >>> hs.plot.plot_images(hs.transpose(s2, s3, s4, signal_axes=2)) # doctest: +SKIP + +.. _signal.transpose_optimize: + +The :meth:`~.api.signals.BaseSignal.transpose` method accepts keyword argument +``optimize``, which is ``False`` by default, meaning modifying the output +signal data **always modifies the original data** i.e. the data is just a view +of the original data. If ``True``, the method ensures the data in memory is +stored in the most efficient manner for iterating by making a copy of the data +if required, hence modifying the output signal data **not always modifies the +original data**. + +The convenience methods :meth:`~.api.signals.BaseSignal.as_signal1D` and +:meth:`~.api.signals.BaseSignal.as_signal2D` internally use +:meth:`~.api.signals.BaseSignal.transpose`, but always optimize the data +for iteration over the navigation axes if required. Hence, these methods do not +always return a view of the original data. If a copy of the data is required +use +:meth:`~.api.signals.BaseSignal.deepcopy` on the output of any of these +methods e.g.: + +.. code-block:: python + + >>> hs.signals.Signal1D(np.zeros((4,5,6))).T.deepcopy() + + + +Applying apodization window +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Apodization window (also known as apodization function) can be applied to a signal +using :meth:`~.api.signals.BaseSignal.apply_apodization` method. By default standard +Hann window is used: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.ones(1000)) + >>> sa = s.apply_apodization() + >>> sa.metadata.General.title = 'Hann window' + >>> sa.plot() + + +.. figure:: ../images/hann_window.png + :align: center + :width: 400 + +Higher order Hann window can be used in order to keep larger fraction of intensity of original signal. +This can be done providing an integer number for the order of the window through +keyword argument ``hann_order``. (The last one works only together with default value of ``window`` argument +or with ``window='hann'``.) + +.. code-block:: python + + >>> im = hs.data.wave_image().isig[:200, :200] + >>> ima = im.apply_apodization(window='hann', hann_order=3) + >>> hs.plot.plot_images([im, ima], vmax=3000, tight_layout=True) + [, ] + + +.. figure:: ../images/hann_3d_order_ref_holo.png + :align: center + :width: 800 + +In addition to Hann window also Hamming or Tukey windows can be applied using ``window`` attribute +selecting ``'hamming'`` or ``'tukey'`` respectively. + +The shape of Tukey window can be adjusted using parameter alpha +provided through ``tukey_alpha`` keyword argument (only used when ``window='tukey'``). +The parameter represents the fraction of the window inside the cosine tapered region, +i.e. smaller is alpha larger is the middle flat region where the original signal +is preserved. If alpha is one, the Tukey window is equivalent to a Hann window. +(Default value is 0.5) + +Apodization can be applied in place by setting keyword argument ``inplace`` to ``True``. +In this case method will not return anything. diff --git a/doc/user_guide/signal/gpu.rst b/doc/user_guide/signal/gpu.rst new file mode 100644 index 0000000000..350ae84b84 --- /dev/null +++ b/doc/user_guide/signal/gpu.rst @@ -0,0 +1,34 @@ +.. _gpu_processing: + +GPU support +----------- + +.. versionadded:: 1.7 + +GPU processing is supported thanks to the numpy dispatch mechanism of array functions +- read `NEP-18 `_ +and `NEP-35 `_ +for more information. It means that most HyperSpy functions will work on a GPU +if the data is a :class:`cupy.ndarray` and the required functions are +implemented in ``cupy``. + +.. note:: + GPU processing with hyperspy requires numpy>=1.20 and dask>=2021.3.0, to be + able to use NEP-18 and NEP-35. + +.. code-block:: python + + >>> import cupy as cp # doctest: +SKIP + >>> # Create a cupy array (on GPU device) + >>> data = cp.random.random(size=(20, 20, 100, 100)) # doctest: +SKIP + >>> s = hs.signals.Signal2D(data) # doctest: +SKIP + >>> type(s.data) # doctest: +SKIP + ... cupy._core.core.ndarray + +Two convenience methods are available to transfer data between the host and +the (GPU) device memory: + +- :meth:`~.api.signals.BaseSignal.to_host` +- :meth:`~.api.signals.BaseSignal.to_device` + +For lazy processing, see the :ref:`corresponding section`. diff --git a/doc/user_guide/signal/index.rst b/doc/user_guide/signal/index.rst new file mode 100644 index 0000000000..b44323e69f --- /dev/null +++ b/doc/user_guide/signal/index.rst @@ -0,0 +1,45 @@ +.. _signal-label: + +The Signal class +**************** + +.. WARNING:: + This subsection can be a bit confusing for beginners. + Do not worry if you do not understand it all. + + +HyperSpy stores the data in the :class:`~.api.signals.BaseSignal` class, that is +the object that you get when e.g. you load a single file using +:func:`~.api.load`. Most of the data analysis functions are also contained in +this class or its specialized subclasses. The :class:`~.api.signals.BaseSignal` +class contains general functionality that is available to all the subclasses. +The subclasses provide functionality that is normally specific to a particular +type of data, e.g. the :class:`~.api.signals.Signal1D` class provides +common functionality to deal with one-dimensional (e.g. spectral) data and +:class:`exspy.signals.EELSSpectrum` (which is a subclass of +:class:`~.api.signals.Signal1D`) adds extra functionality to the +:class:`~.api.signals.Signal1D` class for electron energy-loss +spectroscopy data analysis. + +A signal store other objects in what are called attributes. For +examples, the data is stored in a numpy array in the +:attr:`~.api.signals.BaseSignal.data` attribute, the original parameters in the +:attr:`~.api.signals.BaseSignal.original_metadata` attribute, the mapped parameters +in the :attr:`~.signals.BaseSignal.metadata` attribute and the axes +information (including calibration) can be accessed (and modified) in the +:class:`~.axes.AxesManager` attribute. + + +.. toctree:: + :maxdepth: 2 + + signal_basics.rst + ragged.rst + binned_signals.rst + indexing.rst + generic_tools.rst + basic_statistical_analysis.rst + setting_noise_properties.rst + speeding_up_operation.rst + complex_datatype.rst + gpu.rst diff --git a/doc/user_guide/signal/indexing.rst b/doc/user_guide/signal/indexing.rst new file mode 100644 index 0000000000..f97a9b94f4 --- /dev/null +++ b/doc/user_guide/signal/indexing.rst @@ -0,0 +1,239 @@ +.. _signal.indexing: + +Indexing +-------- + +Indexing a :class:`~.api.signals.BaseSignal` provides a powerful, convenient and +Pythonic way to access and modify its data. In HyperSpy indexing is achieved +using :attr:`~.api.signals.BaseSignal.isig` and :attr:`~.api.signals.BaseSignal.inav`, +which allow the navigation and signal dimensions to be indexed independently. +The idea is essentially to specify a subset of the data based on its position +in the array and it is therefore essential to know the convention adopted for +specifying that position, which is described here. + +Those new to Python may find indexing a somewhat esoteric concept but once +mastered it is one of the most powerful features of Python based code and +greatly simplifies many common tasks. HyperSpy's Signal indexing is similar +to numpy array indexing and those new to Python are encouraged to read the +corresponding `numpy documentation `_. + + +Key features of indexing in HyperSpy are as follows (note that some of these +features differ from numpy): + +* HyperSpy indexing does: + + + Allow independent indexing of signal and navigation dimensions + + Support indexing with decimal numbers. + + Support indexing with units. + + Support indexing with relative coordinates i.e. 'rel0.5' + + Use the image order for indexing i.e. [x, y, z,...] (HyperSpy) vs + [..., z, y, x] (numpy) + +* HyperSpy indexing does not: + + + Support indexing using arrays. + + Allow the addition of new axes using the newaxis object. + +The examples below illustrate a range of common indexing tasks. + +First consider indexing a single spectrum, which has only one signal dimension +(and no navigation dimensions) so we use :attr:`~.api.signals.BaseSignal.isig`: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(10)) + >>> s + + >>> s.data + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> s.isig[0] + + >>> s.isig[0].data + array([0]) + >>> s.isig[9].data + array([9]) + >>> s.isig[-1].data + array([9]) + >>> s.isig[:5] + + >>> s.isig[:5].data + array([0, 1, 2, 3, 4]) + >>> s.isig[5::-1] + + >>> s.isig[5::-1] + + >>> s.isig[5::2] + + >>> s.isig[5::2].data + array([5, 7, 9]) + +Unlike numpy, HyperSpy supports indexing using decimal numbers or strings +(containing a decimal number and units), in which case +HyperSpy indexes using the axis scales instead of the indices. Additionally, +one can index using relative coordinates, for example ``'rel0.5'`` to index the +middle of the axis. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(10)) + >>> s + + >>> s.data + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> s.axes_manager[0].scale = 0.5 + >>> s.axes_manager[0].axis + array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5]) + >>> s.isig[0.5:4.].data + array([1, 2, 3, 4, 5, 6, 7]) + >>> s.isig[0.5:4].data + array([1, 2, 3]) + >>> s.isig[0.5:4:2].data + array([1, 3]) + >>> s.axes_manager[0].units = 'µm' + >>> s.isig[:'2000 nm'].data + array([0, 1, 2, 3]) + >>> s.isig[:'rel0.5'].data + array([0, 1, 2, 3]) + +Importantly the original :class:`~.api.signals.BaseSignal` and its "indexed self" +share their data and, therefore, modifying the value of the data in one +modifies the same value in the other. Note also that in the example below +s.data is used to access the data as a numpy array directly and this array is +then indexed using numpy indexing. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(10)) + >>> s + + >>> s.data + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> si = s.isig[::2] + >>> si.data + array([0, 2, 4, 6, 8]) + >>> si.data[:] = 10 + >>> si.data + array([10, 10, 10, 10, 10]) + >>> s.data + array([10, 1, 10, 3, 10, 5, 10, 7, 10, 9]) + >>> s.data[:] = 0 + >>> si.data + array([0, 0, 0, 0, 0]) + +Of course it is also possible to use the same syntax to index multidimensional +data treating navigation axes using :attr:`~.api.signals.BaseSignal.inav` +and signal axes using :attr:`~.api.signals.BaseSignal.isig`. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(2*3*4).reshape((2,3,4))) + >>> s + + >>> s.data # doctest: +SKIP + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]]]) + >>> s.axes_manager[0].name = 'x' + >>> s.axes_manager[1].name = 'y' + >>> s.axes_manager[2].name = 't' + >>> s.axes_manager.signal_axes + (,) + >>> s.axes_manager.navigation_axes + (, ) + >>> s.inav[0,0].data + array([0, 1, 2, 3]) + >>> s.inav[0,0].axes_manager + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + t | 4 | 0 | 0 | 1 | + >>> s.inav[0,0].isig[::-1].data + array([3, 2, 1, 0]) + >>> s.isig[0] + + >>> s.isig[0].axes_manager + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + x | 3 | 0 | 0 | 1 | + y | 2 | 0 | 0 | 1 | + ---------------- | ------ | ------ | ------- | ------- | ------ + >>> s.isig[0].data + array([[ 0, 4, 8], + [12, 16, 20]]) + +Independent indexation of the signal and navigation dimensions is demonstrated +further in the following: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(2*3*4).reshape((2,3,4))) + >>> s + + >>> s.data # doctest: +SKIP + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]]]) + >>> s.axes_manager[0].name = 'x' + >>> s.axes_manager[1].name = 'y' + >>> s.axes_manager[2].name = 't' + >>> s.axes_manager.signal_axes + (,) + >>> s.axes_manager.navigation_axes + (, ) + >>> s.inav[0,0].data + array([0, 1, 2, 3]) + >>> s.inav[0,0].axes_manager + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + t | 4 | 0 | 0 | 1 | + >>> s.isig[0] + + >>> s.isig[0].axes_manager + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + x | 3 | 0 | 0 | 1 | + y | 2 | 0 | 0 | 1 | + ---------------- | ------ | ------ | ------- | ------- | ------ + >>> s.isig[0].data + array([[ 0, 4, 8], + [12, 16, 20]]) + + +The same syntax can be used to set the data values in signal and navigation +dimensions respectively: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(2*3*4).reshape((2,3,4))) + >>> s + + >>> s.data # doctest: +SKIP + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]]]) + >>> s.inav[0,0].data + array([0, 1, 2, 3]) + >>> s.inav[0,0] = 1 + >>> s.inav[0,0].data + array([1, 1, 1, 1]) + >>> s.inav[0,0] = s.inav[1,1] + >>> s.inav[0,0].data + array([16, 17, 18, 19]) diff --git a/doc/user_guide/signal/ragged.rst b/doc/user_guide/signal/ragged.rst new file mode 100644 index 0000000000..eba9cb5663 --- /dev/null +++ b/doc/user_guide/signal/ragged.rst @@ -0,0 +1,100 @@ +.. _signal.ragged: + +Ragged signals +-------------- + +A ragged array (also called jagged array) is an array created with +sequences-of-sequences, where the nested sequences don't have the same length. +For example, a numpy ragged array can be created as follow: + +.. code-block:: python + + >>> arr = np.array([[1, 2, 3], [1]], dtype=object) + >>> arr + array([list([1, 2, 3]), list([1])], dtype=object) + +Note that the array shape is (2, ): + +.. code-block:: python + + >>> arr.shape + (2,) + + +Numpy ragged array must have python ``object`` type to allow the variable length of +the nested sequences - here ``[1, 2, 3]`` and ``[1]``. As explained in +`NEP-34 `_, +``dtype=object`` needs to be specified when creating the array to avoid ambiguity +about the shape of the array. + +HyperSpy supports the use of ragged array with the following conditions: + +- The signal must be explicitly defined as being :attr:`~.api.signals.BaseSignal.ragged`, either when creating + the signal or by changing the ragged attribute of the signal +- The signal dimension is the variable length dimension of the array +- The :attr:`~.api.signals.BaseSignal.isig` syntax is not supported +- Signal with ragged array can't be transposed +- Signal with ragged array can't be plotted + +To create a hyperspy signal of a numpy ragged array: + +.. code-block:: python + + >>> s = hs.signals.BaseSignal(arr, ragged=True) + >>> s + + + >>> s.ragged + True + + >>> s.axes_manager + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + | 2 | 0 | 0 | 1 | + ---------------- | ------ | ------ | ------- | ------- | ------ + Ragged axis | Variable length + +.. note:: + When possible, numpy will cast sequences-of-sequences to "non-ragged" array: + + .. code-block:: python + + >>> arr = np.array([np.array([1, 2]), np.array([1, 2])], dtype=object) + >>> arr + array([[1, 2], + [1, 2]], dtype=object) + + + Unlike in the previous example, here the array is not ragged, because + the length of the nested sequences are equal (2) and numpy will create + an array of shape (2, 2) instead of (2, ) as in the previous example of + ragged array + + .. code-block:: python + + >>> arr.shape + (2, 2) + +In addition to the use of the keyword ``ragged`` when creating an hyperspy +signal, the :attr:`~.api.signals.BaseSignal.ragged` attribute can also +be set to specify whether the signal contains a ragged array or not. + +In the following example, an hyperspy signal is created without specifying that +the array is ragged. In this case, the signal dimension is 2, which *can be* +misleading, because each item contains a list of numbers. To provide a unambiguous +representation of the fact that the signal contains a ragged array, the +:attr:`~.api.signals.BaseSignal.ragged` attribute can be set to ``True``. +By doing so, the signal space will be described as "ragged" and the navigation shape +will become the same as the shape of the ragged array: + +.. code-block:: python + + >>> arr = np.array([[1, 2, 3], [1]], dtype=object) + >>> s = hs.signals.BaseSignal(arr) + >>> s + + + >>> s.ragged = True + >>> s + diff --git a/doc/user_guide/signal/setting_noise_properties.rst b/doc/user_guide/signal/setting_noise_properties.rst new file mode 100644 index 0000000000..3f39b5ab6a --- /dev/null +++ b/doc/user_guide/signal/setting_noise_properties.rst @@ -0,0 +1,70 @@ +.. _signal.noise_properties: + +Setting the noise properties +---------------------------- + +Some data operations require the data variance. Those methods use the +``metadata.Signal.Noise_properties.variance`` attribute if it exists. You can +set this attribute as in the following example where we set the variance to be +10: + +.. code-block:: python + + >>> s.metadata.Signal.set_item("Noise_properties.variance", 10) # doctest: +SKIP + +You can also use the functions :meth:`~.api.signals.BaseSignal.set_noise_variance` +and :meth:`~.api.signals.BaseSignal.get_noise_variance` for convenience: + +.. code-block:: python + + >>> s.set_noise_variance(10) # doctest: +SKIP + >>> s.get_noise_variance() # doctest: +SKIP + 10 + +For heteroscedastic noise the ``variance`` attribute must be a +:class:`~.api.signals.BaseSignal`. Poissonian noise is a common case of +heteroscedastic noise where the variance is equal to the expected value. The +:meth:`~.api.signals.BaseSignal.estimate_poissonian_noise_variance` +method can help setting the variance of data with +semi-Poissonian noise. With the default arguments, this method simply sets the +variance attribute to the given ``expected_value``. However, more generally +(although the noise is not strictly Poissonian), the variance may be +proportional to the expected value. Moreover, when the noise is a mixture of +white (Gaussian) and Poissonian noise, the variance is described by the +following linear model: + + .. math:: + + \mathrm{Var}[X] = (a * \mathrm{E}[X] + b) * c + +Where `a` is the ``gain_factor``, `b` is the ``gain_offset`` (the Gaussian +noise variance) and `c` the ``correlation_factor``. The correlation +factor accounts for correlation of adjacent signal elements that can +be modelled as a convolution with a Gaussian point spread function. +:meth:`~.api.signals.BaseSignal.estimate_poissonian_noise_variance` can be used to +set the noise properties when the variance can be described by this linear +model, for example: + + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.ones(100)) + >>> s.add_poissonian_noise() + >>> s.metadata + ├── General + │ └── title = + └── Signal + └── signal_type = + + >>> s.estimate_poissonian_noise_variance() + >>> s.metadata + ├── General + │ └── title = + └── Signal + ├── Noise_properties + │ ├── Variance_linear_model + │ │ ├── correlation_factor = 1 + │ │ ├── gain_factor = 1 + │ │ └── gain_offset = 0 + │ └── variance = + └── signal_type = diff --git a/doc/user_guide/signal/signal_basics.rst b/doc/user_guide/signal/signal_basics.rst new file mode 100644 index 0000000000..08838e5f89 --- /dev/null +++ b/doc/user_guide/signal/signal_basics.rst @@ -0,0 +1,288 @@ +Basics of signals +----------------- + +.. _signal_initialization: + +Signal initialization +^^^^^^^^^^^^^^^^^^^^^ + +Many of the values in the :class:`~.axes.AxesManager` can be +set when making the :class:`~.api.signals.BaseSignal` object. + +.. code-block:: python + + >>> dict0 = {'size': 10, 'name':'Axis0', 'units':'A', 'scale':0.2, 'offset':1} + >>> dict1 = {'size': 20, 'name':'Axis1', 'units':'B', 'scale':0.1, 'offset':2} + >>> s = hs.signals.BaseSignal(np.random.random((10,20)), axes=[dict0, dict1]) + >>> s.axes_manager + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + Axis1 | 20 | 0 | 2 | 0.1 | B + Axis0 | 10 | 0 | 1 | 0.2 | A + +This also applies to the :attr:`~.signals.BaseSignal.metadata`. + +.. code-block:: python + + >>> metadata_dict = {'General':{'name':'A BaseSignal'}} + >>> metadata_dict['General']['title'] = 'A BaseSignal title' + >>> s = hs.signals.BaseSignal(np.arange(10), metadata=metadata_dict) + >>> s.metadata + ├── General + │ ├── name = A BaseSignal + │ └── title = A BaseSignal title + └── Signal + └── signal_type = + +Instead of using a list of *axes dictionaries* ``[dict0, dict1]`` during signal +initialization, you can also pass a list of *axes objects*: ``[axis0, axis1]``. + +The navigation and signal dimensions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HyperSpy can deal with data of arbitrary dimensions. Each dimension is +internally classified as either "navigation" or "signal" and the way this +classification is done determines the behaviour of the signal. + +The concept is probably best understood with an example: let's imagine a three +dimensional dataset e.g. a numpy array with dimensions `(10, 20, 30)`. This +dataset could be an spectrum image acquired by scanning over a sample in two +dimensions. As in this case the signal is one-dimensional we use a +:class:`~.api.signals.Signal1D` subclass for this data e.g.: + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.random((10, 20, 30))) + >>> s + + +In HyperSpy's terminology, the *signal dimension* of this dataset is 30 and +the navigation dimensions (20, 10). Notice the separator `|` between the +navigation and signal dimensions. + + +However, the same dataset could also be interpreted as an image +stack instead. Actually it could has been acquired by capturing two +dimensional images at different wavelengths. Then it would be natural to +identify the two spatial dimensions as the signal dimensions and the wavelength +dimension as the navigation dimension. To view the data in this way we could +have used a :class:`~.api.signals.Signal2D` instead e.g.: + +.. code-block:: python + + >>> im = hs.signals.Signal2D(np.random.random((10, 20, 30))) + >>> im + + +Indeed, for data analysis purposes, +one may like to operate with an image stack as if it was a set of spectra or +viceversa. One can easily switch between these two alternative ways of +classifying the dimensions of a three-dimensional dataset by +:ref:`transforming between BaseSignal subclasses +`. + +The same dataset could be seen as a three-dimensional signal: + +.. code-block:: python + + >>> td = hs.signals.BaseSignal(np.random.random((10, 20, 30))) + >>> td + + +Notice that with use :class:`~.api.signals.BaseSignal` because there is +no specialised subclass for three-dimensional data. Also note that by default +:class:`~.api.signals.BaseSignal` interprets all dimensions as signal dimensions. +We could also configure it to operate on the dataset as a three-dimensional +array of scalars by changing the default *view* of +:class:`~.api.signals.BaseSignal` by taking the transpose of it: + +.. code-block:: python + + >>> scalar = td.T + >>> scalar + + +For more examples of manipulating signal axes in the "signal-navigation" space +can be found in :ref:`signal.transpose`. + +.. NOTE:: + + Although each dimension can be arbitrarily classified as "navigation + dimension" or "signal dimension", for most common tasks there is no need to + modify HyperSpy's default choice. + + +.. _signal-subclasses: + +Signal subclasses +^^^^^^^^^^^^^^^^^ + +The :mod:`~.api.signals` module, which contains all available signal subclasses, +is imported in the user namespace when loading HyperSpy. In the following +example we create a Signal2D instance from a 2D numpy array: + +.. code-block:: python + + >>> im = hs.signals.Signal2D(np.random.random((64,64))) + >>> im + + +The :ref:`table below ` summarises all the +:class:`~.api.signals.BaseSignal` subclasses currently distributed +with HyperSpy. From HyperSpy 2.0, all domain specific signal +subclasses, characterized by the ``signal_type`` metadata attribute, are +provided by dedicated :ref:`extension packages `. + +The generic subclasses provided by HyperSpy are characterized by the the data +``dtype`` and the signal dimension. In particular, there are specialised signal +subclasses to handle complex data. See the table and diagram below. Where +appropriate, functionalities are restricted to certain +:class:`~.api.signals.BaseSignal` subclasses. + +.. _signal_overview_figure-label: + +.. figure:: ../images/HyperSpySignalOverview.png + :align: center + :width: 500 + + Diagram showing the inheritance structure of the different subclasses. The + upper part contains the generic classes shipped with HyperSpy. The lower + part contains examples of domain specific subclasses provided by some of the + :ref:`hyperspy_extensions-label`. + +.. _signal_subclasses_table-label: + +.. table:: BaseSignal subclass characteristics. + + +----------------------------------------+------------------+-------------+---------+ + | BaseSignal subclass | signal_dimension | signal_type | dtype | + +========================================+==================+=============+=========+ + | :class:`~.api.signals.BaseSignal` | - | - | real | + +----------------------------------------+------------------+-------------+---------+ + | :class:`~.api.signals.Signal1D` | 1 | - | real | + +----------------------------------------+------------------+-------------+---------+ + | :class:`~.api.signals.Signal2D` | 2 | - | real | + +----------------------------------------+------------------+-------------+---------+ + | :class:`~.api.signals.ComplexSignal` | - | - | complex | + +----------------------------------------+------------------+-------------+---------+ + | :class:`~.api.signals.ComplexSignal1D` | 1 | - | complex | + +----------------------------------------+------------------+-------------+---------+ + | :class:`~.api.signals.ComplexSignal2D` | 2 | - | complex | + +----------------------------------------+------------------+-------------+---------+ + +.. versionchanged:: 1.0 + The subclasses ``Simulation``, ``SpectrumSimulation`` and ``ImageSimulation`` + were removed. + +.. versionadded:: 1.5 + External packages can register extra :class:`~.api.signals.BaseSignal` + subclasses. + +.. versionchanged:: 2.0 + The subclasses ``EELS``, ``EDS_SEM``, ``EDS_TEM`` and + ``DielectricFunction`` have been moved to the extension package + ``exspy`` and the subclass ``hologram`` has been + moved to the extension package ``holospy``. + +.. _hyperspy_extensions-label: + +HyperSpy extensions +^^^^^^^^^^^^^^^^^^^ + +Domain specific functionalities for specific types of data are provided through +a number of dedicated python packages that qualify as `HyperSpy extensions`. These +packages provide subclasses of the generic signal classes listed above, depending +on the dimensionality and type of the data. Some examples are included in the +:ref:`diagram above `. +If an extension package is installed on your system, the provided signal +subclasses are registered with HyperSpy and these classes are directly +available when loading the ``hyperspy.api`` into the namespace. A `list of packages +that extend HyperSpy `_ +is curated in a dedicated repository. + +The metadata attribute ``signal_type`` describes the nature of the signal. It can +be any string, normally the acronym associated with a particular signal. To print +all :class:`~.api.signals.BaseSignal` subclasses available in your system call +the function :func:`~.api.print_known_signal_types` as in the following +example (assuming the extensions :external+exspy:ref:`eXSpy ` and +:external+holospy:ref:`holoSpy ` are installed): + +.. code-block:: python + + >>> hs.print_known_signal_types() # doctest: +SKIP + +--------------------+---------------------+--------------------+----------+ + | signal_type | aliases | class name | package | + +--------------------+---------------------+--------------------+----------+ + | DielectricFunction | dielectric function | DielectricFunction | exspy | + | EDS_SEM | | EDSSEMSpectrum | exspy | + | EDS_TEM | | EDSTEMSpectrum | exspy | + | EELS | TEM EELS | EELSSpectrum | exspy | + | hologram | | HologramImage | holospy | + +--------------------+---------------------+--------------------+----------+ + +When :ref:`loading data `, the ``signal_type`` will be +set automatically by the file reader, as defined in ``rosettasciio``. If the +extension providing the corresponding signal subclass is installed, +:func:`~.api.load` will return the subclass from the hyperspy extension, +otherwise a warning will be raised to explain that +no registered signal class can be assigned to the given ``signal_type``. + +Since the :func:`~.api.load` can return domain specific signal objects (e.g. +``EDSSEMSpectrum`` from ``eXSpy``) provided by extensions, the corresponding +functionalities (so-called `method` of `object` in object-oriented programming, +e.g. ``EDSSEMSpectrum.get_lines_intensity()``) implemented in signal classes of +the extension can be accessed directly. To use additional functionalities +implemented in extensions, but not as method of the signal class, the extensions +need to be imported explicitly (e.g. ``import exspy``). Check the user guides +of the respective `HyperSpy extensions +`_ for details on the +provided methods and functions. + +For details on how to write and register extensions see +:ref:`writing_extensions-label`. + +.. _transforming_signal-label: + +Transforming between signal subclasses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`~.api.signals.BaseSignal` method +:meth:`~.api.signals.BaseSignal.set_signal_type` changes the ``signal_type`` +in place, which may result in a :class:`~.api.signals.BaseSignal` subclass +transformation. + +The following example shows how to change the signal dimensionality and how +to transform between different subclasses (converting to ``EELS`` requires the +extension :external+exspy:ref:`eXSpy ` to be installed): + + .. code-block:: python + + >>> s = hs.signals.Signal1D(np.random.random((10,20,100))) + >>> s + + >>> s.metadata + ├── General + │ └── title = + └── Signal + └── signal_type = + >>> im = s.to_signal2D() + >>> im + + >>> im.metadata + ├── General + │ └── title = + └── Signal + └── signal_type = + >>> s.set_signal_type("EELS") + >>> s + + >>> s.metadata + ├── General + │ └── title = + └── Signal + └── signal_type = EELS + >>> s.change_dtype("complex") + >>> s + diff --git a/doc/user_guide/signal/speeding_up_operation.rst b/doc/user_guide/signal/speeding_up_operation.rst new file mode 100644 index 0000000000..7bc5c20953 --- /dev/null +++ b/doc/user_guide/signal/speeding_up_operation.rst @@ -0,0 +1,35 @@ +Speeding up operations +---------------------- + +Reusing a Signal for output +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Many signal methods create and return a new signal. For fast operations, the +new signal creation time is non-negligible. Also, when the operation is +repeated many times, for example in a loop, the cumulative creation time can +become significant. Therefore, many operations on +:class:`~.api.signals.BaseSignal` accept an optional argument `out`. If an +existing signal is passed to `out`, the function output will be placed into +that signal, instead of being returned in a new signal. The following example +shows how to use this feature to slice a :class:`~.api.signals.BaseSignal`. It is +important to know that the :class:`~.api.signals.BaseSignal` instance passed in +the `out` argument must be well-suited for the purpose. Often this means that +it must have the same axes and data shape as the +:class:`~.api.signals.BaseSignal` that would normally be returned by the +operation. + +.. code-block:: python + + >>> s = hs.signals.Signal1D(np.arange(10)) + >>> s_sum = s.sum(0) + >>> s_sum.data + array([45]) + >>> s.isig[:5].sum(0, out=s_sum) + >>> s_sum.data + array([10]) + >>> s_roi = s.isig[:3] + >>> s_roi + + >>> s.isig.__getitem__(slice(None, 5), out=s_roi) + >>> s_roi + diff --git a/doc/user_guide/signal1d.rst b/doc/user_guide/signal1d.rst index 73a66aa81f..dce0928f84 100644 --- a/doc/user_guide/signal1d.rst +++ b/doc/user_guide/signal1d.rst @@ -1,3 +1,4 @@ +.. _signal1D-label: Signal1D Tools ************** @@ -5,37 +6,39 @@ Signal1D Tools The methods described in this section are only available for one-dimensional signals in the Signal1D class. + .. _signal1D.crop: Cropping -------- -The :py:meth:`~._signals.signal1d.Signal1D.crop_signal1D` crops the -spectral energy range *in-place*. If no parameter is passed, a user interface +The :meth:`~.api.signals.Signal1D.crop_signal` crops the +the signal object along the signal axis (e.g. the spectral energy range) +*in-place*. If no parameter is passed, a user interface appears in which to crop the one dimensional signal. For example: .. code-block:: python - s = hs.datasets.example_signals.EDS_TEM_Spectrum() - s.crop_signal1D(5, 15) # s is cropped in place + >>> s = hs.data.two_gaussians() + >>> s.crop_signal(5, 15) # s is cropped in place Additionally, cropping in HyperSpy can be performed using the :ref:`Signal -indexing ` syntax. For example, the following crops a spectrum -to the 5 keV-15 keV region: +indexing ` syntax. For example, the following crops a signal +to the 5.0-15.0 region: .. code-block:: python - s = hs.datasets.example_signals.EDS_TEM_Spectrum() - sc = s.isig[5.:15.] # s is not cropped, sc is a "cropped view" of s + >>> s = hs.data.two_gaussians() + >>> sc = s.isig[5.:15.] # s is not cropped, sc is a "cropped view" of s It is possible to crop interactively using :ref:`roi-label`. For example: .. code-block:: python - s = hs.datasets.example_signals.EDS_TEM_Spectrum() - roi = hs.roi.SpanROI(left=5, right=15) - s.plot() - sc = roi.interactive(s) + >>> s = hs.data.two_gaussians() + >>> roi = hs.roi.SpanROI(left=5, right=15) + >>> s.plot() + >>> sc = roi.interactive(s) .. _interactive_signal1d_cropping_image: @@ -44,6 +47,7 @@ It is possible to crop interactively using :ref:`roi-label`. For example: Interactive spectrum cropping using a ROI. + .. _signal1D.remove_background: Background removal @@ -53,11 +57,11 @@ Background removal ``zero_fill`` and ``plot_remainder`` keyword arguments and big speed improvements. -The :py:meth:`~._signals.signal1d.Signal1D.remove_background` method provides +The :meth:`~.api.signals.Signal1D.remove_background` method provides background removal capabilities through both a CLI and a GUI. The GUI displays an interactive preview of the remainder after background subtraction. Currently, the following background types are supported: Doniach, Exponential, Gaussian, -Lorentzian, Polynomial, Power law (default), Offset, Skew normal, Split Voigt +Lorentzian, Polynomial, Power law (default), Offset, Skew normal, Split Voigt and Voigt. By default, the background parameters are estimated using analytical approximations (keyword argument ``fast=True``). The fast option is not accurate for most background types - except Gaussian, Offset and Power law - @@ -69,8 +73,8 @@ Example of usage: .. code-block:: python - s = hs.datasets.artificial_data.get_core_loss_eels_signal(add_powerlaw=True) - s.remove_background(zero_fill=False) + >>> s = exspy.data.EELS_MnFe(add_powerlaw=True) # doctest: +SKIP + >>> s.remove_background() # doctest: +SKIP .. figure:: images/signal_1d_remove_background.png :align: center @@ -80,12 +84,14 @@ Example of usage: figure) click inside the axes of the figure and drag to the right without releasing the button. + Calibration ----------- -The :py:meth:`~._signals.signal1d.Signal1D.calibrate` method provides a user +The :meth:`~.api.signals.Signal1D.calibrate` method provides a user interface to calibrate the spectral axis. + Alignment --------- @@ -93,19 +99,23 @@ The following methods use sub-pixel cross-correlation or user-provided shifts to align spectra. They support applying the same transformation to multiple files. -* :py:meth:`~._signals.signal1d.Signal1D.align1D` -* :py:meth:`~._signals.signal1d.Signal1D.shift1D` +* :meth:`~.api.signals.Signal1D.align1D` +* :meth:`~.api.signals.Signal1D.shift1D` + .. _integrate_1D-label: Integration ----------- -.. deprecated:: 1.3 - :py:meth:`~._signals.signal1d.Signal1D.integrate_in_range`. - It will be removed in 2.0. Use :py:meth:`~.signal.BaseSignal.integrate1D` - instead, possibly in combination with a :ref:`ROI-label` if interactivity - is required. +To integrate signals use the :meth:`~.api.signals.BaseSignal.integrate1D` method. +Possibly in combination with a :ref:`ROI-label` if interactivity is required. +Otherwise, a signal subrange for integration can also be chosen with the +:attr:`~.api.signals.BaseSignal.isig` method. + +.. code-block:: python + + >>> s.isig[0.2:0.5].integrate1D(axis=0) # doctest: +SKIP Data smoothing @@ -114,15 +124,16 @@ Data smoothing The following methods (that include user interfaces when no arguments are passed) can perform data smoothing with different algorithms: -* :py:meth:`~._signals.signal1d.Signal1D.smooth_lowess` +* :meth:`~.api.signals.Signal1D.smooth_lowess` (requires ``statsmodels`` to be installed) -* :py:meth:`~._signals.signal1d.Signal1D.smooth_tv` -* :py:meth:`~._signals.signal1d.Signal1D.smooth_savitzky_golay` +* :meth:`~.api.signals.Signal1D.smooth_tv` +* :meth:`~.api.signals.Signal1D.smooth_savitzky_golay` + Spike removal -------------- -:py:meth:`~._signals.signal1d.Signal1D.spikes_removal_tool` provides an user +:meth:`~.api.signals.Signal1D.spikes_removal_tool` provides an user interface to remove spikes from spectra. The ``derivative histogram`` allows to identify the appropriate threshold. It is possible to use this tool on a specific interval of the data by :ref:`slicing the data @@ -132,7 +143,7 @@ indices 8 and 17: .. code-block:: python >>> s = hs.signals.Signal1D(np.arange(5*10*20).reshape((5, 10, 20))) - >>> s.isig[8:17].spikes_removal_tool() + >>> s.isig[8:17].spikes_removal_tool() # doctest: +SKIP The options ``navigation_mask`` or ``signal_mask`` provide more flexibility in the @@ -142,8 +153,12 @@ to be created manually: .. code-block:: python >>> s = hs.signals.Signal1D(np.arange(5*10*20).reshape((5, 10, 20))) - >>> mask = (s.data > 50) & (s.data < 150) - >>> s.spikes_removal_tool(signal_mask=mask) + + To get a signal mask, get the mean over the navigation space + + >>> s_mean = s.mean() + >>> mask = s_mean > 495 + >>> s.spikes_removal_tool(signal_mask=mask) # doctest: +SKIP .. figure:: images/spikes_removal_tool.png :align: center @@ -156,16 +171,25 @@ Peak finding ------------ A peak finding routine based on the work of T. O'Haver is available in HyperSpy -through the :py:meth:`~._signals.signal1d.Signal1D.find_peaks1D_ohaver` +through the :meth:`~.api.signals.Signal1D.find_peaks1D_ohaver` method. +Estimate peak width +------------------- + +For asymmetric peaks, `fitted functions ` may not provide +an accurate description of the peak, in particular the peak width. The function +:meth:`~.api.signals.Signal1D.estimate_peak_width` +determines the width of a peak at a certain fraction of its maximum value. + + Other methods ------------- * Interpolate the spectra in between two positions - :py:meth:`~._signals.signal1d.Signal1D.interpolate_in_between` + :meth:`~.api.signals.Signal1D.interpolate_in_between` * Convolve the spectra with a gaussian - :py:meth:`~._signals.signal1d.Signal1D.gaussian_filter` + :meth:`~.api.signals.Signal1D.gaussian_filter` * Apply a hanning taper to the spectra - :py:meth:`~._signals.signal1d.Signal1D.hanning_taper` + :meth:`~.api.signals.Signal1D.hanning_taper` diff --git a/doc/user_guide/signal2d.rst b/doc/user_guide/signal2d.rst index 1955cafec3..e0517052cd 100644 --- a/doc/user_guide/signal2d.rst +++ b/doc/user_guide/signal2d.rst @@ -1,27 +1,29 @@ +.. _signal2d-label: + Signal2D Tools ************** The methods described in this section are only available for two-dimensional -signals in the :py:class:`~._signals.signal2d.Signal2D`. class. +signals in the :class:`~.api.signals.Signal2D`. class. .. _signal2D.align: Signal registration and alignment --------------------------------- -The :py:meth:`~._signals.signal2d.Signal2D.align2D` and -:py:meth:`~._signals.signal2d.Signal2D.estimate_shift2D` methods provide +The :meth:`~.api.signals.Signal2D.align2D` and +:meth:`~.api.signals.Signal2D.estimate_shift2D` methods provide advanced image alignment functionality. .. code-block:: python # Estimate shifts, then align the images - >>> shifts = s.estimate_shift2D() - >>> s.align2D(shifts=shifts) + >>> shifts = s.estimate_shift2D() # doctest: +SKIP + >>> s.align2D(shifts=shifts) # doctest: +SKIP # Estimate and align in a single step - >>> s.align2D() + >>> s.align2D() # doctest: +SKIP .. warning:: @@ -40,39 +42,40 @@ Sub-pixel accuracy can be achieved in two ways: .. code-block:: python # skimage upsampling method - >>> shifts = s.estimate_shift2D(sub_pixel_factor=20) + >>> shifts = s.estimate_shift2D(sub_pixel_factor=20) # doctest: +SKIP # stat method - >>> shifts = s.estimate_shift2D(reference="stat") + >>> shifts = s.estimate_shift2D(reference="stat") # doctest: +SKIP # combined upsampling and statistical method - >>> shifts = s.estimate_shift2D(reference="stat", sub_pixel_factor=20) + >>> shifts = s.estimate_shift2D(reference="stat", sub_pixel_factor=20) # doctest: +SKIP + +If you have a large stack of images, the image alignment is automatically done in +parallel. -If you have a large stack of images, you can perform the image alignment step in -parallel by passing ``parallel=True``. You can control the number of threads used -with the ``max_workers`` argument. See the :ref:`map documentation ` -for more information. +You can control the number of threads used with the ``num_workers`` argument. Or by adjusting +the :ref:`scheduler `. .. code-block:: python # Estimate shifts - >>> shifts = s.estimate_shift2D() + >>> shifts = s.estimate_shift2D() # doctest: +SKIP # Align images in parallel using 4 threads - >>> s.align2D(shifts=shifts, parallel=True, max_workers=4) + >>> s.align2D(shifts=shifts, num_workers=4) # doctest: +SKIP .. _signal2D.crop: -Cropping an image ------------------ +Cropping a Signal2D +------------------- -The :py:meth:`~._signals.signal2d.Signal2D.crop_image` method crops the +The :meth:`~.api.signals.Signal2D.crop_signal` method crops the image *in-place* e.g.: .. code-block:: python - >>> im = hs.datasets.example_signals.object_hologram() - >>> imc = im.crop(left=120, top=300, bottom=560) # im is cropped in-place + >>> im = hs.data.wave_image() + >>> im.crop_signal(left=0.5, top=0.7, bottom=2.0) # im is cropped in-place Cropping in HyperSpy is performed using the :ref:`Signal indexing @@ -80,17 +83,17 @@ Cropping in HyperSpy is performed using the :ref:`Signal indexing .. code-block:: python - >>> im = hs.datasets.example_signals.object_hologram() + >>> im = hs.data.wave_image() >>> # im is not cropped, imc is a "cropped view" of im - >>> imc = im.isig[120.:, 300.:560.] + >>> imc = im.isig[0.5:, 0.7:2.0] It is possible to crop interactively using :ref:`roi-label`. For example: .. code-block:: python - >>> im = hs.datasets.example_signals.object_hologram() - >>> roi = hs.roi.RectangularROI(left=120, right=460., top=300, bottom=560) + >>> im = hs.data.wave_image() + >>> roi = hs.roi.RectangularROI() >>> im.plot() >>> imc = roi.interactive(im) >>> imc.plot() @@ -104,17 +107,37 @@ It is possible to crop interactively using :ref:`roi-label`. For example: Interactive image cropping using a ROI. +Interactive calibration +----------------------- + +The scale can be calibrated interactively by using +:meth:`~.api.signals.Signal2D.calibrate`, which is used to +set the scale by dragging a line across some feature of known size. + +.. code-block:: python + + >>> s = hs.signals.Signal2D(np.random.random((200, 200))) + >>> s.calibrate() # doctest: +SKIP + + +The same function can also be used non-interactively. + +.. code-block:: python + + >>> s = hs.signals.Signal2D(np.random.random((200, 200))) + >>> s.calibrate(x0=1, y0=1, x1=5, y1=5, new_length=3.4, units="nm", interactive=False) + Add a linear ramp ----------------- A linear ramp can be added to the signal via the -:py:meth:`~._signals.signal2d.Signal2D.add_ramp` method. The parameters -`ramp_x` and `ramp_y` dictate the slope of the ramp in `x`- and `y` direction, -while the offset is determined by the `offset` parameter. The fulcrum of the +:meth:`~.api.signals.Signal2D.add_ramp` method. The parameters +``ramp_x`` and ``ramp_y`` dictate the slope of the ramp in ``x``- and ``y`` direction, +while the offset is determined by the ``offset`` parameter. The fulcrum of the linear ramp is at the origin and the slopes are given in units of the axis with the according scale taken into account. Both are available via the -:py:class:`~.axes.AxesManager` of the signal. +:class:`~.axes.AxesManager` of the signal. .. _peak_finding-label: @@ -123,7 +146,7 @@ Peak finding .. versionadded:: 1.6 -The :py:meth:`~._signals.signal2d.Signal2D.find_peaks` method provides access +The :meth:`~.api.signals.Signal2D.find_peaks` method provides access to a number of algorithms for peak finding in two dimensional signals. The methods available are: @@ -132,24 +155,24 @@ Maximum based peak finder .. code-block:: python - >>> s.find_peaks(method='local_max') - >>> s.find_peaks(method='max') - >>> s.find_peaks(method='minmax') + >>> s.find_peaks(method='local_max') # doctest: +SKIP + >>> s.find_peaks(method='max') # doctest: +SKIP + >>> s.find_peaks(method='minmax') # doctest: +SKIP These methods search for peaks using maximum (and minimum) values in the image. There all have a ``distance`` parameter to set the minimum distance between the peaks. -- the ``'local_max'`` method uses the :py:func:`skimage.feature.peak_local_max` +- the ``'local_max'`` method uses the :func:`skimage.feature.peak_local_max` function (``distance`` and ``threshold`` parameters are mapped to ``min_distance`` and ``threshold_abs``, respectively). - the ``'max'`` method uses the - :py:func:`~.utils.peakfinders2D.find_peaks_max` function to search + :func:`~.hyperspy.utils.peakfinders2D.find_peaks_max` function to search for peaks higher than ``alpha * sigma``, where ``alpha`` is parameters and ``sigma`` is the standard deviation of the image. It also has a ``distance`` parameters to set the minimum distance between peaks. - the ``'minmax'`` method uses the - :py:func:`~.utils.peakfinders2D.find_peaks_minmax` function to locate + :func:`~.hyperspy.utils.peakfinders2D.find_peaks_minmax` function to locate the positive peaks in an image by comparing maximum and minimum filtered images. Its ``threshold`` parameter defines the minimum difference between the maximum and minimum filtered images. @@ -159,15 +182,15 @@ Zaeferrer peak finder .. code-block:: python - >>> s.find_peaks(method='zaefferer') + >>> s.find_peaks(method='zaefferer') # doctest: +SKIP This algorithm was developed by Zaefferer :ref:`[Zaefferer2000] `. It is based on a gradient threshold followed by a local maximum search within a square window, which is moved until it is centered on the brightest point, which is taken as a peak if it is within a certain distance of the starting point. It uses the -:py:func:`~.utils.peakfinders2D.find_peaks_zaefferer` function, which can take +:func:`~.hyperspy.utils.peakfinders2D.find_peaks_zaefferer` function, which can take ``grad_threshold``, ``window_size`` and ``distance_cutoff`` as parameters. See -the :py:func:`~.utils.peakfinders2D.find_peaks_zaefferer` function documentation +the :func:`~.hyperspy.utils.peakfinders2D.find_peaks_zaefferer` function documentation for more details. Ball statistical peak finder @@ -175,15 +198,15 @@ Ball statistical peak finder .. code-block:: python - >>> s.find_peaks(method='stat') + >>> s.find_peaks(method='stat') # doctest: +SKIP Described by White :ref:`[White2009] `, this method is based on finding points that have a statistically higher value than the surrounding areas, then iterating between smoothing and binarising until the number of peaks has converged. This method can be slower than the others, but is very robust to a variety of image types. -It uses the :py:func:`~.utils.peakfinders2D.find_peaks_stat` function, which can take +It uses the :func:`~.hyperspy.utils.peakfinders2D.find_peaks_stat` function, which can take ``alpha``, ``window_radius`` and ``convergence_ratio`` as parameters. See the -:py:func:`~.utils.peakfinders2D.find_peaks_stat` function documentation for more +:func:`~.hyperspy.utils.peakfinders2D.find_peaks_stat` function documentation for more details. Matrix based peak finding @@ -191,12 +214,12 @@ Matrix based peak finding .. code-block:: python - >>> s.find_peaks(method='laplacian_of_gaussians') - >>> s.find_peaks(method='difference_of_gaussians') + >>> s.find_peaks(method='laplacian_of_gaussians') # doctest: +SKIP + >>> s.find_peaks(method='difference_of_gaussians') # doctest: +SKIP These methods are essentially wrappers around the -Laplacian of Gaussian (:py:func:`skimage.feature.blob_log`) or the difference -of Gaussian (:py:func:`skimage.feature.blob_dog`) methods, based on stacking +Laplacian of Gaussian (:func:`skimage.feature.blob_log`) or the difference +of Gaussian (:func:`skimage.feature.blob_dog`) methods, based on stacking the Laplacian/difference of images convolved with Gaussian kernels of various standard deviations. For more information, see the example in the `scikit-image documentation `_. @@ -208,11 +231,12 @@ Template matching >>> x, y = np.meshgrid(np.arange(-2, 2.5, 0.5), np.arange(-2, 2.5, 0.5)) >>> template = hs.model.components2D.Gaussian2D().function(x, y) - >>> s.find_peaks(method='template_matching', template=template) + >>> s.find_peaks(method='template_matching', template=template, interactive=False) + This method locates peaks in the cross correlation between the image and a -template using the :py:func:`~.utils.peakfinders2D.find_peaks_xc` function. See -the :py:func:`~.utils.peakfinders2D.find_peaks_xc` function documentation for +template using the :func:`~.hyperspy.utils.peakfinders2D.find_peaks_xc` function. See +the :func:`~.hyperspy.utils.peakfinders2D.find_peaks_xc` function documentation for more details. Interactive parametrization @@ -224,7 +248,7 @@ used to set to select the method and set the parameters interactively: .. code-block:: python - >>> s.find_peaks(interactive=True) + >>> s.find_peaks(interactive=True) # doctest: +SKIP Several widgets are available: diff --git a/doc/user_guide/visualisation.rst b/doc/user_guide/visualisation.rst index c89112bed0..f5fd542dd4 100644 --- a/doc/user_guide/visualisation.rst +++ b/doc/user_guide/visualisation.rst @@ -5,8 +5,8 @@ Data visualization ****************** -The object returned by :py:func:`~.io.load`, a :py:class:`~.signal.BaseSignal` -instance, has a :py:meth:`~.signal.BaseSignal.plot` method that is powerful and +The object returned by :func:`~.api.load`, a :class:`~.api.signals.BaseSignal` +instance, has a :meth:`~.api.signals.BaseSignal.plot` method that is powerful and flexible to visualize n-dimensional data. In this chapter, the visualisation of multidimensional data is exemplified with two experimental datasets: an EELS spectrum image and an EDX dataset consisting of a secondary @@ -16,8 +16,8 @@ acquired by recording two signals in parallel in a FIB/SEM. .. code-block:: python - >>> s = hs.load('YourDataFilenameHere') - >>> s.plot() + >>> s = hs.load('YourDataFilenameHere') # doctest: +SKIP + >>> s.plot() # doctest: +SKIP if the object is single spectrum or an image one window will appear when calling the plot method. @@ -61,7 +61,7 @@ on and the spectrum or navigator figure is selected**. When using the numpad arrows the PageUp and PageDown keys change the size of the step. The current coordinates can be either set by navigating the -:py:meth:`~.signal.BaseSignal.plot`, or specified by pixel indices +:meth:`~.api.signals.BaseSignal.plot`, or specified by pixel indices in ``s.axes_manager.indices`` or as calibrated coordinates in ``s.axes_manager.coordinates``. @@ -83,6 +83,10 @@ Hotkeys and modifier keys for navigating the plot can be set in the Note that some combinations will not work for all platforms, as some systems reserve them for other purposes. +If you want to jump to some point in the dataset. In that case you can hold the ``Shift`` key +and click the point you are interested in. That will automatically take you to that point in the +data. This also helps with lazy data as you don't have to load every chunk in between. + .. figure:: images/second_pointer.png :align: center :width: 500 @@ -121,17 +125,17 @@ To close all the figures run the following command: .. NOTE:: - ``plt.close('all')`` is a `matplotlib `_ command. + ``plt.close('all')`` is a `matplotlib `_ command. Matplotlib is the library that HyperSpy uses to produce the plots. You can learn how to pan/zoom and more `in the matplotlib documentation - `_ + `_ .. NOTE:: Plotting ``float16`` images is currently not supported by matplotlib; however, it is possible to convert the type of the data by using the - :py:meth:`~.signal.BaseSignal.change_dtype` method, e.g. ``s.change_dtype('float32')``. + :meth:`~.api.signals.BaseSignal.change_dtype` method, e.g. ``s.change_dtype('float32')``. Multidimensional image data =========================== @@ -186,16 +190,13 @@ Customising image plot The image plot can be customised by passing additional arguments when plotting. Colorbar, scalebar and contrast controls are HyperSpy-specific, however -`matplotlib.imshow -`_ -arguments are supported as well: +:meth:`matplotlib.axes.Axes.imshow` arguments are supported as well: .. code-block:: python >>> import scipy - >>> img = hs.signals.Signal2D(scipy.misc.ascent()) - >>> img.plot(colorbar=True, scalebar=False, - ... axes_ticks=True, cmap='RdYlBu_r') + >>> img = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> img.plot(colorbar=True, scalebar=False, axes_ticks=True, cmap='RdYlBu_r') .. figure:: images/custom_cmap.png @@ -253,6 +254,7 @@ The following example shows the effect of centring the color map: >>> data2[data2 < 0] /= 4 >>> im = hs.signals.Signal2D([data1, data2]) >>> hs.plot.plot_images(im, cmap="RdBu", tight_layout=True) + [, ] .. figure:: images/divergent_cmap.png @@ -272,8 +274,8 @@ The same example with the feature disabled: >>> data2 = data1.copy() >>> data2[data2 < 0] /= 4 >>> im = hs.signals.Signal2D([data1, data2]) - >>> hs.plot.plot_images(im, centre_colormap=False, - ... cmap="RdBu", tight_layout=True) + >>> hs.plot.plot_images(im, centre_colormap=False, cmap="RdBu", tight_layout=True) + [, ] .. figure:: images/divergent_cmap_no_centre.png @@ -285,6 +287,12 @@ The same example with the feature disabled: .. _plot.customize_navigator: +.. versionadded:: 2.0.0 + ``plot_style`` keyword argument to allow for "horizontal" or "vertical" alignment of subplots (e.g. navigator + and signal) when using the `ipympl` or `widget` backends. A default value can also be set using the + :ref:`HyperSpy plot preferences `. + + Customizing the "navigator" =========================== @@ -300,7 +308,7 @@ the section :ref:`plot.customize_images` can be passed as a dictionary to the >>> import numpy as np >>> import scipy - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) + >>> im = hs.signals.Signal2D(scipy.datasets.ascent()) >>> ims = hs.signals.BaseSignal(np.random.rand(15,13)).T * im >>> ims.metadata.General.title = 'My Images' >>> ims.plot(colorbar=False, @@ -324,31 +332,30 @@ Data files used in the following examples can be downloaded using .. code-block:: python >>> #Download the data (130MB) - >>> from urllib.request import urlretrieve, urlopen - >>> from zipfile import ZipFile + >>> from urllib.request import urlretrieve, urlopen # doctest: +SKIP + >>> from zipfile import ZipFile # doctest: +SKIP >>> files = urlretrieve("https://www.dropbox.com/s/s7cx92mfh2zvt3x/" ... "HyperSpy_demos_EDX_SEM_files.zip?raw=1", - ... "./HyperSpy_demos_EDX_SEM_files.zip") - >>> with ZipFile("HyperSpy_demos_EDX_SEM_files.zip") as z: - >>> z.extractall() + ... "./HyperSpy_demos_EDX_SEM_files.zip") # doctest: +SKIP + >>> with ZipFile("HyperSpy_demos_EDX_SEM_files.zip") as z: # doctest: +SKIP + ... z.extractall() .. NOTE:: See also the - `SEM EDS tutorials `_ . + `SEM EDS tutorials `__. .. NOTE:: The sample and the data used in this chapter are described in - P. Burdet, `et al.`, Acta Materialia, 61, p. 3090-3098 (2013) (see - `abstract `_). + :ref:`[Burdet2013] <[Burdet2013]>`. Stack of 2D images can be imported as an 3D image and plotted with a slider instead of the 2D navigator as in the previous example. .. code-block:: python - >>> img = hs.load('Ni_superalloy_0*.tif', stack=True) - >>> img.plot(navigator='slider') + >>> img = hs.load('Ni_superalloy_0*.tif', stack=True) # doctest: +SKIP + >>> img.plot(navigator='slider') # doctest: +SKIP .. figure:: images/3D_image.png @@ -363,8 +370,8 @@ plotted with sliders. .. code-block:: python - >>> s = hs.load('Ni_superalloy_0*.rpl', stack=True).as_signal1D(0) - >>> s.plot() + >>> s = hs.load('Ni_superalloy_0*.rpl', stack=True).as_signal1D(0) # doctest: +SKIP + >>> s.plot() # doctest: +SKIP .. figure:: images/3D_spectrum.png @@ -379,12 +386,14 @@ can be used as an external signal for the navigator. .. code-block:: python - >>> im = hs.load('Ni_superalloy_0*.tif', stack=True) - >>> s = hs.load('Ni_superalloy_0*.rpl', stack=True).as_signal1D(0) - >>> dim = s.axes_manager.navigation_shape - >>> #Rebin the image - >>> im = im.rebin([dim[2], dim[0], dim[1]]) - >>> s.plot(navigator=im) + >>> im = hs.load('Ni_superalloy_0*.tif', stack=True) # doctest: +SKIP + >>> s = hs.load('Ni_superalloy_0*.rpl', stack=True).as_signal1D(0) # doctest: +SKIP + >>> dim = s.axes_manager.navigation_shape # doctest: +SKIP + + Rebin the image + + >>> im = im.rebin([dim[2], dim[0], dim[1]]) # doctest: +SKIP + >>> s.plot(navigator=im) # doctest: +SKIP .. figure:: images/3D_spectrum_external.png @@ -398,8 +407,8 @@ alternative display. .. code-block:: python - >>> imgSpec = hs.load('Ni_superalloy_0*.rpl', stack=True) - >>> imgSpec.plot(navigator='spectrum') + >>> imgSpec = hs.load('Ni_superalloy_0*.rpl', stack=True) # doctest: +SKIP + >>> imgSpec.plot(navigator='spectrum') # doctest: +SKIP .. figure:: images/3D_image_spectrum.png @@ -413,9 +422,9 @@ the "maximum spectrum" for which each channel is the maximum of all pixels. .. code-block:: python - >>> imgSpec = hs.load('Ni_superalloy_0*.rpl', stack=True) - >>> specMax = imgSpec.max(-1).max(-1).max(-1).as_signal1D(0) - >>> imgSpec.plot(navigator=specMax) + >>> imgSpec = hs.load('Ni_superalloy_0*.rpl', stack=True) # doctest: +SKIP + >>> specMax = imgSpec.max(-1).max(-1).max(-1).as_signal1D(0) # doctest: +SKIP + >>> imgSpec.plot(navigator=specMax) # doctest: +SKIP .. figure:: images/3D_image_spectrum_external.png @@ -436,30 +445,29 @@ Data files used in the following examples can be downloaded using .. code-block:: python - >>> from urllib.request import urlretrieve - >>> url = 'http://cook.msm.cam.ac.uk//~hyperspy//EDS_tutorial//' - >>> urlretrieve(url + 'Ni_La_intensity.hdf5', 'Ni_La_intensity.hdf5') + >>> from urllib.request import urlretrieve # doctest: +SKIP + >>> url = 'http://cook.msm.cam.ac.uk/~hyperspy/EDS_tutorial/' # doctest: +SKIP + >>> urlretrieve(url + 'Ni_La_intensity.hdf5', 'Ni_La_intensity.hdf5') # doctest: +SKIP .. NOTE:: - See also the - `EDS tutorials `_ . + See also the `EDS tutorials `__. Although HyperSpy does not currently support plotting when signal_dimension is -greater than 2, `Mayavi `_ can be +greater than 2, `Mayavi `_ can be used for this purpose. -In the following example we also use `scikit-image `_ +In the following example we also use `scikit-image `_ for noise reduction. More details about -:py:meth:`~._signals.eds.EDSSpectrum.get_lines_intensity` method can be -found in :ref:`EDS lines intensity`. +:meth:`exspy.signals.EDSSpectrum.get_lines_intensity` method can be +found in :external+exspy:ref:`EDS lines intensity`. .. code-block:: python - >>> from mayavi import mlab - >>> ni = hs.load('Ni_La_intensity.hdf5') - >>> mlab.figure() - >>> mlab.contour3d(ni.data, contours=[85]) - >>> mlab.outline(color=(0, 0, 0)) + >>> from mayavi import mlab # doctest: +SKIP + >>> ni = hs.load('Ni_La_intensity.hdf5') # doctest: +SKIP + >>> mlab.figure() # doctest: +SKIP + >>> mlab.contour3d(ni.data, contours=[85]) # doctest: +SKIP + >>> mlab.outline(color=(0, 0, 0)) # doctest: +SKIP .. figure:: images/plot_3D_mayavi.png @@ -470,7 +478,7 @@ found in :ref:`EDS lines intensity`. .. NOTE:: See also the - `SEM EDS tutorials `_ . + `SEM EDS tutorials `__. .. NOTE:: @@ -484,19 +492,19 @@ Plotting multiple signals ========================= HyperSpy provides three functions to plot multiple signals (spectra, images or -other signals): :py:func:`~.drawing.utils.plot_images`, -:py:func:`~.drawing.utils.plot_spectra`, and -:py:func:`~.drawing.utils.plot_signals` in the ``utils.plot`` package. +other signals): :func:`~.api.plot.plot_images`, +:func:`~.api.plot.plot_spectra`, and +:func:`~.api.plot.plot_signals` in the :mod:`~.api.plot` package. .. _plot.images: Plotting several images ----------------------- -:py:func:`~.drawing.utils.plot_images` is used to plot several images in the +:func:`~.api.plot.plot_images` is used to plot several images in the same figure. It supports many configurations and has many options available to customize the resulting output. The function returns a list of -`matplotlib axes `_, +:class:`matplotlib.axes.Axes`, which can be used to further customize the figure. Some examples are given below. Plots generated from another installation may look slightly different due to ``matplotlib`` GUI backends and default font sizes. To change the @@ -504,31 +512,31 @@ font size globally, use the command ``matplotlib.rcParams.update({'font .size': 8})``. .. versionadded:: 1.5 - Add support for plotting :py:class:`~.signal.BaseSignal` with navigation + Add support for plotting :class:`~.api.signals.BaseSignal` with navigation dimension 2 and signal dimension 0. -A common usage for :py:func:`~.drawing.utils.plot_images` is to view the +A common usage for :func:`~.api.plot.plot_images` is to view the different slices of a multidimensional image (a *hyperimage*): .. code-block:: python >>> import scipy - >>> image = hs.signals.Signal2D([scipy.misc.ascent()]*6) + >>> image = hs.signals.Signal2D([scipy.datasets.ascent()]*6) >>> angles = hs.signals.BaseSignal(range(10,70,10)) >>> image.map(scipy.ndimage.rotate, angle=angles.T, reshape=False) - >>> hs.plot.plot_images(image, tight_layout=True) + >>> hs.plot.plot_images(image, tight_layout=True) # doctest: +SKIP .. figure:: images/plot_images_defaults.png :align: center :width: 500 - Figure generated with :py:func:`~.drawing.utils.plot_images` using the + Figure generated with :func:`~.api.plot.plot_images` using the default values. This example is explained in :ref:`Signal iterator`. -By default, :py:func:`~.drawing.utils.plot_images` will attempt to auto-label +By default, :func:`~.api.plot.plot_images` will attempt to auto-label the images based on the Signal titles. The labels (and title) can be customized with the `suptitle` and `label` arguments. In this example, the axes labels and the ticks are also disabled with `axes_decor`: @@ -536,22 +544,22 @@ axes labels and the ticks are also disabled with `axes_decor`: .. code-block:: python >>> import scipy - >>> image = hs.signals.Signal2D([scipy.misc.ascent()]*6) + >>> image = hs.signals.Signal2D([scipy.datasets.ascent()]*6) >>> angles = hs.signals.BaseSignal(range(10,70,10)) >>> image.map(scipy.ndimage.rotate, angle=angles.T, reshape=False) >>> hs.plot.plot_images( ... image, suptitle='Turning Ascent', axes_decor='off', ... label=['Rotation {}$^\degree$'.format(angles.data[i]) for - ... i in range(angles.data.shape[0])], colorbar=None) + ... i in range(angles.data.shape[0])], colorbar=None) # doctest: +SKIP .. figure:: images/plot_images_custom-labels.png :align: center :width: 500 - Figure generated with :py:func:`~.drawing.utils.plot_images` with customised + Figure generated with :func:`~.api.plot.plot_images` with customised labels. -:py:func:`~.drawing.utils.plot_images` can also be used to easily plot a list +:func:`~.api.plot.plot_images` can also be used to easily plot a list of `Images`, comparing different `Signals`, including RGB images. This example also demonstrates how to wrap labels using `labelwrap` (for preventing overlap) and using a single `colorbar` for all the Images, as opposed to @@ -562,23 +570,27 @@ multiple individual ones: >>> import scipy >>> import numpy as np >>> - >>> # load red channel of raccoon as an image - >>> image0 = hs.signals.Signal2D(scipy.misc.face()[:,:,0]) + + Load red channel of raccoon as an image + + >>> image0 = hs.signals.Signal2D(scipy.datasets.face()[:,:,0]) >>> image0.metadata.General.title = 'Rocky Raccoon - R' - >>> - >>> # load ascent into a length 6 hyper-image - >>> image1 = hs.signals.Signal2D([scipy.misc.ascent()]*6) + + Load ascent into a length 6 hyper-image + + >>> image1 = hs.signals.Signal2D([scipy.datasets.ascent()]*6) >>> angles = hs.signals.BaseSignal(np.arange(10,70,10)).T - >>> image1.map(scipy.ndimage.rotate, angle=angles, - ... show_progressbar=False, reshape=False) + >>> image1.map(scipy.ndimage.rotate, angle=angles, reshape=False) >>> image1.data = np.clip(image1.data, 0, 255) # clip data to int range - >>> - >>> # load green channel of raccoon as an image - >>> image2 = hs.signals.Signal2D(scipy.misc.face()[:,:,1]) + + Load green channel of raccoon as an image + + >>> image2 = hs.signals.Signal2D(scipy.datasets.face()[:,:,1]) >>> image2.metadata.General.title = 'Rocky Raccoon - G' >>> - >>> # load rgb image of the raccoon - >>> rgb = hs.signals.Signal1D(scipy.misc.face()) + Load rgb image of the raccoon + + >>> rgb = hs.signals.Signal1D(scipy.datasets.face()) >>> rgb.change_dtype("rgb8") >>> rgb.metadata.General.title = 'Raccoon - RGB' >>> @@ -588,13 +600,13 @@ multiple individual ones: ... ax[0].name, ax[1].name = 'x', 'y' ... ax[0].units, ax[1].units = 'mm', 'mm' >>> hs.plot.plot_images(images, tight_layout=True, - ... colorbar='single', labelwrap=20) + ... colorbar='single', labelwrap=20) # doctest: +SKIP .. figure:: images/plot_images_image-list.png :align: center :width: 500 - Figure generated with :py:func:`~.drawing.utils.plot_images` from a list of + Figure generated with :func:`~.api.plot.plot_images` from a list of images. Data files used in the following example can be downloaded using (These data @@ -603,40 +615,40 @@ are described in :ref:`[Rossouw2015] `. .. code-block:: python >>> #Download the data (1MB) - >>> from urllib.request import urlretrieve, urlopen - >>> from zipfile import ZipFile + >>> from urllib.request import urlretrieve, urlopen # doctest: +SKIP + >>> from zipfile import ZipFile # doctest: +SKIP >>> files = urlretrieve("https://www.dropbox.com/s/ecdlgwxjq04m5mx/" ... "HyperSpy_demos_EDS_TEM_files.zip?raw=1", - ... "./HyperSpy_demos_EDX_TEM_files.zip") + ... "./HyperSpy_demos_EDX_TEM_files.zip") # doctest: +SKIP >>> with ZipFile("HyperSpy_demos_EDX_TEM_files.zip") as z: - >>> z.extractall() + ... z.extractall() # doctest: +SKIP Another example for this function is plotting EDS line intensities see -:ref:`EDS chapter `. One can use the following commands +:external+exspy:ref:`EDS chapter in the eXSpy documentation `. +One can use the following commands to get a representative figure of the X-ray line intensities of an EDS spectrum image. This example also demonstrates changing the colormap (with `cmap`), adding scalebars to the plots (with `scalebar`), and changing the `padding` between the images. The padding is specified as a dictionary, -which is used to call subplots_adjust method of matplotlib -(see `documentation `_). +which is passed to :meth:`matplotlib.figure.Figure.subplots_adjust`. .. code-block:: python - >>> si_EDS = hs.load("core_shell.hdf5") - >>> im = si_EDS.get_lines_intensity() + >>> si_EDS = hs.load("core_shell.hdf5") # doctest: +SKIP + >>> im = si_EDS.get_lines_intensity() # doctest: +SKIP >>> hs.plot.plot_images(im, ... tight_layout=True, cmap='RdYlBu_r', axes_decor='off', ... colorbar='single', vmin='1th', vmax='99th', scalebar='all', ... scalebar_color='black', suptitle_fontsize=16, ... padding={'top':0.8, 'bottom':0.10, 'left':0.05, - ... 'right':0.85, 'wspace':0.20, 'hspace':0.10}) + ... 'right':0.85, 'wspace':0.20, 'hspace':0.10}) # doctest: +SKIP .. figure:: images/plot_images_eds.png :align: center :width: 500 - Using :py:func:`~.drawing.utils.plot_images` to plot the output of - :py:meth:`~._signals.eds.EDSSpectrum.get_lines_intensity`. + Using :func:`~.api.plot.plot_images` to plot the output of + :meth:`~.exspy.signals.EDSSpectrum.get_lines_intensity`. .. |subplots_adjust| image:: images/plot_images_subplots.png @@ -646,79 +658,78 @@ which is used to call subplots_adjust method of matplotlib |subplots_adjust| button in the GUI (button may be different when using different graphical backends). -Finally, the ``cmap`` option of :py:func:`~.drawing.utils.plot_images` +Finally, the ``cmap`` option of :func:`~.api.plot.plot_images` supports iterable types, allowing the user to specify different colormaps for the different images that are plotted by providing a list or other generator: .. code-block:: python - >>> si_EDS = hs.load("core_shell.hdf5") - >>> im = si_EDS.get_lines_intensity() + >>> si_EDS = hs.load("core_shell.hdf5") # doctest: +SKIP + >>> im = si_EDS.get_lines_intensity() # doctest: +SKIP >>> hs.plot.plot_images(im, - >>> tight_layout=True, cmap=['viridis', 'plasma'], axes_decor='off', - >>> colorbar='multi', vmin='1th', vmax='99th', scalebar=[0], - >>> scalebar_color='white', suptitle_fontsize=16) + ... tight_layout=True, cmap=['viridis', 'plasma'], axes_decor='off', + ... colorbar='multi', vmin='1th', vmax='99th', scalebar=[0], + ... scalebar_color='white', suptitle_fontsize=16) # doctest: +SKIP .. figure:: images/plot_images_eds_cmap_list.png :align: center :width: 500 - Using :py:func:`~.drawing.utils.plot_images` to plot the output of - :py:meth:`~._signals.eds.EDSSpectrum.get_lines_intensity` using a unique + Using :func:`~.api.plot.plot_images` to plot the output of + :meth:`~.exspy.signals.EDSSpectrum.get_lines_intensity` using a unique colormap for each image. The ``cmap`` argument can also be given as ``'mpl_colors'``, and as a result, the images will be plotted with colormaps generated from the default ``matplotlib`` colors, which is very helpful when plotting multiple spectral signals and their relative intensities (such as the results of a -:py:func:`~.learn.mva.decomposition` analysis). This example uses -:py:func:`~.drawing.utils.plot_spectra`, which is explained in the +:meth:`~.api.signals.BaseSignal.decomposition` analysis). This example uses +:func:`~.api.plot.plot_spectra`, which is explained in the `next section`__. __ plot.spectra_ .. code-block:: python - >>> si_EDS = hs.load("core_shell.hdf5") - >>> si_EDS.change_dtype('float') - >>> si_EDS.decomposition(True, algorithm='NMF', output_dimension=3) - >>> factors = si_EDS.get_decomposition_factors() + >>> si_EDS = hs.load("core_shell.hdf5") # doctest: +SKIP + >>> si_EDS.change_dtype('float') # doctest: +SKIP + >>> si_EDS.decomposition(True, algorithm='NMF', output_dimension=3) # doctest: +SKIP + >>> factors = si_EDS.get_decomposition_factors() # doctest: +SKIP >>> >>> # the first factor is a very strong carbon background component, so we >>> # normalize factor intensities for easier qualitative comparison >>> for f in factors: - >>> f.data /= f.data.max() + ... f.data /= f.data.max() # doctest: +SKIP >>> - >>> loadings = si_EDS.get_decomposition_loadings() + >>> loadings = si_EDS.get_decomposition_loadings() # doctest: +SKIP >>> - >>> hs.plot.plot_spectra(factors.isig[:14.0], style='cascade', - >>> padding=-1) + >>> hs.plot.plot_spectra(factors.isig[:14.0], style='cascade', padding=-1) # doctest: +SKIP >>> >>> # add some lines to nicely label the peak positions - >>> plt.axvline(6.403, c='C2', ls=':', lw=0.5) - >>> plt.text(x=6.503, y=0.85, s='Fe-K$_\\alpha$', color='C2') - >>> plt.axvline(9.441, c='C1', ls=':', lw=0.5) - >>> plt.text(x=9.541, y=0.85, s='Pt-L$_\\alpha$', color='C1') - >>> plt.axvline(2.046, c='C1', ls=':', lw=0.5) - >>> plt.text(x=2.146, y=0.85, s='Pt-M', color='C1') - >>> plt.axvline(8.040, ymax=0.8, c='k', ls=':', lw=0.5) - >>> plt.text(x=8.14, y=0.35, s='Cu-K$_\\alpha$', color='k') + >>> plt.axvline(6.403, c='C2', ls=':', lw=0.5) # doctest: +SKIP + >>> plt.text(x=6.503, y=0.85, s='Fe-K$_\\alpha$', color='C2') # doctest: +SKIP + >>> plt.axvline(9.441, c='C1', ls=':', lw=0.5) # doctest: +SKIP + >>> plt.text(x=9.541, y=0.85, s='Pt-L$_\\alpha$', color='C1') # doctest: +SKIP + >>> plt.axvline(2.046, c='C1', ls=':', lw=0.5) # doctest: +SKIP + >>> plt.text(x=2.146, y=0.85, s='Pt-M', color='C1') # doctest: +SKIP + >>> plt.axvline(8.040, ymax=0.8, c='k', ls=':', lw=0.5) # doctest: +SKIP + >>> plt.text(x=8.14, y=0.35, s='Cu-K$_\\alpha$', color='k') # doctest: +SKIP >>> >>> hs.plot.plot_images(loadings, cmap='mpl_colors', - >>> axes_decor='off', per_row=1, - >>> label=['Background', 'Pt core', 'Fe shell'], - >>> scalebar=[0], scalebar_color='white', - >>> padding={'top': 0.95, 'bottom': 0.05, - >>> 'left': 0.05, 'right':0.78}) + ... axes_decor='off', per_row=1, + ... label=['Background', 'Pt core', 'Fe shell'], + ... scalebar=[0], scalebar_color='white', + ... padding={'top': 0.95, 'bottom': 0.05, + ... 'left': 0.05, 'right':0.78}) # doctest: +SKIP .. figure:: images/plot_images_eds_cmap_factors_side_by_side.png :align: center :width: 500 - Using :py:func:`~.drawing.utils.plot_images` with ``cmap='mpl_colors'`` - together with :py:func:`~.drawing.utils.plot_spectra` to visualize the + Using :func:`~.api.plot.plot_images` with ``cmap='mpl_colors'`` + together with :func:`~.api.plot.plot_spectra` to visualize the output of a non-negative matrix factorization of the EDS data. @@ -737,9 +748,9 @@ __ plot.spectra_ functionality is only enabled if a ``matplotlib`` backend that supports the ``button_press_event`` in the figure canvas is being used. -It is also possible to plot multiple images overlayed on the same figure by -passing the argument ``overlay=True`` to the -:py:func:`~.drawing.utils.plot_images` function. This should only be done when +It is also possible to plot multiple images overlayed on the same figure by +passing the argument ``overlay=True`` to the +:func:`~.api.plot.plot_images` function. This should only be done when images have the same scale (eg. for elemental maps from the same dataset). Using the same data as above, the Fe and Pt signals can be plotted using different colours. Any color can be input via matplotlib color characters or @@ -747,10 +758,10 @@ hex values. .. code-block:: python - >>> si_EDS = hs.load("core_shell.hdf5") - >>> im = si_EDS.get_lines_intensity() - >>> hs.plot.plot_images(im,scalebar='all', overlay=True, suptitle=False, - >>> axes_decor='off') + >>> si_EDS = hs.load("core_shell.hdf5") # doctest: +SKIP + >>> im = si_EDS.get_lines_intensity() # doctest: +SKIP + >>> hs.plot.plot_images(im,scalebar='all', overlay=True, suptitle=False, + ... axes_decor='off') # doctest: +SKIP .. figure:: images/plot_images_overlay.png :align: center @@ -761,17 +772,17 @@ hex values. Plotting several spectra ------------------------ -:py:func:`~.drawing.utils.plot_spectra` is used to plot several spectra in the +:func:`~.api.plot.plot_spectra` is used to plot several spectra in the same figure. It supports different styles, the default being "overlap". .. versionadded:: 1.5 - Add support for plotting :py:class:`~.signal.BaseSignal` with navigation + Add support for plotting :class:`~.api.signals.BaseSignal` with navigation dimension 1 and signal dimension 0. In the following example we create a list of 9 single spectra (gaussian functions with different sigma values) and plot them in the same figure using -:py:func:`~.drawing.utils.plot_spectra`. Note that, in this case, the legend +:func:`~.api.plot.plot_spectra`. Note that, in this case, the legend labels are taken from the individual spectrum titles. By clicking on the legended line, a spectrum can be toggled on and off. @@ -793,14 +804,14 @@ legended line, a spectrum can be toggled on and off. ... gaussians.append(gs) ... >>> hs.plot.plot_spectra(gaussians,legend='auto') - + .. figure:: images/plot_spectra_overlap.png :align: center :width: 500 - Figure generated by :py:func:`~.drawing.utils.plot_spectra` using the + Figure generated by :func:`~.api.plot.plot_spectra` using the `overlap` style. @@ -812,16 +823,16 @@ a file: .. code-block:: python - >>> import scipy.misc - >>> s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) + >>> import scipy + >>> s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10]) >>> cascade_plot = hs.plot.plot_spectra(s, style='cascade') - >>> cascade_plot.figure.savefig("cascade_plot.png") + >>> cascade_plot.figure.savefig("cascade_plot.png") # doctest: +SKIP .. figure:: images/plot_spectra_cascade.png :align: center :width: 350 - Figure generated by :py:func:`~.drawing.utils.plot_spectra` using the + Figure generated by :func:`~.api.plot.plot_spectra` using the `cascade` style. The "cascade" `style` has a `padding` option. The default value, 1, keeps the @@ -834,18 +845,19 @@ and provide the legend labels: .. code-block:: python - >>> import scipy.misc - >>> s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) + >>> import scipy + >>> s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10]) >>> color_list = ['red', 'red', 'blue', 'blue', 'red', 'red'] - >>> line_style_list = ['-','--','steps','-.',':','-'] + >>> linestyle_list = ['-', '--', '-.', ':', '-'] >>> hs.plot.plot_spectra(s, style='cascade', color=color_list, - >>> line_style=line_style_list,legend='auto') + ... linestyle=linestyle_list, legend='auto') + .. figure:: images/plot_spectra_color.png :align: center :width: 350 - Customising the line colors in :py:func:`~.drawing.utils.plot_spectra`. + Customising the line colors in :func:`~.api.plot.plot_spectra`. A simple extension of this functionality is to customize the colormap that @@ -854,70 +866,78 @@ generate a list of colors that follows a certain colormap: .. code-block:: python - >>> import scipy.misc + >>> import scipy >>> fig, axarr = plt.subplots(1,2) - >>> s1 = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) - >>> s2 = hs.signals.Signal1D(scipy.misc.ascent()[200:260:10]) + >>> s1 = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10]) + >>> s2 = hs.signals.Signal1D(scipy.datasets.ascent()[200:260:10]) >>> hs.plot.plot_spectra(s1, ... style='cascade', ... color=[plt.cm.RdBu(i/float(len(s1)-1)) ... for i in range(len(s1))], ... ax=axarr[0], ... fig=fig) + >>> hs.plot.plot_spectra(s2, ... style='cascade', ... color=[plt.cm.summer(i/float(len(s1)-1)) ... for i in range(len(s1))], ... ax=axarr[1], ... fig=fig) + >>> axarr[0].set_xlabel('RdBu (colormap)') + Text(0.5, 0, 'RdBu (colormap)') >>> axarr[1].set_xlabel('summer (colormap)') - >>> fig.canvas.draw() + Text(0.5, 0, 'summer (colormap)') + .. figure:: images/plot_spectra_colormap.png :align: center :width: 500 - Customising the line colors in :py:func:`~.drawing.utils.plot_spectra` using + Customising the line colors in :func:`~.api.plot.plot_spectra` using a colormap. There are also two other styles, "heatmap" and "mosaic": .. code-block:: python - >>> import scipy.misc - >>> s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) + >>> import scipy + >>> s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10]) >>> hs.plot.plot_spectra(s, style='heatmap') + .. figure:: images/plot_spectra_heatmap.png :align: center :width: 500 - Figure generated by :py:func:`~.drawing.utils.plot_spectra` using the + Figure generated by :func:`~.api.plot.plot_spectra` using the `heatmap` style. .. code-block:: python - >>> import scipy.misc - >>> s = hs.signals.Signal1D(scipy.misc.ascent()[100:120:10]) + >>> import scipy + >>> s = hs.signals.Signal1D(scipy.datasets.ascent()[100:120:10]) >>> hs.plot.plot_spectra(s, style='mosaic') + array([, + ], + dtype=object) .. figure:: images/plot_spectra_mosaic.png :align: center :width: 350 - Figure generated by :py:func:`~.drawing.utils.plot_spectra` using the + Figure generated by :func:`~.api.plot.plot_spectra` using the `mosaic` style. For the "heatmap" style, different -`matplotlib color schemes `_ +`matplotlib color schemes `_ can be used: .. code-block:: python >>> import matplotlib.cm - >>> import scipy.misc - >>> s = hs.signals.Signal1D(scipy.misc.ascent()[100:120:10]) + >>> import scipy + >>> s = hs.signals.Signal1D(scipy.datasets.ascent()[100:120:10]) >>> ax = hs.plot.plot_spectra(s, style="heatmap") >>> ax.images[0].set_cmap(matplotlib.cm.plasma) @@ -925,7 +945,7 @@ can be used: :align: center :width: 500 - Figure generated by :py:func:`~.drawing.utils.plot_spectra` using the + Figure generated by :func:`~.api.plot.plot_spectra` using the `heatmap` style showing how to customise the color map. Any parameter that can be passed to matplotlib.pyplot.figure can also be used @@ -936,17 +956,19 @@ directly to matplotlib.pyplot.figure as keyword arguments: .. code-block:: python - >>> import scipy.misc - >>> s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) + >>> import scipy + >>> s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10]) >>> legendtext = ['Plot 0', 'Plot 1', 'Plot 2', 'Plot 3', ... 'Plot 4', 'Plot 5'] >>> cascade_plot = hs.plot.plot_spectra( ... s, style='cascade', legend=legendtext, dpi=60, ... facecolor='lightblue', frameon=True, num=5) >>> cascade_plot.set_xlabel("X-axis") + Text(0.5, 0, 'X-axis') >>> cascade_plot.set_ylabel("Y-axis") + Text(0, 0.5, 'Y-axis') >>> cascade_plot.set_title("Cascade plot") - >>> plt.draw() + Text(0.5, 1.0, 'Cascade plot') .. figure:: images/plot_spectra_kwargs.png :align: center @@ -959,13 +981,15 @@ the figure: .. code-block:: python - >>> import scipy.misc - >>> s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) + >>> import scipy + >>> s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10]) >>> cascade_plot = hs.plot.plot_spectra(s) >>> cascade_plot.set_xlabel("An axis") + Text(0.5, 0, 'An axis') >>> cascade_plot.set_ylabel("Another axis") + Text(0, 0.5, 'Another axis') >>> cascade_plot.set_title("A title!") - >>> plt.draw() + Text(0.5, 1.0, 'A title!') .. figure:: images/plot_spectra_customize.png :align: center @@ -979,15 +1003,16 @@ and "overlap" styles: .. code-block:: python - >>> import scipy.misc + >>> import scipy >>> fig, axarr = plt.subplots(1,2) - >>> s1 = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) - >>> s2 = hs.signals.Signal1D(scipy.misc.ascent()[200:260:10]) + >>> s1 = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10]) + >>> s2 = hs.signals.Signal1D(scipy.datasets.ascent()[200:260:10]) >>> hs.plot.plot_spectra(s1, style='cascade', ... color='blue', ax=axarr[0], fig=fig) + >>> hs.plot.plot_spectra(s2, style='cascade', ... color='red', ax=axarr[1], fig=fig) - >>> fig.canvas.draw() + .. figure:: images/plot_spectra_ax_argument.png :align: center @@ -996,51 +1021,20 @@ and "overlap" styles: Plotting on existing matplotlib axes. -.. _plot_profiles_interactive-label: - -Plotting profiles interactively -------------------------------- - -Spectra or line profile can be plotted interactively on the same figure using -the :py:func:`~.drawing.utils.plot_spectra` function. For example, profiles -obtained from different Signal2D using the :py:class:`~.roi.Line2DROI` ROI can -be plotted interactively: - -.. code-block:: python - - >>> im0 = hs.datasets.example_signals.reference_hologram() - >>> im1 = hs.datasets.example_signals.object_hologram() - >>> im0.plot() - >>> im1.plot() - >>> # Create the ROI - >>> line_profile = hs.roi.Line2DROI(400, 250, 220, 600) - >>> # Obtain the signals to plot by "slicing" the signals with the ROI - >>> line0 = line_profile.interactive(im0) - >>> line1 = line_profile.interactive(im1) - >>> # Plotting the profile on the same figure - >>> hs.plot.plot_spectra([line0, line1]) - -.. figure:: images/interactive_profiles.gif - :align: center - :width: 1024 - - Plotting profiles from different images interactively. - - .. _plot.signals: Plotting several signals ^^^^^^^^^^^^^^^^^^^^^^^^ -:py:func:`~.drawing.utils.plot_signals` is used to plot several signals at the +:func:`~.api.plot.plot_signals` is used to plot several signals at the same time. By default the navigation position of the signals will be synced, and the signals must have the same dimensions. To plot two spectra at the same time: .. code-block:: python - >>> import scipy.misc - >>> s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0).inav[:,:3] + >>> import scipy + >>> s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0).inav[:,:3] >>> s2 = s1.deepcopy()*-1 >>> hs.plot.plot_signals([s1, s2]) @@ -1048,7 +1042,7 @@ same time: :align: center :width: 500 - The :py:func:`~.drawing.utils.plot_signals` plots several signals with + The :func:`~.api.plot.plot_signals` plots several signals with optional synchronized navigation. The navigator can be specified by using the navigator argument, where the @@ -1059,16 +1053,16 @@ To specify the navigator: .. code-block:: python - >>> import scipy.misc - >>> s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0).inav[:,:3] + >>> import scipy + >>> s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0).inav[:,:3] >>> s2 = s1.deepcopy()*-1 - >>> hs.plot.plot_signals([s1, s2], navigator="slider") + >>> hs.plot.plot_signals([s1, s2], navigator="slider") # doctest: +SKIP .. figure:: images/plot_signals_slider.png :align: center :width: 500 - Customising the navigator in :py:func:`~.drawing.utils.plot_signals`. + Customising the navigator in :func:`~.api.plot.plot_signals`. Navigators can also be set differently for different plots using the navigator_list argument. Where the navigator_list be the same length @@ -1077,17 +1071,17 @@ For example: .. code-block:: python - >>> import scipy.misc - >>> s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0).inav[:,:3] + >>> import scipy + >>> s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0).inav[:,:3] >>> s2 = s1.deepcopy()*-1 >>> s3 = hs.signals.Signal1D(np.linspace(0,9,9).reshape([3,3])) - >>> hs.plot.plot_signals([s1, s2], navigator_list=["slider", s3]) + >>> hs.plot.plot_signals([s1, s2], navigator_list=["slider", s3]) # doctest: +SKIP .. figure:: images/plot_signals_navigator_list.png :align: center :width: 500 - Customising the navigator in :py:func:`~.drawing.utils.plot_signals` by + Customising the navigator in :func:`~.api.plot.plot_signals` by providing a navigator list. Several signals can also be plotted without syncing the navigation by using @@ -1096,32 +1090,32 @@ each plot: .. code-block:: python - >>> import scipy.misc - >>> s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0)[:,:3] + >>> import scipy + >>> s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0).inav[:,:3] >>> s2 = s1.deepcopy()*-1 - >>> hs.plot.plot_signals([s1, s2], sync=False, - ... navigator_list=["slider", "slider"]) + >>> hs.plot.plot_signals([s1, s2], sync=False, navigator_list=["slider", "slider"]) # doctest: +SKIP .. figure:: images/plot_signals_sync.png :align: center :width: 500 - Disabling syncronised navigation in :py:func:`~.drawing.utils.plot_signals`. + Disabling syncronised navigation in :func:`~.api.plot.plot_signals`. .. _plot.markers: Markers ======= -HyperSpy provides an easy access to the main marker of matplotlib. The markers -can be used in a static way +HyperSpy provides an easy access the collections classes of matplotlib. These markers provide +powerful ways to annotate high dimensional datasets easily. .. code-block:: python - >>> import scipy.misc - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) - >>> m = hs.plot.markers.rectangle(x1=150, y1=100, - ... x2=400, y2=400, color='red') + >>> import scipy + >>> im = hs.signals.Signal2D(scipy.datasets.ascent()) + >>> m = hs.plot.markers.Rectangles( + ... offsets=[[275, 250],], widths= [250,], + ... heights=[300],color="red", facecolor="none") >>> im.add_marker(m) .. figure:: images/plot_markers_std.png @@ -1137,15 +1131,12 @@ for each R, G and B channel of a colour image. .. code-block:: python >>> from skimage.feature import peak_local_max - >>> import scipy.misc - >>> ims = hs.signals.BaseSignal(scipy.misc.face()).as_signal2D([1,2]) - >>> index = np.array([peak_local_max(im.data, min_distance=100, - ... num_peaks=4) - ... for im in ims]) - >>> for i in range(4): - ... m = hs.plot.markers.point(x=index[:, i, 1], - ... y=index[:, i, 0], color='red') - ... ims.add_marker(m) + >>> import scipy + >>> ims = hs.signals.BaseSignal(scipy.datasets.face()).as_signal2D([1,2]) + >>> index = ims.map(peak_local_max,min_distance=100, + ... num_peaks=4, inplace=False, ragged=True) + >>> m = hs.plot.markers.Points.from_signal(index, color='red') + >>> ims.add_marker(m) .. figure:: images/plot_markers_im.gif @@ -1154,22 +1145,27 @@ for each R, G and B channel of a colour image. Point markers in image. -The markers can be added to the navigator as well. In the following example, +Markers can be added to the navigator as well. In the following example, each slice of a 2D spectrum is tagged with a text marker on the signal plot. Each slice is indicated with the same text on the navigator. .. code-block:: python + >>> import numpy as np >>> s = hs.signals.Signal1D(np.arange(100).reshape([10,10])) >>> s.plot(navigator='spectrum') - >>> for i in range(s.axes_manager.shape[0]): - ... m = hs.plot.markers.text(y=s.sum(-1).data[i]+5, - ... x=i, text='abcdefghij'[i]) - ... s.add_marker(m, plot_on_signal=False) - >>> x = s.axes_manager.shape[-1]/2 #middle of signal plot - >>> m = hs.plot.markers.text(x=x, y=s.isig[x].data+2, - ... text=[i for i in 'abcdefghij']) - >>> s.add_marker(m) + >>> offsets = [[i, s.sum(-1).data[i]+5] for i in range(s.axes_manager.shape[0])] + >>> text = 'abcdefghij' + >>> m = hs.plot.markers.Texts(offsets=offsets, texts=[*text], verticalalignment="bottom") + >>> s.add_marker(m, plot_on_signal=False) + >>> offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + >>> texts = np.empty(s.axes_manager.navigation_shape, dtype=object) + >>> x = 4 + >>> for i in range(10): + ... offsets[i] = [[x, int(s.inav[i].isig[x].data)],] + ... texts[i] = np.array([text[i],]) + >>> m_sig = hs.plot.markers.Texts(offsets=offsets, texts=texts, verticalalignment="bottom") + >>> s.add_marker(m_sig) .. figure:: images/plot_markers_nav.gif @@ -1178,6 +1174,8 @@ Each slice is indicated with the same text on the navigator. Multi-dimensional markers. +Permanent markers +----------------- .. versionadded:: 1.2 Permanent markers. @@ -1188,10 +1186,10 @@ These markers can also be permanently added to a signal, which is saved in .. code-block:: python >>> s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) - >>> marker = hs.markers.point(5, 9) + >>> marker = hs.plot.markers.Points(offsets = [[5,9]], sizes=1, units="xy") >>> s.add_marker(marker, permanent=True) >>> s.metadata.Markers - └── point = + └── Points = >>> s.plot() @@ -1206,11 +1204,11 @@ Markers can be removed by deleting them from the metadata .. code-block:: python >>> s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) - >>> marker = hs.markers.point(5, 9) + >>> marker = hs.plot.markers.Points(offsets = [[5,9]], sizes=1) >>> s.add_marker(marker, permanent=True) >>> s.metadata.Markers - └── point = - >>> del s.metadata.Markers.point + └── Points = + >>> del s.metadata.Markers.Points >>> s.metadata.Markers # Returns nothing @@ -1220,32 +1218,41 @@ calling `s.plot`: .. code-block:: python >>> s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) - >>> marker = hs.markers.point(5, 9) + >>> marker = hs.plot.markers.Points(offsets=[[5,9]], sizes=1, units="xy") >>> s.add_marker(marker, permanent=True, plot_marker=False) >>> s.plot(plot_markers=False) If the signal has a navigation dimension, the markers can be made to change -as a function of the navigation index. For a signal with 1 navigation axis: +as a function of the navigation index by passing in kwargs with ``dtype=object``. +For a signal with 1 navigation axis: .. code-block:: python >>> s = hs.signals.Signal2D(np.arange(300).reshape(3, 10, 10)) - >>> marker = hs.markers.point((5, 1, 2), (9, 8, 1), color='red') + >>> offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + >>> marker_pos = [[5,9], [1,8], [2,1]] + >>> for i,m in zip(np.ndindex(3), marker_pos): + ... offsets[i] = m + >>> marker = hs.plot.markers.Points(offsets=offsets, color="red", sizes=10) >>> s.add_marker(marker, permanent=True) .. figure:: images/plot_markers_nav_index.gif :align: center :width: 100% - Plotting with markers that change with the navigation index. +Plotting with markers that change with the navigation index. Or for a signal with 2 navigation axes: .. code-block:: python >>> s = hs.signals.Signal2D(np.arange(400).reshape(2, 2, 10, 10)) - >>> marker = hs.markers.point(((5, 1), (1, 2)), ((2, 6), (9, 8))) + >>> marker_pos = np.array([[[5,1], [1,2]],[[2,9],[6,8]]]) + >>> offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + >>> for i in np.ndindex(s.axes_manager.navigation_shape): + ... offsets[i] = [marker_pos[i],] + >>> marker = hs.plot.markers.Points(offsets=offsets, sizes=10) >>> s.add_marker(marker, permanent=True) .. figure:: images/plot_markers_2dnav_index.gif @@ -1261,56 +1268,38 @@ This can be extended to 4 (or more) navigation dimensions: >>> s = hs.signals.Signal2D(np.arange(1600).reshape(2, 2, 2, 2, 10, 10)) >>> x = np.arange(16).reshape(2, 2, 2, 2) >>> y = np.arange(16).reshape(2, 2, 2, 2) - >>> marker = hs.markers.point(x=x, y=y, color='red') - >>> s.add_marker(marker, permanent=True) + >>> offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + >>> for i in np.ndindex(s.axes_manager.navigation_shape): + ... offsets[i] = [[x[i],y[i]],] + >>> marker = hs.plot.markers.Points(offsets=offsets, color='red', sizes=10) + >>> s.add_marker(marker, permanent=True) # doctest: +SKIP -.. versionadded:: 1.2 - ``markers`` keyword arguments can take an iterable in addition to single - marker. -If you want to add a large amount of markers at the same time we advise -to add them as an iterable (list, tuple, ...), which will be much faster: +You can add a couple of different types of markers at the same time. .. code-block:: python - >>> from numpy.random import random - >>> s = hs.signals.Signal2D(np.arange(300).reshape(3, 10, 10)) - >>> markers = (hs.markers.point(tuple(random()*10 for i in range(3)), - ... tuple(random()*10 for i in range(3)), - ... size=30, color=np.random.rand(3,1)) - ... for i in range(500)) - >>> s.add_marker(markers, permanent=True) - -.. figure:: images/plot_markers_2dnav_random_iter.gif - :align: center - :width: 100% - - Plotting many markers with an iterable so they change with the navigation - index. - -This can also be done using different types of markers - -.. code-block:: python - - >>> from numpy.random import random + >>> import hyperspy.api as hs + >>> import numpy as np >>> s = hs.signals.Signal2D(np.arange(300).reshape(3, 10, 10)) >>> markers = [] - >>> for i in range(200): - ... markers.append(hs.markers.horizontal_line( - ... tuple(random()*10 for i in range(3)), - ... color=np.random.rand(3,1))) - ... markers.append(hs.markers.vertical_line( - ... tuple(random()*10 for i in range(3)), - ... color=np.random.rand(3,1))) - ... markers.append(hs.markers.point( - ... tuple(random()*10 for i in range(3)), - ... tuple(random()*10 for i in range(3)), - ... color=np.random.rand(3,1))) - ... markers.append(hs.markers.text( - ... x=tuple(random()*10 for i in range(3)), - ... y=tuple(random()*10 for i in range(3)), - ... text=tuple("sometext" for i in range(3)))) - >>> s.add_marker(markers, permanent=True) + >>> v_line_pos = np.empty(3, dtype=object) + >>> point_offsets = np.empty(3, dtype=object) + >>> text_offsets = np.empty(3, dtype=object) + >>> h_line_pos = np.empty(3, dtype=object) + >>> random_colors = np.empty(3, dtype=object) + >>> num=200 + >>> for i in range(3): + ... v_line_pos[i] = np.random.rand(num)*10 + ... h_line_pos[i] = np.random.rand(num)*10 + ... point_offsets[i] = np.random.rand(num,2)*10 + ... text_offsets[i] = np.random.rand(num,2)*10 + ... random_colors = np.random.rand(num,3) + >>> v_marker = hs.plot.markers.VerticalLines(offsets=v_line_pos, color=random_colors) + >>> h_marker = hs.plot.markers.HorizontalLines(offsets=h_line_pos, color=random_colors) + >>> p_marker = hs.plot.markers.Points(offsets=point_offsets, color=random_colors, sizes=(.1,)) + >>> t_marker = hs.plot.markers.Texts(offsets=text_offsets, texts=["sometext", ]) + >>> s.add_marker([v_marker,h_marker, p_marker, t_marker], permanent=True) .. figure:: images/plot_markers_2dnav_random_iter_many_types.gif :align: center @@ -1324,11 +1313,192 @@ Permanent markers are stored in the HDF5 file if the signal is saved: .. code-block:: python >>> s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) - >>> marker = hs.markers.point(2, 1, color='red') + >>> marker = hs.plot.markers.Points([[2, 1]], color='red') >>> s.add_marker(marker, plot_marker=False, permanent=True) >>> s.metadata.Markers - └── point = - >>> s.save("storing_marker.hdf5") - >>> s1 = hs.load("storing_marker.hdf5") - >>> s1.metadata.Markers - └── point = + └── Points = + >>> s.save("storing_marker.hspy") # doctest: +SKIP + >>> s1 = hs.load("storing_marker.hspy") # doctest: +SKIP + >>> s1.metadata.Markers # doctest: +SKIP + └── Points = + +Supported markers +----------------- + +The markers currently supported in HyperSpy are: + +.. table:: List of supported markers, their signature and their corresponding matplotlib objects. + :widths: 20 40 40 + + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | HyperSpy Markers | Signature | Matplotlib Collection | + +==============================================================+====================================================+====================================================+ + | :class:`~.drawing._markers.arrows.Arrows` | ``offsets``, ``U``, ``V``, ``C``, ``**kwargs`` | :class:`matplotlib.quiver.Quiver` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.circles.Circles` | ``offsets``, ``sizes``, ``**kwargs`` | :class:`matplotlib.collections.CircleCollection` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.ellipses.Ellipses` | ``offsets``, ``widths``, ``heights``, ``**kwargs`` | :class:`matplotlib.collections.EllipseCollection` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.horizontal_lines.HorizontalLines` | ``offsets``, ``**kwargs`` | :class:`matplotlib.collections.LineCollection` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.lines.Lines` | ``segments``, ``**kwargs`` | :class:`matplotlib.collections.LineCollection` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing.markers.Markers` | ``offsets``, ``**kwargs`` | | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.points.Points` | ``offsets``, ``**kwargs`` | :class:`matplotlib.collections.CircleCollection` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.polygons.Polygons` | ``verts``, ``**kwargs`` | :class:`matplotlib.collections.PolyCollection` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.rectangles.Rectangles` | ``offsets``, ``widths``, ``heights``, ``**kwargs`` | Custom ``RectangleCollection`` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.squares.Squares` | ``offsets``, ``widths``, ``**kwargs`` | Custom ``SquareCollection`` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.texts.Texts` | ``offsets``, ``texts``, ``**kwargs`` | Custom ``TextCollection`` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + | :class:`~.drawing._markers.vertical_lines.VerticalLines` | ``offsets``, ``**kwargs`` | :class:`matplotlib.collections.LineCollection` | + +--------------------------------------------------------------+----------------------------------------------------+----------------------------------------------------+ + +Marker properties +----------------- + +The optional parameters (``**kwargs``, keyword arguments) can be used for extra parameters used for +each matplotlib collection. Any parameter which can be set using the :meth:`matplotlib.collections.Collection.set` +method can be used as an iterating parameter with respect to the navigation index by passing in a numpy array +with ``dtype=object``. Otherwise to set the parameter globally the kwarg can directly be passed. + +Additionally, if some ``**kwargs`` are shorter in length to some other parameter it will be cycled such that + +>>> prop[i % len(prop)] # doctest: +SKIP + +where i is the ith element of the collection. + +Relative Markers +---------------- + +Many times when annotating 1-D Plots, you want to add markers which are relative to the data. For example, +you may want to add a line which goes from [0, y], where y is the value at x. To do this, you can set the +``offset_transform`` and/or ``transform`` to ``"relative"``. + +.. code:: + + >>> s = hs.signals.Signal1D(np.random.rand(3, 15)) + >>> from matplotlib.collections import LineCollection + >>> m = hs.plot.markers.Lines(segments=[[[2, 0], [2, 1.0]]], transform="relative") + >>> s.plot() + >>> s.add_marker(m) + +This marker will create a line at a value=2, which extends from 0 --> 1 and updates as the index changes. + +Path Markers +------------ + +Adding new types of markers to hyperspy is relatively simple. Currently, hyperspy supports any +:class:`matplotlib.collections.Collection` object. For most common cases this should be sufficient, +as matplotlib has a large number of built-in collections beyond what is available directly in hyperspy. + +In the event that you want a specific shape that is not supported, you can define a custom +:class:`matplotlib.path.Path` object and then use the :class:`matplotlib.collections.PathCollection` +to add the markers to the plot. Currently, there is no support for saving Path based markers but that can +be added if there are specific use cases. + + +Extra information about Markers +------------------------------- +.. versionadded:: 2.0 + Marker Collections for faster plotting of many markers + +Hyperspy's `Markers` class and its subclasses extends the capabilities of the +:class:`matplotlib.collections.Collection` class and subclasses. +Primarily it allows dynamic markers to be initialized by passing key word arguments with ``dtype=object``. Those +attributes are then updated with the plot as you navigate through the plot. + +In most cases the ``offsets`` kwarg is used to map some marker to multiple positions in the plot. For example we can +define a plot of Ellipses using: + +.. code-block:: python + + >>> import numpy as np + >>> import hyperspy.api as hs + >>> hs.plot.markers.Ellipses(heights=(.4,), widths=(1,), + ... angles=(10,), offsets=np.array([[0,0], [1,1]])) + + +Alternatively, if we want to make ellipses with different heights and widths we can pass multiple values to +heights, widths and angles. In general these properties will be applied such that ``prop[i % len(prop)]`` so +passing ``heights=(.1,.2,.3)`` will result in the ellipse at ``offsets[0]`` with a height of 0.1 the ellipse at +``offsets[1]`` with a height of 0.1, ellipse at ``offsets[2]`` has a height of 0.3 and the ellipse at ``offsets[3]`` has +a height of 0.1 and so on. + +For attributes which we want to by dynamic and change with the navigation coordinates, we can pass those values as +an array with ``dtype=object``. Each of those values will be set as the index changes, similarly to signal data, +the current ``index`` of the axis manager is used to retrieve the current array of +markers at that ``index``. Additionally, lazy markers are treated similarly where the current +chunk for a marker is cached. + +.. NOTE:: + Only kwargs which can be passed to :meth:`matplotlib.collections.Collection.set` can be dynamic. + + +:class:`~.api.plot.markers.Markers` operates in a similar way to signals when the data is +retrieved. The current ``index`` for the signal is used to retrieve the current array of +markers at that ``index``. Additionally, lazy markers are treated similarly where the current +chunk for a marker is cached. + + +If we want to plot a series of points, we can use the following code, in this case +both the ``sizes`` and ``offsets`` kwargs are dynamic and change with each index. + +.. code-block:: python + + >>> import numpy as np + >>> import hyperspy.api as hs + >>> data = np.empty((2,2), dtype=object) + >>> sizes = np.empty((2,2), dtype=object) + >>> for i, ind in enumerate(np.ndindex((2,2))): + ... data[ind] = np.random.rand(i+1,2)*3 # dynamic positions + ... sizes[ind] = [(i+1)/10,] # dynamic sizes + >>> m = hs.plot.markers.Points(sizes=sizes, offsets=data, color="r", units="xy") + >>> s = hs.signals.Signal2D(np.zeros((2,2,4,4))) + >>> s.plot() + >>> s.add_marker(m) + + +The :class:`~.api.plot.markers.Markers` also has a class method :meth:`~.api.plot.markers.Markers.from_signal` which can +be used to create a set of markers from the output of some map function. In this case ``signal.data`` is mapped +to some ``key`` and used to initialize a :class:`~.api.plot.markers.Markers` object. If the signal has the attribute +``signal.metadata.Peaks.signal_axes`` and convert_units = True then the values will be converted to the proper units +before creating the :class:`~.api.plot.markers.Markers` object. + +.. NOTE:: + For kwargs like size, height, etc. the scale and the units of the x axis are used to plot. + +Let's consider how plotting a bunch of different collections might look: + +.. code-block:: python + + >>> import hyperspy.api as hs + >>> import numpy as np + + >>> collections = [hs.plot.markers.Points, + ... hs.plot.markers.Ellipses, + ... hs.plot.markers.Rectangles, + ... hs.plot.markers.Arrows, + ... hs.plot.markers.Circles, + ... ] + >>> num_col = len(collections) + >>> offsets = [np.stack([np.ones(num_col)*i, np.arange(num_col)], axis=1) for i in range(len(collections))] + >>> kwargs = [{"sizes":(.4,),"facecolor":"black"}, + ... {"widths":(.2,), "heights":(.7,), "angles":(60,), "facecolor":"black"}, + ... {"widths":(.4,), "heights":(.5,), "facecolor":"none", "edgecolor":"black"}, + ... {"U":(.5,), "V":(.2), "facecolor":"black"}, + ... {"sizes":(.4,), "facecolor":"black"},] + >>> for k, o, c in zip(kwargs, offsets, collections): + ... k["offsets"] = o + >>> collections = [C(**k) for k,C in zip(kwargs, collections)] + >>> s = hs.signals.Signal2D(np.zeros((2, num_col, num_col))) + >>> s.plot() + >>> s.add_marker(collections) + +.. figure:: images/plot_marker_collection.png + :align: center + :width: 100% diff --git a/examples/Markers/README.rst b/examples/Markers/README.rst new file mode 100644 index 0000000000..befd179873 --- /dev/null +++ b/examples/Markers/README.rst @@ -0,0 +1,6 @@ +.. _gallery.markers: + +Markers +======= + +Gallery of examples on using HyperSpy markers. diff --git a/examples/Markers/add_remove_marker.py b/examples/Markers/add_remove_marker.py new file mode 100644 index 0000000000..09995d66d5 --- /dev/null +++ b/examples/Markers/add_remove_marker.py @@ -0,0 +1,65 @@ +""" +Add/Remove items from existing Markers +====================================== + +This example shows how to add or remove marker from an existing collection. +This is done by setting the parameters (offsets, sizes, etc.) of the collection. + +""" +#%% +# Create a signal +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.arange(15*100*100).reshape((15, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# Create text marker + +# Define the position of the texts +offsets = np.stack([np.arange(0, 100, 10)]*2).T + np.array([5,]*2) +texts = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'f', 'h', 'i']) + +m = hs.plot.markers.Texts( + offsets=offsets, + texts=texts, + sizes=3, + ) + +print(f'Number of markers is {len(m)}.') + +s.plot() +s.add_marker(m) + +#%% +# Remove the last text of the collection +# ###################################### + + + +# Set new texts and offsets parameters with one less item +m.remove_items(indices=-1) + +print(f'Number of markers is {len(m)} after removing one marker.') + +s.plot() +s.add_marker(m) + +#%% +# Add another text of the collection +# ################################## + +# Define the position in the middle of the axes + +m.add_items(offsets=np.array([[50, 50]]), texts=np.array(["new text"])) + +print(f'Number of markers is {len(m)} after adding the text {texts[-1]}.') + +s.plot() +s.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/arrows.py b/examples/Markers/arrows.py new file mode 100644 index 0000000000..ce7b00c2ed --- /dev/null +++ b/examples/Markers/arrows.py @@ -0,0 +1,35 @@ +""" +Arrow markers +============= + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((50, 100, 100)) +s = hs.signals.Signal2D(data) + +for axis in s.axes_manager.signal_axes: + axis.scale = 2*np.pi / 100 + +#%% +# This example shows how to draw arrows + +# Define the position of the arrows +X, Y = np.meshgrid(np.arange(0, 2 * np.pi, .2), np.arange(0, 2 * np.pi, .2)) +offsets = np.column_stack((X.ravel(), Y.ravel())) +U = np.cos(X).ravel() / 7.5 +V = np.sin(Y).ravel() / 7.5 +C = np.hypot(U, V) + +m = hs.plot.markers.Arrows(offsets, U, V, C=C) +s.plot() +s.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/arrows_navigation.py b/examples/Markers/arrows_navigation.py new file mode 100644 index 0000000000..e0cba9a31a --- /dev/null +++ b/examples/Markers/arrows_navigation.py @@ -0,0 +1,83 @@ +""" +Arrow markers +============= + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((50, 100, 100)) +s = hs.signals.Signal2D(data) + +for axis in s.axes_manager.signal_axes: + axis.scale = 2*np.pi / 100 + +#%% +# +# Dynamic Arrow Markers: Changing Length +# ###################################### +# +# The first example shows how to change the length of the arrows when changing +# the navigation coordinates + +X, Y = np.meshgrid(np.arange(0, 2 * np.pi, .2), np.arange(0, 2 * np.pi, .2)) +offsets = np.column_stack((X.ravel(), Y.ravel())) + +weight = np.cos(np.linspace(0, 4*np.pi, num=50)) + +U = np.empty(s.axes_manager.navigation_shape, dtype=object) +V = np.empty(s.axes_manager.navigation_shape, dtype=object) +C = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(U.shape): + U[ind] = np.cos(X).ravel() / 7.5 * weight[ind] + V[ind] = np.sin(Y).ravel() / 7.5 * weight[ind] + C[ind] = np.hypot(U[ind], V[ind]) + +m = hs.plot.markers.Arrows( + offsets, + U, + V, + C=C + ) + +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Arrow Markers: Changing Position +# ######################################## +# +# The second example shows how to change the position of the arrows when changing +# the navigation coordinates + +X, Y = np.meshgrid(np.arange(0, 2 * np.pi, .2), np.arange(0, 2 * np.pi, .2)) +U = np.cos(X).ravel() / 7.5 +V = np.sin(Y).ravel() / 7.5 +C = np.hypot(U, V) + +weight_x = np.sin(np.linspace(0, 2*np.pi, num=50)) +weight_y = np.cos(np.linspace(0, 2*np.pi, num=50)) + +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(offsets.shape): + offsets[ind] = np.column_stack((X.ravel() + weight_x[ind], Y.ravel() + weight_y[ind])) + +m = hs.plot.markers.Arrows( + offsets, + U, + V, + C=C + ) + +s.plot() +s.add_marker(m) + + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/arrows_ragged.py b/examples/Markers/arrows_ragged.py new file mode 100644 index 0000000000..dc5746b03f --- /dev/null +++ b/examples/Markers/arrows_ragged.py @@ -0,0 +1,51 @@ +""" +Varying number of arrows per navigation position +================================================ + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((10, 100, 100)) +s = hs.signals.Signal2D(data) + +for axis in s.axes_manager.signal_axes: + axis.scale = 2*np.pi / 100 + +# Select navigation position 5 +s.axes_manager.indices = (5, ) + +#%% +# +# Dynamic Arrow Markers: Changing Length +# ###################################### +# +# This example shows how to use the Arrows marker with a varying number of +# arrows per navigation position + +# Define the position of the arrows, use ragged array to enable the navigation +# position dependence +offsets= np.empty(s.axes_manager.navigation_shape, dtype=object) +U = np.empty(s.axes_manager.navigation_shape, dtype=object) +V = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(U.shape): + offsets[ind] = rng.random((ind[0]+1, 2)) * 6 + U[ind] = rng.random(ind[0]+1) * 2 + V[ind] = rng.random(ind[0]+1) * 2 + +m = hs.plot.markers.Arrows( + offsets, + U, + V, + ) + +s.plot() +s.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/circles.py b/examples/Markers/circles.py new file mode 100644 index 0000000000..e8221540a9 --- /dev/null +++ b/examples/Markers/circles.py @@ -0,0 +1,61 @@ +""" +Circle Markers +============== + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 1 navigation dimension +rng = np.random.default_rng(0) +data = np.ones((50, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static circles + +# Define the position of the circles (start at (0, 0) and increment by 10) +offsets = np.array([np.arange(0, 100, 10)]*2).T + +m = hs.plot.markers.Circles( + sizes=10, + offsets=offsets, + edgecolor='r', + linewidth=5, + ) + +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Circle Markers +# ###################### +# +# This second example shows how to draw dynamic circles whose position and +# radius change depending on the navigation position + +s2 = hs.signals.Signal2D(data) + +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) +sizes = np.empty(s.axes_manager.navigation_shape, dtype=object) + +for ind in np.ndindex(offsets.shape): + offsets[ind] = rng.random((5, 2)) * 100 + sizes[ind] = rng.random((5, )) * 10 + +m = hs.plot.markers.Circles( + sizes=sizes, + offsets=offsets, + edgecolor='r', + linewidth=5, + ) + +s2.plot() +s2.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 4 diff --git a/examples/Markers/circles_color_radius.py b/examples/Markers/circles_color_radius.py new file mode 100644 index 0000000000..2a2bf64d2b --- /dev/null +++ b/examples/Markers/circles_color_radius.py @@ -0,0 +1,56 @@ +""" +Circle Markers with Radius Dependent Coloring +============================================= + +This example shows how to draw circle with the color of the circle scaling with +the radius of the circle + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import matplotlib.pyplot as plt +import numpy as np + +# Create a Signal2D +rng = np.random.default_rng(0) +s = hs.signals.Signal2D(np.ones((25, 100, 100))) + +#%% +# This first example shows how to draw arrows + +# Define the size of the circles +sizes = rng.random((10, )) * 20 + 5 + +# Define the position of the circles +offsets = rng.random((10, 2)) * 100 + +m = hs.plot.markers.Circles( + sizes=sizes, + offsets=offsets, + linewidth=2, + ) + +s.plot() +s.add_marker(m) + +#%% +# .. note:: +# Any changes to the marker made by setting :py:class:`matplotlib.collections.Collection` +# attributes will not be saved when saving as ``hspy``/``zspy`` file. + +# Set the color of the circles +m.set_ScalarMappable_array(sizes.ravel() / 2) + +# Add corresponding colorbar +cbar = m.plot_colorbar() +cbar.set_label('Circle radius') + +# Set animated state of colorbar to support blitting +animated = plt.gcf().canvas.supports_blit +cbar.ax.yaxis.set_animated(animated) +cbar.solids.set_animated(animated) + +#%% +# sphinx_gallery_thumbnail_number = diff --git a/examples/Markers/ellipses.py b/examples/Markers/ellipses.py new file mode 100644 index 0000000000..d15dde1da9 --- /dev/null +++ b/examples/Markers/ellipses.py @@ -0,0 +1,68 @@ +""" +Ellipse markers +=============== + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((25, 25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static ellipses + +# Define the position of the ellipses +offsets = rng.random((10, 2)) * 100 + +m = hs.plot.markers.Ellipses( + widths=(8,), + heights=(10,), + angles=(45,), + offsets=offsets, + facecolor="red", + ) + +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Ellipse Markers +# ####################### +# +# This first example shows how to draw dynamic ellipses, whose position, widths +# heights and angles depends on the navigation coordinates + +s2 = hs.signals.Signal2D(data) + +widths = np.empty(s.axes_manager.navigation_shape, dtype=object) +heights = np.empty(s.axes_manager.navigation_shape, dtype=object) +angles = np.empty(s.axes_manager.navigation_shape, dtype=object) +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + +for index in np.ndindex(offsets.shape): + widths[index] = rng.random((10, )) * 10 + heights[index] = rng.random((10, )) * 7 + angles[index] = rng.random((10, )) * 180 + offsets[index] = rng.random((10, 2)) * 100 + + +m = hs.plot.markers.Ellipses( + widths=widths, + heights=heights, + angles=angles, + offsets=offsets, + facecolor="red", + ) + +s2.plot() +s2.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/filled_circles.py b/examples/Markers/filled_circles.py new file mode 100644 index 0000000000..5cbd1fb981 --- /dev/null +++ b/examples/Markers/filled_circles.py @@ -0,0 +1,61 @@ +""" +Filled Circle Markers +===================== + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import matplotlib as mpl +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((25, 25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static filled circles + +# Define the position of the circles +offsets = rng.random((10, 2)) * 100 + +m = hs.plot.markers.Points( + sizes=20, + offsets=offsets, + facecolors="red", + ) + +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Filled Circle Markers +# ############################# +# +# This second example shows how to draw dynamic filled circles, whose size, +# color and position change depending on the navigation position + +s2 = hs.signals.Signal2D(data) + +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) +sizes = np.empty(s.axes_manager.navigation_shape, dtype=object) +colors = list(mpl.colors.TABLEAU_COLORS.values())[:10] + +for ind in np.ndindex(offsets.shape): + offsets[ind] = rng.random((10, 2)) * 100 + sizes[ind] = rng.random((10, )) * 50 + +m = hs.plot.markers.Points( + sizes=sizes, + offsets=offsets, + facecolors=colors, + ) + +s2.plot() +s2.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/from_signal.py b/examples/Markers/from_signal.py new file mode 100644 index 0000000000..9dc7faeabf --- /dev/null +++ b/examples/Markers/from_signal.py @@ -0,0 +1,65 @@ +""" +Creating Markers from a signal +============================== + +This example shows how to create markers from a signal. This is useful for creating lazy +markers from some operation such as peak finding on a signal. Here we show how to create +markers from a simple map function which finds the maximum value and plots a marker at +that position. +""" +import numpy as np +import hyperspy.api as hs + + +# Making some artificial data +def find_maxima(data, scale, offset): + ind = np.array(np.unravel_index(np.argmax(data, axis=None), data.shape)).astype(int) + d = data[ind] + ind = ind * scale + offset # convert to physical units + print(ind) + print(d) + return np.array( + [ + [ind[0], d[0]], + ] + ) + + +def find_maxima_lines(data, scale, offset): + ind = np.array(np.unravel_index(np.argmax(data, axis=None), data.shape)).astype(int) + ind = ind * scale + offset # convert to physical units + return ind + + +def gaussian(x, mu, sig): + return ( + 1.0 / (np.sqrt(2.0 * np.pi) * sig) * np.exp(-np.power((x - mu) / sig, 2.0) / 2) + ) + + +data = np.empty((4, 120)) +for i in range(4): + x_values = np.linspace(-3 + i * 0.1, 3 + i * 0.1, 120) + data[i] = gaussian(x_values, mu=0, sig=10) + +s = hs.signals.Signal1D(data) +s.axes_manager.signal_axes[0].scale = 6 / 120 +s.axes_manager.signal_axes[0].offset = -3 + + +scale = s.axes_manager.signal_axes[0].scale +offset = s.axes_manager.signal_axes[0].offset +max_values = s.map(find_maxima, scale=scale, offset=offset, inplace=False, ragged=True) +max_values_lines = s.map( + find_maxima_lines, scale=scale, offset=offset, inplace=False, ragged=True +) + +point_markers = hs.plot.markers.Points.from_signal(max_values, signal_axes=None) +line_markers = hs.plot.markers.VerticalLines.from_signal( + max_values_lines, signal_axes=None +) + + +s.plot() +s.add_marker(point_markers) +s.add_marker(line_markers) diff --git a/examples/Markers/lines.py b/examples/Markers/lines.py new file mode 100644 index 0000000000..b6b9a16024 --- /dev/null +++ b/examples/Markers/lines.py @@ -0,0 +1,64 @@ +""" +Line Markers +============= + +""" + +#%% +# Create a signal + +import hyperspy.api as hs +import matplotlib.pyplot as plt +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((25, 25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static stars markers using the matplotlib +# StarPolygonCollection + +# Define the position of the lines +# line0: (x0, y0), (x1, y1) +# line1: (x0, y0), (x1, y1) +# ... + +segments = rng.random((10, 2, 2)) * 100 + +m = hs.plot.markers.Lines( + segments=segments, + linewidth=3, + colors='g', + ) + +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Line Markers +# #################### +# +# This first example shows how to draw dynamic lines markers, whose position +# depends on the navigation coordinates + +segments = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(segments.shape): + segments[ind] = rng.random((10, 2, 2)) * 100 + +# Get list of colors +colors = list(plt.rcParams['axes.prop_cycle'].by_key()['color']) + +m = hs.plot.markers.Lines( + segments=segments, + colors=colors, + linewidth=5, + ) + +s.plot() +s.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/polygons.py b/examples/Markers/polygons.py new file mode 100644 index 0000000000..bd488d5d1e --- /dev/null +++ b/examples/Markers/polygons.py @@ -0,0 +1,66 @@ +""" +Polygon Markers +================ + +""" + +#%% +# Create a signal + +import hyperspy.api as hs +import matplotlib.pyplot as plt +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((25, 25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static polygon markers using the matplotlib +# PolygonCollection + +# Define the vertexes of the polygons +# poylgon1: [[x0, y0], [x1, y1], [x2, y2], [x3, x3]] +# poylgon2: [[x0, y0], [x1, y1], [x2, y2]] +# ... +poylgon1 = [[1, 1], [20, 20], [1, 20], [25, 5]] +poylgon2 = [[50, 60], [90, 40], [60, 40], [23, 60]] + +verts = [poylgon1, poylgon2] + +m = hs.plot.markers.Polygons( + verts=verts, + linewidth=3, + facecolors=('g',), + ) + +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Polygon Markers +# ####################### +# +# This example shows how to draw dynamic polygon markers, whose position +# depends on the navigation coordinates + +verts = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(verts.shape): + verts[ind] = rng.random((10, 4, 2)) * 100 + +# Get list of colors +colors = list(plt.rcParams['axes.prop_cycle'].by_key()['color']) + +m = hs.plot.markers.Polygons( + verts=verts, + facecolors=colors, + linewidth=3, + alpha=0.6 +) +s.plot() +s.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/ragged_points.py b/examples/Markers/ragged_points.py new file mode 100644 index 0000000000..c0749857bf --- /dev/null +++ b/examples/Markers/ragged_points.py @@ -0,0 +1,38 @@ +""" +Ragged Points +============= + +As for ragged signals, the number of markers at each position can vary and this +is done by passing a ragged array to the constructor of the markers. + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.arange(25*100*100).reshape((25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# Create the ragged array with varying number of markers for each navigation +# position + +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(offsets.shape): + num = rng.integers(3, 10) + offsets[ind] = rng.random((num, 2)) * 100 + +m = hs.plot.markers.Points( + offsets=offsets, + facecolor='orange', + ) + +s.plot() +s.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/rectangles.py b/examples/Markers/rectangles.py new file mode 100644 index 0000000000..08e7dc3aee --- /dev/null +++ b/examples/Markers/rectangles.py @@ -0,0 +1,70 @@ +""" +Rectangle Markers +================= + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((25, 25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static rectangle markers + +# Define the position of the rectangles +offsets = np.array([np.arange(0, 100, 10)]*2).T + +m = hs.plot.markers.Rectangles( + offsets=offsets, + widths=(5,), + heights=(7,), + angles=(0,), + color="red", + + ) +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Rectangle Markers +# ######################### +# +# This first example shows how to draw dynamic rectangle markers, whose +# position, widths, heights and angles depends on the navigation coordinates + +s2 = hs.signals.Signal2D(data) + +widths = np.empty(s.axes_manager.navigation_shape, dtype=object) +heights = np.empty(s.axes_manager.navigation_shape, dtype=object) +angles = np.empty(s.axes_manager.navigation_shape, dtype=object) +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + +for index in np.ndindex(offsets.shape): + widths[index] = rng.random((10, )) * 50 + heights[index] = rng.random((10, )) * 25 + angles[index] = rng.random((10, )) * 180 + offsets[index] = rng.random((10, 2)) * 100 + +m = hs.plot.markers.Rectangles( + offsets=offsets, + widths=widths, + heights=heights, + angles=angles, + color="red", + facecolor="none", + linewidth=3 + ) + + +s2.plot() +s2.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 4 diff --git a/examples/Markers/rotation_makers.py b/examples/Markers/rotation_makers.py new file mode 100644 index 0000000000..cf7ae738b1 --- /dev/null +++ b/examples/Markers/rotation_makers.py @@ -0,0 +1,62 @@ +""" +Rotation of markers +=================== + +This example shows how markers are rotated. + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D +data = np.ones([100, 100]) +s = hs.signals.Signal2D(data) + +num = 2 +angle = 25 +color = ["tab:orange", "tab:blue"] + +#%% +# Create the markers, the first and second elements are at 0 and 20 degrees + +# Define the position of the markers +offsets = np.array([20*np.ones(num)]*2).T +angles = np.arange(0, angle*num, angle) + +m1 = hs.plot.markers.Rectangles( + offsets=offsets, + widths=np.ones(num)*20, + heights=np.ones(num)*10, + angles=angles, + facecolor='none', + edgecolor=color, + ) + +m2 = hs.plot.markers.Ellipses( + offsets=offsets + np.array([0, 20]), + widths=np.ones(num)*20, + heights=np.ones(num)*10, + angles=angles, + facecolor='none', + edgecolor=color, + ) + +m3 = hs.plot.markers.Squares( + offsets=offsets + np.array([0, 50]), + widths=np.ones(num)*20, + angles=angles, + facecolor='none', + edgecolor=color, + ) + +#%% +# Plot the signals and add all the markers + +s.plot() +s.add_marker([m1, m2, m3]) + +#%% +# sphinx_gallery_thumbnail_number = 1 diff --git a/examples/Markers/squares.py b/examples/Markers/squares.py new file mode 100644 index 0000000000..a73ef1848e --- /dev/null +++ b/examples/Markers/squares.py @@ -0,0 +1,68 @@ +""" +Square Markers +============== + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((25, 25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static square markers + +# Define the position of the squares (start at (0, 0) and increment by 10) +offsets = np.array([np.arange(0, 100, 10)]*2).T + +m = hs.plot.markers.Squares( + offsets=offsets, + widths=(5,), + angles=(0,), + color="orange", + + ) +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Square Markers +# ######################### +# +# This first example shows how to draw dynamic squres markers, whose +# position, widths and angles depends on the navigation coordinates + +s2 = hs.signals.Signal2D(data) + +widths = np.empty(s.axes_manager.navigation_shape, dtype=object) +heights = np.empty(s.axes_manager.navigation_shape, dtype=object) +angles = np.empty(s.axes_manager.navigation_shape, dtype=object) +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + +for index in np.ndindex(offsets.shape): + widths[index] = rng.random((10, )) * 50 + heights[index] = rng.random((10, )) * 25 + angles[index] = rng.random((10, )) * 180 + offsets[index] = rng.random((10, 2)) * 100 + +m = hs.plot.markers.Squares( + offsets=offsets, + widths=widths, + angles=angles, + color="orange", + facecolor="none", + linewidth=3 + ) + + +s2.plot() +s2.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 4 diff --git a/examples/Markers/stars.py b/examples/Markers/stars.py new file mode 100644 index 0000000000..b519be7e84 --- /dev/null +++ b/examples/Markers/stars.py @@ -0,0 +1,61 @@ +""" +Star Markers +============ + +""" + +#%% +# Create a signal + +import hyperspy.api as hs +import matplotlib as mpl +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = np.ones((25, 25, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static stars markers using the matplotlib +# StarPolygonCollection + +# Define the position of the boxes +offsets = rng.random((10, 2)) * 100 + +# every other star has a size of 50/100 +m = hs.plot.markers.Markers(collection=mpl.collections.StarPolygonCollection, + offsets=offsets, + numsides=5, + color="orange", + sizes=(50, 100)) +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Star Markers +# ###################### +# +# This second example shows how to draw dynamic stars markers, whose position +# depends on the navigation coordinates + +# Create a Signal2D with 2 navigation dimensions +s2 = hs.signals.Signal2D(data) + +# Create a ragged array of offsets +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(offsets.shape): + offsets[ind] = rng.random((10, 2)) * 100 + +m2 = hs.plot.markers.Markers(collection=mpl.collections.StarPolygonCollection, + offsets=offsets, + numsides=5, + color="blue", + sizes=(50, 100)) + +s2.plot() +s2.add_marker(m2) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/texts.py b/examples/Markers/texts.py new file mode 100644 index 0000000000..0ba862761a --- /dev/null +++ b/examples/Markers/texts.py @@ -0,0 +1,57 @@ +""" +Text Markers +============ + +""" +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +# Create a Signal2D with 1 navigation dimension +rng = np.random.default_rng(0) +data = np.ones((10, 100, 100)) +s = hs.signals.Signal2D(data) + +#%% +# This first example shows how to draw static Text markers + +# Define the position of the texts +offsets = np.stack([np.arange(0, 100, 10)]*2).T + np.array([5,]*2) +texts = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'f', 'h', 'i']) + +m = hs.plot.markers.Texts( + offsets=offsets, + texts=texts, + sizes=3, + facecolor="black", + ) +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Text Markers +# #################### +# + +s2 = hs.signals.Signal2D(data) + +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + +for index in np.ndindex(offsets.shape): + offsets[index] = rng.random((10, 2)) * 100 + +m2 = hs.plot.markers.Texts( + offsets=offsets, + texts=texts, + sizes=3, + facecolor="black", + ) + +s2.plot() +s2.add_marker(m2) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/Markers/transforms_and_units.py b/examples/Markers/transforms_and_units.py new file mode 100644 index 0000000000..078955bd01 --- /dev/null +++ b/examples/Markers/transforms_and_units.py @@ -0,0 +1,141 @@ +""" +Transforms and Units +==================== + +This example shows how to use both the ``offset_transform`` and ```transforms`` +parameters for markers +""" + +#%% +# Create a signal + +import hyperspy.api as hs +import numpy as np + +rng = np.random.default_rng() +data = np.arange(1, 101).reshape(10, 10)*2 + rng.random((10, 10)) +signal = hs.signals.Signal1D(data) + +#%% +# The first example shows how to draw markers which are relative to some +# 1D signal. This is how the EDS and EELS Lines are implemented in the +# exspy package. + + +segments = np.zeros((10, 2, 2)) # line segemnts for realative markers +segments[:, 1, 1] = 1 # set y values end (1 means to the signal curve) +segments[:, 0, 0] = np.arange(10).reshape(10) # set x for line start +segments[:, 1, 0] = np.arange(10).reshape(10) # set x for line stop + +offsets = np.zeros((10,2)) # offsets for texts positions +offsets[:, 1] = 1 # set y value for text position ((1 means to the signal curve)) +offsets[:, 0] = np.arange(10).reshape(10) # set x for line start + +markers = hs.plot.markers.Lines(segments=segments,transform="relative") +texts = hs.plot.markers.Texts(offsets=offsets, + texts=["a", "b", "c", "d", "e", "f", "g", "h", "i"], + sizes=10, + offset_transform="relative", + shift=0.005) # shift in axes units for some constant displacement +signal.plot() +signal.add_marker(markers) +signal.add_marker(texts) + +#%% +# The second example shows how to draw markers which extend to the edges of the +# axes. This is how the VerticalLines and HorizontalLines markers are implemented. + +markers = hs.plot.markers.Lines(segments=segments, + transform="xaxis") + + +signal.plot() +signal.add_marker(markers) + +#%% +# The third example shows how an ``offset_transform`` of ``'axes'`` can be +# used to annotate a signal. +# +# The size of the marker is specified in units defined by the ``transform``, +# in this case ``"xaxis_scale"``, ``"yaxis_scale"`` or ``"display"`` + +offsets = [[1, 13.5], ] # offsets for positions +sizes =1 +units = 'x' +offset_transform = 'data' +string = (f" sizes={sizes}, offset_transform='{offset_transform}', units='{units}', offsets={offsets}",) + +marker1text = hs.plot.markers.Texts(offsets=offsets, + texts=string, + sizes=1, + horizontalalignment="left", + verticalalignment="baseline", + offset_transform=offset_transform) + +marker = hs.plot.markers.Points(offsets=offsets, + sizes=sizes, units=units, offset_transform=offset_transform) + + +offsets = [[.1, .1], ] # offsets for positions +sizes =10 +units = 'points' +offset_transform = 'axes' +string = (f" sizes={sizes}, offset_transform='{offset_transform}', units='{units}', offsets={offsets}",) + +marker2text = hs.plot.markers.Texts(offsets=offsets, + texts=string, + sizes=1, + horizontalalignment="left", + verticalalignment="baseline", + offset_transform=offset_transform) + +marker2 = hs.plot.markers.Points(offsets=offsets, + sizes=sizes, units=units, offset_transform=offset_transform) + + +offsets = [[.1, .8], ] # offsets for positions +sizes =1 +units = 'y' +offset_transform = 'axes' +string = (f" sizes={sizes}, offset_transform='{offset_transform}', units='{units}', offsets={offsets}",) + +marker3text = hs.plot.markers.Texts(offsets=offsets, + texts=string, + sizes=1, + horizontalalignment="left", + verticalalignment="baseline", + offset_transform=offset_transform) + +marker3 = hs.plot.markers.Points(offsets=offsets, + sizes=sizes, units=units, offset_transform=offset_transform) + + +offsets = [[1, 7.5], ] # offsets for positions +sizes =1 +units = 'xy' +offset_transform = 'data' +string = (f" sizes={sizes}, offset_transform='{offset_transform}', units='{units}', offsets={offsets}",) + +marker4text = hs.plot.markers.Texts(offsets=offsets, + texts=string, + sizes=1, + horizontalalignment="left", + verticalalignment="baseline", + offset_transform=offset_transform) + +marker4 = hs.plot.markers.Points(offsets=offsets, + sizes=sizes, units=units, offset_transform=offset_transform) + + +signal.plot() +signal.add_marker(marker) +signal.add_marker(marker1text) +signal.add_marker(marker2) +signal.add_marker(marker2text) +signal.add_marker(marker3) +signal.add_marker(marker3text) +signal.add_marker(marker4) +signal.add_marker(marker4text) + +#%% +#sphinx_gallery_thumbnail_number = 2 \ No newline at end of file diff --git a/examples/Markers/vertical_lines.py b/examples/Markers/vertical_lines.py new file mode 100644 index 0000000000..49cb8b753a --- /dev/null +++ b/examples/Markers/vertical_lines.py @@ -0,0 +1,59 @@ +""" +Vertical Line Markers +===================== + +""" + +#%% +# Create a signal + +import hyperspy.api as hs +import matplotlib.pyplot as plt +import numpy as np + +# Create a Signal2D with 2 navigation dimensions +rng = np.random.default_rng(0) +data = rng.random((25, 25, 100)) +s = hs.signals.Signal1D(data) + +#%% +# This first example shows how to draw 3 static (same position for all +# navigation coordinate) vetical lines + +offsets = np.array([10, 20, 40]) + +m = hs.plot.markers.VerticalLines( + offsets=offsets, + linewidth=3, + colors=['r', 'g', 'b'], + ) + +s.plot() +s.add_marker(m) + +#%% +# +# Dynamic Line Markers +# #################### +# +# This example shows how to draw dynamic lines markers, whose positions and +# numbers depends on the navigation coordinates + +offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) +for ind in np.ndindex(offsets.shape): + offsets[ind] = rng.random(rng.integers(10)) * 100 + +# Get list of colors +colors = list(plt.rcParams['axes.prop_cycle'].by_key()['color']) + +m = hs.plot.markers.VerticalLines( + offsets=offsets, + linewidth=5, + colors=colors, + ) + +s.plot() +s.add_marker(m) + +#%% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 0000000000..1919f40033 --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,7 @@ +.. _examples-index: + +Gallery of Examples +=================== + +This gallery contains the commented code for short examples illustrating simple +tasks that can be performed with HyperSpy. diff --git a/examples/data_navigation/1D_image_stack.py b/examples/create_signal/1D_image_stack.py similarity index 83% rename from examples/data_navigation/1D_image_stack.py rename to examples/create_signal/1D_image_stack.py index 6933406314..e01887ed1e 100644 --- a/examples/data_navigation/1D_image_stack.py +++ b/examples/create_signal/1D_image_stack.py @@ -1,9 +1,12 @@ -"""Creates a 3D image and plots it +""" +Creates a 3D image +================== + +This example creates an image stack and plots it. """ import numpy as np import hyperspy.api as hs -import matplotlib.pyplot as plt # Create an image stack with random data im = hs.signals.Signal2D(np.random.random((16, 32, 32))) @@ -28,6 +31,4 @@ im.metadata.General.title = 'Random image stack' # Plot it -im.plot() - -plt.show() # No necessary when running in the HyperSpy's IPython profile +im.plot() \ No newline at end of file diff --git a/examples/data_navigation/2D_image_stack.py b/examples/create_signal/2D_image_stack.py similarity index 86% rename from examples/data_navigation/2D_image_stack.py rename to examples/create_signal/2D_image_stack.py index 86ee576817..4afb33be42 100644 --- a/examples/data_navigation/2D_image_stack.py +++ b/examples/create_signal/2D_image_stack.py @@ -1,9 +1,13 @@ -"""Creates a 4D image and plots it +""" +Creates a 4D image +================== + +This example creates a 4D dataset, i.e. 2 navigation dimension and +2 signal dimension and plots it. """ import numpy as np import hyperspy.api as hs -import matplotlib.pyplot as plt # Create a 2D image stack with random data im = hs.signals.Signal2D(np.random.random((16, 16, 32, 32))) @@ -32,5 +36,4 @@ # Give a title im.metadata.General.title = 'Random 2D image stack' -im.plot() -plt.show() # No necessary when running in the HyperSpy's IPython profile +im.plot() \ No newline at end of file diff --git a/examples/create_signal/La2NiO4_eels.txt b/examples/create_signal/La2NiO4_eels.txt new file mode 100644 index 0000000000..1f53986f23 --- /dev/null +++ b/examples/create_signal/La2NiO4_eels.txt @@ -0,0 +1,2043 @@ +424 1972253.5 +424.25 1973433 +424.5 1968742.4 +424.75 1959542 +425 1951448 +425.25 1946670.1 +425.5 1951562.9 +425.75 1949855.3 +426 1951618.5 +426.25 1949977.6 +426.5 1945089 +426.75 1947867.8 +427 1939487.1 +427.25 1925765.4 +427.5 1923765.5 +427.75 1928403.1 +428 1921843.4 +428.25 1922545.3 +428.5 1926399.5 +428.75 1924963.1 +429 1922939.9 +429.25 1921199.3 +429.5 1910973.1 +429.75 1896888.3 +430 1895388.3 +430.25 1891468.1 +430.5 1886561.8 +430.75 1887090.5 +431 1889450 +431.25 1881893.9 +431.5 1876287.1 +431.75 1877521.5 +432 1875121.8 +432.25 1872565.1 +432.5 1872217.4 +432.75 1864848.5 +433 1866412.1 +433.25 1875750.9 +433.5 1878132.3 +433.75 1870701.6 +434 1857188.3 +434.25 1847480.8 +434.5 1846539.3 +434.75 1851893.6 +435 1854335.9 +435.25 1854228.3 +435.5 1852053.6 +435.75 1850068.5 +436 1841735.4 +436.25 1834997 +436.5 1828297.9 +436.75 1832508.1 +437 1838283.9 +437.25 1832351.6 +437.5 1830715 +437.75 1835600.1 +438 1833884.3 +438.25 1828291.9 +438.5 1820165.4 +438.75 1815462 +439 1814248.9 +439.25 1808390.8 +439.5 1796798 +439.75 1786832.9 +440 1781920 +440.25 1778449 +440.5 1776243.8 +440.75 1786986.8 +441 1793931.9 +441.25 1793862.4 +441.5 1796310.6 +441.75 1786149.1 +442 1775421.4 +442.25 1771857.1 +442.5 1777435.1 +442.75 1778028.1 +443 1770159.4 +443.25 1774165.6 +443.5 1774503.5 +443.75 1767918.3 +444 1760056.1 +444.25 1749255.4 +444.5 1749071.1 +444.75 1748871.9 +445 1738109.5 +445.25 1729611.5 +445.5 1725513.9 +445.75 1726621.4 +446 1727422.1 +446.25 1728260.6 +446.5 1734204.6 +446.75 1733336.9 +447 1719656 +447.25 1718650.6 +447.5 1719804 +447.75 1719936.3 +448 1717081.9 +448.25 1716824.9 +448.5 1716061 +448.75 1721095.8 +449 1728264.9 +449.25 1726953.4 +449.5 1716566.6 +449.75 1712042.8 +450 1721373.6 +450.25 1722663.6 +450.5 1721448.5 +450.75 1716256.6 +451 1713901.4 +451.25 1713433.1 +451.5 1707166.3 +451.75 1702758.8 +452 1699809.9 +452.25 1695113.6 +452.5 1695600.4 +452.75 1694005.9 +453 1685199.4 +453.25 1682080.4 +453.5 1677539.9 +453.75 1668315.1 +454 1661892 +454.25 1658885.8 +454.5 1652023.8 +454.75 1647821.4 +455 1653870.3 +455.25 1655796.6 +455.5 1652573.5 +455.75 1650919.9 +456 1645567.6 +456.25 1644047.5 +456.5 1643894 +456.75 1632302.3 +457 1628069.8 +457.25 1634911.5 +457.5 1635628.8 +457.75 1634277.9 +458 1633629.5 +458.25 1631401.5 +458.5 1630673.9 +458.75 1627482.8 +459 1616698.5 +459.25 1612787.8 +459.5 1616328.4 +459.75 1624661.9 +460 1628465 +460.25 1623522.4 +460.5 1627155.5 +460.75 1626308.3 +461 1615348.1 +461.25 1602593.8 +461.5 1600546.1 +461.75 1603932.5 +462 1610761.1 +462.25 1606102.9 +462.5 1596724.5 +462.75 1586921.1 +463 1578337.1 +463.25 1569114.5 +463.5 1566897 +463.75 1572286 +464 1566624.5 +464.25 1565516.8 +464.5 1569322.1 +464.75 1562960.5 +465 1553178 +465.25 1551459.1 +465.5 1556376.8 +465.75 1553559.1 +466 1542759.3 +466.25 1541128.6 +466.5 1538344.3 +466.75 1532339.4 +467 1530133.8 +467.25 1532882.8 +467.5 1531904.8 +467.75 1530793.8 +468 1536542.5 +468.25 1535217.8 +468.5 1525556.3 +468.75 1518201.3 +469 1507882.1 +469.25 1502862 +469.5 1509868.3 +469.75 1521067.6 +470 1528903.4 +470.25 1532137.9 +470.5 1527241.5 +470.75 1516269 +471 1516899.4 +471.25 1512895.5 +471.5 1502782.6 +471.75 1499234.1 +472 1498469.4 +472.25 1497750 +472.5 1496420.5 +472.75 1501413.9 +473 1500728.9 +473.25 1498394.8 +473.5 1503446.9 +473.75 1505751.4 +474 1505913.6 +474.25 1508314.3 +474.5 1505014.5 +474.75 1497313.5 +475 1496268.5 +475.25 1498246.8 +475.5 1501794.9 +475.75 1497938.4 +476 1484759.5 +476.25 1483181.9 +476.5 1485210.5 +476.75 1485373.8 +477 1480158.3 +477.25 1475614.1 +477.5 1473741.4 +477.75 1470760.8 +478 1457524.4 +478.25 1446894.6 +478.5 1448152.5 +478.75 1447558.8 +479 1438753.6 +479.25 1429775.6 +479.5 1428072.4 +479.75 1433916.6 +480 1439950.5 +480.25 1449275.8 +480.5 1451282.8 +480.75 1450007.1 +481 1445509.4 +481.25 1443521.5 +481.5 1447193.8 +481.75 1445733.5 +482 1436123.1 +482.25 1435311.3 +482.5 1442101.6 +482.75 1446306.5 +483 1445001.1 +483.25 1444808.5 +483.5 1441279.8 +483.75 1434061.8 +484 1428745.6 +484.25 1423363.3 +484.5 1417527.8 +484.75 1422485 +485 1430041.9 +485.25 1429134.9 +485.5 1422919.8 +485.75 1420826.9 +486 1421817.3 +486.25 1416055.4 +486.5 1409856 +486.75 1409324.4 +487 1404520 +487.25 1396494 +487.5 1395073.4 +487.75 1395668.3 +488 1392418.4 +488.25 1383897.8 +488.5 1373803.6 +488.75 1370892.6 +489 1377257.5 +489.25 1376917.9 +489.5 1375367.3 +489.75 1381322.8 +490 1382834.1 +490.25 1380283.6 +490.5 1378016.6 +490.75 1375351.9 +491 1368116.4 +491.25 1361174.9 +491.5 1360850.8 +491.75 1356984.8 +492 1356367.3 +492.25 1356419.1 +492.5 1353448.5 +492.75 1355862 +493 1357079 +493.25 1351499.1 +493.5 1351581.9 +493.75 1355261.3 +494 1351394.5 +494.25 1347087.3 +494.5 1348133.3 +494.75 1347305.3 +495 1341126 +495.25 1335302.4 +495.5 1327909.8 +495.75 1328644.3 +496 1330083.4 +496.25 1325232.6 +496.5 1324170.6 +496.75 1322853 +497 1322835.3 +497.25 1315180.9 +497.5 1301590.5 +497.75 1293455.3 +498 1294331.5 +498.25 1298754.1 +498.5 1294798.5 +498.75 1290012.8 +499 1290113.6 +499.25 1295286 +499.5 1302034.3 +499.75 1297500.1 +500 1292354 +500.25 1294498.3 +500.5 1293583.3 +500.75 1289748.5 +501 1289326.8 +501.25 1286158 +501.5 1281622.1 +501.75 1282404.6 +502 1287983.9 +502.25 1293992.8 +502.5 1287701.1 +502.75 1281499.9 +503 1278317.4 +503.25 1283854.1 +503.5 1289554.1 +503.75 1290190.6 +504 1283919.8 +504.25 1286448.4 +504.5 1290942.6 +504.75 1289339.3 +505 1285933.8 +505.25 1281436.1 +505.5 1285900.8 +505.75 1293025.5 +506 1291699.3 +506.25 1288478.1 +506.5 1284267.9 +506.75 1276413.5 +507 1269307.9 +507.25 1259083.4 +507.5 1257634.5 +507.75 1258134 +508 1256660.3 +508.25 1256236.9 +508.5 1258935 +508.75 1253695.4 +509 1249931.4 +509.25 1252508.6 +509.5 1246486.5 +509.75 1245963.5 +510 1249234 +510.25 1248267 +510.5 1249408.1 +510.75 1250755.9 +511 1251529.3 +511.25 1251491.8 +511.5 1247370.5 +511.75 1239423.8 +512 1232990.5 +512.25 1228761.1 +512.5 1226554.4 +512.75 1224700 +513 1227478.4 +513.25 1229395.5 +513.5 1227403.1 +513.75 1218417.5 +514 1215040.4 +514.25 1212872.5 +514.5 1206063.8 +514.75 1210264.8 +515 1223738.6 +515.25 1228386.4 +515.5 1218659.6 +515.75 1208407.6 +516 1202559.6 +516.25 1201683.3 +516.5 1201528.4 +516.75 1199038.3 +517 1201962.3 +517.25 1199344.4 +517.5 1193669.4 +517.75 1188708.4 +518 1187095.4 +518.25 1185891.6 +518.5 1188617.8 +518.75 1188549.3 +519 1181793.1 +519.25 1176193.9 +519.5 1176956.4 +519.75 1180480.8 +520 1183432.1 +520.25 1183040.6 +520.5 1176009.3 +520.75 1173445.1 +521 1169827.1 +521.25 1162797.9 +521.5 1160264.9 +521.75 1164991.6 +522 1168465 +522.25 1164546.9 +522.5 1156463 +522.75 1157401.3 +523 1166487.6 +523.25 1170777.3 +523.5 1168869 +523.75 1165130.5 +524 1160797.8 +524.25 1160120.5 +524.5 1159683.1 +524.75 1160033.5 +525 1160194.1 +525.25 1164088.8 +525.5 1167823.6 +525.75 1162815.4 +526 1157384.1 +526.25 1156845.4 +526.5 1154551.4 +526.75 1153977.6 +527 1158619.5 +527.25 1158447.9 +527.5 1155515 +527.75 1154944.4 +528 1158838.5 +528.25 1158247.5 +528.5 1157251.8 +528.75 1154978.5 +529 1151367.9 +529.25 1155403.9 +529.5 1153634.1 +529.75 1153945.8 +530 1159796.6 +530.25 1163021.8 +530.5 1168206.4 +530.75 1168001.5 +531 1169477.6 +531.25 1176983.4 +531.5 1182320.3 +531.75 1177096.3 +532 1170733.3 +532.25 1172641.3 +532.5 1175371.5 +532.75 1173782 +533 1175465.3 +533.25 1185288 +533.5 1194485.4 +533.75 1201897.8 +534 1211745.8 +534.25 1221420.9 +534.5 1230429.4 +534.75 1232106.5 +535 1238404.5 +535.25 1240704.3 +535.5 1241484 +535.75 1242437.8 +536 1243922.9 +536.25 1244743.1 +536.5 1245324.6 +536.75 1253419.6 +537 1261170.8 +537.25 1259614.3 +537.5 1262544 +537.75 1262524.3 +538 1258943.4 +538.25 1262126 +538.5 1262049.4 +538.75 1257375.9 +539 1254319.3 +539.25 1244989.3 +539.5 1232609.4 +539.75 1223610.9 +540 1217007.8 +540.25 1211642 +540.5 1207798 +540.75 1203146.8 +541 1194918 +541.25 1190041.4 +541.5 1187470 +541.75 1189129.8 +542 1188181.1 +542.25 1184857.5 +542.5 1184800.8 +542.75 1185209 +543 1191958.5 +543.25 1195784 +543.5 1197103.4 +543.75 1191739.1 +544 1190982.3 +544.25 1187908.8 +544.5 1184963.4 +544.75 1178761.3 +545 1169716.3 +545.25 1165353.4 +545.5 1162841.6 +545.75 1163474.6 +546 1169082.1 +546.25 1168422.4 +546.5 1163836.4 +546.75 1156591.3 +547 1150609.4 +547.25 1153930.6 +547.5 1154354.5 +547.75 1155066.3 +548 1153551.3 +548.25 1153287.9 +548.5 1147791.4 +548.75 1148254.3 +549 1149599.9 +549.25 1151154.5 +549.5 1152601.1 +549.75 1156727 +550 1156801.9 +550.25 1148714.6 +550.5 1144826 +550.75 1150254 +551 1153632.4 +551.25 1157473.5 +551.5 1161323.6 +551.75 1167006.8 +552 1164153.1 +552.25 1153990.9 +552.5 1146259.1 +552.75 1145339.4 +553 1141835.9 +553.25 1136509.5 +553.5 1139784.9 +553.75 1143411.4 +554 1142748.1 +554.25 1139917.3 +554.5 1134869.5 +554.75 1132709.6 +555 1128741.8 +555.25 1117004.9 +555.5 1116212.8 +555.75 1119292.3 +556 1122684.3 +556.25 1124258.3 +556.5 1129085.4 +556.75 1131197 +557 1126582.9 +557.25 1123167 +557.5 1125347.3 +557.75 1124935.3 +558 1122355 +558.25 1121327.3 +558.5 1117540.4 +558.75 1122264.3 +559 1128854.9 +559.25 1126450.9 +559.5 1124803.8 +559.75 1132211.8 +560 1142420.5 +560.25 1145783.5 +560.5 1141882.8 +560.75 1137414.9 +561 1136686.4 +561.25 1133355.9 +561.5 1130061.3 +561.75 1129527.8 +562 1127887.3 +562.25 1125849 +562.5 1126815.9 +562.75 1126824.6 +563 1124284.1 +563.25 1128773.5 +563.5 1131417.6 +563.75 1127122.9 +564 1122313.5 +564.25 1119005.5 +564.5 1118918.1 +564.75 1122533.1 +565 1126208.9 +565.25 1125924.9 +565.5 1129934.8 +565.75 1130425.9 +566 1129626.6 +566.25 1131710.1 +566.5 1129800.3 +566.75 1125674.1 +567 1127599.6 +567.25 1130178.4 +567.5 1124916.5 +567.75 1120551.3 +568 1121604 +568.25 1116099.8 +568.5 1108937.6 +568.75 1104255.8 +569 1101433.1 +569.25 1101746.3 +569.5 1101605.4 +569.75 1098796.6 +570 1097942.6 +570.25 1101207.6 +570.5 1103963.8 +570.75 1100024.6 +571 1103174.9 +571.25 1106569.6 +571.5 1102378.8 +571.75 1093864.9 +572 1087572.6 +572.25 1085689.9 +572.5 1090010.1 +572.75 1094077.4 +573 1096561.1 +573.25 1092412.3 +573.5 1092381.9 +573.75 1089630.4 +574 1086686.3 +574.25 1086046.5 +574.5 1083417.4 +574.75 1081304.8 +575 1074209.5 +575.25 1071625 +575.5 1076971.9 +575.75 1081062.3 +576 1081801.5 +576.25 1084214.1 +576.5 1081886.9 +576.75 1081008.4 +577 1081755.1 +577.25 1083560.5 +577.5 1087249.6 +577.75 1087381.6 +578 1079697.5 +578.25 1075411 +578.5 1072701.3 +578.75 1070872.9 +579 1071697.9 +579.25 1069330.8 +579.5 1066753.4 +579.75 1067499.9 +580 1067383.6 +580.25 1066464.1 +580.5 1071819.8 +580.75 1074891 +581 1071060.9 +581.25 1066458.1 +581.5 1063719.4 +581.75 1060880.8 +582 1060642.3 +582.25 1060672 +582.5 1060829.3 +582.75 1060341.3 +583 1061728.1 +583.25 1065953.4 +583.5 1063801.6 +583.75 1061604.3 +584 1055739.1 +584.25 1050910 +584.5 1046553 +584.75 1045870.1 +585 1048875.9 +585.25 1043639.8 +585.5 1045800.9 +585.75 1046739.4 +586 1042503.1 +586.25 1044617.1 +586.5 1046385.9 +586.75 1040544.2 +587 1040089.4 +587.25 1038012.8 +587.5 1036527.8 +587.75 1038663.1 +588 1040656.5 +588.25 1044382.6 +588.5 1042672.1 +588.75 1036847.5 +589 1033950.8 +589.25 1034156.7 +589.5 1033869 +589.75 1033159.6 +590 1033439.9 +590.25 1030712.3 +590.5 1027132.4 +590.75 1022036.4 +591 1023622.7 +591.25 1025710.8 +591.5 1023263.2 +591.75 1023945.9 +592 1023429.3 +592.25 1021452.8 +592.5 1019797.4 +592.75 1016884.8 +593 1018851.8 +593.25 1025368.1 +593.5 1034133.9 +593.75 1028563.4 +594 1018313.6 +594.25 1017954.4 +594.5 1020829.7 +594.75 1021785.9 +595 1017586.1 +595.25 1013945.4 +595.5 1010149.1 +595.75 1013386.6 +596 1016457.9 +596.25 1022689.5 +596.5 1026612 +596.75 1029250.6 +597 1033627.1 +597.25 1033138.3 +597.5 1028136.6 +597.75 1023453.5 +598 1020280.6 +598.25 1021780.8 +598.5 1020553.9 +598.75 1016758.2 +599 1015438.5 +599.25 1015978.6 +599.5 1012284.8 +599.75 1011330.8 +600 1008819.9 +600.25 1004821.8 +600.5 1000596.4 +600.75 998393.13 +601 995201.69 +601.25 995730.13 +601.5 995949.63 +601.75 989677.88 +602 985717.5 +602.25 986236.06 +602.5 988512.88 +602.75 989754.5 +603 990489.44 +603.25 994576.94 +603.5 993048.75 +603.75 984287.5 +604 978011.25 +604.25 977244.56 +604.5 977962 +604.75 979203.81 +605 974236.19 +605.25 973293.75 +605.5 971040.13 +605.75 973387.13 +606 975557.56 +606.25 970369.88 +606.5 966203.5 +606.75 960739.88 +607 964301.56 +607.25 966021.38 +607.5 963304.56 +607.75 959206 +608 958453.88 +608.25 960149.25 +608.5 963612.13 +608.75 968617 +609 963354.06 +609.25 963302.75 +609.5 965367.25 +609.75 963373.75 +610 960514.44 +610.25 959287.06 +610.5 960816.56 +610.75 960682.56 +611 958888.88 +611.25 959582.06 +611.5 964020.88 +611.75 965499.19 +612 965732.38 +612.25 965854.25 +612.5 964460.25 +612.75 956140.31 +613 945270.19 +613.25 943902.88 +613.5 944934.38 +613.75 945450.69 +614 943424 +614.25 940625.75 +614.5 942451.5 +614.75 942617.13 +615 937849.81 +615.25 936638.44 +615.5 933978.38 +615.75 930378.5 +616 930229.19 +616.25 932766.75 +616.5 934757.81 +616.75 939464.88 +617 942382.75 +617.25 940803.38 +617.5 939563.56 +617.75 939755.06 +618 939337.94 +618.25 944040.81 +618.5 943577.69 +618.75 938232.44 +619 936114.75 +619.25 940111.63 +619.5 940598.25 +619.75 936406.75 +620 939929.63 +620.25 940733.31 +620.5 939810.19 +620.75 935583.13 +621 932527.25 +621.25 930892.19 +621.5 928886.56 +621.75 924113.5 +622 917863.25 +622.25 915756.19 +622.5 917548.31 +622.75 916658.63 +623 916092.25 +623.25 918931.38 +623.5 922874.63 +623.75 923372.56 +624 920365.19 +624.25 918067.38 +624.5 918719.38 +624.75 920223.5 +625 919237.63 +625.25 917453.31 +625.5 912941.56 +625.75 915525.94 +626 919042.63 +626.25 920197.19 +626.5 915250.44 +626.75 911994.94 +627 910759.94 +627.25 906807.13 +627.5 904890.81 +627.75 901997.31 +628 899049.94 +628.25 896737.81 +628.5 895632.88 +628.75 895162.63 +629 896084 +629.25 891708 +629.5 886417.69 +629.75 884804.56 +630 884452.31 +630.25 883422.19 +630.5 886438.88 +630.75 888678.44 +631 887838.69 +631.25 883922.81 +631.5 883770 +631.75 882785.75 +632 882376.63 +632.25 883959.19 +632.5 888401.44 +632.75 888288.13 +633 887058.63 +633.25 886127.06 +633.5 886282.06 +633.75 889915.94 +634 890009.31 +634.25 886422.63 +634.5 884907.38 +634.75 882514.44 +635 879330.75 +635.25 878097.5 +635.5 874117.63 +635.75 869476.25 +636 871763.06 +636.25 869282.81 +636.5 868225.06 +636.75 869493.75 +637 869202.75 +637.25 866313.69 +637.5 870447.5 +637.75 870931.25 +638 868914.69 +638.25 869245.44 +638.5 869558.44 +638.75 868404.56 +639 867494.81 +639.25 870818.75 +639.5 869411.13 +639.75 863907.94 +640 862334.75 +640.25 859428.44 +640.5 857676.81 +640.75 858115.75 +641 859157.81 +641.25 860457.56 +641.5 862238 +641.75 866016.63 +642 864924.44 +642.25 860784.44 +642.5 858910.19 +642.75 856738.38 +643 853747.19 +643.25 852618.13 +643.5 850396.56 +643.75 848821.06 +644 846486.25 +644.25 845406.13 +644.5 843585.19 +644.75 845783.31 +645 844312.5 +645.25 839855.19 +645.5 835532.25 +645.75 839842.5 +646 846063.81 +646.25 847409.19 +646.5 843712.44 +646.75 838911.25 +647 836878.31 +647.25 834700.25 +647.5 832856.44 +647.75 831639.75 +648 834803.88 +648.25 834987.13 +648.5 834297.88 +648.75 832750.25 +649 830319.5 +649.25 832910.81 +649.5 830666.44 +649.75 821469.69 +650 820516.06 +650.25 822797.81 +650.5 823511.69 +650.75 824743.44 +651 824180.13 +651.25 819643.19 +651.5 820702.13 +651.75 822136.06 +652 818950.69 +652.25 816432.88 +652.5 817508.38 +652.75 817674.81 +653 819124.25 +653.25 819394.75 +653.5 816789.06 +653.75 816669.94 +654 820081.81 +654.25 824900 +654.5 823761.06 +654.75 821367.75 +655 818880.5 +655.25 818732.13 +655.5 821148.19 +655.75 822767.81 +656 821912.88 +656.25 821550.75 +656.5 820593.94 +656.75 815363.81 +657 815069.88 +657.25 819769.56 +657.5 819719.69 +657.75 815129.88 +658 813328.75 +658.25 813253.5 +658.5 811602.81 +658.75 811337.69 +659 808509.88 +659.25 809364.69 +659.5 813852.69 +659.75 815239.94 +660 815308.63 +660.25 809592.19 +660.5 806597.94 +660.75 807793.13 +661 808957.38 +661.25 809882.31 +661.5 806841.5 +661.75 803598.81 +662 800297.94 +662.25 798922.38 +662.5 796784.5 +662.75 797974.31 +663 802693.06 +663.25 803223.19 +663.5 799870.5 +663.75 795003.81 +664 794039.75 +664.25 793195.94 +664.5 792568.75 +664.75 791381.13 +665 791248.5 +665.25 789844.31 +665.5 789140.38 +665.75 785890.88 +666 783905.06 +666.25 784841.25 +666.5 782832.19 +666.75 780277.56 +667 779504 +667.25 777368.94 +667.5 779062.19 +667.75 786274.75 +668 788626.19 +668.25 787589.75 +668.5 784257.31 +668.75 780275.44 +669 779009.5 +669.25 779507.69 +669.5 779595 +669.75 776462.69 +670 775947.13 +670.25 776863.56 +670.5 780520.06 +670.75 783941.44 +671 788016.56 +671.25 786669.88 +671.5 781785.94 +671.75 774531.31 +672 770892.81 +672.25 767750.69 +672.5 768530.31 +672.75 769296.88 +673 770497.13 +673.25 769148 +673.5 769070.06 +673.75 768142.69 +674 765866.31 +674.25 761172.13 +674.5 761680.56 +674.75 763271.38 +675 760363.81 +675.25 755578 +675.5 757222.88 +675.75 759284.19 +676 758119.88 +676.25 758245.19 +676.5 755421.94 +676.75 755051.44 +677 760503.94 +677.25 763476.25 +677.5 764657.38 +677.75 763577.06 +678 760549.56 +678.25 759057.13 +678.5 756331.44 +678.75 757762.81 +679 760118.69 +679.25 762070.31 +679.5 761398.81 +679.75 760951.06 +680 758545.44 +680.25 756809.81 +680.5 756186.38 +680.75 756801.94 +681 755537.44 +681.25 752102.81 +681.5 748281.13 +681.75 748943.38 +682 748347.56 +682.25 750910.44 +682.5 749442.81 +682.75 741315.31 +683 737509.13 +683.25 738602.19 +683.5 741549.69 +683.75 738170.69 +684 736854.19 +684.25 737639.69 +684.5 737931.94 +684.75 735280 +685 731798.75 +685.25 731048.63 +685.5 729906.25 +685.75 730414.13 +686 728347 +686.25 726465.56 +686.5 727653.81 +686.75 728950.19 +687 726479.25 +687.25 726738.25 +687.5 727480.13 +687.75 725809.31 +688 721483.31 +688.25 724080 +688.5 728604.25 +688.75 729702.25 +689 728379.31 +689.25 729349.63 +689.5 729535.5 +689.75 732704.13 +690 733923.63 +690.25 732573.44 +690.5 728259.69 +690.75 724505.25 +691 723337.5 +691.25 721695.81 +691.5 718307.69 +691.75 717605.94 +692 718644 +692.25 717085.81 +692.5 715700.19 +692.75 713849.88 +693 710234.63 +693.25 711505.06 +693.5 712629.94 +693.75 713467.25 +694 716486.06 +694.25 716980.94 +694.5 717346 +694.75 719290.69 +695 717734 +695.25 711261.63 +695.5 709943.56 +695.75 710849.88 +696 710720.75 +696.25 708704.81 +696.5 712091.5 +696.75 714660.81 +697 712882.5 +697.25 711312.69 +697.5 713585.13 +697.75 714464.56 +698 714957.38 +698.25 713196.5 +698.5 712658.94 +698.75 710060.94 +699 708929.31 +699.25 709312.13 +699.5 710052.56 +699.75 708890.44 +700 706651.5 +700.25 704415.38 +700.5 703106.38 +700.75 704376.06 +701 704828.31 +701.25 707303.19 +701.5 705181.38 +701.75 705693.88 +702 707451.25 +702.25 709377.94 +702.5 705998.69 +702.75 704414.25 +703 704929.25 +703.25 705614.56 +703.5 706132.81 +703.75 704196.44 +704 701313.31 +704.25 703160.56 +704.5 702822.19 +704.75 699513.44 +705 695716.06 +705.25 693370.25 +705.5 691361.94 +705.75 689705.19 +706 689113.5 +706.25 688375 +706.5 690344.81 +706.75 689433.75 +707 685561.75 +707.25 683656.75 +707.5 681759.38 +707.75 682505.69 +708 681440.75 +708.25 675476.56 +708.5 675852.13 +708.75 678282.56 +709 677175.75 +709.25 674216.81 +709.5 674727.19 +709.75 678179.44 +710 678854.94 +710.25 680370.88 +710.5 680792.69 +710.75 680530 +711 676277.38 +711.25 674233.75 +711.5 673380 +711.75 673110.69 +712 674169.75 +712.25 673177.94 +712.5 669769.69 +712.75 668714.13 +713 665792.38 +713.25 663296.88 +713.5 663951.06 +713.75 662631.75 +714 662430 +714.25 662423.25 +714.5 662929.88 +714.75 662698 +715 663803.81 +715.25 666555.13 +715.5 665099.81 +715.75 661908.06 +716 659819.56 +716.25 659230.69 +716.5 660102.69 +716.75 661153.88 +717 661979.06 +717.25 661909 +717.5 663463.56 +717.75 663209.38 +718 662517.94 +718.25 660283.81 +718.5 659957.75 +718.75 655754.94 +719 650916.63 +719.25 651704.94 +719.5 651053.5 +719.75 650874.44 +720 648461 +720.25 646483.56 +720.5 647897.06 +720.75 651520.13 +721 652278.94 +721.25 649209 +721.5 645939.31 +721.75 645693 +722 646469.13 +722.25 645956.13 +722.5 648739.75 +722.75 650769.56 +723 652826.88 +723.25 653334.13 +723.5 654901.75 +723.75 655290.63 +724 650683.06 +724.25 645608.75 +724.5 642496.69 +724.75 642778.5 +725 645416.63 +725.25 647453 +725.5 648494.31 +725.75 648317.5 +726 650036.13 +726.25 648471.75 +726.5 645797.44 +726.75 645053.25 +727 645100.06 +727.25 643118.25 +727.5 640901.88 +727.75 640069.5 +728 638585.94 +728.25 640576.44 +728.5 640665.38 +728.75 638959.19 +729 639296.19 +729.25 637456.69 +729.5 638448.69 +729.75 639273.81 +730 639892.69 +730.25 635098.94 +730.5 634867.56 +730.75 636936.19 +731 635442.25 +731.25 632461.38 +731.5 629351.25 +731.75 627338 +732 631189.69 +732.25 634519.75 +732.5 634891.19 +732.75 636046.5 +733 629180.19 +733.25 624136.75 +733.5 626713.13 +733.75 630756.19 +734 632298.31 +734.25 634153.38 +734.5 633021.5 +734.75 627788.94 +735 626473.69 +735.25 624262.19 +735.5 619663.25 +735.75 616147.75 +736 615912.75 +736.25 618604.38 +736.5 619396.06 +736.75 620474.94 +737 617979.25 +737.25 614180.88 +737.5 613250.94 +737.75 611798.5 +738 612590.56 +738.25 615531.63 +738.5 615278.31 +738.75 610561.69 +739 605311.06 +739.25 602922.63 +739.5 600693.75 +739.75 602779.75 +740 606132.31 +740.25 608161.81 +740.5 608467.31 +740.75 611092.75 +741 610914.44 +741.25 611816.56 +741.5 612365.06 +741.75 607630.13 +742 604835.5 +742.25 603514.75 +742.5 602385.44 +742.75 604093.63 +743 603717.5 +743.25 598784.75 +743.5 595480.38 +743.75 595553.81 +744 597211.25 +744.25 594670.06 +744.5 589952.19 +744.75 587464.5 +745 589564.63 +745.25 591329.63 +745.5 592251 +745.75 594645.56 +746 596140.5 +746.25 596980.06 +746.5 592818.13 +746.75 591482.63 +747 593109.13 +747.25 593872.31 +747.5 592142.81 +747.75 588379.56 +748 587738.19 +748.25 591816.19 +748.5 594692.63 +748.75 596238.56 +749 596203.19 +749.25 593984.06 +749.5 591470.56 +749.75 589668.06 +750 589539.56 +750.25 587539.5 +750.5 588078.38 +750.75 588013 +751 589254.06 +751.25 589538.25 +751.5 585786 +751.75 584380.19 +752 584562.75 +752.25 584090.19 +752.5 582969 +752.75 583033.81 +753 583691.44 +753.25 583326.94 +753.5 580647.19 +753.75 579375.38 +754 580482.69 +754.25 583971.69 +754.5 588095.38 +754.75 587671.75 +755 584664.13 +755.25 580207.38 +755.5 581747.56 +755.75 584244.19 +756 585741.25 +756.25 583771.44 +756.5 582598.75 +756.75 579569.94 +757 576189.75 +757.25 577186.88 +757.5 576083.69 +757.75 574788.25 +758 574805.13 +758.25 576473.38 +758.5 578858.38 +758.75 580279 +759 578627.56 +759.25 575946.06 +759.5 571912.69 +759.75 573074.56 +760 574446.75 +760.25 574044.69 +760.5 571292.19 +760.75 570842.5 +761 572829.75 +761.25 571342.5 +761.5 567359 +761.75 566842.44 +762 564343.25 +762.25 560879.69 +762.5 561552.88 +762.75 561738.06 +763 565009.56 +763.25 563379.38 +763.5 559874.56 +763.75 557685.5 +764 558636.69 +764.25 556929.44 +764.5 559148.69 +764.75 557934.5 +765 555639.31 +765.25 555598.88 +765.5 554140.81 +765.75 553764 +766 555813.56 +766.25 557715.13 +766.5 556766.63 +766.75 553388.38 +767 553689.19 +767.25 552361.94 +767.5 549275.38 +767.75 546875.94 +768 545951.69 +768.25 546579.56 +768.5 548343.44 +768.75 552333.44 +769 554473.88 +769.25 555096 +769.5 552315.38 +769.75 548679.31 +770 548298 +770.25 549708.56 +770.5 550784.44 +770.75 549442.88 +771 547505.44 +771.25 548832.44 +771.5 547024.31 +771.75 546840.13 +772 549281.5 +772.25 548038.56 +772.5 543562.63 +772.75 541425.38 +773 542223.63 +773.25 542108.38 +773.5 542693.5 +773.75 543162.5 +774 540816 +774.25 537692.06 +774.5 536428.38 +774.75 534598.19 +775 537006 +775.25 538685.94 +775.5 538467 +775.75 539419.31 +776 541301.69 +776.25 541593.19 +776.5 542327.56 +776.75 540078.75 +777 538245.31 +777.25 534995.06 +777.5 536979.88 +777.75 540450.88 +778 540002.88 +778.25 536972.56 +778.5 533131 +778.75 533809.06 +779 535880.19 +779.25 536224.69 +779.5 535568.25 +779.75 532735.69 +780 531500.19 +780.25 530412.13 +780.5 530826.13 +780.75 532588.88 +781 531303.19 +781.25 530334.56 +781.5 526921 +781.75 526007.44 +782 523844.53 +782.25 523018.28 +782.5 523072.91 +782.75 524973.13 +783 524994.81 +783.25 524049.94 +783.5 522495.63 +783.75 522117.88 +784 522004.81 +784.25 524386.75 +784.5 524167.69 +784.75 522804.41 +785 524075.84 +785.25 523794.63 +785.5 524421.63 +785.75 525128.06 +786 525220.69 +786.25 526690 +786.5 527387.38 +786.75 526235.94 +787 524405.5 +787.25 520809.81 +787.5 518277.81 +787.75 518273.97 +788 517787.09 +788.25 516536.13 +788.5 514878.84 +788.75 515915.19 +789 517147.38 +789.25 517477.19 +789.5 517660.31 +789.75 515143.19 +790 514749.47 +790.25 513084.78 +790.5 512745.13 +790.75 514285.84 +791 513080.91 +791.25 511968.94 +791.5 509350.88 +791.75 509904.03 +792 510079.44 +792.25 510750.63 +792.5 512209.78 +792.75 512642 +793 513602.5 +793.25 513052.75 +793.5 511318 +793.75 508011.09 +794 504846.16 +794.25 505120.28 +794.5 505149.41 +794.75 506373.41 +795 506428.69 +795.25 504307.09 +795.5 504353.5 +795.75 502961.31 +796 501131.91 +796.25 500037.53 +796.5 497239.16 +796.75 495365.97 +797 496682.75 +797.25 499013.91 +797.5 503701.16 +797.75 503271.28 +798 497757.19 +798.25 494992.31 +798.5 495098.5 +798.75 496226.06 +799 498268.81 +799.25 500032.88 +799.5 497822.13 +799.75 496989.84 +800 498094.63 +800.25 500035.78 +800.5 499165.91 +800.75 495223.59 +801 497061.84 +801.25 500199.53 +801.5 500762.94 +801.75 499796.28 +802 497121.69 +802.25 496255.97 +802.5 495437.53 +802.75 495095.06 +803 492265.44 +803.25 490032.78 +803.5 489008.72 +803.75 490544.59 +804 491248.69 +804.25 490973.22 +804.5 489060.28 +804.75 487643.09 +805 487874.16 +805.25 486816.94 +805.5 487825.06 +805.75 488631.06 +806 486044.22 +806.25 484211.97 +806.5 484154.44 +806.75 484691.09 +807 483566.34 +807.25 483796.91 +807.5 484748.91 +807.75 486601.5 +808 486345.06 +808.25 487626.44 +808.5 487988.38 +808.75 483553.19 +809 482400 +809.25 484631.25 +809.5 484525.84 +809.75 484420.97 +810 483822.38 +810.25 482560.38 +810.5 478778.03 +810.75 477368.91 +811 478615.72 +811.25 479862.63 +811.5 479620.88 +811.75 476253.88 +812 472691.25 +812.25 474312.22 +812.5 475392.81 +812.75 476676.97 +813 474818.53 +813.25 474250.72 +813.5 476908.47 +813.75 479065.22 +814 479594.38 +814.25 476570.31 +814.5 474784.06 +814.75 477470.72 +815 478146.53 +815.25 479050.66 +815.5 476557.09 +815.75 474882.59 +816 474359.47 +816.25 471669.56 +816.5 473566.34 +816.75 474506.75 +817 478385.38 +817.25 480561.13 +817.5 476923.19 +817.75 473688.59 +818 473042.41 +818.25 471746.5 +818.5 470048.28 +818.75 469130.22 +819 465992.03 +819.25 464628.41 +819.5 465318.38 +819.75 467359.97 +820 465750.38 +820.25 465199.69 +820.5 465430.34 +820.75 465782.09 +821 468027.75 +821.25 464785.56 +821.5 461754.81 +821.75 461918.91 +822 464871.34 +822.25 466304.13 +822.5 466432.72 +822.75 463710.03 +823 460761.88 +823.25 459594.25 +823.5 460690.16 +823.75 462998.53 +824 462283.44 +824.25 460382.63 +824.5 459412.81 +824.75 460344.41 +825 461838.09 +825.25 461828.66 +825.5 459945.94 +825.75 458764.19 +826 457890.59 +826.25 458028.47 +826.5 457718.53 +826.75 456281.03 +827 455248.56 +827.25 452641.09 +827.5 449454.91 +827.75 451645.31 +828 454890.25 +828.25 457344.41 +828.5 456423.19 +828.75 453720.22 +829 452935.41 +829.25 452945.06 +829.5 455782.38 +829.75 458271.75 +830 456813.31 +830.25 457465.63 +830.5 458182.94 +830.75 459224.91 +831 460329.88 +831.25 461750.31 +831.5 464210.94 +831.75 467448.88 +832 470777.41 +832.25 473539.66 +832.5 475683.94 +832.75 475804.53 +833 479273.69 +833.25 483105.88 +833.5 484805.69 +833.75 489298.41 +834 494391.84 +834.25 496637.13 +834.5 500589.81 +834.75 507979.69 +835 514463.59 +835.25 519898.59 +835.5 525299.75 +835.75 535320.31 +836 547083.38 +836.25 557775.13 +836.5 570812.63 +836.75 584161.06 +837 598158.31 +837.25 615145.56 +837.5 633760.19 +837.75 656079.19 +838 684918.25 +838.25 718093.88 +838.5 751152.56 +838.75 792431.81 +839 839782.88 +839.25 882846.31 +839.5 921626.69 +839.75 962708.13 +840 992301.63 +840.25 1002625.9 +840.5 995889.19 +840.75 982363.25 +841 965136.56 +841.25 935635.69 +841.5 900264 +841.75 866282.94 +842 832994.13 +842.25 797902.38 +842.5 760037.56 +842.75 722129.31 +843 690678.75 +843.25 666437.38 +843.5 646365.19 +843.75 626022.69 +844 606098.69 +844.25 590375.75 +844.5 576774.31 +844.75 563774.81 +845 555952.19 +845.25 549451.38 +845.5 543540.25 +845.75 537600.81 +846 534780.63 +846.25 531915.88 +846.5 527231.88 +846.75 522762.69 +847 520215.84 +847.25 517249.13 +847.5 516536.47 +847.75 514103.72 +848 511795.22 +848.25 512041.13 +848.5 514387.34 +848.75 513855.16 +849 513681.09 +849.25 513494.16 +849.5 511803.22 +849.75 513932.63 +850 517588.09 +850.25 520776.72 +850.5 523810 +850.75 528603.38 +851 534395.56 +851.25 541262.81 +851.5 547540.94 +851.75 558509.25 +852 569154.19 +852.25 581131.25 +852.5 594577.25 +852.75 611098.19 +853 623629.44 +853.25 642066.38 +853.5 667668.88 +853.75 691952.38 +854 717694.56 +854.25 745224.31 +854.5 778733.38 +854.75 819628.19 +855 863935.56 +855.25 912784 +855.5 955766.13 +855.75 999743.13 +856 1041357 +856.25 1071069.8 +856.5 1092516.8 +856.75 1108065.6 +857 1113768.8 +857.25 1110544.3 +857.5 1102733.9 +857.75 1088833.1 +858 1068276.8 +858.25 1043771.5 +858.5 1011341.9 +858.75 978435.5 +859 952333.69 +859.25 926755.88 +859.5 899804.94 +859.75 867781.19 +860 837200.31 +860.25 813263.69 +860.5 792688.06 +860.75 774009.38 +861 756433.06 +861.25 743736.56 +861.5 730835.06 +861.75 720112.69 +862 711500.06 +862.25 705528.94 +862.5 698132.69 +862.75 687311.88 +863 678653.56 +863.25 676044 +863.5 673070.94 +863.75 666851.44 +864 662007.69 +864.25 656769 +864.5 652148.69 +864.75 649342 +865 646063.25 +865.25 642197.81 +865.5 637794.63 +865.75 637882.63 +866 639784.19 +866.25 641482.31 +866.5 639542.19 +866.75 638010.13 +867 639991.94 +867.25 645667.19 +867.5 650122.63 +867.75 651437.5 +868 654375.06 +868.25 659729.69 +868.5 664736.69 +868.75 659998.06 +869 657290.38 +869.25 662875.19 +869.5 668669.75 +869.75 672576.13 +870 671184.38 +870.25 672637.19 +870.5 676810.81 +870.75 675794.13 +871 674604.31 +871.25 673613.94 +871.5 674629 +871.75 675766.56 +872 677098.19 +872.25 679872.88 +872.5 677895.44 +872.75 676245.75 +873 673818.81 +873.25 671349.5 +873.5 669544.19 +873.75 666626.56 +874 662628.38 +874.25 661039.81 +874.5 660303.81 +874.75 657367.31 +875 654186.56 +875.25 652412.88 +875.5 652998.56 +875.75 657843.38 +876 662026.06 +876.25 661593.88 +876.5 660142.63 +876.75 660615.13 +877 662543.31 +877.25 660375.88 +877.5 656307.56 +877.75 655213.44 +878 654256.44 +878.25 653437.44 +878.5 657906.13 +878.75 658902.06 +879 653253.38 +879.25 650587.81 +879.5 653609.25 +879.75 654202.13 +880 653396.31 +880.25 648219.19 +880.5 648456.81 +880.75 651894.56 +881 653254.5 +881.25 654878.63 +881.5 657016.63 +881.75 655283.63 +882 654823.06 +882.25 651699.69 +882.5 651782.19 +882.75 656070.44 +883 662658.19 +883.25 668708.63 +883.5 672907.44 +883.75 677819 +884 681629.38 +884.25 684387.19 +884.5 686233.5 +884.75 687673.38 +885 689353.44 +885.25 690767.25 +885.5 692426.06 +885.75 692459.31 +886 697117 +886.25 699632.88 +886.5 696493.81 +886.75 694567.63 +887 695802.38 +887.25 699441.63 +887.5 706589.69 +887.75 708867.13 +888 704579.25 +888.25 702414.75 +888.5 704862.69 +888.75 706446.56 +889 703578.94 +889.25 700840.31 +889.5 700409.56 +889.75 699593.94 +890 697000.94 +890.25 697027.88 +890.5 695370.31 +890.75 693116.38 +891 691707.19 +891.25 688264.5 +891.5 688620 +891.75 688695.19 +892 688989.88 +892.25 688017.88 +892.5 688044.75 +892.75 686337.88 +893 678274.31 +893.25 671584.75 +893.5 668974.44 +893.75 670529.06 +894 668100.25 +894.25 665800.19 +894.5 664996.06 +894.75 662058 +895 663518.69 +895.25 664554.88 +895.5 664198.94 +895.75 665031.88 +896 663811.69 +896.25 658828.31 +896.5 656726.44 +896.75 659566.5 +897 662038.5 +897.25 663264.75 +897.5 663843 +897.75 658233.13 +898 651036.69 +898.25 648135.38 +898.5 646088.75 +898.75 646081.19 +899 645752.56 +899.25 645314.75 +899.5 645509.25 +899.75 649346.69 +900 653200.38 +900.25 651014.44 +900.5 646913.56 +900.75 644196.69 +901 644776.44 +901.25 647088.63 +901.5 647428.63 +901.75 646320.31 +902 643718.13 +902.25 641733.31 +902.5 641079.25 +902.75 642911.19 +903 644005.25 +903.25 645343.13 +903.5 644747 +903.75 640295.81 +904 636687.81 +904.25 633766.06 +904.5 631799.06 +904.75 632671.56 +905 635935.63 +905.25 636536.25 +905.5 637093.31 +905.75 635762.5 +906 635922.19 +906.25 634848.38 +906.5 633870.56 +906.75 636302.5 +907 636522.81 +907.25 630539.63 +907.5 627171.44 +907.75 628344.31 +908 629185.5 +908.25 633685.94 +908.5 635819.94 +908.75 632438.81 +909 630533.75 +909.25 629471.81 +909.5 626408.94 +909.75 629522.56 +910 631885.44 +910.25 629482.94 +910.5 626603.5 +910.75 626995 +911 626653.31 +911.25 625882.75 +911.5 623687.81 +911.75 622070.56 +912 623889.69 +912.25 620457.25 +912.5 620371.31 +912.75 621871 +913 623491.19 +913.25 623899.06 +913.5 620926.88 +913.75 617846.06 +914 621341.88 +914.25 625346.69 +914.5 622742.13 +914.75 620980.06 +915 618626 +915.25 615457.63 +915.5 615777.81 +915.75 616898.69 +916 615991.5 +916.25 612656.5 +916.5 612500.63 +916.75 614688.31 +917 615878.19 +917.25 614935.63 +917.5 613573.56 +917.75 615012.69 +918 621574.25 +918.25 621548.94 +918.5 615475.63 +918.75 612089.75 +919 610626.94 +919.25 606892.25 +919.5 605617.44 +919.75 610772.56 +920 615683.25 +920.25 617037.63 +920.5 616559.63 +920.75 617537.75 +921 616751.38 +921.25 616449.5 +921.5 616665.13 +921.75 617150.31 +922 619211.5 +922.25 621970.31 +922.5 620099.06 +922.75 617061.38 +923 618063.56 +923.25 617843.56 +923.5 618005.5 +923.75 618226.44 +924 619871.56 +924.25 619071.94 +924.5 617845.5 +924.75 616499.81 +925 612577.69 +925.25 613112.56 +925.5 614325.94 +925.75 614686.88 +926 612546.06 +926.25 608518.06 +926.5 604745.5 +926.75 604870.44 +927 602799.19 +927.25 601955.38 +927.5 604453.25 +927.75 606147.5 +928 606738 +928.25 610203.31 +928.5 610055.44 +928.75 607111.25 +929 607304.81 +929.25 607593.44 +929.5 606097.94 +929.75 606203.75 +930 603664.69 +930.25 600906.56 +930.5 604564.81 +930.75 603857.94 +931 600106.81 +931.25 599721.19 +931.5 600341.06 +931.75 598711.88 +932 597625.5 +932.25 598012.5 +932.5 598670 +932.75 597625.5 +933 595088.5 +933.25 594471.25 +933.5 593349.75 +933.75 594710.38 +934 595438.13 +934.25 595849.75 +934.5 593494.88 \ No newline at end of file diff --git a/examples/create_signal/README.rst b/examples/create_signal/README.rst new file mode 100644 index 0000000000..95e6536431 --- /dev/null +++ b/examples/create_signal/README.rst @@ -0,0 +1,4 @@ +Signal Creation +=============== + +Below is a gallery of examples on creating a signal and plotting. diff --git a/examples/create_signal/from_tabular_data.py b/examples/create_signal/from_tabular_data.py new file mode 100644 index 0000000000..e0e5a3f69b --- /dev/null +++ b/examples/create_signal/from_tabular_data.py @@ -0,0 +1,50 @@ +""" +Creates a signal1D from tabular data +==================================== + +This example creates a signal from tabular data, where the signal axis is given by an array +of data values (the ``x`` column) and the tabular data are ordered in columns with 5 columns +containing each 20 values and each column corresponding to a position in the +navigation space (linescan). +""" + +import numpy as np +import hyperspy.api as hs + +#%% +# Create a set of tabular data: + +x = np.linspace(0, 10, 20) +y = np.random.default_rng().random((20, 5)) + +#%% +# Define the axes of the signal and then create the signal: + +axes = [ + # length of the navigation axis + dict(size=y.shape[1], scale=0.1, name="Position", units="nm"), + # use values to define non-uniform axis for the signal axis + dict(axis=x, name="Energy", units="eV"), + ] + +s = hs.signals.Signal1D(y.T, axes=axes) + +#%% +# Convert the non-uniform signal axis to a uniform axis, because non-uniform axes do not +# support all functionalities of HyperSpy. +# In this case, the error introduced during conversion to uniform `scale` is negligeable. + +s.axes_manager.signal_axes[0].convert_to_uniform_axis() + +#%% +# Set title of the dataset and label for the data axis: + +s.metadata.set_item("General.title", "Random test data") +s.metadata.set_item("Signal.quantity", "Intensity (counts)") + +#%% +# Plot the dataset: + +s.plot() +# Choose the second figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/create_signal/from_tabular_data_file.py b/examples/create_signal/from_tabular_data_file.py new file mode 100644 index 0000000000..94a0e8f0d9 --- /dev/null +++ b/examples/create_signal/from_tabular_data_file.py @@ -0,0 +1,44 @@ +""" +Creates a signal1D from a text file +=================================== + +This example creates a signal from tabular data imported from a txt file using +:func:`numpy.loadtxt`. The signal axis and the EELS intensity values are +given by the first and second columns, respectively. + +The tabular data are taken from https://eelsdb.eu/spectra/la2nio4-structure-of-k2nif4/ +""" + +import numpy as np +import hyperspy.api as hs + +#%% +# Read tabular data from a text file: +x, y = np.loadtxt("La2NiO4_eels.txt", unpack=True) + +#%% +# Define the axes of the signal and then create the signal: + +axes = [ + # use values from first column to define non-uniform signal axis + dict(axis=x, name="Energy", units="eV"), + ] + +s = hs.signals.Signal1D(y, axes=axes) + +#%% +# Convert the non-uniform axis to a uniform axis, because non-uniform axes do not +# support all functionalities of HyperSpy. +# In this case, the error introduced during conversion to uniform `scale` is negligeable. + +s.axes_manager.signal_axes[0].convert_to_uniform_axis() + +#%% +# Set title of the dataset and label for the data axis: +s.metadata.set_item("General.title", "La2NiO4 EELS") +s.metadata.set_item("Signal.quantity", "Intensity (counts)") + +#%% +# Plot the dataset: + +s.plot() diff --git a/examples/data_navigation/line_spectrum.py b/examples/create_signal/line_spectrum.py similarity index 79% rename from examples/data_navigation/line_spectrum.py rename to examples/create_signal/line_spectrum.py index 8b14f5c5ee..a22c56499d 100644 --- a/examples/data_navigation/line_spectrum.py +++ b/examples/create_signal/line_spectrum.py @@ -1,9 +1,12 @@ -"""Creates a line spectrum and plots it +""" +Creates a line spectrum +======================= + +This example creates a line spectrum and plots it. """ import numpy as np import hyperspy.api as hs -import matplotlib.pyplot as plt # Create a line spectrum with random data s = hs.signals.Signal1D(np.random.random((100, 1024))) @@ -23,6 +26,4 @@ s.metadata.General.title = 'Random line spectrum' # Plot it -s.plot() - -plt.show() # No necessary when running in the HyperSpy's IPython profile +s.plot() \ No newline at end of file diff --git a/examples/data_navigation/spectrum_image.py b/examples/create_signal/spectrum_image.py similarity index 85% rename from examples/data_navigation/spectrum_image.py rename to examples/create_signal/spectrum_image.py index 06501f66cc..af84863ad7 100644 --- a/examples/data_navigation/spectrum_image.py +++ b/examples/create_signal/spectrum_image.py @@ -1,4 +1,9 @@ -"""Creates a spectrum image and plots it +""" +Creates a spectrum image +======================== + +This example creates a spectrum image, i.e. navigation dimension 2 and +signal dimension 1, and plots it. """ import numpy as np diff --git a/examples/data_visualization/README.rst b/examples/data_visualization/README.rst new file mode 100644 index 0000000000..8ad0ca4d15 --- /dev/null +++ b/examples/data_visualization/README.rst @@ -0,0 +1,10 @@ +Data Visualization +================== + +This gallery shows how to plot data using the convenience functions +:func:`~.api.plot.plot_spectra`, :func:`~.api.plot.plot_images` and +:func:`~.api.plot.plot_signals`. +Unlike the :meth:`~.api.signals.BaseSignal.plot`, these functions +can plot multiple signal together and can be useful to compose figure +comparing signals, etc. + diff --git a/examples/data_visualization/compose_figure.py b/examples/data_visualization/compose_figure.py new file mode 100644 index 0000000000..6a0261aad0 --- /dev/null +++ b/examples/data_visualization/compose_figure.py @@ -0,0 +1,30 @@ +""" +Composing Figure +================ + +This example shows how to compose a figure using +:func:`~.api.plot.plot_images` and :func:`~.api.plot.plot_spectra` +""" + +#%% +import hyperspy.api as hs +import matplotlib.pyplot as plt +import numpy as np + +#%% +# Create the 1D and 2D signals + +#%% +s2D_0 = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) +s2D_1 = -s2D_0 + +s1D_0 = hs.signals.Signal1D(np.arange(100)) +s1D_1 = -s1D_0 + +#%% +# Create an array of :external+matplotlib:class:`matplotlib.axis.Axis` using :external+matplotlib:func:`matplotlib.pyplot.subplots` + +#%% +fig, axs = plt.subplots(ncols=2, nrows=2) +hs.plot.plot_images([s2D_0, s2D_1], ax=axs[:, 0], axes_decor="off") +hs.plot.plot_spectra([s1D_0, s1D_1], ax=axs[:, 1], style="mosaic") diff --git a/examples/data_visualization/specifying_ax.py b/examples/data_visualization/specifying_ax.py new file mode 100644 index 0000000000..f6f2dd1b31 --- /dev/null +++ b/examples/data_visualization/specifying_ax.py @@ -0,0 +1,57 @@ +""" +Specifying Matplotlib Axis +========================== + +""" + +#%% +import hyperspy.api as hs +import matplotlib.pyplot as plt +import numpy as np + +#%% +# +# Signal2D +# -------- +# +# Create two Signal2D + +#%% +s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) +s2 = -s + +#%% +# Create a list of :external+matplotlib:class:`matplotlib.axis.Axis` using +# :external+matplotlib:func:`matplotlib.pyplot.subplots` and +# specify the second matplotlib axis of the list to :func:`~.api.plot.plot_images` + +fig, axs = plt.subplots(ncols=3, nrows=1) +hs.plot.plot_images(s, ax=axs[1], axes_decor="off") + +#%% +# The same can be done for a list of signals and axis + +fig, axs = plt.subplots(ncols=3, nrows=1) +hs.plot.plot_images([s, s2], ax=axs[1:3], axes_decor="off") + +#%% +# +# Signal1D +# -------- +# +# The same can be for :class:`~.api.signals.Signal1D` +# +# Create two Signal2D + +#%% +s = hs.signals.Signal1D(np.arange(100)) +s2 = -s + +#%% +# Create an array of :external+matplotlib:class:`matplotlib.axis.Axis` using +# :external+matplotlib:func:`matplotlib.pyplot.subplots` and +# specify the two las matplotlib axis of the second line to :func:`~.api.plot.plot_spectra` + +#%% +fig, axs = plt.subplots(ncols=3, nrows=2) +hs.plot.plot_spectra([s, s2], ax=axs[1, 1:3], style="mosaic") diff --git a/examples/hyperspy_as_library/coreloss_spectrum.msa b/examples/hyperspy_as_library/coreloss_spectrum.msa deleted file mode 100644 index 7ed567b70c..0000000000 --- a/examples/hyperspy_as_library/coreloss_spectrum.msa +++ /dev/null @@ -1,2062 +0,0 @@ -#FORMAT : EMSA/MAS Spectral Data File -#VERSION : 1.0 -#TITLE : -#OWNER : -#NPOINTS : 2048.00 -#NCOLUMNS : 1.00000 -#XUNITS : eV -#YUNITS : Counts -#DATATYPE : Y -#XPERCHAN : 0.200000 -#OFFSET : 360.000 -#SIGNALTYPE : ELS -#SPECTRUM : -47179 -47304 -47325 -47353 -46918 -46638 -46882 -47240 -47058 -46843 -46758 -46682 -47022 -46769 -46742 -46499 -46512 -46774 -46622 -46676 -46409 -46261 -46162 -45879 -45988 -46177 -46387 -46233 -46035 -45543 -45580 -45597 -45662 -45441 -45727 -45299 -45132 -45164 -45099 -45321 -45184 -45156 -45098 -45302 -45166 -45239 -45028 -44930 -44999 -45088 -44914 -45095 -45294 -44934 -44805 -44695 -44196 -44029 -44030 -43885 -43630 -43950 -44364 -43746 -43513 -44164 -44216 -44140 -43968 -43544 -43621 -43839 -43577 -43350 -42967 -43322 -42955 -43098 -42762 -42720 -42718 -42516 -42526 -42329 -42417 -42105 -41838 -41619 -41644 -42048 -42055 -41894 -42438 -42711 -42551 -42319 -42196 -42401 -41957 -41528 -41749 -41348 -41322 -40803 -40960 -40991 -41251 -41188 -41038 -41291 -41170 -40849 -40934 -41205 -41510 -40884 -40811 -40439 -40207 -40365 -40544 -40425 -40318 -40218 -40402 -40551 -40296 -40030 -40440 -40520 -39982 -39847 -39795 -39848 -39693 -39517 -39671 -39361 -39661 -39545 -39358 -39304 -39242 -39171 -39549 -39133 -39133 -39033 -39175 -39319 -39569 -40025 -39436 -39112 -38948 -39016 -38945 -38660 -38730 -38577 -38371 -38333 -37951 -37618 -37447 -37530 -38008 -38008 -37408 -37740 -37805 -37675 -37657 -37622 -37637 -37555 -37650 -37853 -38001 -38375 -38199 -37555 -37831 -37948 -37799 -37635 -37012 -36803 -37438 -37467 -37511 -37566 -37526 -37588 -37572 -37150 -37006 -37028 -36980 -36628 -36788 -36417 -36056 -36113 -36151 -36040 -36238 -36394 -36495 -36160 -36057 -35959 -35789 -35772 -35761 -35471 -35810 -36020 -35529 -35797 -35782 -35564 -35645 -35288 -35365 -35718 -35786 -35931 -35822 -35739 -35656 -35462 -35637 -35414 -35218 -35512 -35583 -34873 -34755 -34878 -35298 -34842 -34890 -34941 -34906 -34886 -35032 -35194 -35071 -35168 -35101 -34622 -34544 -34308 -34179 -34038 -34233 -34181 -34124 -33967 -33581 -33702 -33927 -34053 -33987 -33976 -33893 -33611 -33230 -33432 -33406 -33423 -33391 -33328 -33221 -32997 -33104 -33332 -33385 -32884 -33141 -33244 -33253 -33233 -33241 -33052 -33382 -33022 -33358 -33424 -32814 -32661 -32660 -32501 -32440 -32515 -32924 -32909 -32878 -32623 -32362 -31985 -31793 -31430 -31536 -31998 -32225 -32344 -32256 -32178 -32243 -32338 -32475 -32079 -31972 -32118 -32039 -31940 -31807 -31667 -31544 -31879 -31830 -31863 -31712 -31997 -31594 -31563 -31489 -31663 -31542 -31174 -31169 -31263 -31311 -31487 -31586 -31373 -31071 -30529 -30863 -30846 -31108 -31309 -31147 -30649 -30522 -30541 -30426 -30210 -30053 -30829 -31130 -30871 -30788 -30507 -30019 -30203 -30344 -30023 -30613 -30506 -30280 -30109 -29726 -29956 -29758 -29833 -30206 -30226 -30061 -30116 -30087 -30000 -30017 -29938 -29683 -29591 -29699 -29568 -29843 -29540 -29584 -29306 -29181 -29458 -29405 -29444 -29319 -29202 -29093 -28913 -28848 -29230 -29090 -29172 -29466 -29492 -29097 -28782 -28872 -29020 -28947 -28940 -29026 -28716 -28708 -28823 -28686 -28726 -28650 -28824 -29105 -28361 -28585 -28305 -28464 -28453 -28373 -28255 -28653 -28600 -28393 -28008 -28169 -28470 -28753 -28595 -27946 -28000 -27857 -27196 -27690 -27853 -27818 -27631 -27509 -27310 -27485 -27541 -27543 -27349 -27416 -27623 -27450 -27284 -26930 -27368 -27048 -27181 -27103 -26909 -26486 -26934 -27283 -27245 -27168 -27242 -27098 -26975 -27095 -27103 -26916 -27378 -27314 -27251 -26738 -26824 -26931 -27272 -26987 -26864 -26916 -26702 -26424 -26581 -26456 -26340 -26084 -26246 -26385 -26442 -26480 -26504 -26339 -26386 -26419 -26550 -26219 -26341 -26221 -26043 -25575 -25442 -25918 -25556 -26132 -25939 -26011 -25832 -25959 -25725 -25733 -26069 -25609 -25744 -25816 -25911 -25919 -26178 -25863 -25584 -25218 -25106 -25310 -25411 -25492 -25652 -25348 -25432 -25480 -25498 -25600 -25215 -25158 -25316 -25291 -25326 -25176 -25061 -25176 -25135 -25214 -24929 -25018 -24773 -24732 -24832 -24599 -24662 -24643 -24677 -24728 -24956 -24803 -24885 -24882 -24469 -24645 -24728 -24627 -24460 -24732 -24657 -24870 -25109 -25044 -24796 -24674 -24552 -24593 -24471 -24097 -24034 -24024 -24034 -24037 -24023 -24036 -23963 -23646 -24196 -24160 -24005 -23766 -23408 -23944 -23933 -23861 -24111 -24125 -23843 -23965 -24335 -24232 -24348 -24070 -24003 -24066 -24023 -23657 -23997 -23735 -23897 -23657 -23556 -23302 -23270 -23361 -23146 -23237 -23226 -23150 -23314 -23173 -23235 -23266 -23352 -23243 -23177 -23042 -23024 -23146 -22973 -22710 -22716 -22863 -22765 -23067 -23049 -23134 -23072 -23004 -22886 -22840 -22769 -22942 -22694 -22490 -22957 -22873 -22896 -22834 -22549 -22369 -22416 -22630 -22575 -22704 -22601 -22318 -22274 -22057 -22289 -22118 -22187 -22208 -21862 -21830 -21725 -21641 -22018 -22183 -21972 -22127 -22189 -22481 -22114 -21871 -21773 -21451 -21722 -21794 -21482 -21576 -21467 -21588 -21449 -21373 -21520 -21517 -21506 -21305 -21165 -21354 -21590 -21238 -21251 -21521 -21476 -21595 -21643 -21544 -21219 -21318 -21516 -21365 -21416 -21413 -21193 -21379 -21401 -21442 -21511 -21912 -21555 -21225 -21422 -21389 -21097 -21123 -20948 -21141 -20862 -20726 -20878 -20701 -20567 -20644 -20516 -20261 -20353 -20301 -20494 -20504 -20542 -20726 -20780 -21120 -20849 -20499 -20397 -20576 -20375 -20911 -20796 -20524 -20238 -20452 -20299 -20302 -20223 -20301 -20094 -20133 -20413 -20192 -20288 -20043 -20060 -20031 -20147 -20073 -19879 -19913 -20038 -20250 -20322 -20134 -20241 -20114 -20014 -19785 -20064 -20052 -20156 -19825 -19884 -19713 -19883 -19913 -19920 -20038 -19858 -19826 -19877 -19630 -19887 -19862 -19971 -19751 -19568 -19643 -19534 -19414 -19378 -19560 -19496 -19822 -19653 -19652 -19569 -19687 -19355 -19407 -19587 -19252 -19496 -19354 -19362 -19037 -18876 -18882 -18902 -19027 -19273 -19425 -19452 -19262 -19096 -18696 -18639 -18840 -18879 -18987 -19097 -18939 -18750 -18748 -18991 -18859 -18845 -19097 -19061 -18945 -18929 -19067 -18964 -18871 -18718 -18631 -18441 -18720 -18604 -18651 -18778 -18655 -18948 -18871 -18776 -18805 -18684 -18526 -18711 -18871 -18732 -18334 -18556 -18224 -18305 -18273 -18255 -18526 -18469 -18339 -18116 -18037 -18277 -18059 -17995 -18083 -18051 -17797 -18050 -18043 -18168 -18213 -18257 -17875 -17969 -17887 -17968 -18053 -18042 -17981 -17980 -18162 -18105 -18266 -18329 -18450 -18474 -18391 -18465 -18943 -19210 -19341 -19623 -20012 -20339 -20556 -20952 -21443 -21469 -21791 -22031 -21851 -21970 -22028 -21986 -21817 -21682 -21960 -22000 -22006 -21840 -21849 -21960 -21999 -22480 -22661 -22686 -22996 -23189 -23512 -23932 -24696 -24866 -25007 -24929 -24876 -25146 -25076 -25100 -24899 -24859 -24561 -24390 -24105 -23862 -23477 -23233 -22810 -22747 -22393 -22156 -21551 -21247 -21125 -21171 -21450 -21214 -21375 -21391 -21740 -21609 -22197 -22469 -22528 -22816 -22910 -23233 -23310 -23621 -23585 -23768 -23532 -23389 -23587 -23453 -23709 -23756 -23522 -23547 -23688 -23474 -23426 -23433 -23232 -22818 -22815 -22721 -22752 -22327 -21867 -21832 -22087 -21945 -21735 -21668 -21456 -21371 -21467 -21435 -21356 -21401 -21124 -20870 -21147 -20786 -20646 -20398 -20412 -20622 -20580 -20692 -20590 -21045 -21069 -21055 -20860 -20580 -20467 -20589 -20685 -20569 -20550 -20589 -20641 -20788 -20638 -20577 -20759 -20727 -20830 -20609 -20675 -20663 -20507 -20273 -20438 -20936 -20702 -20599 -20748 -20641 -20707 -20588 -21113 -20746 -20738 -21008 -21156 -21163 -21268 -21161 -21095 -20937 -20846 -20680 -20922 -20981 -21112 -21146 -21319 -21175 -21249 -21491 -21531 -21391 -21309 -21386 -21309 -21310 -21326 -21344 -21402 -21277 -21246 -21157 -21440 -21681 -21447 -21208 -21142 -20992 -21226 -21143 -21266 -21471 -21701 -21571 -21691 -21543 -21438 -21268 -21244 -20936 -21223 -21348 -21231 -21196 -21180 -21091 -21056 -21319 -21380 -21117 -20831 -20806 -20936 -20854 -20671 -20952 -20862 -20824 -21148 -21005 -20991 -20771 -20642 -20820 -20650 -20996 -21034 -20986 -20916 -20738 -20896 -20586 -20683 -20658 -20687 -20740 -20652 -20648 -20543 -20761 -20447 -20432 -20383 -20324 -20295 -20366 -20558 -20269 -20407 -20342 -20320 -20251 -20252 -20251 -20062 -20515 -20523 -20082 -19783 -19799 -20073 -20434 -20405 -20112 -19833 -19984 -20147 -19932 -19881 -19777 -19654 -19975 -19523 -19710 -19391 -19376 -19766 -19417 -19344 -19352 -19441 -19373 -19309 -19475 -19560 -19514 -19407 -19252 -19200 -19124 -19423 -19440 -19565 -19304 -19506 -19915 -19719 -19524 -19125 -19065 -18766 -18784 -18978 -19344 -19030 -19117 -19157 -19025 -18828 -19126 -19092 -18976 -19089 -19013 -18863 -18917 -19263 -19179 -18882 -18747 -18646 -18575 -18729 -19191 -19093 -18914 -18902 -18872 -19031 -18795 -18860 -18718 -18654 -18845 -18746 -18777 -18822 -18710 -18526 -18644 -18906 -18676 -18818 -18910 -18974 -18914 -18851 -18851 -18719 -18753 -18812 -18850 -18794 -18886 -18923 -19014 -18811 -18664 -18488 -18563 -18782 -18667 -18504 -18663 -18760 -18671 -18677 -18765 -18585 -18439 -18511 -18587 -18419 -18397 -18357 -18467 -18110 -17932 -18312 -18536 -18873 -18865 -18588 -18464 -18395 -18236 -18169 -18438 -18501 -18451 -18344 -18349 -18469 -18109 -17914 -18050 -18106 -18008 -17905 -17891 -17926 -18166 -17973 -17584 -17515 -17653 -17580 -17511 -17648 -17670 -17632 -17404 -17589 -17640 -17894 -18037 -17917 -17499 -17467 -17794 -17735 -17642 -17519 -17479 -17603 -17766 -17535 -17741 -17534 -17325 -17801 -17814 -17595 -17387 -17293 -17264 -17495 -17578 -17712 -17525 -17306 -17117 -17323 -17395 -17280 -17470 -17221 -17303 -17405 -17419 -17216 -17179 -16961 -17280 -17286 -17227 -17148 -17104 -16941 -16814 -16680 -16774 -16719 -16892 -17059 -17011 -17061 -17158 -17099 -17188 -16880 -16901 -16817 -17011 -16888 -17192 -17088 -17044 -16932 -16970 -16888 -16678 -16683 -16747 -16604 -16407 -16677 -16489 -16556 -16649 -16638 -16400 -16398 -16423 -16468 -16570 -16386 -16407 -16441 -16367 -16626 -16524 -16635 -16884 -17048 -16791 -16809 -16908 -16592 -16475 -16494 -16366 -16753 -16665 -16422 -16302 -16188 -16059 -16170 -16492 -16438 -16059 -15944 -16001 -15870 -15963 -16099 -16108 -16230 -16225 -16592 -16769 -16575 -16444 -16250 -15998 -16265 -16180 -16026 -16135 -16282 -16023 -15865 -16075 -16274 -16148 -16050 -16155 -16174 -15896 -15920 -16467 -16489 -16597 -16635 -16883 -17054 -17406 -18016 -18412 -18959 -19570 -21079 -22176 -23205 -24124 -25454 -26518 -27284 -27594 -28433 -29118 -29801 -30480 -30981 -31032 -30767 -30415 -29933 -29656 -29180 -28546 -27474 -26926 -25905 -25328 -24537 -23750 -23170 -22586 -22045 -21395 -21093 -20736 -20211 -20110 -19883 -19814 -19702 -19189 -18900 -18889 -18583 -18469 -18523 -18333 -18277 -18169 -18227 -18474 -18247 -18334 -18570 -18982 -19466 -19586 -19953 -20358 -20662 -21277 -21746 -22329 -22606 -23173 -23547 -23800 -23571 -23674 -23888 -23912 -23805 -23313 -23233 -22950 -22421 -22082 -21440 -20905 -20378 -20232 -20245 -19835 -19296 -19174 -18914 -18579 -18694 -18463 -18405 -18298 -18121 -18027 -18039 -18070 -18112 -18342 -18164 -18145 -17849 -17990 -18214 -18099 -18098 -18092 -17871 -17675 -17737 -17623 -17549 -17360 -17496 -17612 -17787 -17617 -17775 -17874 -17750 -17673 -17861 -18028 -17827 -17681 -17766 -18052 -17664 -17475 -17553 -17735 -17795 -17565 -17613 -17667 -17649 -17625 -17576 -17667 -17858 -17888 -17724 -17809 -17925 -17856 -17876 -17801 -17762 -17704 -17749 -17672 -17953 -18031 -17891 -18003 -18137 -18229 -18334 -18314 -18372 -18277 -18017 -18125 -18331 -18284 -18348 -18297 -18205 -18480 -18251 -18130 -18070 -17994 -18156 -18432 -18426 -18505 -18267 -18127 -17992 -17967 -18078 -18015 -18073 -18086 -18126 -17860 -18020 -18102 -18328 -18262 -18198 -18112 -18450 -18438 -18156 -18084 -18078 -18169 -18254 -18285 -18026 -17852 -17965 -18184 -18410 -18296 -17972 -17936 -18086 -18208 -18102 -18068 -18121 -17908 -17947 -17847 -17799 -17778 -17847 -17795 -18026 -18198 -18235 -18100 -17942 -18159 -18197 -18287 -18187 -18142 -18101 -18068 -18124 -17823 -17932 -18028 -18187 -18007 -17878 -17609 -17747 -17842 -17798 -17802 -17717 -17675 -17627 -17599 -17498 -17534 -17702 -17587 -17987 -18207 -17824 -17437 -17413 -17484 -17355 -17457 -17415 -17437 -17430 -17304 -17470 -17643 -17454 -17193 -17325 -17356 -17051 -16833 -16946 -17007 -16894 -16635 -16622 -16929 -17109 -17192 -16946 -16969 -16932 -16941 -16917 -16985 -16838 -16823 -16803 -16971 -16872 -16863 -16989 -16575 -16690 -16725 -16647 -16850 -17042 -17055 -17021 -16904 -16774 -16699 -16757 -16676 -16510 -16572 -16457 -16664 -16831 -16855 -16790 -16603 -16561 -16522 -16500 -16664 -16427 -16227 -16309 -16106 -16106 -16109 -16076 -15988 -16312 -16492 -16620 -16471 -16353 -16249 -16223 -16345 -16205 -16130 -16278 -16083 -16017 -15920 -16014 -16123 -16046 -16059 -16160 -16088 -16215 -16522 -16317 -16193 -15975 -16055 -16059 -15743 -15713 -16047 -16097 -16279 -16278 -15997 -16114 -15972 -15985 -16026 -15833 -16007 -16187 -16336 -16196 -15959 -15788 -16012 -16247 -16108 -15927 -15867 -16075 -15926 -16023 -15851 -15899 -15827 -15786 -15685 -15647 -15788 -15840 -15567 -15500 -15594 -15706 -15861 -15684 -15314 -15334 -15318 -15784 -15659 -15418 -15226 -15371 -15401 -15460 -15431 -15301 -15252 -15203 -15306 -15501 -15519 -15369 -15415 -15333 -15412 -15427 -15508 -15488 -15271 -15179 -15229 -15337 -15433 -15335 -15343 -15162 -15283 -15208 -15213 -15133 -15206 -15283 -15140 -14886 -15006 -15137 -15055 -14994 -15205 -14890 -15010 -14868 -14792 -14828 -15208 -15216 -15174 -15210 -15321 -15227 -14910 -14921 -14876 -14954 -15162 -14944 -14793 -14831 -14834 -15036 -14922 -14829 -14853 -14637 -14692 -14775 -14973 -14853 -14621 -14787 -14649 -14734 -14958 -15157 -15034 -14836 -14766 -14901 -14831 -14820 -14823 -14596 -14660 -14713 -14596 -14510 -14317 -14440 -14431 -14465 -14532 -14384 -14303 -14379 -14550 -14469 -14196 -14292 -14365 -14343 -14584 -14465 -14520 -14492 -14346 -14313 -14405 -14312 -14316 -14231 -14391 -14465 -14326 -14401 -14278 -14282 -14164 -14234 -14178 -13917 -13782 -14146 -14374 -14315 -14178 -14276 -14077 -14125 -14239 -14144 -14054 -14117 -14195 -13980 -14085 -13868 -13940 -13759 -13928 -13947 -14249 -14038 -14065 -14009 -14154 -13991 -13956 -14065 -13913 -13714 -13641 -13584 -13786 -13804 -13918 -13704 -13544 -13582 -13451 -13531 -13555 -13434 -13524 -13842 -13895 -13719 -13499 -13595 -13552 -13573 -13539 -13521 -13599 -13441 -13426 -13653 -13474 -13321 -13125 -13281 -13476 -13737 -13720 -13586 -13527 -13459 -13459 -13173 -13274 -13328 -13261 -13140 -13238 -13403 -13431 -13594 -13522 -13876 -13735 -13625 -13736 -13525 -13473 -13422 -13376 -13132 -13149 -13051 -13262 -13465 -13588 -13524 -13666 -13536 -13455 -13387 -13409 -13395 -13468 -13070 -12852 -12620 -12924 -#ENDOFDATA : diff --git a/examples/hyperspy_as_library/curve_fitting_data.py b/examples/hyperspy_as_library/curve_fitting_data.py deleted file mode 100644 index 88812267a4..0000000000 --- a/examples/hyperspy_as_library/curve_fitting_data.py +++ /dev/null @@ -1,25 +0,0 @@ -""" Loads hyperspy as a regular python library, loads spectra from files, -does curve fitting, and plotting the model and original spectrum to a png -file""" - -import hyperspy.api as hs -import matplotlib.pyplot as plt - -coreLossSpectrumFileName = "coreloss_spectrum.msa" -lowlossSpectrumFileName = "lowloss_spectrum.msa" - -s = hs.load(coreLossSpectrumFileName).to_EELS() -s.add_elements(("Mn", "O")) -s.set_microscope_parameters( - beam_energy=300, - convergence_angle=24.6, - collection_angle=13.6) - -ll = hs.load(lowlossSpectrumFileName).to_EELS() - -m = s.create_model(ll=ll) -m.enable_fine_structure() -m.multifit(kind="smart") -m.plot() - -plt.savefig("model_original_spectrum_plot.png") diff --git a/examples/hyperspy_as_library/lowloss_spectrum.msa b/examples/hyperspy_as_library/lowloss_spectrum.msa deleted file mode 100644 index 9c83a765e7..0000000000 --- a/examples/hyperspy_as_library/lowloss_spectrum.msa +++ /dev/null @@ -1,2064 +0,0 @@ -#FORMAT : EMSA/MAS Spectral Data File -#VERSION : 1.0 -#TITLE : -#DATE : Tue Sep 04 2012 -#TIME : 23:14:50 -#OWNER : -#NPOINTS : 2048.00 -#NCOLUMNS : 1.00000 -#XUNITS : eV -#YUNITS : Counts -#DATATYPE : Y -#XPERCHAN : 0.200000 -#OFFSET : -40.0000 -#SIGNALTYPE : ELS -#SPECTRUM : -35 -23 -20 -28 -13 -22 -35 -40 -6 -36 -17 -7 -17 -22 -33 -45 -26 -24 -29 -33 -11 -22 -27 -6 -13 -31 -6 -32 -28 -23 -29 -20 -36 -32 -24 -12 -33 -38 -3 -44 -37 -36 -43 -30 -37 -38 -39 -29 -25 -24 -20 -48 -39 -25 -31 -31 -4 -35 -23 -37 -36 -40 -34 -45 -15 -35 -33 -58 -22 -30 -29 -43 -42 -42 -33 -49 -42 -52 -41 -28 -34 -52 -45 -45 -60 -40 -40 -39 -32 -36 -55 -46 -53 -44 -41 -53 -55 -62 -45 -52 -38 -66 -61 -56 -54 -59 -63 -55 -40 -47 -51 -58 -67 -68 -71 -62 -63 -61 -72 -74 -63 -75 -81 -71 -82 -79 -79 -91 -81 -77 -85 -65 -60 -74 -96 -98 -97 -98 -91 -89 -93 -93 -110 -118 -108 -108 -125 -106 -123 -120 -127 -135 -138 -134 -145 -152 -181 -164 -183 -182 -205 -195 -227 -213 -223 -217 -226 -219 -231 -244 -249 -260 -275 -284 -330 -330 -337 -386 -406 -462 -525 -609 -764 -913 -1138 -1434 -1801 -2303 -2949 -3650 -4804 -6273 -7956 -10190 -12526 -15380 -18548 -22116 -25603 -29111 -32578 -36042 -39901 -42855 -44384 -44145 -42722 -40767 -37719 -33854 -30222 -27048 -24388 -21472 -18144 -14501 -11079 -8158 -6244 -4985 -4114 -3249 -2647 -2247 -1879 -1622 -1465 -1299 -1216 -1117 -1080 -1069 -979 -916 -929 -939 -910 -904 -898 -884 -867 -924 -885 -875 -849 -819 -804 -784 -840 -848 -851 -880 -901 -914 -917 -959 -987 -1031 -1015 -1047 -1039 -1097 -1148 -1216 -1212 -1254 -1226 -1180 -1221 -1261 -1308 -1331 -1426 -1453 -1454 -1363 -1342 -1364 -1316 -1283 -1230 -1254 -1274 -1261 -1189 -1167 -1124 -1125 -1112 -1134 -1081 -1011 -1005 -995 -1047 -1035 -977 -1019 -1063 -1064 -1064 -1096 -1116 -1079 -1114 -1110 -1082 -1084 -1048 -1090 -1115 -1172 -1167 -1123 -1134 -1142 -1187 -1172 -1159 -1181 -1173 -1211 -1193 -1208 -1245 -1251 -1235 -1259 -1264 -1319 -1328 -1373 -1433 -1443 -1498 -1483 -1475 -1498 -1512 -1546 -1573 -1569 -1586 -1720 -1721 -1772 -1860 -1895 -1958 -1992 -2023 -2080 -2164 -2262 -2336 -2289 -2325 -2310 -2257 -2277 -2390 -2317 -2298 -2238 -2281 -2228 -2155 -2160 -2104 -2144 -2076 -1965 -1948 -1920 -1893 -1845 -1834 -1803 -1765 -1727 -1693 -1636 -1626 -1646 -1632 -1605 -1585 -1578 -1590 -1573 -1554 -1421 -1306 -1370 -1409 -1361 -1363 -1347 -1289 -1221 -1222 -1294 -1233 -1199 -1208 -1186 -1159 -1111 -1121 -1143 -1144 -1106 -1078 -1011 -1049 -1053 -1054 -1071 -1033 -1053 -1065 -1013 -973 -954 -953 -998 -907 -864 -887 -901 -890 -902 -925 -889 -827 -800 -766 -767 -766 -763 -777 -763 -736 -759 -789 -726 -737 -750 -750 -727 -745 -755 -740 -715 -729 -698 -677 -721 -771 -705 -700 -733 -682 -724 -718 -691 -703 -722 -762 -763 -760 -798 -781 -776 -767 -787 -801 -772 -793 -822 -837 -828 -756 -781 -774 -788 -732 -701 -706 -686 -690 -694 -723 -703 -672 -689 -678 -709 -670 -654 -656 -642 -637 -637 -639 -638 -655 -600 -640 -635 -615 -654 -579 -619 -630 -612 -601 -631 -651 -603 -594 -633 -579 -583 -570 -554 -597 -602 -583 -594 -598 -574 -624 -570 -557 -558 -589 -614 -573 -608 -572 -531 -541 -531 -551 -589 -515 -484 -457 -473 -495 -499 -474 -478 -447 -497 -488 -452 -464 -467 -459 -470 -494 -509 -516 -486 -508 -468 -437 -466 -435 -414 -447 -466 -476 -439 -402 -382 -427 -439 -412 -396 -378 -357 -402 -361 -367 -400 -387 -394 -390 -366 -387 -348 -379 -391 -390 -374 -398 -378 -380 -366 -348 -341 -344 -339 -325 -369 -379 -381 -382 -360 -324 -339 -337 -311 -342 -338 -347 -337 -334 -309 -303 -341 -334 -313 -342 -346 -307 -317 -277 -304 -357 -344 -344 -366 -349 -341 -313 -300 -313 -311 -315 -289 -283 -308 -310 -290 -343 -309 -289 -304 -313 -313 -283 -253 -269 -258 -260 -259 -230 -259 -265 -258 -281 -237 -276 -252 -229 -247 -251 -233 -258 -235 -235 -219 -210 -210 -240 -258 -277 -255 -258 -230 -216 -205 -219 -233 -250 -224 -226 -202 -232 -215 -240 -253 -252 -239 -208 -216 -197 -198 -210 -238 -205 -194 -223 -228 -220 -237 -235 -218 -221 -242 -214 -204 -193 -204 -196 -201 -229 -226 -228 -220 -214 -205 -205 -187 -181 -183 -176 -197 -184 -201 -231 -249 -209 -239 -206 -192 -215 -176 -171 -199 -194 -216 -223 -206 -203 -191 -175 -203 -205 -164 -195 -160 -160 -178 -218 -212 -214 -199 -170 -184 -209 -205 -180 -168 -177 -188 -211 -182 -160 -174 -172 -162 -184 -175 -166 -167 -164 -189 -192 -223 -240 -231 -211 -206 -202 -208 -242 -235 -246 -222 -260 -282 -278 -235 -261 -286 -271 -281 -301 -307 -298 -270 -265 -246 -295 -309 -270 -288 -280 -294 -279 -265 -278 -308 -306 -296 -305 -291 -248 -242 -257 -220 -255 -231 -243 -246 -229 -236 -227 -246 -223 -245 -223 -231 -234 -250 -231 -212 -212 -193 -186 -182 -236 -221 -234 -216 -237 -240 -213 -229 -228 -202 -207 -184 -155 -213 -232 -214 -204 -178 -192 -180 -169 -169 -209 -199 -204 -196 -183 -192 -168 -182 -188 -199 -178 -197 -195 -198 -204 -183 -176 -214 -196 -162 -196 -214 -205 -199 -179 -160 -155 -177 -195 -194 -184 -160 -160 -159 -178 -184 -181 -190 -167 -150 -146 -136 -161 -190 -173 -190 -176 -177 -162 -157 -153 -158 -163 -150 -130 -133 -143 -127 -118 -118 -127 -136 -137 -141 -183 -162 -157 -132 -118 -118 -149 -125 -147 -147 -140 -133 -174 -149 -160 -152 -146 -152 -154 -133 -114 -114 -117 -108 -116 -105 -115 -113 -129 -146 -126 -119 -123 -141 -125 -140 -144 -166 -138 -123 -120 -129 -129 -128 -115 -115 -122 -103 -118 -104 -111 -99 -100 -110 -113 -129 -135 -109 -94 -100 -114 -111 -103 -103 -96 -118 -146 -121 -106 -120 -113 -101 -125 -132 -123 -131 -122 -95 -95 -91 -107 -112 -129 -122 -119 -106 -118 -88 -92 -92 -115 -122 -95 -68 -63 -77 -63 -71 -87 -96 -67 -75 -64 -75 -81 -58 -78 -69 -78 -60 -86 -84 -71 -69 -78 -78 -90 -79 -64 -78 -90 -62 -78 -77 -98 -80 -85 -80 -68 -71 -64 -80 -75 -70 -74 -55 -63 -59 -48 -72 -50 -57 -67 -49 -56 -53 -72 -71 -72 -68 -106 -75 -85 -65 -70 -51 -55 -53 -51 -86 -72 -57 -42 -67 -60 -74 -61 -65 -63 -42 -47 -79 -66 -66 -51 -70 -45 -72 -53 -39 -74 -81 -67 -71 -62 -43 -58 -50 -42 -28 -35 -60 -77 -91 -74 -90 -70 -68 -70 -81 -78 -62 -54 -71 -77 -58 -35 -44 -43 -39 -34 -65 -41 -52 -49 -71 -45 -67 -39 -51 -55 -47 -47 -49 -65 -57 -68 -51 -39 -50 -35 -45 -54 -48 -53 -42 -58 -53 -53 -61 -48 -41 -58 -67 -50 -57 -53 -44 -64 -68 -64 -75 -65 -59 -42 -57 -40 -40 -60 -49 -53 -70 -58 -46 -46 -56 -34 -44 -50 -47 -54 -57 -45 -41 -53 -37 -30 -25 -26 -30 -41 -61 -59 -32 -29 -34 -42 -67 -49 -48 -64 -57 -52 -56 -65 -69 -70 -49 -49 -58 -56 -39 -39 -71 -65 -50 -50 -47 -15 -31 -36 -19 -24 -41 -47 -44 -70 -56 -58 -53 -47 -49 -36 -40 -49 -44 -57 -66 -37 -35 -47 -27 -44 -40 -8 -18 -23 -49 -57 -38 -48 -52 -44 -23 -54 -72 -45 -51 -50 -59 -42 -75 -34 -46 -36 -38 -35 -42 -48 -24 -44 -34 -21 -45 -21 -33 -24 -27 -22 -61 -31 -50 -63 -43 -58 -41 -34 -28 -38 -35 -26 -26 -41 -47 -26 -44 -26 -54 -36 -43 -44 -29 -26 -43 -39 -48 -40 -34 -41 -24 -17 -9 -17 -30 -34 -19 -15 -26 -37 -29 -15 -23 -33 -26 -34 -33 -47 -22 -32 -26 -35 -45 -16 -37 -41 -56 -32 -47 -16 -46 -11 -30 -16 -30 -31 -26 -18 -28 -30 -36 -17 -16 -28 -17 -29 -22 -39 -45 -26 -17 -29 -27 -14 -25 -23 -22 -25 -36 -27 -34 -39 -9 -36 -6 -11 -19 -18 -36 -17 -31 -32 -20 -35 -14 -9 -25 -31 -27 -29 -30 -34 -51 -20 -26 -44 -45 -24 -14 -43 -25 -43 -32 -35 -35 -31 -14 -31 -25 -38 -17 -20 -31 -25 -19 -39 -30 -39 -44 -30 -35 -47 -25 -39 -23 -29 -38 -45 -38 -33 -21 -34 -61 -27 -30 -54 -10 -21 -30 -37 -49 -3 -34 -24 -25 -27 -24 -35 -31 -11 -4 -10 -42 -28 -10 -8 -29 -10 -31 -26 -36 -5 -15 -30 -22 -14 -10 -21 -27 -31 --1 -19 -34 -46 -18 -31 -36 -34 -22 -37 -40 -40 -39 -24 -40 -27 -38 -34 -15 -27 -48 --5 -13 -37 -24 -18 -11 -36 -24 -47 -32 -27 -24 -22 -3 -10 -21 -7 -3 -10 -17 -34 -9 -13 --2 -11 -31 -26 -0 -37 -30 -14 -20 -10 -23 -12 -21 -14 -15 -14 -16 -2 -26 -8 -10 -14 -20 -31 -20 -6 --2 -21 -24 -26 -27 -21 -22 -18 -16 -36 -26 -14 -18 -18 -20 -34 -7 -22 -16 -19 -46 -26 -21 -25 -31 -26 -11 -15 -6 -14 -26 -25 -28 -19 -24 -31 -6 -10 -14 -13 -28 -35 -35 -19 -17 -34 -34 -24 -13 -2 -47 -30 -21 -7 -12 -34 -27 -20 -9 -33 -40 -38 -25 -22 -22 -44 -26 -24 -22 -17 --6 -13 -12 -12 -33 -17 -17 -15 -5 -7 -19 -20 -21 -38 -25 -12 -8 -26 -33 -20 -33 -16 -29 -26 -32 -23 -43 -48 -22 -18 -23 -19 -21 -21 -12 -18 -12 -28 -23 -23 -9 --6 -44 -37 -23 -13 -17 -20 -25 -37 -17 -18 -10 -16 -17 -29 -24 -15 -12 --1 -23 -23 -21 -16 -24 -16 --1 -10 -25 -11 -25 -34 -5 -10 -9 -25 -16 -22 -17 -6 -19 -27 -37 -30 -17 -11 -14 -9 -12 -15 -7 -25 -9 -35 -20 -26 -1 -24 -0 -14 -13 -13 -7 -17 -17 -15 -35 -17 -19 -20 -11 -26 -12 -29 -19 -10 -25 -16 -14 -17 -3 -6 -20 -26 -14 -21 -8 -31 -23 -23 -3 -27 -26 -17 -13 -16 -18 -19 -16 -16 -31 -11 -29 -8 -14 -10 -45 --5 -14 -17 -0 --3 -21 -13 -20 -21 -20 -6 -1 -23 -0 -18 -18 -19 -12 -7 -17 -21 -17 -18 -15 -19 --4 -3 -10 -6 -13 -29 -18 --5 -3 -10 -20 --6 -22 -35 -14 -9 -1 -17 -5 --2 -7 -18 -23 -17 -11 -21 -20 -13 -19 -15 -27 -27 -22 -26 -17 -35 -37 -33 -37 -31 -24 -20 -45 -34 -17 -14 -20 -21 -9 -7 -4 -16 -21 -17 -10 -6 -17 -13 -16 -4 -24 -7 --2 -22 -20 -12 -13 -12 --1 -14 -2 -7 -18 -21 -14 -2 -24 -49 -27 -20 -14 -10 -12 -14 -13 -7 -17 -14 -17 -7 -21 -14 -6 -15 -27 -20 -28 -16 -2 --7 -18 -16 -2 -14 -7 -6 -6 --4 -5 -9 -11 -16 -11 --11 -35 --2 -10 -25 -8 --2 -12 --5 --2 --3 -19 -26 -13 -21 -3 -28 -7 --15 -6 -6 -15 -5 -6 --4 --8 --6 -15 -8 -18 -15 --3 -3 -4 -5 -12 -4 -5 -5 -5 -9 -11 -14 -12 -9 -9 -11 -6 -0 -6 --3 -9 --15 -11 -11 -1 -21 -2 -10 -8 -8 -6 -15 --3 --3 -10 -9 -4 -7 -4 -0 --4 -25 --3 -8 --7 --2 -19 -45 -26 -17 --7 -4 -18 -13 -25 -30 -28 -2 -9 -31 -18 -22 -1 -29 -34 -19 -8 --2 -4 -2 -3 --2 -24 -9 -8 -8 -21 -10 -11 -12 -6 -5 --13 -7 -8 -12 -6 --5 -6 -10 -18 -24 -7 -0 -13 -18 -18 -28 -9 -13 -17 -#ENDOFDATA : diff --git a/examples/hyperspy_as_library/minimal_example.py b/examples/hyperspy_as_library/minimal_example.py deleted file mode 100644 index 510e2451e5..0000000000 --- a/examples/hyperspy_as_library/minimal_example.py +++ /dev/null @@ -1,10 +0,0 @@ -""" Loads hyperspy as a regular python library, creates a spectrum with random numbers and plots it to a file""" - -import hyperspy.api as hs -import numpy as np -import matplotlib.pyplot as plt - -s = hs.signals.Signal1D(np.random.rand(1024)) -s.plot() - -plt.savefig("testSpectrum.png") diff --git a/examples/io/README.rst b/examples/io/README.rst new file mode 100644 index 0000000000..e3a3715683 --- /dev/null +++ b/examples/io/README.rst @@ -0,0 +1,4 @@ +Loading, saving and exporting +============================= + +Below is a gallery of examples on loading, saving and exporting data. diff --git a/examples/io/convert_color_image.py b/examples/io/convert_color_image.py new file mode 100644 index 0000000000..ffd870f505 --- /dev/null +++ b/examples/io/convert_color_image.py @@ -0,0 +1,81 @@ +""" +Adjust contrast and save RGB images +=================================== + +This example shows how to adjust the contrast and intensities using +scikit-image and save it as an RGB image. + +When saving an RGB image to ``jpg``, only 8 bits are supported and the image +intensity needs to be rescaled to 0-255 before converting to 8 bits, +otherwise, the intensities will be cropped at the value of 255. + +""" + +import hyperspy.api as hs +import numpy as np +import skimage as ski + +#%% +# +# Adjust contrast +# ############### +# +# In hyperspy, color images are defined as Signal1D with the signal dimension +# corresponding to the color channel (red, green and blue) + +# The dtype can be changed to a custom dtype, which is convenient to visualise +# the color image +s = hs.signals.Signal1D(ski.data.astronaut()) +s.change_dtype("rgb8") +print(s) + +#%% +# Display the color image +s.plot() + +#%% +# Processing is usually performed on standard dtype (e.g. ``uint8``, ``uint16``), because +# most functions from scikit-image, numpy, scipy, etc. only support standard ``dtype``. +# Convert from RGB to unsigned integer 16 bits +s.change_dtype("uint8") +print(s) + +#%% +# Adjust contrast (gamma correction) +s.data = ski.exposure.adjust_gamma(s.data, gamma=0.2) + +#%% +# +# Save to ``jpg`` +# ############### +# +# Change dtype back to custom dtype ``rgb8`` +s.change_dtype("rgb8") + +#%% +# Save as jpg +s.save("rgb8_image.jpg", overwrite=True) + + +#%% +# +# Save ``rgb16`` image to ``jpg`` +# ############################### +# +# The last part of this example shows how to save ``rgb16`` to a ``jpg`` file +# +# Create a signal with ``rgb16`` dtype +s2 = hs.signals.Signal1D(ski.data.astronaut().astype("uint16") * 100) + +#%% +# To save a color image to ``jpg``, the signal needs to be converted to ``rgb8`` because +# ``jpg`` only support 8-bit RGB +# Rescale intensity to fit the unsigned integer 8 bits (2**8 = 256 intensity level) +s2.data = ski.exposure.rescale_intensity(s2.data, out_range=(0, 255)) + +#%% +# Now that the values have been rescaled to the 0-255 range, we can convert the data type +# to unsigned integer 8 bit and then ``rgb8`` to be able to save the RGB image in ``jpg`` format +s2.change_dtype("uint8") +s2.change_dtype("rgb8") +s2.save("rgb16_image_saved_as_jpg.jpg", overwrite=True) diff --git a/examples/io/export_single_spectrum.py b/examples/io/export_single_spectrum.py new file mode 100644 index 0000000000..f10d1c61d4 --- /dev/null +++ b/examples/io/export_single_spectrum.py @@ -0,0 +1,26 @@ +""" +Export single spectrum +====================== + +Creates a single spectrum image, saves it and plots it: + +1. Create a single sprectrum using `Signal1D` signal. +2. Save signal as a msa file +3. Plot the signal using the `plot` method +4. Save the figure as a png file + +""" + +# Set the matplotlib backend of your choice, for example +# %matploltib qt +import hyperspy.api as hs +import numpy as np + +s = hs.signals.Signal1D(np.random.rand(1024)) + +# Export as msa file, very similar to a csv file but containing standardised +# metadata +s.save('testSpectrum.msa', overwrite=True) + +# Plot it +s.plot() diff --git a/examples/model_fitting/README.rst b/examples/model_fitting/README.rst new file mode 100644 index 0000000000..8342229be2 --- /dev/null +++ b/examples/model_fitting/README.rst @@ -0,0 +1,4 @@ +Model fitting +============= + +Below is a gallery of examples on model fitting. diff --git a/examples/model_fitting/plot_residual.py b/examples/model_fitting/plot_residual.py new file mode 100644 index 0000000000..2c28817874 --- /dev/null +++ b/examples/model_fitting/plot_residual.py @@ -0,0 +1,36 @@ +""" +Plot Residual +============= + +Fit an affine function and plot the residual. + +""" + +import numpy as np +import hyperspy.api as hs + +#%% +# Create a signal: +data = np.arange(1000, dtype=np.int64).reshape((10, 100)) +s = hs.signals.Signal1D(data) + +#%% +# Add noise: +s.add_poissonian_noise(random_state=0) + +#%% +# Create model: +m = s.create_model() +line = hs.model.components1D.Expression("a * x + b", name="Affine") +m.append(line) + +#%% +# Fit for all navigation positions: +m.multifit() + +#%% +# Plot the fitted model with residual: +m.plot(plot_residual=True) +# Choose the second figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 2 + diff --git a/examples/model_fitting/simple_arctan_fit.py b/examples/model_fitting/simple_arctan_fit.py index dbf947e1af..f9e89ff786 100644 --- a/examples/model_fitting/simple_arctan_fit.py +++ b/examples/model_fitting/simple_arctan_fit.py @@ -1,11 +1,17 @@ -"""Creates a spectrum, and fits an arctan to it.""" +""" +Simple arctan fit +================= + +Fit an arctan function. + +""" import numpy as np import hyperspy.api as hs # Generate the data and make the spectrum -s = hs.signals.Spectrum( - np.arctan(np.arange(-500, 500))) +data = np.arctan(np.arange(-500, 500)) +s = hs.signals.Signal1D(data) s.axes_manager[0].offset = -500 s.axes_manager[0].units = "" s.axes_manager[0].name = "x" diff --git a/examples/plotting/README.rst b/examples/plotting/README.rst new file mode 100644 index 0000000000..eced9110f7 --- /dev/null +++ b/examples/plotting/README.rst @@ -0,0 +1,10 @@ +.. _subfigure_examples_label: + +Making Custom Layouts for Plots +=============================== + +Below is a gallery of examples on making simple custom layouts for plotting data +using :class:`matplotlib.figure.SubFigure`. + +.. Note:: + Plotting data with subfigures is slower than using separates figures. diff --git a/examples/plotting/ROI_insets.py b/examples/plotting/ROI_insets.py new file mode 100644 index 0000000000..16056070dc --- /dev/null +++ b/examples/plotting/ROI_insets.py @@ -0,0 +1,86 @@ +""" +========== +ROI Insets +========== + +ROI's can be powerful tools to help visualize data. In this case we will define ROI's in hyperspy, sum +the data within the ROI, and then plot the sum as a signal. Using the :class:`matplotlib.figure.SubFigure` class +we can create a custom layout to visualize and interact with the data. + +.. Note:: + Plotting data with subfigures is slower than using separates figures. + +We can connect these ROI's using the :func:`hyperspy.api.interactive` function which allows us to move the ROI's and see the sum of the underlying data. +""" +import matplotlib.pyplot as plt +import hyperspy.api as hs +import numpy as np + +rng = np.random.default_rng() + +fig = plt.figure(figsize=(5, 3)) +gs = fig.add_gridspec(6, 10) +sub1 = fig.add_subfigure(gs[0:6, 0:6]) +sub2 = fig.add_subfigure(gs[0:2, 6:8]) +sub3 = fig.add_subfigure(gs[2:4, 7:9]) +sub4 = fig.add_subfigure(gs[4:6, 6:8]) + + +s = hs.signals.Signal2D(rng.random((10, 10, 30, 30))) +r1 = hs.roi.RectangularROI(1, 1, 3, 3) +r2 = hs.roi.RectangularROI(4, 4, 6, 6) +r3 = hs.roi.RectangularROI(3, 7, 5, 9) + +navigator = s.sum(axis=(2, 3)).T # create a navigator signal +navigator.plot(fig=sub1, colorbar=False, axes_off=True, title="", plot_indices=False) + + +s2 = r1.interactive(s, navigation_signal=navigator, color="red") +s3 = r2.interactive(s, navigation_signal=navigator, color="g") +s4 = r3.interactive(s, navigation_signal=navigator, color="y") + +s2_int = s2.sum() +s3_int = s3.sum() +s4_int = s4.sum() + +s2_int.plot(fig=sub2, colorbar=False, axes_off=True, title="", plot_indices=False) +s3_int.plot(fig=sub3, colorbar=False, axes_off=True, title="", plot_indices=False) +s4_int.plot(fig=sub4, colorbar=False, axes_off=True, title="", plot_indices=False) + +# Connect ROIS +for s, s_int, roi in zip([s2, s3, s4], [s2_int, s3_int, s4_int],[r1,r2,r3]): + hs.interactive( + s.sum, + event=roi.events.changed, + recompute_out_event=None, + out=s_int, + ) + +# Add Borders to the match the color of the ROI + +for signal,color, label in zip([s2_int, s3_int, s4_int], ["r", "g", "y"], ["b.", "c.", "d."]): + edge = hs.plot.markers.Squares( + offset_transform="axes", + offsets=(0.5, 0.5), + units="width", + widths=1, + color=color, + linewidth=5, + facecolor="none", + ) + + signal.add_marker(edge) + + label = hs.plot.markers.Texts( + texts=(label,), offsets=[[0.85, 0.85]], offset_transform="axes", sizes=2, color="w" + ) + signal.add_marker(label) + +# Label the big plot + +label = hs.plot.markers.Texts( + texts=("a.",), offsets=[[0.9, 0.9]], offset_transform="axes", sizes=10, color="w" + ) +navigator.add_marker(label) + +# %% diff --git a/examples/plotting/custom_figure_layout.py b/examples/plotting/custom_figure_layout.py new file mode 100644 index 0000000000..c785ee0df1 --- /dev/null +++ b/examples/plotting/custom_figure_layout.py @@ -0,0 +1,41 @@ +""" +======================= +Creating Custom Layouts +======================= + +Custom layouts for hyperspy figures can be created using the :class:`matplotlib.figure.SubFigure` class. Passing +the ``fig`` argument to the :meth:`~.api.signals.BaseSignal.plot` method of a hyperspy signal object will target +that figure instead of creating a new one. This is useful for creating custom layouts with multiple subplots. + +.. Note:: + Plotting data with subfigures is slower than using separates figures. + +""" + +# Creating a simple layout with two subplots + +import matplotlib.pyplot as plt +import hyperspy.api as hs +import numpy as np + +rng = np.random.default_rng() +s = hs.signals.Signal2D(rng.random((10, 10, 10, 10))) +fig = plt.figure(figsize=(10, 5), layout="constrained") +subfigs = fig.subfigures(1, 2, wspace=0.07) +s.plot(navigator_kwds=dict(fig=subfigs[0]), fig=subfigs[1]) + +# %% + +# Sharing a navigator between two hyperspy signals + +s = hs.signals.Signal2D(rng.random((10, 10, 10, 10))) +s2 = hs.signals.Signal2D(rng.random((10, 10, 50, 50))) + +fig = plt.figure(figsize=(8, 7), layout="constrained") +head_figures = fig.subfigures(1, 2, wspace=0.07) +signal_figures = head_figures[1].subfigures(2, 1, hspace=0.07) +s.plot(navigator_kwds=dict(fig=head_figures[0], colorbar=None), fig=signal_figures[0]) +s2.plot(navigator=None, fig=signal_figures[1], axes_manager=s.axes_manager) + +# %% +# sphinx_gallery_thumbnail_number = 2 diff --git a/examples/region_of_interest/ExtractLineProfile.py b/examples/region_of_interest/ExtractLineProfile.py new file mode 100644 index 0000000000..c4983a98a6 --- /dev/null +++ b/examples/region_of_interest/ExtractLineProfile.py @@ -0,0 +1,64 @@ +""" +Extract line profile from image interactively +============================================= + +Interactively extract a line profile (with a certain width) from an image using +:py:class:`~.api.roi.Line2DROI`. Use :func:`~.api.plot.plot_spectra` to plot several +line profiles on the same figure. Save a profile data as ``msa`` file. + +""" + +#%% +#.. figure:: ../../_static/interactive_profiles.gif +# :align: center +# :width: 1024 +# :alt: Interactive example gif of region of interest and associated profile plots. +# +# Extracting line profiles and interactive plotting. + +#%% +# Initialize image data as HyperSpy signal: +import hyperspy.api as hs +import holospy as holo +im0 = holo.data.Fe_needle_reference_hologram() +im1 = holo.data.Fe_needle_hologram() + +#%% +# Intialize Line-ROI from position (400,250) to position (220,600) of width 5 +# in calibrated axes units (in the current example equal to the image pixels): +line_roi = hs.roi.Line2DROI(400, 250, 220, 600, 5) + +#%% +# Extract data along the ROI as new signal by "slicing" the signal and plot the +# profile: +profile = line_roi(im0) +profile.plot() + +#%% +# Slicing of the signal is not interactive. If you want to modify the line along +# which the profile is extracted, you can plot the image and display the ROI +# interactively (creates a new signal object). You can even display the same ROI +# on a second image to make sure that a profile is well placed on both images: +im0.plot() +profile1 = line_roi.interactive(im0, color='green') +im1.plot() +profile2 = line_roi.interactive(im1, color='green') + +#%% +# You can then drag and drop the ends of the ROI to adjust the placement. +# +# If you want to later update the ROI initialization with the modified parameters, +# you can print these: +print(tuple(line_roi)) + +#%% +# You can now directly access the data of the profile objects, e.g. to plot both +# profiles in a single plot: +hs.plot.plot_spectra([profile1, profile2]) +# Choose the fourth figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 4 + +#%% +# Since the profile is a signal object, you can use any other functionality provided +# by hyperspy, e.g. to save a profile as `.msa` text file: +profile1.save('extracted-line-profile.msa', format='XY', overwrite=True) diff --git a/examples/region_of_interest/README.rst b/examples/region_of_interest/README.rst new file mode 100644 index 0000000000..62df532256 --- /dev/null +++ b/examples/region_of_interest/README.rst @@ -0,0 +1,4 @@ +Region of Interest +================== + +Below is a gallery of examples on using regions of interest with HyperSpy signals. diff --git a/examples/region_of_interest/ROI_navigator.py b/examples/region_of_interest/ROI_navigator.py new file mode 100644 index 0000000000..53e4de15bc --- /dev/null +++ b/examples/region_of_interest/ROI_navigator.py @@ -0,0 +1,39 @@ +""" +Navigator ROI +============= + +Use a RectangularROI to take the sum of an area of the navigation space. + +""" + +import hyperspy.api as hs + +#%% +# Create a signal: +s = hs.data.two_gaussians() + +#%% +# Create the roi, here a :py:class:`~.api.roi.RectangularROI` for the two dimension navigation space: +roi = hs.roi.RectangularROI() + +#%% +# Slice signal with roi with the ROI. By using the `interactive` function, the +# output signal ``s_roi`` will update automatically. +# The ROI will be added automatically on the signal figure. +# +# By default, the ROI will be added to the navigation or signal. We specify +# ``recompute_out_event=None`` to avoid redundant computation when changing the ROI + +s.plot() +s_roi = roi.interactive(s, recompute_out_event=None, color='C1') + +# We use :py:class:`~.interactive` function to compute the sum over the ROI interactively: + +roi_sum = hs.interactive(s_roi.sum, recompute_out_event=None) + +# Choose the second figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 1 + +#%% +# Plot the signal sliced by the ROI: +roi_sum.plot() diff --git a/examples/region_of_interest/SpanROI.py b/examples/region_of_interest/SpanROI.py new file mode 100644 index 0000000000..f8c2474b6a --- /dev/null +++ b/examples/region_of_interest/SpanROI.py @@ -0,0 +1,34 @@ +""" +SpanROI on signal axis +====================== + +Use a SpanROI interactively on a Signal1D. + +""" + +import hyperspy.api as hs + +#%% +# Create a signal: +s = hs.data.two_gaussians() + +#%% +# Create the roi, here a :py:class:`~.api.roi.SpanROI` for one dimensional ROI: +roi = hs.roi.SpanROI(left=10, right=20) + +#%% +# Slice signal with roi with the ROI. By using the `interactive` function, the +# output signal ``s_roi`` will update automatically. +# The ROI will be added automatically on the signal figure. +# +# Specify the ``axes`` to add the ROI on either the navigation or signal dimension: + +s.plot() +sliced_signal = roi.interactive(s, axes=s.axes_manager.signal_axes) +# Choose the second figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 2 + +#%% +# Plot the signal sliced by the ROI and use ``autoscale='xv'`` to update the +# limits of the plot automatically: +sliced_signal.plot(autoscale='xv') diff --git a/examples/region_of_interest/SpanROI_interactive_sum.py b/examples/region_of_interest/SpanROI_interactive_sum.py new file mode 100644 index 0000000000..c4982fc851 --- /dev/null +++ b/examples/region_of_interest/SpanROI_interactive_sum.py @@ -0,0 +1,47 @@ +""" +Interactive integration of one dimensional signal +================================================= + +This example shows how to integrate a signal using an interactive ROI. + +""" + +import hyperspy.api as hs + +#%% +# Create a signal: +s = hs.data.two_gaussians() + +#%% +# Create SpanROI: +roi = hs.roi.SpanROI(left=10, right=20) + +#%% +# Slice signal with roi with the ROI. By using the `interactive` function, the +# output signal ``s_roi`` will update automatically. +# The ROI will be added automatically on the signal figure: +s.plot() +sliced_signal = roi.interactive(s, axes=s.axes_manager.signal_axes) +# Choose the second figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 2 + +#%% +# Create a placeholder signal for the integrated signal and set metadata: +integrated_sliced_signal = sliced_signal.sum(axis=-1).T +integrated_sliced_signal.metadata.General.title = "Integrated intensity" + +#%% +# Create the interactive computation, which will update when the ROI ``roi`` is +# changed. wWe use the ``out`` argument to place the results of the integration +# in the placeholder signal defined in the previous step: +hs.interactive( + sliced_signal.sum, + axis=sliced_signal.axes_manager.signal_axes, + event=roi.events.changed, + recompute_out_event=None, + out=integrated_sliced_signal, +) + +#%% +# Plot the integrated sum signal: +integrated_sliced_signal.plot() diff --git a/examples/region_of_interest/fast_fourier_transform_live.py b/examples/region_of_interest/fast_fourier_transform_live.py new file mode 100644 index 0000000000..77ff856bde --- /dev/null +++ b/examples/region_of_interest/fast_fourier_transform_live.py @@ -0,0 +1,42 @@ +""" +Live FFT +======== + +Get interactive fast Fourier transform (FFT) from a subset of a Signal2D +using RectangularROI. + +""" + +import hyperspy.api as hs +import numpy as np + +#%% +# Create a signal: +s = hs.data.atomic_resolution_image() + +#%% +# Add noise to the signal to make it more realistic +s.data *= 1E3 +s.data += np.random.default_rng().poisson(s.data) + +#%% +# Create the ROI, here a :py:class:`~.api.roi.RectangularROI`: +roi = hs.roi.RectangularROI() + +#%% +# Slice signal with the ROI. By using the `interactive` function, the +# output signal ``sliced_signal`` will update automatically. +# The ROI will be added automatically on the signal plot. +s.plot() +sliced_signal = roi.interactive(s, recompute_out_event=None) + +# Choose the second figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 2 + +#%% +# Get the FFT of this sliced signal, and plot it +# Apodization is used to smoothen the edge of the image before taking the FFT +# to remove streaks from the FFT - see the :ref:`signal.fft` section of the +# user guide for more details: +s_fft = hs.interactive(sliced_signal.fft, apodization=True, shift=True, recompute_out_event=None) +s_fft.plot(power_spectrum=True) diff --git a/examples/region_of_interest/map_signal.py b/examples/region_of_interest/map_signal.py new file mode 100644 index 0000000000..b4767a03cc --- /dev/null +++ b/examples/region_of_interest/map_signal.py @@ -0,0 +1,32 @@ +""" +Create Map from ROI in signal space +=================================== + +Use the :func:`~.api.plot.plot_roi_map` function to create interactive maps defined +from ROIs in signal space. + +""" + +import hyperspy.api as hs + +#%% +# Create a signal: +s = hs.data.two_gaussians() + +#%% +# Add 2 ROIs in signal space and map the corresponding signal using :func:`~.api.plot.plot_roi_map` +# The ROIs are added to the plot of the signal: +s.plot() +roi = hs.plot.plot_roi_map(s, rois=2) + +#%% +# Same as above, but plotting the maps in a single figure: +s.plot() +roi = hs.plot.plot_roi_map(s, rois=2, single_figure=True) +# Choose this figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 7 + +#%% +# Same as in previous step, but additionally specifying ``cmap`` and ``colors``: +s.plot() +roi = hs.plot.plot_roi_map(s, rois=2, color=["r", "b"], cmap="gray", single_figure=True) diff --git a/examples/region_of_interest/map_signal_circleROI.py b/examples/region_of_interest/map_signal_circleROI.py new file mode 100644 index 0000000000..f90595eb24 --- /dev/null +++ b/examples/region_of_interest/map_signal_circleROI.py @@ -0,0 +1,32 @@ +""" +Create Map from CircleROI in signal space +========================================= + +Use the :func:`~.api.plot.plot_roi_map` function to create interactive maps defined +from a :class:`~.api.roi.CircleROI` in signal space. + +""" +import hyperspy.api as hs +import numpy as np + +#%% +# Create a signal: +rng = np.random.default_rng(0) +data = rng.random(size=(100, 100, 50, 50)) +s = hs.signals.Signal2D(data) + +#%% +# Add 2 ROIs in signal space and map the corresponding signal using :func:`~.api.plot.plot_roi_map`. +# The ROIs are added to the plot of the signal and by default a +# :class:`~.api.roi.RectangularROI` is used +s.plot() +roi = hs.plot.plot_roi_map(s, rois=2) + +#%% +# Same as above but with using :class:`~.api.roi.CircleROI` with predefined position: +roi1 = hs.roi.CircleROI(cx=25, cy=25, r=5) +roi2 = hs.roi.CircleROI(cx=25, cy=25, r=15, r_inner=10) +s.plot() +roi = hs.plot.plot_roi_map(s, rois=[roi1, roi2]) +# Choose this figure as gallery thumbnail: +# sphinx_gallery_thumbnail_number = 6 \ No newline at end of file diff --git a/examples/simple_simulations/README.rst b/examples/simple_simulations/README.rst new file mode 100644 index 0000000000..b1077cc4d9 --- /dev/null +++ b/examples/simple_simulations/README.rst @@ -0,0 +1,5 @@ +Simple simulations +================== + +Below is a gallery of examples on simulating signals which can be used to test +HyperSpy functionalities diff --git a/examples/simple_simulations/two_gaussians.py b/examples/simple_simulations/two_gaussians.py index 6f2ba5a682..ec506ad37d 100644 --- a/examples/simple_simulations/two_gaussians.py +++ b/examples/simple_simulations/two_gaussians.py @@ -1,6 +1,10 @@ -"""Creates a 2D hyperspectrum consisting of two Gaussians and plots it. +""" +Simple simulation (2 Gaussians) +=============================== + +Creates a 2D hyperspectrum consisting of two Gaussians and plots it. -This example can serve as starting point to test other functionality on the +This example can serve as starting point to test other functionalities on the simulated hyperspectrum. """ diff --git a/hyperspy/Release.py b/hyperspy/Release.py deleted file mode 100644 index a70efc79ee..0000000000 --- a/hyperspy/Release.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -name = 'hyperspy' - -# The commit following to a release must update the version number -# to the version number of the release followed by ".dev", e.g. -# if the version of the last release is 0.4.1 the version of the -# next development version afterwards must be 0.4.1.dev. -# When running setup.py the ".dev" string will be replaced (if possible) -# by the output of "git describe" if git is available or the git -# hash if .git is present. -version = "1.7.0.dev0" -description = "Multidimensional data analysis toolbox" -license = 'GPL v3' - -authors = { - 'all': ('The HyperSpy developers',), } - -url = 'http://hyperspy.org' - -download_url = 'http://www.hyperspy.org' -documentation_url = 'http://hyperspy.org/hyperspy-doc/current/index.html' - -PROJECT_URLS = { - 'Bug Tracker': 'https://github.com/hyperspy/hyperspy/issues', - 'Documentation': 'https://hyperspy.org/hyperspy-doc/current/index.html', - 'Source Code': 'https://github.com/hyperspy/hyperspy', - 'Support' : 'https://gitter.im/hyperspy/hyperspy' -} - -platforms = ['Linux', 'Mac OSX', 'Windows XP/2000/NT', 'Windows 95/98/ME'] - -keywords = ['EDX', - 'EELS', - 'EFTEM', - 'EMSA', - 'FEI', - 'ICA', - 'PCA', - 'PES', - 'STEM', - 'TEM', - 'curve fitting', - 'data analysis', - 'dm3', - 'electron energy loss spectroscopy', - 'electron microscopy', - 'emi', - 'energy dispersive x-rays', - 'hyperspectral', - 'hyperspectrum', - 'multidimensional', - 'hyperspy', - 'machine learning', - 'microscopy', - 'model', - 'msa', - 'numpy', - 'python', - 'quantification', - 'scipy', - 'ser', - 'spectroscopy', - 'spectrum image'] - -info = """ - H y p e r S p y - Version %s - - http://www.hyperspy.org - - """ % version.replace('_', ' ') diff --git a/hyperspy/__init__.py b/hyperspy/__init__.py index 84eb8a6074..00216f0b24 100644 --- a/hyperspy/__init__.py +++ b/hyperspy/__init__.py @@ -1,34 +1,62 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import importlib import logging +from importlib.metadata import version +from pathlib import Path + +from hyperspy import docstrings _logger = logging.getLogger(__name__) +__version__ = version("hyperspy") -from hyperspy import docstrings +# For development version, `setuptools_scm` will be used at build time +# to get the dev version, in case of missing vcs information (git archive, +# shallow repository), the fallback version defined in pyproject.toml will +# be used + +# if we have a editable install from a git repository try to use +# `setuptools_scm` to find a more accurate version: +# `importlib.metadata` will provide the version at installation +# time and for editable version this may be different + +# we only do that if we have enough git history, e.g. not shallow checkout +_root = Path(__file__).resolve().parents[1] +if (_root / ".git").exists() and not (_root / ".git/shallow").exists(): + try: + # setuptools_scm may not be installed + from setuptools_scm import get_version -__doc__ = """ + __version__ = get_version(_root, version_scheme="release-branch-semver") + except ImportError: # pragma: no cover + # setuptools_scm not install, we keep the existing __version__ + pass + + +__doc__ = ( + """ HyperSpy: a multi-dimensional data analysis package for Python ============================================================== Documentation is available in the docstrings and online at -http://hyperspy.org/hyperspy-doc/current/index.html. +https://hyperspy.org/hyperspy-doc/current/index.html. All public packages, functions and classes are in :mod:`~hyperspy.api`. All other packages and modules are for internal consumption and should not be @@ -38,10 +66,22 @@ More details in the :mod:`~hyperspy.api` docstring. -""" % docstrings.START_HSPY +""" + % docstrings.START_HSPY +) + + +__all__ = ["api", "__version__"] + +def __dir__(): + return sorted(__all__) -from . import Release -__all__ = ["api"] -__version__ = Release.version +def __getattr__(name): + if name in __all__: # pragma: no cover + # We can't get this block covered in the test suite because it is + # already imported, when running the test suite. + # If this is broken, a lot of things will be broken! + return importlib.import_module("." + name, "hyperspy") + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/hyperspy/_components/arctan.py b/hyperspy/_components/arctan.py index 184f5e9baf..6affa10127 100644 --- a/hyperspy/_components/arctan.py +++ b/hyperspy/_components/arctan.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # # This file is part of HyperSpy. # @@ -14,14 +14,13 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy._components.expression import Expression class Arctan(Expression): - r"""Arctan function component. .. math:: @@ -39,7 +38,7 @@ class Arctan(Expression): Parameters - ----------- + ---------- A : float Amplitude parameter. :math:`\lim_{x\to -\infty}f(x)=-A` and :math:`\lim_{x\to\infty}f(x)=A` @@ -51,12 +50,12 @@ class Arctan(Expression): """ - def __init__(self, A=1., k=1., x0=1., module=["numpy", "scipy"], **kwargs): - # Not to break scripts once we remove the legacy Arctan + def __init__(self, A=1.0, k=1.0, x0=1.0, module="numpy", **kwargs): + # To be able to still read old file versions that contain this argument if "minimum_at_zero" in kwargs: del kwargs["minimum_at_zero"] super().__init__( - expression="A * arctan(k * (x - x0))", + expression="A * atan(k * (x - x0))", name="Arctan", A=A, k=k, diff --git a/hyperspy/_components/bleasdale.py b/hyperspy/_components/bleasdale.py index 16b90a5797..a2b64f5ea0 100644 --- a/hyperspy/_components/bleasdale.py +++ b/hyperspy/_components/bleasdale.py @@ -1,52 +1,57 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np from hyperspy._components.expression import Expression -class Bleasdale(Expression): +class Bleasdale(Expression): r"""Bleasdale function component. - Also called the Bleasdale-Nelder function. Originates from the description of the yield-density relationship in crop growth. + Also called the Bleasdale-Nelder function. Originates from + the description of the yield-density relationship in crop growth. .. math:: f(x) = \left(a+b\cdot x\right)^{-1/c} Parameters - ----------- - a : Float - - b : Float - - c : Float - - **kwargs - Extra keyword arguments are passed to the ``Expression`` component. - + ---------- + a : float, default=1.0 + The value of Parameter a. + b : float, default=1.0 + The value of Parameter b. + c : float, default=1.0 + The value of Parameter c. + **kwargs + Extra keyword arguments are passed to + :class:`~.api.model.components1D.Expression`. + + Notes + ----- For :math:`(a+b\cdot x)\leq0`, the component will be set to 0. + """ - def __init__(self, a=1., b=1., c=1., module="numexpr", **kwargs): + def __init__(self, a=1.0, b=1.0, c=1.0, module=None, **kwargs): super().__init__( - expression="where((a + b * x) > 0, (a + b * x) ** (-1 / c), 0)", + expression="where((a + b * x) > 0, pow(a + b * x, -1 / c), 0)", name="Bleasdale", a=a, b=b, @@ -54,7 +59,16 @@ def __init__(self, a=1., b=1., c=1., module="numexpr", **kwargs): module=module, autodoc=False, compute_gradients=False, - **kwargs) + linear_parameter_list=["b"], + check_parameter_linearity=False, + **kwargs, + ) + module = self._whitelist["module"][1] + if module in ("numpy", "scipy"): + # Issue with calculating component at 0... + raise ValueError( + f"{module} is not supported for this component, use numexpr instead." + ) def grad_a(self, x): """ @@ -64,7 +78,7 @@ def grad_a(self, x): b = self.b.value c = self.c.value - return np.where((a + b * x) > 0, -(a + b * x) ** (-1 / c - 1) / c, 0) + return np.where((a + b * x) > 0, -((a + b * x) ** (-1 / c - 1)) / c, 0) def grad_b(self, x): """ @@ -74,8 +88,7 @@ def grad_b(self, x): b = self.b.value c = self.c.value - return np.where((a + b * x) > 0, -x * (a + b * x) ** (-1 / c - 1) / c - , 0) + return np.where((a + b * x) > 0, -x * (a + b * x) ** (-1 / c - 1) / c, 0) def grad_c(self, x): """ @@ -84,5 +97,6 @@ def grad_c(self, x): a = self.a.value b = self.b.value c = self.c.value - return np.where((a + b * x) > 0, np.log(a + b * x) / (c ** 2. * - (b * x + a) ** (1. / c)), 0) + return np.where( + (a + b * x) > 0, np.log(a + b * x) / (c**2.0 * (b * x + a) ** (1.0 / c)), 0 + ) diff --git a/hyperspy/_components/doniach.py b/hyperspy/_components/doniach.py index 70cda41a60..bc9c5b7d42 100644 --- a/hyperspy/_components/doniach.py +++ b/hyperspy/_components/doniach.py @@ -1,38 +1,37 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import math + import numpy as np -from hyperspy.component import _get_scaling_factor from hyperspy._components.expression import Expression from hyperspy._components.gaussian import _estimate_gaussian_parameters -from hyperspy.misc.utils import is_binned # remove in v2.0 +from hyperspy.component import _get_scaling_factor sqrt2pi = math.sqrt(2 * math.pi) -tiny = np.finfo(np.float64).eps +tiny = np.finfo(float).eps class Doniach(Expression): - - r""" Doniach Sunjic lineshape + r"""Doniach Sunjic lineshape component. .. math:: :nowrap: @@ -59,7 +58,7 @@ class Doniach(Expression): =============== =========== Parameters - ----------- + ---------- A : float Height sigma : float @@ -69,9 +68,10 @@ class Doniach(Expression): centre : float Location of the maximum (peak position). **kwargs - Extra keyword arguments are passed to the ``Expression`` component. + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. - Note + Notes ----- This is an asymmetric lineshape, originially design for xps but generally useful for fitting peaks with low side tails @@ -81,11 +81,18 @@ class Doniach(Expression): """ - def __init__(self, centre=0., A=1., sigma=1., alpha=0.5, - module=["numpy", "scipy"], **kwargs): + def __init__( + self, + centre=0.0, + A=1.0, + sigma=1.0, + alpha=0.5, + module="numpy", + **kwargs, + ): super().__init__( expression="A*cos(0.5*pi*alpha+\ - ((1.0 - alpha) * arctan( (x-centre+offset)/sigma) ) )\ + ((1.0 - alpha) * atan( (x-centre+offset)/sigma) ) )\ /(sigma**2 + (x-centre+offset)**2)**(0.5 * (1.0 - alpha));\ offset = 2.354820*sigma / (2 * tan(pi / (2 - alpha)))", name="Doniach", @@ -113,7 +120,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -140,35 +147,33 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): >>> s.axes_manager[-1].offset = -10 >>> s.axes_manager[-1].scale = 0.01 >>> g.estimate_parameters(s, -10, 10, False) + True """ super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] - centre, height, sigma = _estimate_gaussian_parameters(signal, x1, x2, - only_current) + centre, height, sigma = _estimate_gaussian_parameters( + signal, x1, x2, only_current + ) scaling_factor = _get_scaling_factor(signal, axis, centre) if only_current is True: self.centre.value = centre self.sigma.value = sigma self.A.value = height * 1.3 - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: self.A.value /= scaling_factor return True else: if self.A.map is None: self._create_arrays() - self.A.map['values'][:] = height * 1.3 - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.A.map['values'][:] /= scaling_factor - self.A.map['is_set'][:] = True - self.sigma.map['values'][:] = sigma - self.sigma.map['is_set'][:] = True - self.centre.map['values'][:] = centre - self.centre.map['is_set'][:] = True + self.A.map["values"][:] = height * 1.3 + if axis.is_binned: + self.A.map["values"][:] /= scaling_factor + self.A.map["is_set"][:] = True + self.sigma.map["values"][:] = sigma + self.sigma.map["is_set"][:] = True + self.centre.map["values"][:] = centre + self.centre.map["is_set"][:] = True self.fetch_stored_values() return True diff --git a/hyperspy/_components/eels_arctan.py b/hyperspy/_components/eels_arctan.py deleted file mode 100644 index d72b84503b..0000000000 --- a/hyperspy/_components/eels_arctan.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy._components.expression import Expression - - -class Arctan(Expression): - # Legacy class to be removed in v2.0 - r"""This is the legacy Arctan component dedicated to EELS measurements - that will renamed to `EELSArctan` in v2.0. - - To use the new Arctan component - set `minimum_at_zero=False`. See the documentation of - :meth:`hyperspy._components.arctan.Arctan` for details on - the usage. - - The EELS version :meth:`hyperspy._components.eels_arctan.EELSArctan` - (`minimum_at_zero=True`) shifts the function by A in the y direction - - """ - - def __init__(self, minimum_at_zero=False, **kwargs): - if minimum_at_zero: - from hyperspy.misc.utils import deprecation_warning - msg = ( - "The API of the `Arctan` component will change in v2.0. " - "This component will become `EELSArctan`." - "To use the new API, omit the `minimum_at_zero` option.") - deprecation_warning(msg) - - self.__class__ = EELSArctan - self.__init__(**kwargs) - else: - from hyperspy._components.arctan import Arctan - self.__class__ = Arctan - self.__init__(**kwargs) - - -class EELSArctan(Expression): - - r"""Arctan function component for EELS (with minimum at zero). - - .. math:: - - f(x) = A \cdot \left( \frac{\pi}{2} + - \arctan \left[ k \left( x-x_0 \right) \right] \right) - - - ============ ============= - Variable Parameter - ============ ============= - :math:`A` A - :math:`k` k - :math:`x_0` x0 - ============ ============= - - - Parameters - ----------- - A : float - Amplitude parameter. :math:`\lim_{x\to -\infty}f(x)=0` and - :math:`\lim_{x\to\infty}f(x)=2A` - k : float - Slope (steepness of the step). The larger :math:`k`, the sharper the - step. - x0 : float - Center parameter (:math:`f(x_0)=A`). - - """ - - def __init__(self, A=1., k=1., x0=1., module=["numpy", "scipy"], **kwargs): - # Not to break scripts once we remove the legacy Arctan - if "minimum_at_zero" in kwargs: - del kwargs["minimum_at_zero"] - super().__init__( - expression="A * (pi /2 + arctan(k * (x - x0)))", - name="Arctan", - A=A, - k=k, - x0=x0, - position="x0", - module=module, - autodoc=False, - **kwargs, - ) - - self.isbackground = False - self.convolved = True diff --git a/hyperspy/_components/eels_cl_edge.py b/hyperspy/_components/eels_cl_edge.py deleted file mode 100644 index da14f64938..0000000000 --- a/hyperspy/_components/eels_cl_edge.py +++ /dev/null @@ -1,377 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - - -import math -import logging - -import numpy as np -from scipy.interpolate import splev - -from hyperspy.component import Component -from hyperspy.misc.eels.hartree_slater_gos import HartreeSlaterGOS -from hyperspy.misc.eels.hydrogenic_gos import HydrogenicGOS -from hyperspy.misc.eels.effective_angle import effective_angle -from hyperspy.ui_registry import add_gui_method - - -_logger = logging.getLogger(__name__) - - -@add_gui_method(toolkey="hyperspy.EELSCLEdge_Component") -class EELSCLEdge(Component): - - """EELS core loss ionisation edge from hydrogenic or tabulated - Hartree-Slater GOS with splines for fine structure fitting. - - Hydrogenic GOS are limited to K and L shells. - - Currently it only supports Peter Rez's Hartree Slater cross sections - parametrised as distributed by Gatan in their Digital Micrograph (DM) - software. If Digital Micrograph is installed in the system HyperSpy in the - standard location HyperSpy should find the path to the HS GOS folder. - Otherwise, the location of the folder can be defined in HyperSpy - preferences, which can be done through hs.preferences.gui() or the - hs.preferences.EELS.eels_gos_files_path variable. - - Parameters - ---------- - element_subshell : {str, dict} - Usually a string, for example, 'Ti_L3' for the GOS of the titanium L3 - subshell. If a dictionary is passed, it is assumed that Hartree Slater - GOS was exported using `GOS.as_dictionary`, and will be reconstructed. - GOS : {'hydrogenic', 'Hartree-Slater', None} - The GOS to use. If None it will use the Hartree-Slater GOS if - they are available, otherwise it will use the hydrogenic GOS. - - Attributes - ---------- - onset_energy : Parameter - The edge onset position - intensity : Parameter - The factor by which the cross section is multiplied, what in - favourable cases is proportional to the number of atoms of - the element. It is a component.Parameter instance. - It is fixed by default. - fine_structure_coeff : Parameter - The coefficients of the spline that fits the fine structure. - Fix this parameter to fix the fine structure. It is a - component.Parameter instance. - effective_angle : Parameter - The effective collection semi-angle. It is automatically - calculated by set_microscope_parameters. It is a - component.Parameter instance. It is fixed by default. - fine_structure_smoothing : float between 0 and 1 - Controls the level of smoothing of the fine structure model. - Decreasing the value increases the level of smoothing. - fine_structure_active : bool - Activates/deactivates the fine structure feature. - - """ - _fine_structure_smoothing = 0.3 - - def __init__(self, element_subshell, GOS=None): - # Declare the parameters - Component.__init__(self, - ['intensity', - 'fine_structure_coeff', - 'effective_angle', - 'onset_energy']) - if isinstance(element_subshell, dict): - self.element = element_subshell['element'] - self.subshell = element_subshell['subshell'] - else: - self.element, self.subshell = element_subshell.split('_') - self.name = "_".join([self.element, self.subshell]) - self.energy_scale = None - self.effective_angle.free = False - self.fine_structure_active = False - self.fine_structure_width = 30. - self.fine_structure_coeff.ext_force_positive = False - self.GOS = None - # Set initial actions - if GOS is None: - try: - self.GOS = HartreeSlaterGOS(element_subshell) - GOS = 'Hartree-Slater' - except IOError: - GOS = 'hydrogenic' - _logger.info( - 'Hartree-Slater GOS not available. ' - 'Using hydrogenic GOS') - if self.GOS is None: - if GOS == 'Hartree-Slater': - self.GOS = HartreeSlaterGOS(element_subshell) - elif GOS == 'hydrogenic': - self.GOS = HydrogenicGOS(element_subshell) - else: - raise ValueError( - 'gos must be one of: None, \'hydrogenic\'' - ' or \'Hartree-Slater\'') - self.onset_energy.value = self.GOS.onset_energy - self.onset_energy.free = False - self._position = self.onset_energy - self.free_onset_energy = False - self.intensity.grad = self.grad_intensity - self.intensity.value = 1 - self.intensity.bmin = 0. - self.intensity.bmax = None - - self._whitelist['GOS'] = ('init', GOS) - if GOS == 'Hartree-Slater': - self._whitelist['element_subshell'] = ( - 'init', - self.GOS.as_dictionary(True)) - elif GOS == 'hydrogenic': - self._whitelist['element_subshell'] = ('init', element_subshell) - self._whitelist['fine_structure_active'] = None - self._whitelist['fine_structure_width'] = None - self._whitelist['fine_structure_smoothing'] = None - self.effective_angle.events.value_changed.connect( - self._integrate_GOS, []) - self.onset_energy.events.value_changed.connect(self._integrate_GOS, []) - self.onset_energy.events.value_changed.connect( - self._calculate_knots, []) - - # Automatically fix the fine structure when the fine structure is - # disable. - # In this way we avoid a common source of problems when fitting - # However the fine structure must be *manually* freed when we - # reactivate the fine structure. - def _get_fine_structure_active(self): - return self.__fine_structure_active - - def _set_fine_structure_active(self, arg): - if arg is False: - self.fine_structure_coeff.free = False - self.__fine_structure_active = arg - # Force replot - self.intensity.value = self.intensity.value - fine_structure_active = property(_get_fine_structure_active, - _set_fine_structure_active) - - def _get_fine_structure_width(self): - return self.__fine_structure_width - - def _set_fine_structure_width(self, arg): - self.__fine_structure_width = arg - self._set_fine_structure_coeff() - fine_structure_width = property(_get_fine_structure_width, - _set_fine_structure_width) - - # E0 - def _get_E0(self): - return self.__E0 - - def _set_E0(self, arg): - self.__E0 = arg - self._calculate_effective_angle() - E0 = property(_get_E0, _set_E0) - - # Collection semi-angle - def _get_collection_angle(self): - return self.__collection_angle - - def _set_collection_angle(self, arg): - self.__collection_angle = arg - self._calculate_effective_angle() - collection_angle = property(_get_collection_angle, - _set_collection_angle) - # Convergence semi-angle - - def _get_convergence_angle(self): - return self.__convergence_angle - - def _set_convergence_angle(self, arg): - self.__convergence_angle = arg - self._calculate_effective_angle() - convergence_angle = property(_get_convergence_angle, - _set_convergence_angle) - - def _calculate_effective_angle(self): - try: - self.effective_angle.value = effective_angle( - self.E0, - self.GOS.onset_energy, - self.convergence_angle, - self.collection_angle) - except BaseException: - # All the parameters may not be defined yet... - pass - - @property - def fine_structure_smoothing(self): - return self._fine_structure_smoothing - - @fine_structure_smoothing.setter - def fine_structure_smoothing(self, value): - if 0 <= value <= 1: - self._fine_structure_smoothing = value - self._set_fine_structure_coeff() - else: - raise ValueError( - "The value must be a number between 0 and 1") - - # It is needed because the property cannot be used to sort the edges - def _onset_energy(self): - return self.onset_energy.value - - def _set_fine_structure_coeff(self): - if self.energy_scale is None: - return - self.fine_structure_coeff._number_of_elements = int( - round(self.fine_structure_smoothing * - self.fine_structure_width / - self.energy_scale)) + 4 - self.fine_structure_coeff.bmin = None - self.fine_structure_coeff.bmax = None - self._calculate_knots() - if self.fine_structure_coeff.map is not None: - self.fine_structure_coeff._create_array() - - def set_microscope_parameters(self, E0, alpha, beta, energy_scale): - """ - Parameters - ---------- - E0 : float - Electron beam energy in keV. - alpha: float - Convergence semi-angle in mrad. - beta: float - Collection semi-angle in mrad. - energy_scale : float - The energy step in eV. - """ - # Relativistic correction factors - old = self.effective_angle.value - with self.effective_angle.events.value_changed.suppress_callback( - self._integrate_GOS): - self.convergence_angle = alpha - self.collection_angle = beta - self.energy_scale = energy_scale - self.E0 = E0 - if self.effective_angle.value != old: - self._integrate_GOS() - - def _integrate_GOS(self): - # Integration over q using splines - angle = self.effective_angle.value * 1e-3 # in rad - self.tab_xsection = self.GOS.integrateq( - self.onset_energy.value, angle, self.E0) - # Calculate extrapolation powerlaw extrapolation parameters - E1 = self.GOS.energy_axis[-2] + self.GOS.energy_shift - E2 = self.GOS.energy_axis[-1] + self.GOS.energy_shift - y1 = self.GOS.qint[-2] # in m**2/bin */ - y2 = self.GOS.qint[-1] # in m**2/bin */ - self._power_law_r = math.log(y2 / y1) / math.log(E1 / E2) - self._power_law_A = y1 / E1 ** -self._power_law_r - - def _calculate_knots(self): - start = self.onset_energy.value - stop = start + self.fine_structure_width - self.__knots = np.r_[ - [start] * 4, - np.linspace( - start, - stop, - self.fine_structure_coeff._number_of_elements)[ - 2:-2], - [stop] * 4] - - def function(self, E): - """Returns the number of counts in barns - - """ - shift = self.onset_energy.value - self.GOS.onset_energy - if shift != self.GOS.energy_shift: - # Because hspy Events are not executed in any given order, - # an external function could be in the same event execution list - # as _integrate_GOS and be executed first. That can potentially - # cause an error that enforcing _integrate_GOS here prevents. Note - # that this is suboptimal because _integrate_GOS is computed twice - # unnecessarily. - self._integrate_GOS() - Emax = self.GOS.energy_axis[-1] + self.GOS.energy_shift - cts = np.zeros((len(E))) - bsignal = (E >= self.onset_energy.value) - if self.fine_structure_active is True: - bfs = bsignal * ( - E < (self.onset_energy.value + self.fine_structure_width)) - cts[bfs] = splev( - E[bfs], ( - self.__knots, - self.fine_structure_coeff.value + (0,) * 4, - 3)) - bsignal[bfs] = False - itab = bsignal * (E <= Emax) - cts[itab] = self.tab_xsection(E[itab]) - bsignal[itab] = False - cts[bsignal] = self._power_law_A * E[bsignal] ** -self._power_law_r - return cts * self.intensity.value - - def grad_intensity(self, E): - return self.function(E) / self.intensity.value - - def fine_structure_coeff_to_txt(self, filename): - np.savetxt(filename + '.dat', self.fine_structure_coeff.value, - fmt="%12.6G") - - def txt_to_fine_structure_coeff(self, filename): - fs = np.loadtxt(filename) - self._calculate_knots() - if len(fs) == len(self.__knots): - self.fine_structure_coeff.value = fs - else: - raise ValueError( - "The provided fine structure file " - "doesn't match the size of the current fine structure") - - def get_fine_structure_as_signal1D(self): - """Returns a spectrum containing the fine structure. - - Notes - ----- - The fine structure is corrected from multiple scattering if - the model was convolved with a low-loss spectrum - - """ - from hyperspy._signals.eels import EELSSpectrum - channels = int(np.floor( - self.fine_structure_width / self.energy_scale)) - data = np.zeros(self.fine_structure_coeff.map.shape + - (channels,)) - s = EELSSpectrum( - data, - axes=self.intensity._axes_manager._get_axes_dicts()) - s.get_dimensions_from_data() - s.axes_manager.signal_axes[0].offset = self.onset_energy.value - # Backup the axes_manager - original_axes_manager = self._axes_manager - self._axes_manager = s.axes_manager - for spectrum in s: - self.fetch_stored_values() - spectrum.data[:] = self.function( - s.axes_manager.signal_axes[0].axis) - # Restore the axes_manager and the values - self._axes_manager = original_axes_manager - self.fetch_stored_values() - - s.metadata.General.title = self.name.replace( - '_', ' ') + ' fine structure' - - return s diff --git a/hyperspy/_components/eels_double_power_law.py b/hyperspy/_components/eels_double_power_law.py deleted file mode 100644 index 1742597c11..0000000000 --- a/hyperspy/_components/eels_double_power_law.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - -from hyperspy.docstrings.parameters import FUNCTION_ND_DOCSTRING -from hyperspy._components.expression import Expression - - -class DoublePowerLaw(Expression): - - r"""Double power law component for EELS spectra. - - .. math:: - - f(x) = A \cdot [s_r \cdot (x - x_0 - x_s)^{-r} + (x - x_0)^{-r}] - - ============= ============= - Variable Parameter - ============= ============= - :math:`A` A - :math:`r` r - :math:`x_0` origin - :math:`x_s` shift - :math:`s_r` ratio - ============= ============= - - Parameters - ---------- - A : float - Height parameter. - r : float - Power law coefficient. - origin : float - Location parameter. - shift : float - Offset of second power law. - ratio : float - Height ratio of the two power law components. - **kwargs - Extra keyword arguments are passed to the ``Expression`` component. - - The `left_cutoff` parameter can be used to set a lower threshold from which - the component will return 0. - """ - - def __init__(self, A=1e-5, r=3., origin=0., shift=20., ratio=1., - left_cutoff=0.0, module="numexpr", compute_gradients=False, - **kwargs): - super().__init__( - expression="where(x > left_cutoff, \ - A * (ratio * (x - origin - shift) ** -r \ - + (x - origin) ** -r), 0)", - name="DoublePowerLaw", - A=A, - r=r, - origin=origin, - shift=shift, - ratio=ratio, - left_cutoff=left_cutoff, - position="origin", - autodoc=False, - module=module, - compute_gradients=compute_gradients, - **kwargs, - ) - - self.origin.free = False - self.shift.value = 20. - self.shift.free = False - - # Boundaries - self.A.bmin = 0. - self.A.bmax = None - self.r.bmin = 1. - self.r.bmax = 5. - - self.isbackground = True - self.convolved = False - - def function_nd(self, axis): - """%s - - """ - return super().function_nd(axis) - - function_nd.__doc__ %= FUNCTION_ND_DOCSTRING - - # Define gradients - def grad_A(self, x): - return self.function(x) / self.A.value - - def grad_r(self, x): - return np.where(x > self.left_cutoff.value, -self.A.value * - self.ratio.value * (x - self.origin.value - - self.shift.value) ** (-self.r.value) * - np.log(x - self.origin.value - self.shift.value) - - self.A.value * (x - self.origin.value) ** - (-self.r.value) * np.log(x - self.origin.value), 0) - - def grad_origin(self, x): - return np.where(x > self.left_cutoff.value, self.A.value * self.r.value - * self.ratio.value * (x - self.origin.value - self.shift.value) - ** (-self.r.value - 1) + self.A.value * self.r.value - * (x - self.origin.value) ** (-self.r.value - 1), 0) - - def grad_shift(self, x): - return np.where(x > self.left_cutoff.value, self.A.value * self.r.value - * self.ratio.value * (x - self.origin.value - - self.shift.value) ** (-self.r.value - 1), 0) - - def grad_ratio(self, x): - return np.where(x > self.left_cutoff.value, self.A.value * - (x - self.origin.value - self.shift.value) ** - (-self.r.value), 0) diff --git a/hyperspy/_components/eels_vignetting.py b/hyperspy/_components/eels_vignetting.py deleted file mode 100644 index fa516284af..0000000000 --- a/hyperspy/_components/eels_vignetting.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -from hyperspy.component import Component -from hyperspy._components.gaussian import Gaussian - - -class Vignetting(Component): - - """ - Model the vignetting of the lens with a cos^4 law multiplied by lines on - the edges - """ - - def __init__(self): - Component.__init__(self, - ['optical_center', - 'height', - 'period', - 'left_slope', - 'right_slope', - 'left', - 'right', - 'sigma']) - self.left.value = np.nan - self.right.value = np.nan - self.side_vignetting = False - self.fix_side_vignetting() - self.gaussian = Gaussian() - self.gaussian.centre.free, self.gaussian.A.free = False, False - self.sigma.value = 1. - self.gaussian.A.value = 1. - self.period.value = 1. - self.extension_nch = 100 - self._position = self.optical_center - - def function(self, x): - sigma = self.sigma.value - x0 = self.optical_center.value - A = self.height.value - period = self.period.value - la = self.left_slope.value - ra = self.right_slope.value - l = self.left.value - r = self.right.value - ex = self.extension_nch - if self.side_vignetting is True: - - x = x.tolist() - x = list(range(-ex, 0)) + x + \ - list(range(int(x[-1]) + 1, int(x[-1]) + ex + 1)) - x = np.array(x) - v1 = A * np.cos((x - x0) / (2 * np.pi * period)) ** 4 - v2 = np.where(x < l, - 1. - (l - x) * la, - np.where(x < r, - 1., - 1. - (x - r) * ra)) - self.gaussian.sigma.value = sigma - self.gaussian.origin.value = (x[-1] + x[0]) / 2 - result = np.convolve(self.gaussian.function(x), v1 * v2, 'same') - return result[ex:-ex] - else: - return A * np.cos((x - x0) / (2 * np.pi * period)) ** 4 - - def free_side_vignetting(self): - self.left.free = True - self.right.free = True - self.left_slope.free = True - self.right_slope.free = True - self.sigma.free = True - - def fix_side_vignetting(self): - self.left.free = False - self.right.free = False - self.left_slope.free = False - self.right_slope.free = False - self.sigma.free = False - - def free_cos_vignetting(self): - self.optical_center.free = True - self.period.free = True - self.height.free = True - - def fix_cos_vignetting(self): - self.optical_center.free = False - self.period.free = False - self.height.free = False diff --git a/hyperspy/_components/error_function.py b/hyperspy/_components/error_function.py index be0806c6d2..1b57b4581d 100644 --- a/hyperspy/_components/error_function.py +++ b/hyperspy/_components/error_function.py @@ -1,28 +1,25 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy._components.expression import Expression -from distutils.version import LooseVersion -import sympy class Erf(Expression): - r"""Error function component. .. math:: @@ -41,19 +38,20 @@ class Erf(Expression): Parameters ---------- - A : float - The min/max values of the distribution are -A/2 and A/2. - sigma : float - Width of the distribution. - origin : float - Position of the zero crossing. + A : float + The min/max values of the distribution are -A/2 and A/2. + sigma : float + Width of the distribution. + origin : float + Position of the zero crossing. + **kwargs + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. """ - def __init__(self, A=1., sigma=1., origin=0., module=["numpy", "scipy"], - **kwargs): - if LooseVersion(sympy.__version__) < LooseVersion("1.3"): - raise ImportError("The `ErrorFunction` component requires " - "SymPy >= 1.3") + def __init__( + self, A=1.0, sigma=1.0, origin=0.0, module=["numpy", "scipy"], **kwargs + ): super().__init__( expression="A * erf((x - origin) / sqrt(2) / sigma) / 2", name="Erf", @@ -66,7 +64,7 @@ def __init__(self, A=1., sigma=1., origin=0., module=["numpy", "scipy"], ) # Boundaries - self.A.bmin = 0. + self.A.bmin = 0.0 self.isbackground = False self.convolved = True diff --git a/hyperspy/_components/exponential.py b/hyperspy/_components/exponential.py index a6a93661b0..dcdd1984bd 100644 --- a/hyperspy/_components/exponential.py +++ b/hyperspy/_components/exponential.py @@ -1,32 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import numpy as np import logging +import numpy as np + from hyperspy._components.expression import Expression -from hyperspy.misc.utils import is_binned # remove in v2.0 _logger = logging.getLogger(__name__) class Exponential(Expression): - r"""Exponential function component. .. math:: @@ -42,16 +41,17 @@ class Exponential(Expression): Parameters - ----------- + ---------- A: float Maximum intensity tau: float Scale parameter (time constant) **kwargs - Extra keyword arguments are passed to the ``Expression`` component. + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. """ - def __init__(self, A=1., tau=1., module="numexpr", **kwargs): + def __init__(self, A=1.0, tau=1.0, module=None, **kwargs): super().__init__( expression="A * exp(-x / tau)", name="Exponential", @@ -70,7 +70,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : BaseSignal instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -106,13 +106,14 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): if s._lazy: import dask.array as da + exp = da.exp log = da.log else: exp = np.exp log = np.log - with np.errstate(divide='raise', invalid='raise'): + with np.errstate(divide="raise", invalid="raise"): try: # use log and exp to compute geometric mean to avoid overflow a1 = s.isig[i1:i_mid].data @@ -124,8 +125,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): x1 = (x_start + x_mid) / 2 x2 = (x_mid + x_end) / 2 - A = exp((log(geo_mean1) - (x1 / x2) * log(geo_mean2)) / - (1 - x1 / x2)) + A = exp((log(geo_mean1) - (x1 / x2) * log(geo_mean2)) / (1 - x1 / x2)) t = -x2 / (log(geo_mean2) - log(A)) if s._lazy: @@ -135,19 +135,22 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): A = np.nan_to_num(A) t = np.nan_to_num(t) - except (FloatingPointError): + except FloatingPointError: if i1 == i2: - _logger.warning('Exponential parameters estimation failed ' - 'because signal range includes only one ' - 'point.') + _logger.warning( + "Exponential parameters estimation failed " + "because signal range includes only one " + "point." + ) else: - _logger.warning('Exponential parameters estimation failed ' - 'with a "divide by zero" error (likely log of ' - 'a zero or negative value).') + _logger.warning( + "Exponential parameters estimation failed " + 'with a "divide by zero" error (likely log of ' + "a zero or negative value)." + ) return False - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + + if axis.is_binned: if axis.is_uniform: A /= axis.scale else: @@ -161,10 +164,10 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): if self.A.map is None: self._create_arrays() - self.A.map['values'][:] = A - self.A.map['is_set'][:] = True - self.tau.map['values'][:] = t - self.tau.map['is_set'][:] = True + self.A.map["values"][:] = A + self.A.map["is_set"][:] = True + self.tau.map["values"][:] = t + self.tau.map["is_set"][:] = True self.fetch_stored_values() return True diff --git a/hyperspy/_components/expression.py b/hyperspy/_components/expression.py index 7a30adde76..14c8db0acc 100644 --- a/hyperspy/_components/expression.py +++ b/hyperspy/_components/expression.py @@ -1,32 +1,36 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import importlib +import logging +import warnings from functools import wraps + import numpy as np import sympy -import warnings from hyperspy.component import Component from hyperspy.docstrings.parameters import FUNCTION_ND_DOCSTRING +_logger = logging.getLogger(__name__) + -_CLASS_DOC = \ - """%s component (created with Expression). +_CLASS_DOC = """%s component (created with Expression). .. math:: @@ -52,129 +56,173 @@ def fn_wrapped(self, x, y): def _parse_substitutions(string): - splits = map(str.strip, string.split(';')) + splits = map(str.strip, string.split(";")) expr = sympy.sympify(next(splits)) # We substitute one by one manually, as passing all at the same time does # not work as we want (substitutions inside other substitutions do not work) for sub in splits: - t = tuple(map(str.strip, sub.split('='))) + t = tuple(map(str.strip, sub.split("="))) expr = expr.subs(t[0], sympy.sympify(t[1])) return expr class Expression(Component): - """Create a component from a string expression. - """ - def __init__(self, expression, name, position=None, module="numpy", - autodoc=True, add_rotation=False, rotation_center=None, - rename_pars={}, compute_gradients=True, **kwargs): - """Create a component from a string expression. - - It automatically generates the partial derivatives and the - class docstring. - - Parameters - ---------- - expression : str - Component function in SymPy text expression format with - substitutions separated by `;`. See examples and the SymPy - documentation for details. In order to vary the components along the - signal dimensions, the variables `x` and `y` must be included for 1D - or 2D components. Also, if `module` is "numexpr" the - functions are limited to those that numexpr support. See its - documentation for details. - name : str - Name of the component. - position : str, optional - The parameter name that defines the position of the component if - applicable. It enables interative adjustment of the position of the - component in the model. For 2D components, a tuple must be passed - with the name of the two parameters e.g. `("x0", "y0")`. - module : {"numpy", "numexpr", "scipy"}, default "numpy" - Module used to evaluate the function. numexpr is often faster but - it supports fewer functions and requires installing numexpr. - add_rotation : bool, default False - This is only relevant for 2D components. If `True` it automatically - adds `rotation_angle` parameter. - rotation_center : {None, tuple} - If None, the rotation center is the center i.e. (0, 0) if `position` - is not defined, otherwise the center is the coordinates specified - by `position`. Alternatively a tuple with the (x, y) coordinates - of the center can be provided. - rename_pars : dictionary - The desired name of a parameter may sometimes coincide with e.g. - the name of a scientific function, what prevents using it in the - `expression`. `rename_parameters` is a dictionary to map the name - of the parameter in the `expression`` to the desired name of the - parameter in the `Component`. For example: {"_gamma": "gamma"}. - compute_gradients : bool, optional - If `True`, compute the gradient automatically using sympy. If sympy - does not support the calculation of the partial derivatives, for - example in case of expression containing a "where" condition, - it can be disabled by using `compute_gradients=False`. - **kwargs - Keyword arguments can be used to initialise the value of the - parameters. - - Note - ---- - As of version 1.4, Sympy's lambdify function, that the ``Expression`` - components uses internally, does not support the differentiation of - some expressions, for example those containing a "where" condition. - In such cases, the gradients can be set manually if required. - - Examples - -------- - The following creates a Gaussian component and set the initial value - of the parameters: - - >>> hs.model.components1D.Expression( - ... expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2)", - ... name="Gaussian", - ... height=1, - ... fwhm=1, - ... x0=0, - ... position="x0",) - - Substitutions for long or complicated expressions are separated by - semicolumns: - - >>> expr = 'A*B/(A+B) ; A = sin(x)+one; B = cos(y) - two; y = tan(x)' - >>> comp = hs.model.components1D.Expression( - ... expression=expr, - ... name='my function') - >>> comp.parameters - (, - ) + It automatically generates the partial derivatives and the + class docstring. + + Parameters + ---------- + expression : str + Component function in SymPy text expression format with + substitutions separated by `;`. See examples and the SymPy + documentation for details. In order to vary the components along the + signal dimensions, the variables `x` and `y` must be included for 1D + or 2D components. Also, if `module` is "numexpr" the + functions are limited to those that numexpr support. See its + documentation for details. + name : str + Name of the component. + position : str, optional + The parameter name that defines the position of the component if + applicable. It enables interative adjustment of the position of the + component in the model. For 2D components, a tuple must be passed + with the name of the two parameters e.g. `("x0", "y0")`. + module : None or str {``"numpy"`` | ``"numexpr"`` | ``"scipy"``}, default "numpy" + Module used to evaluate the function. numexpr is often faster but + it supports fewer functions and requires installing numexpr. + If None, the "numexpr" will be used if installed. + add_rotation : bool, default False + This is only relevant for 2D components. If `True` it automatically + adds `rotation_angle` parameter. + rotation_center : None or tuple + If None, the rotation center is the center i.e. (0, 0) if `position` + is not defined, otherwise the center is the coordinates specified + by `position`. Alternatively a tuple with the (x, y) coordinates + of the center can be provided. + rename_pars : dict + The desired name of a parameter may sometimes coincide with e.g. + the name of a scientific function, what prevents using it in the + `expression`. `rename_parameters` is a dictionary to map the name + of the parameter in the `expression`` to the desired name of the + parameter in the `Component`. For example: {"_gamma": "gamma"}. + compute_gradients : bool, optional + If `True`, compute the gradient automatically using sympy. If sympy + does not support the calculation of the partial derivatives, for + example in case of expression containing a "where" condition, + it can be disabled by using `compute_gradients=False`. + linear_parameter_list : list + A list of the components parameters that are known to be linear + parameters. + check_parameter_linearity : bool + If `True`, automatically check if each parameter is linear and set + its corresponding attribute accordingly. If `False`, the default is to + set all parameters, except for those who are specified in + ``linear_parameter_list``. + + **kwargs : dict + Keyword arguments can be used to initialise the value of the + parameters. + + Notes + ----- + As of version 1.4, Sympy's lambdify function, that the + :class:`~.api.model.components1D.Expression` + components uses internally, does not support the differentiation of + some expressions, for example those containing a "where" condition. + In such cases, the gradients can be set manually if required. + + Examples + -------- + The following creates a Gaussian component and set the initial value + of the parameters: + + >>> hs.model.components1D.Expression( + ... expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2)", + ... name="Gaussian", + ... height=1, + ... fwhm=1, + ... x0=0, + ... position="x0",) + + + Substitutions for long or complicated expressions are separated by + semicolumns: + + >>> expr = 'A*B/(A+B) ; A = sin(x)+one; B = cos(y) - two; y = tan(x)' + >>> comp = hs.model.components1D.Expression( + ... expression=expr, + ... name='my function' + ... ) + >>> comp.parameters + (, + ) - """ + """ + + def __init__( + self, + expression, + name, + position=None, + module="numpy", + autodoc=True, + add_rotation=False, + rotation_center=None, + rename_pars={}, + compute_gradients=True, + linear_parameter_list=None, + check_parameter_linearity=True, + **kwargs, + ): + if module is None: + module = "numexpr" + + if module == "numexpr": + numexpr_spec = importlib.util.find_spec("numexpr") + if numexpr_spec is None: + module = "numpy" + _logger.warning( + "Numexpr is not installed, falling back to numpy, " + "which is slower to calculate model." + ) + + if linear_parameter_list is None: + linear_parameter_list = [] self._add_rotation = add_rotation self._str_expression = expression - self._rename_pars = rename_pars + self._module = module + self._rename_pars = rename_pars if rename_pars is not None else {} + # Since the expression string uses the parameter name before renaming + # it is useful to have the inverse mapping + self._rename_pars_inv = {v: k for k, v in self._rename_pars.items()} self._compute_gradients = compute_gradients + if rotation_center is None: self.compile_function(module=module, position=position) else: self.compile_function(module=module, position=rotation_center) + # Initialise component - Component.__init__(self, self._parameter_strings) + Component.__init__(self, self._parameter_strings, linear_parameter_list) # When creating components using Expression (for example GaussianHF) # we shouldn't add anything else to the _whitelist as the # component should be initizialized with its own kwargs. # An exception is "module" - self._whitelist['module'] = ('init', module) + self._whitelist["module"] = ("init", module) if self.__class__ is Expression: - self._whitelist['expression'] = ('init', expression) - self._whitelist['name'] = ('init', name) - self._whitelist['position'] = ('init', position) - self._whitelist['rename_pars'] = ('init', rename_pars) - self._whitelist['compute_gradients'] = ('init', compute_gradients) + self._whitelist["expression"] = ("init", expression) + self._whitelist["name"] = ("init", name) + self._whitelist["position"] = ("init", position) + self._whitelist["rename_pars"] = ("init", rename_pars) + self._whitelist["linear_parameter_list"] = ("init", linear_parameter_list) + self._whitelist["compute_gradients"] = ("init", compute_gradients) if self._is2D: - self._whitelist['add_rotation'] = ('init', self._add_rotation) - self._whitelist['rotation_center'] = ('init', rotation_center) + self._whitelist["add_rotation"] = ("init", self._add_rotation) + self._whitelist["rotation_center"] = ("init", rotation_center) self.name = name + # Set the position parameter if position: if self._is2D: @@ -182,35 +230,46 @@ class docstring. self._position_y = getattr(self, position[1]) else: self._position = getattr(self, position) + # Set the initial value of the parameters if kwargs: for kwarg, value in kwargs.items(): - setattr(getattr(self, kwarg), 'value', value) + setattr(getattr(self, kwarg), "value", value) if autodoc: - self.__doc__ = _CLASS_DOC % ( - name, sympy.latex(_parse_substitutions(expression))) - - def compile_function(self, module="numpy", position=False): + self.__doc__ = _CLASS_DOC % (name, sympy.latex(self._parsed_expr)) + + for parameter_name in linear_parameter_list: + setattr(getattr(self, parameter_name), "_linear", True) + + if check_parameter_linearity: + for p in self.parameters: + if p.name not in linear_parameter_list: + # _parsed_expr used "non public" parameter name and we + # need to use the correct parameter name by using + # _rename_pars_inv + p._linear = _check_parameter_linearity( + self._parsed_expr, self._rename_pars_inv.get(p.name, p.name) + ) + + def compile_function(self, module, position=False): """ Compile the function and calculate the gradient automatically when possible. Useful to recompile the function and gradient with a different module. """ - import sympy - from sympy.utilities.lambdify import lambdify try: # Expression is just a constant float(self._str_expression) except ValueError: pass else: - raise ValueError('Expression must contain a symbol, i.e. x, a, ' - 'etc.') + raise ValueError("Expression must contain a symbol, i.e. x, a, " "etc.") expr = _parse_substitutions(self._str_expression) + self._parsed_expr = expr # Extract x x = [symbol for symbol in expr.free_symbols if symbol.name == "x"] - if not x: # Expression is just a parameter, no x -> Offset + if not x: # Expression is just a parameter, no x -> Offset # lambdify doesn't support constant # https://github.com/sympy/sympy/issues/5642 # x = [sympy.Symbol('x')] @@ -226,53 +285,61 @@ def compile_function(self, module="numpy", position=False): position = position or (0, 0) rotx = sympy.sympify( "{0} + (x - {0}) * cos(rotation_angle) - (y - {1}) *" - " sin(rotation_angle)" - .format(*position)) + " sin(rotation_angle)".format(*position) + ) roty = sympy.sympify( "{1} + (x - {0}) * sin(rotation_angle) + (y - {1}) *" - "cos(rotation_angle)" - .format(*position)) + "cos(rotation_angle)".format(*position) + ) expr = expr.subs({"x": rotx, "y": roty}, simultaneous=False) - rvars = sympy.symbols([s.name for s in expr.free_symbols], real=True) - real_expr = expr.subs( - {orig: real_ for (orig, real_) in zip(expr.free_symbols, rvars)}) + original_vars = [symbol for symbol in expr.free_symbols] + real_vars = sympy.symbols([symbol.name for symbol in original_vars], real=True) # just replace with the assumption that all our variables are real - expr = real_expr - + # as this helps with differentiation + expr = expr.subs( + {orig: real_ for (orig, real_) in zip(original_vars, real_vars)} + ) eval_expr = expr.evalf() # Extract parameters - variables = ("x", "y") if self._is2D else ("x", ) + variables = ("x", "y") if self._is2D else ("x",) parameters = [ - symbol for symbol in expr.free_symbols - if symbol.name not in variables] - parameters.sort(key=lambda x: x.name) # to have a reliable order + symbol for symbol in expr.free_symbols if symbol.name not in variables + ] + # to have a reliable order + parameters.sort(key=lambda parameter: parameter.name) # Create compiled function variables = [x, y] if self._is2D else [x] - self._f = lambdify(variables + parameters, eval_expr, - modules=module, dummify=False) + self._f = sympy.utilities.lambdify( + variables + parameters, eval_expr, modules=module, dummify=False + ) if self._is2D: - def f(x, y): return self._f( - x, y, *[p.value for p in self.parameters]) + + def f(x, y): + return self._f(x, y, *[p.value for p in self.parameters]) else: - def f(x): return self._f(x, *[p.value for p in self.parameters]) + + def f(x): + return self._f(x, *[p.value for p in self.parameters]) + setattr(self, "function", f) - parnames = [symbol.name if symbol.name not in self._rename_pars else self._rename_pars[symbol.name] - for symbol in parameters] + parnames = [ + self._rename_pars.get(symbol.name, symbol.name) for symbol in parameters + ] self._parameter_strings = parnames if self._compute_gradients: try: - ffargs = (_fill_function_args_2d if - self._is2D else _fill_function_args) - for parameter in parameters: - grad_expr = sympy.diff(eval_expr, parameter) - name = parameter.name if parameter.name not in self._rename_pars else self._rename_pars[ - parameter.name] - f_grad = lambdify(variables + parameters, - grad_expr.evalf(), - modules=module, - dummify=False) + ffargs = _fill_function_args_2d if self._is2D else _fill_function_args + for p in parameters: + grad_expr = sympy.diff(eval_expr, p) + name = self._rename_pars.get(p.name, p.name) + f_grad = sympy.utilities.lambdify( + variables + parameters, + grad_expr.evalf(), + modules=module, + dummify=False, + ) grad_p = ffargs(f_grad).__get__(self, Expression) if len(grad_expr.free_symbols) == 0: # Vectorize in case of constant function @@ -281,28 +348,157 @@ def f(x): return self._f(x, *[p.value for p in self.parameters]) setattr(self, f"grad_{name}", grad_p) except (SyntaxError, AttributeError): - warnings.warn("The gradients can not be computed with sympy.", - UserWarning) + warnings.warn( + "The gradients can not be computed with sympy.", UserWarning + ) def function_nd(self, *args): - """%s - - """ + """%s""" if self._is2D: x, y = args[0], args[1] # navigation dimension is 0, f_nd same as f if not self._is_navigation_multidimensional: return self.function(x, y) else: - return self._f(x[np.newaxis, ...], y[np.newaxis, ...], - *[p.map['values'][..., np.newaxis, np.newaxis] - for p in self.parameters]) + return self._f( + x[np.newaxis, ...], + y[np.newaxis, ...], + *[ + p.map["values"][..., np.newaxis, np.newaxis] + for p in self.parameters + ], + ) else: x = args[0] if not self._is_navigation_multidimensional: return self.function(x) else: - return self._f(x[np.newaxis, ...], - *[p.map['values'][..., np.newaxis] - for p in self.parameters]) + return self._f( + x[np.newaxis, ...], + *[p.map["values"][..., np.newaxis] for p in self.parameters], + ) + function_nd.__doc__ %= FUNCTION_ND_DOCSTRING + + @property + def _constant_term(self): + """ + Get value of constant term of component, assuming that the nonlinear + term are fixed. + + The 'constant' part of a component is any part that doesn't change + when the free parameters are changed. + """ + free_linear_parameters = [ + # Use `_free` private attribute not to interfere with twin + self._rename_pars_inv.get(p.name, p.name) + for p in self.parameters + if p._linear and p._free + ] + + expr = sympy.sympify(self._str_expression) + args = [sympy.sympify(arg, strict=False) for arg in free_linear_parameters] + constant_expr, _ = expr.as_independent(*args, as_Add=True) + + # Then replace symbols with value of each parameter + free_symbols = [str(free) for free in constant_expr.free_symbols] + for p in self.parameters: + if p.name in free_symbols: + name = self._rename_pars_inv.get(p.name, p.name) + constant_expr = constant_expr.subs(name, p.value) + + return float(constant_expr.evalf()) + + def _separate_pseudocomponents(self): + """ + Separate an expression into a group of lambdified functions + that can compute the free parts of the expression, and a single + lambdified function that computes the fixed parts of the expression + + Used by the _compute_expression_part method. + """ + expr = self._str_expression + ex = sympy.sympify(expr) + remaining_elements = ex.copy() + free_pseudo_components = {} + variables = ("x", "y") if self._is2D else ("x",) + + for para in self.free_parameters: + name = self._rename_pars_inv.get(para.name, para.name) + symbol = sympy.sympify(name, strict=False) + element = ex.as_independent(symbol)[-1] + remaining_elements -= element + element_names = set([str(p) for p in element.free_symbols]) - set(variables) + free_pseudo_components[para.name] = { + "function": sympy.utilities.lambdify( + variables + tuple(element_names), element, modules=self._module + ), + "parameters": [ + getattr(self, self._rename_pars.get(e, e)) for e in element_names + ], + } + + element_names = set([str(p) for p in remaining_elements.free_symbols]) - set( + variables + ) + + fixed_pseudo_components = { + "function": sympy.utilities.lambdify( + variables + tuple(element_names), + remaining_elements, + modules=self._module, + ), + "parameters": [ + getattr(self, self._rename_pars.get(e, e)) for e in element_names + ], + } + + return ( + free_pseudo_components, + fixed_pseudo_components, + ) + + def _compute_expression_part(self, part): + """Compute the expression for a given value or map["values"].""" + model = self.model + function = part["function"] + parameters = [para.value for para in part["parameters"]] + try: + model_convolved = model.convolved + except NotImplementedError: + model_convolved = False + if model_convolved and self.convolved: + data = model._convolve_component_values( + function(model.convolution_axis, *parameters) + ) + else: + axes = [ax.axis for ax in model.axes_manager.signal_axes] + mesh = np.meshgrid(*axes) + data = function(*mesh, *parameters) + slice_ = np.where(model._channel_switches) + if len(np.shape(data)) == 0: + # For calculation of constant term of the component + signal_shape = model.axes_manager._signal_shape_in_array + data = data * np.ones(signal_shape)[slice_] + else: + data = np.moveaxis(data[slice_], 0, -1) + + return data + + +def _check_parameter_linearity(expr, name): + """Check whether expression is linear for a given parameter.""" + symbol = sympy.Symbol(name) + try: + if not sympy.diff(expr, symbol, 2) == 0: + return False + except AttributeError: + # AttributeError occurs if the expression cannot be parsed + # for instance some expressions with where. + warnings.warn( + f"The linearity of the parameter `{name}` can't be " + "determined automatically.", + UserWarning, + ) + return False + return True diff --git a/hyperspy/_components/gaussian.py b/hyperspy/_components/gaussian.py index c988feb82e..2c13b990c5 100644 --- a/hyperspy/_components/gaussian.py +++ b/hyperspy/_components/gaussian.py @@ -1,29 +1,28 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import math -import numpy as np import dask.array as da +import numpy as np -from hyperspy.component import _get_scaling_factor from hyperspy._components.expression import Expression -from hyperspy.misc.utils import is_binned # remove in v2.0 +from hyperspy.component import _get_scaling_factor sqrt2pi = math.sqrt(2 * math.pi) sigma2fwhm = 2 * math.sqrt(2 * math.log(2)) @@ -35,23 +34,31 @@ def _estimate_gaussian_parameters(signal, x1, x2, only_current): X = axis.axis[i1:i2] if only_current is True: - data = signal()[i1:i2] + data = signal._get_current_data()[i1:i2] X_shape = (len(X),) i = 0 centre_shape = (1,) else: i = axis.index_in_array - data_gi = [slice(None), ] * len(signal.data.shape) + data_gi = [ + slice(None), + ] * len(signal.data.shape) data_gi[axis.index_in_array] = slice(i1, i2) data = signal.data[tuple(data_gi)] - X_shape = [1, ] * len(signal.data.shape) + X_shape = [ + 1, + ] * len(signal.data.shape) X_shape[axis.index_in_array] = data.shape[i] centre_shape = list(data.shape) centre_shape[i] = 1 centre = np.sum(X.reshape(X_shape) * data, i) / np.sum(data, i) - sigma = np.sqrt(np.abs(np.sum((X.reshape(X_shape) - centre.reshape( - centre_shape)) ** 2 * data, i) / np.sum(data, i))) + sigma = np.sqrt( + abs( + np.sum((X.reshape(X_shape) - centre.reshape(centre_shape)) ** 2 * data, i) + / np.sum(data, i) + ) + ) height = data.max(i) if isinstance(data, da.Array): @@ -61,7 +68,6 @@ def _estimate_gaussian_parameters(signal, x1, x2, only_current): class Gaussian(Expression): - r"""Normalized Gaussian function component. .. math:: @@ -79,29 +85,33 @@ class Gaussian(Expression): Parameters - ----------- + ---------- A : float - Height scaled by :math:`\sigma\sqrt{(2\pi)}`. ``GaussianHF`` - implements the Gaussian function with a height parameter + Area, equals height scaled by :math:`\sigma\sqrt{(2\pi)}`. + ``GaussianHF`` implements the Gaussian function with a height parameter corresponding to the peak height. sigma : float Scale parameter of the Gaussian distribution. centre : float Location of the Gaussian maximum (peak position). **kwargs - Extra keyword arguments are passed to the ``Expression`` component. - + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. - For convenience the `fwhm` and `height` attributes can be used to get and set - the full width at half maximum and height of the distribution, respectively. + Attributes + ---------- + fwhm : float + Convenience attribute to get and set the full width at half maximum. + height : float + Convenience attribute to get and set the height. - See also + See Also -------- - hyperspy._components.gaussianhf.GaussianHF + GaussianHF """ - def __init__(self, A=1., sigma=1., centre=0., module="numexpr", **kwargs): + def __init__(self, A=1.0, sigma=1.0, centre=0.0, module=None, **kwargs): super().__init__( expression="A * (1 / (sigma * sqrt(2*pi))) * exp(-(x - centre)**2 \ / (2 * sigma**2))", @@ -112,13 +122,14 @@ def __init__(self, A=1., sigma=1., centre=0., module="numexpr", **kwargs): position="centre", module=module, autodoc=False, - **kwargs) + **kwargs, + ) # Boundaries - self.A.bmin = 0. + self.A.bmin = 0.0 self.A.bmax = None - self.sigma.bmin = 0. + self.sigma.bmin = 0.0 self.sigma.bmax = None self.isbackground = False @@ -129,7 +140,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -146,7 +157,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Notes ----- - Adapted from http://www.scipy.org/Cookbook/FittingData + Adapted from https://scipy-cookbook.readthedocs.io/items/FittingData.html Examples -------- @@ -159,36 +170,34 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): >>> s.axes_manager[-1].offset = -10 >>> s.axes_manager[-1].scale = 0.01 >>> g.estimate_parameters(s, -10, 10, False) + True """ super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] - centre, height, sigma = _estimate_gaussian_parameters(signal, x1, x2, - only_current) + centre, height, sigma = _estimate_gaussian_parameters( + signal, x1, x2, only_current + ) scaling_factor = _get_scaling_factor(signal, axis, centre) if only_current is True: self.centre.value = centre self.sigma.value = sigma self.A.value = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: self.A.value /= scaling_factor return True else: if self.A.map is None: self._create_arrays() - self.A.map['values'][:] = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.A.map['values'] /= scaling_factor - self.A.map['is_set'][:] = True - self.sigma.map['values'][:] = sigma - self.sigma.map['is_set'][:] = True - self.centre.map['values'][:] = centre - self.centre.map['is_set'][:] = True + self.A.map["values"][:] = height * sigma * sqrt2pi + if axis.is_binned: + self.A.map["values"] /= scaling_factor + self.A.map["is_set"][:] = True + self.sigma.map["values"][:] = sigma + self.sigma.map["is_set"][:] = True + self.centre.map["values"][:] = centre + self.centre.map["is_set"][:] = True self.fetch_stored_values() return True diff --git a/hyperspy/_components/gaussian2d.py b/hyperspy/_components/gaussian2d.py index 6e9c71375d..f7092441ce 100644 --- a/hyperspy/_components/gaussian2d.py +++ b/hyperspy/_components/gaussian2d.py @@ -1,25 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import math + import numpy as np -from hyperspy._components.expression import Expression +from hyperspy._components.expression import Expression sigma2fwhm = 2 * np.sqrt(2 * np.log(2)) @@ -45,7 +46,8 @@ class Gaussian2D(Expression): Parameters ---------- A : float - Amplitude (height of the peak scaled by :math:`2 \pi s_x s_y`). + Volume (height of the peak scaled by :math:`2 \pi s_x s_y`) -- + eqivalent to the area in a 1D Gaussian. sigma_x : float Width (scale parameter) of the Gaussian distribution in `x` direction. sigma_y : float @@ -57,6 +59,9 @@ class Gaussian2D(Expression): add_rotation : bool If True, add the parameter `rotation_angle` corresponding to the angle between the `x` and the horizontal axis. + **kwargs + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. Attributes ---------- @@ -65,8 +70,16 @@ class Gaussian2D(Expression): the two axes. """ - def __init__(self, A=1., sigma_x=1., sigma_y=1., centre_x=0., - centre_y=0, module="numexpr", **kwargs): + def __init__( + self, + A=1.0, + sigma_x=1.0, + sigma_y=1.0, + centre_x=0.0, + centre_y=0, + module=None, + **kwargs, + ): super().__init__( expression="A * (1 / (sigma_x * sigma_y * 2 * pi)) * \ exp(-((x - centre_x) ** 2 / (2 * sigma_x ** 2) \ @@ -80,13 +93,14 @@ def __init__(self, A=1., sigma_x=1., sigma_y=1., centre_x=0., position=("centre_x", "centre_y"), module=module, autodoc=False, - **kwargs) + **kwargs, + ) # Boundaries - self.A.bmin = 0. + self.A.bmin = 0.0 - self.sigma_x.bmin = 0. - self.sigma_y.bmin = 0. + self.sigma_x.bmin = 0.0 + self.sigma_y.bmin = 0.0 self.isbackground = False self.convolved = True @@ -101,7 +115,6 @@ def fwhm_x(self, value): @property def fwhm_y(self): - return self.sigma_y.value * sigma2fwhm @fwhm_y.setter @@ -139,8 +152,7 @@ def sigma_minor(self): @property def ellipticity(self): - """float: Ratio between the major and minor axis. - """ + """float: Ratio between the major and minor axis.""" return self.sigma_major / self.sigma_minor @property diff --git a/hyperspy/_components/gaussianhf.py b/hyperspy/_components/gaussianhf.py index c61d8b5aff..1d8151fe3c 100644 --- a/hyperspy/_components/gaussianhf.py +++ b/hyperspy/_components/gaussianhf.py @@ -1,37 +1,36 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import math from hyperspy._components.expression import Expression from hyperspy._components.gaussian import _estimate_gaussian_parameters from hyperspy.component import _get_scaling_factor -from hyperspy.misc.utils import is_binned # remove in v2.0 sqrt2pi = math.sqrt(2 * math.pi) sigma2fwhm = 2 * math.sqrt(2 * math.log(2)) class GaussianHF(Expression): - - r"""Normalized gaussian function component, with a `fwhm` parameter instead - of the sigma parameter, and a `height` parameter instead of the `A` - parameter (scaling difference of :math:`\sigma \sqrt{\left(2\pi\right)}`). + r"""Normalized gaussian function component, with a ``fwhm`` parameter + instead of the ``sigma`` parameter, and a ``height`` parameter instead of + the area parameter ``A`` (scaling difference of + :math:`\sigma \sqrt{\left(2\pi\right)}`). This makes the parameter vs. peak maximum independent of :math:`\sigma`, and thereby makes locking of the parameter more viable. As long as there is no binning, the `height` parameter corresponds directly to the peak @@ -64,20 +63,25 @@ class GaussianHF(Expression): centre: float Location of the gaussian maximum, also the mean position. **kwargs - Extra keyword arguments are passed to the ``Expression`` component. - - - The helper properties `sigma` and `A` are also defined for compatibility - with `Gaussian` component. + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. - See also + Attributes + ---------- + A : float + Convenience attribute to get, set the area and defined for + compatibility with `Gaussian` component. + sigma : float + Convenience attribute to get, set the width and defined for + compatibility with `Gaussian` component. + + See Also -------- - hyperspy._components.gaussian.Gaussian + Gaussian """ - def __init__(self, height=1., fwhm=1., centre=0., module="numexpr", - **kwargs): + def __init__(self, height=1.0, fwhm=1.0, centre=0.0, module=None, **kwargs): super().__init__( expression="height * exp(-(x - centre)**2 * 4 * log(2)/fwhm**2)", name="GaussianHF", @@ -91,10 +95,10 @@ def __init__(self, height=1., fwhm=1., centre=0., module="numexpr", ) # Boundaries - self.height.bmin = 0. + self.height.bmin = 0.0 self.height.bmax = None - self.fwhm.bmin = 0. + self.fwhm.bmin = 0.0 self.fwhm.bmax = None self.isbackground = False @@ -105,7 +109,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -121,7 +125,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Notes ----- - Adapted from http://www.scipy.org/Cookbook/FittingData + Adapted from https://scipy-cookbook.readthedocs.io/items/FittingData.html Examples -------- @@ -134,36 +138,34 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): >>> s.axes_manager[-1].offset = -10 >>> s.axes_manager[-1].scale = 0.01 >>> g.estimate_parameters(s, -10, 10, False) + True """ super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] - centre, height, sigma = _estimate_gaussian_parameters(signal, x1, x2, - only_current) + centre, height, sigma = _estimate_gaussian_parameters( + signal, x1, x2, only_current + ) scaling_factor = _get_scaling_factor(signal, axis, centre) if only_current is True: self.centre.value = centre self.fwhm.value = sigma * sigma2fwhm self.height.value = float(height) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: self.height.value /= scaling_factor return True else: if self.height.map is None: self._create_arrays() - self.height.map['values'][:] = height - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.height.map['values'][:] /= scaling_factor - self.height.map['is_set'][:] = True - self.fwhm.map['values'][:] = sigma * sigma2fwhm - self.fwhm.map['is_set'][:] = True - self.centre.map['values'][:] = centre - self.centre.map['is_set'][:] = True + self.height.map["values"][:] = height + if axis.is_binned: + self.height.map["values"][:] /= scaling_factor + self.height.map["is_set"][:] = True + self.fwhm.map["values"][:] = sigma * sigma2fwhm + self.fwhm.map["is_set"][:] = True + self.centre.map["values"][:] = centre + self.centre.map["is_set"][:] = True self.fetch_stored_values() return True @@ -187,5 +189,4 @@ def integral_as_signal(self): """ Utility function to get gaussian integral as Signal1D """ - return (self.height.as_signal() * self.fwhm.as_signal() * - sqrt2pi / sigma2fwhm) + return self.height.as_signal() * self.fwhm.as_signal() * sqrt2pi / sigma2fwhm diff --git a/hyperspy/_components/heaviside.py b/hyperspy/_components/heaviside.py index e8ab93d50f..8608a44ba5 100644 --- a/hyperspy/_components/heaviside.py +++ b/hyperspy/_components/heaviside.py @@ -1,27 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy._components.expression import Expression class HeavisideStep(Expression): - r"""The Heaviside step function. Based on the corresponding `numpy function @@ -47,27 +46,31 @@ class HeavisideStep(Expression): Parameters - ----------- + ---------- n : float Location parameter defining the x position of the step. A : float Height parameter for x>n. **kwargs - Extra keyword arguments are passed to the ``Expression`` component. + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. """ - def __init__(self, A=1., n=0., module="numpy", compute_gradients=True, - **kwargs): + def __init__(self, A=1.0, n=0.0, module="numpy", compute_gradients=False, **kwargs): super().__init__( - expression="A*heaviside(x-n,0.5)", + expression="A*Heaviside(x-n,0.5)", name="HeavisideStep", A=A, n=n, position="n", module=module, - autodoc=False, + # compute gradient with sympy is not supported + # NameError: name 'DiracDelta' is not defined + # See https://github.com/sympy/sympy/issues/26663#issuecomment-2160451385 compute_gradients=compute_gradients, - **kwargs) + autodoc=False, + **kwargs, + ) self.isbackground = True self.convolved = False diff --git a/hyperspy/_components/logistic.py b/hyperspy/_components/logistic.py index be14ad0ddd..2f664c1893 100644 --- a/hyperspy/_components/logistic.py +++ b/hyperspy/_components/logistic.py @@ -1,26 +1,25 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy._components.expression import Expression class Logistic(Expression): - r"""Logistic function (sigmoid or s-shaped curve) component. .. math:: @@ -39,24 +38,25 @@ class Logistic(Expression): Parameters - ----------- - a : Float + ---------- + a : float The curve's maximum y-value, :math:`\mathrm{lim}_{x\to\infty}\left(y\right) = a` - b : Float + b : float Additional parameter: b>1 shifts origin to larger values; 0. +# along with HyperSpy. If not, see . -import numpy as np import dask.array as da +import numpy as np -from hyperspy.component import _get_scaling_factor from hyperspy._components.expression import Expression -from hyperspy.misc.utils import is_binned # remove in v2.0 +from hyperspy.component import _get_scaling_factor def _estimate_lorentzian_parameters(signal, x1, x2, only_current): @@ -30,23 +29,25 @@ def _estimate_lorentzian_parameters(signal, x1, x2, only_current): X = axis.axis[i1:i2] if only_current is True: - data = signal()[i1:i2] + data = signal._get_current_data()[i1:i2] i = 0 centre_shape = (1,) else: i = axis.index_in_array - data_gi = [slice(None), ] * len(signal.data.shape) + data_gi = [ + slice(None), + ] * len(signal.data.shape) data_gi[axis.index_in_array] = slice(i1, i2) data = signal.data[tuple(data_gi)] centre_shape = list(data.shape) centre_shape[i] = 1 - cdf = np.cumsum(data,i) - cdfnorm = cdf/np.max(cdf, i).reshape(centre_shape) + cdf = np.cumsum(data, i) + cdfnorm = cdf / np.max(cdf, i).reshape(centre_shape) - icentre = np.argmin(np.abs(0.5 - cdfnorm), i) - igamma1 = np.argmin(np.abs(0.75 - cdfnorm), i) - igamma2 = np.argmin(np.abs(0.25 - cdfnorm), i) + icentre = np.argmin(abs(0.5 - cdfnorm), i) + igamma1 = np.argmin(abs(0.75 - cdfnorm), i) + igamma2 = np.argmin(abs(0.25 - cdfnorm), i) if isinstance(data, da.Array): icentre, igamma1, igamma2 = da.compute(icentre, igamma1, igamma2) @@ -59,7 +60,6 @@ def _estimate_lorentzian_parameters(signal, x1, x2, only_current): class Lorentzian(Expression): - r"""Cauchy-Lorentz distribution (a.k.a. Lorentzian function) component. .. math:: @@ -77,9 +77,9 @@ class Lorentzian(Expression): Parameters - ----------- + ---------- A : float - Height parameter, where :math:`A/(\gamma\pi)` is the maximum of the + Area parameter, where :math:`A/(\gamma\pi)` is the maximum (height) of peak. gamma : float Scale parameter corresponding to the half-width-at-half-maximum of the @@ -87,18 +87,19 @@ class Lorentzian(Expression): centre : float Location of the peak maximum. **kwargs - Extra keyword arguments are passed to the ``Expression`` component. + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. For convenience the `fwhm` and `height` attributes can be used to get and set the full-with-half-maximum and height of the distribution, respectively. """ - def __init__(self, A=1., gamma=1., centre=0., module="numexpr", **kwargs): + def __init__(self, A=1.0, gamma=1.0, centre=0.0, module=None, **kwargs): # We use `_gamma` internally to workaround the use of the `gamma` # function in sympy super().__init__( - expression="A / pi * (_gamma / ((x - centre)**2 + _gamma**2))", + expression="A / pi * (gamma_ / ((x - centre)**2 + gamma_**2))", name="Lorentzian", A=A, gamma=gamma, @@ -106,11 +107,12 @@ def __init__(self, A=1., gamma=1., centre=0., module="numexpr", **kwargs): position="centre", module=module, autodoc=False, - rename_pars={"_gamma": "gamma"}, - **kwargs) + rename_pars={"gamma_": "gamma"}, + **kwargs, + ) # Boundaries - self.A.bmin = 0. + self.A.bmin = 0.0 self.A.bmax = None self.gamma.bmin = None @@ -128,7 +130,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -159,36 +161,34 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): >>> s.axes_manager[-1].offset = -10 >>> s.axes_manager[-1].scale = 0.01 >>> g.estimate_parameters(s, -10, 10, False) + True """ super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] - centre, height, gamma = _estimate_lorentzian_parameters(signal, x1, x2, - only_current) + centre, height, gamma = _estimate_lorentzian_parameters( + signal, x1, x2, only_current + ) scaling_factor = _get_scaling_factor(signal, axis, centre) if only_current is True: self.centre.value = centre self.gamma.value = gamma self.A.value = height * gamma * np.pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: self.A.value /= scaling_factor return True else: if self.A.map is None: self._create_arrays() - self.A.map['values'][:] = height * gamma * np.pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.A.map['values'] /= scaling_factor - self.A.map['is_set'][:] = True - self.gamma.map['values'][:] = gamma - self.gamma.map['is_set'][:] = True - self.centre.map['values'][:] = centre - self.centre.map['is_set'][:] = True + self.A.map["values"][:] = height * gamma * np.pi + if axis.is_binned: + self.A.map["values"] /= scaling_factor + self.A.map["is_set"][:] = True + self.gamma.map["values"][:] = gamma + self.gamma.map["is_set"][:] = True + self.centre.map["values"][:] = centre + self.centre.map["is_set"][:] = True self.fetch_stored_values() return True diff --git a/hyperspy/_components/offset.py b/hyperspy/_components/offset.py index 138f732b9a..46dca73d56 100644 --- a/hyperspy/_components/offset.py +++ b/hyperspy/_components/offset.py @@ -1,31 +1,29 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np from hyperspy.component import Component from hyperspy.docstrings.parameters import FUNCTION_ND_DOCSTRING -from hyperspy.misc.utils import is_binned # remove in v2.0 class Offset(Component): - r"""Component to add a constant value in the y-axis. .. math:: @@ -39,13 +37,14 @@ class Offset(Component): ============ ============= Parameters - ----------- + ---------- offset : float + The offset to be fitted """ - def __init__(self, offset=0.): - Component.__init__(self, ('offset',)) + def __init__(self, offset=0.0): + Component.__init__(self, ("offset",), ["offset"]) self.offset.free = True self.offset.value = offset @@ -70,7 +69,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : BaseSignal instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -89,47 +88,52 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] i1, i2 = axis.value_range_to_indices(x1, x2) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: # using the mean of the gradient for non-uniform axes is a best # guess to the scaling of binned signals for the estimation - scaling_factor = axis.scale if axis.is_uniform \ - else np.mean(np.gradient(axis.axis), axis=-1) + scaling_factor = ( + axis.scale + if axis.is_uniform + else np.mean(np.gradient(axis.axis), axis=-1) + ) if only_current is True: - self.offset.value = signal()[i1:i2].mean() - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + self.offset.value = signal._get_current_data()[i1:i2].mean() + if axis.is_binned: self.offset.value /= scaling_factor return True else: if self.offset.map is None: self._create_arrays() dc = signal.data - gi = [slice(None), ] * len(dc.shape) + gi = [ + slice(None), + ] * len(dc.shape) gi[axis.index_in_array] = slice(i1, i2) - self.offset.map['values'][:] = dc[tuple( - gi)].mean(axis.index_in_array) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.offset.map['values'] /= scaling_factor - self.offset.map['is_set'][:] = True + self.offset.map["values"][:] = dc[tuple(gi)].mean(axis.index_in_array) + if axis.is_binned: + self.offset.map["values"] /= scaling_factor + self.offset.map["is_set"][:] = True self.fetch_stored_values() return True def function_nd(self, axis): - """%s - - """ + """%s""" if self._is_navigation_multidimensional: x = axis[np.newaxis, :] - o = self.offset.map['values'][..., np.newaxis] + o = self.offset.map["values"][..., np.newaxis] else: x = axis o = self.offset.value return self._function(x, o) function_nd.__doc__ %= FUNCTION_ND_DOCSTRING + + @property + def _constant_term(self): + "Get value of constant term of component" + # First get currently constant parameters + if self.offset.free: + return 0 + else: + return self.offset.value diff --git a/hyperspy/_components/pes_core_line_shape.py b/hyperspy/_components/pes_core_line_shape.py deleted file mode 100644 index 5855b0f195..0000000000 --- a/hyperspy/_components/pes_core_line_shape.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import math - -from hyperspy.component import Component -from hyperspy.docstrings.parameters import FUNCTION_ND_DOCSTRING - -sqrt2pi = np.sqrt(2 * np.pi) - - -def _calculate_shirley_background(values): - cf = np.cumsum(values, axis=-1) - # necessary to work with `function_nd` - return cf[..., -1][np.newaxis].T - cf - - -class PESCoreLineShape(Component): - - """ - """ - - def __init__(self, A=1., FWHM=1., origin=0., ab=0.0, shirley=0.0): - Component.__init__(self, ['A', 'FWHM', 'origin', 'ab', 'shirley']) - self.ab.value = 0 - self.ab.free = False - self.A.value = A - self.FWHM.value = FWHM - self.origin.value = origin - self._position = self.origin - - # Boundaries - self.A.bmin = 0. - self.A.bmax = None - self.FWHM.bmin = None - self.FWHM.bmax = None - - self.isbackground = False - self.convolved = True - - # Gradients - self.A.grad = self.grad_A - self.FWHM.grad = self.grad_FWHM - self.origin.grad = self.grad_origin - self.ab.grad = self.grad_ab - - # Options - self.Shirley = False - self._whitelist['Shirley'] = None - - @property - def Shirley(self): - return self._Shirley - - @Shirley.setter - def Shirley(self, value): - self._Shirley = value - self.shirley.free = value - - def _function(self, x, A, origin, FWHM, ab, shirley): - """ - Given an one dimensional array x containing the energies at which - you want to evaluate the background model, returns the background - model for the current parameters. - """ - f = A * np.exp(-1 * math.log(2) * ((x - (origin - ab)) / FWHM) ** 2) - if self.Shirley: - return _calculate_shirley_background(f) * shirley + f - else: - return f - - def function(self, x): - return self._function(x, self.A.value, - self.origin.value, - self.FWHM.value, - self.ab.value, - self.shirley.value) - - def function_nd(self, axis): - """%s - - """ - if self._is_navigation_multidimensional: - x = axis[np.newaxis, :] - A = self.A.map['values'][..., np.newaxis] - origin = self.origin.map['values'][..., np.newaxis] - FWHM = self.FWHM.map['values'][..., np.newaxis] - ab = self.ab.map['values'][..., np.newaxis] - shirley = self.shirley.map['values'][..., np.newaxis] - return self._function(x, A, origin, FWHM, ab, shirley) - else: - return self.function(axis) - - function_nd.__doc__ %= FUNCTION_ND_DOCSTRING - - def grad_A(self, x): - return self.function(x) / self.A.value - - def grad_FWHM(self, x): - a0 = self.A.value - a1 = self.origin.value - a2 = self.FWHM.value - a3 = self.ab.value - return (2 * math.log(2) * a0 * (x + a3 - a1) ** 2 * - np.exp(-(math.log(2) * (x + a3 - a1) ** 2) / a2 ** 2)) / a2 ** 3 - - def grad_origin(self, x): - a0 = self.A.value - a1 = self.origin.value - a2 = self.FWHM.value - a3 = self.ab.value - return (2 * math.log(2) * a0 * (x + a3 - a1) * - np.exp(-(math.log(2) * (x + a3 - a1) ** 2) / a2 ** 2)) / a2 ** 2 - - def grad_ab(self, x): - return -self.grad_origin(x) - - def grad_shirley(self, x): - a0 = self.A.value - a1 = self.origin.value - a2 = self.FWHM.value - a3 = self.ab.value - f = a0 * np.exp(-1 * math.log(2) * ((x - (a1 - a3)) / a2) ** 2) - return _calculate_shirley_background(f) diff --git a/hyperspy/_components/pes_see.py b/hyperspy/_components/pes_see.py deleted file mode 100644 index 6563ede21c..0000000000 --- a/hyperspy/_components/pes_see.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - - -import numpy as np -import logging - -from hyperspy._components.expression import Expression - -_logger = logging.getLogger(__name__) - - -class SEE(Expression): - - r"""Secondary electron emission component for Photoemission Spectroscopy. - - .. math:: - :nowrap: - - \[ - f(x) = - \begin{cases} - 0, & x \leq \Phi\\ - A\cdot{ (x-\Phi) / (x-\Phi+B)^{4}}, & x > \Phi - \end{cases} - \] - - ============= ============= - Variable Parameter - ============= ============= - :math:`A` A - :math:`\Phi` Phi - :math:`B` B - ============= ============= - - Parameters - ---------- - A : float - Height parameter - Phi : float - Position parameter - B : float - Tail or asymmetry parameter - **kwargs - Extra keyword arguments are passed to the ``Expression`` component. - - """ - - def __init__(self, A=1., Phi=1., B=0., module="numexpr", - compute_gradients=False, **kwargs): - if kwargs.pop('sigma', False): - _logger.warning('The `sigma` parameter was broken and it has been ' - 'removed.') - - super().__init__( - expression="where(x > Phi, A * (x - Phi) / (x - Phi + B) ** 4, 0)", - name="SEE", - A=A, - Phi=Phi, - B=B, - position="Phi", - module=module, - autodoc=False, - compute_gradients=compute_gradients, - **kwargs, - ) - - # Boundaries - self.A.bmin = 0. - self.A.bmax = None - - self.convolved = True - - def grad_A(self, x): - """ - """ - return np.where(x > self.Phi.value, (x - self.Phi.value) / - (x - self.Phi.value + self.B.value) ** 4, 0) - - def grad_Phi(self, x): - """ - """ - return np.where( - x > self.Phi.value, - (4 * (x - self.Phi.value) * self.A.value) / - (self.B.value + x - self.Phi.value) ** 5 - - self.A.value / (self.B.value + x - self.Phi.value) ** 4, 0) - - def grad_B(self, x): - return np.where( - x > self.Phi.value, - -(4 * (x - self.Phi.value) * self.A.value) / - (self.B.value + x - self.Phi.value) ** 5, 0) diff --git a/hyperspy/_components/pes_voigt.py b/hyperspy/_components/pes_voigt.py deleted file mode 100644 index d8c0cb55fa..0000000000 --- a/hyperspy/_components/pes_voigt.py +++ /dev/null @@ -1,324 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import math - -from hyperspy.component import Component, _get_scaling_factor -from hyperspy._components.gaussian import _estimate_gaussian_parameters -from hyperspy.misc.utils import is_binned # remove in v2.0 - -sqrt2pi = math.sqrt(2 * math.pi) -sigma2fwhm = 2 * math.sqrt(2 * math.log(2)) - - -def voigt(x, FWHM=1, gamma=1, center=0, scale=1): - r"""Voigt lineshape. - - The voigt peak is the convolution of a Lorentz peak with a Gaussian peak: - - .. math:: - f(x) = G(x) \cdot L(x) - - where :math:`G(x)` is the Gaussian function and :math:`L(x)` is the - Lorentzian function. In this case using an approximate formula by David - (see Notes). This approximation improves on the pseudo-Voigt function - (linear combination instead of convolution of the distributions) and is, - to a very good approximation, equivalent to a Voigt function: - - .. math:: - z(x) &= \frac{x + i \gamma}{\sqrt{2} \sigma} \\ - w(z) &= \frac{e^{-z^2} \text{erfc}(-i z)}{\sqrt{2 \pi} \sigma} \\ - f(x) &= A \cdot \Re\left\{ w \left[ z(x - x_0) \right] \right\} - - - ============== ============= - Variable Parameter - ============== ============= - :math:`x_0` center - :math:`A` scale - :math:`\gamma` gamma - :math:`\sigma` sigma - ============== ============= - - - Parameters - ---------- - gamma : real - The half-width half-maximum of the Lorentzian. - FWHM : real - The FWHM = :math:`2 \sigma \sqrt{(2 \log(2))}` of the Gaussian. - center : real - Location of the center of the peak. - scale : real - Value at the highest point of the peak. - - Notes - ----- - Ref: W.I.F. David, J. Appl. Cryst. (1986). 19, 63-64 - doi:10.1107/S0021889886089999 - """ - # wofz function = w(z) = Fad[d][e][y]eva function = exp(-z**2)erfc(-iz) - from scipy.special import wofz - sigma = FWHM / 2.3548200450309493 - z = (np.asarray(x) - center + 1j * gamma) / (sigma * math.sqrt(2)) - V = wofz(z) / (math.sqrt(2 * np.pi) * sigma) - return scale * V.real - - -class Voigt(Component): - # Legacy class to be removed in v2.0 - - r"""This is the legacy Voigt profile component dedicated to photoemission - spectroscopy data analysis that will renamed to `PESVoigt` in v2.0. To use - the new Voigt lineshape component set `legacy=False`. See the - documentation of :meth:`hyperspy._components.voigt.Voigt` for details on - the usage of the new Voigt component and - :meth:`hyperspy._components.pes_voigt.PESVoigt` for the legacy component. - - .. math:: - f(x) = G(x) \cdot L(x) - - where :math:`G(x)` is the Gaussian function and :math:`L(x)` is the - Lorentzian function. This component uses an approximate formula by David - (see Notes). - - - Notes - ----- - Uses an approximate formula according to - W.I.F. David, J. Appl. Cryst. (1986). 19, 63-64. - doi:10.1107/S0021889886089999 - """ - - def __init__(self, legacy=True, **kwargs): - self.legacy = legacy - if legacy: - from hyperspy.misc.utils import deprecation_warning - msg = ( - "The API of the `Voigt` component will change in v2.0. " - "This component will become `PESVoigt`. " - "To use the new API set `legacy=False`.") - deprecation_warning(msg) - - self.__class__ = PESVoigt - self.__init__(**kwargs) - else: - from hyperspy._components.voigt import Voigt - self.__class__ = Voigt - self.__init__(**kwargs) - - @property - def gwidth(self): - if not self.legacy: - return super().sigma.value * sigma2fwhm - - @gwidth.setter - def gwidth(self, value): - if not self.legacy: - super(Voigt, self.__class__).sigma.value.fset(self, value - / sigma2fwhm) - - @property - def FWHM(self): - if not self.legacy: - return super().sigma.value * sigma2fwhm - - @FWHM.setter - def FWHM(self, value): - if not self.legacy: - super(Voigt, self.__class__).sigma.value.fset(self, value - / sigma2fwhm) - - @property - def lwidth(self): - if not self.legacy: - return super().gamma.value * 2 - - @lwidth.setter - def lwidth(self, value): - if not self.legacy: - super(Voigt, self.__class__).gamma.value.fset(self, value / 2) - - -class PESVoigt(Component): - - r"""Voigt component for photoemission spectroscopy data analysis. - - Voigt profile component with support for shirley background, - non_isochromaticity, transmission_function corrections and spin orbit - splitting specially suited for photoemission spectroscopy data analysis. - - .. math:: - f(x) = G(x) \cdot L(x) - - where :math:`G(x)` is the Gaussian function and :math:`L(x)` is the - Lorentzian function. This component uses an approximate formula by David - (see Notes). - - - Parameters - ---------- - - area : Parameter - Intensity below the peak. - centre: Parameter - Location of the maximum of the peak. - FWHM : Parameter - FWHM = :math:`2 \sigma \sqrt{(2 \log(2))}` of the Gaussian distribution. - gamma : Parameter - :math:`\gamma` of the Lorentzian distribution. - resolution : Parameter - shirley_background : Parameter - non_isochromaticity : Parameter - transmission_function : Parameter - spin_orbit_splitting : Bool - spin_orbit_branching_ratio : float - spin_orbit_splitting_energy : float - - Notes - ----- - Uses an approximate formula according to - W.I.F. David, J. Appl. Cryst. (1986). 19, 63-64. - doi:10.1107/S0021889886089999 - """ - - def __init__(self): - Component.__init__(self, ( - 'area', - 'centre', - 'FWHM', - 'gamma', - 'resolution', - 'shirley_background', - 'non_isochromaticity', - 'transmission_function')) - self._position = self.centre - self.FWHM.value = 1 - self.gamma.value = 0 - self.area.value = 1 - self.resolution.value = 0 - self.resolution.free = False - self.shirley_background.free = False - self.non_isochromaticity.value = 0 - self.non_isochromaticity.free = False - self.transmission_function.value = 1 - self.transmission_function.free = False - # Options - self.shirley_background.active = False - self.spin_orbit_splitting = False - self.spin_orbit_branching_ratio = 0.5 - self.spin_orbit_splitting_energy = 0.61 - self.isbackground = False - self.convolved = True - - def function(self, x): - area = self.area.value * self.transmission_function.value - centre = self.centre.value - ab = self.non_isochromaticity.value - if self.resolution.value == 0: - FWHM = self.FWHM.value - else: - FWHM = math.sqrt(self.FWHM.value ** 2 + self.resolution.value ** 2) - gamma = self.gamma.value - k = self.shirley_background.value - f = voigt(x, - FWHM=FWHM, gamma=gamma, center=centre - ab, scale=area) - if self.spin_orbit_splitting is True: - ratio = self.spin_orbit_branching_ratio - shift = self.spin_orbit_splitting_energy - f2 = voigt(x, FWHM=FWHM, gamma=gamma, - center=centre - ab - shift, scale=area * ratio) - f += f2 - if self.shirley_background.active: - cf = np.cumsum(f) - cf = cf[-1] - cf - self.cf = cf - return cf * k + f - else: - return f - - def estimate_parameters(self, signal, E1, E2, only_current=False): - """Estimate the Voigt function by calculating the momenta of the - Gaussian. - - Parameters - ---------- - signal : Signal1D instance - x1 : float - Defines the left limit of the spectral range to use for the - estimation. - x2 : float - Defines the right limit of the spectral range to use for the - estimation. - - only_current : bool - If False estimates the parameters for the full dataset. - - Returns - ------- - : bool - Exit status required for the :meth:`remove_background` function. - - Notes - ----- - Adapted from http://www.scipy.org/Cookbook/FittingData - - Examples - -------- - - >>> g = hs.model.components1D.PESVoigt() - >>> x = np.arange(-10, 10, 0.01) - >>> data = np.zeros((32, 32, 2000)) - >>> data[:] = g.function(x).reshape((1, 1, 2000)) - >>> s = hs.signals.Signal1D(data) - >>> s.axes_manager[-1].offset = -10 - >>> s.axes_manager[-1].scale = 0.01 - >>> g.estimate_parameters(s, -10, 10, False) - - """ - super()._estimate_parameters(signal) - axis = signal.axes_manager.signal_axes[0] - centre, height, sigma = _estimate_gaussian_parameters(signal, E1, E2, - only_current) - scaling_factor = _get_scaling_factor(signal, axis, centre) - - if only_current is True: - self.centre.value = centre - self.FWHM.value = sigma * sigma2fwhm - self.area.value = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.area.value /= scaling_factor - return True - else: - if self.area.map is None: - self._create_arrays() - self.area.map['values'][:] = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.area.map['values'][:] /= scaling_factor - self.area.map['is_set'][:] = True - self.FWHM.map['values'][:] = sigma * sigma2fwhm - self.FWHM.map['is_set'][:] = True - self.centre.map['values'][:] = centre - self.centre.map['is_set'][:] = True - self.fetch_stored_values() - return True diff --git a/hyperspy/_components/polynomial.py b/hyperspy/_components/polynomial.py index 20d1d3aaa8..6884c681c6 100644 --- a/hyperspy/_components/polynomial.py +++ b/hyperspy/_components/polynomial.py @@ -1,33 +1,32 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import numpy as np import logging +import numpy as np + from hyperspy._components.expression import Expression from hyperspy.misc.utils import ordinal -from hyperspy.misc.utils import is_binned # remove in v2.0 _logger = logging.getLogger(__name__) class Polynomial(Expression): - """n-order polynomial component. Polynomial component consisting of order + 1 parameters. @@ -46,24 +45,26 @@ class Polynomial(Expression): Order of the polynomial, must be different from 0. **kwargs Keyword arguments can be used to initialise the value of the - parameters, i.e. a2=2, a1=3, a0=1. + parameters, i.e. a2=2, a1=3, a0=1. Extra keyword arguments are passed + to the :class:`~.api.model.components1D.Expression` component. """ - def __init__(self, order=2, module="numexpr", **kwargs): - # Not to break scripts once we remove the legacy Polynomial - if "legacy" in kwargs: - del kwargs["legacy"] + def __init__(self, order=2, module=None, **kwargs): if order == 0: raise ValueError("Polynomial of order 0 is not supported.") - coeff_list = ['{}'.format(o).zfill(len(list(str(order)))) for o in - range(order, -1, -1)] - expr = "+".join(["a{}*x**{}".format(c, o) for c, o in - zip(coeff_list, range(order, -1, -1))]) + coeff_list = [ + "{}".format(o).zfill(len(list(str(order)))) for o in range(order, -1, -1) + ] + expr = "+".join( + ["a{}*x**{}".format(c, o) for c, o in zip(coeff_list, range(order, -1, -1))] + ) name = "{} order Polynomial".format(ordinal(order)) - super().__init__(expression=expr, name=name, module=module, - autodoc=False, **kwargs) - self._id_name = "eab91275-88db-4855-917a-cdcbe7209592" + super().__init__( + expression=expr, name=name, module=module, autodoc=False, **kwargs + ) + # Need to save order to be able to reload component after being saved + self._whitelist["order"] = ("init", order) def get_polynomial_order(self): return len(self.parameters) - 1 @@ -73,7 +74,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -93,21 +94,22 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): axis = signal.axes_manager.signal_axes[0] i1, i2 = axis.value_range_to_indices(x1, x2) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: # using the mean of the gradient for non-uniform axes is a best # guess to the scaling of binned signals for the estimation - scaling_factor = axis.scale if axis.is_uniform \ - else np.mean(np.gradient(axis.axis), axis=-1) + scaling_factor = ( + axis.scale + if axis.is_uniform + else np.mean(np.gradient(axis.axis), axis=-1) + ) if only_current is True: - estimation = np.polyfit(axis.axis[i1:i2], - signal()[i1:i2], - self.get_polynomial_order()) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + estimation = np.polyfit( + axis.axis[i1:i2], + signal._get_current_data()[i1:i2], + self.get_polynomial_order(), + ) + if axis.is_binned: for para, estim in zip(self.parameters[::-1], estimation): para.value = estim / scaling_factor else: @@ -123,25 +125,24 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): data = signal.data # For polyfit the spectrum goes in the first axis if axis.index_in_array > 0: - data = data.T # Unfolded, so simply transpose - fit = np.polyfit(axis.axis[i1:i2], data[i1:i2, ...], - self.get_polynomial_order()) + data = data.T # Unfolded, so simply transpose + fit = np.polyfit( + axis.axis[i1:i2], data[i1:i2, ...], self.get_polynomial_order() + ) if axis.index_in_array > 0: - fit = fit.T # Transpose back if needed + fit = fit.T # Transpose back if needed # Shape needed to fit parameter.map: - cmap_shape = nav_shape + (self.get_polynomial_order() + 1, ) + cmap_shape = nav_shape + (self.get_polynomial_order() + 1,) fit = fit.reshape(cmap_shape) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: for i, para in enumerate(self.parameters[::-1]): - para.map['values'][:] = fit[..., i] / scaling_factor - para.map['is_set'][:] = True + para.map["values"][:] = fit[..., i] / scaling_factor + para.map["is_set"][:] = True else: for i, para in enumerate(self.parameters[::-1]): - para.map['values'][:] = fit[..., i] - para.map['is_set'][:] = True + para.map["values"][:] = fit[..., i] + para.map["is_set"][:] = True self.fetch_stored_values() return True @@ -150,19 +151,19 @@ def convert_to_polynomial(poly_dict): """ Convert the dictionary from the old to the new polynomial definition """ - _logger.info("Converting the polynomial to the new definition") - poly_order = poly_dict['order'] - coeff_list = ['{}'.format(o).zfill(len(list(str(poly_dict['order'])))) - for o in range(poly_dict['order'], -1, -1)] + _logger.info("Converting the polynomial to the new definition.") + coeff_list = [ + "{}".format(o).zfill(len(list(str(poly_dict["order"])))) + for o in range(poly_dict["order"], -1, -1) + ] poly2_dict = dict(poly_dict) - coefficient_dict = poly_dict['parameters'][0] - poly2_dict['parameters'] = [] - poly2_dict['_id_name'] = "eab91275-88db-4855-917a-cdcbe7209592" + coefficient_dict = poly_dict["parameters"][0] + poly2_dict["parameters"] = [] for i, coeff in enumerate(coeff_list): param_dict = dict(coefficient_dict) - param_dict['_id_name'] = 'a{}'.format(coeff) - for v in ['value', '_bounds']: + param_dict["_id_name"] = "a{}".format(coeff) + for v in ["value", "_bounds"]: param_dict[v] = coefficient_dict[v][i] - poly2_dict['parameters'].append(param_dict) + poly2_dict["parameters"].append(param_dict) return poly2_dict diff --git a/hyperspy/_components/polynomial_deprecated.py b/hyperspy/_components/polynomial_deprecated.py deleted file mode 100644 index 362f1a5243..0000000000 --- a/hyperspy/_components/polynomial_deprecated.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import logging - - -from hyperspy.component import Component -from hyperspy.misc.utils import ordinal -from hyperspy.docstrings.parameters import FUNCTION_ND_DOCSTRING -from hyperspy.misc.utils import is_binned # remove in v2.0 - -_logger = logging.getLogger(__name__) - - -class Polynomial(Component): - - """n-order polynomial component. (DEPRECATED) - Polynomial component defined by the coefficients parameters which is an - array of len the order of the polynomial. - For example, the [1,2,3] coefficients define the following 3rd order - polynomial: f(x) = 1x² + 2x + 3 `Polynomial` will be replaced by - `Polynomial2` - - This API is deprecated and will be replaced by - :py:class:`hyperspy._components.polynomial.Polynomial` in HyperSpy v2.0. - To use the new API, set `legacy` to `False`. - - Attributes - ---------- - coeffcients : array - """ - - def __init__(self, order=2, legacy=True, module="numexpr", **kwargs): - """Polynomial component (DEPRECATED) - - This API is deprecated and will be replaced by - :py:class:`hyperspy._components.polynomial.Polynomial` in HyperSpy v2.0. - To use the new API, set `legacy` to `False`. - - Parameters - ---------- - order: int - Order of the polynomial. - legacy: bool, default True - If `False`, use the new API. - module: str - See the docstring - of :py:class:`hyperspy._components.polynomial.Polynomial` - for details. - """ - if legacy: - from hyperspy.misc.utils import deprecation_warning - msg = ( - "The API of the `Polynomial` component will change in v2.0." - "To use the new API set `legacy=False`.") - deprecation_warning(msg) - - Component.__init__(self, ['coefficients', ]) - self._whitelist['order'] = ('init', order) - self.coefficients._number_of_elements = order + 1 - self.coefficients.value = np.zeros((order + 1,)) - self.coefficients.grad = self.grad_coefficients - else: - from hyperspy._components.polynomial import Polynomial - self.__class__ = Polynomial - self.__init__(order=order, module=module, **kwargs) - - def get_polynomial_order(self): - return len(self.coefficients.value) - 1 - - def function(self, x): - return self._function(x, self.coefficients.value) - - def _function(self, x, coefficients): - return np.polyval(coefficients, x) - - def grad_one_coefficient(self, x, index): - """Returns the gradient of one coefficient""" - values = np.array(self.coefficients.value) - values[index] = 1 - return np.polyval(values, x) - - def grad_coefficients(self, x): - return np.vstack([self.grad_one_coefficient(x, i) for i in - range(self.coefficients._number_of_elements)]) - - def __repr__(self): - text = "%s order Polynomial component" % ordinal( - self.get_polynomial_order()) - if self.name: - text = "%s (%s)" % (self.name, text) - return "<%s>" % text - - def estimate_parameters(self, signal, x1, x2, only_current=False): - """Estimate the parameters by the two area method - - Parameters - ---------- - signal : Signal1D instance - x1 : float - Defines the left limit of the spectral range to use for the - estimation. - x2 : float - Defines the right limit of the spectral range to use for the - estimation. - only_current : bool - If False estimates the parameters for the full dataset. - - Returns - ------- - bool - """ - super()._estimate_parameters(signal) - axis = signal.axes_manager.signal_axes[0] - i1, i2 = axis.value_range_to_indices(x1, x2) - - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - # using the mean of the gradient for non-uniform axes is a best - # guess to the scaling of binned signals for the estimation - scaling_factor = axis.scale if axis.is_uniform \ - else np.mean(np.gradient(axis.axis), axis=-1) - - if only_current is True: - estimation = np.polyfit(axis.axis[i1:i2], - signal()[i1:i2], - self.get_polynomial_order()) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.coefficients.value = estimation / scaling_factor - else: - self.coefficients.value = estimation - return True - else: - if self.coefficients.map is None: - self._create_arrays() - nav_shape = signal.axes_manager._navigation_shape_in_array - with signal.unfolded(): - dc = signal.data - # For polyfit the spectrum goes in the first axis - if axis.index_in_array > 0: - dc = dc.T # Unfolded, so simply transpose - cmaps = np.polyfit(axis.axis[i1:i2], dc[i1:i2, ...], - self.get_polynomial_order()) - if axis.index_in_array > 0: - cmaps = cmaps.T # Transpose back if needed - # Shape needed to fit coefficients.map: - cmap_shape = nav_shape + (self.get_polynomial_order() + 1, ) - self.coefficients.map['values'][:] = cmaps.reshape(cmap_shape) - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.coefficients.map["values"] /= scaling_factor - self.coefficients.map['is_set'][:] = True - self.fetch_stored_values() - return True - - def function_nd(self, axis): - """%s - """ - x = axis[np.newaxis, :] - coefficients = self.coefficients.map["values"][..., np.newaxis] - return self._function(x, coefficients) - - function_nd.__doc__ %= FUNCTION_ND_DOCSTRING diff --git a/hyperspy/_components/power_law.py b/hyperspy/_components/power_law.py index e232d09d9b..a309e91cbe 100644 --- a/hyperspy/_components/power_law.py +++ b/hyperspy/_components/power_law.py @@ -1,32 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import numpy as np import logging -from hyperspy._components.expression import Expression +import numpy as np +from hyperspy._components.expression import Expression _logger = logging.getLogger(__name__) class PowerLaw(Expression): - r"""Power law component. .. math:: @@ -51,15 +50,25 @@ class PowerLaw(Expression): origin : float Location parameter. **kwargs - Extra keyword arguments are passed to the ``Expression`` component. - + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. - The `left_cutoff` parameter can be used to set a lower threshold from which - the component will return 0. + Attributes + ---------- + left_cutoff : float + For x <= left_cutoff, the function returns 0. Default value is 0.0. """ - def __init__(self, A=10e5, r=3., origin=0., left_cutoff=0.0, - module="numexpr", compute_gradients=False, **kwargs): + def __init__( + self, + A=10e5, + r=3.0, + origin=0.0, + left_cutoff=0.0, + module=None, + compute_gradients=False, + **kwargs, + ): super().__init__( expression="where(left_cutoff self.left_cutoff.value, -self.A.value * - np.log(x - self.origin.value) * - (x - self.origin.value) ** (-self.r.value), 0) + return np.where( + x > self.left_cutoff.value, + -self.A.value + * np.log(x - self.origin.value) + * (x - self.origin.value) ** (-self.r.value), + 0, + ) def grad_origin(self, x): - return np.where(x > self.left_cutoff.value, self.r.value * - (x - self.origin.value) ** (-self.r.value - 1) * - self.A.value, 0) + return np.where( + x > self.left_cutoff.value, + self.r.value + * (x - self.origin.value) ** (-self.r.value - 1) + * self.A.value, + 0, + ) diff --git a/hyperspy/_components/rc.py b/hyperspy/_components/rc.py index 9a9c6244d2..58ef47fdee 100644 --- a/hyperspy/_components/rc.py +++ b/hyperspy/_components/rc.py @@ -1,26 +1,25 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy._components.expression import Expression class RC(Expression): - r""" RC function component (based on the time-domain capacitor voltage response of an RC-circuit) @@ -40,7 +39,7 @@ class RC(Expression): Parameters - ----------- + ---------- Vmax : float maximum voltage, asymptote of the function for :math:`\mathrm{lim}_{x\to\infty}` @@ -49,11 +48,12 @@ class RC(Expression): tau : float tau=RC is the RC circuit time constant (voltage rise time) **kwargs - Extra keyword arguments are passed to the ``Expression`` component. + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. """ - def __init__(self, Vmax=1., V0=0., tau=1., module="numexpr", **kwargs): + def __init__(self, Vmax=1.0, V0=0.0, tau=1.0, module=None, **kwargs): super().__init__( expression="V0 + Vmax * (1 - exp(-x / tau))", name="RC", diff --git a/hyperspy/_components/scalable_fixed_pattern.py b/hyperspy/_components/scalable_fixed_pattern.py index caf0ba5536..491b2279de 100644 --- a/hyperspy/_components/scalable_fixed_pattern.py +++ b/hyperspy/_components/scalable_fixed_pattern.py @@ -1,33 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np -from scipy.interpolate import interp1d +from scipy.interpolate import make_interp_spline from hyperspy.component import Component -from hyperspy.ui_registry import add_gui_method from hyperspy.docstrings.parameters import FUNCTION_ND_DOCSTRING -from hyperspy.misc.utils import is_binned # remove in v2.0 +from hyperspy.ui_registry import add_gui_method @add_gui_method(toolkey="hyperspy.ScalableFixedPattern_Component") class ScalableFixedPattern(Component): - r"""Fixed pattern component with interpolation support. .. math:: @@ -42,40 +40,51 @@ class ScalableFixedPattern(Component): :math:`x_0` shift ============ ============= - - The fixed pattern is defined by a single spectrum which must be provided to - the ScalableFixedPattern constructor, e.g.: - - .. code-block:: ipython - - In [1]: s = load('my_spectrum.hspy') - In [2]: my_fixed_pattern = components.ScalableFixedPattern(s)) - Parameters ---------- + yscale : float + The scaling factor in y (intensity axis). + xscale : float + The scaling factor in x. + shift : float + The shift of the component + interpolate : bool + If False no interpolation is performed and only a y-scaled spectrum is + returned. - yscale : Float - xscale : Float - shift : Float - interpolate : Bool + Attributes + ---------- + yscale : :class:`~.component.Parameter` + The scaling factor in y (intensity axis). + xscale : :class:`~.component.Parameter` + The scaling factor in x. + shift : :class:`~.component.Parameter` + The shift of the component + interpolate : bool If False no interpolation is performed and only a y-scaled spectrum is returned. Methods ------- + prepare_interpolator - prepare_interpolator : method to fine tune the interpolation + Examples + -------- - """ + The fixed pattern is defined by a Signal1D of navigation 0 which must be + provided to the ScalableFixedPattern constructor, e.g.: - def __init__(self, signal1D, yscale=1.0, xscale=1.0, - shift=0.0, interpolate=True): + >>> s = hs.load('data.hspy') # doctest: +SKIP + >>> my_fixed_pattern = hs.model.components1D.ScalableFixedPattern(s) # doctest: +SKIP - Component.__init__(self, ['yscale', 'xscale', 'shift']) + """ + + def __init__(self, signal1D, yscale=1.0, xscale=1.0, shift=0.0, interpolate=True): + Component.__init__(self, ["yscale", "xscale", "shift"], ["yscale"]) self._position = self.shift - self._whitelist['signal1D'] = ('init,sig', signal1D) - self._whitelist['interpolate'] = None + self._whitelist["signal1D"] = ("init,sig", signal1D) + self._whitelist["interpolate"] = None self.signal = signal1D self.yscale.free = True self.yscale.value = yscale @@ -98,66 +107,48 @@ def interpolate(self, value): self.xscale.free = value self.shift.free = value - def prepare_interpolator(self, kind='linear', fill_value=0, **kwargs): - """Prepare interpolation. + def prepare_interpolator(self, **kwargs): + """Fine-tune the interpolation. Parameters ---------- x : array The spectral axis of the fixed pattern - kind : str or int, optional - Specifies the kind of interpolation as a string - ('linear', 'nearest', 'zero', 'slinear', 'quadratic, 'cubic') - or as an integer specifying the order of the spline interpolator - to use. Default is 'linear'. - - fill_value : float, optional - If provided, then this value will be used to fill in for requested - points outside of the data range. If not provided, then the default - is NaN. - - Notes - ----- - Any extra keyword argument is passed to `scipy.interpolate.interp1d` - + **kwargs : dict + Keywords argument are passed to + :func:`scipy.interpolate.make_interp_spline` """ - self.f = interp1d( + self.f = make_interp_spline( self.signal.axes_manager.signal_axes[0].axis, self.signal.data.squeeze(), - kind=kind, - bounds_error=False, - fill_value=fill_value, - **kwargs) + **kwargs, + ) def _function(self, x, xscale, yscale, shift): if self.interpolate is True: result = yscale * self.f(x * xscale - shift) else: result = yscale * self.signal.data - if is_binned(self.signal): - # in v2 replace by - #if self.signal.axes_manager.signal_axes[0].is_binned: - if self.signal.axes_manager.signal_axes[0].is_uniform: - return result / self.signal.axes_manager.signal_axes[0].scale + axis = self.signal.axes_manager.signal_axes[0] + if axis.is_binned: + if axis.is_uniform: + return result / axis.scale else: - return result / np.gradient(self.signal.axes_manager.signal_axes[0].axis) + return result / np.gradient(axis.axis) else: return result def function(self, x): - return self._function(x, self.xscale.value, self.yscale.value, - self.shift.value) + return self._function(x, self.xscale.value, self.yscale.value, self.shift.value) def function_nd(self, axis): - """%s - - """ + """%s""" if self._is_navigation_multidimensional: x = axis[np.newaxis, :] - xscale = self.xscale.map['values'][..., np.newaxis] - yscale = self.yscale.map['values'][..., np.newaxis] - shift = self.shift.map['values'][..., np.newaxis] + xscale = self.xscale.map["values"][..., np.newaxis] + yscale = self.yscale.map["values"][..., np.newaxis] + shift = self.shift.map["values"][..., np.newaxis] return self._function(x, xscale, yscale, shift) else: return self.function(axis) diff --git a/hyperspy/_components/skew_normal.py b/hyperspy/_components/skew_normal.py index ec46caffe9..0d0abd60d0 100644 --- a/hyperspy/_components/skew_normal.py +++ b/hyperspy/_components/skew_normal.py @@ -1,30 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import numpy as np import dask.array as da -import sympy +import numpy as np -from hyperspy.component import _get_scaling_factor from hyperspy._components.expression import Expression -from hyperspy.misc.utils import is_binned # remove in v2.0 - -from distutils.version import LooseVersion +from hyperspy.component import _get_scaling_factor sqrt2pi = np.sqrt(2 * np.pi) @@ -34,16 +30,20 @@ def _estimate_skewnormal_parameters(signal, x1, x2, only_current): i1, i2 = axis.value_range_to_indices(x1, x2) X = axis.axis[i1:i2] if only_current is True: - data = signal()[i1:i2] + data = signal._get_current_data()[i1:i2] X_shape = (len(X),) i = 0 x0_shape = (1,) else: i = axis.index_in_array - data_gi = [slice(None), ] * len(signal.data.shape) + data_gi = [ + slice(None), + ] * len(signal.data.shape) data_gi[axis.index_in_array] = slice(i1, i2) data = signal.data[tuple(data_gi)] - X_shape = [1, ] * len(signal.data.shape) + X_shape = [ + 1, + ] * len(signal.data.shape) X_shape[axis.index_in_array] = data.shape[i] x0_shape = list(data.shape) x0_shape[i] = 1 @@ -51,17 +51,21 @@ def _estimate_skewnormal_parameters(signal, x1, x2, only_current): a1 = np.sqrt(2 / np.pi) b1 = (4 / np.pi - 1) * a1 m1 = np.sum(X.reshape(X_shape) * data, i) / np.sum(data, i) - m2 = np.abs(np.sum((X.reshape(X_shape) - m1.reshape(x0_shape)) ** 2 * data, i) - / np.sum(data, i)) - m3 = np.abs(np.sum((X.reshape(X_shape) - m1.reshape(x0_shape)) ** 3 * data, i) - / np.sum(data, i)) + m2 = abs( + np.sum((X.reshape(X_shape) - m1.reshape(x0_shape)) ** 2 * data, i) + / np.sum(data, i) + ) + m3 = abs( + np.sum((X.reshape(X_shape) - m1.reshape(x0_shape)) ** 3 * data, i) + / np.sum(data, i) + ) x0 = m1 - a1 * (m3 / b1) ** (1 / 3) - scale = np.sqrt(m2 + a1 ** 2 * (m3 / b1) ** (2 / 3)) + scale = np.sqrt(m2 + a1**2 * (m3 / b1) ** (2 / 3)) delta = np.sqrt(1 / (a1**2 + m2 * (b1 / m3) ** (2 / 3))) shape = delta / np.sqrt(1 - delta**2) - iheight = np.argmin(np.abs(X.reshape(X_shape) - x0.reshape(x0_shape)), i) + iheight = np.argmin(abs(X.reshape(X_shape) - x0.reshape(x0_shape)), i) # height is the value of the function at x0, shich has to be computed # differently for dask array (lazy) and depending on the dimension if isinstance(data, da.Array): @@ -69,26 +73,25 @@ def _estimate_skewnormal_parameters(signal, x1, x2, only_current): if only_current is True or signal.axes_manager.navigation_dimension == 0: height = data.vindex[iheight].compute() elif signal.axes_manager.navigation_dimension == 1: - height = data.vindex[np.arange(signal.axes_manager.navigation_size), - iheight].compute() + height = data.vindex[ + np.arange(signal.axes_manager.navigation_size), iheight + ].compute() else: - height = data.vindex[(*np.indices(signal.axes_manager.navigation_shape), - iheight)].compute() + height = data.vindex[ + (*np.indices(signal.axes_manager.navigation_shape), iheight) + ].compute() else: if only_current is True or signal.axes_manager.navigation_dimension == 0: height = data[iheight] elif signal.axes_manager.navigation_dimension == 1: - height = data[np.arange(signal.axes_manager.navigation_size), - iheight] + height = data[np.arange(signal.axes_manager.navigation_size), iheight] else: - height = data[(*np.indices(signal.axes_manager.navigation_shape), - iheight)] + height = data[(*np.indices(signal.axes_manager.navigation_shape), iheight)] return x0, height, scale, shape class SkewNormal(Expression): - r"""Skew normal distribution component. | Asymmetric peak shape based on a normal distribution. @@ -118,7 +121,7 @@ class SkewNormal(Expression): Parameters - ----------- + ---------- x0 : float Location of the peak position (not maximum, which is given by the `mode` property). @@ -131,17 +134,19 @@ class SkewNormal(Expression): distribution (Gaussian) is obtained. The distribution is right skewed (longer tail to the right) if shape>0 and is left skewed if shape<0. + **kwargs + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. - + Notes + ----- The properties `mean` (position), `variance`, `skewness` and `mode` - (=position of maximum) are defined for convenience. + (position of maximum) are defined for convenience. """ - def __init__(self, x0=0., A=1., scale=1., shape=0., - module=['numpy', 'scipy'], **kwargs): - if LooseVersion(sympy.__version__) < LooseVersion("1.3"): - raise ImportError("The `SkewNormal` component requires " - "SymPy >= 1.3") + def __init__( + self, x0=0.0, A=1.0, scale=1.0, shape=0.0, module=["numpy", "scipy"], **kwargs + ): # We use `_shape` internally because `shape` is already taken in sympy # https://github.com/sympy/sympy/pull/20791 super().__init__( @@ -161,7 +166,7 @@ def __init__(self, x0=0., A=1., scale=1., shape=0., ) # Boundaries - self.A.bmin = 0. + self.A.bmin = 0.0 self.scale.bmin = 0 @@ -173,7 +178,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -204,13 +209,14 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): >>> s.axes_manager._axes[-1].offset = -10 >>> s.axes_manager._axes[-1].scale = 0.01 >>> g.estimate_parameters(s, -10, 10, False) + True """ super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] x0, height, scale, shape = _estimate_skewnormal_parameters( signal, x1, x2, only_current - ) + ) scaling_factor = _get_scaling_factor(signal, axis, x0) if only_current is True: @@ -218,54 +224,63 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): self.A.value = height * sqrt2pi self.scale.value = scale self.shape.value = shape - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: self.A.value /= scaling_factor return True else: if self.A.map is None: self._create_arrays() - self.A.map['values'][:] = height * sqrt2pi - - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.A.map['values'] /= scaling_factor - self.A.map['is_set'][:] = True - self.x0.map['values'][:] = x0 - self.x0.map['is_set'][:] = True - self.scale.map['values'][:] = scale - self.scale.map['is_set'][:] = True - self.shape.map['values'][:] = shape - self.shape.map['is_set'][:] = True + self.A.map["values"][:] = height * sqrt2pi + + if axis.is_binned: + self.A.map["values"] /= scaling_factor + self.A.map["is_set"][:] = True + self.x0.map["values"][:] = x0 + self.x0.map["is_set"][:] = True + self.scale.map["values"][:] = scale + self.scale.map["is_set"][:] = True + self.shape.map["values"][:] = shape + self.shape.map["is_set"][:] = True self.fetch_stored_values() return True @property def mean(self): + """Mean (position) of the component.""" delta = self.shape.value / np.sqrt(1 + self.shape.value**2) return self.x0.value + self.scale.value * delta * np.sqrt(2 / np.pi) @property def variance(self): + """Variance of the component.""" delta = self.shape.value / np.sqrt(1 + self.shape.value**2) return self.scale.value**2 * (1 - 2 * delta**2 / np.pi) @property def skewness(self): + """Skewness of the component.""" delta = self.shape.value / np.sqrt(1 + self.shape.value**2) - return (4 - np.pi)/2 * (delta * np.sqrt(2/np.pi))**3 / (1 - - 2 * delta**2 / np.pi)**(3/2) + return ( + (4 - np.pi) + / 2 + * (delta * np.sqrt(2 / np.pi)) ** 3 + / (1 - 2 * delta**2 / np.pi) ** (3 / 2) + ) @property def mode(self): + """Mode (position of maximum) of the component.""" delta = self.shape.value / np.sqrt(1 + self.shape.value**2) muz = np.sqrt(2 / np.pi) * delta sigmaz = np.sqrt(1 - muz**2) if self.shape.value == 0: return self.x0.value else: - m0 = muz - self.skewness * sigmaz / 2 - np.sign(self.shape.value) \ - / 2 * np.exp(- 2 * np.pi / np.abs(self.shape.value)) + m0 = ( + muz + - self.skewness * sigmaz / 2 + - np.sign(self.shape.value) + / 2 + * np.exp(-2 * np.pi / abs(self.shape.value)) + ) return self.x0.value + self.scale.value * m0 diff --git a/hyperspy/_components/split_voigt.py b/hyperspy/_components/split_voigt.py index 34e84e2096..2f8c65c818 100644 --- a/hyperspy/_components/split_voigt.py +++ b/hyperspy/_components/split_voigt.py @@ -1,33 +1,32 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np -from hyperspy.component import Component, _get_scaling_factor from hyperspy._components.gaussian import _estimate_gaussian_parameters +from hyperspy.component import Component, _get_scaling_factor from hyperspy.docstrings.parameters import FUNCTION_ND_DOCSTRING -from hyperspy.misc.utils import is_binned # remove in v2.0 sqrt2pi = np.sqrt(2 * np.pi) -class SplitVoigt(Component): - r"""Split pseudo-Voigt +class SplitVoigt(Component): + r"""Split pseudo-Voigt component. .. math:: :nowrap: @@ -56,18 +55,17 @@ class SplitVoigt(Component): :math:`centre` centre ================= =========== - Note + Notes ----- This is a voigt function in which the upstream and downstream variance or sigma is allowed to vary to create an asymmetric profile - In this case the voigt is a pseudo voigt- consisting of a + In this case the voigt is a pseudo voigt consisting of a mixed gaussian and lorentzian sum """ - def __init__(self, A=1., sigma1=1., sigma2=1.0, fraction=0.0, centre=0.): - Component.__init__( - self, ('A', 'sigma1', 'sigma2', 'centre', 'fraction')) + def __init__(self, A=1.0, sigma1=1.0, sigma2=1.0, fraction=0.0, centre=0.0): + Component.__init__(self, ("A", "sigma1", "sigma2", "centre", "fraction")) self.A.value = A self.sigma1.value = sigma1 self.sigma2.value = sigma2 @@ -88,11 +86,13 @@ def __init__(self, A=1., sigma1=1., sigma2=1.0, fraction=0.0, centre=0.): self.convolved = True def _function(self, x, A, sigma1, sigma2, fraction, centre): - arg = (x - centre) - lor1 = (A / (1.0 + ((1.0 * arg) / sigma1) ** 2)) \ - / (0.5 * np.pi * (sigma1 + sigma2)) - lor2 = (A / (1.0 + ((1.0 * arg) / sigma2) ** 2)) \ - / (0.5 * np.pi * (sigma1 + sigma2)) + arg = x - centre + lor1 = (A / (1.0 + ((1.0 * arg) / sigma1) ** 2)) / ( + 0.5 * np.pi * (sigma1 + sigma2) + ) + lor2 = (A / (1.0 + ((1.0 * arg) / sigma2) ** 2)) / ( + 0.5 * np.pi * (sigma1 + sigma2) + ) prefactor = A / (sqrt2pi * 0.5 * (sigma1 + sigma2)) gauss1 = prefactor * np.exp(-0.5 * arg * arg / (sigma1 * sigma1)) @@ -131,16 +131,14 @@ def function(self, x): return self._function(x, A, sigma1, sigma2, fraction, centre) def function_nd(self, axis): - """%s - - """ + """%s""" if self._is_navigation_multidimensional: x = axis[np.newaxis, :] - A = self.A.map['values'][..., np.newaxis] - sigma1 = self.sigma1.map['values'][..., np.newaxis] - sigma2 = self.sigma2.map['values'][..., np.newaxis] - fraction = self.fraction.map['values'][..., np.newaxis] - centre = self.centre.map['values'][..., np.newaxis] + A = self.A.map["values"][..., np.newaxis] + sigma1 = self.sigma1.map["values"][..., np.newaxis] + sigma2 = self.sigma2.map["values"][..., np.newaxis] + fraction = self.fraction.map["values"][..., np.newaxis] + centre = self.centre.map["values"][..., np.newaxis] else: x = axis A = self.A.value @@ -158,7 +156,7 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. @@ -175,25 +173,27 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Notes ----- - Adapted from http://www.scipy.org/Cookbook/FittingData + Adapted from https://scipy-cookbook.readthedocs.io/items/FittingData.html Examples -------- - >>> g = hs.model.components1D.Gaussian() - >>> x = np.arange(-10,10, 0.01) - >>> data = np.zeros((32,32,2000)) - >>> data[:] = g.function(x).reshape((1,1,2000)) - >>> s = hs.signals.Signal1D({'data' : data}) - >>> s.axes_manager.axes[-1].offset = -10 - >>> s.axes_manager.axes[-1].scale = 0.01 - >>> g.estimate_parameters(s, -10,10, False) + >>> g = hs.model.components1D.SplitVoigt() + >>> x = np.arange(-10, 10, 0.01) + >>> data = np.zeros((32, 32, 2000)) + >>> data[:] = g.function(x).reshape((1, 1, 2000)) + >>> s = hs.signals.Signal1D(data) + >>> s.axes_manager[-1].offset = -10 + >>> s.axes_manager[-1].scale = 0.01 + >>> g.estimate_parameters(s, -10, 10, False) + True """ super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] - centre, height, sigma = _estimate_gaussian_parameters(signal, x1, x2, - only_current) + centre, height, sigma = _estimate_gaussian_parameters( + signal, x1, x2, only_current + ) scaling_factor = _get_scaling_factor(signal, axis, centre) if only_current is True: @@ -201,26 +201,22 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): self.sigma1.value = sigma self.sigma2.value = sigma self.A.value = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: self.A.value /= scaling_factor return True else: if self.A.map is None: self._create_arrays() - self.A.map['values'][:] = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.A.map['values'][:] /= scaling_factor - self.A.map['is_set'][:] = True - self.sigma1.map['values'][:] = sigma - self.sigma1.map['is_set'][:] = True - self.sigma2.map['values'][:] = sigma - self.sigma2.map['is_set'][:] = True - self.centre.map['values'][:] = centre - self.centre.map['is_set'][:] = True + self.A.map["values"][:] = height * sigma * sqrt2pi + if axis.is_binned: + self.A.map["values"][:] /= scaling_factor + self.A.map["is_set"][:] = True + self.sigma1.map["values"][:] = sigma + self.sigma1.map["is_set"][:] = True + self.sigma2.map["values"][:] = sigma + self.sigma2.map["is_set"][:] = True + self.centre.map["values"][:] = centre + self.centre.map["is_set"][:] = True self.fetch_stored_values() return True diff --git a/hyperspy/_components/voigt.py b/hyperspy/_components/voigt.py index 1d87015c7b..0905d608fa 100644 --- a/hyperspy/_components/voigt.py +++ b/hyperspy/_components/voigt.py @@ -1,37 +1,32 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import math -import sympy -import numpy as np -from hyperspy.component import _get_scaling_factor from hyperspy._components.expression import Expression from hyperspy._components.gaussian import _estimate_gaussian_parameters -from hyperspy.misc.utils import is_binned # remove in v2.0 -from distutils.version import LooseVersion +from hyperspy.component import _get_scaling_factor sqrt2pi = math.sqrt(2 * math.pi) sigma2fwhm = 2 * math.sqrt(2 * math.log(2)) class Voigt(Expression): - r"""Voigt component. Symmetric peak shape based on the convolution of a Lorentzian and Normal @@ -63,7 +58,7 @@ class Voigt(Expression): Parameters - ----------- + ---------- centre : float Location of the maximum of the peak. area : float @@ -72,33 +67,36 @@ class Voigt(Expression): :math:`\gamma` = HWHM of the Lorentzian distribution. sigma: float :math:`2 \sigma \sqrt{(2 \log(2))}` = FWHM of the Gaussian distribution. + **kwargs + Extra keyword arguments are passed to the + :class:`~.api.model.components1D.Expression` component. - + Notes + ----- For convenience the `gwidth` and `lwidth` attributes can also be used to set and get the FWHM of the Gaussian and Lorentzian parts of the distribution, respectively. For backwards compatability, `FWHM` is another alias for the Gaussian width. - Notes - ----- W.I.F. David, J. Appl. Cryst. (1986). 19, 63-64, doi:10.1107/S0021889886089999 """ - def __init__(self, centre=10., area=1., gamma=0.2, sigma=0.1, - module=["numpy", "scipy"], **kwargs): - # Not to break scripts once we remove the legacy Voigt - if "legacy" in kwargs: - del kwargs["legacy"] - if LooseVersion(sympy.__version__) < LooseVersion("1.3"): - raise ImportError("The `Voigt` component requires " - "SymPy >= 1.3") - # We use `_gamma` internally to workaround the use of the `gamma` + def __init__( + self, + centre=10.0, + area=1.0, + gamma=0.2, + sigma=0.1, + module=["numpy", "scipy"], + **kwargs, + ): + # We use `gamma_` internally to workaround the use of the `gamma` # function in sympy super().__init__( - expression="area * real(V); \ + expression="area * re(V); \ V = wofz(z) / (sqrt(2.0 * pi) * sigma); \ - z = (x - centre + 1j * _gamma) / (sigma * sqrt(2.0))", + z = (x - centre + 1j * gamma_) / (sigma * sqrt(2.0))", name="Voigt", centre=centre, area=area, @@ -106,15 +104,16 @@ def __init__(self, centre=10., area=1., gamma=0.2, sigma=0.1, sigma=sigma, position="centre", module=module, + compute_gradients=False, autodoc=False, - rename_pars={"_gamma": "gamma"}, + rename_pars={"gamma_": "gamma"}, **kwargs, ) # Boundaries - self.area.bmin = 0. - self.gamma.bmin = 0. - self.sigma.bmin = 0. + self.area.bmin = 0.0 + self.gamma.bmin = 0.0 + self.sigma.bmin = 0.0 self.isbackground = False self.convolved = True @@ -125,30 +124,29 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): Parameters ---------- - signal : Signal1D instance + signal : :class:`~.api.signals.Signal1D` x1 : float Defines the left limit of the spectral range to use for the estimation. x2 : float Defines the right limit of the spectral range to use for the estimation. - only_current : bool If False estimates the parameters for the full dataset. Returns ------- - : bool - Exit status required for the :meth:`remove_background` function. + bool + Exit status required for the :meth:`~.api.signals.Signal1D.remove_background` function. Notes ----- - Adapted from http://www.scipy.org/Cookbook/FittingData + Adapted from https://scipy-cookbook.readthedocs.io/items/FittingData.html Examples -------- - >>> g = hs.model.components1D.Voigt(legacy=False) + >>> g = hs.model.components1D.Voigt() >>> x = np.arange(-10, 10, 0.01) >>> data = np.zeros((32, 32, 2000)) >>> data[:] = g.function(x).reshape((1, 1, 2000)) @@ -156,36 +154,34 @@ def estimate_parameters(self, signal, x1, x2, only_current=False): >>> s.axes_manager[-1].offset = -10 >>> s.axes_manager[-1].scale = 0.01 >>> g.estimate_parameters(s, -10, 10, False) + True """ super()._estimate_parameters(signal) axis = signal.axes_manager.signal_axes[0] - centre, height, sigma = _estimate_gaussian_parameters(signal, x1, x2, - only_current) + centre, height, sigma = _estimate_gaussian_parameters( + signal, x1, x2, only_current + ) scaling_factor = _get_scaling_factor(signal, axis, centre) if only_current is True: self.centre.value = centre self.sigma.value = sigma self.area.value = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: + if axis.is_binned: self.area.value /= scaling_factor return True else: if self.area.map is None: self._create_arrays() - self.area.map['values'][:] = height * sigma * sqrt2pi - if is_binned(signal): - # in v2 replace by - #if axis.is_binned: - self.area.map['values'][:] /= scaling_factor - self.area.map['is_set'][:] = True - self.sigma.map['values'][:] = sigma - self.sigma.map['is_set'][:] = True - self.centre.map['values'][:] = centre - self.centre.map['is_set'][:] = True + self.area.map["values"][:] = height * sigma * sqrt2pi + if axis.is_binned: + self.area.map["values"][:] /= scaling_factor + self.area.map["is_set"][:] = True + self.sigma.map["values"][:] = sigma + self.sigma.map["is_set"][:] = True + self.centre.map["values"][:] = centre + self.centre.map["is_set"][:] = True self.fetch_stored_values() return True diff --git a/hyperspy/_components/volume_plasmon_drude.py b/hyperspy/_components/volume_plasmon_drude.py deleted file mode 100644 index 2e6fccbb20..0000000000 --- a/hyperspy/_components/volume_plasmon_drude.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - -from hyperspy._components.expression import Expression - - -class VolumePlasmonDrude(Expression): - - r"""Drude volume plasmon energy loss function component, the energy loss - function is defined as: - - .. math:: - - f(E) = I_0 \frac{E(\Delta E_p)E_p^2}{(E^2-E_p^2)^2+(E\Delta E_p)^2} - - ================== =============== - Variable Parameter - ================== =============== - :math:`I_0` intensity - :math:`E_p` plasmon_energy - :math:`\Delta E_p` fwhm - ================== =============== - - Parameters - ---------- - intensity : float - plasmon_energy : float - fwhm : float - - Notes - ----- - Refer to Egerton, R. F., Electron Energy-Loss Spectroscopy in the - Electron Microscope, 2nd edition, Plenum Press 1996, pp. 154-158 - for details, including original equations. - """ - - def __init__(self, intensity=1., plasmon_energy=15., fwhm=1.5, - module="numexpr", compute_gradients=False, **kwargs): - super().__init__( - expression="where(x > 0, intensity * (pe2 * x * fwhm) \ - / ((x ** 2 - pe2) ** 2 + (x * fwhm) ** 2), 0); \ - pe2 = plasmon_energy ** 2", - name="VolumePlasmonDrude", - intensity=intensity, - plasmon_energy=plasmon_energy, - fwhm=fwhm, - position="plasmon_energy", - module=module, - autodoc=False, - compute_gradients=compute_gradients, - **kwargs, - ) - - # Partial derivative with respect to the plasmon energy E_p - def grad_plasmon_energy(self, x): - plasmon_energy = self.plasmon_energy.value - fwhm = self.fwhm.value - intensity = self.intensity.value - - return np.where( - x > 0, - 2 * x * fwhm * plasmon_energy * intensity * ( - (x ** 4 + (x * fwhm) ** 2 - plasmon_energy ** 4) / - (x ** 4 + x ** 2 * (fwhm ** 2 - 2 * plasmon_energy ** 2) + - plasmon_energy ** 4) ** 2), - 0) - - # Partial derivative with respect to the plasmon linewidth delta_E_p - def grad_fwhm(self, x): - plasmon_energy = self.plasmon_energy.value - fwhm = self.fwhm.value - intensity = self.intensity.value - - return np.where( - x > 0, - x * plasmon_energy * intensity * ( - (x ** 4 - x ** 2 * (2 * plasmon_energy ** 2 + fwhm ** 2) + - plasmon_energy ** 4) / - (x ** 4 + x ** 2 * (fwhm ** 2 - 2 * plasmon_energy ** 2) + - plasmon_energy ** 4) ** 2), - 0) - - def grad_intensity(self, x): - return self.function(x) / self.intensity.value diff --git a/hyperspy/_lazy_signals.py b/hyperspy/_lazy_signals.py index 731edc5e66..4ee86b5e87 100644 --- a/hyperspy/_lazy_signals.py +++ b/hyperspy/_lazy_signals.py @@ -1,29 +1,37 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy._signals.lazy import LazySignal -from hyperspy._signals.signal1d import LazySignal1D -from hyperspy._signals.signal2d import LazySignal2D -from hyperspy._signals.eels import LazyEELSSpectrum -from hyperspy._signals.eds_sem import LazyEDSSEMSpectrum -from hyperspy._signals.eds_tem import LazyEDSTEMSpectrum from hyperspy._signals.complex_signal import LazyComplexSignal from hyperspy._signals.complex_signal1d import LazyComplexSignal1D from hyperspy._signals.complex_signal2d import LazyComplexSignal2D -from hyperspy._signals.dielectric_function import LazyDielectricFunction -from hyperspy._signals.hologram_image import LazyHologramImage +from hyperspy._signals.lazy import LazySignal +from hyperspy._signals.signal1d import LazySignal1D +from hyperspy._signals.signal2d import LazySignal2D + +__all__ = [ + "LazyComplexSignal", + "LazyComplexSignal1D", + "LazyComplexSignal2D", + "LazySignal", + "LazySignal1D", + "LazySignal2D", +] + + +def __dir__(): + return sorted(__all__) diff --git a/hyperspy/_signals/common_signal1d.py b/hyperspy/_signals/common_signal1d.py index 956529bc8b..204def90c0 100644 --- a/hyperspy/_signals/common_signal1d.py +++ b/hyperspy/_signals/common_signal1d.py @@ -1,28 +1,27 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy.exceptions import DataDimensionError from hyperspy.docstrings.signal import OPTIMIZE_ARG +from hyperspy.exceptions import DataDimensionError class CommonSignal1D: - """Common functions for 1-dimensional signals.""" def to_signal2D(self, optimize=True): @@ -36,7 +35,9 @@ def to_signal2D(self, optimize=True): See Also -------- - transpose, as_signal1D, as_signal2D, hs.transpose + hyperspy.api.signals.BaseSignal.as_signal2D, + hyperspy.api.signals.BaseSignal.transpose, + hyperspy.api.transpose Raises ------ @@ -47,9 +48,12 @@ def to_signal2D(self, optimize=True): """ if self.data.ndim < 2: raise DataDimensionError( - "A Signal dimension must be >= 2 to be converted to Signal2D") + "A Signal dimension must be >= 2 to be converted to Signal2D" + ) nat = self.axes_manager._get_axes_in_natural_order() - im = self.transpose(signal_axes=nat[:2], navigation_axes=nat[2:], - optimize=optimize) + im = self.transpose( + signal_axes=nat[:2], navigation_axes=nat[2:], optimize=optimize + ) return im - to_signal2D.__doc__ %= (OPTIMIZE_ARG.replace('False', 'True')) + + to_signal2D.__doc__ %= OPTIMIZE_ARG.replace("False", "True") diff --git a/hyperspy/_signals/common_signal2d.py b/hyperspy/_signals/common_signal2d.py index 2dad59332e..0c5e901150 100644 --- a/hyperspy/_signals/common_signal2d.py +++ b/hyperspy/_signals/common_signal2d.py @@ -1,27 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy.docstrings.signal import OPTIMIZE_ARG class CommonSignal2D: - """Common functions for 2-dimensional signals.""" def to_signal1D(self, optimize=True): @@ -31,12 +30,11 @@ def to_signal1D(self, optimize=True): See Also -------- - as_signal1D : a method for the same purpose with more options. - signals.Signal1D.to_signal1D : performs the inverse operation on one - dimensional signals. - - as_signal2D, transpose, hs.transpose + hyperspy.api.signals.BaseSignal.as_signal1D, + hyperspy.api.signals.BaseSignal.transpose, + hyperspy.api.transpose """ return self.as_signal1D(0 + 3j, optimize=optimize) - to_signal1D.__doc__ %= (OPTIMIZE_ARG.replace('False', 'True')) + + to_signal1D.__doc__ %= OPTIMIZE_ARG.replace("False", "True") diff --git a/hyperspy/_signals/complex_signal.py b/hyperspy/_signals/complex_signal.py index 8a5397c6c9..0d5c4f464a 100644 --- a/hyperspy/_signals/complex_signal.py +++ b/hyperspy/_signals/complex_signal.py @@ -1,37 +1,46 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from functools import wraps import numpy as np +from packaging.version import Version -from hyperspy.signal import BaseSignal -from hyperspy._signals.signal2d import Signal2D from hyperspy._signals.lazy import LazySignal +from hyperspy._signals.signal2d import Signal2D from hyperspy.docstrings.plot import ( - BASE_PLOT_DOCSTRING, BASE_PLOT_DOCSTRING_PARAMETERS, COMPLEX_DOCSTRING, - PLOT2D_KWARGS_DOCSTRING) -from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + COMPLEX_DOCSTRING, + PLOT2D_KWARGS_DOCSTRING, +) +from hyperspy.docstrings.signal import ( + LAZYSIGNAL_DOC, + NUM_WORKERS_ARG, + SHOW_PROGRESSBAR_ARG, +) from hyperspy.misc.utils import parse_quantity +from hyperspy.signal import BaseSignal - -ERROR_MESSAGE_SETTER = ('Setting the {} with a complex signal is ambiguous, ' - 'use a numpy array or a real signal.') +ERROR_MESSAGE_SETTER = ( + "Setting the {} with a complex signal is ambiguous, " + "use a numpy array or a real signal." +) PROPERTY_DOCSTRING_TEMPLATE = """Get/set the {} of the data.""" @@ -45,16 +54,17 @@ def signal_wrapper(*args, **kwargs): if signal.metadata.General.title: title = signal.metadata.General.title else: - title = 'Untitled Signal' - signal.metadata.General.title = f'{thing}({title})' + title = "Untitled Signal" + signal.metadata.General.title = f"{thing}({title})" return signal + return signal_wrapper + return title_decorator class ComplexSignal(BaseSignal): - - """BaseSignal subclass for complex data.""" + """General signal class for complex data.""" _dtype = "complex" @@ -67,77 +77,81 @@ def __init__(self, *args, **kwargs): if not np.issubdtype(self.data.dtype, np.complexfloating): self.data = self.data.astype(np.complex128) - @format_title('real') + @format_title("real") def _get_real(self): real = self._deepcopy_with_new_data(self.data.real) real._assign_subclass() return real - real = property(lambda s: s._get_real(), - lambda s, v: s._set_real(v), - doc=PROPERTY_DOCSTRING_TEMPLATE.format('real part') - ) + real = property( + lambda s: s._get_real(), + lambda s, v: s._set_real(v), + doc=PROPERTY_DOCSTRING_TEMPLATE.format("real part"), + ) def _set_real(self, real): if isinstance(real, self.__class__): - raise TypeError(ERROR_MESSAGE_SETTER.format('real part')) + raise TypeError(ERROR_MESSAGE_SETTER.format("real part")) elif isinstance(real, BaseSignal): real = real.data self.data = real + 1j * self.data.imag self.events.data_changed.trigger(self) - @format_title('imag') + @format_title("imag") def _get_imag(self): imag = self._deepcopy_with_new_data(self.data.imag) imag._assign_subclass() return imag - imag = property(lambda s: s._get_imag(), - lambda s, v: s._set_imag(v), - doc=PROPERTY_DOCSTRING_TEMPLATE.format('imaginary part') - ) + imag = property( + lambda s: s._get_imag(), + lambda s, v: s._set_imag(v), + doc=PROPERTY_DOCSTRING_TEMPLATE.format("imaginary part"), + ) def _set_imag(self, imag): if isinstance(imag, self.__class__): - raise TypeError(ERROR_MESSAGE_SETTER.format('imaginary part')) + raise TypeError(ERROR_MESSAGE_SETTER.format("imaginary part")) elif isinstance(imag, BaseSignal): imag = imag.data self.data = self.data.real + 1j * imag self.events.data_changed.trigger(self) - @format_title('amplitude') + @format_title("amplitude") def _get_amplitude(self): amplitude = self._deepcopy_with_new_data(abs(self.data)) amplitude._assign_subclass() return amplitude - amplitude = property(lambda s: s._get_amplitude(), - lambda s, v: s._set_amplitude(v), - doc=PROPERTY_DOCSTRING_TEMPLATE.format('amplitude') - ) + amplitude = property( + lambda s: s._get_amplitude(), + lambda s, v: s._set_amplitude(v), + doc=PROPERTY_DOCSTRING_TEMPLATE.format("amplitude"), + ) def _set_amplitude(self, amplitude): if isinstance(amplitude, self.__class__): - raise TypeError(ERROR_MESSAGE_SETTER.format('amplitude')) + raise TypeError(ERROR_MESSAGE_SETTER.format("amplitude")) elif isinstance(amplitude, BaseSignal): amplitude = amplitude.data.real self.data = amplitude * np.exp(1j * np.angle(self.data)) self.events.data_changed.trigger(self) - @format_title('phase') + @format_title("phase") def _get_phase(self): phase = self._deepcopy_with_new_data(np.angle(self.data)) phase._assign_subclass() return phase - phase = property(lambda s: s._get_phase(), - lambda s, v: s._set_phase(v), - doc=PROPERTY_DOCSTRING_TEMPLATE.format('phase') - ) + phase = property( + lambda s: s._get_phase(), + lambda s, v: s._set_phase(v), + doc=PROPERTY_DOCSTRING_TEMPLATE.format("phase"), + ) def _set_phase(self, phase): if isinstance(phase, self.__class__): - raise TypeError(ERROR_MESSAGE_SETTER.format('phase')) + raise TypeError(ERROR_MESSAGE_SETTER.format("phase")) elif isinstance(phase, BaseSignal): phase = phase.data self.data = abs(self.data) * np.exp(1j * phase) @@ -148,40 +162,42 @@ def change_dtype(self, dtype): Parameters ---------- - dtype : str or dtype + dtype : str or numpy.dtype Typecode or data-type to which the array is cast. For complex signals only other - complex dtypes are allowed. If real valued properties are required use `real`, - `imag`, `amplitude` and `phase` instead. + complex dtypes are allowed. If real valued properties are required use ``real``, + ``imag``, ``amplitude`` and ``phase`` instead. """ if np.issubdtype(dtype, np.complexfloating): self.data = self.data.astype(dtype) else: raise ValueError( - 'Complex data can only be converted into other complex dtypes!') + "Complex data can only be converted into other complex dtypes!" + ) - def unwrapped_phase(self, wrap_around=False, seed=None, - show_progressbar=None, parallel=None, max_workers=None): + def unwrapped_phase( + self, wrap_around=False, seed=None, show_progressbar=None, num_workers=None + ): """Return the unwrapped phase as an appropriate HyperSpy signal. Parameters ---------- - wrap_around : bool or sequence of bool, optional + wrap_around : bool or iterable of bool, default False When an element of the sequence is `True`, the unwrapping process will regard the edges along the corresponding axis of the image to be connected and use this connectivity to guide the phase unwrapping process. If only a single boolean is given, it will apply to all axes. Wrap around is not supported for 1D arrays. - seed : int, optional - Unwrapping 2D or 3D images uses random initialization. This sets the - seed of the PRNG to achieve deterministic behavior. - %s + seed : numpy.random.Generator, int or None, default None + Pass to the `rng` argument of the :func:`~skimage.restoration.unwrap_phase` + function. Unwrapping 2D or 3D images uses random initialization. + This sets the seed of the PRNG to achieve deterministic behavior. %s %s Returns ------- - phase_image: :class:`~hyperspy._signals.BaseSignal` subclass - Unwrapped phase. + :class:`~hyperspy.api.signals.BaseSignal` (or subclass) + The unwrapped phase. Notes ----- @@ -192,33 +208,50 @@ def unwrapped_phase(self, wrap_around=False, seed=None, Vol. 41, No. 35, pp. 7437, 2002 """ + import skimage from skimage.restoration import unwrap_phase + + kwargs = {} + if Version(skimage.__version__) >= Version("0.21"): + kwargs["rng"] = seed + else: + kwargs["seed"] = seed + phase = self.phase - phase.map(unwrap_phase, wrap_around=wrap_around, seed=seed, - show_progressbar=show_progressbar, ragged=False, - parallel=parallel, max_workers=max_workers) - phase.metadata.General.title = f'unwrapped {phase.metadata.General.title}' + phase.map( + unwrap_phase, + wrap_around=wrap_around, + show_progressbar=show_progressbar, + ragged=False, + num_workers=num_workers, + **kwargs, + ) + phase.metadata.General.title = f"unwrapped {phase.metadata.General.title}" return phase # Now unwrapped! - unwrapped_phase.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) + unwrapped_phase.__doc__ %= (SHOW_PROGRESSBAR_ARG, NUM_WORKERS_ARG) - def __call__(self, axes_manager=None, power_spectrum=False, - fft_shift=False): - value = super().__call__(axes_manager=axes_manager, - fft_shift=fft_shift) + def _get_current_data( + self, axes_manager=None, power_spectrum=False, fft_shift=False, as_numpy=None + ): + value = super()._get_current_data( + axes_manager=axes_manager, fft_shift=fft_shift, as_numpy=as_numpy + ) if power_spectrum: - value = np.abs(value)**2 + value = abs(value) ** 2 return value - def plot(self, - power_spectrum=False, - representation='cartesian', - same_axes=True, - fft_shift=False, - navigator="auto", - axes_manager=None, - norm="auto", - **kwargs): + def plot( + self, + power_spectrum=False, + representation="cartesian", + same_axes=True, + fft_shift=False, + navigator="auto", + axes_manager=None, + norm="auto", + **kwargs, + ): """%s %s %s @@ -226,21 +259,26 @@ def plot(self, """ if norm == "auto": - norm = 'log' if power_spectrum else 'linear' - - kwargs.update({'norm': norm, - 'fft_shift': fft_shift, - 'navigator': navigator, - 'axes_manager': self.axes_manager}) - if representation == 'cartesian': - if ((same_axes and self.axes_manager.signal_dimension == 1) or - power_spectrum): - kwargs['power_spectrum'] = power_spectrum + norm = "log" if power_spectrum else "linear" + + kwargs.update( + { + "norm": norm, + "fft_shift": fft_shift, + "navigator": navigator, + "axes_manager": self.axes_manager, + } + ) + if representation == "cartesian": + if ( + same_axes and self.axes_manager.signal_dimension == 1 + ) or power_spectrum: + kwargs["power_spectrum"] = power_spectrum super().plot(**kwargs) else: self.real.plot(**kwargs) self.imag.plot(**kwargs) - elif representation == 'polar': + elif representation == "polar": if same_axes and self.axes_manager.signal_dimension == 1: amp = self.amplitude amp.change_dtype("complex") @@ -250,110 +288,134 @@ def plot(self, self.amplitude.plot(**kwargs) self.phase.plot(**kwargs) else: - raise ValueError(f'{representation} is not a valid input for ' - 'representation (use "cartesian" or "polar")!') - - self._plot_kwargs = {'power_spectrum': power_spectrum, - 'representation': representation, - 'norm': norm, - 'fft_shift': fft_shift, - 'same_axes': same_axes} - plot.__doc__ %= (BASE_PLOT_DOCSTRING, COMPLEX_DOCSTRING, - BASE_PLOT_DOCSTRING_PARAMETERS, PLOT2D_KWARGS_DOCSTRING) - - @format_title('angle') + raise ValueError( + f"{representation} is not a valid input for " + 'representation (use "cartesian" or "polar")!' + ) + + self._plot_kwargs = { + "power_spectrum": power_spectrum, + "representation": representation, + "norm": norm, + "fft_shift": fft_shift, + "same_axes": same_axes, + } + + plot.__doc__ %= ( + BASE_PLOT_DOCSTRING, + COMPLEX_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT2D_KWARGS_DOCSTRING, + ) + + @format_title("angle") def angle(self, deg=False): - r"""Return the angle (also known as phase or argument). If the data is real, the angle is 0 + r"""Return the angle (also known as phase or argument). + If the data is real, the angle is 0 for positive values and :math:`2\pi` for negative values. Parameters ---------- - deg : bool, optional - Return angle in degrees if True, radians if False (default). + deg : bool, default False + Return angle in degrees if True, radians if False. Returns ------- - angle : HyperSpy signal + :class:`~hyperspy.api.signals.BaseSignal` The counterclockwise angle from the positive real axis on the complex plane, with dtype as numpy.float64. """ angle = self._deepcopy_with_new_data(np.angle(self.data, deg)) - angle.set_signal_type('') + angle.set_signal_type("") return angle def argand_diagram(self, size=[256, 256], range=None): """ - Calculate and plot Argand diagram of complex signal + Calculate and plot Argand diagram of complex signal. Parameters ---------- - size : [int, int], optional - Size of the Argand plot in pixels - (Default: [256, 256]) - range : array_like, shape(2,2) or shape(2,) optional - The position of the edges of the diagram - (if not specified explicitly in the bins parameters): [[xmin, xmax], [ymin, ymax]]. - All values outside of this range will be considered outliers and not tallied in the histogram. - (Default: None) + size : list of int, optional + Size of the Argand plot in pixels. Default is [256, 256]. + range : None, numpy.ndarray, default None + The position of the edges of the diagram with shape (2, 2) or (2,). + All values outside of this range will be considered outliers and not + tallied in the histogram. If None use the mininum and maximum values. Returns ------- - argand_diagram: - Argand diagram as Signal2D + :class:`~hyperspy.api.signals.Signal2D` + The Argand diagram Examples -------- - >>> import hyperspy.api as hs - >>> holo = hs.datasets.example_signals.object_hologram() - >>> ref = hs.datasets.example_signals.reference_hologram() - >>> w = holo.reconstruct_phase(ref) - >>> w.argand_diagram(range=[-3, 3]).plot() + >>> import holospy as holo # doctest: +SKIP + >>> hologram = holo.data.Fe_needle_hologram() # doctest: +SKIP + >>> ref = holo.data.Fe_needle_reference_hologram() # doctest: +SKIP + >>> w = hologram.reconstruct_phase(ref) # doctest: +SKIP + >>> w.argand_diagram(range=[-3, 3]).plot() # doctest: +SKIP """ if self._lazy: raise NotImplementedError( "Argand diagram is not implemented for lazy signals. Use " "`compute()` to convert the signal to a regular one." - ) + ) for axis in self.axes_manager.signal_axes: if not axis.is_uniform: raise NotImplementedError( - "The function is not implemented for non-uniform axes.") + "The function is not implemented for non-uniform axes." + ) im = self.imag.data.ravel() re = self.real.data.ravel() if range: if np.asarray(range).shape == (2,): - range = [[range[0], range[1]], - [range[0], range[1]]] + range = [[range[0], range[1]], [range[0], range[1]]] elif np.asarray(range).shape != (2, 2): - raise ValueError('display_range should be array_like, shape(2,2) or shape(2,).') - - argand_diagram, real_edges, imag_edges = np.histogram2d(re, im, bins=size, range=range) - argand_diagram = Signal2D(argand_diagram.T) - argand_diagram.metadata = self.metadata.deepcopy() - argand_diagram.metadata.General.title = f'Argand diagram of {self.metadata.General.title}' + raise ValueError( + "display_range should be array_like, shape(2,2) or shape(2,)." + ) - if self.real.metadata.Signal.has_item('quantity'): - quantity_real, units_real = parse_quantity(self.real.metadata.Signal.quantity) + argand_diagram, real_edges, imag_edges = np.histogram2d( + re, im, bins=size, range=range + ) + argand_diagram = Signal2D( + argand_diagram.T, + metadata=self.metadata.as_dictionary(), + ) + argand_diagram.metadata.General.title = ( + f"Argand diagram of {self.metadata.General.title}" + ) + + if self.real.metadata.Signal.has_item("quantity"): + quantity_real, units_real = parse_quantity( + self.real.metadata.Signal.quantity + ) argand_diagram.axes_manager.signal_axes[0].name = quantity_real else: - argand_diagram.axes_manager.signal_axes[0].name = 'Real' + argand_diagram.axes_manager.signal_axes[0].name = "Real" units_real = None argand_diagram.axes_manager.signal_axes[0].offset = real_edges[0] - argand_diagram.axes_manager.signal_axes[0].scale = np.abs(real_edges[0] - real_edges[1]) - - if self.imag.metadata.Signal.has_item('quantity'): - quantity_imag, units_imag = parse_quantity(self.imag.metadata.Signal.quantity) + argand_diagram.axes_manager.signal_axes[0].scale = abs( + real_edges[0] - real_edges[1] + ) + + if self.imag.metadata.Signal.has_item("quantity"): + quantity_imag, units_imag = parse_quantity( + self.imag.metadata.Signal.quantity + ) argand_diagram.axes_manager.signal_axes[1].name = quantity_imag else: - argand_diagram.axes_manager.signal_axes[1].name = 'Imaginary' + argand_diagram.axes_manager.signal_axes[1].name = "Imaginary" units_imag = None argand_diagram.axes_manager.signal_axes[1].offset = imag_edges[0] - argand_diagram.axes_manager.signal_axes[1].scale = np.abs(imag_edges[0] - imag_edges[1]) + argand_diagram.axes_manager.signal_axes[1].scale = abs( + imag_edges[0] - imag_edges[1] + ) if units_real: argand_diagram.axes_manager.signal_axes[0].units = units_real if units_imag: @@ -363,5 +425,6 @@ def argand_diagram(self, size=[256, 256], range=None): class LazyComplexSignal(ComplexSignal, LazySignal): + """Lazy general signal class for complex data.""" - pass + __doc__ += LAZYSIGNAL_DOC.replace("__BASECLASS__", "ComplexSignal") diff --git a/hyperspy/_signals/complex_signal1d.py b/hyperspy/_signals/complex_signal1d.py index 4dda75c8c9..b5b88c5718 100644 --- a/hyperspy/_signals/complex_signal1d.py +++ b/hyperspy/_signals/complex_signal1d.py @@ -1,40 +1,37 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy._signals.common_signal1d import CommonSignal1D -from hyperspy._signals.complex_signal import (ComplexSignal, LazyComplexSignal) +from hyperspy._signals.complex_signal import ComplexSignal, LazyComplexSignal +from hyperspy.docstrings.signal import LAZYSIGNAL_DOC class ComplexSignal1D(ComplexSignal, CommonSignal1D): - - """BaseSignal subclass for complex 1-dimensional data.""" + """Signal class for complex 1-dimensional data.""" _signal_dimension = 1 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if self.axes_manager.signal_dimension != 1: - self.axes_manager.set_signal_dimension(1) class LazyComplexSignal1D(ComplexSignal1D, LazyComplexSignal): + """Lazy signal class for complex 1-dimensional data.""" - """BaseSignal subclass for lazy complex 1-dimensional data.""" - - pass + __doc__ += LAZYSIGNAL_DOC.replace("__BASECLASS__", "ComplexSignal1D") diff --git a/hyperspy/_signals/complex_signal2d.py b/hyperspy/_signals/complex_signal2d.py index 5ff49a5d19..55905c1cf7 100644 --- a/hyperspy/_signals/complex_signal2d.py +++ b/hyperspy/_signals/complex_signal2d.py @@ -1,39 +1,41 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy._signals.common_signal2d import CommonSignal2D -from hyperspy._signals.complex_signal import (ComplexSignal, LazyComplexSignal) +from hyperspy._signals.complex_signal import ComplexSignal, LazyComplexSignal from hyperspy.docstrings.plot import ( - BASE_PLOT_DOCSTRING, BASE_PLOT_DOCSTRING_PARAMETERS, PLOT2D_DOCSTRING, - COMPLEX_DOCSTRING, PLOT2D_KWARGS_DOCSTRING) + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + COMPLEX_DOCSTRING, + PLOT2D_DOCSTRING, + PLOT2D_KWARGS_DOCSTRING, +) +from hyperspy.docstrings.signal import LAZYSIGNAL_DOC class ComplexSignal2D(ComplexSignal, CommonSignal2D): - - """BaseSignal subclass for complex 2-dimensional data.""" + """Signal class for complex 2-dimensional data.""" _signal_dimension = 2 def __init__(self, *args, **kw): super().__init__(*args, **kw) - if self.axes_manager.signal_dimension != 2: - self.axes_manager.set_signal_dimension(2) def add_phase_ramp(self, ramp_x, ramp_y, offset=0): """Add a linear phase ramp to the wave. @@ -48,39 +50,40 @@ def add_phase_ramp(self, ramp_x, ramp_y, offset=0): Offset of the ramp at the fulcrum. Notes ----- - The fulcrum of the linear ramp is at the origin and the slopes are - given in units of the axis with the according scale taken into - account. Both are available via the `axes_manager` of the signal. + The fulcrum of the linear ramp is at the origin and the slopes are + given in units of the axis with the corresponding scale taken into + account. Both are available via the + :attr:`~hyperspy.api.signals.BaseSignal.axes_manager`. """ phase = self.phase phase.add_ramp(ramp_x, ramp_y, offset) self.phase = phase - def plot(self, - power_spectrum=False, - fft_shift=False, - navigator="auto", - plot_markers=True, - autoscale='v', - saturated_pixels=None, - norm="auto", - vmin=None, - vmax=None, - gamma=1.0, - linthresh=0.01, - linscale=0.1, - scalebar=True, - scalebar_color="white", - axes_ticks=None, - axes_off=False, - axes_manager=None, - no_nans=False, - colorbar=True, - centre_colormap="auto", - min_aspect=0.1, - **kwargs - ): + def plot( + self, + power_spectrum=False, + fft_shift=False, + navigator="auto", + plot_markers=True, + autoscale="v", + norm="auto", + vmin=None, + vmax=None, + gamma=1.0, + linthresh=0.01, + linscale=0.1, + scalebar=True, + scalebar_color="white", + axes_ticks=None, + axes_off=False, + axes_manager=None, + no_nans=False, + colorbar=True, + centre_colormap="auto", + min_aspect=0.1, + **kwargs, + ): """%s %s %s @@ -94,7 +97,6 @@ def plot(self, navigator=navigator, plot_markers=plot_markers, autoscale=autoscale, - saturated_pixels=saturated_pixels, norm=norm, vmin=vmin, vmax=vmax, @@ -110,15 +112,19 @@ def plot(self, colorbar=colorbar, centre_colormap=centre_colormap, min_aspect=min_aspect, - **kwargs + **kwargs, ) - plot.__doc__ %= (BASE_PLOT_DOCSTRING, COMPLEX_DOCSTRING, - BASE_PLOT_DOCSTRING_PARAMETERS, - PLOT2D_DOCSTRING, PLOT2D_KWARGS_DOCSTRING) + plot.__doc__ %= ( + BASE_PLOT_DOCSTRING, + COMPLEX_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT2D_DOCSTRING, + PLOT2D_KWARGS_DOCSTRING, + ) -class LazyComplexSignal2D(ComplexSignal2D, LazyComplexSignal): - """BaseSignal subclass for lazy complex 2-dimensional data.""" +class LazyComplexSignal2D(ComplexSignal2D, LazyComplexSignal): + """Lazy Signal class for complex 2-dimensional data.""" - pass + __doc__ += LAZYSIGNAL_DOC.replace("__BASECLASS__", "ComplexSignal2D") diff --git a/hyperspy/_signals/dielectric_function.py b/hyperspy/_signals/dielectric_function.py deleted file mode 100644 index b669102881..0000000000 --- a/hyperspy/_signals/dielectric_function.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -from scipy import constants -from scipy.integrate import simps, cumtrapz - -from hyperspy._signals.complex_signal1d import (ComplexSignal1D, - LazyComplexSignal1D) -from hyperspy.misc.eels.tools import eels_constant - - -class DielectricFunction(ComplexSignal1D): - - _signal_type = "DielectricFunction" - _alias_signal_types = ["dielectric function"] - - def get_number_of_effective_electrons(self, nat, cumulative=False): - r"""Compute the number of effective electrons using the Bethe f-sum - rule. - - The Bethe f-sum rule gives rise to two definitions of the effective - number (see [*]_), neff1 and neff2: - - .. math:: - - n_{\mathrm{eff_{1}}} = n_{\mathrm{eff}}\left(-\Im\left(\epsilon^{-1}\right)\right) - - and: - - .. math:: - - n_{\mathrm{eff_{2}}} = n_{\mathrm{eff}}\left(\epsilon_{2}\right) - - This method computes and return both. - - Parameters - ---------- - nat: float - Number of atoms (or molecules) per unit volume of the sample. - - Returns - ------- - neff1, neff2: Signal1D - Signal1D instances containing neff1 and neff2. The signal and - navigation dimensions are the same as the current signal if - `cumulative` is True, otherwise the signal dimension is 0 - and the navigation dimension is the same as the current - signal. - - Notes - ----- - .. [*] Ray Egerton, "Electron Energy-Loss Spectroscopy - in the Electron Microscope", Springer-Verlag, 2011. - - """ - - m0 = constants.value("electron mass") - epsilon0 = constants.epsilon_0 # Vacuum permittivity [F/m] - hbar = constants.hbar # Reduced Plank constant [J·s] - k = 2 * epsilon0 * m0 / (np.pi * nat * hbar ** 2) - - axis = self.axes_manager.signal_axes[0] - if cumulative is False: - dneff1 = k * simps((-1. / self.data).imag * axis.axis, - x=axis.axis, - axis=axis.index_in_array) - dneff2 = k * simps(self.data.imag * axis.axis, - x=axis.axis, - axis=axis.index_in_array) - neff1 = self._get_navigation_signal(data=dneff1) - neff2 = self._get_navigation_signal(data=dneff2) - else: - neff1 = self._deepcopy_with_new_data( - k * cumtrapz((-1. / self.data).imag * axis.axis, - x=axis.axis, - axis=axis.index_in_array, - initial=0)) - neff2 = self._deepcopy_with_new_data( - k * cumtrapz(self.data.imag * axis.axis, - x=axis.axis, - axis=axis.index_in_array, - initial=0)) - - # Prepare return - neff1.metadata.General.title = ( - r"$n_{\mathrm{eff}}\left(-\Im\left(\epsilon^{-1}\right)\right)$ " - "calculated from " + - self.metadata.General.title + - " using the Bethe f-sum rule.") - neff2.metadata.General.title = ( - r"$n_{\mathrm{eff}}\left(\epsilon_{2}\right)$ " - "calculated from " + - self.metadata.General.title + - " using the Bethe f-sum rule.") - - return neff1, neff2 - - def get_electron_energy_loss_spectrum(self, zlp, t): - for axis in self.axes_manager.signal_axes: - if not axis.is_uniform: - raise NotImplementedError( - "The function is not implemented for non-uniform axes.") - data = ((-1 / self.data).imag * eels_constant(self, zlp, t).data * - self.axes_manager.signal_axes[0].scale) - s = self._deepcopy_with_new_data(data) - s.data = s.data.real - s.set_signal_type("EELS") - s.metadata.General.title = ("EELS calculated from " + - self.metadata.General.title) - return s - - -class LazyDielectricFunction(DielectricFunction, LazyComplexSignal1D): - pass diff --git a/hyperspy/_signals/eds.py b/hyperspy/_signals/eds.py deleted file mode 100644 index 3f357f5de0..0000000000 --- a/hyperspy/_signals/eds.py +++ /dev/null @@ -1,1119 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . -import itertools -import logging - -import numpy as np -import warnings -from collections.abc import Iterable -from matplotlib import pyplot as plt - -from hyperspy import utils -from hyperspy.signal import BaseSignal -from hyperspy._signals.signal1d import Signal1D, LazySignal1D -from hyperspy.misc.elements import elements as elements_db -from hyperspy.misc.eds import utils as utils_eds -from hyperspy.misc.utils import isiterable -from hyperspy.utils.plot import markers -from hyperspy.docstrings.plot import (BASE_PLOT_DOCSTRING_PARAMETERS, - PLOT1D_DOCSTRING) - - -_logger = logging.getLogger(__name__) - - -class EDSSpectrum(Signal1D): - - _signal_type = "EDS" - - def __init__(self, *args, **kwards): - super().__init__(*args, **kwards) - if self.metadata.Signal.signal_type == 'EDS': - warnings.warn('The microscope type is not set. Use ' - 'set_signal_type(\'EDS_TEM\') ' - 'or set_signal_type(\'EDS_SEM\')') - self.axes_manager.signal_axes[0].is_binned = True - self._xray_markers = {} - - def _get_line_energy(self, Xray_line, FWHM_MnKa=None): - """ - Get the line energy and the energy resolution of a Xray line. - - The return values are in the same units than the signal axis - - Parameters - ---------- - Xray_line : strings - Valid element X-ray lines e.g. Fe_Kb - FWHM_MnKa: {None, float, 'auto'} - The energy resolution of the detector in eV - if 'auto', used the one in - 'self.metadata.Acquisition_instrument.SEM.Detector.EDS.energy_resolution_MnKa' - - Returns - ------- - float: the line energy, if FWHM_MnKa is None - (float,float): the line energy and the energy resolution, if FWHM_MnKa - is not None - """ - - units_name = self.axes_manager.signal_axes[0].units - - if FWHM_MnKa == 'auto': - if self.metadata.Signal.signal_type == "EDS_SEM": - FWHM_MnKa = self.metadata.Acquisition_instrument.SEM.\ - Detector.EDS.energy_resolution_MnKa - elif self.metadata.Signal.signal_type == "EDS_TEM": - FWHM_MnKa = self.metadata.Acquisition_instrument.TEM.\ - Detector.EDS.energy_resolution_MnKa - else: - raise NotImplementedError( - "This method only works for EDS_TEM or EDS_SEM signals. " - "You can use `set_signal_type('EDS_TEM')` or" - "`set_signal_type('EDS_SEM')` to convert to one of these" - "signal types.") - line_energy = utils_eds._get_energy_xray_line(Xray_line) - if units_name == 'eV': - line_energy *= 1000 - if FWHM_MnKa is not None: - line_FWHM = utils_eds.get_FWHM_at_Energy( - FWHM_MnKa, line_energy / 1000) * 1000 - elif units_name == 'keV': - if FWHM_MnKa is not None: - line_FWHM = utils_eds.get_FWHM_at_Energy(FWHM_MnKa, - line_energy) - else: - raise ValueError( - f"{units_name} is not a valid units for the energy axis. " - "Only `eV` and `keV` are supported. " - "If `s` is the variable containing this EDS spectrum:\n " - ">>> s.axes_manager.signal_axes[0].units = 'keV' \n") - if FWHM_MnKa is None: - return line_energy - else: - return line_energy, line_FWHM - - def _get_beam_energy(self): - """ - Get the beam energy. - - The return value is in the same units than the signal axis - """ - - if "Acquisition_instrument.SEM.beam_energy" in self.metadata: - beam_energy = self.metadata.Acquisition_instrument.SEM.beam_energy - elif "Acquisition_instrument.TEM.beam_energy" in self.metadata: - beam_energy = self.metadata.Acquisition_instrument.TEM.beam_energy - else: - raise AttributeError( - "The beam energy is not defined in `metadata`. " - "Use `set_microscope_parameters` to set it.") - - units_name = self.axes_manager.signal_axes[0].units - - if units_name == 'eV': - beam_energy *= 1000 - return beam_energy - - def _get_xray_lines_in_spectral_range(self, xray_lines): - """ - Return the lines in the energy range - - Parameters - ---------- - xray_lines: List of string - The xray_lines - - Return - ------ - The list of xray_lines in the energy range - """ - ax = self.axes_manager.signal_axes[0] - low_value = ax.low_value - high_value = ax.high_value - try: - if self._get_beam_energy() < high_value: - high_value = self._get_beam_energy() - except AttributeError: - # in case the beam energy is not defined in the metadata - pass - xray_lines_in_range = [] - xray_lines_not_in_range = [] - for xray_line in xray_lines: - line_energy = self._get_line_energy(xray_line) - if low_value < line_energy < high_value: - xray_lines_in_range.append(xray_line) - else: - xray_lines_not_in_range.append(xray_line) - return xray_lines_in_range, xray_lines_not_in_range - - def sum(self, axis=None, out=None): - if axis is None: - axis = self.axes_manager.navigation_axes - s = super().sum(axis=axis, out=out) - s = out or s - - # Update live time by the change in navigation axes dimensions - time_factor = ( - np.prod([ax.size for ax in self.axes_manager.navigation_axes]) - / np.prod([ax.size for ax in s.axes_manager.navigation_axes]) - ) - aimd = s.metadata.get_item('Acquisition_instrument', None) - if aimd is not None: - aimd = s.metadata.Acquisition_instrument - if "SEM.Detector.EDS.live_time" in aimd: - aimd.SEM.Detector.EDS.live_time *= time_factor - elif "TEM.Detector.EDS.live_time" in aimd: - aimd.TEM.Detector.EDS.live_time *= time_factor - else: - _logger.info("Live_time could not be found in the metadata and " - "has not been updated.") - - if out is None: - return s - sum.__doc__ = Signal1D.sum.__doc__ - - def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, - out=None): - factors = self._validate_rebin_args_and_get_factors( - new_shape=new_shape, - scale=scale,) - m = super().rebin(new_shape=new_shape, scale=scale, crop=crop, - dtype=dtype, out=out) - m = out or m - time_factor = np.prod([factors[axis.index_in_array] - for axis in m.axes_manager.navigation_axes]) - aimd = m.metadata.Acquisition_instrument - if "Acquisition_instrument.SEM.Detector.EDS.real_time" in m.metadata: - aimd.SEM.Detector.EDS.real_time *= time_factor - elif "Acquisition_instrument.TEM.Detector.EDS.real_time" in m.metadata: - aimd.TEM.Detector.EDS.real_time *= time_factor - else: - _logger.info( - "real_time could not be found in the metadata and has not been updated.") - if "Acquisition_instrument.SEM.Detector.EDS.live_time" in m.metadata: - aimd.SEM.Detector.EDS.live_time *= time_factor - elif "Acquisition_instrument.TEM.Detector.EDS.live_time" in m.metadata: - aimd.TEM.Detector.EDS.live_time *= time_factor - else: - _logger.info( - "Live_time could not be found in the metadata and has not been updated.") - - if out is None: - return m - else: - out.events.data_changed.trigger(obj=out) - return m - - rebin.__doc__ = BaseSignal.rebin.__doc__ - - def set_elements(self, elements): - """Erase all elements and set them. - - Parameters - ---------- - elements : list of strings - A list of chemical element symbols. - - See also - -------- - add_elements, set_lines, add_lines - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> print(s.metadata.Sample.elements) - >>> s.set_elements(['Al']) - >>> print(s.metadata.Sample.elements) - ['Al' 'C' 'Cu' 'Mn' 'Zr'] - ['Al'] - - """ - # Erase previous elements and X-ray lines - if "Sample.elements" in self.metadata: - del self.metadata.Sample.elements - self.add_elements(elements) - - def add_elements(self, elements): - """Add elements and the corresponding X-ray lines. - - The list of elements is stored in `metadata.Sample.elements` - - Parameters - ---------- - elements : list of strings - The symbol of the elements. - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> print(s.metadata.Sample.elements) - >>> s.add_elements(['Ar']) - >>> print(s.metadata.Sample.elements) - ['Al' 'C' 'Cu' 'Mn' 'Zr'] - ['Al', 'Ar', 'C', 'Cu', 'Mn', 'Zr'] - - See also - -------- - set_elements, add_lines, set_lines - - """ - if not isiterable(elements) or isinstance(elements, str): - raise ValueError( - "Input must be in the form of a list. For example, " - "if `s` is the variable containing this EDS spectrum:\n " - ">>> s.add_elements(('C',))\n" - "See the docstring for more information.") - if "Sample.elements" in self.metadata: - elements_ = set(self.metadata.Sample.elements) - else: - elements_ = set() - for element in elements: - if element in elements_db: - elements_.add(element) - else: - raise ValueError( - f"{element} is not a valid chemical element symbol.") - self.metadata.set_item('Sample.elements', sorted(list(elements_))) - - def _get_xray_lines(self, xray_lines=None, only_one=None, - only_lines=('a',)): - if xray_lines is None: - if 'Sample.xray_lines' in self.metadata: - xray_lines = self.metadata.Sample.xray_lines - elif 'Sample.elements' in self.metadata: - xray_lines = self._get_lines_from_elements( - self.metadata.Sample.elements, - only_one=only_one, - only_lines=only_lines) - else: - raise ValueError( - "Not X-ray line, set them with `add_elements`.") - return xray_lines - - def set_lines(self, - lines, - only_one=True, - only_lines=('a',)): - """Erase all Xrays lines and set them. - - See add_lines for details. - - Parameters - ---------- - lines : list of strings - A list of valid element X-ray lines to add e.g. Fe_Kb. - Additionally, if `metadata.Sample.elements` is - defined, add the lines of those elements that where not - given in this list. - only_one: bool - If False, add all the lines of each element in - `metadata.Sample.elements` that has not line - defined in lines. If True (default), - only add the line at the highest energy - above an overvoltage of 2 (< beam energy / 2). - only_lines : {None, list of strings} - If not None, only the given lines will be added. - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.add_lines() - >>> print(s.metadata.Sample.xray_lines) - >>> s.set_lines(['Cu_Ka']) - >>> print(s.metadata.Sample.xray_lines) - ['Al_Ka', 'C_Ka', 'Cu_La', 'Mn_La', 'Zr_La'] - ['Al_Ka', 'C_Ka', 'Cu_Ka', 'Mn_La', 'Zr_La'] - - See also - -------- - add_lines, add_elements, set_elements - - """ - only_lines = utils_eds._parse_only_lines(only_lines) - if "Sample.xray_lines" in self.metadata: - del self.metadata.Sample.xray_lines - self.add_lines(lines=lines, - only_one=only_one, - only_lines=only_lines) - - def add_lines(self, - lines=(), - only_one=True, - only_lines=("a",)): - """Add X-rays lines to the internal list. - - Although most functions do not require an internal list of - X-ray lines because they can be calculated from the internal - list of elements, ocassionally it might be useful to customize the - X-ray lines to be use by all functions by default using this method. - The list of X-ray lines is stored in - `metadata.Sample.xray_lines` - - Parameters - ---------- - lines : list of strings - A list of valid element X-ray lines to add e.g. Fe_Kb. - Additionally, if `metadata.Sample.elements` is - defined, add the lines of those elements that where not - given in this list. If the list is empty (default), and - `metadata.Sample.elements` is - defined, add the lines of all those elements. - only_one: bool - If False, add all the lines of each element in - `metadata.Sample.elements` that has not line - defined in lines. If True (default), - only add the line at the highest energy - above an overvoltage of 2 (< beam energy / 2). - only_lines : {None, list of strings} - If not None, only the given lines will be added. - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.add_lines() - >>> print(s.metadata.Sample.xray_lines) - ['Al_Ka', 'C_Ka', 'Cu_La', 'Mn_La', 'Zr_La'] - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.set_microscope_parameters(beam_energy=30) - >>> s.add_lines() - >>> print(s.metadata.Sample.xray_lines) - ['Al_Ka', 'C_Ka', 'Cu_Ka', 'Mn_Ka', 'Zr_La'] - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.add_lines() - >>> print(s.metadata.Sample.xray_lines) - >>> s.add_lines(['Cu_Ka']) - >>> print(s.metadata.Sample.xray_lines) - ['Al_Ka', 'C_Ka', 'Cu_La', 'Mn_La', 'Zr_La'] - ['Al_Ka', 'C_Ka', 'Cu_Ka', 'Cu_La', 'Mn_La', 'Zr_La'] - - See also - -------- - set_lines, add_elements, set_elements - - """ - only_lines = utils_eds._parse_only_lines(only_lines) - if "Sample.xray_lines" in self.metadata: - xray_lines = set(self.metadata.Sample.xray_lines) - else: - xray_lines = set() - # Define the elements which Xray lines has been customized - # So that we don't attempt to add new lines automatically - elements = set() - for line in xray_lines: - elements.add(line.split("_")[0]) - for line in lines: - try: - element, subshell = line.split("_") - except ValueError: - raise ValueError( - "Invalid line symbol. " - "Please provide a valid line symbol e.g. Fe_Ka") - if element in elements_db: - elements.add(element) - if subshell in elements_db[element]['Atomic_properties' - ]['Xray_lines']: - lines_len = len(xray_lines) - xray_lines.add(line) - if lines_len != len(xray_lines): - _logger.info(f"{line} line added,") - else: - _logger.info(f"{line} line already in.") - else: - raise ValueError( - f"{line} is not a valid line of {element}.") - else: - raise ValueError( - f"{element} is not a valid symbol of an element.") - xray_not_here = self._get_xray_lines_in_spectral_range(xray_lines)[1] - for xray in xray_not_here: - warnings.warn(f"{xray} is not in the data energy range.", - UserWarning) - if "Sample.elements" in self.metadata: - extra_elements = (set(self.metadata.Sample.elements) - - elements) - if extra_elements: - new_lines = self._get_lines_from_elements( - extra_elements, - only_one=only_one, - only_lines=only_lines) - if new_lines: - self.add_lines(list(new_lines) + list(lines)) - self.add_elements(elements) - if not hasattr(self.metadata, 'Sample'): - self.metadata.add_node('Sample') - if "Sample.xray_lines" in self.metadata: - xray_lines = xray_lines.union( - self.metadata.Sample.xray_lines) - self.metadata.Sample.xray_lines = sorted(list(xray_lines)) - - def _get_lines_from_elements(self, - elements, - only_one=False, - only_lines=("a",)): - """Returns the X-ray lines of the given elements in spectral range - of the data. - - Parameters - ---------- - elements : list of strings - A list containing the symbol of the chemical elements. - only_one : bool - If False, add all the lines of each element in the data spectral - range. If True only add the line at the highest energy - above an overvoltage of 2 (< beam energy / 2). - only_lines : {None, list of strings} - If not None, only the given lines will be returned. - - Returns - ------- - list of X-ray lines alphabetically sorted - - """ - - only_lines = utils_eds._parse_only_lines(only_lines) - try: - beam_energy = self._get_beam_energy() - except BaseException: - # Fall back to the high_value of the energy axis - beam_energy = self.axes_manager.signal_axes[0].high_value - lines = [] - elements = [el if isinstance(el, str) else el.decode() - for el in elements] - for element in elements: - # Possible line (existing and excited by electron) - element_lines = [] - for subshell in list(elements_db[element]['Atomic_properties' - ]['Xray_lines'].keys()): - if only_lines and subshell not in only_lines: - continue - element_lines.append(element + "_" + subshell) - element_lines = self._get_xray_lines_in_spectral_range( - element_lines)[0] - if only_one and element_lines: - # Choose the best line - select_this = -1 - element_lines.sort() - for i, line in enumerate(element_lines): - if (self._get_line_energy(line) < beam_energy / 2): - select_this = i - break - element_lines = [element_lines[select_this], ] - - if not element_lines: - _logger.info(f"There is no X-ray line for element {element} " - "in the data spectral range") - else: - lines.extend(element_lines) - lines.sort() - return lines - - def _parse_xray_lines(self, xray_lines, only_one, only_lines): - only_lines = utils_eds._parse_only_lines(only_lines) - xray_lines = self._get_xray_lines(xray_lines, only_one=only_one, - only_lines=only_lines) - xray_lines, xray_not_here = self._get_xray_lines_in_spectral_range( - xray_lines) - for xray in xray_not_here: - warnings.warn(f"{xray} is not in the data energy range. " - "You can remove it with: " - f"`s.metadata.Sample.xray_lines.remove('{xray}')`") - return xray_lines - - def get_lines_intensity(self, - xray_lines=None, - integration_windows=2., - background_windows=None, - plot_result=False, - only_one=True, - only_lines=("a",), - **kwargs): - """Return the intensity map of selected Xray lines. - - The intensities, the number of X-ray counts, are computed by - suming the spectrum over the - different X-ray lines. The sum window width - is calculated from the energy resolution of the detector - as defined in 'energy_resolution_MnKa' of the metadata. - Backgrounds average in provided windows can be subtracted from the - intensities. - - Parameters - ---------- - xray_lines: {None, Iterable* of strings} - If None, - if `metadata.Sample.elements.xray_lines` contains a - list of lines use those. - If `metadata.Sample.elements.xray_lines` is undefined - or empty but `metadata.Sample.elements` is defined, - use the same syntax as `add_line` to select a subset of lines - for the operation. - Alternatively, provide an iterable containing - a list of valid X-ray lines symbols. - * Note that while dictionaries and strings are iterable, - their use is ambiguous and specifically not allowed. - integration_windows: Float or array - If float, the width of the integration windows is the - 'integration_windows_width' times the calculated FWHM of the line. - Else provide an array for which each row corresponds to a X-ray - line. Each row contains the left and right value of the window. - background_windows: None or 2D array of float - If None, no background subtraction. Else, the backgrounds average - in the windows are subtracted from the return intensities. - 'background_windows' provides the position of the windows in - energy. Each line corresponds to a X-ray line. In a line, the two - first values correspond to the limits of the left window and the - two last values correspond to the limits of the right window. - plot_result : bool - If True, plot the calculated line intensities. If the current - object is a single spectrum it prints the result instead. - only_one : bool - If False, use all the lines of each element in the data spectral - range. If True use only the line at the highest energy - above an overvoltage of 2 (< beam energy / 2). - only_lines : {None, list of strings} - If not None, use only the given lines. - kwargs - The extra keyword arguments for plotting. See - `utils.plot.plot_signals` - - Returns - ------- - intensities : list - A list containing the intensities as BaseSignal subclasses. - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.get_lines_intensity(['Mn_Ka'], plot_result=True) - Mn_La at 0.63316 keV : Intensity = 96700.00 - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.plot(['Mn_Ka'], integration_windows=2.1) - >>> s.get_lines_intensity(['Mn_Ka'], - >>> integration_windows=2.1, plot_result=True) - Mn_Ka at 5.8987 keV : Intensity = 53597.00 - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.set_elements(['Mn']) - >>> s.set_lines(['Mn_Ka']) - >>> bw = s.estimate_background_windows() - >>> s.plot(background_windows=bw) - >>> s.get_lines_intensity(background_windows=bw, plot_result=True) - Mn_Ka at 5.8987 keV : Intensity = 46716.00 - - See also - -------- - set_elements, add_elements, estimate_background_windows, - plot - - """ - if xray_lines is not None and \ - (not isinstance(xray_lines, Iterable) or \ - isinstance(xray_lines, (str, dict))): - - raise TypeError( - "xray_lines must be a compatible iterable, but was " - f"mistakenly provided as a {type(xray_lines)}.") - - xray_lines = self._parse_xray_lines(xray_lines, only_one, only_lines) - if hasattr(integration_windows, '__iter__') is False: - integration_windows = self.estimate_integration_windows( - windows_width=integration_windows, xray_lines=xray_lines) - intensities = [] - ax = self.axes_manager.signal_axes[0] - # test Signal1D (0D problem) - # signal_to_index = self.axes_manager.navigation_dimension - 2 - for i, (Xray_line, window) in enumerate( - zip(xray_lines, integration_windows)): - element, line = utils_eds._get_element_and_line(Xray_line) - line_energy = self._get_line_energy(Xray_line) - img = self.isig[window[0]:window[1]].integrate1D(-1) - if np.issubdtype(img.data.dtype, np.integer): - # The operations below require a float dtype with the default - # numpy casting rule ('same_kind') - img.change_dtype("float") - if background_windows is not None: - bw = background_windows[i] - # TODO: test to prevent slicing bug. To be reomved when fixed - indexes = [float(ax.value2index(de)) - for de in list(bw) + window] - if indexes[0] == indexes[1]: - bck1 = self.isig[bw[0]] - else: - bck1 = self.isig[bw[0]:bw[1]].integrate1D(-1) - if indexes[2] == indexes[3]: - bck2 = self.isig[bw[2]] - else: - bck2 = self.isig[bw[2]:bw[3]].integrate1D(-1) - corr_factor = (indexes[5] - indexes[4]) / ( - (indexes[1] - indexes[0]) + (indexes[3] - indexes[2])) - img = img - (bck1 + bck2) * corr_factor - img.metadata.General.title = ( - f'X-ray line intensity of {self.metadata.General.title}: ' - f'{Xray_line} at {line_energy:.2f} ' - f'{self.axes_manager.signal_axes[0].units}') - img.axes_manager.set_signal_dimension(0) - if plot_result and img.axes_manager.navigation_size == 1: - if img._lazy: - img.compute() - print(f"{Xray_line} at {line_energy} {ax.units} : " - f"Intensity = {img.data[0]:.2f}") - img.metadata.set_item("Sample.elements", ([element])) - img.metadata.set_item("Sample.xray_lines", ([Xray_line])) - intensities.append(img) - if plot_result and img.axes_manager.navigation_size != 1: - utils.plot.plot_signals(intensities, **kwargs) - return intensities - - def get_take_off_angle(self): - """Calculate the take-off-angle (TOA). - - TOA is the angle with which the X-rays leave the surface towards - the detector. Parameters are read in 'SEM.Stage.tilt_alpha', - 'Acquisition_instrument.SEM.Detector.EDS.azimuth_angle' and - 'SEM.Detector.EDS.elevation_angle' and 'SEM.Stage.tilt_beta in - 'metadata'. - - Returns - ------- - take_off_angle: float - in Degree - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.get_take_off_angle() - 37.0 - >>> s.set_microscope_parameters(tilt_stage=20.) - >>> s.get_take_off_angle() - 57.0 - - See also - -------- - hs.eds.take_off_angle - """ - if self.metadata.Signal.signal_type == "EDS_SEM": - mp = self.metadata.Acquisition_instrument.SEM - elif self.metadata.Signal.signal_type == "EDS_TEM": - mp = self.metadata.Acquisition_instrument.TEM - - tilt_stage = mp.get_item('Stage.tilt_alpha', None) - azimuth_angle = mp.get_item('Detector.EDS.azimuth_angle', None) - elevation_angle = mp.get_item('Detector.EDS.elevation_angle', None) - beta_tilt = mp.get_item('Stage.tilt_beta', 0.0) - - return utils.eds.take_off_angle( - tilt_stage, - azimuth_angle, - elevation_angle, - beta_tilt - ) - - def estimate_integration_windows(self, - windows_width=2., - xray_lines=None): - """ - Estimate a window of integration for each X-ray line. - - Parameters - ---------- - windows_width: float - The width of the integration windows is the 'windows_width' times - the calculated FWHM of the line. - xray_lines: None or list of string - If None, use 'metadata.Sample.elements.xray_lines'. Else, - provide an iterable containing a list of valid X-ray lines - symbols. - - Return - ------ - integration_windows: 2D array of float - The positions of the windows in energy. Each row corresponds to a - X-ray line. Each row contains the left and right value of the - window. - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.add_lines() - >>> iw = s.estimate_integration_windows() - >>> s.plot(integration_windows=iw) - >>> s.get_lines_intensity(integration_windows=iw, plot_result=True) - Fe_Ka at 6.4039 keV : Intensity = 3710.00 - Pt_La at 9.4421 keV : Intensity = 15872.00 - - See also - -------- - plot, get_lines_intensity - """ - xray_lines = self._get_xray_lines(xray_lines) - integration_windows = [] - for Xray_line in xray_lines: - line_energy, line_FWHM = self._get_line_energy(Xray_line, - FWHM_MnKa='auto') - element, line = utils_eds._get_element_and_line(Xray_line) - det = windows_width * line_FWHM / 2. - integration_windows.append([line_energy - det, line_energy + det]) - return integration_windows - - def estimate_background_windows(self, - line_width=[2, 2], - windows_width=1, - xray_lines=None): - """ - Estimate two windows around each X-ray line containing only the - background. - - Parameters - ---------- - line_width: list of two floats - The position of the two windows around the X-ray line is given by - the `line_width` (left and right) times the calculated FWHM of the - line. - windows_width: float - The width of the windows is is the `windows_width` times the - calculated FWHM of the line. - xray_lines: None or list of string - If None, use `metadata.Sample.elements.xray_lines`. Else, - provide an iterable containing a list of valid X-ray lines - symbols. - - Return - ------ - windows_position: 2D array of float - The position of the windows in energy. Each line corresponds to a - X-ray line. In a line, the two first values correspond to the - limits of the left window and the two last values correspond to - the limits of the right window. - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.add_lines() - >>> bw = s.estimate_background_windows(line_width=[5.0, 2.0]) - >>> s.plot(background_windows=bw) - >>> s.get_lines_intensity(background_windows=bw, plot_result=True) - Fe_Ka at 6.4039 keV : Intensity = 2754.00 - Pt_La at 9.4421 keV : Intensity = 15090.00 - - See also - -------- - plot, get_lines_intensity - """ - xray_lines = self._get_xray_lines(xray_lines) - windows_position = [] - for xray_line in xray_lines: - line_energy, line_FWHM = self._get_line_energy(xray_line, - FWHM_MnKa='auto') - tmp = [ - line_energy - line_FWHM * line_width[0] - - line_FWHM * windows_width, - line_energy - line_FWHM * line_width[0], - line_energy + line_FWHM * line_width[1], - line_energy + line_FWHM * line_width[1] + - line_FWHM * windows_width - ] - windows_position.append(tmp) - windows_position = np.array(windows_position) - # merge ovelapping windows - index = windows_position.argsort(axis=0)[:, 0] - for i in range(len(index) - 1): - ia, ib = index[i], index[i + 1] - if windows_position[ia, 2] > windows_position[ib, 0]: - interv = np.append(windows_position[ia, :2], - windows_position[ib, 2:]) - windows_position[ia] = interv - windows_position[ib] = interv - return windows_position - - def plot(self, - xray_lines=False, - only_lines=("a", "b"), - only_one=False, - background_windows=None, - integration_windows=None, - navigator="auto", - plot_markers=True, - autoscale='v', - norm="auto", - axes_manager=None, - navigator_kwds={}, - **kwargs): - """Plot the EDS spectrum. The following markers can be added - - - The position of the X-ray lines and their names. - - The background windows associated with each X-ray lines. A black line - links the left and right window with the average value in each window. - - Parameters - ---------- - xray_lines: {False, True, 'from_elements', list of string} - If not False, indicate the position and the name of the X-ray - lines. - If True, if `metadata.Sample.elements.xray_lines` contains a - list of lines use those. If `metadata.Sample.elements.xray_lines` - is undefined or empty or if xray_lines equals 'from_elements' and - `metadata.Sample.elements` is defined, use the same syntax as - `add_line` to select a subset of lines for the operation. - Alternatively, provide an iterable containing a list of valid X-ray - lines symbols. - only_lines : None or list of strings - If not None, use only the given lines (eg. ('a','Kb')). - If None, use all lines. - only_one : bool - If False, use all the lines of each element in the data spectral - range. If True use only the line at the highest energy - above an overvoltage of 2 (< beam energy / 2). - background_windows: None or 2D array of float - If not None, add markers at the position of the windows in energy. - Each line corresponds to a X-ray lines. In a line, the two first - value corresponds to the limit of the left window and the two - last values corresponds to the limit of the right window. - integration_windows: None or 'auto' or float or 2D array of float - If not None, add markers at the position of the integration - windows. - If 'auto' (or float), the width of the integration windows is 2.0 - (or float) times the calculated FWHM of the line. see - 'estimate_integration_windows'. - Else provide an array for which each row corresponds to a X-ray - line. Each row contains the left and right value of the window. - %s - %s - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.plot() - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.plot(True) - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.add_lines() - >>> bw = s.estimate_background_windows() - >>> s.plot(background_windows=bw) - - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s.plot(['Mn_Ka'], integration_windows='auto') - - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.add_lines() - >>> bw = s.estimate_background_windows() - >>> s.plot(background_windows=bw, integration_windows=2.1) - - See also - -------- - set_elements, add_elements, estimate_integration_windows, - get_lines_intensity, estimate_background_windows - """ - super().plot(navigator=navigator, - plot_markers=plot_markers, - autoscale=autoscale, - norm=norm, - axes_manager=axes_manager, - navigator_kwds=navigator_kwds, - **kwargs) - self._plot_xray_lines(xray_lines, only_lines, only_one, - background_windows, integration_windows, - render_figure=False) - self._render_figure(plot=['signal_plot']) - - plot.__doc__ %= (BASE_PLOT_DOCSTRING_PARAMETERS, - PLOT1D_DOCSTRING) - - def _plot_xray_lines(self, xray_lines=False, only_lines=("a", "b"), - only_one=False, background_windows=None, - integration_windows=None, render_figure=True): - if (xray_lines is not False or - background_windows is not None or - integration_windows is not None): - if xray_lines is False: - xray_lines = True - only_lines = utils_eds._parse_only_lines(only_lines) - if xray_lines is True or xray_lines == 'from_elements': - if ('Sample.xray_lines' in self.metadata and - xray_lines != 'from_elements'): - xray_lines = self.metadata.Sample.xray_lines - elif 'Sample.elements' in self.metadata: - xray_lines = self._get_lines_from_elements( - self.metadata.Sample.elements, - only_one=only_one, - only_lines=only_lines) - else: - _logger.warning( - "No elements defined, set them with `add_elements`") - # No X-rays lines, nothing to do then - return - - xray_lines, xray_not_here = self._get_xray_lines_in_spectral_range( - xray_lines) - for xray in xray_not_here: - _logger.warning(f"{xray} is not in the data energy range.") - xray_lines = np.unique(xray_lines) - self.add_xray_lines_markers(xray_lines, - render_figure=False) - if background_windows is not None: - self._add_background_windows_markers(background_windows, - render_figure=False) - if integration_windows is not None: - if integration_windows == 'auto': - integration_windows = 2.0 - if hasattr(integration_windows, '__iter__') is False: - integration_windows = self.estimate_integration_windows( - windows_width=integration_windows, - xray_lines=xray_lines) - self._add_vertical_lines_groups(integration_windows, - linestyle='--', - render_figure=False) - # Render figure only at the end - if render_figure: - self._render_figure(plot=['signal_plot']) - - def _add_vertical_lines_groups(self, position, render_figure=True, - **kwargs): - """ - Add vertical markers for each group that shares the color. - - Parameters - ---------- - position: 2D array of float - The position on the signal axis. Each row corresponds to a - group. - kwargs - keywords argument for markers.vertical_line - """ - per_xray = len(position[0]) - colors = itertools.cycle(np.sort( - plt.rcParams['axes.prop_cycle'].by_key()["color"] * per_xray)) - - for x, color in zip(np.ravel(position), colors): - line = markers.vertical_line(x=x, color=color, **kwargs) - self.add_marker(line, render_figure=False) - if render_figure: - self._render_figure(plot=['signal_plot']) - - def add_xray_lines_markers(self, xray_lines, render_figure=True): - """ - Add marker on a spec.plot() with the name of the selected X-ray - lines - - Parameters - ---------- - xray_lines: list of string - A valid list of X-ray lines - """ - - line_energy = [] - intensity = [] - for xray_line in xray_lines: - element, line = utils_eds._get_element_and_line(xray_line) - line_energy.append(self._get_line_energy(xray_line)) - relative_factor = elements_db[element][ - 'Atomic_properties']['Xray_lines'][line]['weight'] - a_eng = self._get_line_energy(f'{element}_{line[0]}a') - idx = self.axes_manager.signal_axes[0].value2index(a_eng) - intensity.append(self.data[..., idx] * relative_factor) - for i in range(len(line_energy)): - line = markers.vertical_line_segment( - x=line_energy[i], y1=None, y2=intensity[i] * 0.8) - self.add_marker(line, render_figure=False) - string = (r'$\mathrm{%s}_{\mathrm{%s}}$' % - utils_eds._get_element_and_line(xray_lines[i])) - text = markers.text( - x=line_energy[i], y=intensity[i] * 1.1, text=string, - rotation=90) - self.add_marker(text, render_figure=False) - self._xray_markers[xray_lines[i]] = [line, text] - line.events.closed.connect(self._xray_marker_closed) - text.events.closed.connect(self._xray_marker_closed) - if render_figure: - self._render_figure(plot=['signal_plot']) - - def _xray_marker_closed(self, obj): - marker = obj - for xray_line, line_markers in reversed(list( - self._xray_markers.items())): - if marker in line_markers: - line_markers.remove(marker) - if not line_markers: - self._xray_markers.pop(xray_line) - - def remove_xray_lines_markers(self, xray_lines, render_figure=True): - """ - Remove marker previosuly added on a spec.plot() with the name of the - selected X-ray lines - - Parameters - ---------- - xray_lines: list of string - A valid list of X-ray lines to remove - """ - for xray_line in xray_lines: - if xray_line in self._xray_markers: - line_markers = self._xray_markers[xray_line] - while line_markers: - m = line_markers.pop() - m.close(render_figure=False) - if render_figure: - self._render_figure(plot=['signal_plot']) - - def _add_background_windows_markers(self, windows_position, - render_figure=True): - """ - Plot the background windows associated with each X-ray lines. - - For X-ray lines, a black line links the left and right window with the - average value in each window. - - Parameters - ---------- - windows_position: 2D array of float - The position of the windows in energy. Each line corresponds to a - X-ray lines. In a line, the two first value corresponds to the - limit of the left window and the two last values corresponds to the - limit of the right window. - - See also - -------- - estimate_background_windows, get_lines_intensity - """ - self._add_vertical_lines_groups(windows_position) - ax = self.axes_manager.signal_axes[0] - for bw in windows_position: - # TODO: test to prevent slicing bug. To be reomved when fixed - if ax.value2index(bw[0]) == ax.value2index(bw[1]): - y1 = self.isig[bw[0]].data - else: - y1 = self.isig[bw[0]:bw[1]].mean(-1).data - if ax.value2index(bw[2]) == ax.value2index(bw[3]): - y2 = self.isig[bw[2]].data - else: - y2 = self.isig[bw[2]:bw[3]].mean(-1).data - line = markers.line_segment( - x1=(bw[0] + bw[1]) / 2., x2=(bw[2] + bw[3]) / 2., - y1=y1, y2=y2, color='black') - self.add_marker(line, render_figure=False) - if render_figure: - self._render_figure(plot=['signal_plot']) - - -class LazyEDSSpectrum(EDSSpectrum, LazySignal1D): - pass diff --git a/hyperspy/_signals/eds_sem.py b/hyperspy/_signals/eds_sem.py deleted file mode 100644 index caf67583a2..0000000000 --- a/hyperspy/_signals/eds_sem.py +++ /dev/null @@ -1,310 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import logging - -import traits.api as t - -from hyperspy._signals.eds import (EDSSpectrum, LazyEDSSpectrum) -from hyperspy.defaults_parser import preferences -from hyperspy.ui_registry import add_gui_method, DISPLAY_DT, TOOLKIT_DT -from hyperspy.signal import BaseSetMetadataItems - - -_logger = logging.getLogger(__name__) - - -@add_gui_method(toolkey="hyperspy.microscope_parameters_EDS_SEM") -class EDSSEMParametersUI(BaseSetMetadataItems): - - beam_energy = t.Float(t.Undefined, - label='Beam energy (keV)') - live_time = t.Float(t.Undefined, - label='Live time (s)') - tilt_stage = t.Float(t.Undefined, - label='Stage tilt (degree)') - azimuth_angle = t.Float(t.Undefined, - label='Azimuth angle (degree)') - elevation_angle = t.Float(t.Undefined, - label='Elevation angle (degree)') - energy_resolution_MnKa = t.Float(t.Undefined, - label='Energy resolution MnKa (eV)') - mapping = { - 'Acquisition_instrument.SEM.beam_energy': 'beam_energy', - 'Acquisition_instrument.TEM.Stage.tilt_alpha': 'tilt_stage', - 'Acquisition_instrument.SEM.Detector.EDS.live_time': - 'live_time', - 'Acquisition_instrument.SEM.Detector.EDS.azimuth_angle': - 'azimuth_angle', - 'Acquisition_instrument.SEM.Detector.EDS.elevation_angle': - 'elevation_angle', - 'Acquisition_instrument.SEM.Detector.EDS.energy_resolution_MnKa': - 'energy_resolution_MnKa', } - - -class EDSSEMSpectrum(EDSSpectrum): - - _signal_type = "EDS_SEM" - - def __init__(self, *args, **kwards): - super().__init__(*args, **kwards) - # Attributes defaults - if 'Acquisition_instrument.SEM.Detector.EDS' not in self.metadata: - if 'Acquisition_instrument.TEM' in self.metadata: - self.metadata.set_item( - "Acquisition_instrument.SEM", - self.metadata.Acquisition_instrument.TEM) - del self.metadata.Acquisition_instrument.TEM - self._set_default_param() - - def get_calibration_from(self, ref, nb_pix=1): - """Copy the calibration and all metadata of a reference. - - Primary use: To add a calibration to ripple file from INCA - software - - Parameters - ---------- - ref : signal - The reference contains the calibration in its - metadata - nb_pix : int - The live time (real time corrected from the "dead time") - is divided by the number of pixel (spectrums), giving an - average live time. - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - - Examples - -------- - >>> ref = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> s = hs.signals.EDSSEMSpectrum( - >>> hs.datasets.example_signals.EDS_SEM_Spectrum().data) - >>> print(s.axes_manager[0].scale) - >>> s.get_calibration_from(ref) - >>> print(s.axes_manager[0].scale) - 1.0 - 0.01 - - """ - - self.original_metadata = ref.original_metadata.deepcopy() - # Setup the axes_manager - ax_m = self.axes_manager.signal_axes[0] - ax_ref = ref.axes_manager.signal_axes[0] - for _axis in [ax_m, ax_ref]: - if not _axis.is_uniform: - raise NotImplementedError( - "The function is not implemented for non-uniform axes.") - ax_m.scale = ax_ref.scale - ax_m.units = ax_ref.units - ax_m.offset = ax_ref.offset - - # Setup metadata - if 'Acquisition_instrument.SEM' in ref.metadata: - mp_ref = ref.metadata.Acquisition_instrument.SEM - elif 'Acquisition_instrument.TEM' in ref.metadata: - mp_ref = ref.metadata.Acquisition_instrument.TEM - else: - raise ValueError( - "The reference has no metadata.Acquisition_instrument.TEM" - "\n nor metadata.Acquisition_instrument.SEM ") - - mp = self.metadata - - mp.Acquisition_instrument.SEM = mp_ref.deepcopy() - - if hasattr(mp_ref.Detector.EDS, 'live_time'): - mp.Acquisition_instrument.SEM.Detector.EDS.live_time = \ - mp_ref.Detector.EDS.live_time / nb_pix - - def _load_from_TEM_param(self): - """Transfer metadata.Acquisition_instrument.TEM to - metadata.Acquisition_instrument.SEM - - """ - - mp = self.metadata - if mp.has_item('Acquisition_instrument.SEM') is False: - mp.add_node('Acquisition_instrument.SEM') - if mp.has_item('Acquisition_instrument.SEM.Detector.EDS') is False: - mp.Acquisition_instrument.SEM.add_node('EDS') - mp.Signal.signal_type = "EDS_SEM" - - # Transfer - if 'Acquisition_instrument.TEM' in mp: - mp.Acquisition_instrument.SEM = mp.Acquisition_instrument.TEM - del mp.Acquisition_instrument.TEM - - def _set_default_param(self): - """Set to value to default (defined in preferences) - - """ - mp = self.metadata - if "Acquisition_instrument.SEM.Stage.tilt_alpha" not in mp: - mp.set_item( - "Acquisition_instrument.SEM.Stage.tilt_alpha", - preferences.EDS.eds_tilt_stage) - if "Acquisition_instrument.SEM.Detector.EDS.elevation_angle" not in mp: - mp.set_item( - "Acquisition_instrument.SEM.Detector.EDS.elevation_angle", - preferences.EDS.eds_detector_elevation) - if "Acquisition_instrument.SEM.Detector.EDS.energy_resolution_MnKa" \ - not in mp: - mp.set_item( - "Acquisition_instrument.SEM.Detector.EDS." - "energy_resolution_MnKa", - preferences.EDS.eds_mn_ka) - if "Acquisition_instrument.SEM.Detector.EDS.azimuth_angle" not in mp: - mp.set_item( - "Acquisition_instrument.SEM.Detector.EDS.azimuth_angle", - preferences.EDS.eds_detector_azimuth) - - def set_microscope_parameters(self, - beam_energy=None, - live_time=None, - tilt_stage=None, - azimuth_angle=None, - elevation_angle=None, - energy_resolution_MnKa=None, - display=True, toolkit=None): - if set([beam_energy, live_time, tilt_stage, azimuth_angle, - elevation_angle, energy_resolution_MnKa]) == {None}: - tem_par = EDSSEMParametersUI(self) - return tem_par.gui(toolkit=toolkit, display=display) - md = self.metadata - - if beam_energy is not None: - md.set_item("Acquisition_instrument.SEM.beam_energy", beam_energy) - if live_time is not None: - md.set_item( - "Acquisition_instrument.SEM.Detector.EDS.live_time", - live_time) - if tilt_stage is not None: - md.set_item( - "Acquisition_instrument.SEM.Stage.tilt_alpha", - tilt_stage) - if azimuth_angle is not None: - md.set_item( - "Acquisition_instrument.SEM.Detector.EDS.azimuth_angle", - azimuth_angle) - if elevation_angle is not None: - md.set_item( - "Acquisition_instrument.SEM.Detector.EDS.elevation_angle", - elevation_angle) - if energy_resolution_MnKa is not None: - md.set_item( - "Acquisition_instrument.SEM.Detector.EDS." - "energy_resolution_MnKa", - energy_resolution_MnKa) - set_microscope_parameters.__doc__ = \ - """ - Set the microscope parameters. - - If no arguments are given, raises an interactive mode to fill - the values. - - Parameters - ---------- - beam_energy: float - The energy of the electron beam in keV - live_time : float - In second - tilt_stage : float - In degree - azimuth_angle : float - In degree - elevation_angle : float - In degree - energy_resolution_MnKa : float - In eV - {} - {} - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_SEM_Spectrum() - >>> print('Default value %s eV' % - >>> s.metadata.Acquisition_instrument. - >>> SEM.Detector.EDS.energy_resolution_MnKa) - >>> s.set_microscope_parameters(energy_resolution_MnKa=135.) - >>> print('Now set to %s eV' % - >>> s.metadata.Acquisition_instrument. - >>> SEM.Detector.EDS.energy_resolution_MnKa) - Default value 130.0 eV - Now set to 135.0 eV - - """.format(DISPLAY_DT, TOOLKIT_DT) - - def _are_microscope_parameters_missing(self): - """Check if the EDS parameters necessary for quantification - are defined in metadata. If not, in interactive mode - raises an UI item to fill the values - - """ - must_exist = ( - 'Acquisition_instrument.SEM.beam_energy', - 'Acquisition_instrument.SEM.Detector.EDS.live_time', ) - - missing_parameters = [] - for item in must_exist: - exists = self.metadata.has_item(item) - if exists is False: - missing_parameters.append(item) - if missing_parameters: - _logger.info("Missing parameters {}".format(missing_parameters)) - return True - else: - return False - - def create_model(self, auto_background=True, auto_add_lines=True, - *args, **kwargs): - """Create a model for the current SEM EDS data. - - Parameters - ---------- - auto_background : boolean, default True - If True, adds automatically a polynomial order 6 to the model, - using the edsmodel.add_polynomial_background method. - auto_add_lines : boolean, default True - If True, automatically add Gaussians for all X-rays generated in - the energy range by an element using the edsmodel.add_family_lines - method. - dictionary : {None, dict}, optional - A dictionary to be used to recreate a model. Usually generated - using :meth:`hyperspy.model.as_dictionary` - - Returns - ------- - - model : `EDSSEMModel` instance. - - """ - from hyperspy.models.edssemmodel import EDSSEMModel - model = EDSSEMModel(self, - auto_background=auto_background, - auto_add_lines=auto_add_lines, - *args, **kwargs) - return model - - -class LazyEDSSEMSpectrum(EDSSEMSpectrum, LazyEDSSpectrum): - pass diff --git a/hyperspy/_signals/eds_tem.py b/hyperspy/_signals/eds_tem.py deleted file mode 100755 index fa26131478..0000000000 --- a/hyperspy/_signals/eds_tem.py +++ /dev/null @@ -1,938 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - - -import warnings -import logging - -import traits.api as t -import numpy as np -from scipy import constants -import pint - -from hyperspy.signal import BaseSetMetadataItems -from hyperspy import utils -from hyperspy._signals.eds import (EDSSpectrum, LazyEDSSpectrum) -from hyperspy.defaults_parser import preferences -from hyperspy.ui_registry import add_gui_method, DISPLAY_DT, TOOLKIT_DT -from hyperspy.misc.eds import utils as utils_eds -from hyperspy.misc.elements import elements as elements_db -from hyperspy.misc.utils import isiterable -from hyperspy.external.progressbar import progressbar -from hyperspy.axes import DataAxis - -_logger = logging.getLogger(__name__) - - -@add_gui_method(toolkey="hyperspy.microscope_parameters_EDS_TEM") -class EDSTEMParametersUI(BaseSetMetadataItems): - beam_energy = t.Float(t.Undefined, - label='Beam energy (keV)') - real_time = t.Float(t.Undefined, - label='Real time (s)') - tilt_stage = t.Float(t.Undefined, - label='Stage tilt (degree)') - live_time = t.Float(t.Undefined, - label='Live time (s)') - probe_area = t.Float(t.Undefined, - label='Beam/probe area (nm²)') - azimuth_angle = t.Float(t.Undefined, - label='Azimuth angle (degree)') - elevation_angle = t.Float(t.Undefined, - label='Elevation angle (degree)') - energy_resolution_MnKa = t.Float(t.Undefined, - label='Energy resolution MnKa (eV)') - beam_current = t.Float(t.Undefined, - label='Beam current (nA)') - mapping = { - 'Acquisition_instrument.TEM.beam_energy': 'beam_energy', - 'Acquisition_instrument.TEM.Stage.tilt_alpha': 'tilt_stage', - 'Acquisition_instrument.TEM.Detector.EDS.live_time': 'live_time', - 'Acquisition_instrument.TEM.Detector.EDS.azimuth_angle': - 'azimuth_angle', - 'Acquisition_instrument.TEM.Detector.EDS.elevation_angle': - 'elevation_angle', - 'Acquisition_instrument.TEM.Detector.EDS.energy_resolution_MnKa': - 'energy_resolution_MnKa', - 'Acquisition_instrument.TEM.beam_current': - 'beam_current', - 'Acquisition_instrument.TEM.probe_area': - 'probe_area', - 'Acquisition_instrument.TEM.Detector.EDS.real_time': - 'real_time', } - - -class EDSTEMSpectrum(EDSSpectrum): - - _signal_type = "EDS_TEM" - - def __init__(self, *args, **kwards): - super().__init__(*args, **kwards) - # Attributes defaults - if 'Acquisition_instrument.TEM.Detector.EDS' not in self.metadata: - if 'Acquisition_instrument.SEM.Detector.EDS' in self.metadata: - self.metadata.set_item( - "Acquisition_instrument.TEM", - self.metadata.Acquisition_instrument.SEM) - del self.metadata.Acquisition_instrument.SEM - self._set_default_param() - - def _set_default_param(self): - """Set to value to default (defined in preferences) - """ - - mp = self.metadata - mp.Signal.signal_type = "EDS_TEM" - - mp = self.metadata - if "Acquisition_instrument.TEM.Stage.tilt_alpha" not in mp: - mp.set_item( - "Acquisition_instrument.TEM.Stage.tilt_alpha", - preferences.EDS.eds_tilt_stage) - if "Acquisition_instrument.TEM.Detector.EDS.elevation_angle" not in mp: - mp.set_item( - "Acquisition_instrument.TEM.Detector.EDS.elevation_angle", - preferences.EDS.eds_detector_elevation) - if "Acquisition_instrument.TEM.Detector.EDS.energy_resolution_MnKa"\ - not in mp: - mp.set_item("Acquisition_instrument.TEM.Detector.EDS." + - "energy_resolution_MnKa", - preferences.EDS.eds_mn_ka) - if "Acquisition_instrument.TEM.Detector.EDS.azimuth_angle" not in mp: - mp.set_item( - "Acquisition_instrument.TEM.Detector.EDS.azimuth_angle", - preferences.EDS.eds_detector_azimuth) - - def set_microscope_parameters(self, - beam_energy=None, - live_time=None, - tilt_stage=None, - azimuth_angle=None, - elevation_angle=None, - energy_resolution_MnKa=None, - beam_current=None, - probe_area=None, - real_time=None, - display=True, - toolkit=None): - if set([beam_energy, live_time, tilt_stage, azimuth_angle, - elevation_angle, energy_resolution_MnKa, beam_current, - probe_area, real_time]) == {None}: - tem_par = EDSTEMParametersUI(self) - return tem_par.gui(display=display, toolkit=toolkit) - md = self.metadata - - if beam_energy is not None: - md.set_item("Acquisition_instrument.TEM.beam_energy ", beam_energy) - if live_time is not None: - md.set_item( - "Acquisition_instrument.TEM.Detector.EDS.live_time", - live_time) - if tilt_stage is not None: - md.set_item( - "Acquisition_instrument.TEM.Stage.tilt_alpha", - tilt_stage) - if azimuth_angle is not None: - md.set_item( - "Acquisition_instrument.TEM.Detector.EDS.azimuth_angle", - azimuth_angle) - if elevation_angle is not None: - md.set_item( - "Acquisition_instrument.TEM.Detector.EDS.elevation_angle", - elevation_angle) - if energy_resolution_MnKa is not None: - md.set_item( - "Acquisition_instrument.TEM.Detector.EDS." + - "energy_resolution_MnKa", - energy_resolution_MnKa) - if beam_current is not None: - md.set_item( - "Acquisition_instrument.TEM.beam_current", - beam_current) - if probe_area is not None: - md.set_item( - "Acquisition_instrument.TEM.probe_area", - probe_area) - if real_time is not None: - md.set_item( - "Acquisition_instrument.TEM.Detector.EDS.real_time", - real_time) - - set_microscope_parameters.__doc__ = \ - """ - Set the microscope parameters. - - If no arguments are given, raises an interactive mode to fill - the values. - - Parameters - ---------- - beam_energy: float - The energy of the electron beam in keV - live_time : float - In seconds - tilt_stage : float - In degree - azimuth_angle : float - In degree - elevation_angle : float - In degree - energy_resolution_MnKa : float - In eV - beam_current: float - In nA - probe_area: float - In nm² - real_time: float - In seconds - {} - {} - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> print(s.metadata.Acquisition_instrument. - >>> TEM.Detector.EDS.energy_resolution_MnKa) - >>> s.set_microscope_parameters(energy_resolution_MnKa=135.) - >>> print(s.metadata.Acquisition_instrument. - >>> TEM.Detector.EDS.energy_resolution_MnKa) - 133.312296 - 135.0 - - """.format(DISPLAY_DT, TOOLKIT_DT) - - def _are_microscope_parameters_missing(self): - """Check if the EDS parameters necessary for quantification are - defined in metadata.""" - must_exist = ( - 'Acquisition_instrument.TEM.beam_energy', - 'Acquisition_instrument.TEM.Detector.EDS.live_time',) - - missing_parameters = [] - for item in must_exist: - exists = self.metadata.has_item(item) - if exists is False: - missing_parameters.append(item) - if missing_parameters: - _logger.info("Missing parameters {}".format(missing_parameters)) - return True - else: - return False - - def get_calibration_from(self, ref, nb_pix=1): - """Copy the calibration and all metadata of a reference. - - Primary use: To add a calibration to ripple file from INCA - software - - Parameters - ---------- - ref : signal - The reference contains the calibration in its - metadata - nb_pix : int - The live time (real time corrected from the "dead time") - is divided by the number of pixel (spectrums), giving an - average live time. - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - - Examples - -------- - >>> ref = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s = hs.signals.EDSTEMSpectrum( - >>> hs.datasets.example_signals.EDS_TEM_Spectrum().data) - >>> print(s.axes_manager[0].scale) - >>> s.get_calibration_from(ref) - >>> print(s.axes_manager[0].scale) - 1.0 - 0.020028 - - """ - - self.original_metadata = ref.original_metadata.deepcopy() - # Setup the axes_manager - ax_m = self.axes_manager.signal_axes[0] - ax_ref = ref.axes_manager.signal_axes[0] - for _axis in [ax_m, ax_ref]: - if not _axis.is_uniform: - raise NotImplementedError( - "The function is not implemented for non-uniform axes.") - ax_m.scale = ax_ref.scale - ax_m.units = ax_ref.units - ax_m.offset = ax_ref.offset - - # Setup metadata - if 'Acquisition_instrument.TEM' in ref.metadata: - mp_ref = ref.metadata.Acquisition_instrument.TEM - elif 'Acquisition_instrument.SEM' in ref.metadata: - mp_ref = ref.metadata.Acquisition_instrument.SEM - else: - raise ValueError("The reference has no metadata " - "'Acquisition_instrument.TEM '" - "or 'metadata.Acquisition_instrument.SEM'.") - - mp = self.metadata - mp.Acquisition_instrument.TEM = mp_ref.deepcopy() - if mp_ref.has_item("Detector.EDS.live_time"): - mp.Acquisition_instrument.TEM.Detector.EDS.live_time = \ - mp_ref.Detector.EDS.live_time / nb_pix - - def quantification(self, - intensities, - method, - factors, - composition_units='atomic', - absorption_correction=False, - take_off_angle='auto', - thickness='auto', - convergence_criterion=0.5, - navigation_mask=1.0, - closing=True, - plot_result=False, - probe_area='auto', - max_iterations=30, - show_progressbar=None, - **kwargs): - """ - Absorption corrected quantification using Cliff-Lorimer, the zeta-factor - method, or ionization cross sections. The function iterates through - quantification function until two successive interations don't change - the final composition by a defined percentage critera (0.5% by default). - - Parameters - ---------- - intensities: list of signal - the intensitiy for each X-ray lines. - method: {'CL', 'zeta', 'cross_section'} - Set the quantification method: Cliff-Lorimer, zeta-factor, or - ionization cross sections. - factors: list of float - The list of kfactors, zeta-factors or cross sections in same order - as intensities. Note that intensities provided by Hyperspy are - sorted by the alphabetical order of the X-ray lines. - eg. factors =[0.982, 1.32, 1.60] for ['Al_Ka', 'Cr_Ka', 'Ni_Ka']. - composition_units: {'atomic', 'weight'} - The quantification returns the composition in 'atomic' percent by - default, but can also return weight percent if specified. - absorption_correction: bool - Specify whether or not an absorption correction should be applied. - 'False' by default so absorption will not be applied unless - specfied. - take_off_angle : {'auto'} - The angle between the sample surface and the vector along which - X-rays travel to reach the centre of the detector. - thickness: {'auto'} - thickness in nm (can be a single value or - have the same navigation dimension as the signal). - NB: Must be specified for 'CL' method. For 'zeta' or 'cross_section' - methods, first quantification step provides a mass_thickness - internally during quantification. - convergence_criterion: The convergence criterium defined as the percentage - difference between 2 successive iterations. 0.5% by default. - navigation_mask : None or float or signal - The navigation locations marked as True are not used in the - quantification. If float is given the vacuum_mask method is used to - generate a mask with the float value as threhsold. - Else provides a signal with the navigation shape. Only for the - 'Cliff-Lorimer' method. - closing: bool - If true, applied a morphologic closing to the mask obtained by - vacuum_mask. - plot_result : bool - If True, plot the calculated composition. If the current - object is a single spectrum it prints the result instead. - probe_area = {'auto'} - This allows the user to specify the probe_area for interaction with - the sample needed specifically for the cross_section method of - quantification. When left as 'auto' the pixel area is used, - calculated from the navigation axes information. - max_iterations : int - An upper limit to the number of calculations for absorption correction. - kwargs - The extra keyword arguments are passed to plot. - - Returns - ------- - A list of quantified elemental maps (signal) giving the composition of - the sample in weight or atomic percent with absorption correciton taken - into account based on the sample thickness estimate provided. - - If the method is 'zeta' this function also returns the mass thickness - profile for the data. - - If the method is 'cross_section' this function also returns the atom - counts for each element. - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s.add_lines() - >>> kfactors = [1.450226, 5.075602] #For Fe Ka and Pt La - >>> bw = s.estimate_background_windows(line_width=[5.0, 2.0]) - >>> s.plot(background_windows=bw) - >>> intensities = s.get_lines_intensity(background_windows=bw) - >>> res = s.quantification(intensities, kfactors, plot_result=True, - >>> composition_units='atomic') - Fe (Fe_Ka): Composition = 15.41 atomic percent - Pt (Pt_La): Composition = 84.59 atomic percent - - See also - -------- - vacuum_mask - """ - if isinstance(navigation_mask, float): - if self.axes_manager.navigation_dimension > 0: - navigation_mask = self.vacuum_mask(navigation_mask, closing) - else: - navigation_mask = None - - xray_lines = [intensity.metadata.Sample.xray_lines[0] for intensity in intensities] - it = 0 - if absorption_correction: - if show_progressbar is None: # pragma: no cover - show_progressbar = preferences.General.show_progressbar - if show_progressbar: - pbar = progressbar(total=None, - desc='Absorption correction calculation') - - composition = utils.stack(intensities, lazy=False, - show_progressbar=False) - - if take_off_angle == 'auto': - toa = self.get_take_off_angle() - else: - toa = take_off_angle - - #determining illumination area for cross sections quantification. - if method == 'cross_section': - if probe_area == 'auto': - parameters = self.metadata.Acquisition_instrument.TEM - if probe_area in parameters: - probe_area = parameters.TEM.probe_area - else: - probe_area = self.get_probe_area( - navigation_axes=self.axes_manager.navigation_axes) - - int_stack = utils.stack(intensities, lazy=False, - show_progressbar=False) - comp_old = np.zeros_like(int_stack.data) - - abs_corr_factor = None # initial - - if method == 'CL': - quantification_method = utils_eds.quantification_cliff_lorimer - kwargs = {"intensities" : int_stack.data, - "kfactors" : factors, - "absorption_correction" : abs_corr_factor, - "mask": navigation_mask} - - elif method == 'zeta': - quantification_method = utils_eds.quantification_zeta_factor - kwargs = {"intensities" : int_stack.data, - "zfactors" : factors, - "dose" : self._get_dose(method), - "absorption_correction" : abs_corr_factor} - - elif method =='cross_section': - quantification_method = utils_eds.quantification_cross_section - kwargs = {"intensities" : int_stack.data, - "cross_sections" : factors, - "dose" : self._get_dose(method, **kwargs), - "absorption_correction" : abs_corr_factor} - - else: - raise ValueError('Please specify method for quantification, ' - 'as "CL", "zeta" or "cross_section".') - - while True: - results = quantification_method(**kwargs) - - if method == 'CL': - composition.data = results * 100. - if absorption_correction: - if thickness is not None: - mass_thickness = intensities[0].deepcopy() - mass_thickness.data = self.CL_get_mass_thickness( - composition.split(), - thickness - ) - mass_thickness.metadata.General.title = 'Mass thickness' - else: - raise ValueError( - 'Thickness is required for absorption correction ' - 'with k-factor method. Results will contain no ' - 'correction for absorption.' - ) - - elif method == 'zeta': - composition.data = results[0] * 100 - mass_thickness = intensities[0].deepcopy() - mass_thickness.data = results[1] - - else: - composition.data = results[0] * 100. - number_of_atoms = composition._deepcopy_with_new_data(results[1]) - - if method == 'cross_section': - if absorption_correction: - abs_corr_factor = utils_eds.get_abs_corr_cross_section(composition.split(), - number_of_atoms.split(), - toa, - probe_area) - kwargs["absorption_correction"] = abs_corr_factor - else: - if absorption_correction: - abs_corr_factor = utils_eds.get_abs_corr_zeta(composition.split(), - mass_thickness, - toa) - kwargs["absorption_correction"] = abs_corr_factor - - res_max = np.max(composition.data - comp_old) - comp_old = composition.data - - if absorption_correction and show_progressbar: - pbar.update(1) - it += 1 - if not absorption_correction or abs(res_max) < convergence_criterion: - break - elif it >= max_iterations: - raise Exception('Absorption correction failed as solution ' - f'did not converge after {max_iterations} ' - 'iterations') - - if method == 'cross_section': - number_of_atoms = composition._deepcopy_with_new_data(results[1]) - number_of_atoms = number_of_atoms.split() - composition = composition.split() - else: - composition = composition.split() - - #convert ouput units to selection as required. - if composition_units == 'atomic': - if method != 'cross_section': - composition = utils.material.weight_to_atomic(composition) - else: - if method == 'cross_section': - composition = utils.material.atomic_to_weight(composition) - - #Label each of the elemental maps in the image stacks for composition. - for i, xray_line in enumerate(xray_lines): - element, line = utils_eds._get_element_and_line(xray_line) - composition[i].metadata.General.title = composition_units + \ - ' percent of ' + element - composition[i].metadata.set_item("Sample.elements", ([element])) - composition[i].metadata.set_item( - "Sample.xray_lines", ([xray_line])) - if plot_result and composition[i].axes_manager.navigation_size == 1: - c = float(composition[i].data) - print(f"{element} ({xray_line}): Composition = {c:.2f} percent") - #For the cross section method this is repeated for the number of atom maps - if method == 'cross_section': - for i, xray_line in enumerate(xray_lines): - element, line = utils_eds._get_element_and_line(xray_line) - number_of_atoms[i].metadata.General.title = \ - 'atom counts of ' + element - number_of_atoms[i].metadata.set_item("Sample.elements", - ([element])) - number_of_atoms[i].metadata.set_item( - "Sample.xray_lines", ([xray_line])) - if plot_result and composition[i].axes_manager.navigation_size != 1: - utils.plot.plot_signals(composition, **kwargs) - - if absorption_correction: - _logger.info(f'Conversion found after {it} interations.') - - if method == 'zeta': - mass_thickness.metadata.General.title = 'Mass thickness' - self.metadata.set_item("Sample.mass_thickness", mass_thickness) - return composition, mass_thickness - elif method == 'cross_section': - return composition, number_of_atoms - elif method == 'CL': - if absorption_correction: - mass_thickness.metadata.General.title = 'Mass thickness' - return composition, mass_thickness - else: - return composition - else: - raise ValueError('Please specify method for quantification, as ' - '"CL", "zeta" or "cross_section"') - - - def vacuum_mask(self, threshold=1.0, closing=True, opening=False): - """ - Generate mask of the vacuum region - - Parameters - ---------- - threshold: float - For a given pixel, maximum value in the energy axis below which the - pixel is considered as vacuum. - closing: bool - If true, applied a morphologic closing to the mask - opnening: bool - If true, applied a morphologic opening to the mask - - Returns - ------- - mask: signal - The mask of the region - - Examples - -------- - >>> # Simulate a spectrum image with vacuum region - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> s_vac = hs.signals.BaseSignal( - np.ones_like(s.data, dtype=float))*0.005 - >>> s_vac.add_poissonian_noise() - >>> si = hs.stack([s]*3 + [s_vac]) - >>> si.vacuum_mask().data - array([False, False, False, True], dtype=bool) - """ - if self.axes_manager.navigation_dimension == 0: - raise RuntimeError('Navigation dimenstion must be higher than 0 ' - 'to estimate a vacuum mask.') - from scipy.ndimage.morphology import binary_dilation, binary_erosion - mask = (self.max(-1) <= threshold) - if closing: - mask.data = binary_dilation(mask.data, border_value=0) - mask.data = binary_erosion(mask.data, border_value=1) - if opening: - mask.data = binary_erosion(mask.data, border_value=1) - mask.data = binary_dilation(mask.data, border_value=0) - return mask - - def decomposition(self, - normalize_poissonian_noise=True, - navigation_mask=1.0, - closing=True, - *args, - **kwargs): - """Apply a decomposition to a dataset with a choice of algorithms. - - The results are stored in ``self.learning_results``. - - Read more in the :ref:`User Guide `. - - Parameters - ---------- - normalize_poissonian_noise : bool, default True - If True, scale the signal to normalize Poissonian noise using - the approach described in [Keenan2004]_. - navigation_mask : None or float or boolean numpy array, default 1.0 - The navigation locations marked as True are not used in the - decomposition. If float is given the vacuum_mask method is used to - generate a mask with the float value as threshold. - closing: bool, default True - If true, applied a morphologic closing to the mask obtained by - vacuum_mask. - algorithm : {"SVD", "MLPCA", "sklearn_pca", "NMF", "sparse_pca", "mini_batch_sparse_pca", "RPCA", "ORPCA", "ORNMF", custom object}, default "SVD" - The decomposition algorithm to use. If algorithm is an object, - it must implement a ``fit_transform()`` method or ``fit()`` and - ``transform()`` methods, in the same manner as a scikit-learn estimator. - output_dimension : None or int - Number of components to keep/calculate. - Default is None, i.e. ``min(data.shape)``. - centre : {None, "navigation", "signal"}, default None - * If None, the data is not centered prior to decomposition. - * If "navigation", the data is centered along the navigation axis. - Only used by the "SVD" algorithm. - * If "signal", the data is centered along the signal axis. - Only used by the "SVD" algorithm. - auto_transpose : bool, default True - If True, automatically transposes the data to boost performance. - Only used by the "SVD" algorithm. - signal_mask : boolean numpy array - The signal locations marked as True are not used in the - decomposition. - var_array : numpy array - Array of variance for the maximum likelihood PCA algorithm. - Only used by the "MLPCA" algorithm. - var_func : None or function or numpy array, default None - * If None, ignored - * If function, applies the function to the data to obtain ``var_array``. - Only used by the "MLPCA" algorithm. - * If numpy array, creates ``var_array`` by applying a polynomial function - defined by the array of coefficients to the data. Only used by - the "MLPCA" algorithm. - reproject : {None, "signal", "navigation", "both"}, default None - If not None, the results of the decomposition will be projected in - the selected masked area. - return_info: bool, default False - The result of the decomposition is stored internally. However, - some algorithms generate some extra information that is not - stored. If True, return any extra information if available. - In the case of sklearn.decomposition objects, this includes the - sklearn Estimator object. - print_info : bool, default True - If True, print information about the decomposition being performed. - In the case of sklearn.decomposition objects, this includes the - values of all arguments of the chosen sklearn algorithm. - svd_solver : {"auto", "full", "arpack", "randomized"}, default "auto" - If auto: - The solver is selected by a default policy based on `data.shape` and - `output_dimension`: if the input data is larger than 500x500 and the - number of components to extract is lower than 80% of the smallest - dimension of the data, then the more efficient "randomized" - method is enabled. Otherwise the exact full SVD is computed and - optionally truncated afterwards. - If full: - run exact SVD, calling the standard LAPACK solver via - :py:func:`scipy.linalg.svd`, and select the components by postprocessing - If arpack: - use truncated SVD, calling ARPACK solver via - :py:func:`scipy.sparse.linalg.svds`. It requires strictly - `0 < output_dimension < min(data.shape)` - If randomized: - use truncated SVD, calling :py:func:`sklearn.utils.extmath.randomized_svd` - to estimate a limited number of components - copy : bool, default True - * If True, stores a copy of the data before any pre-treatments - such as normalization in ``s._data_before_treatments``. The original - data can then be restored by calling ``s.undo_treatments()``. - * If False, no copy is made. This can be beneficial for memory - usage, but care must be taken since data will be overwritten. - **kwargs : extra keyword arguments - Any keyword arguments are passed to the decomposition algorithm. - - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> si = hs.stack([s]*3) - >>> si.change_dtype(float) - >>> si.decomposition() - - See also - -------- - vacuum_mask - """ - if isinstance(navigation_mask, float): - navigation_mask = self.vacuum_mask(navigation_mask, closing) - super().decomposition( - normalize_poissonian_noise=normalize_poissonian_noise, - navigation_mask=navigation_mask, *args, **kwargs) - self.learning_results.loadings = np.nan_to_num( - self.learning_results.loadings) - - def create_model(self, auto_background=True, auto_add_lines=True, - *args, **kwargs): - """Create a model for the current TEM EDS data. - - Parameters - ---------- - auto_background : bool, default True - If True, adds automatically a polynomial order 6 to the model, - using the edsmodel.add_polynomial_background method. - auto_add_lines : bool, default True - If True, automatically add Gaussians for all X-rays generated in - the energy range by an element using the edsmodel.add_family_lines - method. - dictionary : {None, dict}, optional - A dictionary to be used to recreate a model. Usually generated - using :meth:`hyperspy.model.as_dictionary` - - Returns - ------- - model : `EDSTEMModel` instance. - - """ - from hyperspy.models.edstemmodel import EDSTEMModel - model = EDSTEMModel(self, - auto_background=auto_background, - auto_add_lines=auto_add_lines, - *args, **kwargs) - return model - - def get_probe_area(self, navigation_axes=None): - """ - Calculates a pixel area which can be approximated to probe area, - when the beam is larger than or equal to pixel size. - The probe area can be calculated only when the number of navigation - dimension are less than 2 and all the units have the dimensions of - length. - - Parameters - ---------- - navigation_axes : DataAxis, string or integer (or list of) - Navigation axes corresponding to the probe area. If string or - integer, the provided value is used to index the ``axes_manager``. - - Returns - ------- - probe area in nm². - - Examples - -------- - >>> s = hs.datasets.example_signals.EDS_TEM_Spectrum() - >>> si = hs.stack([s]*3) - >>> si.axes_manager.navigation_axes[0].scale = 0.01 - >>> si.axes_manager.navigation_axes[0].units = 'μm' - >>> si.get_probe_area() - 100.0 - - """ - if navigation_axes is None: - navigation_axes = self.axes_manager.navigation_axes - elif not isiterable(navigation_axes): - navigation_axes = [navigation_axes] - if len(navigation_axes) == 0: - raise ValueError("The navigation dimension is zero, the probe " - "area can not be calculated automatically.") - elif len(navigation_axes) > 2: - raise ValueError("The navigation axes corresponding to the probe " - "are ambiguous and the probe area can not be " - "calculated automatically.") - scales = [] - - for axis in navigation_axes: - try: - if not isinstance(navigation_axes, DataAxis): - axis = self.axes_manager[axis] - scales.append(axis.convert_to_units('nm', inplace=False)[0]) - except pint.DimensionalityError: - raise ValueError(f"The unit of the axis {axis} has not the " - "dimension of length.") - - if len(scales) == 1: - probe_area = scales[0] ** 2 - else: - probe_area = scales[0] * scales[1] - - if probe_area == 1: - warnings.warn("Please note that the probe area has been " - "calculated to be 1 nm², meaning that it is highly " - "likley that the scale of the navigation axes have not " - "been set correctly. Please read the user " - "guide for how to set this.") - return probe_area - - - def _get_dose(self, method, beam_current='auto', live_time='auto', - probe_area='auto'): - """ - Calculates the total electron dose for the zeta-factor or cross section - methods of quantification. - - Input given by i*t*N, i the current, t the - acquisition time, and N the number of electron by unit electric charge. - - Parameters - ---------- - method : 'zeta' or 'cross_section' - If 'zeta', the dose is given by i*t*N - If 'cross section', the dose is given by i*t*N/A - where i is the beam current, t is the acquistion time, - N is the number of electrons per unit charge (1/e) and - A is the illuminated beam area or pixel area. - beam_current: float - Probe current in nA - live_time: float - Acquisiton time in s, compensated for the dead time of the detector. - probe_area: float or 'auto' - The illumination area of the electron beam in nm². - If 'auto' the value is extracted from the scale axes_manager. - Therefore we assume the probe is oversampling such that - the illumination area can be approximated to the pixel area of the - spectrum image. - - Returns - -------- - Dose in electrons (zeta factor) or electrons per nm² (cross_section) - - See also - -------- - set_microscope_parameters - """ - - parameters = self.metadata.Acquisition_instrument.TEM - - if beam_current == 'auto': - beam_current = parameters.get_item('beam_current') - if beam_current is None: - raise Exception('Electron dose could not be calculated as the ' - 'beam current is not set. It can set using ' - '`set_microscope_parameters()`.') - - if live_time == 'auto': - live_time = parameters.get_item('Detector.EDS.live_time') - if live_time is None: - raise Exception('Electron dose could not be calculated as ' - 'live time is not set. It can set using ' - '`set_microscope_parameters()`.') - - if method == 'cross_section': - if probe_area == 'auto': - probe_area = parameters.get_item('probe_area') - if probe_area is None: - probe_area = self.get_probe_area( - navigation_axes=self.axes_manager.navigation_axes) - return (live_time * beam_current * 1e-9) / (constants.e * probe_area) - # 1e-9 is included here because the beam_current is in nA. - elif method == 'zeta': - return live_time * beam_current * 1e-9 / constants.e - else: - raise Exception("Method need to be 'zeta' or 'cross_section'.") - - - @staticmethod - def CL_get_mass_thickness(weight_percent, thickness): - """ - Creates a array of mass_thickness based on a known material composition - and measured thickness. Required for absorption correction calcultions - using the Cliff Lorimer method. - - Parameters - ---------- - weight_percent : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) - Stack of compositions as determined from an initial k_factor - quantification. - thickness : float or :py:class:`numpy.ndarray` - Either a float value for thickness in nm or an array equal to the - size of the EDX map with thickness at each position of the sample. - - Returns - ------- - mass_thickness : :py:class:`numpy.ndarray` - Mass thickness in kg/m². - """ - if isinstance(thickness, (float, int)): - thickness_map = np.ones_like(weight_percent[0]) * thickness - else: - thickness_map = thickness - - elements = [intensity.metadata.Sample.elements[0] for intensity in weight_percent] - mass_thickness = np.zeros_like(weight_percent[0]) - densities = np.array( - [elements_db[element]['Physical_properties']['density (g/cm^3)'] - for element in elements]) - for density, element_composition in zip(densities, weight_percent): - # convert composition from % to fraction: factor of 1E-2 - # convert thickness from nm to m: factor of 1E-9 - # convert density from g/cm3 to kg/m2: factor of 1E3 - elemental_mt = element_composition * thickness_map * density * 1E-8 - mass_thickness += elemental_mt - return mass_thickness - - -class LazyEDSTEMSpectrum(EDSTEMSpectrum, LazyEDSSpectrum): - pass diff --git a/hyperspy/_signals/eels.py b/hyperspy/_signals/eels.py deleted file mode 100644 index cecdb79fb7..0000000000 --- a/hyperspy/_signals/eels.py +++ /dev/null @@ -1,1889 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numbers -import logging - -import numpy as np -import dask.array as da -import traits.api as t -from scipy import constants -from prettytable import PrettyTable -import pint - -from hyperspy.signal import BaseSetMetadataItems, BaseSignal -from hyperspy._signals.signal1d import (Signal1D, LazySignal1D) -from hyperspy.signal_tools import EdgesRange -from hyperspy.misc.elements import elements as elements_db -from hyperspy.misc.label_position import SpectrumLabelPosition -import hyperspy.axes -from hyperspy.defaults_parser import preferences -from hyperspy.components1d import PowerLaw -from hyperspy.misc.utils import isiterable, underline, print_html -from hyperspy.misc.utils import is_binned # remove in v2.0 -from hyperspy.misc.math_tools import optimal_fft_size -from hyperspy.misc.eels.tools import get_edges_near_energy -from hyperspy.misc.eels.electron_inelastic_mean_free_path import iMFP_Iakoubovskii, iMFP_angular_correction -from hyperspy.ui_registry import add_gui_method, DISPLAY_DT, TOOLKIT_DT -from hyperspy.docstrings.signal1d import ( - CROP_PARAMETER_DOC, SPIKES_DIAGNOSIS_DOCSTRING, MASK_ZERO_LOSS_PEAK_WIDTH, - SPIKES_REMOVAL_TOOL_DOCSTRING) -from hyperspy.docstrings.signal import ( - SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG, SIGNAL_MASK_ARG, - NAVIGATION_MASK_ARG) - - - -_logger = logging.getLogger(__name__) -_ureg = pint.UnitRegistry() - - -@add_gui_method(toolkey="hyperspy.microscope_parameters_EELS") -class EELSTEMParametersUI(BaseSetMetadataItems): - convergence_angle = t.Float(t.Undefined, - label='Convergence semi-angle (mrad)') - beam_energy = t.Float(t.Undefined, - label='Beam energy (keV)') - collection_angle = t.Float(t.Undefined, - label='Collection semi-angle (mrad)') - mapping = { - 'Acquisition_instrument.TEM.convergence_angle': - 'convergence_angle', - 'Acquisition_instrument.TEM.beam_energy': - 'beam_energy', - 'Acquisition_instrument.TEM.Detector.EELS.collection_angle': - 'collection_angle', - } - - -class EELSSpectrum(Signal1D): - - _signal_type = "EELS" - _alias_signal_types = ["TEM EELS"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Attributes defaults - self.subshells = set() - self.elements = set() - self.edges = list() - if hasattr(self.metadata, 'Sample') and \ - hasattr(self.metadata.Sample, 'elements'): - self.add_elements(self.metadata.Sample.elements) - self.axes_manager.signal_axes[0].is_binned = True - self._edge_markers = {} - - def add_elements(self, elements, include_pre_edges=False): - """Declare the elemental composition of the sample. - - The ionisation edges of the elements present in the current - energy range will be added automatically. - - Parameters - ---------- - elements : tuple of strings - The symbol of the elements. Note this input must always be - in the form of a tuple. Meaning: add_elements(('C',)) will - work, while add_elements(('C')) will NOT work. - include_pre_edges : bool - If True, the ionization edges with an onset below the lower - energy limit of the SI will be included - - Examples - -------- - - >>> s = hs.signals.EELSSpectrum(np.arange(1024)) - >>> s.add_elements(('C', 'O')) - - Raises - ------ - ValueError - - """ - if not isiterable(elements) or isinstance(elements, str): - raise ValueError( - "Input must be in the form of a tuple. For example, " - "if `s` is the variable containing this EELS spectrum:\n " - ">>> s.add_elements(('C',))\n" - "See the docstring for more information.") - - for element in elements: - if isinstance(element, bytes): - element = element.decode() - if element in elements_db: - self.elements.add(element) - else: - raise ValueError( - "%s is not a valid symbol of a chemical element" - % element) - if not hasattr(self.metadata, 'Sample'): - self.metadata.add_node('Sample') - self.metadata.Sample.elements = list(self.elements) - if self.elements: - self.generate_subshells(include_pre_edges) - - def generate_subshells(self, include_pre_edges=False): - """Calculate the subshells for the current energy range for the - elements present in self.elements - - Parameters - ---------- - include_pre_edges : bool - If True, the ionization edges with an onset below the lower - energy limit of the SI will be included - - """ - Eaxis = self.axes_manager.signal_axes[0].axis - if not include_pre_edges: - start_energy = Eaxis[0] - else: - start_energy = 0. - end_energy = Eaxis[-1] - for element in self.elements: - e_shells = list() - for shell in elements_db[element][ - 'Atomic_properties']['Binding_energies']: - if shell[-1] != 'a': - energy = (elements_db[element]['Atomic_properties'] - ['Binding_energies'][shell]['onset_energy (eV)']) - if start_energy <= energy <= end_energy: - subshell = '%s_%s' % (element, shell) - if subshell not in self.subshells: - self.subshells.add( - '%s_%s' % (element, shell)) - e_shells.append(subshell) - - def edges_at_energy(self, energy='interactive', width=10, only_major=False, - order='closest', display=True, toolkit=None): - """Show EELS edges according to an energy range selected from the - spectrum or within a provided energy window - - Parameters - ---------- - energy : 'interactive' or float - If it is 'interactive', a table with edges are shown and it depends - on the energy range selected in the spectrum. If it is a float, a - table with edges are shown and it depends on the energy window - defined by energy +/- (width/2). The default is 'interactive'. - width : float - Width of window, in eV, around energy in which to find nearby - energies, i.e. a value of 10 eV (the default) means to - search +/- 5 eV. The default is 10. - only_major : bool - Whether to show only the major edges. The default is False. - order : str - Sort the edges, if 'closest', return in the order of energy - difference, if 'ascending', return in ascending order, similarly - for 'descending'. The default is 'closest'. - - Returns - ------- - An interactive widget if energy is 'interactive', or a html-format - table or ASCII table, depends on the environment. - """ - - if energy == 'interactive': - er = EdgesRange(self) - return er.gui(display=display, toolkit=toolkit) - else: - return self.print_edges_near_energy(energy, width, only_major, - order) - - @staticmethod - def print_edges_near_energy(energy=None, width=10, only_major=False, - order='closest', edges=None): - """Find and print a table of edges near a given energy that are within - the given energy window. - - Parameters - ---------- - energy : float - Energy to search, in eV - width : float - Width of window, in eV, around energy in which to find nearby - energies, i.e. a value of 10 eV (the default) means to - search +/- 5 eV. The default is 10. - only_major : bool - Whether to show only the major edges. The default is False. - order : str - Sort the edges, if 'closest', return in the order of energy - difference, if 'ascending', return in ascending order, similarly - for 'descending'. The default is 'closest'. - edges : iterable - A sequence of edges, if provided, it overrides energy, width, - only_major and order. - - Returns - ------- - A PrettyText object where its representation is ASCII in terminal and - html-formatted in Jupyter notebook - """ - - if edges is None and energy is not None: - edges = get_edges_near_energy(energy=energy, width=width, - only_major=only_major, order=order) - elif edges is None and energy is None: - raise ValueError('Either energy or edges should be provided.') - - table = PrettyTable() - table.field_names = [ - 'edge', - 'onset energy (eV)', - 'relevance', - 'description'] - - for edge in edges: - element, shell = edge.split('_') - shell_dict = elements_db[element]['Atomic_properties'][ - 'Binding_energies'][shell] - - onset = shell_dict['onset_energy (eV)'] - relevance = shell_dict['relevance'] - threshold = shell_dict['threshold'] - edge_ = shell_dict['edge'] - description = threshold + '. '*(threshold !='' and edge_ !='') + edge_ - - table.add_row([edge, onset, relevance, description]) - - # this ensures the html version try its best to mimick the ASCII one - table.format = True - - return print_html(f_text=table.get_string, - f_html=table.get_html_string) - - def estimate_zero_loss_peak_centre(self, mask=None): - """Estimate the position of the zero-loss peak. - - This function provides just a coarse estimation of the position - of the zero-loss peak centre by computing the position of the maximum - of the spectra. For subpixel accuracy use `estimate_shift1D`. - - Parameters - ---------- - mask : Signal1D of bool data type or bool array - It must have signal_dimension = 0 and navigation_shape equal to the - navigation shape of the current signal. Where mask is True the - shift is not computed and set to nan. - - Returns - ------- - zlpc : Signal1D subclass - The estimated position of the maximum of the ZLP peak. - - Notes - ----- - This function only works when the zero-loss peak is the most - intense feature in the spectrum. If it is not in most cases - the spectrum can be cropped to meet this criterion. - Alternatively use `estimate_shift1D`. - - See Also - -------- - estimate_shift1D, align_zero_loss_peak - - """ - self._check_signal_dimension_equals_one() - self._check_navigation_mask(mask) - if isinstance(mask, BaseSignal): - mask = mask.data - zlpc = self.valuemax(-1) - if mask is not None: - if zlpc._lazy: - zlpc.data = da.where(mask, np.nan, zlpc.data) - else: - zlpc.data[mask] = np.nan - zlpc.set_signal_type("") - title = self.metadata.General.title - zlpc.metadata.General.title = "ZLP(%s)" % title - return zlpc - - def align_zero_loss_peak( - self, - calibrate=True, - also_align=[], - print_stats=True, - subpixel=True, - mask=None, - signal_range=None, - show_progressbar=None, - crop=True, - **kwargs): - """Align the zero-loss peak. - - This function first aligns the spectra using the result of - `estimate_zero_loss_peak_centre` and afterward, if subpixel is True, - proceeds to align with subpixel accuracy using `align1D`. The offset - is automatically correct if `calibrate` is True. - - Parameters - ---------- - calibrate : bool - If True, set the offset of the spectral axis so that the - zero-loss peak is at position zero. - also_align : list of signals - A list containing other spectra of identical dimensions to - align using the shifts applied to the current spectrum. - If `calibrate` is True, the calibration is also applied to - the spectra in the list. - print_stats : bool - If True, print summary statistics of the ZLP maximum before - the alignment. - subpixel : bool - If True, perform the alignment with subpixel accuracy - using cross-correlation. - mask : Signal1D of bool data type or bool array. - It must have signal_dimension = 0 and navigation_shape equal to - the shape of the current signal. Where mask is True the shift is - not computed and set to nan. - signal_range : tuple of integers, tuple of floats. Optional - Will only search for the ZLP within the signal_range. If given - in integers, the range will be in index values. If given floats, - the range will be in spectrum values. Useful if there are features - in the spectrum which are more intense than the ZLP. - Default is searching in the whole signal. Note that ROIs can be used - in place of a tuple. - %s - %s - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - - Examples - -------- - >>> s_ll = hs.signals.EELSSpectrum(np.zeros(1000)) - >>> s_ll.data[100] = 100 - >>> s_ll.align_zero_loss_peak() - - Aligning both the lowloss signal and another signal - - >>> s = hs.signals.EELSSpectrum(np.range(1000)) - >>> s_ll.align_zero_loss_peak(also_align=[s]) - - Aligning within a narrow range of the lowloss signal - - >>> s_ll.align_zero_loss_peak(signal_range=(-10.,10.)) - - - See Also - -------- - estimate_zero_loss_peak_centre, align1D, estimate_shift1D. - - Notes - ----- - Any extra keyword arguments are passed to `align1D`. For - more information read its docstring. - - """ - - def substract_from_offset(value, signals): - # Test that axes is uniform - if not self.axes_manager[-1].is_uniform: - raise NotImplementedError("Support for EELS signals with " - "non-uniform signal axes is not yet implemented.") - if isinstance(value, da.Array): - value = value.compute() - for signal in signals: - signal.axes_manager[-1].offset -= value - signal.events.data_changed.trigger(signal) - - def estimate_zero_loss_peak_centre(s, mask, signal_range): - if signal_range: - zlpc = s.isig[signal_range[0]:signal_range[1]].\ - estimate_zero_loss_peak_centre(mask=mask) - else: - zlpc = s.estimate_zero_loss_peak_centre(mask=mask) - return zlpc - - zlpc = estimate_zero_loss_peak_centre( - self, mask=mask, signal_range=signal_range) - - mean_ = np.nanmean(zlpc.data) - - if print_stats is True: - print(underline("Initial ZLP position statistics")) - zlpc.print_summary_statistics() - - for signal in also_align + [self]: - shift_array = -zlpc.data + mean_ - if zlpc._lazy: - # We must compute right now because otherwise any changes to the - # axes_manager of the signal later in the workflow may result in - # a wrong shift_array - shift_array = shift_array.compute() - signal.shift1D( - shift_array, crop=crop, show_progressbar=show_progressbar) - - if calibrate is True: - zlpc = estimate_zero_loss_peak_centre( - self, mask=mask, signal_range=signal_range) - substract_from_offset(np.nanmean(zlpc.data), - also_align + [self]) - - if subpixel is False: - return - left, right = -3., 3. - if calibrate is False: - left += mean_ - right += mean_ - - left = (left if left > self.axes_manager[-1].axis[0] - else self.axes_manager[-1].axis[0]) - right = (right if right < self.axes_manager[-1].axis[-1] - else self.axes_manager[-1].axis[-1]) - - if self.axes_manager.navigation_size > 1: - self.align1D( - left, - right, - also_align=also_align, - show_progressbar=show_progressbar, - mask=mask, - crop=crop, - **kwargs) - if calibrate is True: - zlpc = estimate_zero_loss_peak_centre( - self, mask=mask, signal_range=signal_range) - substract_from_offset(np.nanmean(zlpc.data), - also_align + [self]) - align_zero_loss_peak.__doc__ %= (SHOW_PROGRESSBAR_ARG, CROP_PARAMETER_DOC) - - def get_zero_loss_peak_mask(self, zero_loss_peak_mask_width=5.0, - signal_mask=None): - """Return boolean array with True value at the position of the zero - loss peak. This mask can be used to restrict operation to the signal - locations not marked as True (masked). - - Parameters - ---------- - zero_loss_peak_mask_width: float - Width of the zero loss peak mask. - %s - - Returns - ------- - bool array - """ - zlpc = self.estimate_zero_loss_peak_centre() - (signal_axis, ) = self.axes_manager[self.axes_manager.signal_axes] - axis = signal_axis.axis - mini_value = zlpc.data.mean() - zero_loss_peak_mask_width / 2 - maxi_value = zlpc.data.mean() + zero_loss_peak_mask_width / 2 - mask = np.logical_and(mini_value <= axis, axis <= maxi_value) - if signal_mask is not None: - signal_mask = np.logical_or(mask, signal_mask) - else: - signal_mask = mask - return signal_mask - - get_zero_loss_peak_mask.__doc__ %= (SIGNAL_MASK_ARG) - - def spikes_diagnosis(self, signal_mask=None, navigation_mask=None, - zero_loss_peak_mask_width=None, **kwargs): - if zero_loss_peak_mask_width is not None: - signal_mask = self.get_zero_loss_peak_mask(zero_loss_peak_mask_width, - signal_mask) - super().spikes_diagnosis(signal_mask=signal_mask, navigation_mask=None, - **kwargs) - - spikes_diagnosis.__doc__ = SPIKES_DIAGNOSIS_DOCSTRING % MASK_ZERO_LOSS_PEAK_WIDTH - - def spikes_removal_tool(self, signal_mask=None, - navigation_mask=None, - threshold='auto', - zero_loss_peak_mask_width=None, - interactive=True, - display=True, - toolkit=None): - if zero_loss_peak_mask_width is not None: - axis = self.axes_manager.signal_axes[0].axis - # check the zero_loss is in the signal - if (axis[0] - zero_loss_peak_mask_width / 2 > 0 or - axis[-1] + zero_loss_peak_mask_width / 2 < 0): - raise ValueError("The zero loss peaks isn't in the energy range.") - signal_mask = self.get_zero_loss_peak_mask(zero_loss_peak_mask_width, - signal_mask) - super().spikes_removal_tool(signal_mask=signal_mask, - navigation_mask=navigation_mask, - threshold=threshold, - interactive=interactive, - display=display, toolkit=toolkit) - spikes_removal_tool.__doc__ = SPIKES_REMOVAL_TOOL_DOCSTRING % ( - SIGNAL_MASK_ARG, NAVIGATION_MASK_ARG, MASK_ZERO_LOSS_PEAK_WIDTH, DISPLAY_DT, TOOLKIT_DT,) - - def estimate_elastic_scattering_intensity( - self, threshold, show_progressbar=None): - """Rough estimation of the elastic scattering intensity by - truncation of a EELS low-loss spectrum. - - Parameters - ---------- - threshold : {Signal1D, float, int} - Truncation energy to estimate the intensity of the elastic - scattering. The threshold can be provided as a signal of the same - dimension as the input spectrum navigation space containing the - threshold value in the energy units. Alternatively a constant - threshold can be specified in energy/index units by passing - float/int. - %s - - Returns - ------- - I0: Signal1D - The elastic scattering intensity. - - See Also - -------- - estimate_elastic_scattering_threshold - - """ - # TODO: Write units tests - self._check_signal_dimension_equals_one() - - if show_progressbar is None: - show_progressbar = preferences.General.show_progressbar - - if isinstance(threshold, numbers.Number): - I0 = self.isig[:threshold].integrate1D(-1) - else: - ax = self.axes_manager.signal_axes[0] - # I0 = self._get_navigation_signal() - # I0.axes_manager.set_signal_dimension(0) - threshold.axes_manager.set_signal_dimension(0) - binned = ax.is_binned - - def estimating_function(data, threshold=None): - if np.isnan(threshold): - return np.nan - else: - # the object is just an array, so have to reimplement - # integrate1D. However can make certain assumptions, for - # example 1D signal and pretty much always binned. Should - # probably at some point be joint - ind = ax.value2index(threshold) - data = data[:ind] - if binned: - return data.sum() - else: - from scipy.integrate import simps - axis = ax.axis[:ind] - return simps(y=data, x=axis) - - I0 = self.map(estimating_function, threshold=threshold, - ragged=False, show_progressbar=show_progressbar, - inplace=False) - I0.metadata.General.title = ( - self.metadata.General.title + ' elastic intensity') - I0.set_signal_type("") - if self.tmp_parameters.has_item('filename'): - I0.tmp_parameters.filename = ( - self.tmp_parameters.filename + - '_elastic_intensity') - I0.tmp_parameters.folder = self.tmp_parameters.folder - I0.tmp_parameters.extension = \ - self.tmp_parameters.extension - return I0 - estimate_elastic_scattering_intensity.__doc__ %= SHOW_PROGRESSBAR_ARG - - def estimate_elastic_scattering_threshold(self, - window=10., - tol=None, - window_length=5, - polynomial_order=3, - start=1.): - """Calculate the first inflexion point of the spectrum derivative - within a window. - - This method assumes that the zero-loss peak is located at position zero - in all the spectra. Currently it looks for an inflexion point, that can - be a local maximum or minimum. Therefore, to estimate the elastic - scattering threshold `start` + `window` must be less than the first - maximum for all spectra (often the bulk plasmon maximum). If there is - more than one inflexion point in energy the window it selects the - smoother one what, often, but not always, is a good choice in this - case. - - Parameters - ---------- - window : {None, float} - If None, the search for the local inflexion point is performed - using the full energy range. A positive float will restrict - the search to the (0,window] energy window, where window is given - in the axis units. If no inflexion point is found in this - spectral range the window value is returned instead. - tol : {None, float} - The threshold tolerance for the derivative. If "auto" it is - automatically calculated as the minimum value that guarantees - finding an inflexion point in all the spectra in given energy - range. - window_length : int - If non zero performs order three Savitzky-Golay smoothing - to the data to avoid falling in local minima caused by - the noise. It must be an odd integer. - polynomial_order : int - Savitzky-Golay filter polynomial order. - start : float - Position from the zero-loss peak centre from where to start - looking for the inflexion point. - - - Returns - ------- - - threshold : Signal1D - A Signal1D of the same dimension as the input spectrum - navigation space containing the estimated threshold. Where the - threshold couldn't be estimated the value is set to nan. - - See Also - -------- - - estimate_elastic_scattering_intensity,align_zero_loss_peak, - find_peaks1D_ohaver, fourier_ratio_deconvolution. - - Notes - ----- - - The main purpose of this method is to be used as input for - `estimate_elastic_scattering_intensity`. Indeed, for currently - achievable energy resolutions, there is not such a thing as a elastic - scattering threshold. Therefore, please be aware of the limitations of - this method when using it. - - """ - self._check_signal_dimension_equals_one() - # Create threshold with the same shape as the navigation dims. - threshold = self._get_navigation_signal().transpose(signal_axes=0) - - # Progress Bar - axis = self.axes_manager.signal_axes[0] - min_index, max_index = axis.value_range_to_indices(start, - start + window) - if max_index < min_index + 10: - raise ValueError("Please select a bigger window") - s = self.isig[min_index:max_index].deepcopy() - if window_length: - s.smooth_savitzky_golay(polynomial_order=polynomial_order, - window_length=window_length, - differential_order=1) - else: - s = s.derivative(-1) - if tol is None: - tol = np.max(np.abs(s.data).min(axis.index_in_array)) - saxis = s.axes_manager[-1] - inflexion = (np.abs(s.data) <= tol).argmax(saxis.index_in_array) - if isinstance(inflexion, da.Array): - inflexion = inflexion.compute() - threshold.data[:] = saxis.index2value(inflexion) - if isinstance(inflexion, np.ndarray): - threshold.data[inflexion == 0] = np.nan - else: # Single spectrum - if inflexion == 0: - threshold.data[:] = np.nan - del s - if np.isnan(threshold.data).any(): - _logger.warning( - "No inflexion point could be found in some positions " - "that have been marked with nans.") - # Create spectrum image, stop and return value - threshold.metadata.General.title = ( - self.metadata.General.title + - ' elastic scattering threshold') - if self.tmp_parameters.has_item('filename'): - threshold.tmp_parameters.filename = ( - self.tmp_parameters.filename + - '_elastic_scattering_threshold') - threshold.tmp_parameters.folder = self.tmp_parameters.folder - threshold.tmp_parameters.extension = \ - self.tmp_parameters.extension - threshold.set_signal_type("") - return threshold - - def estimate_thickness(self, - threshold=None, - zlp=None, - density=None, - mean_free_path=None,): - """Estimates the thickness (relative and absolute) - of a sample using the log-ratio method. - - The current EELS spectrum must be a low-loss spectrum containing - the zero-loss peak. The hyperspectrum must be well calibrated - and aligned. To obtain the thickness relative to the mean free path - don't set the `density` and the `mean_free_path`. - - Parameters - ---------- - threshold : {BaseSignal, float}, optional - If the zero-loss-peak is not provided, use this energy threshold - to roughly estimate its intensity by truncation. - If the threshold is constant across the dataset use a float. Otherwise, - provide a signal of - the same dimension as the input spectrum navigation space - containing the threshold value in the energy units. - zlp : BaseSignal, optional - If not None the zero-loss peak intensity is calculated from the ZLP - spectrum supplied by integration. - mean_free_path : float, optional - The mean free path of the material in nanometers. - If not provided, the thickness - is given relative to the mean free path. - density : float, optional - The density of the material in g/cm**3. This is used to estimate the mean - free path when the mean free path is not known and to perform the - angular corrections. - - Returns - ------- - s : BaseSignal - The thickness relative to the MFP. It returns a Signal1D, - Signal2D or a BaseSignal, depending on the current navigation - dimensions. - - Notes - ----- - For details see Egerton, R. Electron Energy-Loss Spectroscopy in the Electron - Microscope. Springer-Verlag, 2011. - """ - axis = self.axes_manager.signal_axes[0] - total_intensity = self.integrate1D(axis.index_in_array).data - if threshold is None and zlp is None: - raise ValueError("Please provide one of the following keywords: " - "`threshold`, `zlp`") - if zlp is not None: - I0 = zlp.integrate1D(axis.index_in_array).data - else: - I0 = self.estimate_elastic_scattering_intensity( - threshold=threshold,).data - if self._lazy: - t_over_lambda = da.log(total_intensity / I0) - else: - t_over_lambda = np.log(total_intensity / I0) - if density is not None: - if self._are_microscope_parameters_missing(): - raise RuntimeError( - "Some microscope parameters are missing. Please use the " - "`set_microscope_parameters()` method to set them. " - "If you don't know them, don't set the `density` keyword." - ) - else: - md = self.metadata.Acquisition_instrument.TEM - t_over_lambda *= iMFP_angular_correction( - beam_energy=md.beam_energy, - alpha=md.convergence_angle, - beta=md.Detector.EELS.collection_angle, - density=density, - ) - if mean_free_path is None: - mean_free_path = iMFP_Iakoubovskii( - electron_energy=self.metadata.Acquisition_instrument.TEM.beam_energy, - density=density) - _logger.info(f"The estimated iMFP is {mean_free_path} nm") - else: - _logger.warning( - "Computing the thickness without taking into account the effect of " - "the limited collection angle, what usually leads to underestimating " - "the thickness. To perform the angular corrections you must provide " - "the density of the material.") - - s = self._get_navigation_signal(data=t_over_lambda) - if mean_free_path is not None: - s.data *= mean_free_path - s.metadata.General.title = ( - self.metadata.General.title + - ' thickness (nm)') - s.metadata.Signal.quantity = "thickness (nm)" - else: - _logger.warning( - "Computing the relative thickness. To compute the absolute " - "thickness provide the `mean_free_path` and/or the `density`") - s.metadata.General.title = (self.metadata.General.title + - ' $\\frac{t}{\\lambda}$') - s.metadata.Signal.quantity = "$\\frac{t}{\\lambda}$" - if self.tmp_parameters.has_item('filename'): - s.tmp_parameters.filename = ( - self.tmp_parameters.filename + - '_thickness') - s.tmp_parameters.folder = self.tmp_parameters.folder - s.tmp_parameters.extension = \ - self.tmp_parameters.extension - s.axes_manager.set_signal_dimension(0) - s.set_signal_type("") - return s - - def fourier_log_deconvolution(self, - zlp, - add_zlp=False, - crop=False): - """Performs fourier-log deconvolution. - - Parameters - ---------- - zlp : EELSSpectrum - The corresponding zero-loss peak. - - add_zlp : bool - If True, adds the ZLP to the deconvolved spectrum - crop : bool - If True crop the spectrum to leave out the channels that - have been modified to decay smoothly to zero at the sides - of the spectrum. - - Returns - ------- - An EELSSpectrum containing the current data deconvolved. - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - - Notes - ----- - For details see: Egerton, R. Electron Energy-Loss - Spectroscopy in the Electron Microscope. Springer-Verlag, 2011. - - """ - self._check_signal_dimension_equals_one() - if not self.axes_manager.signal_axes[0].is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes") - s = self.deepcopy() - zlp_size = zlp.axes_manager.signal_axes[0].size - self_size = self.axes_manager.signal_axes[0].size - tapped_channels = s.hanning_taper() - # Conservative new size to solve the wrap-around problem - size = zlp_size + self_size - 1 - # Calculate optimal FFT padding for performance - complex_result = (zlp.data.dtype.kind == 'c' or s.data.dtype.kind == 'c') - size = optimal_fft_size(size, not complex_result) - - axis = self.axes_manager.signal_axes[0] - if self._lazy or zlp._lazy: - - z = da.fft.rfft(zlp.data, n=size, axis=axis.index_in_array) - j = da.fft.rfft(s.data, n=size, axis=axis.index_in_array) - j1 = z * da.log(j / z).map_blocks(np.nan_to_num) - sdata = da.fft.irfft(j1, axis=axis.index_in_array) - else: - z = np.fft.rfft(zlp.data, n=size, axis=axis.index_in_array) - j = np.fft.rfft(s.data, n=size, axis=axis.index_in_array) - j1 = z * np.nan_to_num(np.log(j / z)) - sdata = np.fft.irfft(j1, axis=axis.index_in_array) - - s.data = sdata[s.axes_manager._get_data_slice( - [(axis.index_in_array, slice(None, self_size)), ])] - if add_zlp is True: - if self_size >= zlp_size: - if self._lazy: - _slices_before = s.axes_manager._get_data_slice( - [(axis.index_in_array, slice(None, zlp_size)), ]) - _slices_after = s.axes_manager._get_data_slice( - [(axis.index_in_array, slice(zlp_size, None)), ]) - s.data = da.stack((s.data[_slices_before] + zlp.data, - s.data[_slices_after]), - axis=axis.index_in_array) - else: - s.data[s.axes_manager._get_data_slice( - [(axis.index_in_array, slice(None, zlp_size)), ]) - ] += zlp.data - else: - s.data += zlp.data[s.axes_manager._get_data_slice( - [(axis.index_in_array, slice(None, self_size)), ])] - - s.metadata.General.title = (s.metadata.General.title + - ' after Fourier-log deconvolution') - if s.tmp_parameters.has_item('filename'): - s.tmp_parameters.filename = ( - self.tmp_parameters.filename + - '_after_fourier_log_deconvolution') - if crop is True: - s.crop(axis.index_in_axes_manager, - None, int(-tapped_channels)) - return s - - def fourier_ratio_deconvolution(self, ll, - fwhm=None, - threshold=None, - extrapolate_lowloss=True, - extrapolate_coreloss=True): - """Performs Fourier-ratio deconvolution. - - The core-loss should have the background removed. To reduce the noise - amplification the result is convolved with a Gaussian function. - - Parameters - ---------- - ll: EELSSpectrum - The corresponding low-loss (ll) EELSSpectrum. - fwhm : float or None - Full-width half-maximum of the Gaussian function by which - the result of the deconvolution is convolved. It can be - used to select the final SNR and spectral resolution. If - None, the FWHM of the zero-loss peak of the low-loss is - estimated and used. - threshold : {None, float} - Truncation energy to estimate the intensity of the - elastic scattering. If None the threshold is taken as the - first minimum after the ZLP centre. - extrapolate_lowloss, extrapolate_coreloss : bool - If True the signals are extrapolated using a power law, - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - - Notes - ----- - For details see: Egerton, R. Electron Energy-Loss - Spectroscopy in the Electron Microscope. Springer-Verlag, 2011. - - """ - self._check_signal_dimension_equals_one() - if not self.axes_manager.signal_axes[0].is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes.") - if not ll.axes_manager.signal_axes[0].is_uniform: - raise NotImplementedError( - "The low-loss energy axis is non-uniform. " - "This operation is not yet implemented for non-uniform energy axes") - orig_cl_size = self.axes_manager.signal_axes[0].size - - if threshold is None: - threshold = ll.estimate_elastic_scattering_threshold() - - if extrapolate_coreloss is True: - cl = self.power_law_extrapolation( - window_size=20, - extrapolation_size=100) - else: - cl = self.deepcopy() - - if extrapolate_lowloss is True: - ll = ll.power_law_extrapolation( - window_size=100, - extrapolation_size=100) - else: - ll = ll.deepcopy() - - ll.hanning_taper() - cl.hanning_taper() - if self._lazy or ll._lazy: - rfft = da.fft.rfft - irfft = da.fft.irfft - else: - rfft = np.fft.rfft - irfft = np.fft.irfft - - ll_size = ll.axes_manager.signal_axes[0].size - cl_size = self.axes_manager.signal_axes[0].size - # Conservative new size to solve the wrap-around problem - size = ll_size + cl_size - 1 - # Calculate the optimal FFT size - size = optimal_fft_size(size) - - axis = ll.axes_manager.signal_axes[0] - if fwhm is None: - fwhm = float(ll.get_current_signal().estimate_peak_width()()) - _logger.info("FWHM = %1.2f" % fwhm) - - I0 = ll.estimate_elastic_scattering_intensity(threshold=threshold) - I0 = I0.data - if ll.axes_manager.navigation_size > 0: - I0_shape = list(I0.shape) - I0_shape.insert(axis.index_in_array, 1) - I0 = I0.reshape(I0_shape) - - from hyperspy.components1d import Gaussian - g = Gaussian() - g.sigma.value = fwhm / 2.3548 - g.A.value = 1 - g.centre.value = 0 - zl = g.function( - np.linspace(axis.offset, - axis.offset + axis.scale * (size - 1), - size)) - z = np.fft.rfft(zl) - jk = rfft(cl.data, n=size, axis=axis.index_in_array) - jl = rfft(ll.data, n=size, axis=axis.index_in_array) - zshape = [1, ] * len(cl.data.shape) - zshape[axis.index_in_array] = jk.shape[axis.index_in_array] - cl.data = irfft(z.reshape(zshape) * jk / jl, - axis=axis.index_in_array) - cl.data *= I0 - cl.crop(-1, None, int(orig_cl_size)) - cl.metadata.General.title = (self.metadata.General.title + - ' after Fourier-ratio deconvolution') - if cl.tmp_parameters.has_item('filename'): - cl.tmp_parameters.filename = ( - self.tmp_parameters.filename + - 'after_fourier_ratio_deconvolution') - return cl - - def richardson_lucy_deconvolution(self, psf, iterations=15, - show_progressbar=None, - parallel=None, max_workers=None): - """1D Richardson-Lucy Poissonian deconvolution of - the spectrum by the given kernel. - - Parameters - ---------- - psf : EELSSpectrum - It must have the same signal dimension as the current - spectrum and a spatial dimension of 0 or the same as the - current spectrum. - iterations : int - Number of iterations of the deconvolution. Note that - increasing the value will increase the noise amplification. - %s - %s - %s - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - - Notes - ----- - For details on the algorithm see Gloter, A., A. Douiri, - M. Tence, and C. Colliex. “Improving Energy Resolution of - EELS Spectra: An Alternative to the Monochromator Solution.” - Ultramicroscopy 96, no. 3–4 (September 2003): 385–400. - - """ - if not self.axes_manager.signal_axes[0].is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes.") - if show_progressbar is None: - show_progressbar = preferences.General.show_progressbar - self._check_signal_dimension_equals_one() - psf_size = psf.axes_manager.signal_axes[0].size - maxval = self.axes_manager.navigation_size - show_progressbar = show_progressbar and (maxval > 0) - - def deconv_function(signal, kernel=None, - iterations=15, psf_size=None): - imax = kernel.argmax() - result = np.array(signal).copy() - mimax = psf_size - 1 - imax - for _ in range(iterations): - first = np.convolve(kernel, result)[imax: imax + psf_size] - result *= np.convolve(kernel[::-1], signal / - first)[mimax:mimax + psf_size] - return result - - ds = self.map(deconv_function, kernel=psf, iterations=iterations, - psf_size=psf_size, show_progressbar=show_progressbar, - parallel=parallel, max_workers=max_workers, - ragged=False, inplace=False) - - ds.metadata.General.title += ( - ' after Richardson-Lucy deconvolution %i iterations' % - iterations) - if ds.tmp_parameters.has_item('filename'): - ds.tmp_parameters.filename += ( - '_after_R-L_deconvolution_%iiter' % iterations) - return ds - - richardson_lucy_deconvolution.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) - - def _are_microscope_parameters_missing(self, ignore_parameters=[]): - """ - Check if the EELS parameters necessary to calculate the GOS - are defined in metadata. If not, in interactive mode - raises an UI item to fill the values. - The `ignore_parameters` list can be to ignore parameters. - """ - must_exist = ( - 'Acquisition_instrument.TEM.convergence_angle', - 'Acquisition_instrument.TEM.beam_energy', - 'Acquisition_instrument.TEM.Detector.EELS.collection_angle',) - missing_parameters = [] - for item in must_exist: - exists = self.metadata.has_item(item) - if exists is False and item.split( - '.')[-1] not in ignore_parameters: - missing_parameters.append(item) - if missing_parameters: - _logger.info("Missing parameters {}".format(missing_parameters)) - return True - else: - return False - - def set_microscope_parameters(self, - beam_energy=None, - convergence_angle=None, - collection_angle=None, - toolkit=None, - display=True): - if set((beam_energy, convergence_angle, collection_angle)) == {None}: - tem_par = EELSTEMParametersUI(self) - return tem_par.gui(toolkit=toolkit, display=display) - mp = self.metadata - if beam_energy is not None: - mp.set_item("Acquisition_instrument.TEM.beam_energy", beam_energy) - if convergence_angle is not None: - mp.set_item( - "Acquisition_instrument.TEM.convergence_angle", - convergence_angle) - if collection_angle is not None: - mp.set_item( - "Acquisition_instrument.TEM.Detector.EELS.collection_angle", - collection_angle) - set_microscope_parameters.__doc__ = \ - """ - Set the microscope parameters that are necessary to calculate - the GOS. - - If not all of them are defined, in interactive mode - raises an UI item to fill the values. - - beam_energy: float - The energy of the electron beam in keV. - convergence_angle : float - The microscope convergence semi-angle in mrad. - collection_angle : float - The collection semi-angle in mrad. - {} - {} - """.format(TOOLKIT_DT, DISPLAY_DT) - - def power_law_extrapolation(self, - window_size=20, - extrapolation_size=1024, - add_noise=False, - fix_neg_r=False): - """Extrapolate the spectrum to the right using a powerlaw. - - - Parameters - ---------- - window_size : int - The number of channels from the right side of the - spectrum that are used to estimate the power law - parameters. - extrapolation_size : int - Size of the extrapolation in number of channels - add_noise : bool - If True, add poissonian noise to the extrapolated spectrum. - fix_neg_r : bool - If True, the negative values for the "components.PowerLaw" - parameter r will be flagged and the extrapolation will be - done with a constant zero-value. - - Returns - ------- - A new spectrum, with the extrapolation. - - """ - self._check_signal_dimension_equals_one() - axis = self.axes_manager.signal_axes[0] - s = self.deepcopy() - s.metadata.General.title += ( - ' %i channels extrapolated' % - extrapolation_size) - if s.tmp_parameters.has_item('filename'): - s.tmp_parameters.filename += ( - '_%i_channels_extrapolated' % extrapolation_size) - new_shape = list(self.data.shape) - new_shape[axis.index_in_array] += extrapolation_size - if self._lazy: - left_data = s.data - right_shape = list(self.data.shape) - right_shape[axis.index_in_array] = extrapolation_size - right_chunks = list(self.data.chunks) - right_chunks[axis.index_in_array] = (extrapolation_size, ) - right_data = da.zeros( - shape=tuple(right_shape), - chunks=tuple(right_chunks), - dtype=self.data.dtype) - s.data = da.concatenate( - [left_data, right_data], axis=axis.index_in_array) - else: - # just old code - s.data = np.zeros(new_shape) - s.data[..., :axis.size] = self.data - s.get_dimensions_from_data() - pl = PowerLaw() - pl._axes_manager = self.axes_manager - A, r = pl.estimate_parameters( - s, - axis.index2value(axis.size - window_size), - axis.index2value(axis.size - 1), - out=True) - if fix_neg_r is True: - if s._lazy: - _where = da.where - else: - _where = np.where - A = _where(r <= 0, 0, A) - # If the signal is binned we need to bin the extrapolated power law - # what, in a first approximation, can be done by multiplying by the - # axis step size. - if is_binned(self): - # in v2 replace by - # if self.axes_manager[-1].is_binned: - factor = s.axes_manager[-1].scale - else: - factor = 1 - if self._lazy: - # only need new axes if the navigation dimension is not 0 - if s.axes_manager.navigation_dimension: - rightslice = (..., None) - axisslice = (None, slice(axis.size, None)) - else: - rightslice = (..., ) - axisslice = (slice(axis.size, None), ) - right_chunks[axis.index_in_array] = 1 - x = da.from_array( - s.axes_manager.signal_axes[0].axis[axisslice], - chunks=(extrapolation_size, )) - A = A[rightslice] - r = r[rightslice] - right_data = factor * A * x**(-r) - s.data = da.concatenate( - [left_data, right_data], axis=axis.index_in_array) - else: - s.data[..., axis.size:] = ( - factor * A[..., np.newaxis] * - s.axes_manager.signal_axes[0].axis[np.newaxis, axis.size:]**( - -r[..., np.newaxis])) - return s - - def kramers_kronig_analysis(self, - zlp=None, - iterations=1, - n=None, - t=None, - delta=0.5, - full_output=False): - r"""Calculate the complex dielectric function from a single scattering - distribution (SSD) using the Kramers-Kronig relations. - - It uses the FFT method as in [1]_. The SSD is an - EELSSpectrum instance containing SSD low-loss EELS with no zero-loss - peak. The internal loop is devised to approximately subtract the - surface plasmon contribution supposing an unoxidized planar surface and - neglecting coupling between the surfaces. This method does not account - for retardation effects, instrumental broadening and surface plasmon - excitation in particles. - - Note that either refractive index or thickness are required. - If both are None or if both are provided an exception is raised. - - Parameters - ---------- - zlp : {None, number, Signal1D} - ZLP intensity. It is optional (can be None) if `t` is None and `n` - is not None and the thickness estimation is not required. If `t` - is not None, the ZLP is required to perform the normalization and - if `t` is not None, the ZLP is required to calculate the thickness. - If the ZLP is the same for all spectra, the integral of the ZLP - can be provided as a number. Otherwise, if the ZLP intensity is not - the same for all spectra, it can be provided as i) a Signal1D - of the same dimensions as the current signal containing the ZLP - spectra for each location ii) a BaseSignal of signal dimension 0 - and navigation_dimension equal to the current signal containing the - integrated ZLP intensity. - iterations : int - Number of the iterations for the internal loop to remove the - surface plasmon contribution. If 1 the surface plasmon contribution - is not estimated and subtracted (the default is 1). - n : {None, float} - The medium refractive index. Used for normalization of the - SSD to obtain the energy loss function. If given the thickness - is estimated and returned. It is only required when `t` is None. - t : {None, number, Signal1D} - The sample thickness in nm. Used for normalization of the SSD - to obtain the energy loss function. It is only required when - `n` is None. If the thickness is the same for all spectra it can be - given by a number. Otherwise, it can be provided as a BaseSignal - with signal dimension 0 and navigation_dimension equal to the - current signal. - delta : float - A small number (0.1-0.5 eV) added to the energy axis in - specific steps of the calculation the surface loss correction to - improve stability. - full_output : bool - If True, return a dictionary that contains the estimated - thickness if `t` is None and the estimated surface plasmon - excitation and the spectrum corrected from surface plasmon - excitations if `iterations` > 1. - - Returns - ------- - eps: DielectricFunction instance - The complex dielectric function results, - - .. math:: - \epsilon = \epsilon_1 + i*\epsilon_2, - - contained in an DielectricFunction instance. - output: Dictionary (optional) - A dictionary of optional outputs with the following keys - - * ``thickness``: the estimated thickness in nm calculated by - normalization of the SSD (only when ``t`` is None) - * ``surface plasmon estimation``: the estimated surface plasmon - excitation (only if ``iterations`` > 1.) - - Raises - ------ - ValueError - If both `n` and `t` are undefined (None). - AttributeError - If the beam_energy or the collection semi-angle are not defined in - metadata. - NotImplementedError - If the signal axis is a non-uniform axis. - - Notes - ----- - This method is based in Egerton's Matlab code [1]_ with a - minor difference: the wrap-around problem when computing the FFTs is - workarounded by padding the signal instead of subtracting the - reflected tail. - - .. [1] Ray Egerton, "Electron Energy-Loss Spectroscopy in the Electron - Microscope", Springer-Verlag, 2011. - - """ - if not self.axes_manager.signal_axes[0].is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes.") - output = {} - if iterations == 1: - # In this case s.data is not modified so there is no need to make - # a deep copy. - s = self.isig[0.:] - else: - s = self.isig[0.:].deepcopy() - - sorig = self.isig[0.:] - # Avoid singularity at 0 - if s.axes_manager.signal_axes[0].axis[0] == 0: - s = s.isig[1:] - sorig = self.isig[1:] - - # Constants and units - me = constants.value( - 'electron mass energy equivalent in MeV') * 1e3 # keV - - # Mapped parameters - self._are_microscope_parameters_missing( - ignore_parameters=['convergence_angle']) - e0 = s.metadata.Acquisition_instrument.TEM.beam_energy - beta = s.metadata.Acquisition_instrument.TEM.Detector.EELS.\ - collection_angle - - axis = s.axes_manager.signal_axes[0] - eaxis = axis.axis.copy() - - if isinstance(zlp, hyperspy.signal.BaseSignal): - if (zlp.axes_manager.navigation_dimension == - self.axes_manager.navigation_dimension): - if zlp.axes_manager.signal_dimension == 0: - i0 = zlp.data - else: - i0 = zlp.integrate1D(axis.index_in_axes_manager).data - else: - raise ValueError('The ZLP signal dimensions are not ' - 'compatible with the dimensions of the ' - 'low-loss signal') - # The following prevents errors if the signal is a single spectrum - if len(i0) != 1: - i0 = i0.reshape( - np.insert(i0.shape, axis.index_in_array, 1)) - elif isinstance(zlp, numbers.Number): - i0 = zlp - else: - raise ValueError('The zero-loss peak input is not valid, it must be\ - in the BaseSignal class or a Number.') - - if isinstance(t, hyperspy.signal.BaseSignal): - if (t.axes_manager.navigation_dimension == - self.axes_manager.navigation_dimension) and ( - t.axes_manager.signal_dimension == 0): - t = t.data - t = t.reshape( - np.insert(t.shape, axis.index_in_array, 1)) - else: - raise ValueError('The thickness signal dimensions are not ' - 'compatible with the dimensions of the ' - 'low-loss signal') - elif isinstance(t, np.ndarray) and t.shape and t.shape != (1,): - raise ValueError("thickness must be a HyperSpy signal or a number," - " not a NumPy array.") - - # Slicer to get the signal data from 0 to axis.size - slicer = s.axes_manager._get_data_slice( - [(axis.index_in_array, slice(None, axis.size)), ]) - - # Kinetic definitions - ke = e0 * (1 + e0 / 2. / me) / (1 + e0 / me) ** 2 - tgt = e0 * (2 * me + e0) / (me + e0) - rk0 = 2590 * (1 + e0 / me) * np.sqrt(2 * ke / me) - - for io in range(iterations): - # Calculation of the ELF by normalization of the SSD - # Norm(SSD) = Imag(-1/epsilon) (Energy Loss Function, ELF) - - # We start by the "angular corrections" - Im = s.data / (np.log(1 + (beta * tgt / eaxis) ** 2)) / axis.scale - if n is None and t is None: - raise ValueError("The thickness and the refractive index are " - "not defined. Please provide one of them.") - elif n is not None and t is not None: - raise ValueError("Please provide the refractive index OR the " - "thickness information, not both") - elif n is not None: - # normalize using the refractive index. - K = (Im / eaxis).sum(axis=axis.index_in_array, keepdims=True) \ - * axis.scale - K = (K / (np.pi / 2) / (1 - 1. / n ** 2)) - # K = (K / (np.pi / 2) / (1 - 1. / n ** 2)).reshape( - # np.insert(K.shape, axis.index_in_array, 1)) - # Calculate the thickness only if possible and required - if zlp is not None and (full_output is True or - iterations > 1): - te = (332.5 * K * ke / i0) - if full_output is True: - output['thickness'] = te - elif t is not None: - if zlp is None: - raise ValueError("The ZLP must be provided when the " - "thickness is used for normalization.") - # normalize using the thickness - K = t * i0 / (332.5 * ke) - te = t - Im = Im / K - - # Kramers Kronig Transform: - # We calculate KKT(Im(-1/epsilon))=1+Re(1/epsilon) with FFT - # Follows: D W Johnson 1975 J. Phys. A: Math. Gen. 8 490 - # Use an optimal FFT size to speed up the calculation, and - # make it double the closest upper value to workaround the - # wrap-around problem. - esize = optimal_fft_size(2 * axis.size) - q = -2 * np.fft.fft(Im, esize, - axis.index_in_array).imag / esize - - q[slicer] *= -1 - q = np.fft.fft(q, axis=axis.index_in_array) - # Final touch, we have Re(1/eps) - Re = q[slicer].real + 1 - - # Egerton does this to correct the wrap-around problem, but in our - # case this is not necessary because we compute the fft on an - # extended and padded spectrum to avoid this problem. - # Re=real(q) - # Tail correction - # vm=Re[axis.size-1] - # Re[:(axis.size-1)]=Re[:(axis.size-1)]+1-(0.5*vm*((axis.size-1) / - # (axis.size*2-arange(0,axis.size-1)))**2) - # Re[axis.size:]=1+(0.5*vm*((axis.size-1) / - # (axis.size+arange(0,axis.size)))**2) - - # Epsilon appears: - # We calculate the real and imaginary parts of the CDF - e1 = Re / (Re ** 2 + Im ** 2) - e2 = Im / (Re ** 2 + Im ** 2) - - if iterations > 1 and zlp is not None: - # Surface losses correction: - # Calculates the surface ELF from a vacuum border effect - # A simulated surface plasmon is subtracted from the ELF - Srfelf = 4 * e2 / ((e1 + 1) ** 2 + e2 ** 2) - Im - adep = (tgt / (eaxis + delta) * - np.arctan(beta * tgt / axis.axis) - - beta / 1000. / - (beta ** 2 + axis.axis ** 2. / tgt ** 2)) - Srfint = 2000 * K * adep * Srfelf / rk0 / te * axis.scale - s.data = sorig.data - Srfint - _logger.debug('Iteration number: %d / %d', io + 1, iterations) - if iterations == io + 1 and full_output is True: - sp = sorig._deepcopy_with_new_data(Srfint) - sp.metadata.General.title += ( - " estimated surface plasmon excitation.") - output['surface plasmon estimation'] = sp - del sp - del Srfint - - eps = s._deepcopy_with_new_data(e1 + e2 * 1j) - del s - eps.set_signal_type("DielectricFunction") - eps.metadata.General.title = (self.metadata.General.title + - 'dielectric function ' - '(from Kramers-Kronig analysis)') - if eps.tmp_parameters.has_item('filename'): - eps.tmp_parameters.filename = ( - self.tmp_parameters.filename + - '_CDF_after_Kramers_Kronig_transform') - if 'thickness' in output: - # As above,prevent errors if the signal is a single spectrum - if len(te) != 1: - te = te[self.axes_manager._get_data_slice( - [(axis.index_in_array, 0)])] - thickness = eps._get_navigation_signal(data=te) - thickness.metadata.General.title = ( - self.metadata.General.title + ' thickness ' - '(calculated using Kramers-Kronig analysis)') - output['thickness'] = thickness - if full_output is False: - return eps - else: - return eps, output - - def create_model(self, ll=None, auto_background=True, auto_add_edges=True, - GOS=None, dictionary=None): - """Create a model for the current EELS data. - - Parameters - ---------- - ll : EELSSpectrum, optional - If an EELSSpectrum is provided, it will be assumed that it is - a low-loss EELS spectrum, and it will be used to simulate the - effect of multiple scattering by convolving it with the EELS - spectrum. - auto_background : bool, default True - If True, and if spectrum is an EELS instance adds automatically - a powerlaw to the model and estimate the parameters by the - two-area method. - auto_add_edges : bool, default True - If True, and if spectrum is an EELS instance, it will - automatically add the ionization edges as defined in the - Signal1D instance. Adding a new element to the spectrum using - the components.EELSSpectrum.add_elements method automatically - add the corresponding ionisation edges to the model. - GOS : {'hydrogenic' | 'Hartree-Slater'}, optional - The generalized oscillation strength calculations to use for the - core-loss EELS edges. If None the Hartree-Slater GOS are used if - available, otherwise it uses the hydrogenic GOS. - dictionary : {None | dict}, optional - A dictionary to be used to recreate a model. Usually generated - using :meth:`hyperspy.model.as_dictionary` - - Returns - ------- - model : `EELSModel` instance. - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - """ - from hyperspy.models.eelsmodel import EELSModel - if ll is not None and not self.axes_manager.signal_axes[0].is_uniform: - raise NotImplementedError( - "Multiple scattering is not implemented for spectra with a non-uniform energy axis. " - "To create a model that does not account for multiple-scattering do not set " - "the `ll` keyword.") - model = EELSModel(self, - ll=ll, - auto_background=auto_background, - auto_add_edges=auto_add_edges, - GOS=GOS, - dictionary=dictionary) - return model - - def plot(self, plot_edges=False, only_edges=('Major', 'Minor'), - **kwargs): - """Plot the EELS spectrum. Markers indicating the position of the - EELS edges can be added. - - Parameters - ---------- - plot_edges : {False, True, list of string or string} - If True, draws on s.metadata.Sample.elements for edges. - Alternatively, provide a string of a single edge, or an iterable - containing a list of valid elements, EELS families or edges. For - example, an element should be 'Zr', an element edge family should - be 'Zr_L' or an EELS edge 'Zr_L3'. - only_edges : tuple of string - Either 'Major' or 'Minor'. Defaults to both. - kwargs - The extra keyword arguments for plot() - """ - - super().plot(**kwargs) - - if plot_edges is not False: - edges = self._get_edges_to_plot(plot_edges, only_edges) - self.plot_edges_label(edges) - - def plot_edges_label(self, edges, vertical_line_marker=None, - text_marker=None): - """Put the EELS edge label (vertical line segment and text box) on - the signal - - Parameters - ---------- - edges : dictionary - A dictionary with the labels as keys and their energies as values. - For example, {'Fe_L2': 721.0, 'O_K': 532.0} - vertical_line_marker : list - A list contains HyperSpy's vertical line segment marker, if None, - determine from the given edges - text_marker : list - A list contains HyperSpy's text box marker, if None, - determine from the given edges - - Raises - ------ - ValueError - If the size of edges, vertical_line_marker and text_marker do not - match. - """ - - if vertical_line_marker is None or text_marker is None: - # get position of markers for edges if no marker is provided - # no marker provided, implies non-interactive mode - slp = SpectrumLabelPosition(self) - vertical_line_marker, text_marker = slp.get_markers(edges) - # the object is needed to connect replot method when axes_manager - # indices changed - _ = EdgesRange(self, active=list(edges.keys())) - if len(vertical_line_marker) != len(text_marker) or \ - len(edges) != len(vertical_line_marker): - raise ValueError('The size of edges, vertical_line_marker and ' - 'text_marker needs to be the same.') - - # add the markers to the signal and store them - self.add_marker(vertical_line_marker + text_marker, render_figure=False) - added = dict(zip(edges, map(list, zip(vertical_line_marker, text_marker)))) - self._edge_markers.update(added) - - def _get_edges_to_plot(self, plot_edges, only_edges): - # get the dictionary of the edge to be shown - extra_element_edge_family = [] - if plot_edges is True: - try: - elements = self.metadata.Sample.elements - except AttributeError: - raise ValueError("No elements defined. Add them with " - "s.add_elements, or specify elements, edge " - "families or edges directly") - else: - extra_element_edge_family.extend(np.atleast_1d(plot_edges)) - try: - elements = self.metadata.Sample.elements - except: - elements = [] - - element_edge_family = elements + extra_element_edge_family - edges_dict = self._get_edges(element_edge_family, only_edges) - - return edges_dict - - def _get_edges(self, element_edge_family, only_edges): - # get corresponding information depending on whether it is an element - # a particular edge or a family of edge - axis_min = self.axes_manager[-1].low_value - axis_max = self.axes_manager[-1].high_value - - names_and_energies = {} - shells = ["K", "L", "M", "N", "O"] - - errmsg = ("Edge family '{}' is not supported. Supported edge family " - "is {}.") - for member in element_edge_family: - try: - element, ss = member.split("_") - - if len(ss) == 1: - memtype = 'family' - if ss not in shells: - raise AttributeError(errmsg.format(ss, shells)) - if len(ss) == 2: - memtype = 'edge' - if ss[0] not in shells: - raise AttributeError(errmsg.format(ss[0], shells)) - except ValueError: - element = member - ss = '' - memtype = 'element' - - try: - Binding_energies = elements_db[element]["Atomic_properties"]["Binding_energies"] - except KeyError as err: - raise ValueError("'{}' is not a valid element".format(element)) from err - - for edge in Binding_energies.keys(): - relevance = Binding_energies[edge]["relevance"] - energy = Binding_energies[edge]["onset_energy (eV)"] - - isInRel = relevance in only_edges - isInRng = axis_min < energy < axis_max - isSameFamily = ss in edge - - if memtype == 'element': - flag = isInRel & isInRng - edge_key = element + "_" + edge - elif memtype == 'edge': - flag = isInRng & (edge == ss) - edge_key = member - elif memtype == 'family': - flag = isInRel & isInRng & isSameFamily - edge_key = element + "_" + edge - - if flag: - names_and_energies[edge_key] = energy - - return names_and_energies - - def _edge_marker_closed(self, obj): - marker = obj - for EELS_edge, line_markers in reversed(list( - self._edge_markers.items())): - if marker in line_markers: - line_markers.remove(marker) - if not line_markers: - self._edge_markers.pop(EELS_edge) - - def remove_EELS_edges_markers(self, EELS_edges): - for EELS_edge in EELS_edges: - if EELS_edge in self._edge_markers: - line_markers = self._edge_markers[EELS_edge] - while line_markers: - m = line_markers.pop() - m.close(render_figure=False) - - def get_complementary_edges(self, edges, only_major=False): - ''' Get other edges of the same element present within the energy - range of the axis - - Parameters - ---------- - edges : iterable - A sequence of strings contains edges in the format of - element_subshell for EELS. For example, ['Fe_L2', 'O_K'] - only_major : bool - Whether to show only the major edges. The default is False. - - Returns - ------- - complmt_edges : list - A list containing all the complementary edges of the same element - present within the energy range of the axis - ''' - - emin = self.axes_manager[-1].low_value - emax = self.axes_manager[-1].high_value - complmt_edges = [] - - elements = set() - for edge in edges: - element, _ = edge.split('_') - elements.update([element]) - - for element in elements: - ss_info = elements_db[element]['Atomic_properties'][ - 'Binding_energies'] - - for subshell in ss_info: - sse = ss_info[subshell]['onset_energy (eV)'] - ssr = ss_info[subshell]['relevance'] - - if only_major: - if ssr != 'Major': - continue - - edge = element + '_' + subshell - if (emin <= sse <= emax) and (subshell[-1] != 'a') and \ - (edge not in edges): - complmt_edges.append(edge) - - return complmt_edges - - def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, - out=None): - factors = self._validate_rebin_args_and_get_factors( - new_shape=new_shape, - scale=scale) - m = super().rebin(new_shape=new_shape, scale=scale, crop=crop, - dtype=dtype, out=out) - m = out or m - time_factor = np.prod([factors[axis.index_in_array] - for axis in m.axes_manager.navigation_axes]) - mdeels = m.metadata - m.get_dimensions_from_data() - if m.metadata.get_item("Acquisition_instrument.TEM.Detector.EELS"): - mdeels = m.metadata.Acquisition_instrument.TEM.Detector.EELS - if "dwell_time" in mdeels: - mdeels.dwell_time *= time_factor - if "exposure" in mdeels: - mdeels.exposure *= time_factor - else: - _logger.info('No dwell_time could be found in the metadata so ' - 'this has not been updated.') - if out is None: - return m - else: - out.events.data_changed.trigger(obj=out) - return m - rebin.__doc__ = hyperspy.signal.BaseSignal.rebin.__doc__ - - def vacuum_mask(self, threshold=10.0, start_energy=None, - closing=True, opening=False): - """ - Generate mask of the vacuum region - - Parameters - ---------- - threshold: float - For a given navigation coordinate, mean value in the energy axis - below which the pixel is considered as vacuum. - start_energy: float, None - Minimum energy included in the calculation of the mean intensity. - If None, consider only the last quarter of the spectrum to - calculate the mask. - closing: bool - If True, a morphological closing is applied to the mask. - opening: bool - If True, a morphological opening is applied to the mask. - - Returns - ------- - mask: signal - The mask of the region. - """ - if self.axes_manager.navigation_dimension == 0: - raise RuntimeError('Navigation dimenstion must be higher than 0 ' - 'to estimate a vacuum mask.') - signal_axis = self.axes_manager.signal_axes[0] - if start_energy is None: - start_energy = 0.75 * signal_axis.high_value - - mask = (self.isig[start_energy:].mean(-1) <= threshold) - - from scipy.ndimage.morphology import binary_dilation, binary_erosion - if closing: - mask.data = binary_dilation(mask.data, border_value=0) - mask.data = binary_erosion(mask.data, border_value=1) - if opening: - mask.data = binary_erosion(mask.data, border_value=1) - mask.data = binary_dilation(mask.data, border_value=0) - return mask - - -class LazyEELSSpectrum(EELSSpectrum, LazySignal1D): - - pass diff --git a/hyperspy/_signals/hologram_image.py b/hyperspy/_signals/hologram_image.py deleted file mode 100644 index c0daff5eb5..0000000000 --- a/hyperspy/_signals/hologram_image.py +++ /dev/null @@ -1,825 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import logging -from collections import OrderedDict -import scipy.constants as constants -import numpy as np -from dask.array import Array as daArray -from pint import UnitRegistry, UndefinedUnitError - -from hyperspy._signals.signal2d import Signal2D -from hyperspy.signal import BaseSignal -from hyperspy._signals.signal1d import Signal1D -from hyperspy._signals.lazy import LazySignal -from hyperspy.misc.holography.reconstruct import ( - reconstruct, estimate_sideband_position, estimate_sideband_size) -from hyperspy.misc.holography.tools import calculate_carrier_frequency, estimate_fringe_contrast_fourier -from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG - -_logger = logging.getLogger(__name__) - - -def _first_nav_pixel_data(s): - return s._data_aligned_with_axes[(0, ) * - s.axes_manager.navigation_dimension] - - -def _parse_sb_position(s, reference, sb_position, sb, high_cf, parallel): - - if sb_position is None: - _logger.warning('Sideband position is not specified. The sideband ' - 'will be found automatically which may cause ' - 'wrong results.') - if reference is None: - sb_position = s.estimate_sideband_position( - sb=sb, high_cf=high_cf, parallel=parallel) - else: - sb_position = reference.estimate_sideband_position( - sb=sb, high_cf=high_cf, parallel=parallel) - - else: - if isinstance(sb_position, BaseSignal) and \ - not sb_position._signal_dimension == 1: - raise ValueError('sb_position dimension has to be 1.') - - if not isinstance(sb_position, Signal1D): - sb_position = Signal1D(sb_position) - if isinstance(sb_position.data, daArray): - sb_position = sb_position.as_lazy() - - if not sb_position.axes_manager.signal_size == 2: - raise ValueError('sb_position should to have signal size of 2.') - - if sb_position.axes_manager.navigation_size != s.axes_manager.navigation_size: - if sb_position.axes_manager.navigation_size: - raise ValueError('Sideband position dimensions do not match' - ' neither reference nor hologram dimensions.') - # sb_position navdim=0, therefore map function should not iterate: - else: - sb_position_temp = sb_position.data - else: - sb_position_temp = sb_position.deepcopy() - return sb_position, sb_position_temp - - -def _parse_sb_size(s, reference, sb_position, sb_size, parallel): - # Default value is 1/2 distance between sideband and central band - if sb_size is None: - if reference is None: - sb_size = s.estimate_sideband_size( - sb_position, parallel=parallel) - else: - sb_size = reference.estimate_sideband_size( - sb_position, parallel=parallel) - else: - if not isinstance(sb_size, BaseSignal): - if isinstance(sb_size, - (np.ndarray, daArray)) and sb_size.size > 1: - # transpose if np.array of multiple instances - sb_size = BaseSignal(sb_size).T - else: - sb_size = BaseSignal(sb_size) - if isinstance(sb_size.data, daArray): - sb_size = sb_size.as_lazy() - if sb_size.axes_manager.navigation_size != s.axes_manager.navigation_size: - if sb_size.axes_manager.navigation_size: - raise ValueError('Sideband size dimensions do not match ' - 'neither reference nor hologram dimensions.') - # sb_position navdim=0, therefore map function should not iterate: - else: - sb_size_temp = np.float64(sb_size.data) - else: - sb_size_temp = sb_size.deepcopy() - return sb_size, sb_size_temp - - -def _estimate_fringe_contrast_statistical(holo): - """ - Estimates average fringe contrast of a hologram using statistical definition: - V = STD / MEAN. - - Parameters - ---------- - holo_data: ndarray - The data of the hologram. - - Returns - ------- - Fringe contrast as a float - """ - - axes = holo.axes_manager.signal_axes - - return holo.std(axes) / holo.mean(axes) - - -class HologramImage(Signal2D): - """Image subclass for holograms acquired via off-axis electron holography.""" - - _signal_type = 'hologram' - - def set_microscope_parameters(self, - beam_energy=None, - biprism_voltage=None, - tilt_stage=None): - """Set the microscope parameters. - - If no arguments are given, raises an interactive mode to fill - the values. - - Parameters - ---------- - beam_energy: float - The energy of the electron beam in keV - biprism_voltage : float - In volts - tilt_stage : float - In degrees - - Examples - -------- - - >>> s.set_microscope_parameters(beam_energy=300.) - >>> print('Now set to %s keV' % - >>> s.metadata.Acquisition_instrument. - >>> TEM.beam_energy) - - Now set to 300.0 keV - - """ - md = self.metadata - - if beam_energy is not None: - md.set_item("Acquisition_instrument.TEM.beam_energy", beam_energy) - if biprism_voltage is not None: - md.set_item("Acquisition_instrument.TEM.Biprism.voltage", - biprism_voltage) - if tilt_stage is not None: - md.set_item( - "Acquisition_instrument.TEM.Stage.tilt_alpha", - tilt_stage) - - def estimate_sideband_position( - self, - ap_cb_radius=None, - sb='lower', - high_cf=True, - show_progressbar=False, - parallel=None, - max_workers=None, - ): - """ - Estimates the position of the sideband and returns its position. - - Parameters - ---------- - ap_cb_radius: float, None - The aperture radius used to mask out the centerband. - sb : str, optional - Chooses which sideband is taken. 'lower' or 'upper' - high_cf : bool, optional - If False, the highest carrier frequency allowed for the sideband location is equal to - half of the Nyquist frequency (Default: True). - %s - %s - %s - - Returns - ------- - Signal1D instance of sideband positions (y, x), referred to the unshifted FFT. - - Raises - ------ - NotImplementedError - If the signal axes are non-uniform axes. - - Examples - -------- - - >>> import hyperspy.api as hs - >>> s = hs.datasets.example_signals.object_hologram() - >>> sb_position = s.estimate_sideband_position() - >>> sb_position.data - - array([124, 452]) - """ - for axis in self.axes_manager.signal_axes: - if not axis.is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes.") - - sb_position = self.map( - estimate_sideband_position, - holo_sampling=(self.axes_manager.signal_axes[0].scale, - self.axes_manager.signal_axes[1].scale), - central_band_mask_radius=ap_cb_radius, - sb=sb, - high_cf=high_cf, - show_progressbar=show_progressbar, - inplace=False, - parallel=parallel, - max_workers=max_workers, - ragged=False) - - # Workaround to a map disfunctionality: - sb_position.set_signal_type('signal1d') - - return sb_position - - estimate_sideband_position.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) - - def estimate_sideband_size( - self, - sb_position, - show_progressbar=False, - parallel=None, - max_workers=None, - ): - """ - Estimates the size of the sideband and returns its size. - - Parameters - ---------- - sb_position : BaseSignal - The sideband position (y, x), referred to the non-shifted FFT. - %s - %s - %s - - Returns - ------- - sb_size : Signal1D - Sideband size referred to the unshifted FFT. - - Raises - ------ - NotImplementedError - If the signal axes are non-uniform axes. - - Examples - -------- - >>> import hyperspy.api as hs - >>> s = hs.datasets.example_signals.object_hologram() - >>> sb_position = s.estimate_sideband_position() - >>> sb_size = s.estimate_sideband_size(sb_position) - >>> sb_size.data - array([ 68.87670143]) - """ - for axis in self.axes_manager.signal_axes: - if not axis.is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes.") - - sb_size = sb_position.map( - estimate_sideband_size, - holo_shape=self.axes_manager.signal_shape[::-1], - show_progressbar=show_progressbar, - inplace=False, - parallel=parallel, - max_workers=max_workers, - ragged=False) - - return sb_size - - estimate_sideband_size.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) - - def reconstruct_phase( - self, - reference=None, - sb_size=None, - sb_smoothness=None, - sb_unit=None, - sb='lower', - sb_position=None, - high_cf=True, - output_shape=None, - plotting=False, - store_parameters=True, - show_progressbar=False, - parallel=None, - max_workers=None, - ): - """Reconstruct electron holograms. Operates on multidimensional - hyperspy signals. There are several usage schemes: - - * Reconstruct 1d or Nd hologram without reference - * Reconstruct 1d or Nd hologram using single reference hologram - * Reconstruct Nd hologram using Nd reference hologram (applies each - reference to each hologram in Nd stack) - - The reconstruction parameters (sb_position, sb_size, sb_smoothness) - have to be 1d or to have same dimensionality as the hologram. - - Parameters - ---------- - reference : ndarray, Signal2D, None - Vacuum reference hologram. - sb_size : float, ndarray, BaseSignal, None - Sideband radius of the aperture in corresponding unit (see - 'sb_unit'). If None, the radius of the aperture is set to 1/3 of - the distance between sideband and center band. - sb_smoothness : float, ndarray, BaseSignal, None - Smoothness of the aperture in the same unit as sb_size. - sb_unit : str, None - Unit of the two sideband parameters 'sb_size' and 'sb_smoothness'. - Default: None - Sideband size given in pixels - 'nm': Size and smoothness of the aperture are given in 1/nm. - 'mrad': Size and smoothness of the aperture are given in mrad. - sb : str, None - Select which sideband is selected. 'upper' or 'lower'. - sb_position : tuple, Signal1D, None - The sideband position (y, x), referred to the non-shifted FFT. If - None, sideband is determined automatically from FFT. - high_cf : bool, optional - If False, the highest carrier frequency allowed for the sideband - location is equal to half of the Nyquist frequency (Default: True). - output_shape: tuple, None - Choose a new output shape. Default is the shape of the input - hologram. The output shape should not be larger than the input - shape. - plotting : bool - Shows details of the reconstruction (i.e. SB selection). - store_parameters : bool - Store reconstruction parameters in metadata - %s - %s - %s - - Returns - ------- - wave : ComplexSignal2D - Reconstructed electron wave. By default object wave is divided by - reference wave. - - Raises - ------ - NotImplementedError - If the signal axes are non-uniform axes. - - Examples - -------- - >>> import hyperspy.api as hs - >>> s = hs.datasets.example_signals.object_hologram() - >>> sb_position = s.estimate_sideband_position() - >>> sb_size = s.estimate_sideband_size(sb_position) - >>> wave = s.reconstruct_phase(sb_position=sb_position, sb_size=sb_size) - - """ - - # TODO: Use defaults for choosing sideband, smoothness, relative filter - # size and output shape if not provided - # TODO: Plot FFT with marked SB and SB filter if plotting is enabled - - for axis in self.axes_manager.signal_axes: - if not axis.is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes.") - - # Parsing reference: - if not isinstance(reference, HologramImage): - if isinstance(reference, Signal2D): - if (not reference.axes_manager.navigation_shape == - self.axes_manager.navigation_shape and - reference.axes_manager.navigation_size): - - raise ValueError('The navigation dimensions of object and' - 'reference holograms do not match') - - _logger.warning('The reference image signal type is not ' - 'HologramImage. It will be converted to ' - 'HologramImage automatically.') - reference.set_signal_type('hologram') - elif reference is not None: - reference = HologramImage(reference) - if isinstance(reference.data, daArray): - reference = reference.as_lazy() - - # Testing match of navigation axes of reference and self - # (exception: reference nav_dim=1): - if (reference and not reference.axes_manager.navigation_shape == - self.axes_manager.navigation_shape and - reference.axes_manager.navigation_size): - - raise ValueError('The navigation dimensions of object and ' - 'reference holograms do not match') - - if reference and not reference.axes_manager.signal_shape == self.axes_manager.signal_shape: - - raise ValueError('The signal dimensions of object and reference' - ' holograms do not match') - - # Parsing sideband position: - (sb_position, sb_position_temp) = _parse_sb_position(self, - reference, - sb_position, - sb, - high_cf, - parallel) - - # Parsing sideband size: - (sb_size, sb_size_temp) = _parse_sb_size(self, - reference, - sb_position, - sb_size, - parallel) - - # Standard edge smoothness of sideband aperture 5% of sb_size - if sb_smoothness is None: - sb_smoothness = sb_size * 0.05 - else: - if not isinstance(sb_smoothness, BaseSignal): - if isinstance( - sb_smoothness, - (np.ndarray, daArray)) and sb_smoothness.size > 1: - sb_smoothness = BaseSignal(sb_smoothness).T - else: - sb_smoothness = BaseSignal(sb_smoothness) - if isinstance(sb_smoothness.data, daArray): - sb_smoothness = sb_smoothness.as_lazy() - - if sb_smoothness.axes_manager.navigation_size != self.axes_manager.navigation_size: - if sb_smoothness.axes_manager.navigation_size: - raise ValueError('Sideband smoothness dimensions do not match' - ' neither reference nor hologram ' - 'dimensions.') - # sb_position navdim=0, therefore map function should not iterate - # it: - else: - sb_smoothness_temp = np.float64(sb_smoothness.data) - else: - sb_smoothness_temp = sb_smoothness.deepcopy() - - # Convert sideband size from 1/nm or mrad to pixels - if sb_unit == 'nm': - f_sampling = np.divide( - 1, - [a * b for a, b in - zip(self.axes_manager.signal_shape, - (self.axes_manager.signal_axes[0].scale, - self.axes_manager.signal_axes[1].scale))] - ) - sb_size_temp = sb_size_temp / np.mean(f_sampling) - sb_smoothness_temp = sb_smoothness_temp / np.mean(f_sampling) - elif sb_unit == 'mrad': - f_sampling = np.divide( - 1, - [a * b for a, b in - zip(self.axes_manager.signal_shape, - (self.axes_manager.signal_axes[0].scale, - self.axes_manager.signal_axes[1].scale))] - ) - try: - ht = self.metadata.Acquisition_instrument.TEM.beam_energy - except BaseException: - raise AttributeError("Please define the beam energy." - "You can do this e.g. by using the " - "set_microscope_parameters method") - - momentum = 2 * constants.m_e * constants.elementary_charge * ht * \ - 1000 * (1 + constants.elementary_charge * ht * - 1000 / (2 * constants.m_e * constants.c ** 2)) - wavelength = constants.h / np.sqrt(momentum) * 1e9 # in nm - sb_size_temp = sb_size_temp / (1000 * wavelength * - np.mean(f_sampling)) - sb_smoothness_temp = sb_smoothness_temp / (1000 * wavelength * - np.mean(f_sampling)) - - # Find output shape: - if output_shape is None: - # Future improvement will give a possibility to choose - # if sb_size.axes_manager.navigation_size > 0: - # output_shape = (int(sb_size.inav[0].data*2), int(sb_size.inav[0].data*2)) - # else: - # output_shape = (int(sb_size.data*2), int(sb_size.data*2)) - output_shape = self.axes_manager.signal_shape - output_shape = output_shape[::-1] - - # Logging the reconstruction parameters if appropriate: - _logger.info('Sideband position in pixels: {}'.format(sb_position)) - _logger.info('Sideband aperture radius in pixels: {}'.format(sb_size)) - _logger.info('Sideband aperture smoothness in pixels: {}'.format( - sb_smoothness)) - - # Reconstructing object electron wave: - - # Checking if reference is a single image, which requires sideband - # parameters as a nparray to avoid iteration trough those: - wave_object = self.map( - reconstruct, - holo_sampling=(self.axes_manager.signal_axes[0].scale, - self.axes_manager.signal_axes[1].scale), - sb_size=sb_size_temp, - sb_position=sb_position_temp, - sb_smoothness=sb_smoothness_temp, - output_shape=output_shape, - plotting=plotting, - show_progressbar=show_progressbar, - inplace=False, - parallel=parallel, - max_workers=max_workers, - ragged=False) - - # Reconstructing reference wave and applying it (division): - if reference is None: - wave_reference = 1 - # case when reference is 1d - elif reference.axes_manager.navigation_size != self.axes_manager.navigation_size: - - # Prepare parameters for reconstruction of the reference wave: - - if reference.axes_manager.navigation_size == 0 and \ - sb_position.axes_manager.navigation_size > 0: - # 1d reference, but parameters are multidimensional - sb_position_ref = _first_nav_pixel_data(sb_position_temp) - else: - sb_position_ref = sb_position_temp - - if reference.axes_manager.navigation_size == 0 and \ - sb_size.axes_manager.navigation_size > 0: - # 1d reference, but parameters are multidimensional - sb_size_ref = _first_nav_pixel_data(sb_size_temp) - else: - sb_size_ref = sb_size_temp - - if reference.axes_manager.navigation_size == 0 and \ - sb_smoothness.axes_manager.navigation_size > 0: - # 1d reference, but parameters are multidimensional - sb_smoothness_ref = np.float64( - _first_nav_pixel_data(sb_smoothness_temp)) - else: - sb_smoothness_ref = sb_smoothness_temp - # - - wave_reference = reference.map( - reconstruct, - holo_sampling=(self.axes_manager.signal_axes[0].scale, - self.axes_manager.signal_axes[1].scale), - sb_size=sb_size_ref, - sb_position=sb_position_ref, - sb_smoothness=sb_smoothness_ref, - output_shape=output_shape, - plotting=plotting, - show_progressbar=show_progressbar, - inplace=False, - parallel=parallel, - max_workers=max_workers, - ragged=False) - - else: - wave_reference = reference.map( - reconstruct, - holo_sampling=(self.axes_manager.signal_axes[0].scale, - self.axes_manager.signal_axes[1].scale), - sb_size=sb_size_temp, - sb_position=sb_position_temp, - sb_smoothness=sb_smoothness_temp, - output_shape=output_shape, - plotting=plotting, - show_progressbar=show_progressbar, - inplace=False, - parallel=parallel, - max_workers=max_workers, - ragged=False) - - wave_image = wave_object / wave_reference - - # New signal is a complex - wave_image.set_signal_type('complex_signal2d') - - wave_image.axes_manager.signal_axes[0].scale = \ - self.axes_manager.signal_axes[0].scale * \ - self.axes_manager.signal_shape[0] / output_shape[1] - wave_image.axes_manager.signal_axes[1].scale = \ - self.axes_manager.signal_axes[1].scale * \ - self.axes_manager.signal_shape[1] / output_shape[0] - - # Reconstruction parameters are stored in - # holo_reconstruction_parameters: - - if store_parameters: - rec_param_dict = OrderedDict( - [('sb_position', sb_position_temp), ('sb_size', sb_size_temp), - ('sb_units', sb_unit), ('sb_smoothness', sb_smoothness_temp)]) - wave_image.metadata.Signal.add_node('Holography') - wave_image.metadata.Signal.Holography.add_node( - 'Reconstruction_parameters') - wave_image.metadata.Signal.Holography.Reconstruction_parameters.add_dictionary( - rec_param_dict) - _logger.info('Reconstruction parameters stored in metadata') - - return wave_image - - reconstruct_phase.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) - - def statistics( - self, - sb_position=None, - sb='lower', - high_cf=False, - fringe_contrast_algorithm='statistical', - apodization='hanning', - single_values=True, - show_progressbar=False, - parallel=None, - max_workers=None, - ): - """ - Calculates following statistics for off-axis electron holograms: - - 1. Fringe contrast using either statistical definition or - Fourier space approach (see description of `fringe_contrast_algorithm` parameter) - 2. Fringe sampling (in pixels) - 3. Fringe spacing (in calibrated units) - 4. Carrier frequency (in calibrated units, radians and 1/px) - - Parameters - ---------- - sb_position : tuple, Signal1D, None - The sideband position (y, x), referred to the non-shifted FFT. - It has to be tuple or to have the same dimensionality as the hologram. - If None, sideband is determined automatically from FFT. - sb : str, None - Select which sideband is selected. 'upper', 'lower', 'left' or 'right'. - high_cf : bool, optional - If False, the highest carrier frequency allowed for the sideband location is equal to - half of the Nyquist frequency (Default: False). - fringe_contrast_algorithm : str - Select fringe contrast algorithm between: - - * 'fourier': fringe contrast is estimated as 2 * / , - where I(k_0) is intensity of sideband and I(0) is the intensity of central band (FFT origin). - This method delivers also reasonable estimation if the - interference pattern do not cover full field of view. - * 'statistical': fringe contrast is estimated by dividing the - standard deviation by the mean of the hologram intensity in real - space. This algorithm relies on regularly spaced fringes and - covering the entire field of view. - - (Default: 'statistical') - apodization: str or None, optional - Used with `fringe_contrast_algorithm='fourier'`. If 'hanning' or 'hamming' apodization window - will be applied in real space before FFT for estimation of fringe contrast. - Apodization is typically needed to suppress striking due to sharp edges of the image, - which often results in underestimation of the fringe contrast. (Default: 'hanning') - single_values : bool, optional - If True calculates statistics only for the first navigation pixels and - returns the values as single floats (Default: True) - %s - %s - %s - - Returns - ------- - statistics_dict : - Dictionary with the statistics - - Raises - ------ - NotImplementedError - If the signal axes are non-uniform axes. - - Examples - -------- - >>> import hyperspy.api as hs - >>> s = hs.datasets.example_signals.reference_hologram() - >>> sb_position = s.estimate_sideband_position(high_cf=True) - >>> s.statistics(sb_position=sb_position) - {'Fringe spacing (nm)': 3.4860442674236256, - 'Carrier frequency (1/px)': 0.26383819985575441, - 'Carrier frequency (mrad)': 0.56475154609203482, - 'Fringe contrast': 0.071298357213623778, - 'Fringe sampling (px)': 3.7902017241882331, - 'Carrier frequency (1 / nm)': 0.28685808994016415} - """ - - for axis in self.axes_manager.signal_axes: - if not axis.is_uniform: - raise NotImplementedError( - "This operation is not yet implemented for non-uniform energy axes.") - # Testing match of navigation axes of reference and self - # (exception: reference nav_dim=1): - - # Parsing sideband position: - (sb_position, sb_position_temp) = _parse_sb_position( - self, None, sb_position, sb, high_cf, parallel) - - # Calculate carrier frequency in 1/px and fringe sampling: - fourier_sampling = 1. / np.array(self.axes_manager.signal_shape) - if single_values: - carrier_freq_px = calculate_carrier_frequency(_first_nav_pixel_data(self), - sb_position=_first_nav_pixel_data( - sb_position), - scale=fourier_sampling) - else: - carrier_freq_px = self.map(calculate_carrier_frequency, - sb_position=sb_position, - scale=fourier_sampling, - inplace=False, - ragged=False, - show_progressbar=show_progressbar, - parallel=parallel, - max_workers=max_workers) - fringe_sampling = np.divide(1., carrier_freq_px) - - ureg = UnitRegistry() - try: - units = ureg.parse_expression( - str(self.axes_manager.signal_axes[0].units)) - except UndefinedUnitError: - raise ValueError('Signal axes units should be defined.') - - # Calculate carrier frequency in 1/units and fringe spacing in units: - f_sampling_units = np.divide( - 1., - [a * b for a, b in - zip(self.axes_manager.signal_shape, - (self.axes_manager.signal_axes[0].scale, - self.axes_manager.signal_axes[1].scale))] - ) - if single_values: - carrier_freq_units = calculate_carrier_frequency(_first_nav_pixel_data(self), - sb_position=_first_nav_pixel_data( - sb_position), - scale=f_sampling_units) - else: - carrier_freq_units = self.map(calculate_carrier_frequency, - sb_position=sb_position, - scale=f_sampling_units, - inplace=False, - ragged=False, - show_progressbar=show_progressbar, - parallel=parallel, - max_workers=max_workers) - fringe_spacing = np.divide(1., carrier_freq_units) - - # Calculate carrier frequency in mrad: - try: - ht = self.metadata.Acquisition_instrument.TEM.beam_energy - except BaseException: - raise AttributeError("Please define the beam energy." - "You can do this e.g. by using the " - "set_microscope_parameters method.") - - momentum = 2 * constants.m_e * constants.elementary_charge * ht * \ - 1000 * (1 + constants.elementary_charge * ht * - 1000 / (2 * constants.m_e * constants.c ** 2)) - wavelength = constants.h / np.sqrt(momentum) * 1e9 # in nm - carrier_freq_quantity = wavelength * \ - ureg('nm') * carrier_freq_units / units * ureg('rad') - carrier_freq_mrad = carrier_freq_quantity.to('mrad').magnitude - - # Calculate fringe contrast: - if fringe_contrast_algorithm == 'fourier': - if single_values: - fringe_contrast = estimate_fringe_contrast_fourier(_first_nav_pixel_data(self), - sb_position=_first_nav_pixel_data( - sb_position), - apodization=apodization) - else: - fringe_contrast = self.map(estimate_fringe_contrast_fourier, - sb_position=sb_position, - apodization=apodization, - inplace=False, - ragged=False, - show_progressbar=show_progressbar, - parallel=parallel, - max_workers=max_workers) - elif fringe_contrast_algorithm == 'statistical': - if single_values: - fringe_contrast = _first_nav_pixel_data( - self).std() / _first_nav_pixel_data(self).mean() - else: - fringe_contrast = _estimate_fringe_contrast_statistical(self) - else: - raise ValueError( - "fringe_contrast_algorithm can only be set to fourier or statistical.") - - return {'Fringe contrast': fringe_contrast, - 'Fringe sampling (px)': fringe_sampling, - 'Fringe spacing ({:~})'.format(units.units): fringe_spacing, - 'Carrier frequency (1/px)': carrier_freq_px, - 'Carrier frequency ({:~})'.format((1. / units).units): carrier_freq_units, - 'Carrier frequency (mrad)': carrier_freq_mrad} - - statistics.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) - - -class LazyHologramImage(LazySignal, HologramImage): - - _lazy = True diff --git a/hyperspy/_signals/lazy.py b/hyperspy/_signals/lazy.py index 4d93b3565b..a07c5c9a2c 100644 --- a/hyperspy/_signals/lazy.py +++ b/hyperspy/_signals/lazy.py @@ -1,50 +1,76 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging +import os from functools import partial -import warnings - -import numpy as np -import dask.array as da -import dask.delayed as dd -import dask -from dask.diagnostics import ProgressBar from itertools import product -from distutils.version import LooseVersion +import dask +import dask.array as da +import numpy as np +from rsciio.utils import rgb_tools +from rsciio.utils.tools import get_file_handle -from hyperspy.signal import BaseSignal from hyperspy.defaults_parser import preferences -from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG -from hyperspy.exceptions import VisibleDeprecationWarning +from hyperspy.docstrings.signal import ( + LAZYSIGNAL_DOC, + MANY_AXIS_PARAMETER, + SHOW_PROGRESSBAR_ARG, +) from hyperspy.external.progressbar import progressbar -from hyperspy.misc.array_tools import (_requires_linear_rebin, - get_signal_chunk_slice) -from hyperspy.misc.hist_tools import histogram_dask +from hyperspy.misc.array_tools import ( + _get_navigation_dimension_chunk_slice, + _requires_linear_rebin, + get_signal_chunk_slice, +) +from hyperspy.misc.hist_tools import _set_histogram_metadata, histogram_dask from hyperspy.misc.machine_learning import import_sklearn -from hyperspy.misc.utils import (multiply, dummy_context_manager, isiterable, - process_function_blockwise, guess_output_signal_size,) - +from hyperspy.misc.utils import dummy_context_manager, isiterable, multiply +from hyperspy.signal import BaseSignal _logger = logging.getLogger(__name__) -lazyerror = NotImplementedError('This method is not available in lazy signals') +lazyerror = NotImplementedError("This method is not available in lazy signals") + + +try: + from dask.widgets import TEMPLATE_PATHS + + templates_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "..", "misc", "dask_widgets" + ) + TEMPLATE_PATHS.append(templates_path) +except ModuleNotFoundError: + _logger.info("Dask widgets not loaded (dask >=2021.11.1 is required)") + + +def _get(): + try: + get = dask.threaded.get + except AttributeError: # pragma: no cover + # For pyodide + get = dask.get + _logger.warning( + "Dask scheduler with threads is not available in this environment. " + "Falling back to synchronous scheduler (single-threaded)." + ) + return get def to_array(thing, chunks=None): @@ -86,43 +112,149 @@ def to_array(thing, chunks=None): class LazySignal(BaseSignal): - """A Lazy Signal instance that delays computation until explicitly saved - (assuming storing the full result of computation in memory is not feasible) - """ + """Lazy general signal class.""" + _lazy = True + __doc__ += LAZYSIGNAL_DOC.replace("__BASECLASS__", "BaseSignal") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # The _cache_dask_chunk and _cache_dask_chunk_slice attributes are + # used to temporarily cache data contained in one chunk, when + # self.__call__ is used. Typically done when using plot or fitting. + # _cache_dask_chunk has the NumPy array itself, while + # _cache_dask_chunk_slice has the navigation dimension chunk which + # the NumPy array originates from. + self._cache_dask_chunk = None + self._cache_dask_chunk_slice = None + if self._clear_cache_dask_data not in self.events.data_changed.connected: + self.events.data_changed.connect(self._clear_cache_dask_data) + + __init__.__doc__ = BaseSignal.__init__.__doc__.replace( + ":class:`numpy.ndarray`", ":class:`dask.array.Array`" + ) + + def _repr_html_(self): + try: + from dask import config + from dask.array.svg import svg + from dask.utils import format_bytes + from dask.widgets import get_template + + nav_chunks = self.get_chunk_size(self.axes_manager.navigation_axes) + sig_chunks = self.get_chunk_size(self.axes_manager.signal_axes) + if nav_chunks == (): + nav_grid = "" + else: + nav_grid = svg( + chunks=nav_chunks, size=config.get("array.svg.size", 160) + ) + if sig_chunks == (): + sig_grid = "" + else: + sig_grid = svg( + chunks=sig_chunks, size=config.get("array.svg.size", 160) + ) + nbytes = format_bytes(self.data.nbytes) + cbytes = format_bytes( + np.prod(self.data.chunksize) * self.data.dtype.itemsize + ) + return get_template("lazy_signal.html.j2").render( + nav_grid=nav_grid, + sig_grid=sig_grid, + dim=self.axes_manager._get_dimension_str(), + chunks=self._get_chunk_string(), + array=self.data, + signal_type=self._signal_type, + nbytes=nbytes, + cbytes=cbytes, + title=self.metadata.General.title, + ) + + except ModuleNotFoundError: + return self + + def _get_chunk_string(self): + nav_chunks = self.data.chunksize[: len(self.axes_manager.navigation_shape)][ + ::-1 + ] + string = "(" + for chunks, axis in zip(nav_chunks, self.axes_manager.navigation_shape): + if chunks == axis: + string += "" + str(chunks) + "," + else: + string += str(chunks) + "," + string = string.rstrip(",") + string += "|" + + sig_chunks = self.data.chunksize[len(self.axes_manager.navigation_shape) :][ + ::-1 + ] + for chunks, axis in zip(sig_chunks, self.axes_manager.signal_shape): + if chunks == axis: + string += "" + str(chunks) + "," + else: + string += str(chunks) + "," + string = string.rstrip(",") + string += ")" + return string def compute(self, close_file=False, show_progressbar=None, **kwargs): - """Attempt to store the full signal in memory. + """ + Attempt to store the full signal in memory. Parameters ---------- close_file : bool, default False - If True, attemp to close the file associated with the dask + If True, attempt to close the file associated with the dask array data if any. Note that closing the file will make all other associated lazy signals inoperative. %s + **kwargs : dict + Any other keyword arguments for :meth:`dask.array.Array.compute`. + For example `scheduler` or `num_workers`. Returns ------- None - """ - if "progressbar" in kwargs: - warnings.warn( - "The `progressbar` keyword is deprecated and will be removed " - "in HyperSpy 2.0. Use `show_progressbar` instead.", - VisibleDeprecationWarning, - ) - show_progressbar = kwargs["progressbar"] + Notes + ----- + For alternative ways to set the compute settings see + https://docs.dask.org/en/stable/scheduling.html#configuration + + Examples + -------- + >>> import dask.array as da + >>> data = da.zeros((100, 100, 100), chunks=(10, 20, 20)) + >>> s = hs.signals.Signal2D(data).as_lazy() + + With default parameters + + >>> s1 = s.deepcopy() + >>> s1.compute() + + Using 2 workers, which can reduce the memory usage (depending on + the data and your computer hardware). Note that `num_workers` only + work for the 'threads' and 'processes' `scheduler`. + + >>> s2 = s.deepcopy() + >>> s2.compute(num_workers=2) + + Using a single threaded scheduler, which is useful for debugging + + >>> s3 = s.deepcopy() + >>> s3.compute(scheduler='single-threaded') + """ if show_progressbar is None: show_progressbar = preferences.General.show_progressbar - cm = ProgressBar if show_progressbar else dummy_context_manager + cm = dask.diagnostics.ProgressBar if show_progressbar else dummy_context_manager with cm(): da = self.data - data = da.compute() + data = da.compute(**kwargs) if close_file: self.close_file() self.data = data @@ -132,11 +264,7 @@ def compute(self, close_file=False, show_progressbar=None, **kwargs): compute.__doc__ %= SHOW_PROGRESSBAR_ARG - def rechunk(self, - nav_chunks="auto", - sig_chunks=-1, - inplace=True, - **kwargs): + def rechunk(self, nav_chunks="auto", sig_chunks=-1, inplace=True, **kwargs): """Rechunks the data using the same rechunking formula from Dask expect that the navigation and signal chunks are defined seperately. Note, for most functions sig_chunks should remain ``None`` so that it @@ -153,21 +281,17 @@ def rechunk(self, -1 indicates the full size of the corresponding dimension. Default is -1 which automatically spans the full signal dimension **kwargs : dict - Any other keyword arguments for :py:func:`dask.array.rechunk`. + Any other keyword arguments for :func:`dask.array.rechunk`. """ if not isinstance(sig_chunks, tuple): - sig_chunks = (sig_chunks,)*len(self.axes_manager.signal_shape) + sig_chunks = (sig_chunks,) * len(self.axes_manager.signal_shape) if not isinstance(nav_chunks, tuple): - nav_chunks = (nav_chunks,)*len(self.axes_manager.navigation_shape) + nav_chunks = (nav_chunks,) * len(self.axes_manager.navigation_shape) new_chunks = nav_chunks + sig_chunks if inplace: - self.data = self.data.rechunk(new_chunks, - **kwargs) + self.data = self.data.rechunk(new_chunks, **kwargs) else: - return self._deepcopy_with_new_data(self.data.rechunk(new_chunks, - **kwargs) - ) - + return self._deepcopy_with_new_data(self.data.rechunk(new_chunks, **kwargs)) def close_file(self): """Closes the associated data file if any. @@ -176,16 +300,14 @@ def close_file(self): array created from an h5py DataSet (default HyperSpy hdf5 reader). """ - arrkey = None - for key in self.data.dask.keys(): - if "array-original" in key: - arrkey = key - break - if arrkey: - try: - self.data.dask[arrkey].file.close() - except AttributeError: - _logger.exception("Failed to close lazy Signal file") + try: + get_file_handle(self.data).close() + except AttributeError: + _logger.warning("Failed to close lazy signal file") + + def _clear_cache_dask_data(self, obj=None): + self._cache_dask_chunk = None + self._cache_dask_chunk_slice = None def _get_dask_chunks(self, axis=None, dtype=None): """Returns dask chunks. @@ -216,7 +338,9 @@ def _get_dask_chunks(self, axis=None, dtype=None): if axis is not None: need_axes = self.axes_manager[axis] if not np.iterable(need_axes): - need_axes = [need_axes, ] + need_axes = [ + need_axes, + ] else: need_axes = self.axes_manager.signal_axes @@ -228,21 +352,18 @@ def _get_dask_chunks(self, axis=None, dtype=None): want_to_keep = multiply([ax.size for ax in need_axes]) * typesize # @mrocklin reccomends to have around 100MB chunks, so we do that: - num_that_fit = int(100. * 2.**20 / want_to_keep) + num_that_fit = int(100.0 * 2.0**20 / want_to_keep) # want to have at least one "signal" per chunk if num_that_fit < 2: chunks = [tuple(1 for _ in range(i)) for i in dc.shape] for ax in need_axes: - chunks[ax.index_in_array] = dc.shape[ax.index_in_array], + chunks[ax.index_in_array] = (dc.shape[ax.index_in_array],) return tuple(chunks) - sizes = [ - ax.size for ax in self.axes_manager._axes if ax not in need_axes - ] + sizes = [ax.size for ax in self.axes_manager._axes if ax not in need_axes] indices = [ - ax.index_in_array for ax in self.axes_manager._axes - if ax not in need_axes + ax.index_in_array for ax in self.axes_manager._axes if ax not in need_axes ] while True: @@ -257,39 +378,71 @@ def _get_dask_chunks(self, axis=None, dtype=None): if i in indices: size = float(dc.shape[i]) split_array = np.array_split( - np.arange(size), np.ceil(size / sizes[indices.index(i)])) + np.arange(size), np.ceil(size / sizes[indices.index(i)]) + ) chunks.append(tuple(len(sp) for sp in split_array)) else: - chunks.append((dc.shape[i], )) + chunks.append((dc.shape[i],)) return tuple(chunks) - def _get_navigation_chunk_size(self): - nav_axes = self.axes_manager.navigation_indices_in_array - nav_chunks = tuple([self.data.chunks[i] for i in sorted(nav_axes)]) - return nav_chunks + def get_chunk_size(self, axes=None): + """ + Returns the chunk size as tuple for a set of given axes. The order + of the returned tuple follows the order of the dask array. + + Parameters + ---------- + axes : %s + + Examples + -------- + >>> import dask.array as da + >>> data = da.random.random((10, 200, 300)) + >>> data.chunksize + (10, 200, 300) + >>> s = hs.signals.Signal1D(data).as_lazy() + >>> s.get_chunk_size() # All navigation axes + ((10,), (200,)) + >>> s.get_chunk_size(0) # The first navigation axis + ((200,),) + """ + if axes is None: + axes = self.axes_manager.navigation_axes + + axes = self.axes_manager[axes] + + if not np.iterable(axes): + axes = (axes,) + + axes = tuple([axis.index_in_array for axis in axes]) + ax_chunks = tuple([self.data.chunks[i] for i in sorted(axes)]) + + return ax_chunks + + get_chunk_size.__doc__ %= MANY_AXIS_PARAMETER def _make_lazy(self, axis=None, rechunk=False, dtype=None): self.data = self._lazy_data(axis=axis, rechunk=rechunk, dtype=dtype) - def change_dtype(self, dtype, rechunk=True): + def change_dtype(self, dtype, rechunk=False): # To be consistent with the rechunk argument of other method, we use # 'dask_auto' in favour of a chunking which doesn't split signal space. if rechunk: - rechunk = 'dask_auto' - from hyperspy.misc import rgb_tools - if not isinstance(dtype, np.dtype) and (dtype not in - rgb_tools.rgb_dtypes): + rechunk = "dask_auto" + + if not isinstance(dtype, np.dtype) and (dtype not in rgb_tools.rgb_dtypes): dtype = np.dtype(dtype) super().change_dtype(dtype) self._make_lazy(rechunk=rechunk, dtype=dtype) + change_dtype.__doc__ = BaseSignal.change_dtype.__doc__ - def _lazy_data(self, axis=None, rechunk=True, dtype=None): + def _lazy_data(self, axis=None, rechunk=False, dtype=None): """Return the data as a dask array, rechunked if necessary. Parameters ---------- - axis: None, DataAxis or tuple of data axes + axis: None, :class:`~.axes.DataAxis` or tuple of data axes The data axis that must not be broken into chunks when `rechunk` is `True`. If None, it defaults to the current signal axes. rechunk: bool, "dask_auto" @@ -307,11 +460,9 @@ def _lazy_data(self, axis=None, rechunk=True, dtype=None): if isinstance(self.data, da.Array): res = self.data if self.data.chunks != new_chunks and rechunk: - _logger.info( - "Rechunking.\nOriginal chunks: %s" % str(self.data.chunks)) + _logger.info("Rechunking.\nOriginal chunks: %s" % str(self.data.chunks)) res = self.data.rechunk(new_chunks) - _logger.info( - "Final chunks: %s " % str(res.chunks)) + _logger.info("Final chunks: %s " % str(res.chunks)) else: if isinstance(self.data, np.ma.masked_array): data = np.where(self.data.mask, np.nan, self.data) @@ -321,11 +472,12 @@ def _lazy_data(self, axis=None, rechunk=True, dtype=None): assert isinstance(res, da.Array) return res - def _apply_function_on_data_and_remove_axis(self, function, axes, - out=None, rechunk=True): + def _apply_function_on_data_and_remove_axis( + self, function, axes, out=None, rechunk=False + ): def get_dask_function(numpy_name): # Translate from the default numpy to dask functions - translations = {'amax': 'max', 'amin': 'min'} + translations = {"amax": "max", "amin": "min"} if numpy_name in translations: numpy_name = translations[numpy_name] return getattr(da, numpy_name) @@ -333,7 +485,7 @@ def get_dask_function(numpy_name): function = get_dask_function(function.__name__) axes = self.axes_manager[axes] if not np.iterable(axes): - axes = (axes, ) + axes = (axes,) ar_axes = tuple(ax.index_in_array for ax in axes) if len(ar_axes) == 1: ar_axes = ar_axes[0] @@ -346,7 +498,7 @@ def get_dask_function(numpy_name): # Apply reducing function new_data = function(current_data, axis=ar_axes) if not new_data.ndim: - new_data = new_data.reshape((1, )) + new_data = new_data.reshape((1,)) if out: if out.data.shape == new_data.shape: out.data = new_data @@ -354,46 +506,123 @@ def get_dask_function(numpy_name): else: raise ValueError( "The output shape %s does not match the shape of " - "`out` %s" % (new_data.shape, out.data.shape)) + "`out` %s" % (new_data.shape, out.data.shape) + ) else: s = self._deepcopy_with_new_data(new_data) s._remove_axis([ax.index_in_axes_manager for ax in axes]) return s - def rebin(self, new_shape=None, scale=None, - crop=False, dtype=None, out=None, rechunk=True): + def _get_cache_dask_chunk(self, indices): + """Method for handling caching of dask chunks, when using __call__. + + When accessing data in a chunked HDF5 file, the whole chunks needs + to be loaded into memory. So even if you only want to access a single + index in the navigation dimension, the whole chunk in the navigation + dimension needs to be loaded into memory. This method keeps (caches) + this chunk in memory after loading it, so moving to a different + position with the same chunk will be much faster, reducing amount of + data which needs be read from the disk. + + If a navigation index (via the indices parameter) in a different chunk + is asked for, the currently cached chunk is discarded, and the new + chunk is loaded into memory. + + This only works for functions using self.__call__, for example + plot and fitting functions. This will not work with the region of + interest functionality. + + The cached chunk is stored in the attribute s._cache_dask_chunk, + and the slice needed to extract this chunk is in + s._cache_dask_chunk_slice. To these, use s._clear_cache_dask_data() + + Parameters + ---------- + indices : tuple + Must be the same length as navigation dimensions in self. + + Returns + ------- + value : NumPy array + Same shape as the signal shape of self. + + Examples + -------- + >>> import dask.array as da + >>> s = hs.signals.Signal2D(da.ones((5, 10, 20, 30, 40))).as_lazy() + >>> value = s._get_cache_dask_chunk((3, 6, 2)) + >>> cached_chunk = s._cache_dask_chunk # Cached array + >>> cached_chunk_slice = s._cache_dask_chunk_slice # Slice of chunk + >>> s._clear_cache_dask_data() # Clearing both of these + + """ + + sig_dim = self.axes_manager.signal_dimension + chunks = self.get_chunk_size(self.axes_manager.navigation_axes) + navigation_indices = indices[:-sig_dim] + chunk_slice = _get_navigation_dimension_chunk_slice(navigation_indices, chunks) + + if ( + chunk_slice != self._cache_dask_chunk_slice + or self._cache_dask_chunk is None + ): + with dummy_context_manager(): + self._cache_dask_chunk = self.data.__getitem__(chunk_slice).compute() + self._cache_dask_chunk_slice = chunk_slice + + indices = list(indices) + for i, temp_slice in enumerate(chunk_slice): + indices[i] -= temp_slice.start + indices = tuple(indices) + value = self._cache_dask_chunk[indices] + return value + + def rebin( + self, + new_shape=None, + scale=None, + crop=False, + dtype=None, + out=None, + rechunk=False, + ): factors = self._validate_rebin_args_and_get_factors( - new_shape=new_shape, - scale=scale) + new_shape=new_shape, scale=scale + ) if _requires_linear_rebin(arr=self.data, scale=factors): if new_shape: raise NotImplementedError( "Lazy rebin requires that the new shape is a divisor " "of the original signal shape e.g. if original shape " - "(10| 6), new_shape=(5| 3) is valid, (3 | 4) is not.") + "(10| 6), new_shape=(5| 3) is valid, (3 | 4) is not." + ) else: raise NotImplementedError( "Lazy rebin requires scale to be integer and divisor of the " - "original signal shape") - axis = {ax.index_in_array: ax - for ax in self.axes_manager._axes}[factors.argmax()] + "original signal shape" + ) + axis = {ax.index_in_array: ax for ax in self.axes_manager._axes}[ + factors.argmax() + ] self._make_lazy(axis=axis, rechunk=rechunk) - return super().rebin(new_shape=new_shape, scale=scale, crop=crop, - dtype=dtype, out=out) + return super().rebin( + new_shape=new_shape, scale=scale, crop=crop, dtype=dtype, out=out + ) + rebin.__doc__ = BaseSignal.rebin.__doc__ - def __array__(self, dtype=None): - return self.data.__array__(dtype=dtype) + def __array__(self, dtype=None, copy=None): + return self.data.__array__(dtype=dtype, copy=copy) def _make_sure_data_is_contiguous(self): self._make_lazy(rechunk=True) - def diff(self, axis, order=1, out=None, rechunk=True): + def diff(self, axis, order=1, out=None, rechunk=False): if not self.axes_manager[axis].is_uniform: raise NotImplementedError( - "Performing a numerical difference on a non-uniform axis " - "is not implemented. Consider using `derivative` instead." - ) + "Performing a numerical difference on a non-uniform axis " + "is not implemented. Consider using `derivative` instead." + ) arr_axis = self.axes_manager[axis].index_in_array def dask_diff(arr, n, axis): @@ -418,7 +647,7 @@ def dask_diff(arr, n, axis): current_data = self._lazy_data(axis=axis, rechunk=rechunk) new_data = dask_diff(current_data, order, arr_axis) if not new_data.ndim: - new_data = new_data.reshape((1, )) + new_data = new_data.reshape((1,)) s = out or self._deepcopy_with_new_data(new_data) if out: @@ -427,7 +656,8 @@ def dask_diff(arr, n, axis): else: raise ValueError( "The output shape %s does not match the shape of " - "`out` %s" % (new_data.shape, out.data.shape)) + "`out` %s" % (new_data.shape, out.data.shape) + ) axis2 = s.axes_manager[axis] new_offset = self.axes_manager[axis].offset + (order * axis2.scale / 2) axis2.offset = new_offset @@ -439,17 +669,19 @@ def dask_diff(arr, n, axis): diff.__doc__ = BaseSignal.diff.__doc__ - def integrate_simpson(self, axis, out=None): + def integrate_simpson(self, axis, out=None, rechunk=False): axis = self.axes_manager[axis] from scipy import integrate + axis = self.axes_manager[axis] - data = self._lazy_data(axis=axis, rechunk=True) + data = self._lazy_data(axis=axis, rechunk=rechunk) new_data = data.map_blocks( - integrate.simps, + integrate.simpson, x=axis.axis, axis=axis.index_in_array, drop_axis=axis.index_in_array, - dtype=data.dtype) + dtype=data.dtype, + ) s = out or self._deepcopy_with_new_data(new_data) if out: if out.data.shape == new_data.shape: @@ -458,18 +690,18 @@ def integrate_simpson(self, axis, out=None): else: raise ValueError( "The output shape %s does not match the shape of " - "`out` %s" % (new_data.shape, out.data.shape)) + "`out` %s" % (new_data.shape, out.data.shape) + ) else: s._remove_axis(axis.index_in_axes_manager) return s integrate_simpson.__doc__ = BaseSignal.integrate_simpson.__doc__ - def valuemax(self, axis, out=None, rechunk=True): + def valuemax(self, axis, out=None, rechunk=False): idx = self.indexmax(axis, rechunk=rechunk) old_data = idx.data - data = old_data.map_blocks( - lambda x: self.axes_manager[axis].index2value(x)) + data = old_data.map_blocks(lambda x: self.axes_manager[axis].index2value(x)) if out is None: idx.data = data return idx @@ -479,11 +711,10 @@ def valuemax(self, axis, out=None, rechunk=True): valuemax.__doc__ = BaseSignal.valuemax.__doc__ - def valuemin(self, axis, out=None, rechunk=True): + def valuemin(self, axis, out=None, rechunk=False): idx = self.indexmin(axis, rechunk=rechunk) old_data = idx.data - data = old_data.map_blocks( - lambda x: self.axes_manager[axis].index2value(x)) + data = old_data.map_blocks(lambda x: self.axes_manager[axis].index2value(x)) if out is None: idx.data = data return idx @@ -493,14 +724,13 @@ def valuemin(self, axis, out=None, rechunk=True): valuemin.__doc__ = BaseSignal.valuemin.__doc__ - def get_histogram(self, bins='fd', out=None, rechunk=True, **kwargs): - if 'range_bins' in kwargs: - _logger.warning("'range_bins' argument not supported for lazy " - "signals") - del kwargs['range_bins'] + def get_histogram( + self, bins="fd", range_bins=None, out=None, rechunk=False, **kwargs + ): from hyperspy.signals import Signal1D + data = self._lazy_data(rechunk=rechunk).flatten() - hist, bin_edges = histogram_dask(data, bins=bins, **kwargs) + hist, bin_edges = histogram_dask(data, bins=bins, range=range_bins, **kwargs) if out is None: hist_spec = Signal1D(hist) hist_spec._lazy = True @@ -510,13 +740,13 @@ def get_histogram(self, bins='fd', out=None, rechunk=True, **kwargs): # we always overwrite the data because the computation is lazy -> # the result signal is lazy. Assume that the `out` is already lazy hist_spec.data = hist + hist_spec.axes_manager[0].scale = bin_edges[1] - bin_edges[0] hist_spec.axes_manager[0].offset = bin_edges[0] hist_spec.axes_manager[0].size = hist.shape[-1] - hist_spec.axes_manager[0].name = 'value' - hist_spec.axes_manager[0].is_binned = True - hist_spec.metadata.General.title = ( - self.metadata.General.title + " histogram") + + _set_histogram_metadata(self, hist_spec, **kwargs) + if out is None: return hist_spec else: @@ -525,8 +755,9 @@ def get_histogram(self, bins='fd', out=None, rechunk=True, **kwargs): get_histogram.__doc__ = BaseSignal.get_histogram.__doc__ @staticmethod - def _estimate_poissonian_noise_variance(dc, gain_factor, gain_offset, - correlation_factor): + def _estimate_poissonian_noise_variance( + dc, gain_factor, gain_offset, correlation_factor + ): variance = (dc * gain_factor + gain_offset) * correlation_factor # The lower bound of the variance is the gaussian noise. variance = da.clip(variance, gain_offset * correlation_factor, np.inf) @@ -542,7 +773,7 @@ def _estimate_poissonian_noise_variance(dc, gain_factor, gain_offset, # _get_signal_signal.__doc__ = BaseSignal._get_signal_signal.__doc__ - def _calculate_summary_statistics(self, rechunk=True): + def _calculate_summary_statistics(self, rechunk=False): if rechunk is True: # Use dask auto rechunk instead of HyperSpy's one, what should be # better for these operations @@ -553,144 +784,35 @@ def _calculate_summary_statistics(self, rechunk=True): da.nanmean(data), da.nanstd(data), da.nanmin(data), - da.percentile(_raveled, [25, ]), - da.percentile(_raveled, [50, ]), - da.percentile(_raveled, [75, ]), - da.nanmax(data), ) + da.percentile( + _raveled, + [ + 25, + ], + ), + da.percentile( + _raveled, + [ + 50, + ], + ), + da.percentile( + _raveled, + [ + 75, + ], + ), + da.nanmax(data), + ) + # unlike np.percentile, da.percentile returns array + _q1 = _q1 if np.isscalar(_q1) else _q1[0] + _q2 = _q2 if np.isscalar(_q2) else _q2[0] + _q3 = _q3 if np.isscalar(_q3) else _q3[0] return _mean, _std, _min, _q1, _q2, _q3, _max - def _map_all(self, function, inplace=True, **kwargs): - calc_result = dd(function)(self.data, **kwargs) - if inplace: - self.data = da.from_delayed(calc_result, shape=self.data.shape, - dtype=self.data.dtype) - return None - return self._deepcopy_with_new_data(calc_result) - - def _map_iterate(self, - function, - iterating_kwargs=None, - show_progressbar=None, - parallel=None, - max_workers=None, - ragged=False, - inplace=True, - output_signal_size=None, - output_dtype=None, - **kwargs): - # unpacking keyword arguments - if iterating_kwargs is None: - iterating_kwargs = {} - elif isinstance(iterating_kwargs, (tuple, list)): - iterating_kwargs = dict((k, v) for k, v in iterating_kwargs) - - nav_indexes = self.axes_manager.navigation_indices_in_array - if ragged and inplace: - raise ValueError("Ragged and inplace are not compatible with a lazy signal") - chunk_span = np.equal(self.data.chunksize, self.data.shape) - chunk_span = [chunk_span[i] for i in self.axes_manager.signal_indices_in_array] - if not all(chunk_span): - _logger.info("The chunk size needs to span the full signal size, rechunking...") - old_sig = self.rechunk(inplace=False) - else: - old_sig = self - autodetermine = (output_signal_size is None or output_dtype is None) # try to guess output dtype and sig size? - nav_chunks = old_sig._get_navigation_chunk_size() - args = () - arg_keys = () - - for key in iterating_kwargs: - if not isinstance(iterating_kwargs[key], BaseSignal): - iterating_kwargs[key] = BaseSignal(iterating_kwargs[key].T).T - warnings.warn( - "Passing arrays as keyword arguments can be ambigous. " - "This is deprecated and will be removed in HyperSpy 2.0. " - "Pass signal instances instead.", - VisibleDeprecationWarning) - if iterating_kwargs[key]._lazy: - if iterating_kwargs[key]._get_navigation_chunk_size() != nav_chunks: - iterating_kwargs[key].rechunk(nav_chunks=nav_chunks) - else: - iterating_kwargs[key] = iterating_kwargs[key].as_lazy() - iterating_kwargs[key].rechunk(nav_chunks=nav_chunks) - extra_dims = (len(old_sig.axes_manager.signal_shape) - - len(iterating_kwargs[key].axes_manager.signal_shape)) - if extra_dims > 0: - old_shape = iterating_kwargs[key].data.shape - new_shape = old_shape + (1,)*extra_dims - args += (iterating_kwargs[key].data.reshape(new_shape), ) - else: - args += (iterating_kwargs[key].data, ) - arg_keys += (key,) - - if autodetermine: #trying to guess the output d-type and size from one signal - testing_kwargs = {} - for key in iterating_kwargs: - test_ind = (0,) * len(old_sig.axes_manager.navigation_axes) - testing_kwargs[key] = np.squeeze(iterating_kwargs[key].inav[test_ind].data).compute() - testing_kwargs = {**kwargs, **testing_kwargs} - test_data = np.array(old_sig.inav[(0,) * len(old_sig.axes_manager.navigation_shape)].data.compute()) - output_signal_size, output_dtype = guess_output_signal_size(test_signal=test_data, - function=function, - ragged=ragged, - **testing_kwargs) - # Dropping/Adding Axes - if output_signal_size == old_sig.axes_manager.signal_shape: - drop_axis = None - new_axis = None - axes_changed = False - else: - axes_changed = True - if len(output_signal_size) != len(old_sig.axes_manager.signal_shape): - drop_axis = old_sig.axes_manager.signal_indices_in_array - new_axis = tuple(range(len(output_signal_size))) - else: - drop_axis = [it for (o, i, it) in zip(output_signal_size, - old_sig.axes_manager.signal_shape, - old_sig.axes_manager.signal_indices_in_array) - if o != i] - new_axis = drop_axis - chunks = tuple([old_sig.data.chunks[i] for i in sorted(nav_indexes)]) + output_signal_size - mapped = da.map_blocks(process_function_blockwise, - old_sig.data, - *args, - function=function, - nav_indexes=nav_indexes, - drop_axis=drop_axis, - new_axis=new_axis, - output_signal_size=output_signal_size, - dtype=output_dtype, - chunks=chunks, - arg_keys=arg_keys, - **kwargs) - if inplace: - self.data = mapped - sig = self - else: - sig = self._deepcopy_with_new_data(mapped) - if ragged: - sig.axes_manager.remove(sig.axes_manager.signal_axes) - sig._lazy = True - sig._assign_subclass() - - return sig - # remove if too many axes - if axes_changed: - sig.axes_manager.remove(sig.axes_manager.signal_axes[len(output_signal_size):]) - # add additional required axes - for ind in range( - len(output_signal_size) - sig.axes_manager.signal_dimension, 0, -1): - sig.axes_manager._append_axis(size=output_signal_size[-ind], - navigate=False) - if not ragged: - sig.get_dimensions_from_data() - return sig - - def _block_iterator(self, - flat_signal=True, - get=dask.threaded.get, - navigation_mask=None, - signal_mask=None): + def _block_iterator( + self, flat_signal=True, get=None, navigation_mask=None, signal_mask=None + ): """A function that allows iterating lazy signal data by blocks, defining the dask.Array. @@ -702,9 +824,10 @@ def _block_iterator(self, optionally masked elements missing. If false, returns the equivalent of s.inav[{blocks}].data, where masked elements are set to np.nan or 0. - get : dask scheduler - the dask scheduler to use for computations; - default `dask.threaded.get` + get : dask scheduler or None + The dask scheduler to use for computations. If ``None``, + ``dask.threaded.get` will be used if possible, otherwise + ``dask.get`` will be used, for example in pyodide interpreter. navigation_mask : {BaseSignal, numpy array, dask array} The navigation locations marked as True are not returned (flat) or set to NaN or 0. @@ -713,26 +836,32 @@ def _block_iterator(self, to NaN or 0. """ + if get is None: + get = _get() self._make_lazy() data = self._data_aligned_with_axes - nav_chunks = data.chunks[:self.axes_manager.navigation_dimension] + nav_chunks = data.chunks[: self.axes_manager.navigation_dimension] indices = product(*[range(len(c)) for c in nav_chunks]) signalsize = self.axes_manager.signal_size sig_reshape = (signalsize,) if signalsize else () - data = data.reshape((self.axes_manager.navigation_shape[::-1] + - sig_reshape)) + data = data.reshape((self.axes_manager.navigation_shape[::-1] + sig_reshape)) if signal_mask is None: - signal_mask = slice(None) if flat_signal else \ - np.zeros(self.axes_manager.signal_size, dtype='bool') + signal_mask = ( + slice(None) + if flat_signal + else np.zeros(self.axes_manager.signal_size, dtype="bool") + ) else: try: signal_mask = to_array(signal_mask).ravel() except ValueError: # re-raise with a message - raise ValueError("signal_mask has to be a signal, numpy or" - " dask array, but " - "{} was given".format(type(signal_mask))) + raise ValueError( + "signal_mask has to be a signal, numpy or" + " dask array, but " + "{} was given".format(type(signal_mask)) + ) if flat_signal: signal_mask = ~signal_mask @@ -740,30 +869,33 @@ def _block_iterator(self, nav_mask = da.zeros( self.axes_manager.navigation_shape[::-1], chunks=nav_chunks, - dtype='bool') + dtype="bool", + ) else: try: nav_mask = to_array(navigation_mask, chunks=nav_chunks) except ValueError: # re-raise with a message - raise ValueError("navigation_mask has to be a signal, numpy or" - " dask array, but " - "{} was given".format(type(navigation_mask))) + raise ValueError( + "navigation_mask has to be a signal, numpy or" + " dask array, but " + "{} was given".format(type(navigation_mask)) + ) if flat_signal: nav_mask = ~nav_mask for ind in indices: - chunk = get(data.dask, - (data.name, ) + ind + (0,) * bool(signalsize)) - n_mask = get(nav_mask.dask, (nav_mask.name, ) + ind) + chunk = get(data.dask, (data.name,) + ind + (0,) * bool(signalsize)) + n_mask = get(nav_mask.dask, (nav_mask.name,) + ind) if flat_signal: yield chunk[n_mask, ...][..., signal_mask] else: chunk = chunk.copy() - value = np.nan if np.can_cast('float', chunk.dtype) else 0 + value = np.nan if np.can_cast("float", chunk.dtype) else 0 chunk[n_mask, ...] = value chunk[..., signal_mask] = value - yield chunk.reshape(chunk.shape[:-1] + - self.axes_manager.signal_shape[::-1]) + yield chunk.reshape( + chunk.shape[:-1] + self.axes_manager.signal_shape[::-1] + ) def decomposition( self, @@ -772,15 +904,17 @@ def decomposition( output_dimension=None, signal_mask=None, navigation_mask=None, - get=dask.threaded.get, + get=None, num_chunks=None, reproject=True, print_info=True, - **kwargs + **kwargs, ): """Perform Incremental (Batch) decomposition on the data. - The results are stored in ``self.learning_results``. + The results are stored in the + :attr:`~.api.signals.BaseSignal.learning_results` + attribute. Read more in the :ref:`User Guide `. @@ -794,17 +928,18 @@ def decomposition( output_dimension : int or None, default None Number of components to keep/calculate. If None, keep all (only valid for 'SVD' algorithm) - get : dask scheduler - the dask scheduler to use for computations; - default `dask.threaded.get` + get : dask scheduler or None + The dask scheduler to use for computations. If ``None``, + ``dask.threaded.get` will be used if possible, otherwise + ``dask.get`` will be used, for example in pyodide interpreter. num_chunks : int or None, default None the number of dask chunks to pass to the decomposition model. More chunks require more memory, but should run faster. Will be increased to contain at least ``output_dimension`` signals. - navigation_mask : {BaseSignal, numpy array, dask array} + navigation_mask : :class:~.api.signals.BaseSignal, numpy.ndarray or dask.array.Array The navigation locations marked as True are not used in the decomposition. Not implemented for the 'SVD' algorithm. - signal_mask : {BaseSignal, numpy array, dask array} + signal_mask : :class:~.api.signals.BaseSignal, numpy.ndarray or dask.array.Array The signal locations marked as True are not used in the decomposition. Not implemented for the 'SVD' algorithm. reproject : bool, default True @@ -824,31 +959,12 @@ def decomposition( See Also -------- - * :py:meth:`~.learn.mva.MVA.decomposition` for non-lazy signals - * :py:func:`dask.array.linalg.svd` - * :py:class:`sklearn.decomposition.IncrementalPCA` - * :py:class:`~.learn.rpca.ORPCA` - * :py:class:`~.learn.ornmf.ORNMF` + dask.array.linalg.svd, sklearn.decomposition.IncrementalPCA, + hyperspy.learn.rpca.ORPCA, hyperspy.learn.ornmf.ORNMF """ - if kwargs.get("bounds", False): - warnings.warn( - "The `bounds` keyword is deprecated and will be removed " - "in v2.0. Since version > 1.3 this has no effect.", - VisibleDeprecationWarning, - ) - kwargs.pop("bounds", None) - - # Deprecate 'ONMF' for 'ORNMF' - if algorithm == "ONMF": - warnings.warn( - "The argument `algorithm='ONMF'` has been deprecated and will " - "be removed in future. Please use `algorithm='ORNMF'` instead.", - VisibleDeprecationWarning, - ) - algorithm = "ORNMF" - - + if get is None: + get = _get() # Check algorithms requiring output_dimension algorithms_require_dimension = ["PCA", "ORPCA", "ORNMF"] if algorithm in algorithms_require_dimension and output_dimension is None: @@ -877,7 +993,7 @@ def decomposition( "Decomposition info:", f" normalize_poissonian_noise={normalize_poissonian_noise}", f" algorithm={algorithm}", - f" output_dimension={output_dimension}" + f" output_dimension={output_dimension}", ] # LEARN @@ -885,7 +1001,9 @@ def decomposition( if not import_sklearn.sklearn_installed: raise ImportError("algorithm='PCA' requires scikit-learn") - obj = import_sklearn.sklearn.decomposition.IncrementalPCA(n_components=output_dimension) + obj = import_sklearn.sklearn.decomposition.IncrementalPCA( + n_components=output_dimension + ) method = partial(obj.partial_fit, **kwargs) reproject = True to_print.extend(["scikit-learn estimator:", obj]) @@ -918,7 +1036,9 @@ def decomposition( ndim = self.axes_manager.navigation_dimension sdim = self.axes_manager.signal_dimension nm = da.logical_not( - da.zeros(self.axes_manager.navigation_shape[::-1], chunks=nav_chunks) + da.zeros( + self.axes_manager.navigation_shape[::-1], chunks=nav_chunks + ) if navigation_mask is None else to_array(navigation_mask, chunks=nav_chunks) ) @@ -927,8 +1047,6 @@ def decomposition( if signal_mask is None else to_array(signal_mask, chunks=sig_chunks) ) - ndim = self.axes_manager.navigation_dimension - sdim = self.axes_manager.signal_dimension bH, aG = da.compute( data.sum(axis=tuple(range(ndim))), data.sum(axis=tuple(range(ndim, ndim + sdim))), @@ -939,7 +1057,9 @@ def decomposition( raG = da.sqrt(aG) rbH = da.sqrt(bH) - coeff = raG[(...,) + (None,) * rbH.ndim] * rbH[(None,) * raG.ndim + (...,)] + coeff = ( + raG[(...,) + (None,) * rbH.ndim] * rbH[(None,) * raG.ndim + (...,)] + ) coeff.map_blocks(np.nan_to_num) coeff = da.where(coeff == 0, 1, coeff) data = data / coeff @@ -954,7 +1074,9 @@ def decomposition( self._unfolded4decomposition = self.unfold() # TODO: implement masking if navigation_mask is not None or signal_mask is not None: - raise NotImplementedError("Masking is not yet implemented for lazy SVD") + raise NotImplementedError( + "Masking is not yet implemented for lazy SVD" + ) U, S, V = svd(self.data) @@ -968,7 +1090,7 @@ def decomposition( V = V[:min_shape] factors = V.T - explained_variance = S ** 2 / self.data.shape[0] + explained_variance = S**2 / self.data.shape[0] loadings = U * S finally: if self._unfolded4decomposition is True: @@ -1088,26 +1210,25 @@ def post(a): if print_info: print("\n".join([str(pr) for pr in to_print])) - def plot(self, navigator='auto', **kwargs): + def plot(self, navigator="auto", **kwargs): + if self.axes_manager.ragged: + raise RuntimeError("Plotting ragged signal is not supported.") if isinstance(navigator, str): - if navigator == 'spectrum': + if navigator == "spectrum": # We don't support the 'spectrum' option to keep it simple - _logger.warning("The `navigator='spectrum'` option is not " - "supported for lazy signals, 'auto' is used " - "instead.") - navigator = 'auto' - if navigator == 'auto': - nav_dim = self.axes_manager.navigation_dimension - if nav_dim in [1, 2]: - if self.navigator is None: - self.compute_navigator() - navigator = self.navigator - elif nav_dim > 2: - navigator = 'slider' + _logger.warning( + "The `navigator='spectrum'` option is not " + "supported for lazy signals, 'auto' is used " + "instead." + ) + navigator = "auto" + if navigator == "auto": + if self.navigator is None: + self.compute_navigator() + navigator = self.navigator super().plot(navigator=navigator, **kwargs) - def compute_navigator(self, index=None, chunks_number=None, - show_progressbar=None): + def compute_navigator(self, index=None, chunks_number=None, show_progressbar=None): """ Compute the navigator by taking the sum over a single chunk contained the specified coordinate. Taking the sum over a single chunk is a @@ -1132,8 +1253,8 @@ def compute_navigator(self, index=None, chunks_number=None, ------- None. - Note - ---- + Notes + ----- The number of chunks will affect where the sum is taken. If the sum needs to be taken in the centre of the signal space (for example, in the case of diffraction pattern), the number of chunk needs to be an @@ -1148,8 +1269,10 @@ def compute_navigator(self, index=None, chunks_number=None, else: if not isiterable(index): index = [index] * len(signal_shape) - index = [axis._get_index(_idx) - for _idx, axis in zip(index, self.axes_manager.signal_axes)] + index = [ + axis._get_index(_idx) + for _idx, axis in zip(index, self.axes_manager.signal_axes) + ] _logger.info(f"Using index: {index}") if chunks_number is None: @@ -1160,29 +1283,27 @@ def compute_navigator(self, index=None, chunks_number=None, # Determine the chunk size signal_chunks = da.core.normalize_chunks( [int(size / cn) for cn, size in zip(chunks_number, signal_shape)], - shape=signal_shape - ) + shape=signal_shape, + ) # Needs to reverse the chunks list to match dask chunking order signal_chunks = list(signal_chunks)[::-1] - navigation_chunks = ['auto'] * len(self.axes_manager.navigation_shape) - if LooseVersion(dask.__version__) >= LooseVersion("2.30.0"): - kwargs = {'balance':True} - else: - kwargs = {} - chunks = self.data.rechunk([*navigation_chunks, *signal_chunks], - **kwargs).chunks + navigation_chunks = ["auto"] * len(self.axes_manager.navigation_shape) + chunks = self.data.rechunk( + [*navigation_chunks, *signal_chunks], + balance=True, + ).chunks # Get the slice of the corresponding chunk signal_size = len(signal_shape) - signal_chunks = tuple(chunks[i-signal_size] for i in range(signal_size)) + signal_chunks = tuple(chunks[i - signal_size] for i in range(signal_size)) _logger.info(f"Signal chunks: {signal_chunks}") isig_slice = get_signal_chunk_slice(index, chunks) - _logger.info(f'Computing sum over signal dimension: {isig_slice}') + _logger.info(f"Computing sum over signal dimension: {isig_slice}") axes = [axis.index_in_array for axis in self.axes_manager.signal_axes] navigator = self.isig[isig_slice].sum(axes) navigator.compute(show_progressbar=show_progressbar) - navigator.original_metadata.set_item('sum_from', str(isig_slice)) + navigator.original_metadata.set_item("sum_from", str(isig_slice)) self.navigator = navigator.T @@ -1201,21 +1322,18 @@ def _reshuffle_mixed_blocks(array, ndim, sshape, nav_chunks): sshape : tuple of ints The shape """ - splits = np.cumsum([multiply(ar) - for ar in product(*nav_chunks)][:-1]).tolist() + splits = np.cumsum([multiply(ar) for ar in product(*nav_chunks)][:-1]).tolist() if splits: all_chunks = [ ar.reshape(shape + sshape) - for shape, ar in zip( - product(*nav_chunks), np.split(array, splits)) + for shape, ar in zip(product(*nav_chunks), np.split(array, splits)) ] def split_stack_list(what, step, axis): total = len(what) if total != step: return [ - np.concatenate( - what[i:i + step], axis=axis) + np.concatenate(what[i : i + step], axis=axis) for i in range(0, total, step) ] else: diff --git a/hyperspy/_signals/signal1d.py b/hyperspy/_signals/signal1d.py index f31eae1ccf..5c87a812a1 100644 --- a/hyperspy/_signals/signal1d.py +++ b/hyperspy/_signals/signal1d.py @@ -1,64 +1,83 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import os import logging import math +import warnings -import matplotlib.pyplot as plt -import numpy as np import dask.array as da +import numpy as np +import numpy.ma as ma from scipy import interpolate -from scipy.signal import savgol_filter, medfilt -from scipy.ndimage.filters import gaussian_filter1d +from scipy.ndimage import gaussian_filter1d +from scipy.signal import medfilt, savgol_filter -from hyperspy.signal import BaseSignal from hyperspy._signals.common_signal1d import CommonSignal1D -from hyperspy.signal_tools import SpikesRemoval, SpikesRemovalInteractive -from hyperspy.models.model1d import Model1D -from hyperspy.misc.lowess_smooth import lowess -from hyperspy.misc.utils import is_binned # remove in v2.0 - +from hyperspy._signals.lazy import LazySignal +from hyperspy.decorators import interactive_range_selector from hyperspy.defaults_parser import preferences +from hyperspy.docstrings.plot import ( + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT1D_DOCSTRING, +) +from hyperspy.docstrings.signal import ( + LAZYSIGNAL_DOC, + NAVIGATION_MASK_ARG, + NUM_WORKERS_ARG, + SHOW_PROGRESSBAR_ARG, + SIGNAL_MASK_ARG, +) +from hyperspy.docstrings.signal1d import ( + CROP_PARAMETER_DOC, + SPIKES_REMOVAL_TOOL_DOCSTRING, +) +from hyperspy.misc.lowess_smooth import lowess +from hyperspy.misc.tv_denoise import _tv_denoise_1d +from hyperspy.models.model1d import Model1D +from hyperspy.signal import BaseSignal from hyperspy.signal_tools import ( + BackgroundRemoval, + ButterworthFilter, Signal1DCalibration, - SmoothingSavitzkyGolay, + SimpleMessage, SmoothingLowess, + SmoothingSavitzkyGolay, SmoothingTV, - ButterworthFilter) + SpikesRemoval, + SpikesRemovalInteractive, + _get_background_estimator, +) from hyperspy.ui_registry import DISPLAY_DT, TOOLKIT_DT -from hyperspy.misc.tv_denoise import _tv_denoise_1d -from hyperspy.signal_tools import BackgroundRemoval -from hyperspy.decorators import interactive_range_selector -from hyperspy.signal_tools import IntegrateArea, _get_background_estimator -from hyperspy._signals.lazy import LazySignal -from hyperspy.docstrings.signal1d import CROP_PARAMETER_DOC, SPIKES_REMOVAL_TOOL_DOCSTRING -from hyperspy.docstrings.signal import (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG, - SIGNAL_MASK_ARG, NAVIGATION_MASK_ARG) -from hyperspy.docstrings.plot import ( - BASE_PLOT_DOCSTRING, BASE_PLOT_DOCSTRING_PARAMETERS, PLOT1D_DOCSTRING) _logger = logging.getLogger(__name__) -def find_peaks_ohaver(y, x=None, slope_thresh=0., amp_thresh=None, - medfilt_radius=5, maxpeakn=30000, peakgroup=10, - subchannel=True,): +def find_peaks_ohaver( + y, + x=None, + slope_thresh=0.0, + amp_thresh=None, + medfilt_radius=5, + maxpeakn=30000, + peakgroup=10, + subchannel=True, +): """Find peaks along a 1D line. Function to locate the positive peaks in a noisy x-y data set. @@ -132,9 +151,7 @@ def find_peaks_ohaver(y, x=None, slope_thresh=0., amp_thresh=None, else: d = np.gradient(y) n = np.round(peakgroup / 2 + 1) - peak_dt = np.dtype([('position', float), - ('height', float), - ('width', float)]) + peak_dt = np.dtype([("position", float), ("height", float), ("width", float)]) P = np.array([], dtype=peak_dt) peak = 0 for j in range(len(y) - 4): @@ -161,8 +178,8 @@ def find_peaks_ohaver(y, x=None, slope_thresh=0., amp_thresh=None, s += 1 continue elif groupindex > y.shape[0] - 1: - xx = xx[:groupindex - 1] - yy = yy[:groupindex - 1] + xx = xx[: groupindex - 1] + yy = yy[: groupindex - 1] break xx[k - s] = x[groupindex] yy[k - s] = y[groupindex] @@ -172,23 +189,22 @@ def find_peaks_ohaver(y, x=None, slope_thresh=0., amp_thresh=None, # Fit parabola to log10 of sub-group with # centering and scaling yynz = yy != 0 - coef = np.polyfit( - xxf[yynz], np.log10(np.abs(yy[yynz])), 2) + coef = np.polyfit(xxf[yynz], np.log10(abs(yy[yynz])), 2) c1 = coef[2] c2 = coef[1] c3 = coef[0] - with np.errstate(invalid='ignore'): - width = np.linalg.norm(stdev * 2.35703 / - (np.sqrt(2) * np.sqrt(-1 * - c3))) + with np.errstate(invalid="ignore"): + width = np.linalg.norm( + stdev * 2.35703 / (np.sqrt(2) * np.sqrt(-1 * c3)) + ) # if the peak is too narrow for least-squares # technique to work well, just use the max value # of y in the sub-group of points near peak. if peakgroup < 7: height = np.max(yy) - position = xx[np.argmin(np.abs(yy - height))] + position = xx[np.argmin(abs(yy - height))] else: - position = - ((stdev * c2 / (2 * c3)) - avg) + position = -((stdev * c2 / (2 * c3)) - avg) height = np.exp(c1 - c3 * (c2 / (2 * c3)) ** 2) # Fill results array P. One row for each peak # detected, containing the @@ -199,16 +215,16 @@ def find_peaks_ohaver(y, x=None, slope_thresh=0., amp_thresh=None, # no way to know peak width without # the above measurements. width = 0 - if (not np.isnan(position) and 0 < position < x[-1]): - P = np.hstack((P, - np.array([(position, height, width)], - dtype=peak_dt))) + if not np.isnan(position) and 0 < position < x[-1]: + P = np.hstack( + (P, np.array([(position, height, width)], dtype=peak_dt)) + ) peak += 1 # return only the part of the array that contains peaks # (not the whole maxpeakn x 3 array) if len(P) > maxpeakn: - minh = np.sort(P['height'])[-maxpeakn] - P = P[P['height'] >= minh] + minh = np.sort(P["height"])[-maxpeakn] + P = P[P["height"] >= minh] # Sorts the values as a function of position P.sort(0) @@ -221,12 +237,20 @@ def interpolate1D(number_of_interpolation_points, data): ch = len(data) old_ax = np.linspace(0, 100, ch) new_ax = np.linspace(0, 100, ch * ip - (ip - 1)) - interpolator = interpolate.interp1d(old_ax, data) + + data = ma.masked_invalid(data) + interpolator = interpolate.make_interp_spline( + old_ax, + data, + k=1, + check_finite=False, + ) return interpolator(new_ax) -def _estimate_shift1D(data, data_slice=slice(None), ref=None, ip=5, - interpolate=True, mask=None, **kwargs): +def _estimate_shift1D( + data, data_slice=slice(None), ref=None, ip=5, interpolate=True, mask=None, **kwargs +): if bool(mask): # asarray is required for consistency as argmax # returns a numpy scalar array @@ -237,41 +261,45 @@ def _estimate_shift1D(data, data_slice=slice(None), ref=None, ip=5, # Normalise the data before the cross correlation ref = ref - ref.mean() data = data - data.mean() - return (np.argmax(np.correlate(ref, data, 'full')) - len(ref) + 1).astype(float) + return (np.argmax(np.correlate(ref, data, "full")) - len(ref) + 1).astype(float) def _shift1D(data, **kwargs): """Used to shift a data array by a specified amount in axes units. Axis must - be passed as a kwarg. """ - shift = kwargs.get('shift', 0.) - original_axis = kwargs.get('original_axis', None) - fill_value = kwargs.get('fill_value', np.nan) - kind = kwargs.get('kind', 'linear') + be passed as a kwarg.""" + shift = kwargs.get("shift", 0.0) + original_axis = kwargs.get("original_axis", None) - if np.isnan(shift) or shift == 0: + # shift.size for cases where shift is empty + if shift.size == 0 or np.isnan(shift) or shift == 0: return data - #This is the interpolant function - si = interpolate.interp1d(original_axis, data, bounds_error=False, - fill_value=fill_value, kind=kind) + data = ma.masked_invalid(data) + # #This is the interpolant function + si = interpolate.make_interp_spline(original_axis, data, k=1, check_finite=False) - #Evaluate interpolated data at shifted positions - return si(original_axis-shift) + # Evaluate interpolated data at shifted positions + return si(original_axis - shift) class Signal1D(BaseSignal, CommonSignal1D): - """ - """ + """General 1D signal class.""" + _signal_dimension = 1 def __init__(self, *args, **kwargs): + if kwargs.get("ragged", False): + raise ValueError("Signal1D can't be ragged.") super().__init__(*args, **kwargs) - if self.axes_manager.signal_dimension != 1: - self.axes_manager.set_signal_dimension(1) - def _get_spikes_diagnosis_histogram_data(self, signal_mask=None, - navigation_mask=None, - **kwargs): + def _spikes_diagnosis( + self, + signal_mask=None, + navigation_mask=None, + show_plot=False, + use_gui=False, + **kwargs, + ): self._check_signal_dimension_equals_one() dc = self.data axis = self.axes_manager.signal_axes[0].axis @@ -280,22 +308,50 @@ def _get_spikes_diagnosis_histogram_data(self, signal_mask=None, axis = axis[~signal_mask] if navigation_mask is not None: dc = dc[~navigation_mask, :] - der = np.abs(np.gradient(dc, axis, axis=-1)) - n = ((~navigation_mask).sum() if navigation_mask else - self.axes_manager.navigation_size) + if dc.size == 0: + raise ValueError("The data size must be higher than 0.") + der = abs(np.gradient(dc, axis, axis=-1)) + n = ( + (~navigation_mask).sum() + if navigation_mask + else self.axes_manager.navigation_size + ) # arbitrary cutoff for number of spectra necessary before histogram # data is compressed by finding maxima of each spectrum - tmp = BaseSignal(der) if n < 2000 else BaseSignal( - np.ravel(der.max(-1))) + tmp = BaseSignal(der) if n < 2000 else BaseSignal(np.ravel(der.max(-1))) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=Warning, + message="Estimated number of bins using", + module="hyperspy", + ) + s_ = tmp.get_histogram(**kwargs) + s_.axes_manager[0].name = "Derivative magnitude" + s_.metadata.Signal.quantity = "Counts" + s_.metadata.General.title = "Spikes Analysis" + + if s_.data.size == 1: + message = "The derivative of the data is constant." + if use_gui: + m = SimpleMessage(text=message) + try: + m.gui() + except (NotImplementedError, ImportError): + # This is only available for traitsui, in case of ipywidgets + # we show a warning + warnings.warn(message) + else: + warnings.warn(message) + elif show_plot: + s_.plot(norm="log") - # get histogram signal using smart binning and plot - return tmp.get_histogram(**kwargs) + return s_ - def spikes_diagnosis(self, signal_mask=None, - navigation_mask=None, - **kwargs): - """Plots a histogram to help in choosing the threshold for + def spikes_diagnosis(self, signal_mask=None, navigation_mask=None, **kwargs): + """ + Plots a histogram to help in choosing the threshold for spikes removal. Parameters @@ -304,57 +360,60 @@ def spikes_diagnosis(self, signal_mask=None, %s **kwargs : dict Keyword arguments pass to - :py:meth:`~hyperspy.signal.signal.BaseSignal.get_histogram` + :meth:`~hyperspy.api.signals.BaseSignal.get_histogram` - See also + See Also -------- spikes_removal_tool """ - tmph = self._get_spikes_diagnosis_histogram_data(signal_mask, - navigation_mask, - **kwargs) - tmph.plot() - - # Customize plot appearance - plt.gca().set_title('') - plt.gca().fill_between(tmph.axes_manager[0].axis, - tmph.data, - facecolor='#fddbc7', - interpolate=True, - color='none') - ax = tmph._plot.signal_plot.ax - axl = tmph._plot.signal_plot.ax_lines[0] - axl.set_line_properties(color='#b2182b') - plt.xlabel('Derivative magnitude') - plt.ylabel('Log(Counts)') - ax.set_yscale('log') - ax.set_ylim(10 ** -1, plt.ylim()[1]) - ax.set_xlim(plt.xlim()[0], 1.1 * plt.xlim()[1]) - plt.draw() + self._spikes_diagnosis( + signal_mask=signal_mask, + navigation_mask=navigation_mask, + show_plot=True, + use_gui=False, + **kwargs, + ) spikes_diagnosis.__doc__ %= (SIGNAL_MASK_ARG, NAVIGATION_MASK_ARG) - def spikes_removal_tool(self, signal_mask=None, navigation_mask=None, - threshold='auto', interactive=True, - display=True, toolkit=None, **kwargs): + def spikes_removal_tool( + self, + signal_mask=None, + navigation_mask=None, + threshold="auto", + interactive=True, + display=True, + toolkit=None, + **kwargs, + ): self._check_signal_dimension_equals_one() if interactive: - sr = SpikesRemovalInteractive(self, - signal_mask=signal_mask, - navigation_mask=navigation_mask, - threshold=threshold) + sr = SpikesRemovalInteractive( + self, + signal_mask=signal_mask, + navigation_mask=navigation_mask, + threshold=threshold, + ) return sr.gui(display=display, toolkit=toolkit) else: - sr = SpikesRemoval(self, - signal_mask=signal_mask, - navigation_mask=navigation_mask, - threshold=threshold, **kwargs) + sr = SpikesRemoval( + self, + signal_mask=signal_mask, + navigation_mask=navigation_mask, + threshold=threshold, + **kwargs, + ) sr.remove_all_spikes() return sr spikes_removal_tool.__doc__ = SPIKES_REMOVAL_TOOL_DOCSTRING % ( - SIGNAL_MASK_ARG, NAVIGATION_MASK_ARG, "", DISPLAY_DT, TOOLKIT_DT,) + SIGNAL_MASK_ARG, + NAVIGATION_MASK_ARG, + "", + DISPLAY_DT, + TOOLKIT_DT, + ) def create_model(self, dictionary=None): """Create a model for the current data. @@ -371,28 +430,27 @@ def create_model(self, dictionary=None): def shift1D( self, shift_array, - interpolation_method='linear', + interpolation_method="linear", crop=True, expand=False, fill_value=np.nan, - parallel=None, show_progressbar=None, - max_workers=None, + num_workers=None, ): """Shift the data in place over the signal axis by the amount specified by an array. Parameters ---------- - shift_array : BaseSignal or np.array + shift_array : :class:`~hyperspy.api.signals.BaseSignal` or numpy.ndarray An array containing the shifting amount. It must have the same - `axes_manager._navigation_shape` - `axes_manager._navigation_shape_in_array` shape. + ``axes_manager.navigation_shape`` + ``axes_manager._navigation_shape_in_array`` shape. interpolation_method : str or int - Specifies the kind of interpolation as a string ('linear', - 'nearest', 'zero', 'slinear', 'quadratic, 'cubic') or as an - integer specifying the order of the spline interpolator to - use. + Specifies the kind of interpolation as a string (``'linear'``, + ``'nearest'``, ``'zero'``, ``'slinear'``, ``'quadratic'``, + ``'cubic'``) or as an integer specifying the order of the spline + interpolator to use. %s expand : bool If True, the data will be expanded to fit all data after alignment. @@ -402,7 +460,6 @@ def shift1D( interval with the given value where needed. %s %s - %s Raises ------ @@ -421,19 +478,17 @@ def shift1D( if not axis.is_uniform: raise NotImplementedError( - "This operation is not implemented for non-uniform axes.") + "This operation is not implemented for non-uniform axes." + ) # Figure out min/max shifts, and translate to shifts in index as well minimum, maximum = np.nanmin(shift_array), np.nanmax(shift_array) if minimum < 0: - ihigh = 1 + axis.value2index( - axis.high_value + minimum, - rounding=math.floor) + ihigh = 1 + axis.value2index(axis.high_value + minimum, rounding=math.floor) else: ihigh = axis.high_index + 1 if maximum > 0: - ilow = axis.value2index(axis.offset + maximum, - rounding=math.ceil) + ilow = axis.value2index(axis.offset + maximum, rounding=math.ceil) else: ilow = axis.low_index if expand: @@ -446,56 +501,59 @@ def shift1D( pre_shape[ind] = axis.high_index - ihigh + 1 post_shape[ind] = ilow - axis.low_index - for chunks, shape in zip((pre_chunks, post_chunks), - (pre_shape, post_shape)): + for chunks, shape in zip( + (pre_chunks, post_chunks), (pre_shape, post_shape) + ): maxsize = min(np.max(chunks[ind]), shape[ind]) num = np.ceil(shape[ind] / maxsize) - chunks[ind] = tuple(len(ar) for ar in - np.array_split(np.arange(shape[ind]), - num)) - pre_array = da.full(tuple(pre_shape), - fill_value, - chunks=tuple(pre_chunks)) - - post_array = da.full(tuple(post_shape), - fill_value, - chunks=tuple(post_chunks)) - self.data = da.concatenate((pre_array, self.data, post_array), - axis=ind).rechunk({ind:-1}) + chunks[ind] = tuple( + len(ar) for ar in np.array_split(np.arange(shape[ind]), num) + ) + pre_array = da.full( + tuple(pre_shape), fill_value, chunks=tuple(pre_chunks) + ) + + post_array = da.full( + tuple(post_shape), fill_value, chunks=tuple(post_chunks) + ) + self.data = da.concatenate( + (pre_array, self.data, post_array), axis=ind + ).rechunk({ind: -1}) else: padding = [] for i in range(self.data.ndim): if i == axis.index_in_array: - padding.append((axis.high_index - ihigh + 1, - ilow - axis.low_index)) + padding.append( + (axis.high_index - ihigh + 1, ilow - axis.low_index) + ) else: padding.append((0, 0)) - self.data = np.pad(self.data, padding, mode='constant', - constant_values=(fill_value,)) + self.data = np.pad( + self.data, padding, mode="constant", constant_values=(fill_value,) + ) axis.offset += minimum axis.size += axis.high_index - ihigh + 1 + ilow - axis.low_index if isinstance(shift_array, np.ndarray): shift_array = BaseSignal(shift_array.squeeze()).T - self.map(_shift1D, - shift=shift_array, - original_axis=axis.axis, - fill_value=fill_value, - kind=interpolation_method, - show_progressbar=show_progressbar, - parallel=parallel, - max_workers=max_workers, - ragged=False) + self.map( + _shift1D, + shift=shift_array, + original_axis=axis.axis, + fill_value=fill_value, + kind=interpolation_method, + show_progressbar=show_progressbar, + num_workers=num_workers, + ragged=False, + ) if crop and not expand: - _logger.debug("Cropping %s from index %i to %i" - % (self, ilow, ihigh)) - self.crop(axis.index_in_axes_manager, - ilow, - ihigh) + _logger.debug("Cropping %s from index %i to %i" % (self, ilow, ihigh)) + self.crop(axis.index_in_axes_manager, ilow, ihigh) self.events.data_changed.trigger(obj=self) - shift1D.__doc__ %= (CROP_PARAMETER_DOC, SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) + + shift1D.__doc__ %= (CROP_PARAMETER_DOC, SHOW_PROGRESSBAR_ARG, NUM_WORKERS_ARG) def interpolate_in_between( self, @@ -503,8 +561,7 @@ def interpolate_in_between( end, delta=3, show_progressbar=None, - parallel=None, - max_workers=None, + num_workers=None, **kwargs, ): """Replace the data in a given range by interpolation. @@ -521,10 +578,9 @@ def interpolate_in_between( units of the axis value. %s %s - %s - **kwargs : + **kwargs : dict All extra keyword arguments are passed to - :py:func:`scipy.interpolate.interp1d`. See the function documentation + :class:`scipy.interpolate.interp1d`. See the function documentation for details. Raises @@ -543,8 +599,12 @@ def interpolate_in_between( start = axis.axis[start] if isinstance(end, int): end = axis.axis[end] - i0 = axis._get_index(start-delta) if start-delta < axis.low_value else 0 - i3 = axis._get_index(end+delta) if end+delta > axis.high_value else axis.size + i0 = axis._get_index(start - delta) if start - delta < axis.low_value else 0 + i3 = ( + axis._get_index(end + delta) + if end + delta > axis.high_value + else axis.size + ) else: i0 = int(np.clip(i1 - delta, 0, np.inf)) i3 = int(np.clip(i2 + delta, 0, axis.size)) @@ -553,17 +613,20 @@ def interpolating_function(dat): dat_int = interpolate.interp1d( list(range(i0, i1)) + list(range(i2, i3)), dat[i0:i1].tolist() + dat[i2:i3].tolist(), - **kwargs) + **kwargs, + ) dat[i1:i2] = dat_int(list(range(i1, i2))) return dat - self._map_iterate(interpolating_function, - ragged=False, - parallel=parallel, - show_progressbar=show_progressbar, - max_workers=max_workers) + + self.map( + interpolating_function, + ragged=False, + show_progressbar=show_progressbar, + num_workers=num_workers, + ) self.events.data_changed.trigger(obj=self) - interpolate_in_between.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) + interpolate_in_between.__doc__ %= (SHOW_PROGRESSBAR_ARG, NUM_WORKERS_ARG) def estimate_shift1D( self, @@ -575,8 +638,7 @@ def estimate_shift1D( number_of_interpolation_points=5, mask=None, show_progressbar=None, - parallel=None, - max_workers=None, + num_workers=None, ): """Estimate the shifts in the current signal axis using cross-correlation. @@ -585,40 +647,40 @@ def estimate_shift1D( the signal axis. To decrease the memory usage, the time of computation and the accuracy of the results it is convenient to select the feature of interest providing sensible values for - `start` and `end`. By default interpolation is used to obtain + ``start`` and ``end``. By default interpolation is used to obtain subpixel precision. Parameters ---------- - start, end : int, float or None + start, end : int, float or None, default None The limits of the interval. If int they are taken as the axis index. If float they are taken as the axis value. - reference_indices : tuple of ints or None + reference_indices : tuple of int or None, default None Defines the coordinates of the spectrum that will be used as reference. If None the spectrum at the current coordinates is used for this purpose. max_shift : int "Saturation limit" for the shift. - interpolate : bool + interpolate : bool, default True If True, interpolation is used to provide sub-pixel accuracy. number_of_interpolation_points : int Number of interpolation points. Warning: making this number too big can saturate the memory - mask : `BaseSignal` of bool. + mask : :class:`~.api.signals.BaseSignal` of bool. It must have signal_dimension = 0 and navigation_shape equal to the current signal. Where mask is True the shift is not computed and set to nan. %s %s - %s Returns ------- - An array with the result of the estimation in the axis units. - Although the computation is performed in batches if the signal is - lazy, the result is computed in memory because it depends on the - current state of the axes that could change later on in the workflow. + numpy.ndarray + An array with the result of the estimation in the axis units. + Although the computation is performed in batches if the signal is + lazy, the result is computed in memory because it depends on the + current state of the axes that could change later on in the workflow. Raises ------ @@ -634,7 +696,8 @@ def estimate_shift1D( axis = self.axes_manager.signal_axes[0] if not axis.is_uniform: raise NotImplementedError( - "The function is not implemented for non-uniform signal axes.") + "The function is not implemented for non-uniform signal axes." + ) self._check_navigation_mask(mask) # we compute for now if isinstance(start, da.Array): @@ -656,10 +719,9 @@ def estimate_shift1D( ip=ip, interpolate=interpolate, ragged=False, - parallel=parallel, inplace=False, show_progressbar=show_progressbar, - max_workers=max_workers, + num_workers=num_workers, ) shift_array = shift_signal.data if max_shift is not None: @@ -669,30 +731,32 @@ def estimate_shift1D( if interpolate is True: shift_array = shift_array / ip shift_array = shift_array * axis.scale - if self._lazy: + if shift_signal._lazy: # We must compute right now because otherwise any changes to the # axes_manager of the signal later in the workflow may result in # a wrong shift_array shift_array = shift_array.compute() return shift_array - estimate_shift1D.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) - - def align1D(self, - start=None, - end=None, - reference_indices=None, - max_shift=None, - interpolate=True, - number_of_interpolation_points=5, - interpolation_method='linear', - crop=True, - expand=False, - fill_value=np.nan, - also_align=None, - mask=None, - show_progressbar=None, - iterpath="flyback"): + estimate_shift1D.__doc__ %= (SHOW_PROGRESSBAR_ARG, NUM_WORKERS_ARG) + + def align1D( + self, + start=None, + end=None, + reference_indices=None, + max_shift=None, + interpolate=True, + number_of_interpolation_points=5, + interpolation_method="linear", + crop=True, + expand=False, + fill_value=np.nan, + also_align=None, + mask=None, + show_progressbar=None, + iterpath="serpentine", + ): """Estimate the shifts in the signal axis using cross-correlation and use the estimation to align the data in place. This method can only estimate the shift by comparing @@ -701,7 +765,7 @@ def align1D(self, To decrease memory usage, time of computation and improve accuracy it is convenient to select the feature of interest - setting the `start` and `end` keywords. By default interpolation is + setting the ``start`` and ``end`` keywords. By default interpolation is used to obtain subpixel precision. Parameters @@ -709,7 +773,7 @@ def align1D(self, start, end : int, float or None The limits of the interval. If int they are taken as the axis index. If float they are taken as the axis value. - reference_indices : tuple of ints or None + reference_indices : tuple of int or None Defines the coordinates of the spectrum that will be used as reference. If None the spectrum at the current coordinates is used for this purpose. @@ -729,15 +793,15 @@ def align1D(self, %s expand : bool If True, the data will be expanded to fit all data after alignment. - Overrides `crop`. + Overrides ``crop`` argument. fill_value : float If crop is False fill the data outside of the original interval with the given value where needed. - also_align : list of signals, None + also_align : list of :class:`~.api.signals.BaseSignal`, None A list of BaseSignal instances that has exactly the same dimensions as this one and that will be aligned using the shift map estimated using the this signal. - mask : `BaseSignal` or bool data type. + mask : :class:`~.api.signals.BaseSignal` or bool It must have signal_dimension = 0 and navigation_shape equal to the current signal. Where mask is True the shift is not computed and set to nan. @@ -745,14 +809,15 @@ def align1D(self, Returns ------- - An array with the result of the estimation. + numpy.ndarray + The result of the estimation. Raises ------ SignalDimensionError If the signal dimension is not 1. - See also + See Also -------- estimate_shift1D """ @@ -760,12 +825,14 @@ def align1D(self, also_align = [] self._check_signal_dimension_equals_one() if self._lazy: - _logger.warning('In order to properly expand, the lazy ' - 'reference signal will be read twice (once to ' - 'estimate shifts, and second time to shift ' - 'appropriately), which might take a long time. ' - 'Use expand=False to only pass through the data ' - 'once.') + _logger.warning( + "In order to properly expand, the lazy " + "reference signal will be read twice (once to " + "estimate shifts, and second time to shift " + "appropriately), which might take a long time. " + "Use expand=False to only pass through the data " + "once." + ) with self.axes_manager.switch_iterpath(iterpath): shift_array = self.estimate_shift1D( start=start, @@ -775,86 +842,20 @@ def align1D(self, interpolate=interpolate, number_of_interpolation_points=number_of_interpolation_points, mask=mask, - show_progressbar=show_progressbar) + show_progressbar=show_progressbar, + ) signals_to_shift = [self] + also_align for signal in signals_to_shift: - signal.shift1D(shift_array=shift_array, - interpolation_method=interpolation_method, - crop=crop, - fill_value=fill_value, - expand=expand, - show_progressbar=show_progressbar) - align1D.__doc__ %= (CROP_PARAMETER_DOC, SHOW_PROGRESSBAR_ARG) - - def integrate_in_range(self, signal_range='interactive', - display=True, toolkit=None): - """Sums the spectrum over an energy range, giving the integrated - area. - The energy range can either be selected through a GUI or the command - line. - - Parameters - ---------- - signal_range : a tuple of this form (l, r) or "interactive" - l and r are the left and right limits of the range. They can be - numbers or None, where None indicates the extremes of the interval. - If l and r are floats the `signal_range` will be in axis units (for - example eV). If l and r are integers the `signal_range` will be in - index units. When `signal_range` is "interactive" (default) the - range is selected using a GUI. Note that ROIs can be used - in place of a tuple. + signal.shift1D( + shift_array=shift_array, + interpolation_method=interpolation_method, + crop=crop, + fill_value=fill_value, + expand=expand, + show_progressbar=show_progressbar, + ) - Returns - -------- - integrated_spectrum : `BaseSignal` subclass - - See Also - -------- - integrate_simpson - - Examples - -------- - Using the GUI - - >>> s = hs.signals.Signal1D(range(1000)) - >>> s.integrate_in_range() #doctest: +SKIP - - Using the CLI - - >>> s_int = s.integrate_in_range(signal_range=(560,None)) - - Selecting a range in the axis units, by specifying the - signal range with floats. - - >>> s_int = s.integrate_in_range(signal_range=(560.,590.)) - - Selecting a range using the index, by specifying the - signal range with integers. - - >>> s_int = s.integrate_in_range(signal_range=(100,120)) - """ - from hyperspy.misc.utils import deprecation_warning - msg = ( - "The `Signal1D.integrate_in_range` method is deprecated and will " - "be removed in v2.0. Use a `roi.SpanRoi` followed by `integrate1D` " - "instead.") - deprecation_warning(msg) - - if signal_range == 'interactive': - self_copy = self.deepcopy() - ia = IntegrateArea(self_copy, signal_range) - ia.gui(display=display, toolkit=toolkit) - integrated_signal1D = self_copy - else: - integrated_signal1D = self._integrate_in_range_commandline( - signal_range) - return integrated_signal1D - - def _integrate_in_range_commandline(self, signal_range): - e1 = signal_range[0] - e2 = signal_range[1] - integrated_signal1D = self.isig[e1:e2].integrate1D(-1) - return integrated_signal1D + align1D.__doc__ %= (CROP_PARAMETER_DOC, SHOW_PROGRESSBAR_ARG) def calibrate(self, display=True, toolkit=None): """ @@ -892,8 +893,7 @@ def smooth_savitzky_golay( polynomial_order=None, window_length=None, differential_order=0, - parallel=None, - max_workers=None, + num_workers=None, display=True, toolkit=None, ): @@ -917,7 +917,6 @@ def smooth_savitzky_golay( %s %s %s - %s Raises ------ @@ -931,14 +930,20 @@ def smooth_savitzky_golay( self._check_signal_dimension_equals_one() if not self.axes_manager.signal_axes[0].is_uniform: raise NotImplementedError( - "This functionality is not implement for signals with non-uniform axes. ") - "Consider using `smooth_lowess` instead." - if (polynomial_order is not None and - window_length is not None): + "This functionality is not implemented for signals with non-uniform axes. " + "Consider using `smooth_lowess` instead." + ) + if polynomial_order is not None and window_length is not None: axis = self.axes_manager.signal_axes[0] - self.map(savgol_filter, window_length=window_length, - polyorder=polynomial_order, deriv=differential_order, - delta=axis.scale, ragged=False, parallel=parallel, max_workers=max_workers) + self.map( + savgol_filter, + window_length=window_length, + polyorder=polynomial_order, + deriv=differential_order, + delta=axis.scale, + ragged=False, + num_workers=num_workers, + ) else: # Interactive mode smoother = SmoothingSavitzkyGolay(self) @@ -949,15 +954,14 @@ def smooth_savitzky_golay( smoother.window_length = window_length return smoother.gui(display=display, toolkit=toolkit) - smooth_savitzky_golay.__doc__ %= (PARALLEL_ARG, MAX_WORKERS_ARG, DISPLAY_DT, TOOLKIT_DT) + smooth_savitzky_golay.__doc__ %= (NUM_WORKERS_ARG, DISPLAY_DT, TOOLKIT_DT) def smooth_lowess( self, smoothing_parameter=None, number_of_iterations=None, show_progressbar=None, - parallel=None, - max_workers=None, + num_workers=None, display=True, toolkit=None, ): @@ -978,7 +982,6 @@ def smooth_lowess( %s %s %s - %s Raises ------ @@ -995,22 +998,29 @@ def smooth_lowess( smoother.number_of_iterations = number_of_iterations return smoother.gui(display=display, toolkit=toolkit) else: - self.map(lowess, - x=self.axes_manager[-1].axis, - f=smoothing_parameter, - n_iter=number_of_iterations, - show_progressbar=show_progressbar, - ragged=False, - parallel=parallel, - max_workers=max_workers) - smooth_lowess.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG, DISPLAY_DT, TOOLKIT_DT) + self.map( + lowess, + x=self.axes_manager[-1].axis, + f=smoothing_parameter, + n_iter=number_of_iterations, + show_progressbar=show_progressbar, + ragged=False, + num_workers=num_workers, + silence_warnings="non-uniform", + ) + + smooth_lowess.__doc__ %= ( + SHOW_PROGRESSBAR_ARG, + NUM_WORKERS_ARG, + DISPLAY_DT, + TOOLKIT_DT, + ) def smooth_tv( self, smoothing_parameter=None, show_progressbar=None, - parallel=None, - max_workers=None, + num_workers=None, display=True, toolkit=None, ): @@ -1026,7 +1036,6 @@ def smooth_tv( %s %s %s - %s Raises ------ @@ -1038,24 +1047,31 @@ def smooth_tv( self._check_signal_dimension_equals_one() if not self.axes_manager.signal_axes[0].is_uniform: raise NotImplementedError( - "This functionality is not implement for signals with non-uniform axes. ") - "Consider using `smooth_lowess` instead." + "This functionality is not implemented for signals with non-uniform axes. " + "Consider using `smooth_lowess` instead." + ) if smoothing_parameter is None: smoother = SmoothingTV(self) return smoother.gui(display=display, toolkit=toolkit) else: - self.map(_tv_denoise_1d, weight=smoothing_parameter, - ragged=False, - show_progressbar=show_progressbar, - parallel=parallel, - max_workers=max_workers) - - smooth_tv.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG, DISPLAY_DT, TOOLKIT_DT) - - def filter_butterworth(self, - cutoff_frequency_ratio=None, - type='low', - order=2, display=True, toolkit=None): + self.map( + _tv_denoise_1d, + weight=smoothing_parameter, + ragged=False, + show_progressbar=show_progressbar, + num_workers=num_workers, + ) + + smooth_tv.__doc__ %= (SHOW_PROGRESSBAR_ARG, NUM_WORKERS_ARG, DISPLAY_DT, TOOLKIT_DT) + + def filter_butterworth( + self, + cutoff_frequency_ratio=None, + type="low", + order=2, + display=True, + toolkit=None, + ): """ Butterworth filter in place. @@ -1073,8 +1089,9 @@ def filter_butterworth(self, """ if not self.axes_manager.signal_axes[0].is_uniform: raise NotImplementedError( - "This functionality is not implement for signals with non-uniform axes. ") - "Consider using `smooth_lowess` instead." + "This functionality is not implemented for signals with non-uniform axes. " + "Consider using `smooth_lowess` instead." + ) self._check_signal_dimension_equals_one() smoother = ButterworthFilter(self) if cutoff_frequency_ratio is not None: @@ -1088,25 +1105,29 @@ def filter_butterworth(self, filter_butterworth.__doc__ %= (DISPLAY_DT, TOOLKIT_DT) def _remove_background_cli( - self, signal_range, background_estimator, fast=True, - zero_fill=False, show_progressbar=None, model=None, - return_model=False): - """ See :py:meth:`~hyperspy._signal1d.signal1D.remove_background`. """ + self, + signal_range, + background_estimator, + fast=True, + zero_fill=False, + show_progressbar=None, + model=None, + return_model=False, + ): + """See :meth:`~hyperspy._signal1d.signal1D.remove_background`.""" if model is None: from hyperspy.models.model1d import Model1D + model = Model1D(self) if background_estimator not in model: model.append(background_estimator) background_estimator.estimate_parameters( - self, - signal_range[0], - signal_range[1], - only_current=False) + self, signal_range[0], signal_range[1], only_current=False + ) if not fast: model.set_signal_range(signal_range[0], signal_range[1]) - model.multifit(show_progressbar=show_progressbar, - iterpath='serpentine') + model.multifit(show_progressbar=show_progressbar, iterpath="serpentine") model.reset_signal_range() if self._lazy: @@ -1114,9 +1135,7 @@ def _remove_background_cli( else: try: axis = self.axes_manager.signal_axes[0] - if is_binned(self): - # in v2 replace by - # if axis.is_binned: + if axis.is_binned: if axis.is_uniform: scale_factor = axis.scale else: @@ -1126,8 +1145,7 @@ def _remove_background_cli( bkg = background_estimator.function_nd(axis.axis) * scale_factor result = self - bkg except MemoryError: - result = self - model.as_signal( - show_progressbar=show_progressbar) + result = self - model.as_signal(show_progressbar=show_progressbar) if zero_fill: if self._lazy: @@ -1136,31 +1154,32 @@ def _remove_background_cli( cropped_da = result.data[low_idx:] result.data = da.concatenate([z, cropped_da]) else: - result.isig[:signal_range[0]] = 0 + result.isig[: signal_range[0]] = 0 if return_model: if fast: # Calculate the variance for each navigation position only when # using fast, otherwise the chisq is already calculated when # doing the multifit - d = result.data[..., np.where(model.channel_switches)[0]] + d = result.data[..., np.where(model._channel_switches)[0]] variance = model._get_variance(only_current=False) - d *= d / (1. * variance) # d = difference^2 / variance. + d *= d / (1.0 * variance) # d = difference^2 / variance. model.chisq.data = d.sum(-1) result = (result, model) return result def remove_background( - self, - signal_range='interactive', - background_type='Power law', - polynomial_order=2, - fast=True, - zero_fill=False, - plot_remainder=True, - show_progressbar=None, - return_model=False, - display=True, - toolkit=None): + self, + signal_range="interactive", + background_type="Power law", + polynomial_order=2, + fast=True, + zero_fill=False, + plot_remainder=True, + show_progressbar=None, + return_model=False, + display=True, + toolkit=None, + ): """ Remove the background, either in place using a GUI or returned as a new spectrum using the command line. The fast option is not accurate for @@ -1170,7 +1189,7 @@ def remove_background( Parameters ---------- - signal_range : "interactive", tuple of ints or floats, optional + signal_range : "interactive", tuple of int or float, optional If this argument is not specified, the signal range has to be selected using a GUI. And the original spectrum will be replaced. If tuple is given, a spectrum will be returned. @@ -1200,18 +1219,18 @@ def remove_background( return_model : bool If True, the background model is returned. The chi² can be obtained from this model using - :py:meth:`~hyperspy.models.model1d.Model1D.chisqd`. + :meth:`~hyperspy.model.BaseModel.chisq`. %s %s %s Returns ------- - {None, signal, background_model or (signal, background_model)} - If signal_range is not 'interactive', the signal with background - subtracted is returned. If return_model is True, returns the + None or (:class:`hyperspy.api.signals.BaseSignal`, :class:`hyperspy.models.model1d.Model1D`) + If ``signal_range`` is not ``'interactive'``, the signal with background + subtracted is returned. If ``return_model=True``, returns the background model, otherwise, the GUI widget dictionary is returned - if `display=False` - see the display parameter documentation. + if ``display=False`` - see the display parameter documentation. Examples -------- @@ -1222,8 +1241,9 @@ def remove_background( Using command line, returns a Signal1D: - >>> s.remove_background(signal_range=(400,450), - background_type='PowerLaw') + >>> s.remove_background( + ... signal_range=(400,450), background_type='PowerLaw' + ... ) Using a full model to fit the background: @@ -1233,9 +1253,9 @@ def remove_background( Returns background subtracted and the model: - >>> s.remove_background(signal_range=(400,450), - fast=False, - return_model=True) + >>> s.remove_background( + ... signal_range=(400,450), fast=False, return_model=True + ... ) (, ) Raises @@ -1247,15 +1267,19 @@ def remove_background( self._check_signal_dimension_equals_one() # Create model here, so that we can return it from hyperspy.models.model1d import Model1D + model = Model1D(self) - if signal_range == 'interactive': - br = BackgroundRemoval(self, background_type=background_type, - polynomial_order=polynomial_order, - fast=fast, - plot_remainder=plot_remainder, - show_progressbar=show_progressbar, - zero_fill=zero_fill, - model=model) + if signal_range == "interactive": + br = BackgroundRemoval( + self, + background_type=background_type, + polynomial_order=polynomial_order, + fast=fast, + plot_remainder=plot_remainder, + show_progressbar=show_progressbar, + zero_fill=zero_fill, + model=model, + ) gui_dict = br.gui(display=display, toolkit=toolkit) if return_model: return model @@ -1264,7 +1288,8 @@ def remove_background( return gui_dict else: background_estimator = _get_background_estimator( - background_type, polynomial_order)[0] + background_type, polynomial_order + )[0] result = self._remove_background_cli( signal_range=signal_range, background_estimator=background_estimator, @@ -1272,13 +1297,19 @@ def remove_background( zero_fill=zero_fill, show_progressbar=show_progressbar, model=model, - return_model=return_model) + return_model=return_model, + ) return result + remove_background.__doc__ %= (SHOW_PROGRESSBAR_ARG, DISPLAY_DT, TOOLKIT_DT) @interactive_range_selector - def crop_signal1D(self, left_value=None, right_value=None,): - """Crop in place the spectral dimension. + def crop_signal( + self, + left_value=None, + right_value=None, + ): + """Crop in place in the signal space. Parameters ---------- @@ -1302,8 +1333,11 @@ def crop_signal1D(self, left_value=None, right_value=None,): except TypeError: # It was not a ROI, we carry on pass - self.crop(axis=self.axes_manager.signal_axes[0].index_in_axes_manager, - start=left_value, end=right_value) + self.crop( + axis=self.axes_manager.signal_axes[0].index_in_axes_manager, + start=left_value, + end=right_value, + ) def gaussian_filter(self, FWHM): """Applies a Gaussian filter in the spectral dimension in place. @@ -1327,20 +1361,20 @@ def gaussian_filter(self, FWHM): for _axis in self.axes_manager.signal_axes: if not _axis.is_uniform: raise NotImplementedError( - "The function is not implemented for non-uniform axes.") + "The function is not implemented for non-uniform axes." + ) if FWHM <= 0: - raise ValueError( - "FWHM must be greater than zero") + raise ValueError("FWHM must be greater than zero") axis = self.axes_manager.signal_axes[0] FWHM *= 1 / axis.scale self.map(gaussian_filter1d, sigma=FWHM / 2.35482, ragged=False) - def hanning_taper(self, side='both', channels=None, offset=0): + def hanning_taper(self, side="both", channels=None, offset=0): """Apply a hanning taper to the data in place. Parameters ---------- - side : 'left', 'right' or 'both' + side : { ``'left'`` | ``'right'`` | ``'both'`` } Specify which side to use. channels : None or int The number of channels to taper. If None 5% of the total @@ -1349,7 +1383,7 @@ def hanning_taper(self, side='both', channels=None, offset=0): Returns ------- - channels + int Raises ------ @@ -1357,14 +1391,16 @@ def hanning_taper(self, side='both', channels=None, offset=0): If the signal dimension is not 1. """ if not np.issubdtype(self.data.dtype, np.floating): - raise TypeError("The data dtype should be `float`. It can be " - "changed by using the `change_dtype('float')` " - "method of the signal.") + raise TypeError( + "The data dtype should be `float`. It can be " + "changed by using the `change_dtype('float')` " + "method of the signal." + ) # TODO: generalize it self._check_signal_dimension_equals_one() if channels is None: - channels = int(round(len(self()) * 0.02)) + channels = int(round(len(self._get_current_data()) * 0.02)) if channels < 20: channels = 20 dc = self._data_aligned_with_axes @@ -1376,96 +1412,90 @@ def hanning_taper(self, side='both', channels=None, offset=0): else: nav_shape = shp[:-1] nav_chunks = dc.chunks[:-1] - zeros = da.zeros(nav_shape + (offset,), - chunks=nav_chunks + ((offset,),)) + zeros = da.zeros(nav_shape + (offset,), chunks=nav_chunks + ((offset,),)) - if side == 'left' or side == 'both': + if side == "left" or side == "both": if self._lazy: - tapered = dc[..., offset:channels + offset] + tapered = dc[..., offset : channels + offset] tapered *= np.hanning(2 * channels)[:channels] - therest = dc[..., channels + offset:] + therest = dc[..., channels + offset :] thelist = [] if offset == 0 else [zeros] thelist.extend([tapered, therest]) dc = da.concatenate(thelist, axis=-1) else: - dc[..., offset:channels + offset] *= ( - np.hanning(2 * channels)[:channels]) - dc[..., :offset] *= 0. - if side == 'right' or side == 'both': + dc[..., offset : channels + offset] *= np.hanning(2 * channels)[ + :channels + ] + dc[..., :offset] *= 0.0 + if side == "right" or side == "both": rl = None if offset == 0 else -offset if self._lazy: - therest = dc[..., :-channels - offset] - tapered = dc[..., -channels - offset:rl] + therest = dc[..., : -channels - offset] + tapered = dc[..., -channels - offset : rl] tapered *= np.hanning(2 * channels)[-channels:] thelist = [therest, tapered] if offset != 0: thelist.append(zeros) dc = da.concatenate(thelist, axis=-1) else: - dc[..., -channels - offset:rl] *= ( - np.hanning(2 * channels)[-channels:]) + dc[..., -channels - offset : rl] *= np.hanning(2 * channels)[-channels:] if offset != 0: - dc[..., -offset:] *= 0. + dc[..., -offset:] *= 0.0 if self._lazy: self.data = dc self.events.data_changed.trigger(obj=self) return channels - def find_peaks1D_ohaver(self, xdim=None, - slope_thresh=0, - amp_thresh=None, - subchannel=True, - medfilt_radius=5, - maxpeakn=30000, - peakgroup=10, - parallel=None, - max_workers=None): + def find_peaks1D_ohaver( + self, + xdim=None, + slope_thresh=0, + amp_thresh=None, + subchannel=True, + medfilt_radius=5, + maxpeakn=30000, + peakgroup=10, + num_workers=None, + ): """Find positive peaks along a 1D Signal. It detects peaks by looking for downward zero-crossings in the first derivative that exceed - 'slope_thresh'. + ``slope_thresh``. - 'slope_thresh' and 'amp_thresh', control sensitivity: higher + ``slope_thresh`` and ``amp_thresh``, control sensitivity: higher values will neglect broad peaks (slope) and smaller features (amp), respectively. - `peakgroup` is the number of points around the top of the peak - that are taken to estimate the peak height. For spikes or very - narrow peaks, set `peakgroup` to 1 or 2; for broad or noisy peaks, - make `peakgroup` larger to reduce the effect of noise. - Parameters ---------- - slope_thresh : float, optional + slope_thresh : float, default 0 1st derivative threshold to count the peak; - higher values will neglect broader features; - default is set to 0. + higher values will neglect broader features. amp_thresh : float, optional - intensity threshold below which peaks are ignored; + Intensity threshold below which peaks are ignored; higher values will neglect smaller features; default is set to 10%% of max(y). - medfilt_radius : int, optional - median filter window to apply to smooth the data - (see :py:func:`scipy.signal.medfilt`); - if 0, no filter will be applied; - default is set to 5. - peakgroup : int, optional - number of points around the "top part" of the peak - that are taken to estimate the peak height; - default is set to 10 - maxpeakn : int, optional - number of maximum detectable peaks; - default is set to 5000. + medfilt_radius : int, default 5 + Median filter window to apply to smooth the data + (see :func:`scipy.signal.medfilt`); + if 0, no filter will be applied. + peakgroup : int, default 10 + Number of points around the "top part" of the peak + that is taken to estimate the peak height. + For spikes or very narrow peaks, set `peakgroup` to 1 or 2; + for broad or noisy peaks, make ``peakgroup`` larger to + reduce the effect of noise. + maxpeakn : int, default 5000 + Number of maximum detectable peaks. subchannel : bool, default True - default is set to True. - %s + Whether to use subchannel precision or not. %s Returns ------- - structured array of shape (npeaks) containing fields: 'position', - 'width', and 'height' for each peak. - + numpy.ndarray + Structured array of shape (npeaks) containing fields: + 'position', 'width', and 'height' for each peak. Raises ------ @@ -1475,30 +1505,34 @@ def find_peaks1D_ohaver(self, xdim=None, # TODO: add scipy.signal.find_peaks_cwt self._check_signal_dimension_equals_one() axis = self.axes_manager.signal_axes[0].axis - peaks = self.map(find_peaks_ohaver, - x=axis, - slope_thresh=slope_thresh, - amp_thresh=amp_thresh, - medfilt_radius=medfilt_radius, - maxpeakn=maxpeakn, - peakgroup=peakgroup, - subchannel=subchannel, - ragged=True, - parallel=parallel, - max_workers=max_workers, - inplace=False) + peaks = self.map( + find_peaks_ohaver, + x=axis, + slope_thresh=slope_thresh, + amp_thresh=amp_thresh, + medfilt_radius=medfilt_radius, + maxpeakn=maxpeakn, + peakgroup=peakgroup, + subchannel=subchannel, + ragged=True, + output_dtype=object, + output_signal_size=(), + num_workers=num_workers, + inplace=False, + lazy_output=False, + silence_warnings="non-uniform", + ) return peaks.data - find_peaks1D_ohaver.__doc__ %= (PARALLEL_ARG, MAX_WORKERS_ARG) + find_peaks1D_ohaver.__doc__ %= NUM_WORKERS_ARG def estimate_peak_width( self, factor=0.5, window=None, return_interval=False, - parallel=None, show_progressbar=None, - max_workers=None, + num_workers=None, ): """Estimate the width of the highest intensity of peak of the spectra at a given fraction of its maximum. @@ -1509,8 +1543,9 @@ def estimate_peak_width( Parameters ---------- - factor : 0 < float < 1 - The default, 0.5, estimates the FWHM. + factor : float, default 0.5 + Normalized height (in interval [0, 1]) at which to estimate the + width. The default (0.5) estimates the FWHM. window : None or float The size of the window centred at the peak maximum used to perform the estimation. @@ -1518,23 +1553,18 @@ def estimate_peak_width( than the width of the peak at some positions or if it is so wide that it includes other more intense peaks this method cannot compute the width and a NaN is stored instead. - return_interval: bool + return_interval : bool If True, returns 2 extra signals with the positions of the desired height fraction at the left and right of the peak. %s %s - %s Returns ------- - width or [width, left, right], depending on the value of - `return_interval`. - - Notes - ----- - Parallel operation of this function is not supported - on Windows platforms. + float or list of float + width or [width, left, right], depending on the value of + `return_interval`. """ if show_progressbar is None: @@ -1543,116 +1573,111 @@ def estimate_peak_width( if not 0 < factor < 1: raise ValueError("factor must be between 0 and 1.") - if parallel != False and os.name in ["nt", "dos"]: # pragma: no cover - # Due to a scipy bug where scipy.interpolate.UnivariateSpline - # appears to not be thread-safe on Windows, we raise a warning - # here. See https://github.com/hyperspy/hyperspy/issues/2320 - # Until/if the scipy bug is fixed, we should do this. - _logger.warning( - "Parallel operation is not supported on Windows. " - "Setting `parallel=False`" - ) - parallel = False - - axis = self.axes_manager.signal_axes[0] - # x = axis.axis + # axis is a keyword already used by self.map so calling this axis_arg + # to avoid "parameter collision + axis_arg = self.axes_manager.signal_axes[0] maxval = self.axes_manager.navigation_size show_progressbar = show_progressbar and maxval > 0 - def estimating_function(spectrum, - window=None, - factor=0.5, - axis=None): - x = axis.axis + def estimating_function(spectrum, window=None, factor=0.5, axis_arg=None): + x = axis_arg.axis if window is not None: - vmax = axis.index2value(spectrum.argmax()) - slices = axis._get_array_slices( - slice(vmax - window * 0.5, vmax + window * 0.5)) + vmax = axis_arg.index2value(spectrum.argmax()) + slices = axis_arg._get_array_slices( + slice(vmax - window * 0.5, vmax + window * 0.5) + ) spectrum = spectrum[slices] x = x[slices] spline = interpolate.UnivariateSpline( - x, - spectrum - factor * spectrum.max(), - s=0) + x, spectrum - factor * spectrum.max(), s=0 + ) roots = spline.roots() if len(roots) == 2: return np.array(roots) else: return np.full((2,), np.nan) - both = self._map_iterate(estimating_function, - window=window, - factor=factor, - axis=axis, - ragged=False, - inplace=False, - parallel=parallel, - show_progressbar=show_progressbar, - max_workers=None) + both = self.map( + estimating_function, + window=window, + factor=factor, + axis_arg=axis_arg, + ragged=False, + inplace=False, + show_progressbar=show_progressbar, + num_workers=num_workers, + ) left, right = both.T.split() width = right - left if factor == 0.5: - width.metadata.General.title = ( - self.metadata.General.title + " FWHM") + width.metadata.General.title = self.metadata.General.title + " FWHM" left.metadata.General.title = ( - self.metadata.General.title + " FWHM left position") + self.metadata.General.title + " FWHM left position" + ) right.metadata.General.title = ( - self.metadata.General.title + " FWHM right position") + self.metadata.General.title + " FWHM right position" + ) else: width.metadata.General.title = ( - self.metadata.General.title + - " full-width at %.1f maximum" % factor) + self.metadata.General.title + " full-width at %.1f maximum" % factor + ) left.metadata.General.title = ( - self.metadata.General.title + - " full-width at %.1f maximum left position" % factor) + self.metadata.General.title + + " full-width at %.1f maximum left position" % factor + ) right.metadata.General.title = ( - self.metadata.General.title + - " full-width at %.1f maximum right position" % factor) + self.metadata.General.title + + " full-width at %.1f maximum right position" % factor + ) for signal in (left, width, right): - signal.axes_manager.set_signal_dimension(0) + signal = signal.transpose(signal_axes=[]) signal.set_signal_type("") if return_interval is True: return [width, left, right] else: return width - estimate_peak_width.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) + estimate_peak_width.__doc__ %= (SHOW_PROGRESSBAR_ARG, NUM_WORKERS_ARG) - def plot(self, - navigator="auto", - plot_markers=True, - autoscale='v', - norm="auto", - axes_manager=None, - navigator_kwds={}, - **kwargs): + def plot( + self, + navigator="auto", + plot_markers=True, + autoscale="v", + norm="auto", + axes_manager=None, + navigator_kwds={}, + **kwargs, + ): """%s %s %s """ for c in autoscale: - if c not in ['x', 'v']: - raise ValueError("`autoscale` only accepts 'x', 'v' as " - "valid characters.") - super().plot(navigator=navigator, - plot_markers=plot_markers, - autoscale=autoscale, - norm=norm, - axes_manager=axes_manager, - navigator_kwds=navigator_kwds, - **kwargs) - plot.__doc__ %= (BASE_PLOT_DOCSTRING, BASE_PLOT_DOCSTRING_PARAMETERS, - PLOT1D_DOCSTRING) + if c not in ["x", "v"]: + raise ValueError( + "`autoscale` only accepts 'x', 'v' as " "valid characters." + ) + super().plot( + navigator=navigator, + plot_markers=plot_markers, + autoscale=autoscale, + norm=norm, + axes_manager=axes_manager, + navigator_kwds=navigator_kwds, + **kwargs, + ) + plot.__doc__ %= ( + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT1D_DOCSTRING, + ) -class LazySignal1D(LazySignal, Signal1D): - """ - """ - _lazy = True +class LazySignal1D(LazySignal, Signal1D): + """Lazy general 1D signal class.""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.axes_manager.set_signal_dimension(1) + __doc__ += LAZYSIGNAL_DOC.replace("__BASECLASS__", "Signal1D") diff --git a/hyperspy/_signals/signal2d.py b/hyperspy/_signals/signal2d.py index 70fd94b1f7..8664a0c38f 100644 --- a/hyperspy/_signals/signal2d.py +++ b/hyperspy/_signals/signal2d.py @@ -1,52 +1,64 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import matplotlib.pyplot as plt -import numpy as np -import numpy.ma as ma -import dask.array as da import logging import warnings +from copy import deepcopy +from functools import partial +import dask.array as da +import matplotlib.pyplot as plt +import numpy as np +import numpy.ma as ma from scipy import ndimage -try: - # For scikit-image >= 0.17.0 - from skimage.registration._phase_cross_correlation import _upsampled_dft -except ModuleNotFoundError: - from skimage.feature.register_translation import _upsampled_dft +from skimage.registration._phase_cross_correlation import _upsampled_dft +from hyperspy._signals.common_signal2d import CommonSignal2D +from hyperspy._signals.lazy import LazySignal +from hyperspy._signals.signal1d import Signal1D from hyperspy.defaults_parser import preferences +from hyperspy.docstrings.plot import ( + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT2D_DOCSTRING, + PLOT2D_KWARGS_DOCSTRING, +) +from hyperspy.docstrings.signal import ( + LAZYSIGNAL_DOC, + NUM_WORKERS_ARG, + SHOW_PROGRESSBAR_ARG, +) from hyperspy.external.progressbar import progressbar -from hyperspy.misc.math_tools import symmetrize, antisymmetrize, optimal_fft_size +from hyperspy.misc.math_tools import antisymmetrize, optimal_fft_size, symmetrize from hyperspy.signal import BaseSignal -from hyperspy._signals.signal1d import Signal1D -from hyperspy._signals.lazy import LazySignal -from hyperspy._signals.common_signal2d import CommonSignal2D -from hyperspy.signal_tools import PeaksFinder2D -from hyperspy.docstrings.plot import ( - BASE_PLOT_DOCSTRING, BASE_PLOT_DOCSTRING_PARAMETERS, PLOT2D_DOCSTRING, - PLOT2D_KWARGS_DOCSTRING) -from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG +from hyperspy.signal_tools import PeaksFinder2D, Signal2DCalibration from hyperspy.ui_registry import DISPLAY_DT, TOOLKIT_DT from hyperspy.utils.peakfinders2D import ( - find_local_max, find_peaks_max, find_peaks_minmax, find_peaks_zaefferer, - find_peaks_stat, find_peaks_log, find_peaks_dog, find_peaks_xc) - + _get_peak_position_and_intensity, + find_local_max, + find_peaks_dog, + find_peaks_log, + find_peaks_max, + find_peaks_minmax, + find_peaks_stat, + find_peaks_xc, + find_peaks_zaefferer, +) _logger = logging.getLogger(__name__) @@ -87,8 +99,8 @@ def hanning2d(M, N): def sobel_filter(im): - sx = ndimage.sobel(im, axis=0, mode='constant') - sy = ndimage.sobel(im, axis=1, mode='constant') + sx = ndimage.sobel(im, axis=0, mode="constant") + sy = ndimage.sobel(im, axis=1, mode="constant") sob = np.hypot(sx, sy) return sob @@ -114,7 +126,7 @@ def fft_correlation(in1, in2, normalize=False, real_only=False): size = s1 + s2 - 1 # Calculate optimal FFT size - complex_result = (in1.dtype.kind == 'c' or in2.dtype.kind == 'c') + complex_result = in1.dtype.kind == "c" or in2.dtype.kind == "c" fsize = [optimal_fft_size(a, not complex_result) for a in size] # For real-valued inputs, rfftn is ~2x faster than fftn @@ -123,22 +135,30 @@ def fft_correlation(in1, in2, normalize=False, real_only=False): else: fft_f, ifft_f = np.fft.fftn, np.fft.ifftn - fprod = fft_f(in1, fsize) - fprod *= fft_f(in2, fsize).conjugate() + fprod = fft_f(in1, fsize, axes=[-2, -1]) + fprod *= fft_f(in2, fsize, axes=[-2, -1]).conjugate() if normalize is True: - fprod = np.nan_to_num(fprod / np.absolute(fprod)) + fprod = np.nan_to_num(fprod / abs(fprod)) ret = ifft_f(fprod).real.copy() return ret, fprod -def estimate_image_shift(ref, image, roi=None, sobel=True, - medfilter=True, hanning=True, plot=False, - dtype='float', normalize_corr=False, - sub_pixel_factor=1, - return_maxval=True): +def estimate_image_shift( + ref, + image, + roi=None, + sobel=True, + medfilter=True, + hanning=True, + plot=False, + dtype="float", + normalize_corr=False, + sub_pixel_factor=1, + return_maxval=True, +): """Estimate the shift in a image using phase correlation This method can only estimate the shift by comparing @@ -149,27 +169,27 @@ def estimate_image_shift(ref, image, roi=None, sobel=True, Parameters ---------- - ref : 2D numpy.ndarray - Reference image - image : 2D numpy.ndarray + ref : numpy.ndarray + Reference image. + image : numpy.ndarray Image to register - roi : tuple of ints (top, bottom, left, right) - Define the region of interest + roi : tuple of int. + Define the region of interest (top, bottom, left, right) sobel : bool apply a sobel filter for edge enhancement medfilter : bool apply a median filter for noise reduction hanning : bool Apply a 2d hanning filter - plot : bool or matplotlib.Figure + plot : bool or :class:`matplotlib.figure.Figure` If True, plots the images after applying the filters and the phase correlation. If a figure instance, the images will be plotted to the given figure. - reference : 'current' or 'cascade' - If 'current' (default) the image at the current - coordinates is taken as reference. If 'cascade' each image + reference : str + If ``'current'`` (default) the image at the current + coordinates is taken as reference. If ``'cascade'`` each image is aligned with the previous one. - dtype : str or dtype + dtype : str or numpy.dtype Typecode or data-type in which the calculations must be performed. normalize_corr : bool @@ -181,7 +201,7 @@ def estimate_image_shift(ref, image, roi=None, sobel=True, Returns ------- shifts: np.array - containing the estimate shifts + containing the estimate shifts in pixels max_value : float The maximum value of the correlation @@ -206,7 +226,9 @@ def estimate_image_shift(ref, image, roi=None, sobel=True, if roi is not None: top, bottom, left, right = roi else: - top, bottom, left, right = [None, ] * 4 + top, bottom, left, right = [ + None, + ] * 4 # Select region of interest ref = ref[top:bottom, left:right] @@ -226,20 +248,25 @@ def estimate_image_shift(ref, image, roi=None, sobel=True, im[:] = sobel_filter(im) # If sub-pixel alignment not being done, use faster real-valued fft - real_only = (sub_pixel_factor == 1) + real_only = sub_pixel_factor == 1 phase_correlation, image_product = fft_correlation( - ref, image, normalize=normalize_corr, real_only=real_only) + ref, image, normalize=normalize_corr, real_only=real_only + ) # Estimate the shift by getting the coordinates of the maximum - argmax = np.unravel_index(np.argmax(phase_correlation), - phase_correlation.shape) - threshold = (phase_correlation.shape[0] / 2 - 1, - phase_correlation.shape[1] / 2 - 1) - shift0 = argmax[0] if argmax[0] < threshold[0] else \ - argmax[0] - phase_correlation.shape[0] - shift1 = argmax[1] if argmax[1] < threshold[1] else \ - argmax[1] - phase_correlation.shape[1] + argmax = np.unravel_index(np.argmax(phase_correlation), phase_correlation.shape) + threshold = (phase_correlation.shape[0] / 2 - 1, phase_correlation.shape[1] / 2 - 1) + shift0 = ( + argmax[0] + if argmax[0] < threshold[0] + else argmax[0] - phase_correlation.shape[0] + ) + shift1 = ( + argmax[1] + if argmax[1] < threshold[1] + else argmax[1] - phase_correlation.shape[1] + ) max_val = phase_correlation.real.max() shifts = np.array((shift0, shift1)) @@ -251,20 +278,22 @@ def estimate_image_shift(ref, image, roi=None, sobel=True, upsampled_region_size = np.ceil(sub_pixel_factor * 1.5) # Center of output array at dftshift + 1 dftshift = np.fix(upsampled_region_size / 2.0) - sub_pixel_factor = np.array(sub_pixel_factor, dtype=np.float64) - normalization = (image_product.size * sub_pixel_factor ** 2) + sub_pixel_factor = np.array(sub_pixel_factor, dtype=float) + normalization = image_product.size * sub_pixel_factor**2 # Matrix multiply DFT around the current shift estimate sample_region_offset = dftshift - shifts * sub_pixel_factor - correlation = _upsampled_dft(image_product.conj(), - upsampled_region_size, - sub_pixel_factor, - sample_region_offset).conj() + correlation = _upsampled_dft( + image_product.conj(), + upsampled_region_size, + sub_pixel_factor, + sample_region_offset, + ).conj() correlation /= normalization # Locate maximum and map back to original pixel grid - maxima = np.array(np.unravel_index( - np.argmax(np.abs(correlation)), - correlation.shape), - dtype=np.float64) + maxima = np.array( + np.unravel_index(np.argmax(abs(correlation)), correlation.shape), + dtype=float, + ) maxima -= dftshift shifts = shifts + maxima / sub_pixel_factor max_val = correlation.real.max() @@ -282,15 +311,14 @@ def estimate_image_shift(ref, image, roi=None, sobel=True, fig, axarr = plt.subplots(1, 3) full_plot = len(axarr[0].images) == 0 if full_plot: - axarr[0].set_title('Reference') - axarr[1].set_title('Image') - axarr[2].set_title('Phase correlation') + axarr[0].set_title("Reference") + axarr[1].set_title("Image") + axarr[2].set_title("Phase correlation") axarr[0].imshow(ref) axarr[1].imshow(image) d = (np.array(phase_correlation.shape) - 1) // 2 extent = [-d[1], d[1], -d[0], d[0]] - axarr[2].imshow(np.fft.fftshift(phase_correlation), - extent=extent) + axarr[2].imshow(np.fft.fftshift(phase_correlation), extent=extent) plt.show() else: axarr[0].images[0].set_data(ref) @@ -309,40 +337,38 @@ def estimate_image_shift(ref, image, roi=None, sobel=True, class Signal2D(BaseSignal, CommonSignal2D): + """General 2D signal class.""" - """ - """ _signal_dimension = 2 - _lazy = False - - def __init__(self, *args, **kw): - super().__init__(*args, **kw) - if self.axes_manager.signal_dimension != 2: - self.axes_manager.set_signal_dimension(2) - - def plot(self, - navigator="auto", - plot_markers=True, - autoscale='v', - saturated_pixels=None, - norm="auto", - vmin=None, - vmax=None, - gamma=1.0, - linthresh=0.01, - linscale=0.1, - scalebar=True, - scalebar_color="white", - axes_ticks=None, - axes_off=False, - axes_manager=None, - no_nans=False, - colorbar=True, - centre_colormap="auto", - min_aspect=0.1, - navigator_kwds={}, - **kwargs - ): + + def __init__(self, *args, **kwargs): + if kwargs.get("ragged", False): + raise ValueError("Signal2D can't be ragged.") + super().__init__(*args, **kwargs) + + def plot( + self, + navigator="auto", + plot_markers=True, + autoscale="v", + norm="auto", + vmin=None, + vmax=None, + gamma=1.0, + linthresh=0.01, + linscale=0.1, + scalebar=True, + scalebar_color="white", + axes_ticks=None, + axes_off=False, + axes_manager=None, + no_nans=False, + colorbar=True, + centre_colormap="auto", + min_aspect=0.1, + navigator_kwds={}, + **kwargs, + ): """%s %s %s @@ -350,14 +376,14 @@ def plot(self, """ for c in autoscale: - if c not in ['x', 'y', 'v']: - raise ValueError("`autoscale` only accepts 'x', 'y', 'v' as " - "valid characters.") + if c not in ["x", "y", "v"]: + raise ValueError( + "`autoscale` only accepts 'x', 'y', 'v' as " "valid characters." + ) super().plot( navigator=navigator, plot_markers=plot_markers, autoscale=autoscale, - saturated_pixels=saturated_pixels, norm=norm, vmin=vmin, vmax=vmax, @@ -374,41 +400,49 @@ def plot(self, centre_colormap=centre_colormap, min_aspect=min_aspect, navigator_kwds=navigator_kwds, - **kwargs + **kwargs, ) - plot.__doc__ %= (BASE_PLOT_DOCSTRING, BASE_PLOT_DOCSTRING_PARAMETERS, - PLOT2D_DOCSTRING, PLOT2D_KWARGS_DOCSTRING) + + plot.__doc__ %= ( + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT2D_DOCSTRING, + PLOT2D_KWARGS_DOCSTRING, + ) def create_model(self, dictionary=None): """Create a model for the current signal Parameters ---------- - dictionary : {None, dict}, optional + dictionary : None or dict, optional A dictionary to be used to recreate a model. Usually generated - using :meth:`hyperspy.model.as_dictionary` + using :meth:`hyperspy.model.BaseModel.as_dictionary` Returns ------- - A Model class + hyperspy.models.model2d.Model2D """ from hyperspy.models.model2d import Model2D + return Model2D(self, dictionary=dictionary) - def estimate_shift2D(self, - reference='current', - correlation_threshold=None, - chunk_size=30, - roi=None, - normalize_corr=False, - sobel=True, - medfilter=True, - hanning=True, - plot=False, - dtype='float', - show_progressbar=None, - sub_pixel_factor=1): + def estimate_shift2D( + self, + reference="current", + correlation_threshold=None, + chunk_size=30, + roi=None, + normalize_corr=False, + sobel=True, + medfilter=True, + hanning=True, + plot=False, + dtype="float", + show_progressbar=None, + sub_pixel_factor=1, + ): """Estimate the shifts in an image using phase correlation. This method can only estimate the shift by comparing @@ -426,20 +460,20 @@ def estimate_shift2D(self, of every image with all the rest is estimated and by performing statistical analysis on the result the translation is estimated. - correlation_threshold : {None, 'auto', float} + correlation_threshold : None, str or float This parameter is only relevant when reference='stat'. If float, the shift estimations with a maximum correlation value lower than the given value are not used to compute the estimated shifts. If 'auto' the threshold is calculated automatically as the minimum maximum correlation value of the automatically selected reference image. - chunk_size : {None, int} + chunk_size : None or int If int and reference='stat' the number of images used as reference are limited to the given value. - roi : tuple of ints or floats (left, right, top, bottom) - Define the region of interest. If int(float) the position - is given axis index(value). Note that ROIs can be used - in place of a tuple. + roi : tuple of int or float + Define the region of interest (left, right, top, bottom). + If int (float), the position is given by axis index (value). + Note that ROIs can be used in place of a tuple. normalize_corr : bool, default False If True, use phase correlation to align the images, otherwise use cross correlation. @@ -449,12 +483,12 @@ def estimate_shift2D(self, Apply a median filter for noise reduction hanning : bool, default True Apply a 2D hanning filter - plot : bool or 'reuse' + plot : bool or str If True plots the images after applying the filters and the phase correlation. If 'reuse', it will also plot the images, but it will only use one figure, and continuously update the images in that figure as it progresses through the stack. - dtype : str or dtype + dtype : str or numpy.dtype Typecode or data-type in which the calculations must be performed. %s @@ -464,24 +498,24 @@ def estimate_shift2D(self, Returns ------- - shifts : list of array - List of estimated shifts + numpy.ndarray + Estimated shifts in pixels. Notes ----- The statistical analysis approach to the translation estimation - when using ``reference='stat'`` roughly follows [Schaffer2004]_. + when using ``reference='stat'`` roughly follows [*]_. If you use it please cite their article. References ---------- - .. [Schaffer2004] Schaffer, Bernhard, Werner Grogger, and Gerald Kothleitner. + .. [*] Schaffer, Bernhard, Werner Grogger, and Gerald Kothleitner. “Automated Spatial Drift Correction for EFTEM Image Series.” Ultramicroscopy 102, no. 1 (December 2004): 27–36. See Also -------- - * :py:meth:`~._signals.signal2d.Signal2D.align2D` + align2D """ if show_progressbar is None: @@ -491,28 +525,32 @@ def estimate_shift2D(self, # Get the indices of the roi yaxis = self.axes_manager.signal_axes[1] xaxis = self.axes_manager.signal_axes[0] - roi = tuple([xaxis._get_index(i) for i in roi[2:]] + - [yaxis._get_index(i) for i in roi[:2]]) + roi = tuple( + [xaxis._get_index(i) for i in roi[2:]] + + [yaxis._get_index(i) for i in roi[:2]] + ) - ref = None if reference == 'cascade' else \ - self.__call__().copy() + ref = None if reference == "cascade" else self._get_current_data().copy() shifts = [] nrows = None images_number = self.axes_manager._max_index + 1 - if plot == 'reuse': + if plot == "reuse": # Reuse figure for plots plot = plt.figure() - if reference == 'stat': - nrows = images_number if chunk_size is None else \ - min(images_number, chunk_size) - pcarray = ma.zeros((nrows, self.axes_manager._max_index + 1, - ), - dtype=np.dtype([('max_value', float), - ('shift', np.int32, - (2,))])) + if reference == "stat": + nrows = ( + images_number if chunk_size is None else min(images_number, chunk_size) + ) + pcarray = ma.zeros( + ( + nrows, + self.axes_manager._max_index + 1, + ), + dtype=np.dtype([("max_value", float), ("shift", np.int32, (2,))]), + ) nshift, max_value = estimate_image_shift( - self(), - self(), + self._get_current_data(), + self._get_current_data(), roi=roi, sobel=sobel, medfilter=medfilter, @@ -520,40 +558,47 @@ def estimate_shift2D(self, normalize_corr=normalize_corr, plot=plot, dtype=dtype, - sub_pixel_factor=sub_pixel_factor) - np.fill_diagonal(pcarray['max_value'], max_value) + sub_pixel_factor=sub_pixel_factor, + ) + np.fill_diagonal(pcarray["max_value"], max_value) pbar_max = nrows * images_number else: pbar_max = images_number # Main iteration loop. Fills the rows of pcarray when reference # is stat - with progressbar(total=pbar_max, - disable=not show_progressbar, - leave=True) as pbar: + with progressbar( + total=pbar_max, disable=not show_progressbar, leave=True + ) as pbar: for i1, im in enumerate(self._iterate_signal()): - if reference in ['current', 'cascade']: + if reference in ["current", "cascade"]: if ref is None: ref = im.copy() - shift = np.array([0., 0.]) + shift = np.array([0.0, 0.0]) nshift, max_val = estimate_image_shift( - ref, im, roi=roi, sobel=sobel, medfilter=medfilter, - hanning=hanning, plot=plot, - normalize_corr=normalize_corr, dtype=dtype, - sub_pixel_factor=sub_pixel_factor) - if reference == 'cascade': + ref, + im, + roi=roi, + sobel=sobel, + medfilter=medfilter, + hanning=hanning, + plot=plot, + normalize_corr=normalize_corr, + dtype=dtype, + sub_pixel_factor=sub_pixel_factor, + ) + if reference == "cascade": shift += nshift ref = im.copy() else: shift = nshift shifts.append(shift.copy()) pbar.update(1) - elif reference == 'stat': + elif reference == "stat": if i1 == nrows: break # Iterate to fill the columns of pcarray - for i2, im2 in enumerate( - self._iterate_signal()): + for i2, im2 in enumerate(self._iterate_signal()): if i2 > i1: nshift, max_value = estimate_image_shift( im, @@ -565,29 +610,28 @@ def estimate_shift2D(self, normalize_corr=normalize_corr, plot=plot, dtype=dtype, - sub_pixel_factor=sub_pixel_factor) + sub_pixel_factor=sub_pixel_factor, + ) pcarray[i1, i2] = max_value, nshift del im2 pbar.update(1) del im - if reference == 'stat': + if reference == "stat": # Select the reference image as the one that has the # higher max_value in the row sqpcarr = pcarray[:, :nrows] - sqpcarr['max_value'][:] = symmetrize(sqpcarr['max_value']) - sqpcarr['shift'][:] = antisymmetrize(sqpcarr['shift']) - ref_index = np.argmax(pcarray['max_value'].min(1)) + sqpcarr["max_value"][:] = symmetrize(sqpcarr["max_value"]) + sqpcarr["shift"][:] = antisymmetrize(sqpcarr["shift"]) + ref_index = np.argmax(pcarray["max_value"].min(1)) self.ref_index = ref_index - shifts = (pcarray['shift'] + - pcarray['shift'][ref_index, :nrows][:, np.newaxis]) + shifts = ( + pcarray["shift"] + pcarray["shift"][ref_index, :nrows][:, np.newaxis] + ) if correlation_threshold is not None: - if correlation_threshold == 'auto': - correlation_threshold = \ - (pcarray['max_value'].min(0)).max() - _logger.info("Correlation threshold = %1.2f", - correlation_threshold) - shifts[pcarray['max_value'] < - correlation_threshold] = ma.masked + if correlation_threshold == "auto": + correlation_threshold = (pcarray["max_value"].min(0)).max() + _logger.info("Correlation threshold = %1.2f", correlation_threshold) + shifts[pcarray["max_value"] < correlation_threshold] = ma.masked shifts.mask[ref_index, :] = False shifts = shifts.mean(0) @@ -606,16 +650,15 @@ def align2D( expand=False, interpolation_order=1, show_progressbar=None, - parallel=None, - max_workers=None, + num_workers=None, **kwargs, ): - """Align the images in-place using :py:func:`scipy.ndimage.shift`. + """Align the images in-place using :func:`scipy.ndimage.shift`. The images can be aligned using either user-provided shifts or by first estimating the shifts. - See :py:meth:`~._signals.signal2d.Signal2D.estimate_shift2D` + See :meth:`~hyperspy.api.signals.Signal2D.estimate_shift2D` for more details on estimating image shifts. Parameters @@ -623,27 +666,29 @@ def align2D( crop : bool If True, the data will be cropped not to include regions with missing data - fill_value : int, float, nan + fill_value : int, float, np.nan The areas with missing data are filled with the given value. - Default is nan. - shifts : None or list of tuples - If None the shifts are estimated using - :py:meth:`~._signals.signal2D.estimate_shift2D`. + Default is np.nan. + shifts : None or numpy.ndarray + The array of shifts must be in pixel units. The shape must be + the navigation shape using numpy order convention. If ``None`` + the shifts are estimated using + :meth:`~hyperspy.api.signals.Signal2D.estimate_shift2D`. expand : bool If True, the data will be expanded to fit all data after alignment. - Overrides `crop`. - interpolation_order: int, default 1. + Overrides ``crop``. + interpolation_order: int The order of the spline interpolation. Default is 1, linear interpolation. %s %s - %s - **kwargs : - Keyword arguments passed to :py:meth:`~._signals.signal2d.Signal2D.estimate_shift2D` + **kwargs : dict + Keyword arguments passed to + :meth:`~hyperspy.api.signals.Signal2D.estimate_shift2D`. Returns ------- - shifts : np.array + numpy.ndarray The estimated shifts are returned only if ``shifts`` is None Raises @@ -653,7 +698,7 @@ def align2D( See Also -------- - * :py:meth:`~._signals.signal2d.Signal2D.estimate_shift2D` + estimate_shift2D """ self._check_signal_dimension_equals_two() @@ -661,7 +706,8 @@ def align2D( for _axis in self.axes_manager.signal_axes: if not _axis.is_uniform: raise NotImplementedError( - "This operation is not implememented for non-uniform axes") + "This operation is not implememented for non-uniform axes" + ) return_shifts = False @@ -689,13 +735,17 @@ def align2D( signal_shifts = shifts if expand: # Expand to fit all valid data - left, right = ( - int(np.floor(signal_shifts.isig[1].min().data)) if signal_shifts.isig[1].min().data < 0 else 0, - int(np.ceil(signal_shifts.isig[1].max().data)) if signal_shifts.isig[1].max().data > 0 else 0, - ) + _min0 = signal_shifts.isig[0].min().data[0] + _max0 = signal_shifts.isig[0].max().data[0] top, bottom = ( - int(np.ceil(signal_shifts.isig[0].min().data)) if signal_shifts.isig[0].min().data < 0 else 0, - int(np.floor(signal_shifts.isig[0].max().data)) if signal_shifts.isig[0].max().data > 0 else 0, + int(np.ceil(_min0)) if _min0 < 0 else 0, + int(np.floor(_max0)) if _max0 > 0 else 0, + ) + _min1 = signal_shifts.isig[1].min().data[0] + _max1 = signal_shifts.isig[1].max().data[0] + left, right = ( + int(np.floor(_min1)) if _min1 < 0 else 0, + int(np.ceil(_max1)) if _max1 > 0 else 0, ) xaxis = self.axes_manager.signal_axes[0] yaxis = self.axes_manager.signal_axes[1] @@ -729,8 +779,7 @@ def align2D( shift_image, shift=signal_shifts, show_progressbar=show_progressbar, - parallel=parallel, - max_workers=max_workers, + num_workers=num_workers, ragged=False, inplace=True, fill_value=fill_value, @@ -739,20 +788,29 @@ def align2D( if crop and not expand: max_shift = signal_shifts.max() - signal_shifts.min() if np.any(max_shift.data >= np.array(self.axes_manager.signal_shape)): - raise ValueError("Shift outside range of signal axes. Cannot crop signal." + - "Max shift:" + str(max_shift.data) + " shape" + str(self.axes_manager.signal_shape)) + raise ValueError( + "Shift outside range of signal axes. Cannot crop signal." + + "Max shift:" + + str(max_shift.data) + + " shape" + + str(self.axes_manager.signal_shape) + ) # Crop the image to the valid size + _min0 = signal_shifts.isig[0].min().data[0] + _max0 = signal_shifts.isig[0].max().data[0] shifts = -shifts bottom, top = ( - int(np.floor(signal_shifts.isig[0].min().data)) if signal_shifts.isig[0].min().data < 0 else None, - int(np.ceil(signal_shifts.isig[0].max().data)) if signal_shifts.isig[0].max().data > 0 else 0, + int(np.floor(_min0)) if _min0 < 0 else None, + int(np.ceil(_max0)) if _max0 > 0 else 0, ) + _min1 = signal_shifts.isig[1].min().data[0] + _max1 = signal_shifts.isig[1].max().data[0] right, left = ( - int(np.floor(signal_shifts.isig[1].min().data)) if signal_shifts.isig[1].min().data < 0 else None, - int(np.ceil(signal_shifts.isig[1].max().data)) if signal_shifts.isig[1].max().data > 0 else 0, + int(np.floor(_min1)) if _min1 < 0 else None, + int(np.ceil(_max1)) if _max1 > 0 else 0, ) - self.crop_image(top, bottom, left, right) + self.crop_signal(top, bottom, left, right) shifts = -shifts self.events.data_changed.trigger(obj=self) @@ -760,15 +818,106 @@ def align2D( if return_shifts: return shifts - align2D.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) + align2D.__doc__ %= (SHOW_PROGRESSBAR_ARG, NUM_WORKERS_ARG) - def crop_image(self, top=None, bottom=None, - left=None, right=None, convert_units=False): - """Crops an image in place. + def calibrate( + self, + x0=None, + y0=None, + x1=None, + y1=None, + new_length=None, + units=None, + interactive=True, + display=True, + toolkit=None, + ): + """Calibrate the x and y signal dimensions. + + Can be used either interactively, or by passing values as parameters. Parameters ---------- - top, bottom, left, right : {int | float} + x0, y0, x1, y1 : float, int, optional + If interactive is False, these must be set. If given in floats + the input will be in scaled axis values. If given in integers, + the input will be in non-scaled pixel values. Similar to how + integer and float input works when slicing using isig and inav. + new_length : scalar, optional + If interactive is False, this must be set. + units : string, optional + If interactive is False, this is used to set the axes units. + interactive : bool, default True + If True, will use a plot with an interactive line for calibration. + If False, x0, y0, x1, y1 and new_length must be set. + display : bool, default True + toolkit : string, optional + + Examples + -------- + >>> s = hs.signals.Signal2D(np.random.random((100, 100))) + >>> s.calibrate() # doctest: +SKIP + + Running non-interactively + + >>> s = hs.signals.Signal2D(np.random.random((100, 100))) + >>> s.calibrate(x0=10, y0=10, x1=60, y1=10, new_length=100, + ... interactive=False, units="nm") + + """ + self._check_signal_dimension_equals_two() + if interactive: + calibration = Signal2DCalibration(self) + calibration.gui(display=display, toolkit=toolkit) + else: + if None in (x0, y0, x1, y1, new_length): + raise ValueError( + "With interactive=False x0, y0, x1, y1 and new_length " + "must be set." + ) + self._calibrate(x0, y0, x1, y1, new_length, units=units) + + def _calibrate(self, x0, y0, x1, y1, new_length, units=None): + scale = self._get_signal2d_scale(x0, y0, x1, y1, new_length) + sa = self.axes_manager.signal_axes + sa[0].scale = scale + sa[1].scale = scale + if units is not None: + sa[0].units = units + sa[1].units = units + + def _get_signal2d_scale(self, x0, y0, x1, y1, length): + sa = self.axes_manager.signal_axes + units = set([a.units for a in sa]) + if len(units) != 1: + _logger.warning( + "The signal axes does not have the same units, this might lead to " + "strange values after this calibration" + ) + scales = set([a.scale for a in sa]) + if len(scales) != 1: + _logger.warning( + "The previous scaling is not the same for both axes, this might lead to " + "strange values after this calibration" + ) + x0 = sa[0]._get_index(x0) + y0 = sa[1]._get_index(y0) + x1 = sa[0]._get_index(x1) + y1 = sa[1]._get_index(y1) + pos = ((x0, y0), (x1, y1)) + old_length = np.linalg.norm(np.diff(pos, axis=0), axis=1)[0] + scale = length / old_length + return scale + + def crop_signal( + self, top=None, bottom=None, left=None, right=None, convert_units=False + ): + """ + Crops in signal space and in place. + + Parameters + ---------- + top, bottom, left, right : int or float If int the values are taken as indices. If float the values are converted to indices. convert_units : bool @@ -776,20 +925,16 @@ def crop_image(self, top=None, bottom=None, If True, convert the signal units using the 'convert_to_units' method of the `axes_manager`. If False, does nothing. - See also + See Also -------- - crop + hyperspy.api.signals.BaseSignal.crop """ self._check_signal_dimension_equals_two() - self.crop(self.axes_manager.signal_axes[1].index_in_axes_manager, - top, - bottom) - self.crop(self.axes_manager.signal_axes[0].index_in_axes_manager, - left, - right) + self.crop(self.axes_manager.signal_axes[1].index_in_axes_manager, top, bottom) + self.crop(self.axes_manager.signal_axes[0].index_in_axes_manager, left, right) if convert_units: - self.axes_manager.convert_units('signal') + self.axes_manager.convert_units("signal") def add_ramp(self, ramp_x, ramp_y, offset=0): """Add a linear ramp to the signal. @@ -812,19 +957,27 @@ def add_ramp(self, ramp_x, ramp_y, offset=0): """ yy, xx = np.indices(self.axes_manager._signal_shape_in_array) if self._lazy: - import dask.array as da - ramp = offset * da.ones(self.data.shape, dtype=self.data.dtype, - chunks=self.data.chunks) + ramp = offset * da.ones( + self.data.shape, dtype=self.data.dtype, chunks=self.data.chunks + ) else: ramp = offset * np.ones(self.data.shape, dtype=self.data.dtype) ramp += ramp_x * xx ramp += ramp_y * yy self.data += ramp - def find_peaks(self, method='local_max', interactive=True, - current_index=False, show_progressbar=None, - parallel=None, max_workers=None, display=True, toolkit=None, - **kwargs): + def find_peaks( + self, + method="local_max", + interactive=True, + current_index=False, + show_progressbar=None, + num_workers=None, + display=True, + toolkit=None, + get_intensity=False, + **kwargs, + ): """Find peaks in a 2D signal. Function to locate the positive peaks in an image using various, user @@ -838,34 +991,34 @@ def find_peaks(self, method='local_max', interactive=True, are: * 'local_max' - simple local maximum search using the - :py:func:`skimage.feature.peak_local_max` function + :func:`skimage.feature.peak_local_max` function * 'max' - simple local maximum search using the - :py:func:`~hyperspy.utils.peakfinders2D.find_peaks_max`. + :func:`~hyperspy.utils.peakfinders2D.find_peaks_max`. * 'minmax' - finds peaks by comparing maximum filter results with minimum filter, calculates centers of mass. See the - :py:func:`~hyperspy.utils.peakfinders2D.find_peaks_minmax` + :func:`~hyperspy.utils.peakfinders2D.find_peaks_minmax` function. * 'zaefferer' - based on gradient thresholding and refinement by local region of interest optimisation. See the - :py:func:`~hyperspy.utils.peakfinders2D.find_peaks_zaefferer` + :func:`~hyperspy.utils.peakfinders2D.find_peaks_zaefferer` function. * 'stat' - based on statistical refinement and difference with respect to mean intensity. See the - :py:func:`~hyperspy.utils.peakfinders2D.find_peaks_stat` + :func:`~hyperspy.utils.peakfinders2D.find_peaks_stat` function. * 'laplacian_of_gaussian' - a blob finder using the laplacian of Gaussian matrices approach. See the - :py:func:`~hyperspy.utils.peakfinders2D.find_peaks_log` + :func:`~hyperspy.utils.peakfinders2D.find_peaks_log` function. * 'difference_of_gaussian' - a blob finder using the difference of Gaussian matrices approach. See the - :py:func:`~hyperspy.utils.peakfinders2D.find_peaks_log` + :func:`~hyperspy.utils.peakfinders2D.find_peaks_dog` function. * 'template_matching' - A cross correlation peakfinder. This method requires providing a template with the ``template`` parameter, which is used as reference pattern to perform the template matching to the signal. It uses the - :py:func:`skimage.feature.match_template` function and the peaks + :func:`skimage.feature.match_template` function and the peaks position are obtained by using `minmax` method on the template matching result. @@ -873,8 +1026,10 @@ def find_peaks(self, method='local_max', interactive=True, If True, the method parameter can be adjusted interactively. If False, the results will be returned. current_index : bool - if True, the computation will be performed for the current index. - %s + If True, the computation will be performed for the current index. + get_intensity : bool + If True, the intensity of the peak will be returned as an additional column, + the last one. %s %s %s @@ -888,70 +1043,83 @@ def find_peaks(self, method='local_max', interactive=True, ----- As a convenience, the 'local_max' method accepts the 'distance' and 'threshold' argument, which will be map to the 'min_distance' and - 'threshold_abs' of the :py:func:`skimage.feature.peak_local_max` + 'threshold_abs' of the :func:`skimage.feature.peak_local_max` function. Returns ------- - peaks : :py:class:`~hyperspy.signal.BaseSignal` or numpy.ndarray if current_index=True - Array of shape `_navigation_shape_in_array` in which each cell - contains an array with dimensions (npeaks, 2) that contains - the `x, y` pixel coordinates of peaks found in each image sorted + peaks : :class:`~hyperspy.signal.BaseSignal` or numpy.ndarray + numpy.ndarray if current_index=True. + Ragged signal with shape (npeaks, 2) that contains the `x, y` + pixel coordinates of peaks found in each image sorted first along `y` and then along `x`. """ method_dict = { - 'local_max': find_local_max, - 'max': find_peaks_max, - 'minmax': find_peaks_minmax, - 'zaefferer': find_peaks_zaefferer, - 'stat': find_peaks_stat, - 'laplacian_of_gaussian': find_peaks_log, - 'difference_of_gaussian': find_peaks_dog, - 'template_matching' : find_peaks_xc, + "local_max": find_local_max, + "max": find_peaks_max, + "minmax": find_peaks_minmax, + "zaefferer": find_peaks_zaefferer, + "stat": find_peaks_stat, + "laplacian_of_gaussian": find_peaks_log, + "difference_of_gaussian": find_peaks_dog, + "template_matching": find_peaks_xc, } # As a convenience, we map 'distance' to 'min_distance' and # 'threshold' to 'threshold_abs' when using the 'local_max' method to # match with the arguments of skimage.feature.peak_local_max. - if method == 'local_max': - if 'distance' in kwargs.keys(): - kwargs['min_distance'] = kwargs.pop('distance') - if 'threshold' in kwargs.keys(): - kwargs['threshold_abs'] = kwargs.pop('threshold') + if method == "local_max": + if "distance" in kwargs.keys(): + kwargs["min_distance"] = kwargs.pop("distance") + if "threshold" in kwargs.keys(): + kwargs["threshold_abs"] = kwargs.pop("threshold") if method in method_dict.keys(): method_func = method_dict[method] else: - raise NotImplementedError(f"The method `{method}` is not " - "implemented. See documentation for " - "available implementations.") + raise NotImplementedError( + f"The method `{method}` is not " + "implemented. See documentation for " + "available implementations." + ) + if get_intensity: + method_func = partial( + _get_peak_position_and_intensity, + f=method_func, + ) if interactive: # Create a peaks signal with the same navigation shape as a # placeholder for the output axes_dict = self.axes_manager._get_axes_dicts( - self.axes_manager.navigation_axes) - peaks = BaseSignal(np.empty(self.axes_manager.navigation_shape), - axes=axes_dict) + self.axes_manager.navigation_axes + ) + peaks = BaseSignal( + np.empty(self.axes_manager.navigation_shape), axes=axes_dict + ) pf2D = PeaksFinder2D(self, method=method, peaks=peaks, **kwargs) pf2D.gui(display=display, toolkit=toolkit) elif current_index: - peaks = method_func(self.__call__(), **kwargs) + peaks = method_func(self._get_current_data(), **kwargs) else: - peaks = self.map(method_func, show_progressbar=show_progressbar, - parallel=parallel, inplace=False, ragged=True, - max_workers=max_workers, **kwargs) - if peaks._lazy: - peaks.compute() - - - + peaks = self.map( + method_func, + show_progressbar=show_progressbar, + inplace=False, + ragged=True, + num_workers=num_workers, + **kwargs, + ) + peaks.metadata.add_node("Peaks") # add information about the signal Axes + peaks.metadata.Peaks.signal_axes = deepcopy(self.axes_manager.signal_axes) return peaks - find_peaks.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG, - DISPLAY_DT, TOOLKIT_DT) + find_peaks.__doc__ %= ( + SHOW_PROGRESSBAR_ARG, + NUM_WORKERS_ARG, + DISPLAY_DT, + TOOLKIT_DT, + ) class LazySignal2D(LazySignal, Signal2D): + """Lazy general 2D signal class.""" - _lazy = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + __doc__ += LAZYSIGNAL_DOC.replace("__BASECLASS__", "Signal2D") diff --git a/hyperspy/api.py b/hyperspy/api.py index 7e8219df9d..4ca9923b8d 100644 --- a/hyperspy/api.py +++ b/hyperspy/api.py @@ -1,42 +1,157 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy.api_nogui import * +import importlib import logging +import sys + +from hyperspy.defaults_parser import preferences +from hyperspy.logger import set_log_level + +# Need to run before other import to use the logger during import _logger = logging.getLogger(__name__) +set_log_level(preferences.General.logging_level) + +from hyperspy import __version__ # noqa: E402 +from hyperspy.docstrings import START_HSPY as _START_HSPY_DOCSTRING # noqa: E402 + +__doc__ = ( + """ + +All public packages, functions and classes are available in this module. + +%s + +Functions: + + :func:`~.api.get_configuration_directory_path` + Return the configuration directory path. + + :func:`~.api.interactive` + Define operations that are automatically recomputed on event changes. + + :func:`~.api.load` + Load data into BaseSignal instances from supported files. + + :data:`~.api.preferences` + Preferences class instance to configure the default value of different + parameters. It has a CLI and a GUI that can be started by execting its + `gui` method i.e. `preferences.gui()`. + + :func:`~.api.print_known_signal_types` + Print all known `signal_type`. + + :func:`~.api.set_log_level` + Convenience function to set HyperSpy's the log level. + + :func:`~.api.stack` + Stack several signals. + + :func:`~.api.transpose` + Transpose a signal. + +The :mod:`~.api` package contains the following submodules/packages: + + :mod:`~.api.signals` + `Signal` classes which are the core of HyperSpy. Use this modules to + create `Signal` instances manually from numpy arrays. Note that to + load data from supported file formats is more convenient to use the + `load` function. + :mod:`~.api.model` + Components that can be used to create a model for curve fitting. + :mod:`~.api.plot` + Plotting functions that operate on multiple signals. + :mod:`~.api.data` + Synthetic datasets. + :mod:`~.api.roi` + Region of interests (ROIs) that operate on `BaseSignal` instances and + include widgets for interactive operation. + :mod:`~.api.samfire` + SAMFire utilities (strategies, Pool, fit convergence tests) + + +For more details see their doctrings. + +""" + % _START_HSPY_DOCSTRING +) + + +def get_configuration_directory_path(): + """Return configuration path""" + from hyperspy.defaults_parser import config_path + + return config_path + + +# ruff: noqa: F822 + +__all__ = [ + "data", + "get_configuration_directory_path", + "interactive", + "load", + "model", + "plot", + "preferences", + "print_known_signal_types", + "roi", + "samfire", + "set_log_level", + "signals", + "stack", + "transpose", + "__version__", +] + + +# mapping following the pattern: from value import key +_import_mapping = { + "interactive": ".utils", + "load": ".io", + "markers": ".utils", + "model": ".utils", + "plot": ".utils", + "print_known_signal_types": ".utils", + "roi": ".utils", + "samfire": ".utils", + "stack": ".utils", + "transpose": ".utils", +} + + +def __dir__(): + return sorted(__all__) + + +def __getattr__(name): + if name in __all__: + if name in _import_mapping.keys(): + import_path = "hyperspy" + _import_mapping.get(name) + return getattr(importlib.import_module(import_path), name) + else: + return importlib.import_module("." + name, "hyperspy") + # Special case _ureg to use it as a singleton + elif name == "_ureg": + if "_ureg" not in globals(): + import pint + + setattr(sys.modules[__name__], "_ureg", pint.get_application_registry()) + return getattr(sys.modules[__name__], "_ureg") -__doc__ = hyperspy.api_nogui.__doc__ - -try: - # Register ipywidgets by importing the module - import hyperspy_gui_ipywidgets -except ImportError: # pragma: no cover - from hyperspy.defaults_parser import preferences - if preferences.GUIs.warn_if_guis_are_missing: - _logger.warning( - "The ipywidgets GUI elements are not available, probably because the " - "hyperspy_gui_ipywidgets package is not installed.") -try: - # Register traitui UI elements by importing the module - import hyperspy_gui_traitsui -except ImportError: # pragma: no cover - from hyperspy.defaults_parser import preferences - if preferences.GUIs.warn_if_guis_are_missing: - _logger.warning( - "The traitsui GUI elements are not available, probably because the " - "hyperspy_gui_traitsui package is not installed.") + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/hyperspy/api_nogui.py b/hyperspy/api_nogui.py deleted file mode 100644 index e25773045d..0000000000 --- a/hyperspy/api_nogui.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -# Set the PyQt API to 2 to avoid incompatibilities between matplotlib -# traitsui - -import logging -_logger = logging.getLogger(__name__) -from hyperspy.logger import set_log_level -from hyperspy.defaults_parser import preferences -set_log_level(preferences.General.logging_level) -from hyperspy import signals -from hyperspy.utils import * -from hyperspy.io import load -from hyperspy import datasets -from hyperspy.Release import version as __version__ -from hyperspy import docstrings - -__doc__ = """ - -All public packages, functions and classes are available in this module. - -%s - -Functions: - - create_model - Create a model for curve fitting. - - get_configuration_directory_path - Return the configuration directory path. - - load - Load data into BaseSignal instances from supported files. - - preferences - Preferences class instance to configure the default value of different - parameters. It has a CLI and a GUI that can be started by execting its - `gui` method i.e. `preferences.gui()`. - - stack - Stack several signals. - - interactive - Define operations that are automatically recomputed on event changes. - - set_log_level - Convenience function to set HyperSpy's the log level. - - -The :mod:`~hyperspy.api` package contains the following submodules/packages: - - :mod:`~hyperspy.api.signals` - `Signal` classes which are the core of HyperSpy. Use this modules to - create `Signal` instances manually from numpy arrays. Note that to - load data from supported file formats is more convenient to use the - `load` function. - :mod:`~hyperspy.api.model` - Contains the :mod:`~hyperspy.api.model.components` module with - components that can be used to create a model for curve fitting. - :mod:`~hyperspy.api.eds` - Functions for energy dispersive X-rays data analysis. - :mod:`~hyperspy.api.material` - Useful functions for materials properties and elements database that - includes physical properties and X-rays and EELS energies. - :mod:`~hyperspy.api.plot` - Plotting functions that operate on multiple signals. - :mod:`~hyperspy.api.datasets` - Example datasets. - :mod:`~hyperspy.api.roi` - Region of interests (ROIs) that operate on `BaseSignal` instances and - include widgets for interactive operation. - :mod:`~hyperspy.api.samfire` - SAMFire utilities (strategies, Pool, fit convergence tests) - - -For more details see their doctrings. - -""" % docstrings.START_HSPY - -# Remove the module to avoid polluting the namespace -del docstrings - - - - - -def get_configuration_directory_path(): - import hyperspy.misc.config_dir - return hyperspy.misc.config_dir.config_path diff --git a/hyperspy/axes.py b/hyperspy/axes.py index 791367ea9c..f175a0ebdd 100644 --- a/hyperspy/axes.py +++ b/hyperspy/axes.py @@ -1,91 +1,63 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from contextlib import contextmanager import copy -import math +import inspect import logging +import math +import warnings +from collections.abc import Iterable +from contextlib import contextmanager -import numpy as np import dask.array as da -import traits.api as t -from traits.trait_errors import TraitError +import numpy as np import pint +import traits.api as t from sympy.utilities.lambdify import lambdify -from hyperspy.events import Events, Event +from traits.trait_errors import TraitError + +from hyperspy._components.expression import _parse_substitutions +from hyperspy.api import _ureg +from hyperspy.defaults_parser import preferences +from hyperspy.events import Event, Events from hyperspy.misc.array_tools import ( - numba_closest_index_round, - numba_closest_index_floor, numba_closest_index_ceil, - round_half_towards_zero, + numba_closest_index_floor, + numba_closest_index_round, round_half_away_from_zero, + round_half_towards_zero, ) -from hyperspy.misc.utils import isiterable, ordinal from hyperspy.misc.math_tools import isfloat +from hyperspy.misc.utils import TupleSA, isiterable, ordinal from hyperspy.ui_registry import add_gui_method, get_gui -from hyperspy.defaults_parser import preferences -from hyperspy._components.expression import _parse_substitutions - - -import warnings -import inspect -from collections.abc import Iterable _logger = logging.getLogger(__name__) -_ureg = pint.UnitRegistry() -FACTOR_DOCSTRING = \ - """factor : float (default: 0.25) +FACTOR_DOCSTRING = """factor : float (default: 0.25) 'factor' is an adjustable value used to determine the prefix of the units. The product `factor * scale * size` is passed to the pint `to_compact` method to determine the prefix.""" class ndindex_nat(np.ndindex): - def __next__(self): - return super(ndindex_nat, self).__next__()[::-1] - - -def generate_uniform_axis(offset, scale, size, offset_index=0): - """Creates a uniform axis vector given the offset, scale and number of - channels. - - Alternatively, the offset_index of the offset channel can be specified. - - Parameters - ---------- - offset : float - scale : float - size : number of channels - offset_index : int - offset_index number of the offset - - Returns - ------- - Numpy array - - """ - - return np.linspace(offset - offset_index * scale, - offset + scale * (size - 1 - offset_index), - size) + return super().__next__()[::-1] def create_axis(**kwargs): @@ -109,9 +81,9 @@ def create_axis(**kwargs): A DataAxis, FunctionalDataAxis or a UniformDataAxis """ - if 'axis' in kwargs.keys(): # non-uniform axis + if "axis" in kwargs.keys(): # non-uniform axis axis_class = DataAxis - elif 'expression' in kwargs.keys(): # Functional axis + elif "expression" in kwargs.keys(): # Functional axis axis_class = FunctionalDataAxis else: # if not argument is provided fall back to uniform axis axis_class = UniformDataAxis @@ -119,11 +91,26 @@ def create_axis(**kwargs): class UnitConversion: + """ + Parent class containing unit conversion functionalities of + Uniform Axis. + + Parameters + ---------- + offset : float + The first value of the axis vector. + scale : float + The spacing between axis points. + size : int + The number of points in the axis. + """ - def __init__(self, units=t.Undefined, scale=1.0, offset=0.0): + def __init__(self, units=None, scale=1.0, offset=0.0): + if units is None: + units = t.Undefined self.units = units self.scale = scale - self.offset = units + self.offset = offset def _ignore_conversion(self, units): if units == t.Undefined: @@ -131,49 +118,51 @@ def _ignore_conversion(self, units): try: _ureg(units) except pint.errors.UndefinedUnitError: - warnings.warn('Unit "{}" not supported for conversion. Nothing ' - 'done.'.format(units), - ) + warnings.warn(f"Unit {units} not supported for conversion. Nothing done.") return True return False def _convert_compact_units(self, factor=0.25, inplace=True): - """ Convert units to "human-readable" units, which means with a - convenient prefix. + """ + Convert units to "human-readable" units, which means with a + convenient prefix. - Parameters - ---------- - %s + Parameters + ---------- + %s """ if self._ignore_conversion(self.units): return scale = self.scale * _ureg(self.units) scale_size = factor * scale * self.size - converted_units = '{:~}'.format(scale_size.to_compact().units) + converted_units = "{:~}".format(scale_size.to_compact().units) return self._convert_units(converted_units, inplace=inplace) _convert_compact_units.__doc__ %= FACTOR_DOCSTRING def _get_value_from_value_with_units(self, value): if self.units is t.Undefined: - raise ValueError("Units conversion can't be perfomed " - f"because the axis '{self}' doesn't have " - "units.") + raise ValueError( + "Units conversion can't be perfomed " + f"because the axis '{self}' doesn't have " + "units." + ) value = _ureg.parse_expression(value) - if not hasattr(value, 'units'): + if not hasattr(value, "units"): raise ValueError(f"`{value}` should contain an units.") return float(value.to(self.units).magnitude) def _convert_units(self, converted_units, inplace=True): - if self._ignore_conversion(converted_units) or \ - self._ignore_conversion(self.units): + if self._ignore_conversion(converted_units) or self._ignore_conversion( + self.units + ): return scale_pint = self.scale * _ureg(self.units) offset_pint = self.offset * _ureg(self.units) scale = float(scale_pint.to(_ureg(converted_units)).magnitude) offset = float(offset_pint.to(_ureg(converted_units)).magnitude) - units = '{:~}'.format(scale_pint.to(_ureg(converted_units)).units) + units = "{:~}".format(scale_pint.to(_ureg(converted_units)).units) if inplace: self.scale = scale self.offset = offset @@ -182,7 +171,8 @@ def _convert_units(self, converted_units, inplace=True): return scale, offset, units def convert_to_units(self, units=None, inplace=True, factor=0.25): - """ Convert the scale and the units of the current axis. If the unit + """ + Convert the scale and the units of the current axis. If the unit of measure is not supported by the pint library, the scale and units are not modified. @@ -207,19 +197,21 @@ def convert_to_units(self, units=None, inplace=True, factor=0.25): convert_to_units.__doc__ %= FACTOR_DOCSTRING - def _get_quantity(self, attribute='scale'): - if attribute == 'scale' or attribute == 'offset': + def _get_quantity(self, attribute="scale"): + if attribute == "scale" or attribute == "offset": units = self.units if units == t.Undefined: - units = '' + units = "" return getattr(self, attribute) * _ureg(units) else: - raise ValueError('`attribute` argument can only take the `scale` ' - 'or the `offset` value.') + raise ValueError( + "`attribute` argument can only take the `scale` " + "or the `offset` value." + ) - def _set_quantity(self, value, attribute='scale'): - if attribute == 'scale' or attribute == 'offset': - units = '' if self.units == t.Undefined else self.units + def _set_quantity(self, value, attribute="scale"): + if attribute == "scale" or attribute == "offset": + units = "" if self.units == t.Undefined else self.units if isinstance(value, str): value = _ureg.parse_expression(value) if isinstance(value, float): @@ -227,16 +219,18 @@ def _set_quantity(self, value, attribute='scale'): # to be consistent, we also need to convert the other one # (scale or offset) when both units differ. - if value.units != units and value.units != '' and units != '': - other = 'offset' if attribute == 'scale' else 'scale' + if value.units != units and value.units != "" and units != "": + other = "offset" if attribute == "scale" else "scale" other_quantity = self._get_quantity(other).to(value.units) setattr(self, other, float(other_quantity.magnitude)) - self.units = '{:~}'.format(value.units) + self.units = "{:~}".format(value.units) setattr(self, attribute, float(value.magnitude)) else: - raise ValueError('`attribute` argument can only take the `scale` ' - 'or the `offset` value.') + raise ValueError( + "`attribute` argument can only take the `scale` " + "or the `offset` value." + ) @property def units(self): @@ -244,7 +238,7 @@ def units(self): @units.setter def units(self, s): - if s == '': + if s == "": self._units = t.Undefined self._units = s @@ -264,66 +258,82 @@ class BaseDataAxis(t.HasTraits): is_binned : bool, optional True if data along the axis is binned. Default False. """ + name = t.Str() units = t.Str() size = t.CInt() low_value = t.Float() high_value = t.Float() - value = t.Range('low_value', 'high_value') + value = t.Range("low_value", "high_value") low_index = t.Int(0) high_index = t.Int() slice = t.Instance(slice) - navigate = t.Bool(t.Undefined) - is_binned = t.Bool(t.Undefined) - index = t.Range('low_index', 'high_index') + navigate = t.Bool(False) + is_binned = t.Bool(False) + index = t.Range("low_index", "high_index") axis = t.Array() - def __init__(self, - index_in_array=None, - name=t.Undefined, - units=t.Undefined, - navigate=False, - is_binned=False, - **kwargs): - super(BaseDataAxis, self).__init__() + def __init__( + self, + index_in_array=None, + name=None, + units=None, + navigate=False, + is_binned=False, + **kwargs, + ): + super().__init__() + if name is None: + name = t.Undefined + if units is None: + units = t.Undefined self.events = Events() - if '_type' in kwargs: - if kwargs.get('_type') != self.__class__.__name__: - raise ValueError('The passed `_type` of axis is inconsistent ' - 'with the given attributes') + if "_type" in kwargs: + _type = kwargs.get("_type") + if _type != self.__class__.__name__: + raise ValueError( + f"The passed `_type` ({_type}) of axis is " + "inconsistent with the given attributes." + ) _name = self.__class__.__name__ - self.events.index_changed = Event(""" + self.events.index_changed = Event( + """ Event that triggers when the index of the `{}` changes Triggers after the internal state of the `{}` has been updated. - Arguments: - --------- + Parameters + ---------- obj : The {} that the event belongs to. index : The new index - """.format(_name, _name, _name), arguments=["obj", 'index']) - self.events.value_changed = Event(""" + """.format(_name, _name, _name), + arguments=["obj", "index"], + ) + self.events.value_changed = Event( + """ Event that triggers when the value of the `{}` changes Triggers after the internal state of the `{}` has been updated. - Arguments: - --------- + Parameters + ---------- obj : The {} that the event belongs to. value : The new value - """.format(_name, _name, _name), arguments=["obj", 'value']) + """.format(_name, _name, _name), + arguments=["obj", "value"], + ) self._suppress_value_changed_trigger = False self._suppress_update_value = False self.name = name self.units = units self.low_index = 0 - self.on_trait_change(self._update_slice, 'navigate') - self.on_trait_change(self.update_index_bounds, 'size') - self.on_trait_change(self._update_bounds, 'axis') + self.on_trait_change(self._update_slice, "navigate") + self.on_trait_change(self.update_index_bounds, "size") + self.on_trait_change(self._update_bounds, "axis") self.index = 0 self.navigate = navigate @@ -353,7 +363,7 @@ def _value_changed(self, name, old, new): self.index = new_index if new == self.axis[self.index]: self.events.value_changed.trigger(obj=self, value=new) - elif old_index == new_index: + else: new_value = self.index2value(new_index) if new_value == old: self._suppress_value_changed_trigger = True @@ -362,8 +372,7 @@ def _value_changed(self, name, old, new): finally: self._suppress_value_changed_trigger = False - elif new_value == new and not\ - self._suppress_value_changed_trigger: + elif new_value == new and not self._suppress_value_changed_trigger: self.events.value_changed.trigger(obj=self, value=new) @property @@ -374,18 +383,19 @@ def index_in_array(self): raise AttributeError( "This {} does not belong to an AxesManager" " and therefore its index_in_array attribute " - " is not defined".format(self.__class__.__name__)) + " is not defined".format(self.__class__.__name__) + ) @property def index_in_axes_manager(self): if self.axes_manager is not None: - return self.axes_manager._get_axes_in_natural_order().\ - index(self) + return self.axes_manager._get_axes_in_natural_order().index(self) else: raise AttributeError( "This {} does not belong to an AxesManager" " and therefore its index_in_array attribute " - " is not defined".format(self.__class__.__name__)) + " is not defined".format(self.__class__.__name__) + ) def _get_positive_index(self, index): # To be used with re @@ -415,8 +425,7 @@ def _get_array_slices(self, slice_): """ if isinstance(slice_, slice): if not self.is_uniform and isfloat(slice_.step): - raise ValueError( - "Float steps are only supported for uniform axes.") + raise ValueError("Float steps are only supported for uniform axes.") v2i = self.value2index @@ -447,8 +456,9 @@ def _get_array_slices(self, slice_): # The start value is above the axis limit raise IndexError( "Start value above axis high bound for axis %s." - "value: %f high_bound: %f" % (repr(self), start, - self.high_value)) + "value: %f high_bound: %f" + % (repr(self), start, self.high_value) + ) else: # The start value is below the axis limit, # we slice from the start. @@ -461,8 +471,8 @@ def _get_array_slices(self, slice_): # The stop value is below the axis limits raise IndexError( "Stop value below axis low bound for axis %s." - "value: %f low_bound: %f" % (repr(self), stop, - self.low_value)) + "value: %f low_bound: %f" % (repr(self), stop, self.low_value) + ) else: # The stop value is below the axis limit, # we slice until the end. @@ -477,17 +487,20 @@ def _slice_me(self, slice_): raise NotImplementedError("This method must be implemented by subclasses") def _get_name(self): - name = (self.name - if self.name is not t.Undefined - else ("Unnamed " + - ordinal(self.index_in_axes_manager)) - if self.axes_manager is not None - else "Unnamed") + name = ( + self.name + if self.name is not t.Undefined + else ("Unnamed " + ordinal(self.index_in_axes_manager)) + if self.axes_manager is not None + else "Unnamed" + ) return name def __repr__(self): - text = '<%s axis, size: %i' % (self._get_name(), - self.size,) + text = "<%s axis, size: %i" % ( + self._get_name(), + self.size, + ) if self.navigate is True: text += ", index: %i" % self.index text += ">" @@ -501,8 +514,7 @@ def update_index_bounds(self): def _update_bounds(self): if len(self.axis) != 0: - self.low_value, self.high_value = ( - self.axis.min(), self.axis.max()) + self.low_value, self.high_value = (self.axis.min(), self.axis.max()) def _update_slice(self, value): if value is False: @@ -511,12 +523,13 @@ def _update_slice(self, value): self.slice = None def get_axis_dictionary(self): - return {'_type': self.__class__.__name__, - 'name': self.name, - 'units': self.units, - 'navigate': self.navigate, - 'is_binned': self.is_binned, - } + return { + "_type": self.__class__.__name__, + "name": _parse_axis_attribute(self.name), + "units": _parse_axis_attribute(self.units), + "navigate": self.navigate, + "is_binned": self.is_binned, + } def copy(self): return self.__class__(**self.get_axis_dictionary()) @@ -529,11 +542,11 @@ def __deepcopy__(self, memo): return cp def _parse_value_from_string(self, value): - """Return calibrated value from a suitable string """ + """Return calibrated value from a suitable string""" if len(value) == 0: raise ValueError("Cannot index with an empty string") # Starting with 'rel', it must be relative slicing - elif value.startswith('rel'): + elif value.startswith("rel"): try: relative_value = float(value[3:]) except ValueError: @@ -547,8 +560,9 @@ def _parse_value_from_string(self, value): if self.is_uniform: value = self._get_value_from_value_with_units(value) else: - raise ValueError("Unit conversion is only supported for " - "uniform axis.") + raise ValueError( + "Unit conversion is only supported for " "uniform axis." + ) else: raise ValueError(f"`{value}` is not a suitable string for slicing.") @@ -570,16 +584,17 @@ def value2index(self, value, rounding=round): Parameters ---------- - value : number or numpy array - rounding : function - Handling of values intermediate between two axis points: - If `rounding=round`, use round-half-away-from-zero strategy to find closest value. - If `rounding=math.floor`, round to the next lower value. - If `round=math.ceil`, round to the next higher value. + value : float or numpy.ndarray + rounding : callable + Handling of values between two axis points: + + - If ``rounding=round``, use round-half-away-from-zero strategy to find closest value. + - If ``rounding=math.floor``, round to the next lower value. + - If ``rounding=math.ceil``, round to the next higher value. Returns ------- - index : integer or numpy array + int or numpy array Raises ------ @@ -592,44 +607,44 @@ def value2index(self, value, rounding=round): else: value = np.asarray(value) - #Should evaluate on both arrays and scalars. Raises error if there are - #nan values in array - if np.all((value >= self.low_value)*(value <= self.high_value)): - #Only if all values will evaluate correctly do we implement rounding - #function. Rounding functions will strictly operate on numpy arrays - #and only evaluate self.axis - v input, v a scalar within value. + # Should evaluate on both arrays and scalars. Raises error if there are + # nan values in array + if np.all((value >= self.low_value) * (value <= self.high_value)): + # Only if all values will evaluate correctly do we implement rounding + # function. Rounding functions will strictly operate on numpy arrays + # and only evaluate self.axis - v input, v a scalar within value. if rounding is round: - #Use argmin(abs) which will return the closest value + # Use argmin(abs) which will return the closest value # rounding_index = lambda x: np.abs(x).argmin() - index = numba_closest_index_round(self.axis,value).astype(int) + index = numba_closest_index_round(self.axis, value).astype(int) elif rounding is math.ceil: - #Ceiling means finding index of the closest xi with xi - v >= 0 - #we look for argmin of strictly non-negative part of self.axis-v. - #The trick is to replace strictly negative values with +np.inf - index = numba_closest_index_ceil(self.axis,value).astype(int) + # Ceiling means finding index of the closest xi with xi - v >= 0 + # we look for argmin of strictly non-negative part of self.axis-v. + # The trick is to replace strictly negative values with +np.inf + index = numba_closest_index_ceil(self.axis, value).astype(int) elif rounding is math.floor: - #flooring means finding index of the closest xi with xi - v <= 0 - #we look for armgax of strictly non-positive part of self.axis-v. - #The trick is to replace strictly positive values with -np.inf - index = numba_closest_index_floor(self.axis,value).astype(int) + # flooring means finding index of the closest xi with xi - v <= 0 + # we look for armgax of strictly non-positive part of self.axis-v. + # The trick is to replace strictly positive values with -np.inf + index = numba_closest_index_floor(self.axis, value).astype(int) else: raise ValueError( - f'Non-supported rounding function. Use ' - f'round, math.ceil or math.floor' - ) - #initialise the index same dimension as input, force type to int + "Non-supported rounding function. Use " + "`round`, `math.ceil` or `math.floor`." + ) + # initialise the index same dimension as input, force type to int # index = np.empty_like(value,dtype=int) - #assign on flat, iterate on flat. + # assign on flat, iterate on flat. # for i,v in enumerate(value): - # index.flat[i] = rounding_index(self.axis - v) - #Squeezing to get a scalar out if scalar in. See squeeze doc + # index.flat[i] = rounding_index(self.axis - v) + # Squeezing to get a scalar out if scalar in. See squeeze doc return np.squeeze(index)[()] else: raise ValueError( - f'The value {value} is out of the limits ' - f'[{self.low_value:.3g}-{self.high_value:.3g}] of the ' + f"The value {value} is out of the limits " + f"[{self.low_value:.3g}-{self.high_value:.3g}] of the " f'"{self._get_name()}" axis.' - ) + ) def index2value(self, index): if isinstance(index, da.Array): @@ -662,8 +677,10 @@ def value_range_to_indices(self, v1, v2): error_message = "Wrong order of the values: for axis with" if self._is_increasing_order: if v1 is not None and v2 is not None and v1 > v2: - raise ValueError(f"{error_message} increasing order, v2 ({v2}) " - f"must be greater than v1 ({v1}).") + raise ValueError( + f"{error_message} increasing order, v2 ({v2}) " + f"must be greater than v1 ({v1})." + ) if v1 is not None and self.low_value < v1 <= self.high_value: i1 = self.value2index(v1) @@ -671,8 +688,10 @@ def value_range_to_indices(self, v1, v2): i2 = self.value2index(v2) else: if v1 is not None and v2 is not None and v1 < v2: - raise ValueError(f"{error_message} decreasing order: v1 ({v1}) " - f"must be greater than v2 ({v2}).") + raise ValueError( + f"{error_message} decreasing order: v1 ({v1}) " + f"must be greater than v2 ({v2})." + ) if v1 is not None and self.high_value > v1 >= self.low_value: i1 = self.value2index(v1) @@ -685,39 +704,118 @@ def update_from(self, axis, attributes): Parameters ---------- - axis : BaseDataAxis - The BaseDataAxis instance to use as a source for values. - attributes : iterable container of strings. + axis : :class:`~hyperspy.axes.BaseDataAxis` + The instance to use as a source for values. + attributes : iterable of str The name of the attribute to update. If the attribute does not exist in either of the AxesManagers, an AttributeError will be raised. Returns ------- - A boolean indicating whether any changes were made. + bool + True if any changes were made, otherwise False. """ any_changes = False changed = {} for f in attributes: - if getattr(self, f) != getattr(axis, f): + a, b = getattr(self, f), getattr(axis, f) + cond = np.allclose(a, b) if isinstance(a, np.ndarray) else a == b + if not cond: changed[f] = getattr(axis, f) if len(changed) > 0: self.trait_set(**changed) any_changes = True return any_changes - def convert_to_uniform_axis(self): - scale = (self.high_value - self.low_value) / self.size + def convert_to_uniform_axis(self, keep_bounds=True, log_scale_error=True): + """ + Convert to an uniform axis. + + Parameters + ---------- + keep_bounds : bool + If ``True``, the first and last value of the axis will not be changed. + The new scale is calculated by substracting the last value by the first + value and dividing by the number of intervals. + If ``False``, the scale and offset are calculated using + :meth:`numpy.polynomial.polynomial.Polynomial.fit`, which minimises + the scale difference over the whole axis range but the bounds of + the axis can change (in some cases quite significantly, in particular when the + interval width is changing continuously). Default is ``True``. + log_scale_error : bool + If ``True``, the maximum scale error will be logged as INFO. + Default is ``True``. + + Examples + -------- + Using ``keep_bounds=True`` (default): + + >>> s = hs.data.luminescence_signal(uniform=False) + >>> print(s.axes_manager) + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + Energy | 1024 | 0 | non-uniform axis | eV + >>> s.axes_manager[-1].convert_to_uniform_axis(keep_bounds=True) + >>> print(s.axes_manager) + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + Energy | 1024 | 0 | 1.6 | 0.0039 | eV + + Using ``keep_bounds=False``: + + >>> s = hs.data.luminescence_signal(uniform=False) + >>> print(s.axes_manager) + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + Energy | 1024 | 0 | non-uniform axis | eV + >>> s.axes_manager[-1].convert_to_uniform_axis(keep_bounds=False) + >>> print(s.axes_manager) + + Name | size | index | offset | scale | units + ================ | ====== | ====== | ======= | ======= | ====== + ---------------- | ------ | ------ | ------- | ------- | ------ + Energy | 1024 | 0 | 1.1 | 0.0033 | eV + + + See Also + -------- + hyperspy.api.signals.BaseSignal.interpolate_on_axis + + Notes + ----- + The function only converts the axis type and doesn't interpolate + the data itself - see :meth:`~.api.signals.BaseSignal.interpolate_on_axis` + to interpolate data on a uniform axis. + + """ + indices = np.arange(self.size) + if keep_bounds: + scale = (self.axis[-1] - self.axis[0]) / (self.size - 1) + offset = self.axis[0] + else: + # polyfit minimize the error over the whole axis + offset, scale = np.polynomial.Polynomial.fit( + indices, self.axis, deg=1 + ).convert() d = self.get_axis_dictionary() axes_manager = self.axes_manager - del d["axis"] - if len(self.axis) > 1: - scale_err = max(self.axis[1:] - self.axis[:-1]) - scale - _logger.warning('The maximum scale error is {}.'.format(scale_err)) - d["_type"] = 'UniformDataAxis' + if "axis" in d: + del d["axis"] + if len(self.axis) > 1 and log_scale_error: + scale_err = np.max(self.axis - (scale * indices + offset)) + _logger.info("The maximum scale error is {}.".format(scale_err)) + d["_type"] = "UniformDataAxis" + d["size"] = self.size self.__class__ = UniformDataAxis - self.__init__(**d, size=self.size, scale=scale, offset=self.low_value) + self.__init__(**d, scale=scale, offset=offset) self.axes_manager = axes_manager @property @@ -762,27 +860,31 @@ class DataAxis(BaseDataAxis): >>> s = hs.signals.Signal1D(np.ones(12), axes=[dict0]) >>> s.axes_manager[0].get_axis_dictionary() {'_type': 'DataAxis', - 'name': , - 'units': , - 'navigate': False, - 'axis': array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100])} + 'name': None, + 'units': None, + 'navigate': False, + 'is_binned': False, + 'axis': array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100])} """ - def __init__(self, - index_in_array=None, - name=t.Undefined, - units=t.Undefined, - navigate=False, - is_binned=False, - axis=[1], - **kwargs): + def __init__( + self, + index_in_array=None, + name=None, + units=None, + navigate=False, + is_binned=False, + axis=[1], + **kwargs, + ): super().__init__( index_in_array=index_in_array, name=name, units=units, navigate=navigate, is_binned=is_binned, - **kwargs) + **kwargs, + ) self.axis = axis self.update_axis() @@ -807,25 +909,22 @@ def _slice_me(self, slice_): def update_axis(self): """Set the value of an axis. The axis values need to be ordered. - Parameters - ---------- - axis : numpy array or list - Raises ------ - ValueError if the axis values are not ordered. + ValueError + If the axis values are not ordered. """ if len(self.axis) > 1: if isinstance(self.axis, list): self.axis = np.asarray(self.axis) if self._is_increasing_order is None: - raise ValueError('The non-uniform axis needs to be ordered.') + raise ValueError("The non-uniform axis needs to be ordered.") self.size = len(self.axis) def get_axis_dictionary(self): d = super().get_axis_dictionary() - d.update({'axis': self.axis}) + d.update({"axis": self.axis}) return d def calibrate(self, *args, **kwargs): @@ -836,19 +935,21 @@ def update_from(self, axis, attributes=None): Parameters ---------- - axis : DataAxis - The DataAxis instance to use as a source for values. - attributes : iterable container of strings. + axis : :class:`~hyperspy.axes.DataAxis` + The instance to use as a source for values. + attributes : iterable of str The name of the attribute to update. If the attribute does not exist in either of the AxesManagers, an AttributeError will be - raised. If `None`, `units` will be updated. + raised. If ``None``, ``units`` will be updated. + Returns ------- - A boolean indicating whether any changes were made. + bool + True if any changes were made, otherwise False. """ if attributes is None: - attributes = ["units"] + attributes = ["axis"] return super().update_from(axis, attributes) def crop(self, start=None, end=None): @@ -904,38 +1005,44 @@ class FunctionalDataAxis(BaseDataAxis): >>> s = hs.signals.Signal1D(np.ones(500), axes=[dict0]) >>> s.axes_manager[0].get_axis_dictionary() {'_type': 'FunctionalDataAxis', - 'name': , - 'units': , - 'navigate': False, - 'expression': 'a / (x + 1) + b', - 'size': 500, - 'x': {'_type': 'UniformDataAxis', - 'name': , - 'units': , - 'navigate': , + 'name': None, + 'units': None, + 'navigate': False, + 'is_binned': False, + 'expression': 'a / (x + 1) + b', + 'size': 500, + 'x': {'_type': 'UniformDataAxis', + 'name': None, + 'units': None, + 'navigate': False, + 'is_binned': False, 'size': 500, 'scale': 1.0, 'offset': 0.0}, - 'a': 100, - 'b': 10} + 'a': 100, + 'b': 10} """ - def __init__(self, - expression, - x=None, - index_in_array=None, - name=t.Undefined, - units=t.Undefined, - navigate=False, - size=t.Undefined, - is_binned=False, - **parameters): + + def __init__( + self, + expression, + x=None, + index_in_array=None, + name=None, + units=None, + navigate=False, + size=1, + is_binned=False, + **parameters, + ): super().__init__( index_in_array=index_in_array, name=name, units=units, navigate=navigate, is_binned=is_binned, - **parameters) + **parameters, + ) # These trait needs to added dynamically to be removed when necessary self.add_trait("x", t.Instance(BaseDataAxis)) if x is None: @@ -949,19 +1056,22 @@ def __init__(self, self.x = x self.size = self.x.size self._expression = expression - if '_type' in parameters: - del parameters['_type'] + if "_type" in parameters: + del parameters["_type"] # Compile function expr = _parse_substitutions(self._expression) variables = ["x"] - expr_parameters = [symbol for symbol in expr.free_symbols - if symbol.name not in variables] + expr_parameters = [ + symbol for symbol in expr.free_symbols if symbol.name not in variables + ] if set(parameters) != set([parameter.name for parameter in expr_parameters]): raise ValueError( "The values of the following expression parameters " - f"must be given as keywords: {set(expr_parameters) - set(parameters)}") + f"must be given as keywords: {set(expr_parameters) - set(parameters)}" + ) self._function = lambdify( - variables + expr_parameters, expr.evalf(), dummify=False) + variables + expr_parameters, expr.evalf(), dummify=False + ) for parameter in parameters.keys(): self.add_trait(parameter, t.CFloat(parameters[parameter])) self.parameters_list = list(parameters.keys()) @@ -982,9 +1092,9 @@ def update_from(self, axis, attributes=None): Parameters ---------- - axis : FunctionalDataAxis - The FunctionalDataAxis instance to use as a source for values. - attributes : iterable container of strings or None. + axis : :class:`~hyperspy.axes.FunctionalDataAxis` + The instance to use as a source for values. + attributes : iterable of str or None A list of the name of the attribute to update. If an attribute does not exist in either of the AxesManagers, an AttributeError will be raised. If None, the parameters of `expression` are updated. @@ -994,7 +1104,7 @@ def update_from(self, axis, attributes=None): """ if attributes is None: - attributes = self.parameters_list + attributes = self.parameters_list + ["_expression", "x"] return super().update_from(axis, attributes) def calibrate(self, *args, **kwargs): @@ -1002,22 +1112,31 @@ def calibrate(self, *args, **kwargs): def get_axis_dictionary(self): d = super().get_axis_dictionary() - d['expression'] = self._expression - d.update({'size': self.size, }) - d.update({'x': self.x.get_axis_dictionary(), }) + d["expression"] = self._expression + d.update( + { + "size": _parse_axis_attribute(self.size), + } + ) + d.update( + { + "x": self.x.get_axis_dictionary(), + } + ) for kwarg in self.parameters_list: d[kwarg] = getattr(self, kwarg) return d def convert_to_non_uniform_axis(self): + """Convert to a non-uniform axis.""" d = super().get_axis_dictionary() axes_manager = self.axes_manager - d["_type"] = 'DataAxis' + d["_type"] = "DataAxis" self.__class__ = DataAxis self.__init__(**d, axis=self.axis) del self._expression del self._function - self.remove_trait('x') + self.remove_trait("x") self.axes_manager = axes_manager def crop(self, start=None, end=None): @@ -1047,6 +1166,7 @@ def crop(self, start=None, end=None): self.x.crop(start=slice_.start, end=slice_.stop) self.size = self.x.size self.update_axis() + crop.__doc__ = DataAxis.crop.__doc__ def _slice_me(self, slice_): @@ -1094,7 +1214,7 @@ class UniformDataAxis(BaseDataAxis, UnitConversion): >>> dict0 = {'offset': 300, 'scale': 1, 'size': 500} >>> s = hs.signals.Signal1D(np.ones(500), axes=[dict0]) - >>> s.axes_manager[0].get_axis_dictionary() + >>> s.axes_manager[0].get_axis_dictionary() # doctest: +SKIP {'_type': 'UniformDataAxis', 'name': , 'units': , @@ -1103,24 +1223,27 @@ class UniformDataAxis(BaseDataAxis, UnitConversion): 'scale': 1.0, 'offset': 300.0} """ - def __init__(self, - index_in_array=None, - name=t.Undefined, - units=t.Undefined, - navigate=False, - size=1, - scale=1., - offset=0., - is_binned=False, - **kwargs): + + def __init__( + self, + index_in_array=None, + name=None, + units=None, + navigate=False, + size=1, + scale=1.0, + offset=0.0, + is_binned=False, + **kwargs, + ): super().__init__( index_in_array=index_in_array, name=name, units=units, navigate=navigate, is_binned=is_binned, - **kwargs - ) + **kwargs, + ) # These traits need to added dynamically to be removed when necessary self.add_trait("scale", t.CFloat) self.add_trait("offset", t.CFloat) @@ -1159,9 +1282,7 @@ def _slice_me(self, _slice): def get_axis_dictionary(self): d = super().get_axis_dictionary() - d.update({'size': self.size, - 'scale': self.scale, - 'offset': self.offset}) + d.update({"size": self.size, "scale": self.scale, "offset": self.offset}) return d def value2index(self, value, rounding=round): @@ -1169,18 +1290,18 @@ def value2index(self, value, rounding=round): Parameters ---------- - value : number or string, or numpy array of number or string - if string, should either be a calibrated unit like "20nm" - or a relative slicing like "rel0.2". - rounding : function - Handling of values intermediate between two axis points: - If `rounding=round`, use python's standard round-half-to-even strategy to find closest value. - If `rounding=math.floor`, round to the next lower value. - If `round=math.ceil`, round to the next higher value. + value : float, str, numpy.ndarray + If string, should either be a calibrated unit like "20nm" + or a relative slicing like "rel0.2". + rounding : callable + Handling of values intermediate between two axis points: + If ``rounding=round``, use python's standard round-half-to-even strategy to find closest value. + If ``rounding=math.floor``, round to the next lower value. + If ``rounding=math.ceil``, round to the next higher value. Returns ------- - index : integer or numpy array + int or numpy.ndarray Raises ------ @@ -1188,7 +1309,7 @@ def value2index(self, value, rounding=round): If value is out of bounds or contains out of bounds values (array). If value is NaN or contains NaN values (array). If value is incorrectly formatted str or contains incorrectly - formatted str (array). + formatted str (array). """ if value is None: @@ -1196,10 +1317,10 @@ def value2index(self, value, rounding=round): value = self._parse_value(value) - multiplier = 1E12 - index = 1 / multiplier * np.trunc( - (value - self.offset) / self.scale * multiplier - ) + multiplier = 1e12 + index = ( + 1 / multiplier * np.trunc((value - self.offset) / self.scale * multiplier) + ) if rounding is round: # When value are negative, we need to use half away from zero @@ -1208,7 +1329,7 @@ def value2index(self, value, rounding=round): value >= 0 if np.sign(self.scale) > 0 else value < 0, round_half_towards_zero(index, decimals=0), round_half_away_from_zero(index, decimals=0), - ) + ) else: if rounding is math.ceil: rounding = np.ceil @@ -1218,11 +1339,13 @@ def value2index(self, value, rounding=round): index = rounding(index) if isinstance(value, np.ndarray): + if np.isnan(value).any(): + raise ValueError("The value can't be 'Not a Number'.") index = index.astype(int) if np.all(self.size > index) and np.all(index >= 0): return index else: - raise ValueError("A value is out of the axis limits") + raise ValueError("One of the values is out of the axis limits") else: index = int(index) if self.size > index >= 0: @@ -1234,8 +1357,7 @@ def update_axis(self): self.axis = self.offset + self.scale * np.arange(self.size) def calibrate(self, value_tuple, index_tuple, modify_calibration=True): - scale = (value_tuple[1] - value_tuple[0]) /\ - (index_tuple[1] - index_tuple[0]) + scale = (value_tuple[1] - value_tuple[0]) / (index_tuple[1] - index_tuple[0]) offset = value_tuple[0] - scale * index_tuple[0] if modify_calibration is True: self.offset = offset @@ -1248,9 +1370,9 @@ def update_from(self, axis, attributes=None): Parameters ---------- - axis : UniformDataAxis + axis : :class:`~hyperspy.axes.UniformDataAxis` The UniformDataAxis instance to use as a source for values. - attributes : iterable container of strings or None + attributes : iterable of str or None The name of the attribute to update. If the attribute does not exist in either of the AxesManagers, an AttributeError will be raised. If `None`, `scale`, `offset` and `units` are updated. @@ -1260,7 +1382,7 @@ def update_from(self, axis, attributes=None): """ if attributes is None: - attributes = ["scale", "offset", "units"] + attributes = ["scale", "offset", "size"] return super().update_from(axis, attributes) def crop(self, start=None, end=None): @@ -1294,21 +1416,23 @@ def crop(self, start=None, end=None): @property def scale_as_quantity(self): - return self._get_quantity('scale') + return self._get_quantity("scale") @scale_as_quantity.setter def scale_as_quantity(self, value): - self._set_quantity(value, 'scale') + self._set_quantity(value, "scale") @property def offset_as_quantity(self): - return self._get_quantity('offset') + return self._get_quantity("offset") @offset_as_quantity.setter def offset_as_quantity(self, value): - self._set_quantity(value, 'offset') + self._set_quantity(value, "offset") - def convert_to_functional_data_axis(self, expression, units=None, name=None, **kwargs): + def convert_to_functional_data_axis( + self, expression, units=None, name=None, **kwargs + ): d = super().get_axis_dictionary() axes_manager = self.axes_manager if units: @@ -1317,10 +1441,10 @@ def convert_to_functional_data_axis(self, expression, units=None, name=None, **k d["name"] = name d.update(kwargs) this_kwargs = self.get_axis_dictionary() - self.remove_trait('scale') - self.remove_trait('offset') + self.remove_trait("scale") + self.remove_trait("offset") self.__class__ = FunctionalDataAxis - d["_type"] = 'FunctionalDataAxis' + d["_type"] = "FunctionalDataAxis" self.__init__(expression=expression, x=UniformDataAxis(**this_kwargs), **d) self.axes_manager = axes_manager @@ -1328,15 +1452,15 @@ def convert_to_non_uniform_axis(self): d = super().get_axis_dictionary() axes_manager = self.axes_manager self.__class__ = DataAxis - d["_type"] = 'DataAxis' - self.remove_trait('scale') - self.remove_trait('offset') + d["_type"] = "DataAxis" + self.remove_trait("scale") + self.remove_trait("offset") self.__init__(**d, axis=self.axis) self.axes_manager = axes_manager def _serpentine_iter(shape): - '''Similar to np.ndindex, but yields indices + """Similar to np.ndindex, but yields indices in serpentine pattern, like snake game. Takes shape in hyperspy order, not numpy order. @@ -1345,11 +1469,11 @@ def _serpentine_iter(shape): Note that the [::-1] reversing is necessary to iterate first along the x-direction on multidimensional navigators. - ''' + """ shape = shape[::-1] N = len(shape) - idx = N*[0] - drc = N*[1] + idx = N * [0] + drc = N * [1] while True: yield (*idx,)[::-1] for j in reversed(range(N)): @@ -1360,9 +1484,11 @@ def _serpentine_iter(shape): else: # pragma: no cover break + def _flyback_iter(shape): "Classic flyback scan pattern generator which yields indices in similar fashion to np.ndindex. Takes shape in hyperspy order, not numpy order." shape = shape[::-1] + class ndindex_reversed(np.ndindex): def __next__(self): next(self._it) @@ -1373,7 +1499,6 @@ def __next__(self): @add_gui_method(toolkey="hyperspy.AxesManager") class AxesManager(t.HasTraits): - """Contains and manages the data axes. It supports indexing, slicing, subscripting and iteration. As an iterator, @@ -1409,7 +1534,7 @@ class AxesManager(t.HasTraits): | 3 | 0 | 0 | 1 | | 2 | 0 | 0 | 1 | ---------------- | ------ | ------ | ------- | ------- | ------ - | 5 | | 0 | 1 | + | 5 | 0 | 0 | 1 | >>> s.axes_manager[0] >>> s.axes_manager[3j] @@ -1423,81 +1548,89 @@ class AxesManager(t.HasTraits): >>> for i in s.axes_manager: ... print(i, s.axes_manager.indices) - ... (0, 0, 0) (0, 0, 0) (1, 0, 0) (1, 0, 0) (2, 0, 0) (2, 0, 0) (3, 0, 0) (3, 0, 0) - (0, 1, 0) (0, 1, 0) - (1, 1, 0) (1, 1, 0) - (2, 1, 0) (2, 1, 0) (3, 1, 0) (3, 1, 0) + (2, 1, 0) (2, 1, 0) + (1, 1, 0) (1, 1, 0) + (0, 1, 0) (0, 1, 0) (0, 2, 0) (0, 2, 0) (1, 2, 0) (1, 2, 0) (2, 2, 0) (2, 2, 0) (3, 2, 0) (3, 2, 0) - (0, 0, 1) (0, 0, 1) - (1, 0, 1) (1, 0, 1) - (2, 0, 1) (2, 0, 1) - (3, 0, 1) (3, 0, 1) + (3, 2, 1) (3, 2, 1) + (2, 2, 1) (2, 2, 1) + (1, 2, 1) (1, 2, 1) + (0, 2, 1) (0, 2, 1) (0, 1, 1) (0, 1, 1) (1, 1, 1) (1, 1, 1) (2, 1, 1) (2, 1, 1) (3, 1, 1) (3, 1, 1) - (0, 2, 1) (0, 2, 1) - (1, 2, 1) (1, 2, 1) - (2, 2, 1) (2, 2, 1) - (3, 2, 1) (3, 2, 1) + (3, 0, 1) (3, 0, 1) + (2, 0, 1) (2, 0, 1) + (1, 0, 1) (1, 0, 1) + (0, 0, 1) (0, 0, 1) """ _axes = t.List(BaseDataAxis) - signal_axes = t.Tuple() - navigation_axes = t.Tuple() _step = t.Int(1) def __init__(self, axes_list): - super(AxesManager, self).__init__() + super().__init__() self.events = Events() - self.events.indices_changed = Event(""" + self.events.indices_changed = Event( + """ Event that triggers when the indices of the `AxesManager` changes Triggers after the internal state of the `AxesManager` has been updated. - Arguments: + Parameters ---------- obj : The AxesManager that the event belongs to. - """, arguments=['obj']) - self.events.any_axis_changed = Event(""" + """, + arguments=["obj"], + ) + self.events.any_axis_changed = Event( + """ Event that trigger when the space defined by the axes transforms. Specifically, it triggers when one or more of the following attributes changes on one or more of the axes: `offset`, `size`, `scale` - Arguments: + Parameters ---------- obj : The AxesManager that the event belongs to. - """, arguments=['obj']) + """, + arguments=["obj"], + ) + + # Remove all axis for cases, we reinitiliase the AxesManager + if self._axes: + self.remove(self._axes) self.create_axes(axes_list) - # set_signal_dimension is called only if there is no current - # view. It defaults to spectrum - navigates = [i.navigate for i in self._axes] - if t.Undefined in navigates: - # Default to Signal1D view if the view is not fully defined - self.set_signal_dimension(len(axes_list)) self._update_attributes() self._update_trait_handlers() - self.iterpath = 'flyback' + self.iterpath = "serpentine" + self._ragged = False + + @property + def ragged(self): + return self._ragged def _update_trait_handlers(self, remove=False): - things = {self._on_index_changed: '_axes.index', - self._on_slice_changed: '_axes.slice', - self._on_size_changed: '_axes.size', - self._on_scale_changed: '_axes.scale', - self._on_offset_changed: '_axes.offset'} + things = { + self._on_index_changed: "_axes.index", + self._on_slice_changed: "_axes.slice", + self._on_size_changed: "_axes.size", + self._on_scale_changed: "_axes.scale", + self._on_offset_changed: "_axes.offset", + } for k, v in things.items(): self.on_trait_change(k, name=v, remove=remove) @@ -1510,30 +1643,46 @@ def _get_positive_index(self, axis): return axis def _array_indices_generator(self): - shape = (self.navigation_shape[::-1] if self.navigation_size > 0 else - [1, ]) + shape = ( + self.navigation_shape[::-1] + if self.navigation_size > 0 + else [ + 1, + ] + ) return np.ndindex(*shape) def _am_indices_generator(self): - shape = (self.navigation_shape if self.navigation_size > 0 else - [1, ])[::-1] + shape = ( + self.navigation_shape + if self.navigation_size > 0 + else [ + 1, + ] + )[::-1] return ndindex_nat(*shape) def __getitem__(self, y): - """x.__getitem__(y) <==> x[y] - - """ + """x.__getitem__(y) <==> x[y]""" if isinstance(y, str) or not np.iterable(y): - return self[(y,)][0] - axes = [self._axes_getter(ax) for ax in y] - _, indices = np.unique( - [_id for _id in map(id, axes)], return_index=True) - ans = tuple(axes[i] for i in sorted(indices)) + if y == "nav": + axes = self.navigation_axes + elif y == "sig": + axes = self.signal_axes + else: + return self[(y,)][0] + else: + axes = [self._axes_getter(ax) for ax in y] + _, indices = np.unique([_id for _id in map(id, axes)], return_index=True) + ans = TupleSA(axes[i] for i in sorted(indices)) return ans def _axes_getter(self, y): - if y in self._axes: - return y + if isinstance(y, BaseDataAxis): + if y in self._axes: + return y + else: + raise ValueError(f"{y} is not in {self}") if isinstance(y, str): axes = list(self._get_axes_in_natural_order()) while axes: @@ -1541,10 +1690,15 @@ def _axes_getter(self, y): if y == axis.name: return axis raise ValueError("There is no DataAxis named %s" % y) - elif (isfloat(y.real) and not y.real.is_integer() or - isfloat(y.imag) and not y.imag.is_integer()): - raise TypeError("axesmanager indices must be integers, " - "complex integers or strings") + elif ( + isfloat(y.real) + and not y.real.is_integer() + or isfloat(y.imag) + and not y.imag.is_integer() + ): + raise TypeError( + "axesmanager indices must be integers, " "complex integers or strings" + ) if y.imag == 0: # Natural order return self._get_axes_in_natural_order()[y] elif y.imag == 3: # Array order @@ -1556,13 +1710,12 @@ def _axes_getter(self, y): elif y.imag == 2: # Signal natural order return self.signal_axes[int(y.real)] else: - raise IndexError("axesmanager imaginary part of complex indices " - "must be 0, 1, 2 or 3") + raise IndexError( + "axesmanager imaginary part of complex indices " "must be 0, 1, 2 or 3" + ) def __getslice__(self, i=None, j=None): - """x.__getslice__(i, j) <==> x[i:j] - - """ + """x.__getslice__(i, j) <==> x[i:j]""" return self._get_axes_in_natural_order()[i:j] def _get_axes_in_natural_order(self): @@ -1578,16 +1731,13 @@ def _signal_shape_in_array(self): @property def shape(self): - nav_shape = (self.navigation_shape - if self.navigation_shape != (0,) - else tuple()) - sig_shape = (self.signal_shape - if self.signal_shape != (0,) - else tuple()) + nav_shape = self.navigation_shape if self.navigation_shape != (0,) else tuple() + sig_shape = self.signal_shape if self.signal_shape != (0,) else tuple() return nav_shape + sig_shape @property def signal_extent(self): + """The low and high values of the signal axes.""" signal_extent = [] for signal_axis in self.signal_axes: signal_extent.append(signal_axis.low_value) @@ -1596,6 +1746,7 @@ def signal_extent(self): @property def navigation_extent(self): + """The low and high values of the navigation axes.""" navigation_extent = [] for navigation_axis in self.navigation_axes: navigation_extent.append(navigation_axis.low_value) @@ -1604,14 +1755,13 @@ def navigation_extent(self): @property def all_uniform(self): - if any([axis.is_uniform == False for axis in self._axes]): + if any([axis.is_uniform is False for axis in self._axes]): return False else: return True def remove(self, axes): - """Remove one or more axes - """ + """Remove one or more axes""" axes = self[axes] if not np.iterable(axes): axes = (axes,) @@ -1644,52 +1794,61 @@ def _get_data_slice(self, fill=None): slice. """ - cslice = [slice(None), ] * len(self._axes) + cslice = [ + slice(None), + ] * len(self._axes) if fill is not None: for index, slice_ in fill: cslice[index] = slice_ return tuple(cslice) def create_axes(self, axes_list): - """Given a list of either axes dictionaries or axes objects, these are + """Given a list of either axes dictionaries, these are added to the AxesManager. In case dictionaries defining the axes - properties are passed, the DataAxis/UniformDataAxis/FunctionalDataAxis - instances are first created. + properties are passed, the + :class:`~hyperspy.axes.DataAxis`, + :class:`~hyperspy.axes.UniformDataAxis`, + :class:`~hyperspy.axes.FunctionalDataAxis` instances are first + created. The index of the axis in the array and in the `_axes` lists can be defined by the index_in_array keyword if given for all axes. Otherwise, it is defined by their index in the list. - See also - -------- - _append_axis + Parameters + ---------- + axes_list : list of dict + The list of axes to create. """ # Reorder axes_list using index_in_array if it is defined # for all axes and the indices are not repeated. - indices = set([axis['index_in_array'] for axis in axes_list if - hasattr(axis, 'index_in_array')]) + indices = set( + [ + axis["index_in_array"] + for axis in axes_list + if hasattr(axis, "index_in_array") + ] + ) if len(indices) == len(axes_list): - axes_list.sort(key=lambda x: x['index_in_array']) + axes_list.sort(key=lambda x: x["index_in_array"]) for axis_dict in axes_list: - if isinstance(axis_dict,dict): + if isinstance(axis_dict, dict): self._append_axis(**axis_dict) else: self._axes.append(axis_dict) def set_axis(self, axis, index_in_axes_manager): """Replace an axis of current signal with one given in argument. + Parameters ---------- - axis: BaseDataAxis axis to replace the current axis with - - index_in_axes_manager: index of the axis in current signal to remplace - with axis passed in argument - - See also - -------- - _append_axis + axis : :class:`~hyperspy.axes.BaseDataAxis` + The axis to replace the current axis with. + index_in_axes_manager : int + The index of the axis in current signal to replace + with the axis passed in argument. """ self._axes[index_in_axes_manager] = axis @@ -1712,31 +1871,31 @@ def iterpath(self): @iterpath.setter def iterpath(self, path): if isinstance(path, str): - if path == 'serpentine': - self._iterpath = 'serpentine' + if path == "serpentine": + self._iterpath = "serpentine" self._iterpath_generator = _serpentine_iter(self.navigation_shape) - elif path == 'flyback': - self._iterpath = 'flyback' + elif path == "flyback": + self._iterpath = "flyback" self._iterpath_generator = _flyback_iter(self.navigation_shape) else: raise ValueError( f'The iterpath scan pattern is set to `"{path}"`. ' 'It must be either "serpentine" or "flyback", or an iterable ' - 'of navigation indices, and is set either as multifit ' - '`iterpath` argument or `axes_manager.iterpath`' - ) + "of navigation indices, and is set either as multifit " + "`iterpath` argument or `axes_manager.iterpath`" + ) else: # Passing a custom indices iterator try: - iter(path) # If this fails, its not an iterable and we raise TypeError + iter(path) # If this fails, its not an iterable and we raise TypeError except TypeError as e: raise TypeError( - f'The iterpath `{path}` is not an iterable. ' - 'Ensure it is an iterable like a list, array or generator.' - ) from e + f"The iterpath `{path}` is not an iterable. " + "Ensure it is an iterable like a list, array or generator." + ) from e try: if not (inspect.isgenerator(path) or type(path) is GeneratorLen): - # If iterpath is a generator, then we can't check its first value, have to trust it + # If iterpath is a generator, then we can't check its first value, have to trust it first_indices = path[0] if not isinstance(first_indices, Iterable): raise TypeError @@ -1745,13 +1904,13 @@ def iterpath(self, path): raise TypeError( f"Each set of indices in the iterpath should be an iterable, e.g. `(0,)` or `(0,0,0)`. " f"The first entry currently looks like: `{first_indices}`, and does not satisfy this requirement." - ) from e + ) from e except AssertionError as e: raise ValueError( f"The current iterpath yields indices of length " f"{len(path)}. It should deliver incides with length " f"equal to the navigation dimension, which is {self.navigation_dimension}." - ) from e + ) from e else: self._iterpath = path self._iterpath_generator = iter(self._iterpath) @@ -1768,17 +1927,21 @@ def _get_iterpath_size(self, masked_elements=0): # Checking if mask indices exist in the iterpath could take a long time, # or may not be possible in the case of a generator. _logger.info( - ("The progressbar length cannot be estimated when using both custom iterpath and a mask." - "The progressbar may terminate before it appears complete. This can safely be ignored."), + ( + "The progressbar length cannot be estimated when using both custom iterpath and a mask." + "The progressbar may terminate before it appears complete. This can safely be ignored." + ), ) except TypeError: # progressbar is shown, so user can monitor "iterations per second" # but the length of the bar is unknown maxval = None _logger.info( - ("The AxesManager `iterpath` is missing the `__len__` method, so does not have a known length. " - "The progressbar will only show run time and iterations per second, but no actual progress indicator."), - ) + ( + "The AxesManager `iterpath` is missing the `__len__` method, so does not have a known length. " + "The progressbar will only show run time and iterations per second, but no actual progress indicator." + ), + ) return maxval def __next__(self): @@ -1820,9 +1983,14 @@ def switch_iterpath(self, iterpath=None): -------- >>> s = hs.signals.Signal1D(np.arange(2*3*4).reshape([3, 2, 4])) >>> with s.axes_manager.switch_iterpath('serpentine'): - >>> for indices in s.axes_manager: - >>> print(indices) - + ... for indices in s.axes_manager: + ... print(indices) + (0, 0) + (1, 0) + (1, 1) + (0, 1) + (0, 2) + (1, 2) """ if iterpath is not None: original_iterpath = self._iterpath @@ -1857,92 +2025,93 @@ def _on_scale_changed(self): def _on_offset_changed(self): self.events.any_axis_changed.trigger(obj=self) - def convert_units(self, axes=None, units=None, same_units=True, - factor=0.25): - """ Convert the scale and the units of the selected axes. If the unit + def convert_units(self, axes=None, units=None, same_units=True, factor=0.25): + """Convert the scale and the units of the selected axes. If the unit of measure is not supported by the pint library, the scale and units are not changed. Parameters ---------- - axes : {int | string | iterable of `DataAxis` | None} - Default = None + axes : int, str, iterable of :class:`~hyperspy.axes.DataAxis` or None, default None Convert to a convenient scale and units on the specified axis. If int, the axis can be specified using the index of the - axis in `axes_manager`. - If string, argument can be `navigation` or `signal` to select the - navigation or signal axes. The axis name can also be provided. - If `None`, convert all axes. - units : {list of string of the same length than axes | str | None} - Default = None + axis in ``axes_manager``. + If string, argument can be ``"navigation"`` or ``"signal"`` to + select the navigation or signal axes. The axis name can also be + provided. If ``None``, convert all axes. + units : list of str, str or None, default None If list, the selected axes will be converted to the provided units. - If str, the navigation or signal axes will be converted to the + If string, the navigation or signal axes will be converted to the provided units. - If `None`, the scale and the units are converted to the appropriate + If ``None``, the scale and the units are converted to the appropriate scale and units to avoid displaying scalebar with >3 digits or too - small number. This can be tweaked by the `factor` argument. + small number. This can be tweaked by the ``factor`` argument. same_units : bool - If `True`, force to keep the same units if the units of + If ``True``, force to keep the same units if the units of the axes differs. It only applies for the same kind of axis, - `navigation` or `signal`. By default the converted units of the - first axis is used for all axes. If `False`, convert all axes - individually. + ``"navigation"`` or ``"signal"``. By default the converted unit + of the first axis is used for all axes. If ``False``, convert all + axes individually. %s - Note - ---- + Notes + ----- Requires a uniform axis. """ convert_navigation = convert_signal = True if axes is None: axes = self.navigation_axes + self.signal_axes - convert_navigation = (len(self.navigation_axes) > 0) - elif axes == 'navigation': + convert_navigation = len(self.navigation_axes) > 0 + elif axes == "navigation": axes = self.navigation_axes convert_signal = False - convert_navigation = (len(self.navigation_axes) > 0) - elif axes == 'signal': + convert_navigation = len(self.navigation_axes) > 0 + elif axes == "signal": axes = self.signal_axes convert_navigation = False elif isinstance(axes, (UniformDataAxis, int, str)): if not isinstance(axes, UniformDataAxis): axes = self[axes] - axes = (axes, ) + axes = (axes,) convert_navigation = axes[0].navigate convert_signal = not convert_navigation else: - raise TypeError( - 'Axes type `{}` is not correct.'.format(type(axes))) + raise TypeError("Axes type `{}` is not correct.".format(type(axes))) for axis in axes: if not axis.is_uniform: raise NotImplementedError( "This operation is not implemented for non-uniform axes " - f"such as {axis}") + f"such as {axis}" + ) if isinstance(units, str) or units is None: units = [units] * len(axes) elif isinstance(units, list): if len(units) != len(axes): - raise ValueError('Length of the provided units list {} should ' - 'be the same than the length of the provided ' - 'axes {}.'.format(units, axes)) + raise ValueError( + "Length of the provided units list {} should " + "be the same than the length of the provided " + "axes {}.".format(units, axes) + ) else: - raise TypeError('Units type `{}` is not correct. It can be a ' - '`string`, a `list` of string or `None`.' - ''.format(type(units))) + raise TypeError( + "Units type `{}` is not correct. It can be a " + "`string`, a `list` of string or `None`." + "".format(type(units)) + ) if same_units: if convert_navigation: - units_nav = units[:self.navigation_dimension] - self._convert_axes_to_same_units(self.navigation_axes, - units_nav, factor) + units_nav = units[: self.navigation_dimension] + self._convert_axes_to_same_units( + self.navigation_axes, units_nav, factor + ) if convert_signal: offset = self.navigation_dimension if convert_navigation else 0 units_sig = units[offset:] - self._convert_axes_to_same_units(self.signal_axes, - units_sig, factor) + self._convert_axes_to_same_units(self.signal_axes, units_sig, factor) else: for axis, unit in zip(axes, units): axis.convert_to_units(unit, factor=factor) @@ -1964,8 +2133,7 @@ def _convert_axes_to_same_units(self, axes, units, factor=0.25): if _ureg(axis.units).dimensionality == _ureg(unit).dimensionality: axis.convert_to_units(unit, factor=factor) - def update_axes_attributes_from(self, axes, - attributes=None): + def update_axes_attributes_from(self, axes, attributes=None): """Update the axes attributes to match those given. The axes are matched by their index in the array. The purpose of this @@ -1974,7 +2142,7 @@ def update_axes_attributes_from(self, axes, Parameters ---------- - axes: iterable of `DataAxis` instances. + axes: iterable of :class:`~hyperspy.axes.DataAxis`. The axes to copy the attributes from. attributes: iterable of strings. The attributes to copy. @@ -1987,7 +2155,8 @@ def update_axes_attributes_from(self, axes, with self.events.any_axis_changed.suppress(): for axis in axes: changed = self._axes[axis.index_in_array].update_from( - axis=axis, attributes=attributes) + axis=axis, attributes=attributes + ) changes = changes or changed if changes: self.events.any_axis_changed.trigger(obj=self) @@ -1995,106 +2164,147 @@ def update_axes_attributes_from(self, axes, def _update_attributes(self): getitem_tuple = [] values = [] - self.signal_axes = () - self.navigation_axes = () + signal_axes = () + navigation_axes = () for axis in self._axes: # Until we find a better place, take property of the axes # here to avoid difficult to debug bugs. axis.axes_manager = self if axis.slice is None: - getitem_tuple += axis.index, + getitem_tuple += (axis.index,) values.append(axis.value) - self.navigation_axes += axis, + navigation_axes += (axis,) else: - getitem_tuple += axis.slice, - self.signal_axes += axis, - if not self.signal_axes and self.navigation_axes: + getitem_tuple += (axis.slice,) + signal_axes += (axis,) + if not signal_axes and navigation_axes: getitem_tuple[-1] = slice(axis.index, axis.index + 1) - self.signal_axes = self.signal_axes[::-1] - self.navigation_axes = self.navigation_axes[::-1] + self._signal_axes = signal_axes[::-1] + self._navigation_axes = navigation_axes[::-1] self._getitem_tuple = tuple(getitem_tuple) - self.signal_dimension = len(self.signal_axes) - self.navigation_dimension = len(self.navigation_axes) - if self.navigation_dimension != 0: - self.navigation_shape = tuple([ - axis.size for axis in self.navigation_axes]) - else: - self.navigation_shape = () - if self.signal_dimension != 0: - self.signal_shape = tuple([ - axis.size for axis in self.signal_axes]) + if len(self.signal_axes) == 1 and self.signal_axes[0].size == 1: + self._signal_dimension = 0 else: - self.signal_shape = () - self.navigation_size = (np.cumprod(self.navigation_shape)[-1] - if self.navigation_shape else 0) - self.signal_size = (np.cumprod(self.signal_shape)[-1] - if self.signal_shape else 0) + self._signal_dimension = len(self.signal_axes) + self._navigation_dimension = len(self.navigation_axes) + + self._signal_size = np.prod(self.signal_shape) if self.signal_shape else 0 + self._navigation_size = ( + np.prod(self.navigation_shape) if self.navigation_shape else 0 + ) + self._update_max_index() - def set_signal_dimension(self, value): - """Set the dimension of the signal. + @property + def signal_axes(self): + """The signal axes as a TupleSA. - Attributes - ---------- - value : int + A TupleSA object is a tuple with a `set` method + to easily set the attributes of its items. + """ + return TupleSA(self._signal_axes) - Raises - ------ - ValueError - If value if greater than the number of axes or is negative. + @property + def navigation_axes(self): + """The navigation axes as a TupleSA. + A TupleSA object is a tuple with a `set` method + to easily set the attributes of its items. """ - if len(self._axes) == 0: + return TupleSA(self._navigation_axes) + + @property + def signal_shape(self): + """The shape of the signal space.""" + return tuple([axis.size for axis in self._signal_axes]) + + @property + def navigation_shape(self): + """The shape of the navigation space.""" + if self.navigation_dimension != 0: + return tuple([axis.size for axis in self._navigation_axes]) + else: + return () + + @property + def signal_size(self): + """The size of the signal space.""" + return self._signal_size + + @property + def navigation_size(self): + """The size of the navigation space.""" + return self._navigation_size + + @property + def navigation_dimension(self): + """The dimension of the navigation space.""" + return self._navigation_dimension + + @property + def signal_dimension(self): + """The dimension of the signal space.""" + return self._signal_dimension + + def _set_signal_dimension(self, value): + if len(self._axes) == 0 or self._signal_dimension == value: + # Nothing to be done return + elif self.ragged and value > 0: + raise ValueError( + "Signal containing ragged array " "must have zero signal dimension." + ) elif value > len(self._axes): raise ValueError( - "The signal dimension cannot be greater" - " than the number of axes which is %i" % len(self._axes)) + "The signal dimension cannot be greater " + f"than the number of axes which is {len(self._axes)}" + ) elif value < 0: - raise ValueError( - "The signal dimension must be a positive integer") + raise ValueError("The signal dimension must be a positive integer") + # Figure out which axis needs navigate=True tl = [True] * len(self._axes) if value != 0: tl[-value:] = (False,) * value - for axis in self._axes: + # Changing navigate attribute will update the axis._slice + # which in turn will trigger _on_slice_changed and call + # _update_attribute axis.navigate = tl.pop(0) def key_navigator(self, event): - 'Set hotkeys for controlling the indices of the navigator plot' + """Set hotkeys for controlling the indices of the navigator plot""" if self.navigation_dimension == 0: # No hotkeys exist that do anything in this case return # keyDict values are (axis_index, direction) - # Using arrow keys without Ctrl will be deprecated in 2.0 mod01 = preferences.Plot.modifier_dims_01 mod23 = preferences.Plot.modifier_dims_23 mod45 = preferences.Plot.modifier_dims_45 - dim0_decrease = mod01 + '+' + preferences.Plot.dims_024_decrease - dim0_increase = mod01 + '+' + preferences.Plot.dims_024_increase - dim1_decrease = mod01 + '+' + preferences.Plot.dims_135_decrease - dim1_increase = mod01 + '+' + preferences.Plot.dims_135_increase - dim2_decrease = mod23 + '+' + preferences.Plot.dims_024_decrease - dim2_increase = mod23 + '+' + preferences.Plot.dims_024_increase - dim3_decrease = mod23 + '+' + preferences.Plot.dims_135_decrease - dim3_increase = mod23 + '+' + preferences.Plot.dims_135_increase - dim4_decrease = mod45 + '+' + preferences.Plot.dims_024_decrease - dim4_increase = mod45 + '+' + preferences.Plot.dims_024_increase - dim5_decrease = mod45 + '+' + preferences.Plot.dims_135_decrease - dim5_increase = mod45 + '+' + preferences.Plot.dims_135_increase + dim0_decrease = mod01 + "+" + preferences.Plot.dims_024_decrease + dim0_increase = mod01 + "+" + preferences.Plot.dims_024_increase + dim1_decrease = mod01 + "+" + preferences.Plot.dims_135_decrease + dim1_increase = mod01 + "+" + preferences.Plot.dims_135_increase + dim2_decrease = mod23 + "+" + preferences.Plot.dims_024_decrease + dim2_increase = mod23 + "+" + preferences.Plot.dims_024_increase + dim3_decrease = mod23 + "+" + preferences.Plot.dims_135_decrease + dim3_increase = mod23 + "+" + preferences.Plot.dims_135_increase + dim4_decrease = mod45 + "+" + preferences.Plot.dims_024_decrease + dim4_increase = mod45 + "+" + preferences.Plot.dims_024_increase + dim5_decrease = mod45 + "+" + preferences.Plot.dims_135_decrease + dim5_increase = mod45 + "+" + preferences.Plot.dims_135_increase keyDict = { # axes 0, 1 - **dict.fromkeys(['left', dim0_decrease, '4'], (0, -1)), - **dict.fromkeys(['right', dim0_increase, '6'], (0, +1)), - **dict.fromkeys(['up', dim1_decrease, '8'], (1, -1)), - **dict.fromkeys(['down', dim1_increase, '2'], (1, +1)), + **dict.fromkeys([dim0_decrease, "4"], (0, -1)), + **dict.fromkeys([dim0_increase, "6"], (0, +1)), + **dict.fromkeys([dim1_decrease, "8"], (1, -1)), + **dict.fromkeys([dim1_increase, "2"], (1, +1)), # axes 2, 3 **dict.fromkeys([dim2_decrease], (2, -1)), **dict.fromkeys([dim2_increase], (2, +1)), @@ -2107,9 +2317,9 @@ def key_navigator(self, event): **dict.fromkeys([dim5_increase], (5, +1)), } - if event.key == 'pageup': + if event.key == "pageup": self._step += 1 - elif event.key == 'pagedown': + elif event.key == "pagedown": if self._step > 1: self._step -= 1 else: @@ -2141,24 +2351,14 @@ def _get_axes_dicts(self, axes=None): def as_dictionary(self): am_dict = {} for i, axis in enumerate(self._axes): - am_dict['axis-%i' % i] = axis.get_axis_dictionary() + am_dict["axis-%i" % i] = axis.get_axis_dictionary() return am_dict def _get_signal_axes_dicts(self): - return [axis.get_axis_dictionary() for axis in - self.signal_axes[::-1]] + return [axis.get_axis_dictionary() for axis in self.signal_axes[::-1]] def _get_navigation_axes_dicts(self): - return [axis.get_axis_dictionary() for axis in - self.navigation_axes[::-1]] - - def show(self): - from hyperspy.exceptions import VisibleDeprecationWarning - msg = ( - "The `AxesManager.show` method is deprecated and will be removed " - "in v2.0. Use `gui` instead.") - warnings.warn(msg, VisibleDeprecationWarning) - self.gui() + return [axis.get_axis_dictionary() for axis in self.navigation_axes[::-1]] def _get_dimension_str(self): string = "(" @@ -2169,84 +2369,111 @@ def _get_dimension_str(self): for axis in self.signal_axes: string += str(axis.size) + ", " string = string.rstrip(", ") + if self.ragged: + string += "ragged" string += ")" return string def __repr__(self): - text = ('\n' % - self._get_dimension_str()) + text = "\n" % self._get_dimension_str() ax_signature_uniform = "% 16s | %6g | %6s | %7.2g | %7.2g | %6s " ax_signature_non_uniform = "% 16s | %6g | %6s | non-uniform axis | %6s " signature = "% 16s | %6s | %6s | %7s | %7s | %6s " - text += signature % ('Name', 'size', 'index', 'offset', 'scale', - 'units') - text += '\n' - text += signature % ('=' * 16, '=' * 6, '=' * 6, - '=' * 7, '=' * 7, '=' * 6) + text += signature % ("Name", "size", "index", "offset", "scale", "units") + text += "\n" + text += signature % ("=" * 16, "=" * 6, "=" * 6, "=" * 7, "=" * 7, "=" * 6) def axis_repr(ax, ax_signature_uniform, ax_signature_non_uniform): if ax.is_uniform: - return ax_signature_uniform % (str(ax.name)[:16], ax.size, - str(ax.index), ax.offset, - ax.scale, ax.units) + return ax_signature_uniform % ( + str(ax.name)[:16], + ax.size, + str(ax.index), + ax.offset, + ax.scale, + ax.units, + ) else: - return ax_signature_non_uniform % (str(ax.name)[:16], ax.size, - str(ax.index), ax.units) + return ax_signature_non_uniform % ( + str(ax.name)[:16], + ax.size, + str(ax.index), + ax.units, + ) for ax in self.navigation_axes: - text += '\n' + text += "\n" text += axis_repr(ax, ax_signature_uniform, ax_signature_non_uniform) - text += '\n' - text += signature % ('-' * 16, '-' * 6, '-' * 6, - '-' * 7, '-' * 7, '-' * 6) + text += "\n" + text += signature % ("-" * 16, "-" * 6, "-" * 6, "-" * 7, "-" * 7, "-" * 6) for ax in self.signal_axes: - text += '\n' + text += "\n" text += axis_repr(ax, ax_signature_uniform, ax_signature_non_uniform) + if self.ragged: + text += "\n" + text += " Ragged axis | Variable length" return text def _repr_html_(self): - text = ("") - text += ('\n

< Axes manager, axes: %s >

\n' % - self._get_dimension_str()) - - def format_row(*args, tag='td', bold=False): + text = ( + "" + ) + text += ( + "\n

< Axes manager, axes: %s >

\n" % self._get_dimension_str() + ) + + def format_row(*args, tag="td", bold=False): if bold: signature = "\n " else: signature = "\n " signature += " ".join(("{}" for _ in args)) + " " - return signature.format(*map(lambda x: - '\n<' + tag + - '>{}', - args)) + return signature.format( + *map(lambda x: "\n<" + tag + ">{}", args) + ) def axis_repr(ax): index = ax.index if ax.navigate else "" if ax.is_uniform: - return format_row(ax.name, ax.size, index, ax.offset, - ax.scale, ax.units) + return format_row( + ax.name, ax.size, index, ax.offset, ax.scale, ax.units + ) else: - return format_row(ax.name, ax.size, index, "non-uniform axis", - "non-uniform axis", ax.units) + return format_row( + ax.name, + ax.size, + index, + "non-uniform axis", + "non-uniform axis", + ax.units, + ) if self.navigation_axes: text += "\n" - text += format_row('Navigation axis name', 'size', 'index', 'offset', - 'scale', 'units', tag='th') + text += format_row( + "Navigation axis name", + "size", + "index", + "offset", + "scale", + "units", + tag="th", + ) for ax in self.navigation_axes: text += axis_repr(ax) text += "
\n" if self.signal_axes: text += "\n" - text += format_row('Signal axis name', 'size', - "", 'offset', 'scale', 'units', tag='th') + text += format_row( + "Signal axis name", "size", "", "offset", "scale", "units", tag="th" + ) for ax in self.signal_axes: text += axis_repr(ax) text += "
\n" @@ -2267,8 +2494,8 @@ def coordinates(self, coordinates): if len(coordinates) != self.navigation_dimension: raise AttributeError( "The number of coordinates must be equal to the " - "navigation dimension that is %i" % - self.navigation_dimension) + "navigation dimension that is %i" % self.navigation_dimension + ) changes = False with self.events.indices_changed.suppress(): for value, axis in zip(coordinates, self.navigation_axes): @@ -2293,8 +2520,8 @@ def indices(self, indices): if len(indices) != self.navigation_dimension: raise AttributeError( "The number of indices must be equal to the " - "navigation dimension that is %i" % - self.navigation_dimension) + "navigation dimension that is %i" % self.navigation_dimension + ) changes = False with self.events.indices_changed.suppress(): for index, axis in zip(indices, self.navigation_axes): @@ -2322,10 +2549,14 @@ def _set_axis_attribute_values(self, attr, values): """ if not isiterable(values): - values = [values, ] * len(self._axes) + values = [ + values, + ] * len(self._axes) elif len(values) != len(self._axes): - raise ValueError("Values must have the same number" - "of items are axes are in this AxesManager") + raise ValueError( + "Values must have the same number" + "of items are axes are in this AxesManager" + ) for axis, value in zip(self._axes, values): setattr(axis, attr, value) @@ -2372,19 +2603,27 @@ def gui_navigation_sliders(self, title="", display=True, toolkit=None): # which is fine to filter # https://github.com/enthought/traitsui/issues/883 with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning, - message="'TraitPrefixList'", - module='traitsui') - warnings.filterwarnings("ignore", category=DeprecationWarning, - message="'TraitMap'", - module='traits') - return get_gui(self=self.navigation_axes, - toolkey="hyperspy.navigation_sliders", - display=display, - toolkit=toolkit, - title=title) - gui_navigation_sliders.__doc__ = \ - """ + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message="'TraitPrefixList'", + module="traitsui", + ) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message="'TraitMap'", + module="traits", + ) + return get_gui( + self=self.navigation_axes, + toolkey="hyperspy.navigation_sliders", + display=display, + toolkit=toolkit, + title=title, + ) + + gui_navigation_sliders.__doc__ = """ Navigation sliders to control the index of the navigation axes. Parameters @@ -2394,6 +2633,7 @@ def gui_navigation_sliders(self, title="", display=True, toolkit=None): %s """ + class GeneratorLen: """ Helper class for creating a generator-like object with a known length. @@ -2409,6 +2649,7 @@ class GeneratorLen: length : int The manually-specified length of the generator. """ + def __init__(self, gen, length): self.gen = gen self.length = length @@ -2418,3 +2659,11 @@ def __len__(self): def __iter__(self): return self.gen + + +def _parse_axis_attribute(value): + """Parse axis attribute""" + if value is t.Undefined: + return None + else: + return value diff --git a/hyperspy/component.py b/hyperspy/component.py index 1e8ecb3f8a..df1836d918 100644 --- a/hyperspy/component.py +++ b/hyperspy/component.py @@ -1,48 +1,45 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . + +import logging +from pathlib import Path import numpy as np -from dask.array import Array as dArray -import traits.api as t -from traits.trait_numeric import Array import sympy +import traits.api as t +from dask.array import Array as dArray +from rsciio.utils.tools import append2pathname, incremental_filename from sympy.utilities.lambdify import lambdify -from distutils.version import LooseVersion -from pathlib import Path +from traits.trait_numeric import Array -import hyperspy -from hyperspy.misc.utils import slugify, is_binned -from hyperspy.misc.io.tools import (incremental_filename, - append2pathname,) -from hyperspy.misc.export_dictionary import export_to_dictionary, \ - load_from_dictionary -from hyperspy.events import Events, Event +from hyperspy.events import Event, Events +from hyperspy.misc.export_dictionary import ( + export_to_dictionary, + load_from_dictionary, +) +from hyperspy.misc.model_tools import CurrentComponentValues +from hyperspy.misc.utils import display, get_object_package_info, slugify from hyperspy.ui_registry import add_gui_method -from IPython.display import display_pretty, display -from hyperspy.misc.model_tools import current_component_values -from hyperspy.misc.utils import get_object_package_info - -import logging _logger = logging.getLogger(__name__) -class NoneFloat(t.CFloat): # Lazy solution, but usable +class NoneFloat(t.CFloat): # Lazy solution, but usable default_value = None def validate(self, object, name, value): @@ -56,63 +53,30 @@ def validate(self, object, name, value): @add_gui_method(toolkey="hyperspy.Parameter") class Parameter(t.HasTraits): - - """Model parameter + """The parameter of a component. Attributes ---------- + bmin : float + The lower bound of the parameter. + bmax : float + The upper bound of the parameter. + ext_force_positive : bool + ext_bounded : bool + free : bool + map : numpy.ndarray + twin : None or Parameter + twin_function_expr : str + twin_inverse_function_expr : str value : float or array The value of the parameter for the current location. The value for other locations is stored in map. - bmin, bmax: float - Lower and upper bounds of the parameter value. - twin : {None, Parameter} - If it is not None, the value of the current parameter is - a function of the given Parameter. The function is by default - the identity function, but it can be defined by twin_function - twin_function_expr: str - Expression of the ``twin_function`` that enables setting a functional - relationship between the parameter and its twin. If ``twin`` is not - ``None``, the parameter value is calculated as the output of calling the - twin function with the value of the twin parameter. The string is - parsed using sympy, so permitted values are any valid sympy expressions - of one variable. If the function is invertible the twin inverse function - is set automatically. - twin_inverse_function_expr : str - Expression of the ``twin_inverse_function`` that enables setting the - value of the twin parameter. If ``twin`` is not - ``None``, its value is set to the output of calling the - twin inverse function with the value provided. The string is - parsed using sympy, so permitted values are any valid sympy expressions - of one variable. - twin_function : function - **Setting this attribute manually - is deprecated in HyperSpy newer than 1.1.2. It will become private in - HyperSpy 2.0. Please use ``twin_function_expr`` instead.** - twin_inverse_function : function - **Setting this attribute manually - is deprecated in HyperSpy newer than 1.1.2. It will become private in - HyperSpy 2.0. Please use ``twin_inverse_function_expr`` instead.** - ext_force_positive : bool - If True, the parameter value is set to be the absolute value - of the input value i.e. if we set Parameter.value = -3, the - value stored is 3 instead. This is useful to bound a value - to be positive in an optimization without actually using an - optimizer that supports bounding. - ext_bounded : bool - Similar to ext_force_positive, but in this case the bounds are - defined by bmin and bmax. It is a better idea to use - an optimizer that supports bounding though. - - Methods - ------- - connect, disconnect(function) - Call the functions connected when the value attribute changes. """ + __number_of_elements = 1 __value = 0 - __free = True + _free = True _bounds = (None, None) __twin = None _axes_manager = None @@ -125,55 +89,63 @@ class Parameter(t.HasTraits): # RangeEditor() as it works with bmin/bmax. value = t.Property(t.Either([t.CFloat(0), Array()])) - units = t.Str('') + units = t.Str("") free = t.Property(t.CBool(True)) - bmin = t.Property(NoneFloat(), label="Lower bounds") - bmax = t.Property(NoneFloat(), label="Upper bounds") + bmin = t.Property(NoneFloat(), label="Lower bound") + bmax = t.Property(NoneFloat(), label="Upper bound") _twin_function_expr = "" _twin_inverse_function_expr = "" - twin_function = None - _twin_inverse_function = None + _twin_function = None + # The inverse function is stored in one of the two following attributes + # depending on whether it was set manually or calculated with sympy + __twin_inverse_function = None _twin_inverse_sympy = None def __init__(self): self._twins = set() self.events = Events() - self.events.value_changed = Event(""" + self.events.value_changed = Event( + """ Event that triggers when the `Parameter.value` changes. The event triggers after the internal state of the `Parameter` has been updated. - Arguments - --------- + Parameters + ---------- obj : Parameter The `Parameter` that the event belongs to value : {float | array} The new value of the parameter - """, arguments=["obj", 'value']) + """, + arguments=["obj", "value"], + ) self.std = None self.component = None self.grad = None - self.name = '' - self.units = '' + self.name = "" + self.units = "" + self._linear = False self.map = None self.model = None - self._whitelist = {'_id_name': None, - 'value': None, - 'std': None, - 'free': None, - 'units': None, - 'map': None, - '_bounds': None, - 'ext_bounded': None, - 'name': None, - 'ext_force_positive': None, - 'twin_function_expr': None, - 'twin_inverse_function_expr': None, - 'self': ('id', None), - } - self._slicing_whitelist = {'map': 'inav'} + self._whitelist = { + "_id_name": None, + "value": None, + "std": None, + "free": None, + "units": None, + "map": None, + "_bounds": None, + "ext_bounded": None, + "name": None, + "_linear": None, + "ext_force_positive": None, + "twin_function_expr": None, + "twin_inverse_function_expr": None, + "self": ("id", None), + } + self._slicing_whitelist = {"map": "inav"} def _load_dictionary(self, dictionary): """Load data from dictionary. @@ -187,7 +159,7 @@ def _load_dictionary(self, dictionary): the dictionary. Has to match with the ``self._id_name``. * _whitelist: a dictionary, which keys are used as keywords to match with the parameter attributes. For more information see - :py:func:`~hyperspy.misc.export_dictionary.load_from_dictionary` + :func:`~hyperspy.misc.export_dictionary.load_from_dictionary` * any field from ``_whitelist.keys()``. Returns @@ -197,19 +169,22 @@ def _load_dictionary(self, dictionary): setting up the correct twins """ - if dictionary['_id_name'] == self._id_name: + if dictionary["_id_name"] == self._id_name: load_from_dictionary(self, dictionary) - return dictionary['self'] + return dictionary["self"] else: - raise ValueError("_id_name of parameter and dictionary do not match, \nparameter._id_name = %s\ - \ndictionary['_id_name'] = %s" % (self._id_name, dictionary['_id_name'])) + raise ValueError( + "_id_name of parameter and dictionary do not match, \nparameter._id_name = %s\ + \ndictionary['_id_name'] = %s" + % (self._id_name, dictionary["_id_name"]) + ) def __repr__(self): - text = '' - text += 'Parameter %s' % self.name + text = "" + text += "Parameter %s" % self.name if self.component is not None: - text += ' of %s' % self.component._get_short_description() - text = '<' + text + '>' + text += " of %s" % self.component._get_short_description() + text = "<" + text + ">" return text def __len__(self): @@ -217,13 +192,22 @@ def __len__(self): @property def twin_function_expr(self): + """ + Expression of the function that enables setting a functional + relationship between the parameter and its twin. If ``twin`` is not + ``None``, the parameter value is calculated as the output of calling the + twin function with the value of the twin parameter. The string is + parsed using sympy, so permitted values are any valid sympy expressions + of one variable. If the function is invertible the twin inverse function + is set automatically. + """ return self._twin_function_expr @twin_function_expr.setter def twin_function_expr(self, value): if not value: - self.twin_function = None - self.twin_inverse_function = None + self._twin_function = None + self.__twin_inverse_function = None self._twin_function_expr = "" self._twin_inverse_sympy = None return @@ -231,29 +215,37 @@ def twin_function_expr(self, value): if len(expr.free_symbols) > 1: raise ValueError("The expression must contain only one variable.") elif len(expr.free_symbols) == 0: - raise ValueError("The expression must contain one variable, " - "it contains none.") + raise ValueError("The expression must contain one variable.") x = tuple(expr.free_symbols)[0] - self.twin_function = lambdify(x, expr.evalf()) + self._twin_function = lambdify(x, expr.evalf()) self._twin_function_expr = value - if not self.twin_inverse_function: + if not self._twin_inverse_function: y = sympy.Symbol(x.name + "2") try: inv = list(sympy.solveset(sympy.Eq(y, expr), x)) self._twin_inverse_sympy = lambdify(y, inv) - self._twin_inverse_function = None + self.__twin_inverse_function = None except BaseException: # Not all may have a suitable solution. - self._twin_inverse_function = None + self.__twin_inverse_function = None self._twin_inverse_sympy = None _logger.warning( - "The function {} is not invertible. Setting the value of " - "{} will raise an AttributeError unless you set manually " - "``twin_inverse_function_expr``. Otherwise, set the " - "value of its twin parameter instead.".format(value, self)) + f"The function {value} is not invertible. Setting the " + f"value of {self} will raise an AttributeError unless " + "you manually set ``twin_inverse_function_expr``. " + "Otherwise, set the value of its twin parameter instead." + ) @property def twin_inverse_function_expr(self): + """ + Expression of the function that enables setting the + value of the twin parameter. If ``twin`` is not + ``None``, its value is set to the output of calling the + twin inverse function with the value provided. The string is + parsed using sympy, so permitted values are any valid sympy expressions + of one variable. + """ if self.twin: return self._twin_inverse_function_expr else: @@ -262,37 +254,37 @@ def twin_inverse_function_expr(self): @twin_inverse_function_expr.setter def twin_inverse_function_expr(self, value): if not value: - self.twin_inverse_function = None + self.__twin_inverse_function = None self._twin_inverse_function_expr = "" return expr = sympy.sympify(value) if len(expr.free_symbols) > 1: raise ValueError("The expression must contain only one variable.") elif len(expr.free_symbols) == 0: - raise ValueError("The expression must contain one variable, " - "it contains none.") + raise ValueError( + "The expression must contain one variable, " "it contains none." + ) x = tuple(expr.free_symbols)[0] - self._twin_inverse_function = lambdify(x, expr.evalf()) + self.__twin_inverse_function = lambdify(x, expr.evalf()) self._twin_inverse_function_expr = value @property - def twin_inverse_function(self): - if (not self.twin_inverse_function_expr and - self.twin_function_expr and self._twin_inverse_sympy): + def _twin_inverse_function(self): + if ( + self.twin_function_expr + and self._twin_inverse_sympy + and not self.twin_inverse_function_expr + ): return lambda x: self._twin_inverse_sympy(x).pop() else: - return self._twin_inverse_function - - @twin_inverse_function.setter - def twin_inverse_function(self, value): - self._twin_inverse_function = value + return self.__twin_inverse_function def _get_value(self): if self.twin is None: return self.__value else: - if self.twin_function: - return self.twin_function(self.twin.value) + if self._twin_function: + return self._twin_function(self.twin.value) else: return self.twin.value @@ -303,27 +295,28 @@ def _set_value(self, value): # TypeError when calling. See issue #349. if len(value) != self._number_of_elements: raise ValueError( - "The length of the parameter must be ", - self._number_of_elements) + "The length of the parameter must be ", self._number_of_elements + ) else: if not isinstance(value, tuple): value = tuple(value) except TypeError: if self._number_of_elements != 1: raise ValueError( - "The length of the parameter must be ", - self._number_of_elements) + "The length of the parameter must be ", self._number_of_elements + ) old_value = self.__value if self.twin is not None: - if self.twin_function is not None: - if self.twin_inverse_function is not None: - self.twin.value = self.twin_inverse_function(value) + if self._twin_function is not None: + if self._twin_inverse_function is not None: + self.twin.value = self._twin_inverse_function(value) return else: raise AttributeError( - "This parameter has a ``twin_function`` but" - "its ``twin_inverse_function`` is not defined.") + "This parameter has a twin function but its " + "``twin_inverse_function_expr`` is not defined." + ) else: self.twin.value = value return @@ -341,40 +334,43 @@ def _set_value(self, value): else: self.__value = value else: - bmin = (self.bmin if self.bmin is not None - else -np.inf) - bmax = (self.bmax if self.bmin is not None - else np.inf) + bmin = self.bmin if self.bmin is not None else -np.inf + bmax = self.bmax if self.bmin is not None else np.inf self.__value = np.clip(value, bmin, bmax) - if (self._number_of_elements != 1 and - not isinstance(self.__value, tuple)): + if self._number_of_elements != 1 and not isinstance(self.__value, tuple): self.__value = tuple(self.__value) if old_value != self.__value: - self.events.value_changed.trigger(value=self.__value, - obj=self) - self.trait_property_changed('value', old_value, self.__value) + self.events.value_changed.trigger(value=self.__value, obj=self) + self.trait_property_changed("value", old_value, self.__value) # Fix the parameter when coupled def _get_free(self): + """Whether the parameter is free or not.""" if self.twin is None: - return self.__free + return self._free else: return False def _set_free(self, arg): - old_value = self.__free - self.__free = arg + if arg and self.twin: + raise ValueError( + f"Parameter {self.name} can't be set free " + "is twinned with {self.twin}." + ) + old_value = self._free + self._free = arg if self.component is not None: self.component._update_free_parameters() - self.trait_property_changed('free', old_value, self.__free) + self.trait_property_changed("free", old_value, self._free) def _on_twin_update(self, value, twin=None): - if (twin is not None - and hasattr(twin, 'events') - and hasattr(twin.events, 'value_changed')): - with twin.events.value_changed.suppress_callback( - self._on_twin_update): + if ( + twin is not None + and hasattr(twin, "events") + and hasattr(twin.events, "value_changed") + ): + with twin.events.value_changed.suppress_callback(self._on_twin_update): self.events.value_changed.trigger(value=value, obj=self) else: self.events.value_changed.trigger(value=value, obj=self) @@ -387,26 +383,32 @@ def _set_twin(self, arg): twin_value = self.value if self in self.twin._twins: self.twin._twins.remove(self) - self.twin.events.value_changed.disconnect( - self._on_twin_update) + self.twin.events.value_changed.disconnect(self._on_twin_update) self.__twin = arg self.value = twin_value else: if self not in arg._twins: arg._twins.add(self) - arg.events.value_changed.connect(self._on_twin_update, - ["value"]) + arg.events.value_changed.connect(self._on_twin_update, ["value"]) self.__twin = arg if self.component is not None: self.component._update_free_parameters() def _get_twin(self): + """ + If it is not None, the value of the current parameter is + a function of the given Parameter. The function is by default + the identity function, but it can be defined by + :attr:`twin_function_expr` + """ return self.__twin + twin = property(_get_twin, _set_twin) def _get_bmin(self): + """The lower value of the bounds.""" if self._number_of_elements == 1: return self._bounds[0] else: @@ -420,9 +422,10 @@ def _set_bmin(self, arg): self._bounds = ((arg, self.bmax),) * self._number_of_elements # Update the value to take into account the new bounds self.value = self.value - self.trait_property_changed('bmin', old_value, arg) + self.trait_property_changed("bmin", old_value, arg) def _get_bmax(self): + """The higher value of the bounds.""" if self._number_of_elements == 1: return self._bounds[1] else: @@ -436,7 +439,7 @@ def _set_bmax(self, arg): self._bounds = ((self.bmin, arg),) * self._number_of_elements # Update the value to take into account the new bounds self.value = self.value - self.trait_property_changed('bmax', old_value, arg) + self.trait_property_changed("bmax", old_value, arg) @property def _number_of_elements(self): @@ -447,9 +450,10 @@ def _number_of_elements(self, arg): # Do nothing if the number of arguments stays the same if self.__number_of_elements == arg: return - if arg <= 1: - raise ValueError("Please provide an integer number equal " - "or greater to 1") + if arg < 1: + raise ValueError( + "Please provide an integer number equal " "or greater to 1" + ) self._bounds = ((self.bmin, self.bmax),) * arg self.__number_of_elements = arg @@ -462,6 +466,11 @@ def _number_of_elements(self, arg): @property def ext_bounded(self): + """ + Similar to :attr:`ext_force_positive`, but in this case the bounds + are defined by bmin and bmax. It is a better idea to use + an optimizer that supports bounding though. + """ return self.__ext_bounded @ext_bounded.setter @@ -473,6 +482,13 @@ def ext_bounded(self, arg): @property def ext_force_positive(self): + """ + If True, the parameter value is set to be the absolute value + of the input value i.e. if we set Parameter.value = -3, the + value stored is 3 instead. This is useful to bound a value + to be positive in an optimization without actually using an + optimizer that supports bounding. + """ return self.__ext_force_positive @ext_force_positive.setter @@ -485,7 +501,7 @@ def ext_force_positive(self, arg): def store_current_value_in_array(self): """Store the value and std attributes. - See also + See Also -------- fetch, assign_current_value_to_all @@ -494,10 +510,10 @@ def store_current_value_in_array(self): # If it is a single spectrum indices is () if not indices: indices = (0,) - self.map['values'][indices] = self.value - self.map['is_set'][indices] = True + self.map["values"][indices] = self.value + self.map["is_set"][indices] = True if self.std is not None: - self.map['std'][indices] = self.std + self.map["std"][indices] = self.std def fetch(self): """Fetch the stored value and std attributes from the @@ -516,9 +532,9 @@ def fetch(self): # If it is a single spectrum indices is () if not indices: indices = (0,) - if self.map['is_set'][indices]: - value = self.map['values'][indices] - std = self.map['std'][indices] + if self.map["is_set"][indices]: + value = self.map["values"][indices] + std = self.map["std"][indices] if isinstance(value, dArray): value = value.compute() if isinstance(std, dArray): @@ -545,9 +561,9 @@ def assign_current_value_to_all(self, mask=None): """ if mask is None: - mask = np.zeros(self.map.shape, dtype='bool') - self.map['values'][mask == False] = self.value - self.map['is_set'][mask == False] = True + mask = np.zeros(self.map.shape, dtype="bool") + self.map["values"][mask == 0] = self.value + self.map["is_set"][mask == 0] = True def _create_array(self): """Create the map array to store the information in @@ -556,23 +572,26 @@ def _create_array(self): """ shape = self._axes_manager._navigation_shape_in_array if not shape: - shape = [1, ] + shape = [ + 1, + ] # Shape-1 fields in dtypes won’t be collapsed to scalars in a future # numpy version (see release notes numpy 1.17.0) if self._number_of_elements > 1: - dtype_ = np.dtype([ - ('values', 'float', self._number_of_elements), - ('std', 'float', self._number_of_elements), - ('is_set', 'bool')]) + dtype_ = np.dtype( + [ + ("values", "float", self._number_of_elements), + ("std", "float", self._number_of_elements), + ("is_set", "bool"), + ] + ) else: - dtype_ = np.dtype([ - ('values', 'float'), - ('std', 'float'), - ('is_set', 'bool')]) - if (self.map is None or self.map.shape != shape or - self.map.dtype != dtype_): + dtype_ = np.dtype( + [("values", "float"), ("std", "float"), ("is_set", "bool")] + ) + if self.map is None or self.map.shape != shape or self.map.dtype != dtype_: self.map = np.zeros(shape, dtype_) - self.map['std'].fill(np.nan) + self.map["std"].fill(np.nan) # TODO: in the future this class should have access to # axes manager and should be able to fetch its own # values. Until then, the next line is necessary to avoid @@ -580,7 +599,7 @@ def _create_array(self): # from the newly defined arrays self.std = None - def as_signal(self, field='values'): + def as_signal(self, field="values"): """Get a parameter map as a signal object. Please note that this method only works when the navigation @@ -591,40 +610,34 @@ def as_signal(self, field='values'): field : {'values', 'std', 'is_set'} Field to return as signal. - Raises - ------ - NavigationDimensionError - If the navigation dimension is 0 - """ from hyperspy.signal import BaseSignal - s = BaseSignal(data=self.map[field], - axes=self._axes_manager._get_navigation_axes_dicts()) - if self.component is not None and \ - self.component.active_is_multidimensional: + s = BaseSignal( + data=self.map[field], axes=self._axes_manager._get_navigation_axes_dicts() + ) + if self.component is not None and self.component.active_is_multidimensional: s.data[np.logical_not(self.component._active_array)] = np.nan - s.metadata.General.title = ("%s parameter" % self.name - if self.component is None - else "%s parameter of %s component" % - (self.name, self.component.name)) + s.metadata.General.title = ( + "%s parameter" % self.name + if self.component is None + else "%s parameter of %s component" % (self.name, self.component.name) + ) for axis in s.axes_manager._axes: axis.navigate = False if self._number_of_elements > 1: s.axes_manager._append_axis( - size=self._number_of_elements, - name=self.name, - navigate=True) + size=self._number_of_elements, name=self.name, navigate=True + ) s._assign_subclass() if field == "values": # Add the variance if available std = self.as_signal(field="std") if not np.isnan(std.data).all(): - std.data = std.data ** 2 + std.data = std.data**2 std.metadata.General.title = "Variance" - s.metadata.set_item( - "Signal.Noise_properties.variance", std) + s.metadata.set_item("Signal.Noise_properties.variance", std) return s def plot(self, **kwargs): @@ -635,8 +648,8 @@ def plot(self, **kwargs): **kwargs Any extra keyword arguments are passed to the signal plot. - Example - ------- + Examples + -------- >>> parameter.plot() #doctest: +SKIP Set the minimum and maximum displayed values @@ -645,8 +658,7 @@ def plot(self, **kwargs): """ self.as_signal().plot(**kwargs) - def export(self, folder=None, name=None, format="hspy", - save_std=False): + def export(self, folder=None, name=None, format="hspy", save_std=False): """Save the data to a file. All the arguments are optional. Parameters @@ -669,14 +681,13 @@ def export(self, folder=None, name=None, format="hspy", if format is None: format = "hspy" if name is None: - name = self.component.name + '_' + self.name - filename = incremental_filename(slugify(name) + '.' + format) + name = self.component.name + "_" + self.name + filename = incremental_filename(slugify(name) + "." + format) if folder is not None: filename = Path(folder).joinpath(filename) self.as_signal().save(filename) if save_std is True: - self.as_signal(field='std').save(append2pathname( - filename, '_std')) + self.as_signal(field="std").save(append2pathname(filename, "_std")) def as_dictionary(self, fullcopy=True): """Returns parameter as a dictionary, saving all attributes from @@ -685,24 +696,25 @@ def as_dictionary(self, fullcopy=True): Parameters ---------- - fullcopy : Bool (optional, False) + fullcopy : bool, optional False Copies of objects are stored, not references. If any found, functions will be pickled and signals converted to dictionaries Returns ------- - A dictionary, containing at least the following fields: + dict + A dictionary, containing at least the following fields: * _id_name: _id_name of the original parameter, used to create the - dictionary. Has to match with the self._id_name + dictionary. Has to match with the ``self._id_name`` * _twins: a list of ids of the twins of the parameter * _whitelist: a dictionary, which keys are used as keywords to match with the parameter attributes. For more information see - :py:func:`~hyperspy.misc.export_dictionary.export_to_dictionary` + :func:`~hyperspy.misc.export_dictionary.export_to_dictionary` * any field from _whitelist.keys() """ - dic = {'_twins': [id(t) for t in self._twins]} + dic = {"_twins": [id(t) for t in self._twins]} export_to_dictionary(self, self._whitelist, dic, fullcopy) return dic @@ -712,87 +724,140 @@ def default_traits_view(self): # gives a ValueError. We therefore implement default_traits_view so # that configure/edit_traits will still work straight out of the box. # A whitelist controls which traits to include in this view. - from traitsui.api import RangeEditor, View, Item - whitelist = ['bmax', 'bmin', 'free', 'name', 'std', 'units', 'value'] - editable_traits = [trait for trait in self.editable_traits() - if trait in whitelist] - if 'value' in editable_traits: - i = editable_traits.index('value') + from traitsui.api import Item, RangeEditor, View + + whitelist = ["bmax", "bmin", "free", "name", "std", "units", "value"] + editable_traits = [ + trait for trait in self.editable_traits() if trait in whitelist + ] + if "value" in editable_traits: + i = editable_traits.index("value") v = editable_traits.pop(i) - editable_traits.insert(i, Item( - v, editor=RangeEditor(low_name='bmin', high_name='bmax'))) - view = View(editable_traits, buttons=['OK', 'Cancel']) + editable_traits.insert( + i, Item(v, editor=RangeEditor(low_name="bmin", high_name="bmax")) + ) + view = View(editable_traits, buttons=["OK", "Cancel"]) return view +COMPONENT_PARAMETERS_DOCSTRING = """Parameters + ---------- + parameter_name_list : list + The list of parameter names. + linear_parameter_list : list, optional + The list of linear parameter. The default is None. + """ + + @add_gui_method(toolkey="hyperspy.Component") class Component(t.HasTraits): + """ + The component of a model. + + Attributes + ---------- + active : bool + name : str + free_parameters : list + parameters : list + """ + __axes_manager = None + # setting dtype for t.Property(t.Bool) causes serialization error with cloudpickle + active = t.Property() + name = t.Property() - active = t.Property(t.CBool(True)) - name = t.Property(t.Str('')) + def __init__( + self, parameter_name_list, linear_parameter_list=None, *args, **kwargs + ): + """ + %s - def __init__(self, parameter_name_list): + """ + super().__init__(*args, **kwargs) self.events = Events() - self.events.active_changed = Event(""" + self.events.active_changed = Event( + """ Event that triggers when the `Component.active` changes. The event triggers after the internal state of the `Component` has been updated. - Arguments - --------- + Parameters + ---------- obj : Component The `Component` that the event belongs to active : bool The new active state - """, arguments=["obj", 'active']) - self.parameters = [] - self.init_parameters(parameter_name_list) + convolved : bool + Whether the `Component` is convolved or not. This enables + not convolving individual `Component`s in models that + support convolution. + + """, + arguments=["obj", "active"], + ) + self._parameters = [] + self._free_parameters = [] + self.init_parameters(parameter_name_list, linear_parameter_list) self._update_free_parameters() self.active = True - self._active_array = None # only if active_is_multidimensional is True + self._active_array = None # only if active_is_multidimensional is True self.isbackground = False self.convolved = True - self.parameters = tuple(self.parameters) self._id_name = self.__class__.__name__ - self._id_version = '1.0' + self._id_version = "1.0" self._position = None self.model = None - self.name = '' - self._whitelist = {'_id_name': None, - 'name': None, - 'active_is_multidimensional': None, - '_active_array': None, - 'active': None - } - self._slicing_whitelist = {'_active_array': 'inav'} - self._slicing_order = ('active', 'active_is_multidimensional', - '_active_array',) - - _name = '' + self.name = "" + self._whitelist = { + "_id_name": None, + "name": None, + "active_is_multidimensional": None, + "_active_array": None, + "active": None, + } + self._slicing_whitelist = {"_active_array": "inav"} + self._slicing_order = ( + "active", + "active_is_multidimensional", + "_active_array", + ) + + __init__.__doc__ %= COMPONENT_PARAMETERS_DOCSTRING + + _name = "" _active_is_multidimensional = False _active = True @property def active_is_multidimensional(self): """In multidimensional signals it is possible to store the value of the - :py:attr:`~.component.Component.active` attribute at each navigation - index. + ``active`` attribute at each navigation index. """ return self._active_is_multidimensional + @property + def free_parameters(self): + """The list of free parameters of the component.""" + return tuple(self._free_parameters) + + @property + def parameters(self): + """The list of parameters of the component.""" + return tuple(self._parameters) + @active_is_multidimensional.setter def active_is_multidimensional(self, value): if not isinstance(value, bool): - raise ValueError('Only boolean values are permitted') + raise ValueError("Only boolean values are permitted") if value == self.active_is_multidimensional: return if value: # Turn on if self._axes_manager.navigation_size < 2: - _logger.info('`navigation_size` < 2, skipping') + _logger.info("`navigation_size` < 2, skipping") return # Store value at current position self._create_active_array() @@ -808,6 +873,8 @@ def _get_name(self): return self._name def _set_name(self, value): + if not isinstance(value, str): + raise ValueError("Only string values are permitted") old_value = self._name if old_value == value: return @@ -815,16 +882,18 @@ def _set_name(self, value): for component in self.model: if value == component.name: raise ValueError( - "Another component already has " - "the name " + str(value)) + "Another component already has " "the name " + str(value) + ) self._name = value - setattr(self.model.components, slugify( - value, valid_variable_name=True), self) - self.model.components.__delattr__( - slugify(old_value, valid_variable_name=True)) + setattr( + self.model._components, slugify(value, valid_variable_name=True), self + ) + self.model._components.__delattr__( + slugify(old_value, valid_variable_name=True) + ) else: self._name = value - self.trait_property_changed('name', old_value, self._name) + self.trait_property_changed("name", old_value, self._name) @property def _axes_manager(self): @@ -838,8 +907,7 @@ def _axes_manager(self, value): @property def _is_navigation_multidimensional(self): - if (self._axes_manager is None or not - self._axes_manager.navigation_dimension): + if self._axes_manager is None or not self._axes_manager.navigation_dimension: return False else: return True @@ -861,45 +929,60 @@ def _set_active(self, arg): if self.active_is_multidimensional is True: self._store_active_value_in_array(arg) self.events.active_changed.trigger(active=self._active, obj=self) - self.trait_property_changed('active', old_value, self._active) + self.trait_property_changed("active", old_value, self._active) + + def init_parameters(self, parameter_name_list, linear_parameter_list=None): + """ + Initialise the parameters of the component. + + %s + + """ + if linear_parameter_list is None: + linear_parameter_list = [] - def init_parameters(self, parameter_name_list): for name in parameter_name_list: parameter = Parameter() - self.parameters.append(parameter) + self._parameters.append(parameter) parameter.name = name + if name in linear_parameter_list: + parameter._linear = True parameter._id_name = name setattr(self, name, parameter) - if hasattr(self, 'grad_' + name): - parameter.grad = getattr(self, 'grad_' + name) + if hasattr(self, "grad_" + name): + parameter.grad = getattr(self, "grad_" + name) parameter.component = self self.add_trait(name, t.Instance(Parameter)) + init_parameters.__doc__ %= COMPONENT_PARAMETERS_DOCSTRING + def _get_long_description(self): if self.name: - text = '%s (%s component)' % (self.name, self.__class__.__name__) + text = "%s (%s component)" % (self.name, self.__class__.__name__) else: - text = '%s component' % self.__class__.__name__ + text = "%s component" % self.__class__.__name__ return text def _get_short_description(self): - text = '' + text = "" if self.name: text += self.name else: text += self.__class__.__name__ - text += ' component' + text += " component" return text def __repr__(self): - text = '<%s>' % self._get_long_description() + text = "<%s>" % self._get_long_description() return text def _update_free_parameters(self): - self.free_parameters = sorted([par for par in self.parameters if - par.free], key=lambda x: x.name) - self._nfree_param = sum([par._number_of_elements for par in - self.free_parameters]) + self._free_parameters = sorted( + [par for par in self.parameters if par.free], key=lambda x: x.name + ) + self._nfree_param = sum( + [par._number_of_elements for par in self._free_parameters] + ) def update_number_parameters(self): i = 0 @@ -928,21 +1011,28 @@ def fetch_values_from_array(self, p, p_std=None, onlyfree=False): i = 0 for parameter in sorted(parameters, key=lambda x: x.name): length = parameter._number_of_elements - parameter.value = (p[i] if length == 1 else p[i:i + length]) + parameter.value = p[i] if length == 1 else p[i : i + length] if p_std is not None: - parameter.std = (p_std[i] if length == 1 else - tuple(p_std[i:i + length])) + parameter.std = ( + p_std[i] if length == 1 else tuple(p_std[i : i + length]) + ) i += length def _create_active_array(self): shape = self._axes_manager._navigation_shape_in_array if len(shape) == 1 and shape[0] == 0: - shape = [1, ] - if (not isinstance(self._active_array, np.ndarray) - or self._active_array.shape != shape): - _logger.debug('Creating _active_array for {}.\n\tCurrent array ' - 'is:\n{}'.format(self, self._active_array)) + shape = [ + 1, + ] + if ( + not isinstance(self._active_array, np.ndarray) + or self._active_array.shape != shape + ): + _logger.debug( + f"Creating _active_array for {self}.\n\tCurrent " + f"array is:\n{self._active_array}" + ) self._active_array = np.ones(shape, dtype=bool) def _create_arrays(self): @@ -961,13 +1051,14 @@ def fetch_stored_values(self, only_fixed=False): # functions. self.active = self.active if only_fixed is True: - parameters = (set(self.parameters) - - set(self.free_parameters)) + parameters = set(self.parameters) - set(self.free_parameters) else: parameters = self.parameters - parameters = [parameter for parameter in parameters - if (parameter.twin is None or - not isinstance(parameter.twin, Parameter))] + parameters = [ + parameter + for parameter in parameters + if (parameter.twin is None or not isinstance(parameter.twin, Parameter)) + ] for parameter in parameters: parameter.fetch() @@ -990,8 +1081,7 @@ def plot(self, only_free=True): for parameter in parameters: parameter.plot() - def export(self, folder=None, format="hspy", save_std=False, - only_free=True): + def export(self, folder=None, format="hspy", save_std=False, only_free=True): """Plot the value of the parameters of the model Parameters @@ -1021,30 +1111,26 @@ def export(self, folder=None, format="hspy", save_std=False, parameters = [k for k in parameters if k.twin is None] for parameter in parameters: - parameter.export(folder=folder, format=format, - save_std=save_std,) + parameter.export( + folder=folder, + format=format, + save_std=save_std, + ) def summary(self): for parameter in self.parameters: - dim = len(parameter.map.squeeze().shape) if parameter.map \ - is not None else 0 + dim = len(parameter.map.squeeze().shape) if parameter.map is not None else 0 if parameter.twin is None: if dim <= 1: - print('%s = %s ± %s %s' % (parameter.name, - parameter.value, - parameter.std, - parameter.units)) - - def __call__(self): - """Returns the corresponding model for the current coordinates - - Returns - ------- - numpy array - """ - axis = self.model.axis.axis[self.model.channel_switches] - component_array = self.function(axis) - return component_array + print( + "%s = %s ± %s %s" + % ( + parameter.name, + parameter.value, + parameter.std, + parameter.units, + ) + ) def _component2plot(self, axes_manager, out_of_range2nans=True): old_axes_manager = None @@ -1052,7 +1138,7 @@ def _component2plot(self, axes_manager, out_of_range2nans=True): old_axes_manager = self.model.axes_manager self.model.axes_manager = axes_manager self.fetch_stored_values() - s = self.model.__call__(component_list=[self]) + s = self.model._get_current_data(component_list=[self]) if not self.active: s.fill(np.nan) if old_axes_manager is not None: @@ -1061,14 +1147,16 @@ def _component2plot(self, axes_manager, out_of_range2nans=True): if out_of_range2nans is True: ns = np.empty(self.model.axis.axis.shape) ns.fill(np.nan) - ns[self.model.channel_switches] = s + ns[self.model._channel_switches] = s s = ns if old_axes_manager is not None: self.model.axes_manager = old_axes_manager self.fetch_stored_values() return s - def set_parameters_free(self, parameter_name_list=None): + def set_parameters_free( + self, parameter_name_list=None, only_linear=False, only_nonlinear=False + ): """ Sets parameters in a component to free. @@ -1078,20 +1166,31 @@ def set_parameters_free(self, parameter_name_list=None): If None, will set all the parameters to free. If list of strings, will set all the parameters with the same name as the strings in parameter_name_list to free. + only_linear : bool + If True, only sets a parameter free if it is linear + only_nonlinear : bool + If True, only sets a parameter free if it is nonlinear Examples -------- >>> v1 = hs.model.components1D.Voigt() >>> v1.set_parameters_free() >>> v1.set_parameters_free(parameter_name_list=['area','centre']) + >>> v1.set_parameters_free(only_linear=True) - See also + See Also -------- set_parameters_not_free hyperspy.model.BaseModel.set_parameters_free hyperspy.model.BaseModel.set_parameters_not_free """ + if only_linear and only_nonlinear: + raise ValueError( + "To set all parameters free, set both `only_linear` and " + "`only_nonlinear` to False." + ) + parameter_list = [] if not parameter_name_list: parameter_list = self.parameters @@ -1101,9 +1200,16 @@ def set_parameters_free(self, parameter_name_list=None): parameter_list.append(_parameter) for _parameter in parameter_list: - _parameter.free = True - - def set_parameters_not_free(self, parameter_name_list=None): + if not only_linear and not only_nonlinear: + _parameter.free = True + elif only_linear and _parameter._linear: + _parameter.free = True + elif only_nonlinear and not _parameter._linear: + _parameter.free = True + + def set_parameters_not_free( + self, parameter_name_list=None, only_linear=False, only_nonlinear=False + ): """ Sets parameters in a component to not free. @@ -1113,20 +1219,32 @@ def set_parameters_not_free(self, parameter_name_list=None): If None, will set all the parameters to not free. If list of strings, will set all the parameters with the same name as the strings in parameter_name_list to not free. + only_linear : bool + If True, only sets a parameter not free if it is linear + only_nonlinear : bool + If True, only sets a parameter not free if it is nonlinear Examples -------- >>> v1 = hs.model.components1D.Voigt() >>> v1.set_parameters_not_free() >>> v1.set_parameters_not_free(parameter_name_list=['area','centre']) + >>> v1.set_parameters_not_free(only_linear=True) + - See also + See Also -------- set_parameters_free hyperspy.model.BaseModel.set_parameters_free hyperspy.model.BaseModel.set_parameters_not_free """ + if only_linear and only_nonlinear: + raise ValueError( + "To set all parameters not free, " + "set both only_linear and _nonlinear to False." + ) + parameter_list = [] if not parameter_name_list: parameter_list = self.parameters @@ -1136,7 +1254,12 @@ def set_parameters_not_free(self, parameter_name_list=None): parameter_list.append(_parameter) for _parameter in parameter_list: - _parameter.free = False + if not only_linear and not only_nonlinear: + _parameter.free = False + elif only_linear and _parameter._linear: + _parameter.free = False + elif only_nonlinear and not _parameter._linear: + _parameter.free = False def _estimate_parameters(self, signal): if self._axes_manager != signal.axes_manager: @@ -1144,13 +1267,14 @@ def _estimate_parameters(self, signal): self._create_arrays() def as_dictionary(self, fullcopy=True): - """Returns component as a dictionary. For more information on method + """ + Returns component as a dictionary. For more information on method and conventions, see - py:meth:`~hyperspy.misc.export_dictionary.export_to_dictionary` + :meth:`~hyperspy.misc.export_dictionary.export_to_dictionary`. Parameters ---------- - fullcopy : Bool (optional, False) + fullcopy : bool, optional False Copies of objects are stored, not references. If any found, functions will be pickled and signals converted to dictionaries @@ -1162,23 +1286,24 @@ def as_dictionary(self, fullcopy=True): * parameters: a list of dictionaries of the parameters, one per component. * _whitelist: a dictionary with keys used as references saved - attributes, for more information, see - :py:func:`~hyperspy.misc.export_dictionary.export_to_dictionary` - * any field from _whitelist.keys() + attributes, for more information, see :func:`~hyperspy.misc.export_dictionary.export_to_dictionary`. + * any field from _whitelist.keys(). + """ - dic = { - 'parameters': [ - p.as_dictionary(fullcopy) for p in self.parameters]} + dic = {"parameters": [p.as_dictionary(fullcopy) for p in self.parameters]} dic.update(get_object_package_info(self)) export_to_dictionary(self, self._whitelist, dic, fullcopy) from hyperspy.model import _COMPONENTS + if self._id_name not in _COMPONENTS: - import dill - dic['_class_dump'] = dill.dumps(self.__class__) + import cloudpickle + + dic["_class_dump"] = cloudpickle.dumps(self.__class__) return dic def _load_dictionary(self, dic): - """Load data from dictionary. + """ + Load data from dictionary. Parameters ---------- @@ -1189,11 +1314,11 @@ def _load_dictionary(self, dic): dictionary. Has to match with the self._id_name * parameters: a list of dictionaries, one per parameter of the component (see - :py:meth:`~hyperspy.component.Parameter.as_dictionary` + :meth:`~hyperspy.component.Parameter.as_dictionary` documentation for more details) * _whitelist: a dictionary, which keys are used as keywords to match with the parameter attributes. For more information see - :py:func:`~hyperspy.misc.export_dictionary.load_from_dictionary` + :func:`~hyperspy.misc.export_dictionary.load_from_dictionary` * any field from _whitelist.keys() Returns @@ -1204,42 +1329,55 @@ def _load_dictionary(self, dic): correct twins. """ - if dic['_id_name'] == self._id_name: - if (self._id_name == "Polynomial" and - LooseVersion(hyperspy.__version__) >= LooseVersion("2.0")): - # in HyperSpy 2.0 the polynomial definition changed + if dic["_id_name"] == self._id_name: + # Restoring of polynomials saved with Hyperspy . +# along with HyperSpy. If not, see . +import importlib + +from hyperspy.extensions import EXTENSIONS as _EXTENSIONS -__doc__ = """ +__all__ = [component for component, specs_ in _EXTENSIONS["components1D"].items()] + + +def __dir__(): + return sorted(__all__) + + +def __getattr__(name): + if name in __all__: + spec = _EXTENSIONS["components1D"][name] + return getattr(importlib.import_module(spec["module"]), name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -Components that can be used to define a 1D model for e.g. curve fitting. -There are some components that are only useful for one particular kind of signal -and therefore their name are preceded by the signal name: eg. eels_cl_edge. +_base_docstring = """ + +Components that can be used to define a 1D model for e.g. curve fitting. Writing a new template is easy: see the user guide documentation on creating components. For more details see each component docstring. -==================================================================== +============================================== """ -# -*- coding: utf-8 -*- -from hyperspy.extensions import EXTENSIONS as _EXTENSIONS -import importlib +def _generate_docstring(base_docstring): + # Generate the documentation + for name in __dir__(): + # get the component class + component = __getattr__(name) + spec = _EXTENSIONS["components1D"][name] + path = spec["module"].replace("hyperspy", "~") + line1 = f":class:`{path}.{name}`" + "\n" + component_doc = component.__doc__ or "..." + # Get the first line only + component_doc = component_doc.split("\n")[0] + line2 = " " + component_doc + "\n\n" + base_docstring += line1 + line2 + + return base_docstring + -_g = globals() -for _component, _specs in _EXTENSIONS["components1D"].items(): - # Don't add the new Polynomial to the API. - # To use it the old `Polynomial` has a `legacy` keyword. - # TODO: remove in HyperSpy v2.0 - if _component == "eab91275-88db-4855-917a-cdcbe7209592": - continue - _g[_component] = getattr( - importlib.import_module( - _specs["module"]), _component) - -del importlib, _component, _specs, _g - -# Generating the documentation - -# Grab all the currently defined globals and make a copy of the keys -# (can't use it directly, as the size changes) -_keys = [key for key in globals().keys()] - -# For every key in alphabetically sorted order -for key in sorted(_keys): - # if it does not start with a "_" - if not key.startswith('_'): - # get the component class (or function) - component = eval(key) - # If the component has documentation, grab the first 43 characters of - # the first line of the documentation. Else just use two dots ("..") - second_part = '..' if component.__doc__ is None else \ - component.__doc__.split('\n')[0][:43] + '..' - # append the component name (up to 25 characters + one space) and the - # start of the documentation as one line to the current doc - __doc__ += key[:25] + ' ' * (26 - len(key)) + second_part + '\n' - -# delete all the temporary things from the namespace once done -# so that they don't show up in the auto-complete -del key, _keys, component, second_part +__doc__ = _generate_docstring(_base_docstring) diff --git a/hyperspy/components2d.py b/hyperspy/components2d.py index 1e17f4396c..022c81f90f 100644 --- a/hyperspy/components2d.py +++ b/hyperspy/components2d.py @@ -1,64 +1,67 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -__doc__ = """ +import importlib + +from hyperspy.extensions import EXTENSIONS as _EXTENSIONS + +__all__ = [component for component, specs_ in _EXTENSIONS["components2D"].items()] + + +def __dir__(): + return sorted(__all__) + -Components that can be used to define a 2D model for e.g. 2D model fitting. +def __getattr__(name): + if name in __all__: + spec = _EXTENSIONS["components2D"][name] + return getattr(importlib.import_module(spec["module"]), name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -Writing a new template is easy: see the user guide documentation on creating + +_base_docstring = """ + +Components that can be used to define a 2D model for e.g. curve fitting. + +Writing a new template is easy, see the user guide documentation on creating components. For more details see each component docstring. -==================================================================== +============================================== """ -from hyperspy.extensions import EXTENSIONS as _EXTENSIONS -import importlib +def _generate_docstring(base_docstring): + # Generate the documentation + for name in __dir__(): + # get the component class + component = __getattr__(name) + spec = _EXTENSIONS["components2D"][name] + path = spec["module"].replace("hyperspy", "~") + line1 = f":class:`{path}.{name}`" + "\n" + component_doc = component.__doc__ or "..." + # Get the first line only + component_doc = component_doc.split("\n")[0] + line2 = " " + component_doc + "\n\n" + base_docstring += line1 + line2 + + return base_docstring + -_g = globals() -for _component, _specs in _EXTENSIONS["components2D"].items(): - _g[_component] = getattr( - importlib.import_module( - _specs["module"]), _component) - -del importlib -# Generating the documentation - -# Grab all the currently defined globals and make a copy of the keys -# (can't use it directly, as the size changes) -_keys = [key for key in globals().keys()] - -# For every key in alphabetically sorted order -for key in sorted(_keys): - # if it does not start with a "_" - if not key.startswith('_'): - # get the component class (or function) - component = eval(key) - # If the component has documentation, grab the first 43 characters of - # the first line of the documentation. Else just use two dots ("..") - second_part = '..' if component.__doc__ is None else \ - component.__doc__.split('\n')[0][:43] + '..' - # append the component name (up to 25 characters + one space) and the - # start of the documentation as one line to the current doc - __doc__ += key[:25] + ' ' * (26 - len(key)) + second_part + '\n' - -# delete all the temporary things from the namespace once done -# so that they don't show up in the auto-complete -del key, _keys, component, second_part +__doc__ = _generate_docstring(_base_docstring) diff --git a/hyperspy/conftest.py b/hyperspy/conftest.py index 63f02c2baf..4bf7af0164 100644 --- a/hyperspy/conftest.py +++ b/hyperspy/conftest.py @@ -1,26 +1,35 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . + +import importlib +import os +from pathlib import Path + +# memory leak in kmeans with mkl +os.environ["OMP_NUM_THREADS"] = "1" + try: # Set traits toolkit to work in a headless system # Capture error when toolkit is already previously set which typically # occurs when building the doc locally from traits.etsconfig.api import ETSConfig + ETSConfig.toolkit = "null" except ValueError: # in case ETSConfig.toolkit was already set previously. @@ -30,29 +39,33 @@ # 'agg' as early as we can is useless for testing. import matplotlib.pyplot as plt +# Use matplotlib fixture to clean up figure, setup backend, etc. +from matplotlib.testing.conftest import mpl_test_settings # noqa: F401 + import pytest import numpy as np import matplotlib +import dask.array as da import hyperspy.api as hs -matplotlib.rcParams['figure.max_open_warning'] = 25 -matplotlib.rcParams['interactive'] = False -hs.preferences.Plot.saturated_pixels = 0.0 -hs.preferences.Plot.cmap_navigator = 'viridis' -hs.preferences.Plot.cmap_signal = 'viridis' +matplotlib.rcParams["figure.max_open_warning"] = 25 +matplotlib.rcParams["interactive"] = False +hs.preferences.Plot.cmap_navigator = "viridis" +hs.preferences.Plot.cmap_signal = "viridis" hs.preferences.Plot.pick_tolerance = 5.0 - -# Set parallel to False by default, so only -# those tests with parallel=True are run in parallel -hs.preferences.General.parallel = False +hs.preferences.Plot.use_subfigure = False +# Don't show progressbar since it contains the runtime which +# will make the doctest fail +hs.preferences.General.show_progressbar = False @pytest.fixture(autouse=True) def add_np(doctest_namespace): - doctest_namespace['np'] = np - doctest_namespace['plt'] = plt - doctest_namespace['hs'] = hs + doctest_namespace["np"] = np + doctest_namespace["plt"] = plt + doctest_namespace["hs"] = hs + doctest_namespace["da"] = da @pytest.fixture @@ -63,19 +76,30 @@ def pdb_cmdopt(request): def setup_module(mod, pdb_cmdopt): if pdb_cmdopt: import dask + dask.set_options(get=dask.local.get_sync) -from matplotlib.testing.conftest import mpl_test_settings +pytest_mpl_spec = importlib.util.find_spec("pytest_mpl") -try: - import pytest_mpl -except ImportError: +if pytest_mpl_spec is None: # Register dummy marker to allow running the test suite without pytest-mpl def pytest_configure(config): config.addinivalue_line( "markers", "mpl_image_compare: dummy marker registration to allow running " - "without the pytest-mpl plugin." + "without the pytest-mpl plugin.", ) +else: + def pytest_configure(config): + # raise an error if the baseline images are not present + # which is the case when installing from a wheel + baseline_images_path = ( + Path(__file__).parent / "tests" / "drawing" / "plot_signal" + ) + if config.getoption("--mpl") and not baseline_images_path.exists(): + raise ValueError( + "`--mpl` flag can't not be used because the " + "baseline images are not packaged." + ) diff --git a/hyperspy/data/__init__.py b/hyperspy/data/__init__.py new file mode 100644 index 0000000000..387550fb96 --- /dev/null +++ b/hyperspy/data/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +""" +The :mod:`hyperspy.api.data` module includes synthetic data signal. +""" + +from .artificial_data import ( + atomic_resolution_image, + luminescence_signal, + wave_image, +) +from .two_gaussians import two_gaussians + +__all__ = [ + "atomic_resolution_image", + "luminescence_signal", + "two_gaussians", + "wave_image", +] + + +def __dir__(): + return sorted(__all__) diff --git a/hyperspy/data/artificial_data.py b/hyperspy/data/artificial_data.py new file mode 100644 index 0000000000..15f936ba30 --- /dev/null +++ b/hyperspy/data/artificial_data.py @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +"""Functions for generating artificial data. + +For use in things like docstrings or to test HyperSpy functionalities. + +""" + +import numpy as np + +from hyperspy import components1d, components2d, signals +from hyperspy.axes import UniformDataAxis +from hyperspy.decorators import jit_ifnumba +from hyperspy.misc.math_tools import check_random_state + +try: + from numba import prange +except ImportError: + # Numba not installed + prange = range + + +ADD_NOISE_DOCSTRING = """add_noise : bool + If True, add noise to the signal. Use ``random_state`` to seed + the noise to generate reproducible noise. + random_state : None, int or numpy.random.Generator, default None + Random seed used to generate the data. + """ + + +@jit_ifnumba(cache=True, parallel=True) +def _create_array_with_gaussian(spacing_x, spacing_y, size_x, size_y, gaussian): + array = np.zeros((size_x, size_y), dtype=np.float32) + for i in prange(int(size_x / spacing_x)): + for j in prange(int(size_y / spacing_y)): + array[ + i * spacing_x : (i + 1) * spacing_x, j * spacing_y : (j + 1) * spacing_y + ] += gaussian + + return array * 1e3 + + +def atomic_resolution_image( + size=512, + spacing=0.2, + column_radius=0.05, + rotation_angle=0, + pixel_size=0.015, + add_noise=False, + random_state=None, +): + """ + Make an artificial atomic resolution image. The atomic columns + are modelled with Gaussian functions. + + Parameters + ---------- + size : int or tuple of int, default=512 + The number of pixels of the image in horizontal and vertical + directions. If int, the size is the same in both directions. + spacing : float or tuple of float, default=14 + The spacing between the atomic columns in horizontal + and vertical directions in nanometer. + column_radius : float, default=0.05 + The radius of the atomic column, i. e. the width of the Gaussian + in nanometer. + rotation_angle : int or float, default=0 + The rotation of the lattice in degree. + pixel_size : float, default=0.015 + The pixel size in nanometer. + %s + + Returns + ------- + :class:`~.api.signals.Signal2D` + + Examples + -------- + >>> import hyperspy.api as hs + >>> s = hs.data.atomic_resolution_image() + >>> s.plot() + + """ + if isinstance(size, int): + size = (size,) * 2 + elif not isinstance(size, tuple): + raise ValueError("`size` must be an integer or tuple of int.") + + if isinstance(spacing, float): + spacing = (spacing,) * 2 + elif not isinstance(spacing, tuple): + raise ValueError("`spacing` must be an integer or tuple of int.") + + size_x, size_y = size + spacing_x, spacing_y = tuple([int(round(v / pixel_size)) for v in spacing]) + + gaussian = components2d.Gaussian2D( + sigma_x=column_radius / pixel_size, + sigma_y=column_radius / pixel_size, + centre_x=spacing_x / 2, + centre_y=spacing_y / 2, + ) + + gaussian_values = gaussian.function(*np.mgrid[:spacing_x, :spacing_y]) + + array = _create_array_with_gaussian( + spacing_x, spacing_y, size_x, size_y, gaussian_values + ) + + if add_noise: + random_state = check_random_state(random_state) + array += random_state.poisson(array) + + s = signals.Signal2D(array) + + if rotation_angle != 0: + from scipy.ndimage import rotate + + s.map(rotate, angle=rotation_angle, reshape=False) + + w, h = s.axes_manager.signal_axes[0].size, s.axes_manager.signal_axes[1].size + wr, hr = _get_largest_rectangle_from_rotation(w, h, rotation_angle) + w_remove, h_remove = (w - wr), (h - hr) + s.crop_signal( + int(w_remove / 2), + int(w - w_remove / 2), + int(h_remove / 2), + int(h - h_remove / 2), + ) + + for axis in s.axes_manager.signal_axes: + axis.scale = pixel_size + axis.units = "nm" + + return s + + +atomic_resolution_image.__doc__ %= ADD_NOISE_DOCSTRING + + +def luminescence_signal( + navigation_dimension=0, + uniform=False, + add_baseline=False, + add_noise=True, + random_state=None, +): + """ + Get an artificial luminescence signal in wavelength scale (nm, uniform) or + energy scale (eV, non-uniform), simulating luminescence data recorded with a + diffracting spectrometer. Some random noise is also added to the spectrum, + to simulate experimental noise. + + Parameters + ---------- + navigation_dimension : int + The navigation dimension(s) of the signal. 0 = single spectrum, + 1 = linescan, 2 = spectral map etc... + uniform : bool + return uniform (wavelength) or non-uniform (energy) spectrum + add_baseline : bool + If true, adds a constant baseline to the spectrum. Conversion to + energy representation will turn the constant baseline into inverse + powerlaw. + %s + + Examples + -------- + >>> import hyperspy.api as hs + >>> s = hs.data.luminescence_signal() + >>> s.plot() + + With constant baseline + + >>> s = hs.data.luminescence_signal(uniform=True, add_baseline=True) + >>> s.plot() + + To make the noise the same for multiple spectra, which can + be useful for testing fitting routines + + >>> s1 = hs.data.luminescence_signal(random_state=10) + >>> s2 = hs.data.luminescence_signal(random_state=10) + >>> print((s1.data == s2.data).all()) + True + + 2D map + + >>> s = hs.data.luminescence_signal(navigation_dimension=2) + >>> s.plot() + + See Also + -------- + atomic_resolution_image + + Returns + ------- + :class:`~.api.signals.Signal1D` + """ + + # Initialisation of random number generator + random_state = check_random_state(random_state) + + # Creating a uniform data axis, roughly similar to Horiba iHR320 with a 150 mm-1 grating + nm_axis = UniformDataAxis( + index_in_array=None, + name="Wavelength", + units="nm", + navigate=False, + size=1024, + scale=0.54, + offset=222.495, + is_binned=False, + ) + + # Artificial luminescence peak + gaussian_peak = components1d.Gaussian(A=5000, centre=375, sigma=25) + + if navigation_dimension >= 0: + # Generate empty data (ones) + data = np.ones([10 for i in range(navigation_dimension)] + [nm_axis.size]) + # Generate spatial axes + spaxes = [ + UniformDataAxis( + index_in_array=None, + name="X{:d}".format(i), + units="um", + navigate=False, + size=10, + scale=2.1, + offset=0, + is_binned=False, + ) + for i in range(navigation_dimension) + ] + # Generate empty signal + sig = signals.Signal1D(data, axes=spaxes + [nm_axis]) + sig.metadata.General.title = "{:d}d-map Artificial Luminescence Signal".format( + navigation_dimension + ) + else: + raise ValueError( + "Value {:d} invalid as navigation dimension.".format(navigation_dimension) + ) + + # Populating data array, possibly with noise and baseline + sig.data *= gaussian_peak.function(nm_axis.axis) + if add_noise: + sig.data += (random_state.uniform(size=sig.data.shape) - 0.5) * 1.4 + if add_baseline: + data += 350.0 + + # if not uniform, transformation into non-uniform axis + if not uniform: + hc = 1239.84198 # nm/eV + # converting to non-uniform axis + sig.axes_manager.signal_axes[0].convert_to_functional_data_axis( + expression="a/x", + name="Energy", + units="eV", + a=hc, + ) + # Reverting the orientation of signal axis to have increasing Energy + sig = sig.isig[::-1] + # Jacobian transformation + Eax = sig.axes_manager.signal_axes[0].axis + sig *= hc / Eax**2 + return sig + + +luminescence_signal.__doc__ %= ADD_NOISE_DOCSTRING + + +def wave_image( + angle=45, wavelength=10, shape=(256, 256), add_noise=True, random_state=None +): + """ + Returns a wave image generated using the sinus function. + + Parameters + ---------- + angle : float, optional + The angle in degree. + wavelength : float, optional + The wavelength the wave in pixel. The default is 10 + shape : tuple of float, optional + The shape of the data. The default is (256, 256). + %s + + Returns + ------- + :class:`~.api.signals.Signal2D` + """ + + x = np.arange(0, shape[0], 1) + y = np.arange(0, shape[1], 1) + X, Y = np.meshgrid(x, y) + + angle = np.deg2rad(angle) + grating = np.sin(2 * np.pi * (X * np.cos(angle) + Y * np.sin(angle)) / wavelength) + if add_noise: + random_state = check_random_state(random_state) + + grating += random_state.random(grating.shape) + + s = signals.Signal2D(grating) + for axis in s.axes_manager.signal_axes: + axis.units = "nm" + axis.scale = 0.01 + + return s + + +wave_image.__doc__ %= ADD_NOISE_DOCSTRING + + +def _get_largest_rectangle_from_rotation(width, height, angle): + """ + Given a rectangle of size wxh that has been rotated by 'angle' (in + degrees), computes the width and height of the largest possible + axis-aligned rectangle (maximal area) within the rotated rectangle. + from: http://stackoverflow.com/a/16778797/1018861 + In hyperspy, it is centered around centre coordinate of the signal. + """ + import math + + angle = math.radians(angle) + if width <= 0 or height <= 0: + return 0, 0 + + width_is_longer = width >= height + side_long, side_short = (width, height) if width_is_longer else (height, width) + + # since the solutions for angle, -angle and 180-angle are all the same, + # if suffices to look at the first quadrant and the absolute values of sin,cos: + sin_a, cos_a = abs(math.sin(angle)), abs(math.cos(angle)) + if side_short <= 2.0 * sin_a * cos_a * side_long: + # half constrained case: two crop corners touch the longer side, + # the other two corners are on the mid-line parallel to the longer line + x = 0.5 * side_short + wr, hr = (x / sin_a, x / cos_a) if width_is_longer else (x / cos_a, x / sin_a) + else: + # fully constrained case: crop touches all 4 sides + cos_2a = cos_a * cos_a - sin_a * sin_a + wr, hr = ( + (width * cos_a - height * sin_a) / cos_2a, + (height * cos_a - width * sin_a) / cos_2a, + ) + + return wr, hr diff --git a/hyperspy/data/two_gaussians.py b/hyperspy/data/two_gaussians.py new file mode 100644 index 0000000000..65d32fbcb5 --- /dev/null +++ b/hyperspy/data/two_gaussians.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + + +import numpy as np + +import hyperspy.api as hs + + +def two_gaussians(add_noise=True, return_model=False): + """ + Create synthetic data consisting of two Gaussian functions with + random centers and area + + Parameters + ---------- + add_noise : bool + If True, add noise to the signal. + return_model : bool + If True, returns the model in addition to the signal. + + Returns + ------- + :class:`~.api.signals.BaseSignal` or tuple + Returns tuple when ``return_model=True``. + """ + + domain = 32 # size of the square domain + hfactor = 600 + cent = (domain // 2, domain // 2) + y, x = np.ogrid[-cent[0] : domain - cent[0], -cent[1] : domain - cent[1]] + + def gaussian2d(x, y, A=1, x0=0, y0=0, sigmax=20, sigmay=10): + return A * np.exp( + -((x - x0) ** 2 / 2 / sigmax**2 + (y - y0) ** 2 / 2 / sigmay**2) + ) + + center_narrow = 50 + 10 * np.sin(3 * np.pi * x / domain) * np.cos( + 4 * np.pi * y / domain + ) + center_wide = 50 + 10 * ( + -0.1 * np.sin(3 * np.pi * x / domain) * np.cos(4 * np.pi * y / domain) + ) + + r = np.sqrt(x**2 + y**2) + h_narrow = 0.5 * (0.5 + np.sin(r) ** 2) * gaussian2d(x, y) * hfactor + h_wide = (0.5 + np.cos(r) ** 2) * gaussian2d(x, y) * hfactor + + s = hs.signals.Signal1D(np.ones((domain, domain, 1024))) + s.metadata.General.title = "Two Gaussians" + s.axes_manager[0].name = "x" + s.axes_manager[0].units = "nm" + s.axes_manager[1].name = "y" + s.axes_manager[1].units = "nm" + + s.axes_manager[2].name = "Energy" + s.axes_manager[2].name = "eV" + s.axes_manager[2].scale = 0.1 + m = s.create_model() + + gs01 = hs.model.components1D.GaussianHF() + gs01.name = "wide" + m.append(gs01) + gs01.fwhm.value = 60 + gs01.centre.map["values"][:] = center_wide + gs01.centre.map["is_set"][:] = True + gs01.height.map["values"][:] = h_wide + gs01.height.map["is_set"][:] = True + + gs02 = hs.model.components1D.GaussianHF() + gs02.name = "narrow" + m.append(gs02) + gs02.fwhm.value = 6 + gs02.centre.map["values"][:] = center_narrow + gs02.centre.map["is_set"][:] = True + gs02.height.map["values"][:] = h_narrow + gs02.height.map["is_set"][:] = True + s.data = m.as_signal(show_progressbar=False).data + s.change_dtype(np.int64) + s.add_poissonian_noise(random_state=0) + m.store("ground truth") + + if return_model: + return s, m + else: + return s diff --git a/hyperspy/datasets/__init__.py b/hyperspy/datasets/__init__.py deleted file mode 100644 index aa66b40688..0000000000 --- a/hyperspy/datasets/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -The :mod:`hyperspy.datasets` module includes access to local and remote -datasets. - -Functions: - - eelsdb - Download spectra from the EELS data base http://eelsdb.eu - -Submodules: - -The :mod:`~hyperspy.api.datasets` module contains the following submodules: - - :mod:`~hyperspy.api.datasets.example_signals` - Example datasets distributed with HyperSpy. - - -""" - -from hyperspy.misc.eels.eelsdb import eelsdb -from hyperspy.datasets import artificial_data diff --git a/hyperspy/datasets/artificial_data.py b/hyperspy/datasets/artificial_data.py deleted file mode 100644 index 58f6dca0f5..0000000000 --- a/hyperspy/datasets/artificial_data.py +++ /dev/null @@ -1,472 +0,0 @@ -"""Functions for generating artificial data. - -For use in things like docstrings or to test HyperSpy functionalities. - -""" - -import numpy as np - -from hyperspy import components1d, components2d -from hyperspy import signals -from hyperspy.misc.math_tools import check_random_state -from hyperspy.axes import UniformDataAxis - - -ADD_POWERLAW_DOCSTRING = \ -"""add_powerlaw : bool - If True, adds a powerlaw background to the spectrum. Default is False. - """ - -ADD_NOISE_DOCSTRING = \ -"""add_noise : bool - If True, add noise to the signal. See note to seed the noise to - generate reproducible noise. - random_state : None or int or RandomState instance, default None - Random seed used to generate the data. - """ - -RETURNS_DOCSTRING = \ -"""Returns - ------- - :py:class:`~hyperspy._signals.eels.EELSSpectrum` - """ - - - -def get_low_loss_eels_signal(add_noise=True, random_state=None): - """Get an artificial low loss electron energy loss spectrum. - - The zero loss peak is offset by 4.1 eV. - - Parameters - ---------- - %s - %s - - Returns - ------- - artificial_low_loss_signal : :py:class:`~hyperspy._signals.eels.EELSSpectrum` - - Example - ------- - >>> s = hs.datasets.artificial_data.get_low_loss_eels_signal() - >>> s.plot() - - See also - -------- - get_core_loss_eels_signal, get_core_loss_eels_model, - get_low_loss_eels_line_scan_signal, get_core_loss_eels_line_scan_signal - - """ - - random_state = check_random_state(random_state) - - x = np.arange(-100, 400, 0.5) - zero_loss = components1d.Gaussian(A=100, centre=4.1, sigma=1) - plasmon = components1d.Gaussian(A=100, centre=60, sigma=20) - - data = zero_loss.function(x) - data += plasmon.function(x) - if add_noise: - data += random_state.uniform(size=len(x)) * 0.7 - - s = signals.EELSSpectrum(data) - s.axes_manager[0].offset = x[0] - s.axes_manager[0].scale = x[1] - x[0] - s.metadata.General.title = 'Artifical low loss EEL spectrum' - s.axes_manager[0].name = 'Electron energy loss' - s.axes_manager[0].units = 'eV' - s.set_microscope_parameters( - beam_energy=200, convergence_angle=26, collection_angle=20) - return s - -get_low_loss_eels_signal.__doc__ %= (ADD_NOISE_DOCSTRING, - RETURNS_DOCSTRING) - - -def get_core_loss_eels_signal(add_powerlaw=False, add_noise=True, random_state=None): - """Get an artificial core loss electron energy loss spectrum. - - Similar to a Mn-L32 edge from a perovskite oxide. - - Some random noise is also added to the spectrum, to simulate - experimental noise. - - Parameters - ---------- - %s - %s - - %s - - Example - ------- - >>> import hs.datasets.artifical_data as ad - >>> s = ad.get_core_loss_eels_signal() - >>> s.plot() - - With the powerlaw background - - >>> s = ad.get_core_loss_eels_signal(add_powerlaw=True) - >>> s.plot() - - To make the noise the same for multiple spectra, which can - be useful for testing fitting routines - - >>> s1 = ad.get_core_loss_eels_signal(random_state=10) - >>> s2 = ad.get_core_loss_eels_signal(random_state=10) - >>> (s1.data == s2.data).all() - True - - See also - -------- - get_core_loss_eels_line_scan_signal, get_low_loss_eels_line_scan_signal, - get_core_loss_eels_model - - """ - - random_state = check_random_state(random_state) - - x = np.arange(400, 800, 1) - arctan = components1d.EELSArctan(A=1, k=0.2, x0=688) - mn_l3_g = components1d.Gaussian(A=100, centre=695, sigma=4) - mn_l2_g = components1d.Gaussian(A=20, centre=720, sigma=4) - - data = arctan.function(x) - data += mn_l3_g.function(x) - data += mn_l2_g.function(x) - if add_noise: - data += random_state.uniform(size=len(x)) * 0.7 - - if add_powerlaw: - powerlaw = components1d.PowerLaw(A=10e8, r=3, origin=0) - data += powerlaw.function(x) - - s = signals.EELSSpectrum(data) - s.axes_manager[0].offset = x[0] - s.metadata.General.title = 'Artifical core loss EEL spectrum' - s.axes_manager[0].name = 'Electron energy loss' - s.axes_manager[0].units = 'eV' - s.set_microscope_parameters( - beam_energy=200, convergence_angle=26, collection_angle=20) - return s - -get_core_loss_eels_signal.__doc__ %= (ADD_POWERLAW_DOCSTRING, - ADD_NOISE_DOCSTRING, - RETURNS_DOCSTRING) - - -def get_low_loss_eels_line_scan_signal(add_noise=True, random_state=None): - """Get an artificial low loss electron energy loss line scan spectrum. - - The zero loss peak is offset by 4.1 eV. - - Parameters - ---------- - %s - - %s - - Example - ------- - >>> s = hs.datasets.artificial_data.get_low_loss_eels_signal() - >>> s.plot() - - See also - -------- - artificial_low_loss_line_scan_signal : :py:class:`~hyperspy._signals.eels.EELSSpectrum` - - - """ - - random_state = check_random_state(random_state) - - x = np.arange(-100, 400, 0.5) - zero_loss = components1d.Gaussian(A=100, centre=4.1, sigma=1) - plasmon = components1d.Gaussian(A=100, centre=60, sigma=20) - - data_signal = zero_loss.function(x) - data_signal += plasmon.function(x) - data = np.zeros((12, len(x))) - for i in range(12): - data[i] += data_signal - if add_noise: - data[i] += random_state.uniform(size=len(x)) * 0.7 - - s = signals.EELSSpectrum(data) - s.axes_manager.signal_axes[0].offset = x[0] - s.axes_manager.signal_axes[0].scale = x[1] - x[0] - s.metadata.General.title = 'Artifical low loss EEL spectrum' - s.axes_manager.signal_axes[0].name = 'Electron energy loss' - s.axes_manager.signal_axes[0].units = 'eV' - s.axes_manager.navigation_axes[0].name = 'Probe position' - s.axes_manager.navigation_axes[0].units = 'nm' - s.set_microscope_parameters( - beam_energy=200, convergence_angle=26, collection_angle=20) - return s - -get_low_loss_eels_line_scan_signal.__doc__ %= (ADD_NOISE_DOCSTRING, - RETURNS_DOCSTRING) - - -def get_core_loss_eels_line_scan_signal(add_powerlaw=False, add_noise=True, random_state=None): - """Get an artificial core loss electron energy loss line scan spectrum. - - Similar to a Mn-L32 and Fe-L32 edge from a perovskite oxide. - - Parameters - ---------- - %s - %s - - %s - - Example - ------- - >>> s = hs.datasets.artificial_data.get_core_loss_eels_line_scan_signal() - >>> s.plot() - - See also - -------- - get_low_loss_eels_line_scan_signal, get_core_loss_eels_model - - """ - - random_state = check_random_state(random_state) - - x = np.arange(400, 800, 1) - arctan_mn = components1d.EELSArctan(A=1, k=0.2, x0=688) - arctan_fe = components1d.EELSArctan(A=1, k=0.2, x0=612) - mn_l3_g = components1d.Gaussian(A=100, centre=695, sigma=4) - mn_l2_g = components1d.Gaussian(A=20, centre=720, sigma=4) - fe_l3_g = components1d.Gaussian(A=100, centre=605, sigma=4) - fe_l2_g = components1d.Gaussian(A=10, centre=630, sigma=3) - - mn_intensity = [1, 1, 1, 1, 1, 1, 0.8, 0.5, 0.2, 0, 0, 0] - fe_intensity = [0, 0, 0, 0, 0, 0, 0.2, 0.5, 0.8, 1, 1, 1] - data = np.zeros((len(mn_intensity), len(x))) - for i in range(len(mn_intensity)): - data[i] += arctan_mn.function(x) * mn_intensity[i] - data[i] += mn_l3_g.function(x) * mn_intensity[i] - data[i] += mn_l2_g.function(x) * mn_intensity[i] - data[i] += arctan_fe.function(x) * fe_intensity[i] - data[i] += fe_l3_g.function(x) * fe_intensity[i] - data[i] += fe_l2_g.function(x) * fe_intensity[i] - if add_noise: - data[i] += random_state.uniform(size=len(x)) * 0.7 - - if add_powerlaw: - powerlaw = components1d.PowerLaw(A=10e8, r=3, origin=0) - data += powerlaw.function_nd(np.stack([x]*len(mn_intensity))) - - if add_powerlaw: - powerlaw = components1d.PowerLaw(A=10e8, r=3, origin=0) - data += powerlaw.function(x) - - s = signals.EELSSpectrum(data) - s.axes_manager.signal_axes[0].offset = x[0] - s.metadata.General.title = 'Artifical core loss EEL spectrum' - s.axes_manager.signal_axes[0].name = 'Electron energy loss' - s.axes_manager.signal_axes[0].units = 'eV' - s.axes_manager.navigation_axes[0].name = 'Probe position' - s.axes_manager.navigation_axes[0].units = 'nm' - s.set_microscope_parameters( - beam_energy=200, convergence_angle=26, collection_angle=20) - return s - -get_core_loss_eels_line_scan_signal.__doc__ %= (ADD_POWERLAW_DOCSTRING, - ADD_NOISE_DOCSTRING, - RETURNS_DOCSTRING) - - -def get_core_loss_eels_model(add_powerlaw=False, add_noise=True, random_state=None): - """Get an artificial core loss electron energy loss model. - - Similar to a Mn-L32 edge from a perovskite oxide. - - Parameters - ---------- - %s - %s - - Returns - ------- - :py:class:`~hyperspy.models.eelsmodel.EELSModel` - - Example - ------- - >>> import hs.datasets.artifical_data as ad - >>> s = ad.get_core_loss_eels_model() - >>> s.plot() - - With the powerlaw background - - >>> s = ad.get_core_loss_eels_model(add_powerlaw=True) - >>> s.plot() - - See also - -------- - get_core_loss_eels_signal - - """ - s = get_core_loss_eels_signal(add_powerlaw=add_powerlaw, - add_noise=add_noise, - random_state=random_state) - m = s.create_model(auto_background=False, GOS='hydrogenic') - return m - -get_core_loss_eels_model.__doc__ %= (ADD_POWERLAW_DOCSTRING, - ADD_NOISE_DOCSTRING) - - -def get_atomic_resolution_tem_signal2d(): - """Get an artificial atomic resolution TEM Signal2D. - - Returns - ------- - :py:class:`~hyperspy._signals.signal2d.Signal2D` - - Example - ------- - >>> s = hs.datasets.artificial_data.get_atomic_resolution_tem_signal2d() - >>> s.plot() - - """ - sX, sY = 2, 2 - x_array, y_array = np.mgrid[0:200, 0:200] - image = np.zeros_like(x_array, dtype=np.float32) - gaussian2d = components2d.Gaussian2D(sigma_x=sX, sigma_y=sY) - for x in range(10, 195, 20): - for y in range(10, 195, 20): - gaussian2d.centre_x.value = x - gaussian2d.centre_y.value = y - image += gaussian2d.function(x_array, y_array) - - s = signals.Signal2D(image) - return s - - -def get_luminescence_signal(navigation_dimension=0, - uniform=False, - add_baseline=False, - add_noise=True, - random_state=None): - """Get an artificial luminescence signal in wavelength scale (nm, uniform) or - energy scale (eV, non-uniform), simulating luminescence data recorded with a - diffracting spectrometer. Some random noise is also added to the spectrum, - to simulate experimental noise. - - Parameters - ---------- - navigation_dimension: positive int. - The navigation dimension(s) of the signal. 0 = single spectrum, - 1 = linescan, 2 = spectral map etc... - uniform: bool. - return uniform (wavelength) or non-uniform (energy) spectrum - add_baseline : bool - If true, adds a constant baseline to the spectrum. Conversion to - energy representation will turn the constant baseline into inverse - powerlaw. - %s - random_state: None or int - initialise state of the random number generator - - Example - ------- - >>> import hyperspy.datasets.artificial_data as ad - >>> s = ad.get_luminescence_signal() - >>> s.plot() - - With constant baseline - - >>> s = ad.get_luminescence_signal(uniform=True, add_baseline=True) - >>> s.plot() - - To make the noise the same for multiple spectra, which can - be useful for testing fitting routines - - >>> s1 = ad.get_luminescence_signal(random_state=10) - >>> s2 = ad.get_luminescence_signal(random_state=10) - >>> (s1.data == s2.data).all() - True - - 2D map - - >>> s = ad.get_luminescence_signal(navigation_dimension=2) - >>> s.plot() - - See also - -------- - get_low_loss_eels_signal, - get_core_loss_eels_signal, - get_low_loss_eels_line_scan_signal, - get_core_loss_eels_line_scan_signal, - get_core_loss_eels_model, - get_atomic_resolution_tem_signal2d, - - """ - - #Initialisation of random number generator - random_state = check_random_state(random_state) - - #Creating a uniform data axis, roughly similar to Horiba iHR320 with a 150 mm-1 grating - nm_axis = UniformDataAxis( - index_in_array=None, - name="Wavelength", - units="nm", - navigate=False, - size=1024, - scale=0.54, - offset=222.495, - is_binned=False, - ) - - #Artificial luminescence peak - gaussian_peak = components1d.Gaussian(A=5000, centre=375, sigma=25) - - if navigation_dimension>=0: - #Generate empty data (ones) - data = np.ones([10 for i in range(navigation_dimension)]+[nm_axis.size]) - #Generate spatial axes - spaxes = [UniformDataAxis(index_in_array=None, - name="X{:d}".format(i), - units="um", - navigate=False, - size=10, - scale=2.1, - offset=0, - is_binned=False, - ) for i in range(navigation_dimension)] - #Generate empty signal - sig = signals.Signal1D(data,axes = spaxes + [nm_axis]) - sig.metadata.General.title = '{:d}d-map Artificial Luminescence Signal'\ - .format(navigation_dimension) - else: - raise ValueError("Value {:d} invalid as navigation dimension.".format(\ - navigation_dimension)) - - #Populating data array, possibly with noise and baseline - sig.data *= gaussian_peak.function(nm_axis.axis) - if add_noise: - sig.data += (random_state.uniform(size=sig.data.shape) - 0.5)*1.4 - if add_baseline: - data += 350. - - #if not uniform, transformation into non-uniform axis - if not uniform: - hc = 1239.84198 #nm/eV - #converting to non-uniform axis - sig.axes_manager.signal_axes[0].convert_to_functional_data_axis(\ - expression="a/x", - name='Energy', - units='eV', - a=hc, - ) - #Reverting the orientation of signal axis to have increasing Energy - sig = sig.isig[::-1] - #Jacobian transformation - Eax = sig.axes_manager.signal_axes[0].axis - sig *= hc/Eax**2 - return sig - -get_luminescence_signal.__doc__ %= (ADD_NOISE_DOCSTRING) diff --git a/hyperspy/datasets/example_signals.py b/hyperspy/datasets/example_signals.py deleted file mode 100644 index 1752d09a4d..0000000000 --- a/hyperspy/datasets/example_signals.py +++ /dev/null @@ -1,10 +0,0 @@ - -from hyperspy.misc.example_signals_loading import load_1D_EDS_SEM_spectrum as\ - EDS_SEM_Spectrum - -from hyperspy.misc.example_signals_loading import load_1D_EDS_TEM_spectrum as\ - EDS_TEM_Spectrum - -from hyperspy.misc.example_signals_loading import load_object_hologram as object_hologram - -from hyperspy.misc.example_signals_loading import load_reference_hologram as reference_hologram diff --git a/hyperspy/decorators.py b/hyperspy/decorators.py index 6ad92176f9..1e7065f070 100644 --- a/hyperspy/decorators.py +++ b/hyperspy/decorators.py @@ -1,32 +1,40 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import inspect +import logging +import warnings from functools import wraps +from typing import Callable, Optional, Union + +from hyperspy.exceptions import VisibleDeprecationWarning + +_logger = logging.getLogger(__name__) def lazify(func, **kwargs): - from hyperspy.signal import BaseSignal from hyperspy.model import BaseModel + from hyperspy.signal import BaseSignal @wraps(func) def lazified_func(self, *args, **kwds): for k in self.__dict__.keys(): - if not k.startswith('__'): + if not k.startswith("__"): v = getattr(self, k) if isinstance(v, BaseSignal): v = v.as_lazy() @@ -40,6 +48,7 @@ def lazified_func(self, *args, **kwds): v.signal.axes_manager = am self.__dict__.update(kwargs) return func(self, *args, **kwds) + return lazified_func @@ -48,16 +57,16 @@ def lazifyTest(original_class): original_class.lazify = lazify thelist = [k for k in original_class.__dict__.keys()] for thing in thelist: - if thing.startswith('test'): - if not thing.startswith('test_lazy'): - newname = 'test_lazy' + thing[4:] + if thing.startswith("test"): + if not thing.startswith("test_lazy"): + newname = "test_lazy" + thing[4:] if newname not in thelist: - newfunc = lazify(getattr(original_class, thing), - **kwargs) + newfunc = lazify(getattr(original_class, thing), **kwargs) newfunc.__name__ = newname setattr(original_class, newname, newfunc) return original_class + if len(args): return lazifyTest(*args) else: @@ -77,12 +86,14 @@ def simple_decorator(decorator): This decorator was taken from: http://wiki.python.org/moin/PythonDecoratorLibrary""" + def new_decorator(f): g = decorator(f) g.__name__ = f.__name__ g.__doc__ = f.__doc__ g.__dict__.update(f.__dict__) return g + # Now a few lines needed to make simple_decorator itself # be a well-behaved decorator. new_decorator.__name__ = decorator.__name__ @@ -93,16 +104,163 @@ def new_decorator(f): @simple_decorator def interactive_range_selector(cm): - from hyperspy.ui_registry import get_gui from hyperspy.signal_tools import Signal1DRangeSelector + from hyperspy.ui_registry import get_gui def wrapper(self, *args, **kwargs): if not args and not kwargs: range_selector = Signal1DRangeSelector(self) range_selector.on_close.append((cm, self)) - get_gui( - range_selector, - toolkey="hyperspy.interactive_range_selector") + get_gui(range_selector, toolkey="hyperspy.interactive_range_selector") else: cm(self, *args, **kwargs) + return wrapper + + +class deprecated: + """Decorator to mark deprecated functions with an informative + warning. + + Inspired by + `scikit-image + `_ + and `matplotlib + `_. + """ + + def __init__( + self, + since: Union[str, int, float], + alternative: Optional[str] = None, + alternative_is_function: bool = True, + removal: Union[str, int, float, None] = None, + ): + """Visible deprecation warning. + + Parameters + ---------- + since + The release at which this API became deprecated. + alternative + An alternative API that the user may use in place of the + deprecated API. + alternative_is_function + Whether the alternative is a function. Default is ``True``. + removal + The expected removal version. + """ + self.since = since + self.alternative = alternative + self.alternative_is_function = alternative_is_function + self.removal = removal + + def __call__(self, func: Callable): + # Wrap function to raise warning when called, and add warning to + # docstring + if self.alternative is not None: + if self.alternative_is_function: + alt_msg = f" Use `{self.alternative}()` instead." + else: + alt_msg = f" Use `{self.alternative}` instead." + else: + alt_msg = "" + if self.removal is not None: + rm_msg = f" and will be removed in version {self.removal}" + else: + rm_msg = "" + msg = f"Function `{func.__name__}()` is deprecated{rm_msg}.{alt_msg}" + + @wraps(func) + def wrapped(*args, **kwargs): + warnings.simplefilter( + action="always", + category=VisibleDeprecationWarning, + append=True, + ) + func_code = func.__code__ + warnings.warn_explicit( + message=msg, + category=VisibleDeprecationWarning, + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1, + ) + return func(*args, **kwargs) + + # Modify docstring to display deprecation warning + old_doc = inspect.cleandoc(func.__doc__ or "").strip("\n") + notes_header = "\nNotes\n-----" + new_doc = ( + f"[*Deprecated*] {old_doc}\n" + f"{notes_header if notes_header not in old_doc else ''}\n" + f".. deprecated:: {self.since}\n" + f" {msg.strip()}" # Matplotlib uses three spaces + ) + wrapped.__doc__ = new_doc + + return wrapped + + +class deprecated_argument: + """Decorator to remove an argument from a function or method's + signature. + + Adapted from `scikit-image + `_. + """ + + def __init__(self, name, since, removal, alternative=None): + self.name = name + self.since = since + self.removal = removal + self.alternative = alternative + + def __call__(self, func): + @wraps(func) + def wrapped(*args, **kwargs): + if self.name in kwargs.keys(): + msg = ( + f"Argument `{self.name}` is deprecated and will be removed in " + f"version {self.removal}. To avoid this warning, please do not use " + f"`{self.name}`. " + ) + if self.alternative is not None: + msg += f"Use `{self.alternative}` instead. " + kwargs[self.alternative] = kwargs.pop( + self.name + ) # replace with alternative kwarg + msg += f"See the documentation of `{func.__name__}()` for more details." + warnings.simplefilter( + action="always", category=VisibleDeprecationWarning + ) + func_code = func.__code__ + warnings.warn_explicit( + message=msg, + category=VisibleDeprecationWarning, + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1, + ) + return func(*args, **kwargs) + + return wrapped + + +def jit_ifnumba(*args, **kwargs): + try: + import numba + + if "nopython" not in kwargs: + kwargs["nopython"] = True + return numba.jit(*args, **kwargs) + except ImportError: + _logger.warning( + "Numba is not installed, falling back to " "non-accelerated implementation." + ) + + def wrap1(func): + def wrap2(*args2, **kwargs2): + return func(*args2, **kwargs2) + + return wrap2 + + return wrap1 diff --git a/hyperspy/defaults_parser.py b/hyperspy/defaults_parser.py index a5dc5aa512..b08e5edc0e 100644 --- a/hyperspy/defaults_parser.py +++ b/hyperspy/defaults_parser.py @@ -1,73 +1,51 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import os import configparser import logging +import os +from pathlib import Path import traits.api as t -import matplotlib.pyplot as plt - -from pathlib import Path -from hyperspy.misc.config_dir import config_path, data_path -from hyperspy.misc.ipython_tools import turn_logging_on, turn_logging_off +from hyperspy.misc.ipython_tools import turn_logging_off, turn_logging_on from hyperspy.ui_registry import add_gui_method -defaults_file = Path(config_path, 'hyperspyrc') -eels_gos_files = Path(data_path, 'EELS_GOS.tar.gz') +config_path = Path("~/.hyperspy").expanduser() +config_path.mkdir(parents=True, exist_ok=True) +defaults_file = Path(config_path, "hyperspyrc") _logger = logging.getLogger(__name__) -def guess_gos_path(): - if os.name in ["nt", "dos"]: - # If DM is installed, use the GOS tables from the default - # installation - # location in windows - program_files = os.environ['PROGRAMFILES'] - gos = 'Gatan\\DigitalMicrograph\\EELS Reference Data\\H-S GOS Tables' - gos_path = Path(program_files, gos) - - # Else, use the default location in the .hyperspy forlder - if not gos_path.is_dir() and 'PROGRAMFILES(X86)' in os.environ: - program_files = os.environ['PROGRAMFILES(X86)'] - gos_path = Path(program_files, gos) - if not gos_path.is_dir(): - gos_path = Path(config_path, 'EELS_GOS') - else: - gos_path = Path(config_path, 'EELS_GOS') - return gos_path - - if defaults_file.is_file(): # Remove config file if obsolated with open(defaults_file) as f: - if 'Not really' in f.readline(): - # It is the old config file + if "Not really" in f.readline(): + # It is the old config file defaults_file_exists = False else: defaults_file_exists = True if not defaults_file_exists: # It actually exists, but is an obsoleted unsupported version of it # so we delete it. - _logger.info('Removing obsoleted config file') + _logger.info("Removing obsoleted config file") os.remove(defaults_file) else: defaults_file_exists = False @@ -83,34 +61,38 @@ def guess_gos_path(): class GeneralConfig(t.HasTraits): logger_on = t.CBool( False, - label='Automatic logging (requires IPython)', - desc='If enabled, HyperSpy will store a log in the current directory ' - 'of all the commands typed') + label="Automatic logging (requires IPython)", + desc="If enabled, HyperSpy will store a log in the current directory " + "of all the commands typed", + ) show_progressbar = t.CBool( True, - label='Show progress bar', - desc='If enabled, show a progress bar when available') + label="Show progress bar", + desc="If enabled, show a progress bar when available", + ) dtb_expand_structures = t.CBool( True, - label='Expand structures in DictionaryTreeBrowser', - desc='If enabled, when printing DictionaryTreeBrowser (e.g. ' - 'metadata), long lists and tuples will be expanded and any ' - 'dictionaries in them will be printed similar to ' - 'DictionaryTreeBrowser, but with double lines') - logging_level = t.Enum(['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', ], - desc='the log level of all hyperspy modules.') - parallel = t.CBool( - True, - desc='Use parallel threads for computations by default.' + label="Expand structures in DictionaryTreeBrowser", + desc="If enabled, when printing DictionaryTreeBrowser (e.g. " + "metadata), long lists and tuples will be expanded and any " + "dictionaries in them will be printed similar to " + "DictionaryTreeBrowser, but with double lines", ) - - nb_progressbar = t.CBool( - True, - desc='Attempt to use ipywidgets progressbar' + logging_level = t.Enum( + [ + "CRITICAL", + "ERROR", + "WARNING", + "INFO", + "DEBUG", + ], + desc="the log level of all hyperspy modules.", ) + nb_progressbar = t.CBool(True, desc="Attempt to use ipywidgets progressbar") + def _logger_on_changed(self, old, new): if new is True: turn_logging_on() @@ -118,100 +100,105 @@ def _logger_on_changed(self, old, new): turn_logging_off() -class EELSConfig(t.HasTraits): - eels_gos_files_path = t.Directory( - guess_gos_path(), - label='GOS directory', - desc='The GOS files are required to create the EELS edge components') - - class GUIs(t.HasTraits): enable_ipywidgets_gui = t.CBool( True, desc="Display ipywidgets in the Jupyter Notebook. " - "Requires installing hyperspy_gui_ipywidgets.") + "Requires installing hyperspy_gui_ipywidgets.", + ) enable_traitsui_gui = t.CBool( True, desc="Display traitsui user interface elements. " - "Requires installing hyperspy_gui_traitsui.") - warn_if_guis_are_missing = t.CBool( - True, - desc="Display warnings, if hyperspy_gui_ipywidgets or hyperspy_gui_traitsui are missing.") + "Requires installing hyperspy_gui_traitsui.", + ) class PlotConfig(t.HasTraits): - saturated_pixels = t.CFloat(0., - label='Saturated pixels (deprecated)', - desc='Warning: this is deprecated and will be removed in HyperSpy v2.0' - ) - cmap_navigator = t.Enum(plt.colormaps(), - label='Color map navigator', - desc='Set the default color map for the navigator.', - ) - cmap_signal = t.Enum(plt.colormaps(), - label='Color map signal', - desc='Set the default color map for the signal plot.', - ) - dims_024_increase = t.Str('right', - label='Navigate right' - ) - dims_024_decrease = t.Str('left', - label='Navigate left', - ) - dims_135_increase = t.Str('down', - label='Navigate down', - ) - dims_135_decrease = t.Str('up', - label='Navigate up', - ) - modifier_dims_01 = t.Enum(['ctrl', 'alt', 'shift', 'ctrl+alt', 'ctrl+shift', 'alt+shift', - 'ctrl+alt+shift'], label='Modifier key for 1st and 2nd dimensions') # 0 elem is default - modifier_dims_23 = t.Enum(['shift', 'alt', 'ctrl', 'ctrl+alt', 'ctrl+shift', 'alt+shift', - 'ctrl+alt+shift'], label='Modifier key for 3rd and 4th dimensions') # 0 elem is default - modifier_dims_45 = t.Enum(['alt', 'ctrl', 'shift', 'ctrl+alt', 'ctrl+shift', 'alt+shift', - 'ctrl+alt+shift'], label='Modifier key for 5th and 6th dimensions') # 0 elem is default - pick_tolerance = t.CFloat(7.5, - label='Pick tolerance', - desc='The pick tolerance of ROIs in screen pixels.' - ) - - -class EDSConfig(t.HasTraits): - eds_mn_ka = t.CFloat(130., - label='Energy resolution at Mn Ka (eV)', - desc='default value for FWHM of the Mn Ka peak in eV,' - 'This value is used as a first approximation' - 'of the energy resolution of the detector.') - eds_tilt_stage = t.CFloat( - 0., - label='Stage tilt', - desc='default value for the stage tilt in degree.') - eds_detector_azimuth = t.CFloat( - 0., - label='Azimuth angle', - desc='default value for the azimuth angle in degree. If the azimuth' - ' is zero, the detector is perpendicular to the tilt axis.') - eds_detector_elevation = t.CFloat( - 35., - label='Elevation angle', - desc='default value for the elevation angle in degree.') + # Don't use t.Enum to list all possible matplotlib colormap to + # avoid importing matplotlib and building the list of colormap + # when importing hyperpsy + widget_plot_style = t.Enum( + ["horizontal", "vertical"], label="Widget plot style: (only with ipympl)" + ) + use_subfigure = t.CBool( + False, + desc="EXPERIMENTAL. Plot navigator and signal on the same figure. " + "Note that this is slower than using separate figures " + "and it requires matplotlib >=3.9.", + ) + cmap_navigator = t.Str( + "gray", + label="Color map navigator", + desc="Set the default color map for the navigator.", + ) + cmap_signal = t.Str( + "gray", + label="Color map signal", + desc="Set the default color map for the signal plot.", + ) + dims_024_increase = t.Str("right", label="Navigate right") + dims_024_decrease = t.Str( + "left", + label="Navigate left", + ) + dims_135_increase = t.Str( + "down", + label="Navigate down", + ) + dims_135_decrease = t.Str( + "up", + label="Navigate up", + ) + modifier_dims_01 = t.Enum( + [ + "ctrl", + "alt", + "shift", + "ctrl+alt", + "ctrl+shift", + "alt+shift", + "ctrl+alt+shift", + ], + label="Modifier key for 1st and 2nd dimensions", + ) # 0 elem is default + modifier_dims_23 = t.Enum( + [ + "shift", + "alt", + "ctrl", + "ctrl+alt", + "ctrl+shift", + "alt+shift", + "ctrl+alt+shift", + ], + label="Modifier key for 3rd and 4th dimensions", + ) # 0 elem is default + modifier_dims_45 = t.Enum( + [ + "alt", + "ctrl", + "shift", + "ctrl+alt", + "ctrl+shift", + "alt+shift", + "ctrl+alt+shift", + ], + label="Modifier key for 5th and 6th dimensions", + ) # 0 elem is default + pick_tolerance = t.CFloat( + 7.5, label="Pick tolerance", desc="The pick tolerance of ROIs in screen pixels." + ) template = { - 'General': GeneralConfig(), - 'GUIs': GUIs(), - 'EELS': EELSConfig(), - 'EDS': EDSConfig(), - 'Plot': PlotConfig(), + "General": GeneralConfig(), + "GUIs": GUIs(), + "Plot": PlotConfig(), } # Set the enums defaults -template['General'].logging_level = 'WARNING' -template['Plot'].cmap_navigator = 'gray' -template['Plot'].cmap_signal = 'gray' - - +template["General"].logging_level = "WARNING" # Defaults template definition ends ###################################### @@ -226,12 +213,10 @@ def config2template(template, config): for section, traited_class in template.items(): config_dict = {} for name, value in config.items(section): - if value == 'True': + if value == "True": value = True - elif value == 'False': + elif value == "False": value = False - if name == 'fine_structure_smoothing': - value = float(value) config_dict[name] = value traited_class.trait_set(True, **config_dict) @@ -264,7 +249,7 @@ def dictionary_from_template(template): rewrite = True if not defaults_file_exists or rewrite is True: - _logger.info('Writing the config file') + _logger.info("Writing the config file") with open(defaults_file, "w") as df: config.write(df) @@ -274,8 +259,6 @@ def dictionary_from_template(template): @add_gui_method(toolkey="hyperspy.Preferences") class Preferences(t.HasTraits): - EELS = t.Instance(EELSConfig) - EDS = t.Instance(EDSConfig) General = t.Instance(GeneralConfig) GUIs = t.Instance(GUIs) Plot = t.Instance(PlotConfig) @@ -283,24 +266,23 @@ class Preferences(t.HasTraits): def save(self): config = configparser.ConfigParser(allow_no_value=True) template2config(template, config) - config.write(open(defaults_file, 'w')) + config.write(open(defaults_file, "w")) preferences = Preferences( - EELS=template['EELS'], - EDS=template['EDS'], - General=template['General'], - GUIs=template['GUIs'], - Plot=template['Plot'], + General=template["General"], + GUIs=template["GUIs"], + Plot=template["Plot"], ) + if preferences.General.logger_on: turn_logging_on(verbose=0) def file_version(fname): - with open(fname, 'r') as f: - for l in f.readlines(): - if '__version__' in l: - return l[l.find('=') + 1:].strip() - return '0' + with open(fname, "r") as f: + for line in f.readlines(): + if "__version__" in line: + return line[line.find("=") + 1 :].strip() + return "0" diff --git a/hyperspy/docstrings/__init__.py b/hyperspy/docstrings/__init__.py index b38e5d8603..5d3be5a8cc 100644 --- a/hyperspy/docstrings/__init__.py +++ b/hyperspy/docstrings/__init__.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- -"""Common docstring snippets. +"""Common docstring snippets.""" -""" - -START_HSPY = \ - """When starting HyperSpy using the ``hyperspy`` script (e.g. by executing +START_HSPY = """When starting HyperSpy using the ``hyperspy`` script (e.g. by executing ``hyperspy`` in a console, using the context menu entries or using the links in the ``Start Menu``, the :mod:`~hyperspy.api` package is imported in the user namespace as ``hs``, i.e. by executing the following: @@ -15,5 +12,5 @@ (Note that code snippets are indicated by three greater-than signs) We recommend to import the HyperSpy API as above also when doing it manually. -The docstring examples assume that `hyperspy` has been imported as `hs`, -numpy as ``np`` and ``matplotlib.pyplot`` as ``plt``. """ +The docstring examples assume that ``hyperspy.api`` has been imported as ``hs``, +``numpy`` as ``np`` and ``matplotlib.pyplot`` as ``plt``. """ diff --git a/hyperspy/docstrings/markers.py b/hyperspy/docstrings/markers.py new file mode 100644 index 0000000000..c3198a1d6a --- /dev/null +++ b/hyperspy/docstrings/markers.py @@ -0,0 +1,28 @@ +"""Common docstrings to Markers""" + +OFFSET_DOCSTRING = """offsets : array-like + The positions [x, y] of the center of the marker. If the offsets are + not provided, the marker will be placed at the current navigation + position. + """ +WIDTHS_DOCSTRING = """widths: array-like + The lengths of the first axes (e.g., major axis lengths). + """ + +HEIGHTS_DOCSTRING = """heights: array-like + The lengths of the second axes. + """ + +ANGLES_DOCSTRING = """angles : array-like + The angles of the first axes, degrees CCW from the x-axis. + """ + +UNITS_DOCSTRING = """units : {``"points"``, ``"inches"``, ``"dots"``, ``"width"``", ``"height"``, ``"x"``, ``"y"``, ``"xy"``} + The units in which majors and minors are given; ``"width"`` and + ``"height"`` refer to the dimensions of the axes, while ``"x"`` and ``"y"`` + refer to the *offsets* data units. ``"xy"`` differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the :class:`matplotlib.patches.Ellipse` with + ``axes.transData`` as its transform. + """ diff --git a/hyperspy/docstrings/model.py b/hyperspy/docstrings/model.py index 899c663df6..c9dbb49893 100644 --- a/hyperspy/docstrings/model.py +++ b/hyperspy/docstrings/model.py @@ -1,82 +1,98 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -"""Common docstring snippets for model. +"""Common docstring snippets for model.""" -""" +# Used by exSpy FIT_PARAMETERS_ARG = """optimizer : str or None, default None The optimization algorithm used to perform the fitting. - * "lm" performs least-squares optimization using the - Levenberg-Marquardt algorithm, and supports bounds - on parameters. - * "trf" performs least-squares optimization using the - Trust Region Reflective algorithm, and supports - bounds on parameters. - * "dogbox" performs least-squares optimization using the - dogleg algorithm with rectangular trust regions, and - supports bounds on parameters. - * "odr" performs the optimization using the orthogonal - distance regression (ODR) algorithm. It does not support - bounds on parameters. See :py:mod:`scipy.odr` for more details. - * All of the available methods for :py:func:`scipy.optimize.minimize` - can be used here. See the :ref:`User Guide ` - documentation for more details. - * "Differential Evolution" is a global optimization method. - It does support bounds on parameters. See - :py:func:`scipy.optimize.differential_evolution` for more - details on available options. - * "Dual Annealing" is a global optimization method. - It does support bounds on parameters. See - :py:func:`scipy.optimize.dual_annealing` for more - details on available options. Requires ``scipy >= 1.2.0``. - * "SHGO" (simplicial homology global optimization" is a global - optimization method. It does support bounds on parameters. See - :py:func:`scipy.optimize.shgo` for more details on available - options. Requires ``scipy >= 1.2.0``. + * Non-linear optimizers: - loss_function : {"ls", "ML-poisson", "huber", callable}, default "ls" + * ``"lm"`` performs least-squares optimization using the + Levenberg-Marquardt algorithm, and supports bounds + on parameters. + * ``"trf"`` performs least-squares optimization using the + Trust Region Reflective algorithm, and supports + bounds on parameters. + * ``"dogbox"`` performs least-squares optimization using the + dogleg algorithm with rectangular trust regions, and + supports bounds on parameters. + * ``"odr"`` performs the optimization using the orthogonal + distance regression (ODR) algorithm. It does not support + bounds on parameters. See :mod:`scipy.odr` for more details. + * All of the available methods for :func:`scipy.optimize.minimize` + can be used here. See the :ref:`User Guide ` + documentation for more details. + * ``"Differential Evolution"`` is a global optimization method. + It does support bounds on parameters. See + :func:`scipy.optimize.differential_evolution` for more + details on available options. + * ``"Dual Annealing"`` is a global optimization method. + It does support bounds on parameters. See + :func:`scipy.optimize.dual_annealing` for more + details on available options. Requires ``scipy >= 1.2.0``. + * ``"SHGO"`` (simplicial homology global optimization) is a global + optimization method. It does support bounds on parameters. See + :func:`scipy.optimize.shgo` for more details on available + options. Requires ``scipy >= 1.2.0``. + + * Linear optimizers: + + * ``"lstsq"`` - least square using :func:`numpy.linalg.lstsq`. + * ``"ols"`` - Ordinary least square using + :class:`sklearn.linear_model.LinearRegression` + * ``"nnls"`` - Linear regression with positive constraints on the + regression coefficients using + :class:`sklearn.linear_model.LinearRegression` + * ``"ridge"`` - least square supporting regularisation using + :class:`sklearn.linear_model.Ridge`. The parameter + ``alpha`` (default set to 0.01) controlling regularization + strength can be passed as keyword argument, see + :class:`sklearn.linear_model.Ridge` for more information. + + loss_function : {``"ls"``, ``"ML-poisson"``, ``"huber"``, callable}, default ``"ls"`` The loss function to use for minimization. Only ``"ls"`` is available - if ``optimizer`` is one of ``["lm", "trf", "dogbox", "odr"]``. + if ``optimizer`` is one of ``"lm"``, ``"trf"``, ``"dogbox"`` or ``"odr"``. - * "ls" minimizes the least-squares loss function. - * "ML-poisson" minimizes the negative log-likelihood for - Poisson-distributed data. Also known as Poisson maximum - likelihood estimation (MLE). - * "huber" minimize the Huber loss function. The delta value - of the Huber function is controlled by the ``huber_delta`` - keyword argument (the default value is 1.0). - * callable supports passing your own minimization function. + * ``"ls"`` minimizes the least-squares loss function. + * ``"ML-poisson"`` minimizes the negative log-likelihood for + Poisson-distributed data. Also known as Poisson maximum + likelihood estimation (MLE). + * ``"huber"`` minimize the Huber loss function. The delta value + of the Huber function is controlled by the ``huber_delta`` + keyword argument (the default value is 1.0). + * callable supports passing your own minimization function. - grad : {"fd", "analytical", callable, None}, default "fd" + grad : {``"fd"``, ``"analytical"``, callable, None}, default ``"fd"`` Whether to use information about the gradient of the loss function as part of the optimization. This parameter has no effect if ``optimizer`` is a derivative-free or global optimization method. - * "fd" uses a finite difference scheme (if available) for numerical - estimation of the gradient. The scheme can be further controlled - with the ``fd_scheme`` keyword argument. - * "analytical" uses the analytical gradient (if available) to speed - up the optimization, since the gradient does not need to be estimated. - * callable should be a function that returns the gradient vector. - * None means that no gradient information is used or estimated. Not - available if ``optimizer`` is in ``["lm", "trf", ``"dogbox"``]. + * ``"fd"`` uses a finite difference scheme (if available) for numerical + estimation of the gradient. The scheme can be further controlled + with the ``fd_scheme`` keyword argument. + * ``"analytical"`` uses the analytical gradient (if available) to speed + up the optimization, since the gradient does not need to be estimated. + * callable should be a function that returns the gradient vector. + * None means that no gradient information is used or estimated. Not + available if ``optimizer`` is one of ``"lm"``, ``"trf"`` or ``"dogbox"``. bounded : bool, default False If True, performs bounded parameter optimization if @@ -88,15 +104,16 @@ print_info : bool, default False If True, print information about the fitting results, which are also stored in ``model.fit_output`` in the form of - a :py:class:`scipy.optimize.OptimizeResult` object. + a :class:`scipy.optimize.OptimizeResult` object. return_info : bool, default True If True, returns the fitting results in the form of - a :py:class:`scipy.optimize.OptimizeResult` object. - fd_scheme : str {"2-point", "3-point", "cs"}, default "2-point" + a :class:`scipy.optimize.OptimizeResult` object. + fd_scheme : str {``"2-point"``, ``"3-point"``, ``"cs"``}, default ``"2-point"`` If ``grad='fd'``, selects the finite difference scheme to use. - See :py:func:`scipy.optimize.minimize` for details. Ignored if - ``optimizer`` is ``"lm"``, ``"trf"`` or ``"dogbox"``. - **kwargs : keyword arguments + See :func:`scipy.optimize.minimize` for details. Ignored if + ``optimizer`` is one of ``"lm"``, ``"trf"`` or ``"dogbox"``. + **kwargs : dict Any extra keyword argument will be passed to the chosen optimizer. For more information, read the docstring of the - optimizer of your choice in :py:mod:`scipy.optimize`.""" + optimizer of your choice in :mod:`scipy.optimize`. + """ diff --git a/hyperspy/docstrings/parameters.py b/hyperspy/docstrings/parameters.py index 1eb32254e9..7fb07ac4fd 100644 --- a/hyperspy/docstrings/parameters.py +++ b/hyperspy/docstrings/parameters.py @@ -1,27 +1,24 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -"""Common docstring snippets for parameters. +"""Common docstring snippets for parameters.""" -""" - -FUNCTION_ND_DOCSTRING = \ - """Returns a numpy array containing the value of the component for all +FUNCTION_ND_DOCSTRING = """Returns a numpy array containing the value of the component for all indices. If enough memory is available, this is useful to quickly to obtain the fitted component without iterating over the navigation axes. """ diff --git a/hyperspy/docstrings/plot.py b/hyperspy/docstrings/plot.py index 52b3c14e2b..1633752e48 100644 --- a/hyperspy/docstrings/plot.py +++ b/hyperspy/docstrings/plot.py @@ -1,88 +1,83 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -"""Common docstring snippets for plot. - -""" - - -BASE_PLOT_DOCSTRING_PARAMETERS = \ - """navigator : str, None, or :py:class:`~hyperspy.signal.BaseSignal` (or subclass). \ - Allowed string values are ``'auto'``, ``'slider'``, and ``'spectrum'``. - - If ``'auto'``: - - - If `navigation_dimension` > 0, a navigator is - provided to explore the data. - - If `navigation_dimension` is 1 and the signal is an image - the navigator is a sum spectrum obtained by integrating - over the signal axes (the image). - - If `navigation_dimension` is 1 and the signal is a spectrum - the navigator is an image obtained by stacking all the - spectra in the dataset horizontally. - - If `navigation_dimension` is > 1, the navigator is a sum - image obtained by integrating the data over the signal axes. - - Additionally, if `navigation_dimension` > 2, a window - with one slider per axis is raised to navigate the data. - - For example, if the dataset consists of 3 navigation axes `X`, - `Y`, `Z` and one signal axis, `E`, the default navigator will - be an image obtained by integrating the data over `E` at the - current `Z` index and a window with sliders for the `X`, `Y`, - and `Z` axes will be raised. Notice that changing the `Z`-axis - index changes the navigator in this case. - - For lazy signals, the navigator will be calculated using the - :py:func:`~hyperspy._signals.lazy.LazySignal.compute_navigator` - method. - - If ``'slider'``: - - - If `navigation dimension` > 0 a window with one slider per - axis is raised to navigate the data. - - If ``'spectrum'``: - - - If `navigation_dimension` > 0 the navigator is always a - spectrum obtained by integrating the data over all other axes. - - Not supported for lazy signals, the ``'auto'`` option will - be used instead. - - If ``None``, no navigator will be provided. - - Alternatively a :py:class:`~hyperspy.signal.BaseSignal` (or subclass) +# along with HyperSpy. If not, see . + +"""Common docstring snippets for plot.""" + +BASE_PLOT_DOCSTRING_PARAMETERS = """navigator : str, None, or :class:`~hyperspy.signal.BaseSignal` (or subclass). + Allowed string values are ``'auto'``, ``'slider'``, and ``'spectrum'``. + + - If ``'auto'``: + + - If ``navigation_dimension`` > 0, a navigator is + provided to explore the data. + - If ``navigation_dimension`` is 1 and the signal is an image + the navigator is a sum spectrum obtained by integrating + over the signal axes (the image). + - If ``navigation_dimension`` is 1 and the signal is a spectrum + the navigator is an image obtained by stacking all the + spectra in the dataset horizontally. + - If ``navigation_dimension`` is > 1, the navigator is a sum + image obtained by integrating the data over the signal axes. + - Additionally, if ``navigation_dimension`` > 2, a window + with one slider per axis is raised to navigate the data. + - For example, if the dataset consists of 3 navigation axes "X", + "Y", "Z" and one signal axis, "E", the default navigator will + be an image obtained by integrating the data over "E" at the + current "Z" index and a window with sliders for the "X", "Y", + and "Z" axes will be raised. Notice that changing the "Z"-axis + index changes the navigator in this case. + - For lazy signals, the navigator will be calculated using the + :func:`~hyperspy._signals.lazy.LazySignal.compute_navigator` + method. + + - If ``'slider'``: + + - If ``navigation dimension`` > 0 a window with one slider per + axis is raised to navigate the data. + + - If ``'spectrum'``: + + - If ``navigation_dimension`` > 0 the navigator is always a + spectrum obtained by integrating the data over all other axes. + - Not supported for lazy signals, the ``'auto'`` option will + be used instead. + + - If ``None``, no navigator will be provided. + + Alternatively a :class:`~hyperspy.api.signals.BaseSignal` (or subclass) instance can be provided. The navigation or signal shape must match the navigation shape of the signal to plot or the - `navigation_shape` + `signal_shape` must be equal to the - `navigator_shape` of the current object (for a dynamic navigator). - If the signal `dtype` is RGB or RGBA this parameter has no effect and + ``navigation_shape`` + ``signal_shape`` must be equal to the + ``navigator_shape`` of the current object (for a dynamic navigator). + If the signal ``dtype`` is RGB or RGBA this parameter has no effect and the value is always set to ``'slider'``. - axes_manager : None or :py:class:`~hyperspy.axes.AxesManager` - If None, the signal's `axes_manager` attribute is used. + axes_manager : None or :class:`~hyperspy.axes.AxesManager` + If None, the signal's ``axes_manager`` attribute is used. plot_markers : bool, default True - Plot markers added using s.add_marker(marker, permanent=True). + Plot markers added using `s.add_marker(marker, permanent=True)`. Note, a large number of markers might lead to very slow plotting. navigator_kwds : dict Only for image navigator, additional keyword arguments for - :py:func:`matplotlib.pyplot.imshow`. + :func:`matplotlib.pyplot.imshow`. """ -BASE_PLOT_DOCSTRING = \ - """Plot the signal at the current coordinates. +BASE_PLOT_DOCSTRING = """Plot the signal at the current coordinates. For multidimensional datasets an optional figure, the "navigator", with a cursor to navigate that data is @@ -95,67 +90,54 @@ """ -PLOT1D_DOCSTRING = \ - """norm : str, optional +PLOT1D_DOCSTRING = """norm : str, default ``'auto'`` The function used to normalize the data prior to plotting. Allowable strings are: ``'auto'``, ``'linear'``, ``'log'``. - (default value is ``'auto'``). If ``'auto'``, intensity is plotted on a linear scale except when ``power_spectrum=True`` (only for complex signals). autoscale : str - The string must contain any combination of the 'x' and 'v' - characters. If 'x' or 'v' (for values) are in the string, the + The string must contain any combination of the ``'x'`` and ``'v'`` + characters. If ``'x'`` or ``'v'`` (for values) are in the string, the corresponding horizontal or vertical axis limits are set to their maxima and the axis limits will reset when the data or the - navigation indices are changed. Default is 'v'. + navigation indices are changed. Default is ``'v'``. """ -PLOT2D_DOCSTRING = \ - """colorbar : bool, optional +PLOT2D_DOCSTRING = """colorbar : bool, optional If true, a colorbar is plotted for non-RGB images. - autoscale : str - The string must contain any combination of the 'x', 'y' and 'v' - characters. If 'x' or 'y' are in the string, the corresponding + autoscale : str, optional + The string must contain any combination of the ``'x'``, ``'y'`` and ``'v'`` + characters. If ``'x'`` or ``'y'`` are in the string, the corresponding axis limits are set to cover the full range of the data at a given - position. If 'v' (for values) is in the string, the contrast of the - image will be set automatically according to `vmin` and `vmax` when - the data or navigation indices change. Default is 'v'. - saturated_pixels : scalar - The percentage of pixels that are left out of the bounds. - For example, the low and high bounds of a value of 1 are the 0.5% - and 99.5% percentiles. It must be in the [0, 100] range. - If None (default value), the value from the preferences is used. - - .. deprecated:: 1.6.0 - `saturated_pixels` will be removed in HyperSpy 2.0.0, it is replaced - by `vmin`, `vmax` and `autoscale`. - - norm : {"auto", "linear", "power", "log", "symlog" or a subclass of :py:class:`matplotlib.colors.Normalise`} - Set the norm of the image to display. If "auto", a linear scale is - used except if when `power_spectrum=True` in case of complex data - type. "symlog" can be used to display negative value on a negative - scale - read :py:class:`matplotlib.colors.SymLogNorm` and the - `linthresh` and `linscale` parameter for more details. + position. If ``'v'`` (for values) is in the string, the contrast of the + image will be set automatically according to ``vmin` and ``vmax`` when + the data or navigation indices change. Default is ``'v'``. + norm : str {``"auto"` | ``"linear"`` | ``"power"`` | ``"log"`` | ``"symlog"``} or :class:`matplotlib.colors.Normalize` + Set the norm of the image to display. If ``"auto"``, a linear scale is + used except if when ``power_spectrum=True`` in case of complex data + type. ``"symlog"`` can be used to display negative value on a negative + scale - read :class:`matplotlib.colors.SymLogNorm` and the + ``linthresh`` and ``linscale`` parameter for more details. vmin, vmax : {scalar, str}, optional - `vmin` and `vmax` are used to normalise the displayed data. It can - be a float or a string. If string, it should be formatted as 'xth', - where 'x' must be an float in the [0, 100] range. 'x' is used to + ``vmin`` and ``vmax`` are used to normalise the displayed data. It can + be a float or a string. If string, it should be formatted as ``'xth'``, + where ``'x'`` must be an float in the [0, 100] range. ``'x'`` is used to compute the x-th percentile of the data. See - :py:func:`numpy.percentile` for more information. - gamma : float + :func:`numpy.percentile` for more information. + gamma : float, optional Parameter used in the power-law normalisation when the parameter - norm="power". Read :py:class:`matplotlib.colors.PowerNorm` for more + ``norm="power"``. Read :class:`matplotlib.colors.PowerNorm` for more details. Default value is 1.0. - linthresh : float - When used with norm="symlog", define the range within which the + linthresh : float, optional + When used with ``norm="symlog"``, define the range within which the plot is linear (to avoid having the plot go to infinity around zero). Default value is 0.01. - linscale : float + linscale : float, optional This allows the linear range (-linthresh to linthresh) to be stretched relative to the logarithmic range. Its value is the number of powers of base to use for each half of the linear range. - See :py:class:`matplotlib.colors.SymLogNorm` for more details. + See :class:`matplotlib.colors.SymLogNorm` for more details. Defaulf value is 0.1. scalebar : bool, optional If True and the units and scale of the x and y axes are the same a @@ -166,40 +148,37 @@ If True, plot the axes ticks. If None axes_ticks are only plotted when the scale bar is not plotted. If False the axes ticks are never plotted. - axes_off : {bool} - Default is False. + axes_off : bool, default False no_nans : bool, optional If True, set nans to zero for plotting. - centre_colormap : {"auto", True, False} + centre_colormap : bool or ``"auto"`` If True the centre of the color scheme is set to zero. This is specially useful when using diverging color schemes. If "auto" (default), diverging color schemes are automatically centred. - min_aspect : float + min_aspect : float, optional Set the minimum aspect ratio of the image and the figure. To keep the image in the aspect limit the pixels are made rectangular. """ -COMPLEX_DOCSTRING = \ - """power_spectrum : bool, default is False. +COMPLEX_DOCSTRING = """power_spectrum : bool, default False. If True, plot the power spectrum instead of the actual signal, if False, plot the real and imaginary parts of the complex signal. - representation : {'cartesian' or 'polar'} - Determines if the real and imaginary part of the complex data is plotted ('cartesian', - default), or if the amplitude and phase should be used ('polar'). + representation : {``'cartesian'`` | ``'polar'``} + Determines if the real and imaginary part of the complex data is plotted (``'cartesian'``, + default), or if the amplitude and phase should be used (``'polar'``). same_axes : bool, default True If True (default) plot the real and imaginary parts (or amplitude and phase) in the same figure if the signal is one-dimensional. fft_shift : bool, default False If True, shift the zero-frequency component. - See :py:func:`numpy.fft.fftshift` for more details. + See :func:`numpy.fft.fftshift` for more details. """ -PLOT2D_KWARGS_DOCSTRING = \ - """**kwargs : dict +PLOT2D_KWARGS_DOCSTRING = """**kwargs : dict Only when plotting an image: additional (optional) keyword - arguments for :py:func:`matplotlib.pyplot.imshow`. + arguments for :func:`matplotlib.pyplot.imshow`. """ diff --git a/hyperspy/docstrings/signal.py b/hyperspy/docstrings/signal.py index 77025965e9..ceae288c04 100644 --- a/hyperspy/docstrings/signal.py +++ b/hyperspy/docstrings/signal.py @@ -1,52 +1,47 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -"""Common docstring snippets for signal. +"""Common docstring snippets for signal.""" -""" - -ONE_AXIS_PARAMETER = \ - """: :py:class:`int`, :py:class:`str`, or :py:class:`~hyperspy.axes.DataAxis` +ONE_AXIS_PARAMETER = """: :class:`int`, :class:`str`, or :class:`~hyperspy.axes.DataAxis` The axis can be passed directly, or specified using the index of - the axis in the Signal's `axes_manager` or the axis name.""" - -MANY_AXIS_PARAMETER = \ - """: :py:class:`int`, :py:class:`str`, :py:class:`~hyperspy.axes.DataAxis`, tuple (of DataAxis) or :py:data:`None` - Either one on its own, or many axes in a tuple can be passed. In - both cases the axes can be passed directly, or specified using the - index in `axes_manager` or the name of the axis. Any duplicates are - removed. If ``None``, the operation is performed over all navigation - axes (default).""" - -OUT_ARG = \ - """out : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) or :py:data:`None` + the axis in the Signal's ``axes_manager`` or the *axis name*. If ``"sig"`` or + ``"nav"``, the signal or navigation axis will be used, respectively.""" + +MANY_AXIS_PARAMETER = """: :class:`int`, :class:`str`, :class:`~hyperspy.axes.DataAxis` or tuple + Either a single or multiple axes in a tuple can be passed. In + both cases, the axes can be passed directly, or specified using the + index in ``axes_manager`` or the *name of the axis*. Any duplicates are + removed. If ``"sig"`` or ``"nav"``, the signal or navigation axes + will be used, respectively. If ``None``, the operation is performed + over all navigation axes (default).""" + +OUT_ARG = """out : :class:`~hyperspy.api.signals.BaseSignal` (or subclass) or None If ``None``, a new Signal is created with the result of the operation and returned (default). If a Signal is passed, it is used to receive the output of the operation, and nothing is returned.""" -NAN_FUNC = \ - """Identical to :py:meth:`~hyperspy.signal.BaseSignal.{0}`, except ignores +NAN_FUNC = """Identical to :meth:`~hyperspy.api.signals.BaseSignal.{0}`, except ignores missing (NaN) values. See that method's documentation for details. """ -OPTIMIZE_ARG = \ - """optimize : bool +OPTIMIZE_ARG = """optimize : bool If ``True``, the location of the data in memory is optimised for the fastest iteration over the navigation axes. This operation can cause a peak of memory usage and requires considerable processing @@ -55,106 +50,109 @@ for more information. When operating on lazy signals, if ``True``, the chunks are optimised for the new axes configuration.""" -RECHUNK_ARG = \ - """rechunk: bool - Only has effect when operating on lazy signal. If ``True`` (default), - the data may be automatically rechunked before performing this - operation.""" - -SHOW_PROGRESSBAR_ARG = \ - """show_progressbar : None or bool - If ``True``, display a progress bar. If ``None``, the default from - the preferences settings is used.""" - -PARALLEL_ARG = \ - """parallel : None or bool - If ``True``, perform computation in parallel using multithreading. If - ``None``, the default from the preferences settings is used. The number - of threads is controlled by the ``max_workers`` argument.""" - -MAX_WORKERS_ARG = \ - """max_workers : None or int - Maximum number of threads used when ``parallel=True``. If None, defaults - to ``min(32, os.cpu_count())``.""" - -CLUSTER_SIGNALS_ARG = \ - """signal : {"mean", "sum", "centroid"}, optional - If "mean" or "sum" return the mean signal or sum respectively - over each cluster. If "centroid", returns the signals closest - to the centroid.""" - -HISTOGRAM_BIN_ARGS = \ - """bins : int or sequence of scalars or str, default "fd" - If `bins` is an int, it defines the number of equal-width - bins in the given range. If `bins` is a - sequence, it defines the bin edges, including the rightmost - edge, allowing for non-uniform bin widths. - - If `bins` is a string from the list below, will use - the method chosen to calculate the optimal bin width and - consequently the number of bins (see `Notes` for more detail on - the estimators) from the data that falls within the requested - range. While the bin width will be optimal for the actual data - in the range, the number of bins will be computed to fill the - entire range, including the empty portions. For visualisation, - using the 'auto' option is suggested. Weighted data is not - supported for automated bin size selection. - - 'auto' - Maximum of the 'sturges' and 'fd' estimators. Provides good - all around performance. - - 'fd' (Freedman Diaconis Estimator) - Robust (resilient to outliers) estimator that takes into - account data variability and data size. - - 'doane' - An improved version of Sturges' estimator that works better - with non-normal datasets. - - 'scott' - Less robust estimator that that takes into account data - variability and data size. - - 'stone' - Estimator based on leave-one-out cross-validation estimate of - the integrated squared error. Can be regarded as a generalization - of Scott's rule. - - 'rice' - Estimator does not take variability into account, only data - size. Commonly overestimates number of bins required. - - 'sturges' - R's default method, only accounts for data size. Only - optimal for gaussian data and underestimates number of bins - for large non-gaussian datasets. - - 'sqrt' - Square root (of data size) estimator, used by Excel and - other programs for its speed and simplicity. - - 'knuth' - Knuth's rule is a fixed-width, Bayesian approach to determining - the optimal bin width of a histogram. - - 'blocks' - Determination of optimal adaptive-width histogram bins using - the Bayesian Blocks algorithm. - """ - -HISTOGRAM_MAX_BIN_ARGS = \ - """max_num_bins : int, default 250 - When estimating the bins using one of the str methods, the - number of bins is capped by this number to avoid a MemoryError - being raised by :py:func:`numpy.histogram`.""" - -SIGNAL_MASK_ARG = \ - """signal_mask: bool array +RECHUNK_ARG = """rechunk : bool + Only has effect when operating on lazy signal. Default ``False``, + which means the chunking structure will be retained. If ``True``, + the data may be automatically rechunked before performing this + operation.""" + +SHOW_PROGRESSBAR_ARG = """show_progressbar : None or bool + If ``True``, display a progress bar. If ``None``, the default from + the preferences settings is used.""" + +LAZY_OUTPUT_ARG = """lazy_output : None or bool + If ``True``, the output will be returned as a lazy signal. This means + the calculation itself will be delayed until either compute() is used, + or the signal is stored as a file. + If ``False``, the output will be returned as a non-lazy signal, this + means the outputs will be calculated directly, and loaded into memory. + If ``None`` the output will be lazy if the input signal is lazy, and + non-lazy if the input signal is non-lazy.""" + +NUM_WORKERS_ARG = """num_workers : None or int + Number of worker used by dask. If None, default + to dask default value.""" + +CLUSTER_SIGNALS_ARG = """signal : {"mean", "sum", "centroid"}, optional + If "mean" or "sum" return the mean signal or sum respectively + over each cluster. If "centroid", returns the signals closest + to the centroid.""" + +HISTOGRAM_BIN_ARGS = """bins : int or sequence of float or str, default "fd" + If ``bins`` is an int, it defines the number of equal-width + bins in the given range. If ``bins`` is a + sequence, it defines the bin edges, including the rightmost + edge, allowing for non-uniform bin widths. + + If ``bins`` is a string from the list below, will use + the method chosen to calculate the optimal bin width and + consequently the number of bins (see Notes for more detail on + the estimators) from the data that falls within the requested + range. While the bin width will be optimal for the actual data + in the range, the number of bins will be computed to fill the + entire range, including the empty portions. For visualisation, + using the ``'auto'`` option is suggested. Weighted data is not + supported for automated bin size selection. + + Possible strings are: + + - ``'auto'`` : Maximum of the 'sturges' and 'fd' estimators. + Provides good all around performance. + - ``'fd'`` : Freedman Diaconis Estimator, robust + (resilient to outliers) estimator that takes into + account data variability and data size. + - ``'doane'`` : An improved version of Sturges' estimator + that works better with non-normal datasets. + - ``'scott'`` : Less robust estimator that that takes into + account data variability and data size. + - ``'stone'`` : Estimator based on leave-one-out cross-validation + estimate of the integrated squared error. Can be regarded + as a generalization of Scott's rule. + - ``'rice'`` : Estimator does not take variability into account, + only data size. Commonly overestimates number of bins required. + - ``'sturges'`` : R's default method, only accounts for data size. + Only optimal for gaussian data and underestimates number + of bins for large non-gaussian datasets. + - ``'sqrt'`` : Square root (of data size) estimator, used by Excel + and other programs for its speed and simplicity. + - ``'knuth'`` : Knuth's rule is a fixed-width, Bayesian approach to + determining the optimal bin width of a histogram. + - ``'blocks'`` : Determination of optimal adaptive-width histogram + bins using the Bayesian Blocks algorithm.""" + +HISTOGRAM_MAX_BIN_ARGS = """max_num_bins : int, default 250 + When estimating the bins using one of the str methods, the + number of bins is capped by this number to avoid a MemoryError + being raised by :func:`numpy.histogram`.""" + +HISTOGRAM_RANGE_ARGS = """range_bins : (float, float), optional + The lower and upper limit of the range of bins. If not provided, + range is simply ``(a.min(), a.max())``. Values outside the range are + ignored. The first element of the range must be less than or + equal to the second. `range` affects the automatic bin + computation as well. While bin width is computed to be optimal + based on the actual data within `range`, the bin count will fill + the entire range including portions containing no data.""" + +HISTOGRAM_WEIGHTS_ARGS = """weights : array_like, optional + An array of weights, of the same shape as `a`. Each value in + `a` only contributes its associated weight towards the bin count + (instead of 1). This is currently not used by any of the bin estimators, + but may be in the future.""" + +SIGNAL_MASK_ARG = """signal_mask : numpy.ndarray of bool Restricts the operation to the signal locations not marked as True (masked).""" -NAVIGATION_MASK_ARG = \ - """navigation_mask: bool array +NAVIGATION_MASK_ARG = """navigation_mask : numpy.ndarray of bool Restricts the operation to the navigation locations not marked as True (masked).""" + +LAZYSIGNAL_DOC = """ + The computation is delayed until explicitly requested. + + This class is not expected to be instantiated directly, instead use: + + >>> data = da.ones((10, 10)) + >>> s = hs.signals.__BASECLASS__(data).as_lazy() + """ diff --git a/hyperspy/docstrings/signal1d.py b/hyperspy/docstrings/signal1d.py index 57e4fa8452..7f1eb3c613 100644 --- a/hyperspy/docstrings/signal1d.py +++ b/hyperspy/docstrings/signal1d.py @@ -1,31 +1,27 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -"""Common docstring snippets for signal1d. +"""Common docstring snippets for signal1d.""" -""" - -CROP_PARAMETER_DOC = \ - """crop : bool +CROP_PARAMETER_DOC = """crop : bool If True automatically crop the signal axis at both ends if needed.""" -SPIKES_DIAGNOSIS_DOCSTRING = \ - """Plots a histogram to help in choosing the threshold for +SPIKES_DIAGNOSIS_DOCSTRING = """Plots a histogram to help in choosing the threshold for spikes removal. Parameters @@ -39,51 +35,47 @@ %s **kwargs : dict Keyword arguments pass to - :py:meth:`~hyperspy.signal.signal.BaseSignal.get_histogram` + :meth:`~hyperspy.api.signals.BaseSignal.get_histogram` - See also + See Also -------- spikes_removal_tool """ -SPIKES_REMOVAL_TOOL_DOCSTRING =\ - """Graphical interface to remove spikes from EELS spectra or - luminescence data. - If non-interactive, it removes all spikes and returns a - `~hyperspy.signals._signal_tools.SpikesRemoval` object. +SPIKES_REMOVAL_TOOL_DOCSTRING = """Graphical interface to remove spikes from EELS spectra or + luminescence data. + If non-interactive, it removes all spikes. Parameters ---------- %s %s - threshold : 'auto' or int - if `int` set the threshold value use for the detecting the spikes. - If `auto`, determine the threshold value as being the first zero + threshold : ``'auto'`` or int + if ``int`` set the threshold value use for the detecting the spikes. + If ``"auto"``, determine the threshold value as being the first zero value in the histogram obtained from the - :py:meth:`~hyperspy.signals._signal1d.Signal1D.spikes_diagnosis` + :meth:`~hyperspy.api.signals.Signal1D.spikes_diagnosis` method. %s interactive : bool - If True, remove the spikes using the graphical user interface. + If True, remove the spikes using the graphical user interface. If False, remove all the spikes automatically, which can - introduce artefacts if used with signal containing peak-like - features. However, this can be mitigated by using the - `signal_mask` argument to mask the signal of interest. + introduce artefacts if used with signal containing peak-like + features. However, this can be mitigated by using the + ``signal_mask`` argument to mask the signal of interest. %s %s **kwargs : dict - Keyword arguments pass to - :py:meth:`~hyperspy.signals._signal_tools.SpikesRemoval` + Keyword arguments pass to ``SpikesRemoval``. - See also + See Also -------- - :py:meth:`~hyperspy.signals._signal1d.Signal1D.spikes_diagnosis` + :meth:`~hyperspy.api.signals.Signal1D.spikes_diagnosis` """ -MASK_ZERO_LOSS_PEAK_WIDTH = \ - """zero_loss_peak_mask_width : None or float +MASK_ZERO_LOSS_PEAK_WIDTH = """zero_loss_peak_mask_width : None or float If None, the zero loss peak is not masked, otherwise, use the provided value as width of the zero loss peak mask. Default is None.""" diff --git a/hyperspy/docstrings/utils.py b/hyperspy/docstrings/utils.py index ffa7ed6215..8e7f1ad3ac 100644 --- a/hyperspy/docstrings/utils.py +++ b/hyperspy/docstrings/utils.py @@ -1,27 +1,24 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -"""Common docstring snippets for utils. +"""Common docstring snippets for utils.""" -""" - -STACK_METADATA_ARG = \ - """stack_metadata : {bool, int} +STACK_METADATA_ARG = """stack_metadata : {bool, int} If integer, this value defines the index of the signal in the signal list, from which the ``metadata`` and ``original_metadata`` are taken. If ``True``, the ``original_metadata`` and ``metadata`` of each signals @@ -31,23 +28,22 @@ If False, the ``metadata`` and ``original_metadata`` are not copied.""" -REBIN_ARGS = \ - """new_shape : list (of floats or integer) or None +REBIN_ARGS = """new_shape : list (of float or int) or None For each dimension specify the new_shape. This will internally be converted into a ``scale`` parameter. - scale : list (of floats or integer) or None + scale : list (of float or int) or None For each dimension, specify the new:old pixel ratio, e.g. a ratio of 1 is no binning and a ratio of 2 means that each pixel in the new spectrum is twice the size of the pixels in the old spectrum. The length of the list should match the dimension of the Signal's underlying data array. - *Note : Only one of `scale` or `new_shape` should be specified, + *Note : Only one of ``scale`` or ``new_shape`` should be specified, otherwise the function will not run* crop : bool Whether or not to crop the resulting rebinned data (default is ``True``). When binning by a non-integer number of pixels it is likely that the final row in each dimension will - contain fewer than the full quota to fill one pixel. For example, + contain fewer than the full quota to fill one pixel. For example, a 5*5 array binned by 2.1 will produce two rows containing 2.1 pixels and one row containing only 0.8 pixels. Selection of ``crop=True`` or ``crop=False`` determines whether or not this @@ -58,5 +54,5 @@ before and after binning.* dtype : {None, numpy.dtype, "same"} Specify the dtype of the output. If None, the dtype will be - determined by the behaviour of :py:func:`numpy.sum`, if "same", + determined by the behaviour of :func:`numpy.sum`, if ``"same"``, the dtype will be kept the same. Default is None.""" diff --git a/hyperspy/drawing/__init__.py b/hyperspy/drawing/__init__.py index c41a8ae211..3d813a7dda 100644 --- a/hyperspy/drawing/__init__.py +++ b/hyperspy/drawing/__init__.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . diff --git a/hyperspy/drawing/_markers/__init__.py b/hyperspy/drawing/_markers/__init__.py index c41a8ae211..3d813a7dda 100644 --- a/hyperspy/drawing/_markers/__init__.py +++ b/hyperspy/drawing/_markers/__init__.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . diff --git a/hyperspy/drawing/_markers/arrows.py b/hyperspy/drawing/_markers/arrows.py new file mode 100644 index 0000000000..c4d55cbf53 --- /dev/null +++ b/hyperspy/drawing/_markers/arrows.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from hyperspy.docstrings.markers import OFFSET_DOCSTRING +from hyperspy.drawing.markers import Markers +from hyperspy.external.matplotlib.quiver import Quiver + + +class Arrows(Markers): + """A set of Arrow markers based on the matplotlib.quiver.Quiver class.""" + + _position_key = "offsets" + + def __init__( + self, offsets, U, V, C=None, scale=1, angles="xy", scale_units="xy", **kwargs + ): + """ + Initialize the set of Arrows Markers. + + Parameters + ---------- + %s + U : array-like + The change in x (horizontal) diraction for the arrows. + V : array-like + The change in y (vertical) diraction for the arrows. + C : array-like or None + kwargs : dict + Keyword arguments are passed to :class:`matplotlib.quiver.Quiver`. + """ + + super().__init__( + collection=Quiver, + # iterating arguments + offsets=offsets, + U=U, + V=V, + C=C, + **kwargs, + ) + self._init_kwargs = dict(scale=scale, angles=angles, scale_units=scale_units) + + __init__.__doc__ %= OFFSET_DOCSTRING + + def _initialize_collection(self): + if self._collection is None: + kwds = self.get_current_kwargs() + offsets = kwds["offsets"] + X = offsets[:, 0] + Y = offsets[:, 1] + U, V, C = kwds["U"], kwds["V"], kwds["C"] + + if C is None: + args = (X, Y, U, V) + else: + args = (X, Y, U, V, C) + + self._collection = self._collection_class( + *args, offset_transform=self.ax.transData, **self._init_kwargs + ) + + def update(self): + if self._is_iterating or "relative" in [ + self._offset_transform, + self._transform, + ]: + kwds = self.get_current_kwargs(only_variable_length=True) + # in case 'U', 'V', 'C' are not position dependent + kwds.setdefault("U", self.kwargs["U"]) + kwds.setdefault("V", self.kwargs["V"]) + kwds.setdefault("C", self.kwargs["C"]) + self._collection.set_offsets(kwds["offsets"]) + # Need to use `set_UVC` and pass all 'U', 'V' and 'C' at once, + # because matplotlib expect same shape + UVC = {k: v for k, v in kwds.items() if k in ["U", "V", "C"]} + self._collection.set_UVC(**UVC) diff --git a/hyperspy/drawing/_markers/circles.py b/hyperspy/drawing/_markers/circles.py new file mode 100644 index 0000000000..8cf6583c31 --- /dev/null +++ b/hyperspy/drawing/_markers/circles.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from hyperspy.docstrings.markers import OFFSET_DOCSTRING, UNITS_DOCSTRING +from hyperspy.drawing.markers import Markers +from hyperspy.external.matplotlib.collections import CircleCollection + + +class Circles(Markers): + """A set of Circle Markers.""" + + _position_key = "offsets" + + def __init__( + self, + offsets, + sizes, + offset_transform="data", + units="x", + facecolors="none", + **kwargs, + ): + """ + Create a set of Circle Markers. + + Parameters + ---------- + %s + sizes : numpy.ndarray + The size of the circles in units defined by the argument units. + facecolors : matplotlib color or list of color + Set the facecolor(s) of the markers. It can be a color + (all patches have same color), or a sequence of colors; + if it is a sequence the patches will cycle through the sequence. + If c is 'none', the patch will not be filled. + %s + kwargs : dict + Keyword arguments are passed to :class:`matplotlib.collections.CircleCollection`. + """ + + if kwargs.setdefault("transform", "display") != "display": + raise ValueError( + "The `transform` argument is not supported for Circles Markers. Instead, " + "use the `offset_transform` argument to specify the transform of the " + "`offsets` and use the `units` argument to specify transform of the " + "`sizes` argument." + ) + + super().__init__( + collection=CircleCollection, + offsets=offsets, + sizes=sizes, + facecolors=facecolors, + offset_transform=offset_transform, + units=units, + **kwargs, + ) + + __init__.__doc__ %= (OFFSET_DOCSTRING, UNITS_DOCSTRING) diff --git a/hyperspy/drawing/_markers/ellipses.py b/hyperspy/drawing/_markers/ellipses.py new file mode 100644 index 0000000000..53ee9bbbbb --- /dev/null +++ b/hyperspy/drawing/_markers/ellipses.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from hyperspy.docstrings.markers import ( + ANGLES_DOCSTRING, + HEIGHTS_DOCSTRING, + OFFSET_DOCSTRING, + UNITS_DOCSTRING, + WIDTHS_DOCSTRING, +) +from hyperspy.drawing.markers import Markers +from hyperspy.external.matplotlib.collections import EllipseCollection + + +class Ellipses(Markers): + """A set of Ellipse Markers""" + + _position_key = "offsets" + + def __init__( + self, + offsets, + heights, + widths, + angles=0, + offset_transform="data", + units="xy", + **kwargs, + ): + """Initialize the set of Ellipse Markers. + + Parameters + ---------- + %s + %s + %s + %s + %s + kwargs: + Additional keyword arguments are passed to :class:`matplotlib.collections.EllipseCollection`. + """ + if kwargs.setdefault("transform", "display") != "display": + raise ValueError( + "The transform argument is not supported for Squares Markers. Instead, " + "use the offset_transform argument to specify the transform of the " + "offsets and use the ``units`` argument to specify transform of the " + "sizes." + ) + super().__init__( + collection=EllipseCollection, + offsets=offsets, + offset_transform=offset_transform, + heights=heights, + widths=widths, + angles=angles, + units=units, + **kwargs, + ) + + __init__.__doc__ %= ( + OFFSET_DOCSTRING, + HEIGHTS_DOCSTRING, + WIDTHS_DOCSTRING, + ANGLES_DOCSTRING, + UNITS_DOCSTRING, + ) diff --git a/hyperspy/drawing/_markers/horizontal_line.py b/hyperspy/drawing/_markers/horizontal_line.py deleted file mode 100644 index e2778c6321..0000000000 --- a/hyperspy/drawing/_markers/horizontal_line.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy.drawing.marker import MarkerBase - - -class HorizontalLine(MarkerBase): - - """Horizontal line marker that can be added to the signal figure - - Parameters - --------- - y : array or float - The position of the line. If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> s = hs.signals.Signal1D(np.random.random([10, 100])) * 10 - >>> m = hs.plot.markers.horizontal_line(y=range(10), color='green') - >>> s.add_marker(m) - - Adding a marker permanently to a signal - - >>> s = hs.signals.Signal1D(np.random.random([10, 100])) - >>> m = hs.plot.markers.horizontal_line(y=5, color='green') - >>> s.add_marker(m, permanent=True) - - """ - - def __init__(self, y, **kwargs): - MarkerBase.__init__(self) - lp = {'linewidth': 1, 'color': 'black'} - self.marker_properties = lp - self.set_data(y1=y) - self.set_marker_properties(**kwargs) - self.name = 'horizontal_line' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('y1'), - self.marker_properties['color'], - self.get_data_position('size'), - ) - return(string) - - def update(self): - if self.auto_update is False: - return - self.marker.set_ydata(self.get_data_position('y1')) - - def _plot_marker(self): - self.marker = self.ax.axhline(self.get_data_position('y1'), - **self.marker_properties) diff --git a/hyperspy/drawing/_markers/horizontal_line_segment.py b/hyperspy/drawing/_markers/horizontal_line_segment.py deleted file mode 100644 index c018bde073..0000000000 --- a/hyperspy/drawing/_markers/horizontal_line_segment.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import matplotlib.pyplot as plt - -from hyperspy.drawing.marker import MarkerBase - - -class HorizontalLineSegment(MarkerBase): - - """Horizontal line segment marker that can be added to the signal figure - - Parameters - ---------- - x1 : array or float - The position of the start of the line segment in x. - If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - x2 : array or float - The position of the end of the line segment in x. - see x1 arguments - y : array or float - The position of line segment in y. - see x1 arguments - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> im = hs.signals.Signal2D(np.zeros((100, 100))) - >>> m = hs.plot.markers.horizontal_line_segment( - >>> x1=20, x2=70, y=70, linewidth=4, color='red', linestyle='dotted') - >>> im.add_marker(m) - - Adding a marker permanently to a signal - - >>> im = hs.signals.Signal2D(np.zeros((100, 100))) - >>> m = hs.plot.markers.horizontal_line_segment( - >>> x1=10, x2=30, y=42, linewidth=4, color='red', linestyle='dotted') - >>> im.add_marker(m, permanent=True) - - """ - - def __init__(self, x1, x2, y, **kwargs): - MarkerBase.__init__(self) - lp = {'color': 'black', 'linewidth': 1} - self.marker_properties = lp - self.set_data(x1=x1, x2=x2, y1=y) - self.set_marker_properties(**kwargs) - self.name = 'horizontal_line_segment' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('x1'), - self.get_data_position('x2'), - self.get_data_position('y1'), - self.marker_properties['color'], - ) - return(string) - - def update(self): - if self.auto_update is False: - return - self._update_segment() - - def _plot_marker(self): - self.marker = self.ax.vlines(0, 0, 1, **self.marker_properties) - self._update_segment() - - def _update_segment(self): - segments = self.marker.get_segments() - segments[0][0, 1] = self.get_data_position('y1') - segments[0][1, 1] = segments[0][0, 1] - if self.get_data_position('x1') is None: - segments[0][0, 0] = plt.getp(self.marker.axes, 'xlim')[0] - else: - segments[0][0, 0] = self.get_data_position('x1') - if self.get_data_position('x2') is None: - segments[0][1, 0] = plt.getp(self.marker.axes, 'xlim')[1] - else: - segments[0][1, 0] = self.get_data_position('x2') - self.marker.set_segments(segments) diff --git a/hyperspy/drawing/_markers/horizontal_lines.py b/hyperspy/drawing/_markers/horizontal_lines.py new file mode 100644 index 0000000000..2790ddee48 --- /dev/null +++ b/hyperspy/drawing/_markers/horizontal_lines.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import copy + +import numpy as np +from matplotlib.collections import LineCollection + +from hyperspy.drawing.markers import Markers + + +class HorizontalLines(Markers): + """A set of HorizontalLines markers""" + + _position_key = "offsets" + _position_key_to_set = "segments" + + def __init__(self, offsets, **kwargs): + """ + Initialize a set of HorizontalLines markers. + + Parameters + ---------- + offsets : array-like + Positions of the markers + kwargs : dict + Keyword arguments passed to the underlying marker collection. Any argument + that is array-like and has `dtype=object` is assumed to be an iterating + argument and is treated as such. + + Examples + -------- + >>> import hyperspy.api as hs + >>> import matplotlib.pyplot as plt + >>> import numpy as np + + >>> # Create a Signal2D with 2 navigation dimensions + >>> rng = np.random.default_rng(0) + >>> data = rng.random((25, 25, 100)) * 100 + >>> s = hs.signals.Signal1D(data) + >>> offsets = np.array([10, 20, 40]) + + >>> m = hs.plot.markers.HorizontalLines( + ... offsets=offsets, + ... linewidth=4, + ... colors=['r', 'g', 'b'], + ... ) + + >>> s.plot() + >>> s.add_marker(m) + """ + + if ( + kwargs.setdefault("offset_transform", "display") != "display" + or kwargs.setdefault("transform", "yaxis") != "yaxis" + ): + raise ValueError( + "Setting 'offset_transform' or 'transform' argument is not " + "supported with the HorizontalLines markers." + ) + + super().__init__(collection=LineCollection, offsets=offsets, **kwargs) + + def get_current_kwargs(self, only_variable_length=False): + kwargs = super().get_current_kwargs(only_variable_length=only_variable_length) + # Need to take a deepcopy to avoid changing `self.kwargs` + kwds = copy.deepcopy(kwargs) + kwds[self._position_key_to_set] = np.array( + [[[0, y], [1, y]] for y in kwds.pop(self._position_key)] + ) + return kwds diff --git a/hyperspy/drawing/_markers/line_segment.py b/hyperspy/drawing/_markers/line_segment.py deleted file mode 100644 index 2c96d888cd..0000000000 --- a/hyperspy/drawing/_markers/line_segment.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy.drawing.marker import MarkerBase - - -class LineSegment(MarkerBase): - - """Line segment marker that can be added to the signal figure - - Parameters - ---------- - x1 : array or float - The position of the start of the line segment in x. - If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - y1 : array or float - The position of the start of the line segment in y. - see x1 arguments - x2 : array or float - The position of the end of the line segment in x. - see x1 arguments - y2 : array or float - The position of the end of the line segment in y. - see x1 arguments - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> im = hs.signals.Signal2D(np.zeros((100, 100))) - >>> m = hs.plot.markers.line_segment( - >>> x1=20, x2=70, y1=20, y2=70, - >>> linewidth=4, color='red', linestyle='dotted') - >>> im.add_marker(m) - - Permanently adding a marker to a signal - - >>> im = hs.signals.Signal2D(np.zeros((100, 100))) - >>> m = hs.plot.markers.line_segment( - >>> x1=10, x2=30, y1=50, y2=70, - >>> linewidth=4, color='red', linestyle='dotted') - >>> im.add_marker(m, permanent=True) - - """ - - def __init__(self, x1, y1, x2, y2, **kwargs): - MarkerBase.__init__(self) - lp = {'color': 'black', 'linewidth': 1} - self.marker_properties = lp - self.set_data(x1=x1, y1=y1, x2=x2, y2=y2) - self.set_marker_properties(**kwargs) - self.name = 'line_segment' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('x1'), - self.get_data_position('x2'), - self.get_data_position('y1'), - self.get_data_position('y2'), - self.marker_properties['color'], - ) - return(string) - - def update(self): - if self.auto_update is False: - return - self._update_segment() - - def _plot_marker(self): - x1 = self.get_data_position('x1') - x2 = self.get_data_position('x2') - y1 = self.get_data_position('y1') - y2 = self.get_data_position('y2') - self.marker = self.ax.plot((x1, x2), (y1, y2), - **self.marker_properties)[0] - - def _update_segment(self): - x1 = self.get_data_position('x1') - x2 = self.get_data_position('x2') - y1 = self.get_data_position('y1') - y2 = self.get_data_position('y2') - self.marker.set_data((x1, x2), (y1, y2)) diff --git a/hyperspy/drawing/_markers/lines.py b/hyperspy/drawing/_markers/lines.py new file mode 100644 index 0000000000..cfc833127c --- /dev/null +++ b/hyperspy/drawing/_markers/lines.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from matplotlib.collections import LineCollection + +from hyperspy.drawing.markers import Markers + + +class Lines(Markers): + """A set of Line Segments Markers.""" + + _position_key = "segments" + + def __init__(self, segments, transform="data", **kwargs): + """Initialize the set of Segments Markers. + + Parameters + ---------- + segments : numpy.ndarray + Must be with shape [n, 2, 2] ragged array with shape (n, 2, 3) at every navigation position. + Defines the lines[[[x1,y1],[x2,y2]], ...] of the center of the ellipse. + kwargs : dict + Additional keyword arguments are passed to :class:`matplotlib.collections.LineCollection`. + + Notes + ----- + Unlike markers using ``offsets`` argument, the positions of the segments + are defined by the ``segments`` argument and the tranform specifying the + coordinate system of the ``segments`` is ``transform``. + + """ + + if kwargs.setdefault("offset_transform", "display") != "display": + raise ValueError( + "The `offset_transform` argument is not supported for Lines Markers. " + "Instead, use the `transform` argument to specify the transform " + "of the lines." + ) + + super().__init__( + collection=LineCollection, segments=segments, transform=transform, **kwargs + ) diff --git a/hyperspy/drawing/_markers/point.py b/hyperspy/drawing/_markers/point.py deleted file mode 100644 index fa3930f3a5..0000000000 --- a/hyperspy/drawing/_markers/point.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy.drawing.marker import MarkerBase - - -class Point(MarkerBase): - - """Point marker that can be added to the signal figure. - - If the signal has one or several navigation axes, the point marker - can change as a function of the navigation position. This done by - using an array for the x and y parameters. This array must have - the same shape as the navigation axes of the signal. - - Parameters - ---------- - x : array or float - The position of the point in x. If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - y : array or float - The position of the point in y. see x arguments - size : array or float, optional, default 20 - The size of the point. see x arguments - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> im = hs.signals.Signal2D(np.random.random([10, 50, 50])) - >>> m = hs.plot.markers.point(x=range(10), y=range(10)[::-1], - color='red') - >>> im.add_marker(m) - - Adding a marker permanently to a signal - - >>> im = hs.signals.Signal2D(np.random.random([10, 50, 50])) - >>> m = hs.plot.markers.point(10, 30, color='blue', size=50) - >>> im.add_marker(m, permanent=True) - - Markers on local maxima - - >>> from skimage.feature import peak_local_max - >>> import scipy.misc - >>> im = hs.signals.Signal2D(scipy.misc.ascent()).as_signal2D([2,0]) - >>> index = array([peak_local_max(i.data, min_distance=100, num_peaks=4) - >>> for i in im]) - >>> for i in range(4): - >>> m = hs.plot.markers.point(x=index[:, i, 1], - >>> y=index[:, i, 0], color='red') - >>> im.add_marker(m) - """ - - def __init__(self, x, y, size=20, **kwargs): - MarkerBase.__init__(self) - lp = {'color': 'black', 'linewidth': None} - self.marker_properties = lp - self.set_data(x1=x, y1=y, size=size) - self.set_marker_properties(**kwargs) - self.name = 'point' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('x1'), - self.get_data_position('y1'), - self.marker_properties['color'], - self.get_data_position('size'), - ) - return(string) - - def update(self): - if self.auto_update is False: - return - self.marker.set_offsets([self.get_data_position('x1'), - self.get_data_position('y1')]) - self.marker._sizes = [self.get_data_position('size')] - - def _plot_marker(self): - self.marker = self.ax.scatter(self.get_data_position('x1'), - self.get_data_position('y1'), - **self.marker_properties) - self.marker._sizes = [self.get_data_position('size')] diff --git a/hyperspy/drawing/_markers/points.py b/hyperspy/drawing/_markers/points.py new file mode 100644 index 0000000000..fb3b508392 --- /dev/null +++ b/hyperspy/drawing/_markers/points.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from hyperspy.docstrings.markers import OFFSET_DOCSTRING, UNITS_DOCSTRING +from hyperspy.drawing.markers import Markers +from hyperspy.external.matplotlib.collections import CircleCollection + + +class Points(Markers): + """ + A set of Points Markers.""" + + _position_key = "offsets" + + def __init__( + self, offsets, sizes=10, offset_transform="data", units="points", **kwargs + ): + """Initialize the set of points Markers. + + Parameters + ---------- + %s + sizes : int, float or array_like, optional + The size of the markers in display coordinate system. + %s + kwargs : dict + Keyword arguments are passed to :class:`matplotlib.collections.CircleCollection` + """ + if kwargs.setdefault("transform", "display") != "display": + raise ValueError( + "The transform argument is not supported for Squares Markers. Instead, " + "use the offset_transform argument to specify the transform of the " + "offsets and use the ``units`` argument to specify transform of the " + "sizes." + ) + + super().__init__( + collection=CircleCollection, + offsets=offsets, + sizes=sizes, + offset_transform=offset_transform, + units=units, + **kwargs, + ) + + __init__.__doc__ %= (OFFSET_DOCSTRING, UNITS_DOCSTRING) diff --git a/hyperspy/drawing/_markers/polygons.py b/hyperspy/drawing/_markers/polygons.py new file mode 100644 index 0000000000..e823ebe4b7 --- /dev/null +++ b/hyperspy/drawing/_markers/polygons.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from matplotlib.collections import PolyCollection + +from hyperspy.drawing.markers import Markers + + +class Polygons(Markers): + """A Collection of Rectangles Markers""" + + _position_key = "verts" + + def __init__(self, verts, transform="data", **kwargs): + """ + Initialize the set of Segments Markers. + + Parameters + ---------- + verts : list of numpy.ndarray or list of list + The verts define the vertices of the polygons. Note that this can be + a ragged list and as such it is not automatically cast to a numpy + array as that would result in an array of objects. + In the form [[[x1,y1], [x2,y2], ... [xn, yn]],[[x1,y1], [x2,y2], ...[xm,ym]], ...]. + **kwargs : dict + Additional keyword arguments are passed to + :class:`matplotlib.collections.PolyCollection` + + Examples + -------- + >>> import hyperspy.api as hs + >>> import numpy as np + >>> # Create a Signal2D with 2 navigation dimensions + >>> data = np.ones((25, 25, 100, 100)) + >>> s = hs.signals.Signal2D(data) + >>> poylgon1 = [[1, 1], [20, 20], [1, 20], [25, 5]] + >>> poylgon2 = [[50, 60], [90, 40], [60, 40], [23, 60]] + >>> verts = [poylgon1, poylgon2] + >>> # Create the markers + >>> m = hs.plot.markers.Polygons( + ... verts=verts, + ... linewidth=3, + ... facecolors=('g',), + ... ) + >>> # Add the marker to the signal + >>> s.plot() + >>> s.add_marker(m) + + Notes + ----- + Unlike markers using ``offsets`` argument, the positions of the polygon + are defined by the ``verts`` argument and the tranform specifying the + coordinate system of the ``verts`` is ``transform``. + + """ + if kwargs.setdefault("offset_transform", "display") != "display": + raise ValueError( + "The `offset_transform` argument is not supported for Polygons Markers. " + "Instead, use the `transform` argument to specify the transform " + "of the polygons." + ) + + super().__init__( + collection=PolyCollection, verts=verts, transform=transform, **kwargs + ) diff --git a/hyperspy/drawing/_markers/rectangle.py b/hyperspy/drawing/_markers/rectangle.py deleted file mode 100644 index 7ddf70b068..0000000000 --- a/hyperspy/drawing/_markers/rectangle.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import matplotlib.pyplot as plt - -from hyperspy.drawing.marker import MarkerBase - - -class Rectangle(MarkerBase): - - """Rectangle marker that can be added to the signal figure - - Parameters - ---------- - x1 : array or float - The position of the up left corner of the rectangle in x. - If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - y1 : array or float - The position of the up left corner of the rectangle in y. - see x1 arguments - x2 : array or float - The position of the down right corner of the rectangle in x. - see x1 arguments - y2 : array or float - The position of the down right of the rectangle in y. - see x1 arguments - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> import scipy.misc - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) - >>> m = hs.plot.markers.rectangle(x1=150, y1=100, x2=400, y2=400, - >>> color='red') - >>> im.add_marker(m) - - Adding a marker permanently to a signal - - >>> im = hs.signals.Signal2D(np.random.random((50, 50)) - >>> m = hs.plot.markers.rectangle(x1=20, y1=30, x2=40, y2=49) - >>> im.add_marker(m, permanent=True) - """ - - def __init__(self, x1, y1, x2, y2, **kwargs): - MarkerBase.__init__(self) - lp = {'color': 'black', 'fill': None, 'linewidth': 1} - self.marker_properties = lp - self.set_data(x1=x1, y1=y1, x2=x2, y2=y2) - self.set_marker_properties(**kwargs) - self.name = 'rectangle' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('x1'), - self.get_data_position('x2'), - self.get_data_position('y1'), - self.get_data_position('y2'), - self.marker_properties['color'], - ) - return(string) - - def update(self): - if self.auto_update is False: - return - width = abs(self.get_data_position('x1') - - self.get_data_position('x2')) - height = abs(self.get_data_position('y1') - - self.get_data_position('y2')) - self.marker.set_xy([self.get_data_position('x1'), - self.get_data_position('y1')]) - self.marker.set_width(width) - self.marker.set_height(height) - - def _plot_marker(self): - width = abs(self.get_data_position('x1') - - self.get_data_position('x2')) - height = abs(self.get_data_position('y1') - - self.get_data_position('y2')) - self.marker = self.ax.add_patch(plt.Rectangle( - (self.get_data_position('x1'), self.get_data_position('y1')), - width, height, **self.marker_properties)) diff --git a/hyperspy/drawing/_markers/rectangles.py b/hyperspy/drawing/_markers/rectangles.py new file mode 100644 index 0000000000..2e103e5149 --- /dev/null +++ b/hyperspy/drawing/_markers/rectangles.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from hyperspy.docstrings.markers import ( + ANGLES_DOCSTRING, + HEIGHTS_DOCSTRING, + OFFSET_DOCSTRING, + UNITS_DOCSTRING, + WIDTHS_DOCSTRING, +) +from hyperspy.drawing.markers import Markers +from hyperspy.external.matplotlib.collections import RectangleCollection + + +class Rectangles(Markers): + """A Collection of Rectangles Markers""" + + _position_key = "offsets" + + def __init__( + self, + offsets, + widths, + heights, + angles=0, + offset_transform="data", + units="xy", + **kwargs, + ): + """Initialize the set of Segments Markers. + + Parameters + ---------- + %s + %s + %s + %s + %s + kwargs: + Additional keyword arguments are passed to + :class:`hyperspy.external.matplotlib.collections.RectangleCollection`. + """ + if kwargs.setdefault("transform", "display") != "display": + raise ValueError( + "The `transform` argument is not supported for Rectangle Markers. Instead, " + "use the `offset_transform` argument to specify the transform of the " + "`offsets` and use the `units` argument to specify transform of the " + "`sizes` argument." + ) + + super().__init__( + collection=RectangleCollection, + offsets=offsets, + widths=widths, + heights=heights, + angles=angles, + offset_transform=offset_transform, + units=units, + **kwargs, + ) + + __init__.__doc__ %= ( + OFFSET_DOCSTRING, + HEIGHTS_DOCSTRING, + WIDTHS_DOCSTRING, + ANGLES_DOCSTRING, + UNITS_DOCSTRING, + ) diff --git a/hyperspy/drawing/_markers/squares.py b/hyperspy/drawing/_markers/squares.py new file mode 100644 index 0000000000..02bcc72070 --- /dev/null +++ b/hyperspy/drawing/_markers/squares.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from hyperspy.docstrings.markers import ( + ANGLES_DOCSTRING, + OFFSET_DOCSTRING, + UNITS_DOCSTRING, + WIDTHS_DOCSTRING, +) +from hyperspy.drawing.markers import Markers +from hyperspy.external.matplotlib.collections import SquareCollection + + +class Squares(Markers): + """ + A Collection of square markers. + """ + + _position_key = "offsets" + + def __init__( + self, offsets, widths, angles=0, offset_transform="data", units="x", **kwargs + ): + """ + Initialize the set of square Markers. + + Parameters + ---------- + %s + %s + %s + %s + kwargs: + Additional keyword arguments are passed to + :class:`hyperspy.external.matplotlib.collections.SquareCollection`. + """ + if kwargs.setdefault("transform", "display") != "display": + raise ValueError( + "The transform argument is not supported for Squares Markers. Instead, " + "use the offset_transform argument to specify the transform of the " + "offsets and use the ``units`` argument to specify transform of the " + "sizes." + ) + + super().__init__( + collection=SquareCollection, + offsets=offsets, + widths=widths, + angles=angles, + offset_transform=offset_transform, + units=units, + **kwargs, + ) + + __init__.__doc__ %= ( + OFFSET_DOCSTRING, + WIDTHS_DOCSTRING, + ANGLES_DOCSTRING, + UNITS_DOCSTRING, + ) diff --git a/hyperspy/drawing/_markers/text.py b/hyperspy/drawing/_markers/text.py deleted file mode 100644 index d2a26aa64b..0000000000 --- a/hyperspy/drawing/_markers/text.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy.drawing.marker import MarkerBase - - -class Text(MarkerBase): - - """Text marker that can be added to the signal figure - - Parameters - ---------- - x : array or float - The position of the text in x. If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - y : array or float - The position of the text in y. see x arguments - text : array or str - The text. see x arguments - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> s = hs.signals.Signal1D(np.arange(100).reshape([10,10])) - >>> s.plot(navigator='spectrum') - >>> for i in range(10): - >>> m = hs.plot.markers.text(y=range(50,1000,100)[i], - >>> x=i, text='abcdefghij'[i]) - >>> s.add_marker(m, plot_on_signal=False) - >>> m = hs.plot.markers.text(x=5, y=range(7,110, 10), - >>> text=[i for i in 'abcdefghij']) - >>> s.add_marker(m) - - Add a marker permanently to a signal - - >>> s = hs.signals.Signal1D(np.arange(100).reshape([10,10])) - >>> m = hs.plot.markers.text(5, 5, "a_text") - >>> s.add_marker(m, permanent=True) - """ - - def __init__(self, x, y, text, **kwargs): - MarkerBase.__init__(self) - lp = {'color': 'black'} - self.marker_properties = lp - self.set_data(x1=x, y1=y, text=text) - self.set_marker_properties(**kwargs) - self.name = 'text' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('x1'), - self.get_data_position('y1'), - self.get_data_position('text'), - self.marker_properties['color'], - ) - return(string) - - def update(self): - if self.auto_update is False: - return - self.marker.set_position([self.get_data_position('x1'), - self.get_data_position('y1')]) - self.marker.set_text(self.get_data_position('text')) - - def _plot_marker(self): - self.marker = self.ax.text( - self.get_data_position('x1'), self.get_data_position('y1'), - self.get_data_position('text'), **self.marker_properties) diff --git a/hyperspy/drawing/_markers/texts.py b/hyperspy/drawing/_markers/texts.py new file mode 100644 index 0000000000..2ede3f0378 --- /dev/null +++ b/hyperspy/drawing/_markers/texts.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +from hyperspy.docstrings.markers import OFFSET_DOCSTRING +from hyperspy.drawing.markers import Markers +from hyperspy.external.matplotlib.collections import TextCollection + + +class Texts(Markers): + """ + A set of text markers + """ + + _position_key = "offsets" + + def __init__(self, offsets, offset_transform="data", transform="display", **kwargs): + """ + Initialize the set of Circle Markers. + + Parameters + ---------- + %s + sizes : array-like + The size of the text in points. + facecolors : (list of) matplotlib color + Set the facecolor(s) of the markers. It can be a color + (all patches have same color), or a sequence of colors; + if it is a sequence the patches will cycle through the sequence. + If c is 'none', the patch will not be filled. + kwargs : dict + Keyword arguments are passed to :class:`matplotlib.collections.CircleCollection`. + """ + super().__init__( + collection=TextCollection, + offsets=offsets, + offset_transform=offset_transform, + transform=transform, + **kwargs, + ) + + __init__.__doc__ %= OFFSET_DOCSTRING diff --git a/hyperspy/drawing/_markers/vertical_line.py b/hyperspy/drawing/_markers/vertical_line.py deleted file mode 100644 index 3d8641f4d1..0000000000 --- a/hyperspy/drawing/_markers/vertical_line.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy.drawing.marker import MarkerBase - - -class VerticalLine(MarkerBase): - - """Vertical line marker that can be added to the signal figure - - Parameters - ---------- - x : array or float - The position of the line. If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> s = hs.signals.Signal1D(np.random.random([10, 100])) - >>> m = hs.plot.markers.vertical_line(x=range(10), color='green') - >>> s.add_marker(m) - - Adding a marker permanently to a signal - - >>> s = hs.signals.Signal1D(np.random.random((100, 100))) - >>> m = hs.plot.markers.vertical_line(x=30) - >>> s.add_marker(m, permanent=True) - """ - - def __init__(self, x, **kwargs): - MarkerBase.__init__(self) - lp = {'linewidth': 1, 'color': 'black'} - self.marker_properties = lp - self.set_data(x1=x) - self.set_marker_properties(**kwargs) - self.name = 'vertical_line' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('x1'), - self.marker_properties['color'], - ) - return(string) - - def update(self): - if self.auto_update is False: - return - self.marker.set_xdata(self.get_data_position('x1')) - - def _plot_marker(self): - self.marker = self.ax.axvline(self.get_data_position('x1'), - **self.marker_properties) diff --git a/hyperspy/drawing/_markers/vertical_line_segment.py b/hyperspy/drawing/_markers/vertical_line_segment.py deleted file mode 100644 index 14dfd2194a..0000000000 --- a/hyperspy/drawing/_markers/vertical_line_segment.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import matplotlib.pyplot as plt - -from hyperspy.drawing.marker import MarkerBase - - -class VerticalLineSegment(MarkerBase): - - """Vertical line segment marker that can be added to the signal figure - - Parameters - ---------- - x : array or float - The position of line segment in x. - If float, the marker is fixed. - If array, the marker will be updated when navigating. The array should - have the same dimensions in the navigation axes. - y1 : array or float - The position of the start of the line segment in x. - see x1 arguments - y2 : array or float - The position of the start of the line segment in y. - see x1 arguments - kwargs : - Keywords argument of axvline valid properties (i.e. recognized by - mpl.plot). - - Example - ------- - >>> im = hs.signals.Signal2D(np.zeros((100, 100))) - >>> m = hs.plot.markers.vertical_line_segment( - >>> x=20, y1=30, y2=70, linewidth=4, color='red', linestyle='dotted') - >>> im.add_marker(m) - - Add a marker permanently to a marker - - >>> im = hs.signals.Signal2D(np.zeros((60, 60))) - >>> m = hs.plot.markers.vertical_line_segment(x=10, y1=20, y2=50) - >>> im.add_marker(m, permanent=True) - """ - - def __init__(self, x, y1, y2, **kwargs): - MarkerBase.__init__(self) - lp = {'color': 'black', 'linewidth': 1} - self.marker_properties = lp - self.set_data(x1=x, y1=y1, y2=y2) - self.set_marker_properties(**kwargs) - self.name = 'vertical_line_segment' - - def __repr__(self): - string = "".format( - self.__class__.__name__, - self.name, - self.get_data_position('x1'), - self.get_data_position('y1'), - self.get_data_position('y2'), - self.marker_properties['color'], - ) - return(string) - - def update(self): - if self.auto_update is False: - return - self._update_segment() - - def _plot_marker(self): - self.marker = self.ax.vlines(0, 0, 1, **self.marker_properties) - self._update_segment() - - def _update_segment(self): - segments = self.marker.get_segments() - segments[0][0, 0] = self.get_data_position('x1') - segments[0][1, 0] = segments[0][0, 0] - if self.get_data_position('y1') is None: - segments[0][0, 1] = plt.getp(self.marker.axes, 'ylim')[0] - else: - segments[0][0, 1] = self.get_data_position('y1') - if self.get_data_position('y2') is None: - segments[0][1, 1] = plt.getp(self.marker.axes, 'ylim')[1] - else: - segments[0][1, 1] = self.get_data_position('y2') - self.marker.set_segments(segments) diff --git a/hyperspy/drawing/_markers/vertical_lines.py b/hyperspy/drawing/_markers/vertical_lines.py new file mode 100644 index 0000000000..c73d2ace28 --- /dev/null +++ b/hyperspy/drawing/_markers/vertical_lines.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import copy + +import numpy as np +from matplotlib.collections import LineCollection + +from hyperspy.drawing.markers import Markers + + +class VerticalLines(Markers): + """A set of Vertical Line Markers""" + + _position_key = "offsets" + _position_key_to_set = "segments" + + def __init__(self, offsets, **kwargs): + """ + Initialize the set of Vertical Line Markers. + + Parameters + ---------- + x: [n] + Positions of the markers + kwargs: dict + Keyword arguments passed to the underlying marker collection. Any argument + that is array-like and has `dtype=object` is assumed to be an iterating + argument and is treated as such. + + Examples + -------- + >>> import hyperspy.api as hs + >>> import numpy as np + >>> # Create a Signal2D with 2 navigation dimensions + >>> rng = np.random.default_rng(0) + >>> data = rng.random((25, 25, 100)) + >>> s = hs.signals.Signal1D(data) + >>> offsets = np.array([10, 20, 40]) + >>> # Create the markers + >>> m = hs.plot.markers.VerticalLines( + ... offsets=offsets, + ... linewidth=3, + ... colors=['r', 'g', 'b'], + ... ) + >>> # Add the marker to the signal + >>> s.plot() + >>> s.add_marker(m) + """ + if ( + kwargs.setdefault("offset_transform", "display") != "display" + or kwargs.setdefault("transform", "xaxis") != "xaxis" + ): + raise ValueError( + "Setting 'offset_transform' or 'transform' argument is not " + "supported with the VerticalLines markers." + ) + + super().__init__(collection=LineCollection, offsets=offsets, **kwargs) + + def get_current_kwargs(self, only_variable_length=False): + kwargs = super().get_current_kwargs(only_variable_length=only_variable_length) + # Need to take a deepcopy to avoid changing `self.kwargs` + kwds = copy.deepcopy(kwargs) + kwds[self._position_key_to_set] = np.array( + [[[x, 0], [x, 1]] for x in kwds.pop(self._position_key)] + ) + return kwds diff --git a/hyperspy/drawing/_widgets/__init__.py b/hyperspy/drawing/_widgets/__init__.py index c41a8ae211..3d813a7dda 100644 --- a/hyperspy/drawing/_widgets/__init__.py +++ b/hyperspy/drawing/_widgets/__init__.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . diff --git a/hyperspy/drawing/_widgets/circle.py b/hyperspy/drawing/_widgets/circle.py index 9f9d0f83c9..ef9ca8b06a 100644 --- a/hyperspy/drawing/_widgets/circle.py +++ b/hyperspy/drawing/_widgets/circle.py @@ -1,30 +1,29 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import numpy as np import matplotlib.pyplot as plt +import numpy as np -from hyperspy.drawing.widgets import Widget2DBase, ResizersMixin +from hyperspy.drawing.widget import ResizersMixin, Widget2DBase class CircleWidget(Widget2DBase, ResizersMixin): - """CircleWidget is a symmetric, Cicle-patch based widget, which can be dragged, and resized by keystrokes/code. """ @@ -32,7 +31,7 @@ class CircleWidget(Widget2DBase, ResizersMixin): def __init__(self, axes_manager, **kwargs): super(CircleWidget, self).__init__(axes_manager, **kwargs) self.size_step = 1.0 - self.size_snap_offset = (0.5 + 1e-8) + self.size_snap_offset = 0.5 + 1e-8 def _set_axes(self, axes): super(CircleWidget, self)._set_axes(axes) @@ -47,8 +46,13 @@ def _do_snap_size(self, value=None): snap_offset = self.size_snap_offset * self.axes[0].scale snap_spacing = self.axes[0].scale * self.size_step for i in range(2): - value[i] = max(0, (round((value[i] - snap_offset) / snap_spacing) * - snap_spacing + snap_offset)) + value[i] = max( + 0, + ( + round((value[i] - snap_offset) / snap_spacing) * snap_spacing + + snap_offset + ), + ) return value def _set_size(self, value): @@ -56,8 +60,7 @@ def _set_size(self, value): change, if the value has changed. """ # Override so that r_inner can be 0 - value = np.minimum(value, - [0.5 * ax.size * ax.scale for ax in self.axes]) + value = np.minimum(value, [0.5 * ax.size * ax.scale for ax in self.axes]) # Changed from base: min_sizes = np.array(((0.5 + 1e-8) * self.axes[0].scale, 0)) value = np.maximum(value, min_sizes) @@ -71,8 +74,7 @@ def _set_size(self, value): self._size_changed() def increase_size(self): - """Increment all sizes by one step. Applied via 'size' property. - """ + """Increment all sizes by one step. Applied via 'size' property.""" s = np.array(self.size) if self.size[1] > 0: s += self.size_step * self.axes[0].scale @@ -81,8 +83,7 @@ def increase_size(self): self.size = s def decrease_size(self): - """Decrement all sizes by one step. Applied via 'size' property. - """ + """Decrement all sizes by one step. Applied via 'size' property.""" s = np.array(self.size) if self.size[1] > 0: s -= self.size_step * self.axes[0].scale @@ -106,34 +107,60 @@ def _set_patch(self): super(CircleWidget, self)._set_patch() xy = self._get_patch_xy() ro, ri = self.size - self.patch = [plt.Circle( - xy, radius=ro, - fill=False, - lw=self.border_thickness, - ec=self.color, - alpha=self.alpha, - picker=True,)] + self._patch = [ + plt.Circle( + xy, + radius=ro, + fill=False, + lw=self.border_thickness, + ec=self.color, + alpha=self.alpha, + picker=True, + ) + ] if ri > 0: - self.patch.append( + self._patch.append( plt.Circle( - xy, radius=ri, + xy, + radius=ri, fill=False, lw=self.border_thickness, ec=self.color, alpha=self.alpha, - picker=True,)) + picker=True, + ) + ) def _validate_pos(self, value): - """Constrict the position within bounds. - """ - value = (min(value[0], self.axes[0].high_value - self._size[0] + - (0.5 + 1e-8) * self.axes[0].scale), - min(value[1], self.axes[1].high_value - self._size[0] + - (0.5 + 1e-8) * self.axes[1].scale)) - value = (max(value[0], self.axes[0].low_value + self._size[0] - - (0.5 + 1e-8) * self.axes[0].scale), - max(value[1], self.axes[1].low_value + self._size[0] - - (0.5 + 1e-8) * self.axes[1].scale)) + """Constrict the position within bounds.""" + value = ( + min( + value[0], + self.axes[0].high_value + - self._size[0] + + (0.5 + 1e-8) * self.axes[0].scale, + ), + min( + value[1], + self.axes[1].high_value + - self._size[0] + + (0.5 + 1e-8) * self.axes[1].scale, + ), + ) + value = ( + max( + value[0], + self.axes[0].low_value + + self._size[0] + - (0.5 + 1e-8) * self.axes[0].scale, + ), + max( + value[1], + self.axes[1].low_value + + self._size[0] + - (0.5 + 1e-8) * self.axes[1].scale, + ), + ) return super(CircleWidget, self)._validate_pos(value) def get_size_in_indices(self): @@ -156,8 +183,8 @@ def _update_patch_size(self): if len(self.patch) == 1: # Need to remove the previous patch before using # `_add_patch_to` - self.ax.artists.remove(self.patch[0]) - self.patch = [] + self._patch[0].remove() + self._patch = [] self._add_patch_to(self.ax) self.patch[1].radius = ri self._update_resizers() @@ -175,7 +202,7 @@ def _update_patch_geometry(self): self.draw_patch() def _onmousemove(self, event): - 'on mouse motion move the patch if picked' + "on mouse motion move the patch if picked" if self.picked is True and event.inaxes: x = event.xdata y = event.ydata @@ -195,17 +222,17 @@ def _onmousemove(self, event): def _get_resizer_pos(self): positions = [] - indices = (0, 1) if len(self.size) > 1 else (0, ) + indices = (0, 1) if len(self.size) > 1 else (0,) for i in indices: r = self._size[i] rsize = self._get_resizer_size() / 2 rp = np.array(self._get_patch_xy()) - p = rp - (r, 0) - rsize # Left + p = rp - (r, 0) - rsize # Left positions.append(p) - p = rp - (0, r) - rsize # Top + p = rp - (0, r) - rsize # Top positions.append(p) - p = rp + (r, 0) - rsize # Right + p = rp + (r, 0) - rsize # Right positions.append(p) - p = rp + (0, r) - rsize # Bottom + p = rp + (0, r) - rsize # Bottom positions.append(p) return positions diff --git a/hyperspy/drawing/_widgets/horizontal_line.py b/hyperspy/drawing/_widgets/horizontal_line.py index 53d3b48e18..e949869318 100644 --- a/hyperspy/drawing/_widgets/horizontal_line.py +++ b/hyperspy/drawing/_widgets/horizontal_line.py @@ -1,46 +1,53 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy.drawing.widgets import Widget1DBase -from hyperspy.drawing.utils import picker_kwargs from hyperspy.defaults_parser import preferences +from hyperspy.drawing.utils import picker_kwargs +from hyperspy.drawing.widget import Widget1DBase class HorizontalLineWidget(Widget1DBase): - - """A draggable, horizontal line widget. - """ + """A draggable, horizontal line widget.""" def _update_patch_position(self): if self.is_on and self.patch: - self.patch[0].set_ydata(self._pos[0]) + self.patch[0].set_ydata([self._pos[0]]) self.draw_patch() + def _add_patch_to(self, ax): + """Create and add the matplotlib patches to 'ax'""" + self.blit = hasattr(ax, "hspy_fig") and ax.figure.canvas.supports_blit + self._set_patch() + for p in self.patch: + p.set_animated(self.blit) + def _set_patch(self): ax = self.ax kwargs = picker_kwargs(preferences.Plot.pick_tolerance) - self.patch = [ax.axhline( - self._pos[0], - color=self.color, - alpha=self.alpha, - **kwargs)] + self._patch = [ + ax.axhline(self._pos[0], color=self.color, alpha=self.alpha, **kwargs) + ] def _onmousemove(self, event): """on mouse motion draw the cursor if picked""" if self.picked is True and event.inaxes: self.position = (event.ydata,) + + def _onjumpclick(self, event): + if event.key == "shift" and event.inaxes and self.is_pointer: + self.position = (event.ydata,) diff --git a/hyperspy/drawing/_widgets/label.py b/hyperspy/drawing/_widgets/label.py index 81014e966c..0442da279f 100644 --- a/hyperspy/drawing/_widgets/label.py +++ b/hyperspy/drawing/_widgets/label.py @@ -1,37 +1,36 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import numpy as np import matplotlib.transforms as transforms +import numpy as np -from hyperspy.drawing.widgets import Widget1DBase +from hyperspy.drawing.widget import Widget1DBase class LabelWidget(Widget1DBase): - """A draggable text widget. Adds the attributes 'string' and 'bbox'. These are all arguments for matplotlib's Text artist. The default y-coordinate of the label is set to 0.9. """ - def __init__(self, axes_manager, color='black', **kwargs): + def __init__(self, axes_manager, color="black", **kwargs): super(LabelWidget, self).__init__(axes_manager, color=color, **kwargs) - self._string = '' + self._string = "" self._snap_position = False if not self.axes: self._pos = np.array((0, 0.9)) @@ -46,8 +45,7 @@ def _set_string(self, value): self._string = value self._update_patch_string() - string = property(lambda s: s._get_string(), - lambda s, v: s._set_string(v)) + string = property(lambda s: s._get_string(), lambda s, v: s._set_string(v)) def _set_position(self, position): try: @@ -64,8 +62,7 @@ def _set_axes(self, axes): if len(self.axes) == 1: self._pos = np.array((self.axes[0].offset, 0.9)) else: - self._pos = np.array((self.axes[0].offset, - self.axes[1].offset)) + self._pos = np.array((self.axes[0].offset, self.axes[1].offset)) def _validate_pos(self, pos): if len(self.axes) == 1: @@ -91,18 +88,20 @@ def _update_patch_string(self): def _set_patch(self): ax = self.ax - trans = transforms.blended_transform_factory( - ax.transData, ax.transAxes) - self.patch = [ax.text( - self._pos[0], - self._pos[1], - self.string, - color=self.color, - alpha=self.alpha, - transform=trans, - horizontalalignment='left', - bbox=self.bbox, - picker=True)] + trans = transforms.blended_transform_factory(ax.transData, ax.transAxes) + self._patch = [ + ax.text( + self._pos[0], + self._pos[1], + self.string, + color=self.color, + alpha=self.alpha, + transform=trans, + horizontalalignment="left", + bbox=self.bbox, + picker=True, + ) + ] def _onmousemove(self, event): """on mouse motion draw the cursor if picked""" diff --git a/hyperspy/drawing/_widgets/line2d.py b/hyperspy/drawing/_widgets/line2d.py index 172f0cdfab..7dbef167b5 100644 --- a/hyperspy/drawing/_widgets/line2d.py +++ b/hyperspy/drawing/_widgets/line2d.py @@ -1,48 +1,48 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging -import numpy as np + import matplotlib.pyplot as plt +import numpy as np -from hyperspy.drawing.widgets import ResizableDraggableWidgetBase from hyperspy.drawing.utils import picker_kwargs - +from hyperspy.drawing.widget import ResizableDraggableWidgetBase _logger = logging.getLogger(__name__) def unit_vector(vector): - """ Returns the unit vector of the vector. """ + """Returns the unit vector of the vector.""" return vector / np.linalg.norm(vector) def angle_between(v1, v2): - """ Returns the angle in radians between the vectors 'v1' and 'v2'. + """Returns the angle in radians between the vectors 'v1' and 'v2'. Examples -------- - >>> angle_between((1, 0), (0, 1)) + >>> print(angle_between((1, 0), (0, 1))) 1.5707963267948966 - >>> angle_between((1, 0), (1, 0)) + >>> print(angle_between((1, 0), (1, 0))) 0.0 - >>> angle_between((1, 0), (-1, 0)) + >>> print(angle_between((1, 0), (-1, 0))) 3.141592653589793 """ v1_u = unit_vector(v1) @@ -58,7 +58,6 @@ def angle_between(v1, v2): class Line2DWidget(ResizableDraggableWidgetBase): - """A free-form line on a 2D plot. Enables dragging and moving the end points, but also allows rotation of the widget by moving the mouse beyond the end points of the line. @@ -96,20 +95,20 @@ class Line2DWidget(ResizableDraggableWidgetBase): """ # Bitfield values for different mouse interaction functions - FUNC_NONE = 0 # Do nothing - FUNC_MOVE = 1 # Move the widget - FUNC_RESIZE = 2 # Move a vertex - FUNC_ROTATE = 4 # Rotate - FUNC_SIZERS = 8 # Change linewidth by indicators - FUNC_A = 32 # Resize/rotate by first vertex - FUNC_B = 64 # Resize/rotate by second vertex + FUNC_NONE = 0 # Do nothing + FUNC_MOVE = 1 # Move the widget + FUNC_RESIZE = 2 # Move a vertex + FUNC_ROTATE = 4 # Rotate + FUNC_SIZERS = 8 # Change linewidth by indicators + FUNC_A = 32 # Resize/rotate by first vertex + FUNC_B = 64 # Resize/rotate by second vertex def __init__(self, axes_manager, **kwargs): super(Line2DWidget, self).__init__(axes_manager, **kwargs) self.linewidth = 1 self.radius_move = self.radius_resize = 5 self.radius_rotate = 15 - self._mfunc = self.FUNC_NONE # Mouse interaction function + self._mfunc = self.FUNC_NONE # Mouse interaction function self._prev_pos = None self._orig_pos = None self.snap_all = False @@ -171,8 +170,7 @@ def connect_navigate(self): raise NotImplementedError("2D lines cannot be used to navigate (yet?)") def _validate_pos(self, pos): - """Make sure all vertices are within axis bounds. - """ + """Make sure all vertices are within axis bounds.""" if np.shape(pos)[1] != len(self.axes): raise ValueError() @@ -194,15 +192,17 @@ def _do_snap_position(self, value=None): def _set_snap_size(self, value): if value and self.axes[0].scale != self.axes[1].scale: - _logger.warning('Snapping the width of the line is not supported ' - 'for axes with different scale.') + _logger.warning( + "Snapping the width of the line is not supported " + "for axes with different scale." + ) return super()._set_snap_size(value) def _do_snap_size(self, value=None): if value is None: value = self._size[0] - if hasattr(value, '__len__'): + if hasattr(value, "__len__"): value = value[0] ax = self.axes[0] # take one axis, different axis scale not supported value = round(value / ax.scale) * ax.scale @@ -211,11 +211,11 @@ def _do_snap_size(self, value=None): return np.array([value]) def _get_line_normal(self): - v = np.diff(self._pos, axis=0) # Line vector + v = np.diff(self._pos, axis=0) # Line vector x = -v[:, 1] y = v[:, 0] - n = np.array([x, y]).T # Normal vector - return n / np.linalg.norm(n) # Normalized + n = np.array([x, y]).T # Normal vector + return n / np.linalg.norm(n) # Normalized def get_line_length(self): """Returns line length in axes coordinates. Requires units on all axes @@ -237,7 +237,7 @@ def _get_width_indicator_coords(self): [[[x0A, y0A], [x1A, y1A]], [[x0B, y0B], [x1B, y1B]]] Where A and B refer to the two lines """ - n = self.size[0] * self._get_line_normal() / 2. + n = self.size[0] * self._get_line_normal() / 2.0 c = np.array(self._pos) return c + n, c - n @@ -248,8 +248,7 @@ def _update_patch_size(self): self._update_patch_geometry() def _update_patch_geometry(self): - """Set line position, and set width indicator's if appropriate - """ + """Set line position, and set width indicator's if appropriate""" if self.is_on and self.patch: self.patch[0].set_data(np.array(self._pos).T) # Update width indicator if present @@ -263,22 +262,25 @@ def _set_patch(self): """Creates the line, and also creates the width indicators if appropriate. """ - self.ax.autoscale(False) # Prevent plotting from rescaling + self.ax.autoscale(False) # Prevent plotting from rescaling xy = np.array(self._pos) - max_r = max(self.radius_move, self.radius_resize, - self.radius_rotate) + max_r = max(self.radius_move, self.radius_resize, self.radius_rotate) kwargs = picker_kwargs(max_r) - self.patch = [plt.Line2D( - xy[:, 0], xy[:, 1], - linestyle='-', - lw=self.linewidth, - c=self.color, - alpha=self.alpha, - marker='s', - markersize=self.radius_resize, - mew=0.1, - mfc='lime', - **kwargs,)] + self._patch = [ + plt.Line2D( + xy[:, 0], + xy[:, 1], + linestyle="-", + lw=self.linewidth, + c=self.color, + alpha=self.alpha, + marker="s", + markersize=self.radius_resize, + mew=0.1, + mfc="lime", + **kwargs, + ) + ] if self._size[0] > 0: self._set_size_patch() @@ -287,31 +289,28 @@ def _set_size_patch(self): # The widget hasn't been fully added to an axis yet. return if self.axes[0].scale != self.axes[1].scale: - raise ValueError("linewidth is not supported for axis with " - "different scale.") + raise ValueError( + "linewidth is not supported for axis with " "different scale." + ) wc = self._get_width_indicator_coords() kwargs = picker_kwargs(self.radius_move) for i in range(2): wi = plt.Line2D( - *wc[i].T, - linestyle=':', - lw=self.linewidth, - c=self.color, - **kwargs) - self.patch.append(wi) + *wc[i].T, linestyle=":", lw=self.linewidth, c=self.color, **kwargs + ) + self._patch.append(wi) self._width_indicator_patches.append(wi) def _remove_size_patch(self): if not self._width_indicator_patches: return for patch in self._width_indicator_patches: - self.patch.remove(patch) + self._patch.remove(patch) patch.remove() self._width_indicator_patches = [] def _get_vertex(self, event): - """Check bitfield on self.func, and return vertex index. - """ + """Check bitfield on self.func, and return vertex index.""" if self.func & self.FUNC_A: return 0 elif self.func & self.FUNC_B: @@ -320,8 +319,7 @@ def _get_vertex(self, event): return None def _get_func_from_pos(self, cx, cy): - """Get interaction function from pixel position (cx,cy) - """ + """Get interaction function from pixel position (cx,cy)""" if not self.patch: return self.FUNC_NONE @@ -330,12 +328,12 @@ def _get_func_from_pos(self, cx, cy): # Calculate the distances to the vertecies, and find nearest one r2 = np.sum(np.power(p - np.array((cx, cy)), 2), axis=1) - mini = np.argmin(r2) # Index of nearest vertex - minr2 = r2[mini] # Distance squared to nearest vertex + mini = np.argmin(r2) # Index of nearest vertex + minr2 = r2[mini] # Distance squared to nearest vertex del r2 # Check for resize: Click within radius_resize from edge points radius = self.radius_resize - if minr2 <= radius ** 2: + if minr2 <= radius**2: ret = self.FUNC_RESIZE ret |= self.FUNC_A if mini == 0 else self.FUNC_B return ret @@ -344,11 +342,11 @@ def _get_func_from_pos(self, cx, cy): radius = self.radius_rotate A = p[0, :] # Vertex A B = p[1, :] # Vertex B. Assumes one line segment only. - c = np.array((cx, cy)) # mouse click position - t = np.dot(c - A, B - A) # t[0]: A->click, t[1]: A->B - bas = np.linalg.norm(B - A)**2 - if minr2 <= radius**2: # If within rotate radius - if t < 0.0 and mini == 0: # "Before" A on the line + c = np.array((cx, cy)) # mouse click position + t = np.dot(c - A, B - A) # t[0]: A->click, t[1]: A->B + bas = np.linalg.norm(B - A) ** 2 + if minr2 <= radius**2: # If within rotate radius + if t < 0.0 and mini == 0: # "Before" A on the line return self.FUNC_ROTATE | self.FUNC_A elif t > bas and mini == 1: # "After" B on the line return self.FUNC_ROTATE | self.FUNC_B @@ -368,7 +366,7 @@ def _get_func_from_pos(self, cx, cy): A = np.array(trans.transform(wc[i][0])) B = np.array(trans.transform(wc[i][1])) t = np.dot(c - A, B - A) - bas = np.linalg.norm(B - A)**2 + bas = np.linalg.norm(B - A) ** 2 if 0 < t < bas: # A + (t/bas)*(B-A) is closest point on line if np.linalg.norm(A + (t / bas) * (B - A) - c) < radius: @@ -388,8 +386,7 @@ def onpick(self, event): self._drag_start = [me.xdata, me.ydata] def _onmousemove(self, event): - """Delegate to _move(), _resize() or _rotate(). - """ + """Delegate to _move(), _resize() or _rotate().""" if self.picked is True: if self.func & self.FUNC_MOVE and event.inaxes: self._move(event) @@ -427,7 +424,7 @@ def _resize(self, event): """ ip = self._get_vertex(event) dx = self._get_diff(event) - p = np.array(self._pos) # Copy + p = np.array(self._pos) # Copy p[ip, 0:2] = self._drag_store[0][ip] + dx self.position = p @@ -450,19 +447,23 @@ def _rotate(self, event): c = trans.transform(np.mean(self._drag_store[0], axis=0)) # Figure out theta - v1 = (event.x, event.y) - c # Center to mouse - v2 = v1 - dx # Center to start pos - theta = angle_between(v2, v1) # Rotation between start and mouse + v1 = (event.x, event.y) - c # Center to mouse + v2 = v1 - dx # Center to start pos + theta = angle_between(v2, v1) # Rotation between start and mouse - if event.key is not None and 'shift' in event.key: + if event.key is not None and "shift" in event.key: base = 30 * np.pi / 180 theta = base * round(float(theta) / base) # vector from points to center w1 = c - trans.transform(self._drag_store[0]) # rotate into w2 for next point - w2 = np.array((w1[:, 0] * np.cos(theta) - w1[:, 1] * np.sin(theta), - w1[:, 1] * np.cos(theta) + w1[:, 0] * np.sin(theta))) + w2 = np.array( + ( + w1[:, 0] * np.cos(theta) - w1[:, 1] * np.sin(theta), + w1[:, 1] * np.cos(theta) + w1[:, 0] * np.sin(theta), + ) + ) self.position = trans.inverted().transform(c + np.rot90(w2)) def _width_resize(self, event): @@ -475,4 +476,4 @@ def _width_resize(self, event): dn = 2 * np.dot(n, dx) if self._selected_artist is self.patch[2]: dn *= -1 - self.size = np.abs(self._drag_store[1] + dn) + self.size = abs(self._drag_store[1] + dn) diff --git a/hyperspy/drawing/_widgets/range.py b/hyperspy/drawing/_widgets/range.py index 23e6333db6..1198bd61fe 100644 --- a/hyperspy/drawing/_widgets/range.py +++ b/hyperspy/drawing/_widgets/range.py @@ -1,46 +1,40 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import inspect import logging +import matplotlib import numpy as np -from matplotlib.widgets import SpanSelector +from packaging.version import Version -from hyperspy.drawing.widgets import ResizableDraggableWidgetBase -from hyperspy.events import Events, Event from hyperspy.defaults_parser import preferences +from hyperspy.drawing.widget import ResizableDraggableWidgetBase +if Version(matplotlib.__version__) >= Version("3.6.0"): + from matplotlib.widgets import SpanSelector +else: + from hyperspy.external.matplotlib.widgets import SpanSelector _logger = logging.getLogger(__name__) -# Track if we have already warned when the widget is out of range -already_warn_out_of_range = False - - -def in_interval(number, interval): - if interval[0] <= number <= interval[1]: - return True - else: - return False class RangeWidget(ResizableDraggableWidgetBase): - """RangeWidget is a span-patch based widget, which can be dragged and resized by mouse/keys. Basically a wrapper for ModifiablepanSelector so that it conforms to the common widget interface. @@ -53,13 +47,27 @@ class RangeWidget(ResizableDraggableWidgetBase): will always stay within bounds. """ - def __init__(self, axes_manager, ax=None, alpha=0.5, **kwargs): + def __init__(self, axes_manager, ax=None, color="r", alpha=0.25, **kwargs): # Parse all kwargs for the matplotlib SpanSelector self._SpanSelector_kwargs = {} for key in inspect.signature(SpanSelector).parameters.keys(): if key in kwargs: self._SpanSelector_kwargs[key] = kwargs.pop(key) - super(RangeWidget, self).__init__(axes_manager, alpha=alpha, **kwargs) + + self._SpanSelector_kwargs.update( + dict( + onselect=lambda *args, **kwargs: None, + interactive=True, + onmove_callback=self._span_changed, + drag_from_anywhere=True, + ignore_event_outside=True, + grab_range=preferences.Plot.pick_tolerance, + ) + ) + self._SpanSelector_kwargs.setdefault("direction", "horizontal") + super(RangeWidget, self).__init__( + axes_manager, color=color, alpha=alpha, **kwargs + ) self.span = None def set_on(self, value, render_figure=True): @@ -69,46 +77,99 @@ def set_on(self, value, render_figure=True): self.connect(self.ax) elif value is False: self.disconnect() + self.span.clear() + self.span = None + self.ax = None if render_figure: self.draw_patch() - if value is False: - self.ax = None + self._is_on = value + @property + def color(self): + return self._color + + @color.setter + def color(self, color): + self._color = color + if getattr(self, "span", None) is not None: + self.span.set_props(color=color) + self.span.set_handle_props(color=color) + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, alpha): + self._alpha = alpha + if getattr(self, "span", None) is not None: + self.span.set_props(alpha=alpha) + self.span.set_handle_props(alpha=min(1.0, alpha * 2)) + + @property + def patch(self): + return self.span.artists + + def _do_snap_position(self, *args, **kwargs): + # set span extents to snap position + self._set_span_extents(*self.span.extents) + return self.span.extents[0] + + def _set_snap_position(self, value): + self._snap_position = value + if self.span is None: + return + axis = self.axes[0] + if value and axis.is_uniform: + o, values = axis.scale / 2, axis.axis + self.span.snap_values = np.append(values - o, [values[-1] + o]) + self._do_snap_position() + else: + self.span.snap_values = None + def _add_patch_to(self, ax): - self.span = ModifiableSpanSelector(ax, **self._SpanSelector_kwargs) - self.span.set_initial(self._get_range()) - self.span.bounds_check = True - self.span.snap_position = self.snap_position - self.span.snap_size = self.snap_size - self.span.can_switch = True - self.span.events.changed.connect(self._span_changed, {'obj': 'widget'}) - self.span.step_ax = self.axes[0] - self.span.tolerance = preferences.Plot.pick_tolerance - self.patch = [self.span.rect] - self.patch[0].set_color(self.color) - self.patch[0].set_alpha(self.alpha) - - def _span_changed(self, widget): - r = self._get_range() - pr = widget.range - if r != pr: - dx = self.axes[0].scale - x = pr[0] + 0.5 * dx - w = pr[1] + 0.5 * dx - x - old_position, old_size = self.position, self.size - self._pos = np.array([x]) - self._size = np.array([w]) - self._validate_geometry() - if self._pos != np.array([x]) or self._size != np.array([w]): - self._update_patch_size() - self._apply_changes(old_size=old_size, old_position=old_position) + self.ax = ax + self._SpanSelector_kwargs.update( + props={"alpha": self.alpha, "color": self.color}, + handle_props={"alpha": min(1.0, self.alpha * 2), "color": self.color}, + useblit=ax.figure.canvas.supports_blit, + ) + self.span = SpanSelector(ax, **self._SpanSelector_kwargs) + self._set_span_extents(*self._get_range()) + self._patch = list(self.span.artists) + + def disconnect(self): + self.span.disconnect_events() + super().disconnect() + + def _set_span_extents(self, left, right): + self.span.extents = (left, right) + try: + # For https://github.com/matplotlib/matplotlib/pull/27409 + self.span._selection_completed = True + except AttributeError: + # Remove when minimum matplotlib is > 3.8.2 + pass + # update internal state range widget + self._span_changed() + + def _span_changed(self, *args, **kwargs): + # This is connected to matplotlib event and will be called + # on any mouse movement with the span selected + extents = self.span.extents + old_range = self._get_range() + # Set extents to self._pos and self._size + self._pos = np.array([extents[0]]) + self._size = np.array([extents[1] - extents[0]]) + # to avoid trigger events when snap is on and the extent + # actually didn't change. + if self._get_range() != old_range: + self.events.changed.trigger(self) def _get_range(self): p = self._pos[0] w = self._size[0] - offset = self.axes[0].scale - p -= 0.5 * offset return (p, p + w) def _parse_bounds_args(self, args, kwargs): @@ -116,14 +177,14 @@ def _parse_bounds_args(self, args, kwargs): return args[0] elif len(args) == 4: return args - elif len(kwargs) == 1 and 'bounds' in kwargs: + elif len(kwargs) == 1 and "bounds" in kwargs: return kwargs.values()[0] else: - x = kwargs.pop('x', kwargs.pop('left', self._pos[0])) - if 'right' in kwargs: - w = kwargs.pop('right') - x + x = kwargs.pop("x", kwargs.pop("left", self._pos[0])) + if "right" in kwargs: + w = kwargs.pop("right") - x else: - w = kwargs.pop('w', kwargs.pop('width', self._size[0])) + w = kwargs.pop("w", kwargs.pop("width", self._size[0])) return x, w def set_ibounds(self, *args, **kwargs): @@ -145,11 +206,7 @@ def set_ibounds(self, *args, **kwargs): ix, iw = self._parse_bounds_args(args, kwargs) x = self.axes[0].index2value(ix) w = self._i2v(self.axes[0], ix + iw) - x - - old_position, old_size = self.position, self.size - self._pos = np.array([x]) - self._size = np.array([w]) - self._apply_changes(old_size=old_size, old_position=old_position) + self.set_bounds(left=x, width=w) def set_bounds(self, *args, **kwargs): """ @@ -166,50 +223,13 @@ def set_bounds(self, *args, **kwargs): If specifying with keywords, any unspecified dimensions will be kept constant (note: width will be kept, not right). """ - global already_warn_out_of_range - - def warn(obj, parameter, value): - global already_warn_out_of_range - if not already_warn_out_of_range: - _logger.info('{}: {} is out of range. It is therefore set ' - 'to the value of {}'.format(obj, parameter, value)) - already_warn_out_of_range = True - x, w = self._parse_bounds_args(args, kwargs) - l0, h0 = self.axes[0].low_value, self.axes[0].high_value - scale = self.axes[0].scale - - in_range = 0 - if x < l0: - x = l0 - warn(self, '`x` or `left`', x) - elif h0 <= x: - x = h0 - scale - warn(self, '`x` or `left`', x) - else: - in_range += 1 - if w < scale: - w = scale - warn(self, '`width` or `right`', w) - elif not (l0 + scale <= x + w <= h0 + scale): - if self.size != np.array([w]): # resize - w = h0 + scale - self.position[0] - warn(self, '`width` or `right`', w) - if self.position != np.array([x]): # moved - x = h0 + scale - self.size[0] - warn(self, '`x` or `left`', x) - else: - in_range += 1 - - # if we are in range again, reset `already_warn_out_of_range` to False - if in_range == 2 and already_warn_out_of_range: - _logger.info('{} back in range.'.format(self.__class__.__name__)) - already_warn_out_of_range = False - - old_position, old_size = self.position, self.size - self._pos = np.array([x]) - self._size = np.array([w]) - self._apply_changes(old_size=old_size, old_position=old_position) + if self.span is not None: + axis = self.axes[0] + if axis.is_uniform and w <= axis.scale: + w = axis.scale + x0, x1 = np.clip([x, x + w], axis.axis[0], axis.axis[-1]) + self._set_span_extents(x0, x1) def _update_patch_position(self): self._update_patch_geometry() @@ -219,399 +239,4 @@ def _update_patch_size(self): def _update_patch_geometry(self): if self.is_on and self.span is not None: - self.span.range = self._get_range() - - def disconnect(self): - super(RangeWidget, self).disconnect() - if self.span: - self.span.turn_off() - self.span = None - - def _set_snap_position(self, value): - super(RangeWidget, self)._set_snap_position(value) - if self.span is not None: - self.span.snap_position = value - self._update_patch_geometry() - - def _set_snap_size(self, value): - super(RangeWidget, self)._set_snap_size(value) - if self.span is not None: - self.span.snap_size = value - self._update_patch_size() - - def _validate_geometry(self, x1=None): - """Make sure the entire patch always stays within bounds. First the - position (either from position property or from x1 argument), is - limited within the bounds. Then, if the right edge are out of - bounds, the position is changed so that they will be at the limit. - - The modified geometry is stored, but no change checks are performed. - Call _apply_changes after this in order to process any changes (the - size might change if it is set larger than the bounds size). - """ - xaxis = self.axes[0] - - # Make sure widget size is not larger than axes - self._size[0] = min(self._size[0], xaxis.size * xaxis.scale) - - # Make sure x1 is within bounds - if x1 is None: - x1 = self._pos[0] # Get it if not supplied - if x1 < xaxis.low_value: - x1 = xaxis.low_value - elif x1 > xaxis.high_value: - x1 = xaxis.high_value - - # Make sure x2 is with upper bound. - # If not, keep dims, and change x1! - x2 = x1 + self._size[0] - if x2 > xaxis.high_value + xaxis.scale: - x2 = xaxis.high_value + xaxis.scale - x1 = x2 - self._size[0] - - self._pos = np.array([x1]) - # Apply snaps if appropriate - if self.snap_position: - self._do_snap_position() - if self.snap_size: - self._do_snap_size() - - -class ModifiableSpanSelector(SpanSelector): - - def __init__(self, ax, **kwargs): - onselect = kwargs.pop('onselect', self.dummy) - direction = kwargs.pop('direction', 'horizontal') - useblit = kwargs.pop('useblit', ax.figure.canvas.supports_blit) - SpanSelector.__init__(self, ax, onselect, direction=direction, - useblit=useblit, span_stays=False, **kwargs) - # The tolerance in points to pick the rectangle sizes - self.tolerance = preferences.Plot.pick_tolerance - self.on_move_cid = None - self._range = None - self.step_ax = None - self.bounds_check = False - self._button_down = False - self.snap_size = False - self.snap_position = False - self.events = Events() - self.events.changed = Event(doc=""" - Event that triggers when the widget was changed. - - Arguments: - ---------- - obj: - The widget that changed - """, arguments=['obj']) - self.events.moved = Event(doc=""" - Event that triggers when the widget was moved. - - Arguments: - ---------- - obj: - The widget that changed - """, arguments=['obj']) - self.events.resized = Event(doc=""" - Event that triggers when the widget was resized. - - Arguments: - ---------- - obj: - The widget that changed - """, arguments=['obj']) - self.can_switch = False - - def dummy(self, *args, **kwargs): - pass - - def _get_range(self): - self.update_range() - return self._range - - def _set_range(self, value): - self.update_range() - if self._range != value: - resized = ( - self._range[1] - - self._range[0]) != ( - value[1] - - value[0]) - moved = self._range[0] != value[0] - self._range = value - if moved: - self._set_span_x(value[0]) - self.events.moved.trigger(self) - if resized: - self._set_span_width(value[1] - value[0]) - self.events.resized.trigger(self) - if moved or resized: - self.draw_patch() - self.events.changed.trigger(self) - - range = property(_get_range, _set_range) - - def _set_span_x(self, value): - if self.direction == 'horizontal': - self.rect.set_x(value) - else: - self.rect.set_y(value) - - def _set_span_width(self, value): - if self.direction == 'horizontal': - self.rect.set_width(value) - else: - self.rect.set_height(value) - - def _get_span_x(self): - if self.direction == 'horizontal': - return self.rect.get_x() - else: - return self.rect.get_y() - - def _get_span_width(self): - if self.direction == 'horizontal': - return self.rect.get_width() - else: - return self.rect.get_height() - - def _get_mouse_position(self, event): - if self.direction == 'horizontal': - return event.xdata - else: - return event.ydata - - def set_initial(self, initial_range=None): - """ - Remove selection events, set the spanner, and go to modify mode. - """ - if initial_range is not None: - self.range = initial_range - - self.disconnect_events() - # And connect to the new ones - self.connect_event('button_press_event', self.mm_on_press) - self.connect_event('button_release_event', self.mm_on_release) - - self.rect.set_visible(True) - self.rect.contains = self.contains - - def update(self, *args): - # Override the SpanSelector `update` method to blit properly all - # artirts before we go to "modify mode" in `set_initial`. - self.set_visible(True) - - def draw_patch(self, *args): - """Update the patch drawing. - """ - try: - if hasattr(self.ax, 'hspy_fig'): - self.ax.hspy_fig.render_figure() - elif self.ax.figure is not None: - self.ax.figure.canvas.draw_idle() - except AttributeError: # pragma: no cover - pass # When figure is None, typically when closing - - def contains(self, mouseevent): - x, y = self.rect.get_transform().inverted().transform_point( - (mouseevent.x, mouseevent.y)) - v = x if self.direction == 'vertical' else y - # Assert y is correct first - if not (0.0 <= v <= 1.0): - return False, {} - x_pt = self._get_point_size_in_data_units() - hit = self._range[0] - x_pt, self._range[1] + x_pt - if hit[0] < self._get_mouse_position < hit[1]: - return True, {} - return False, {} - - def release(self, event): - """When the button is released, the span stays in the screen and the - iteractivity machinery passes to modify mode""" - if self.pressv is None or (self.ignore( - event) and not self._button_down): - return - self._button_down = False - self.update_range() - self.set_initial() - - def _get_point_size_in_data_units(self): - # Calculate the point size in data units - invtrans = self.ax.transData.inverted() - (x, y) = (1, 0) if self.direction == 'horizontal' else (0, 1) - x_pt = self.tolerance * abs((invtrans.transform((x, y)) - - invtrans.transform((0, 0)))[y]) - return x_pt - - def mm_on_press(self, event): - if self.ignore(event) and not self._button_down: - return - self._button_down = True - - x_pt = self._get_point_size_in_data_units() - - # Determine the size of the regions for moving and stretching - self.update_range() - left_region = self._range[0] - x_pt, self._range[0] + x_pt - right_region = self._range[1] - x_pt, self._range[1] + x_pt - middle_region = self._range[0] + x_pt, self._range[1] - x_pt - - if in_interval(self._get_mouse_position(event), left_region) is True: - self.on_move_cid = \ - self.canvas.mpl_connect('motion_notify_event', - self.move_left) - elif in_interval(self._get_mouse_position(event), right_region): - self.on_move_cid = \ - self.canvas.mpl_connect('motion_notify_event', - self.move_right) - elif in_interval(self._get_mouse_position(event), middle_region): - self.pressv = self._get_mouse_position(event) - self.on_move_cid = \ - self.canvas.mpl_connect('motion_notify_event', - self.move_rect) - else: - return - - def update_range(self): - self._range = (self._get_span_x(), - self._get_span_x() + self._get_span_width()) - - def switch_left_right(self, x, left_to_right): - if left_to_right: - if self.step_ax is not None: - if x > self.step_ax.high_value + self.step_ax.scale: - return - w = self._range[1] - self._range[0] - r0 = self._range[1] - self._set_span_x(r0) - r1 = r0 + w - self.canvas.mpl_disconnect(self.on_move_cid) - self.on_move_cid = \ - self.canvas.mpl_connect('motion_notify_event', - self.move_right) - else: - if self.step_ax is not None: - if x < self.step_ax.low_value - self.step_ax.scale: - return - w = self._range[1] - self._range[0] - r1 = self._range[0] - r0 = r1 - w - self.canvas.mpl_disconnect(self.on_move_cid) - self.on_move_cid = \ - self.canvas.mpl_connect('motion_notify_event', - self.move_left) - self._range = (r0, r1) - - def move_left(self, event): - if self._button_down is False or self.ignore(event): - return - x = self._get_mouse_position(event) - if self.step_ax is not None: - if (self.bounds_check and - x < self.step_ax.low_value - self.step_ax.scale): - return - if self.snap_position: - snap_offset = self.step_ax.offset - 0.5 * self.step_ax.scale - elif self.snap_size: - snap_offset = self._range[1] - if self.snap_position or self.snap_size: - rem = (x - snap_offset) % self.step_ax.scale - if rem / self.step_ax.scale < 0.5: - rem = -rem - else: - rem = self.step_ax.scale - rem - x += rem - # Do not move the left edge beyond the right one. - if x >= self._range[1]: - if self.can_switch and x > self._range[1]: - self.switch_left_right(x, True) - self.move_right(event) - return - width_increment = self._range[0] - x - if self._get_span_width() + width_increment <= 0: - return - self._set_span_x(x) - self._set_span_width(self._get_span_width() + width_increment) - self.update_range() - self.events.moved.trigger(self) - self.events.resized.trigger(self) - self.events.changed.trigger(self) - if self.onmove_callback is not None: - self.onmove_callback(*self._range) - self.draw_patch() - - def move_right(self, event): - if self._button_down is False or self.ignore(event): - return - x = self._get_mouse_position(event) - if self.step_ax is not None: - if (self.bounds_check and - x > self.step_ax.high_value + self.step_ax.scale): - return - if self.snap_size: - snap_offset = self._range[0] - rem = (x - snap_offset) % self.step_ax.scale - if rem / self.step_ax.scale < 0.5: - rem = -rem - else: - rem = self.step_ax.scale - rem - x += rem - # Do not move the right edge beyond the left one. - if x <= self._range[0]: - if self.can_switch and x < self._range[0]: - self.switch_left_right(x, False) - self.move_left(event) - return - width_increment = x - self._range[1] - if self._get_span_width() + width_increment <= 0: - return - self._set_span_width(self._get_span_width() + width_increment) - self.update_range() - self.events.resized.trigger(self) - self.events.changed.trigger(self) - if self.onmove_callback is not None: - self.onmove_callback(*self._range) - self.draw_patch() - - def move_rect(self, event): - if (self._button_down is False or self.ignore(event) or - self._get_mouse_position(event) is None): - return - x_increment = self._get_mouse_position(event) - self.pressv - if self.step_ax is not None: - if (self.bounds_check - and self._range[0] <= self.step_ax.low_value - and self._get_mouse_position(event) <= self.pressv): - return - if (self.bounds_check - and self._range[1] >= self.step_ax.high_value - and self._get_mouse_position(event) >= self.pressv): - return - if self.snap_position: - rem = x_increment % self.step_ax.scale - if rem / self.step_ax.scale < 0.5: - rem = -rem - else: - rem = self.step_ax.scale - rem - x_increment += rem - self._set_span_x(self._get_span_x() + x_increment) - self.update_range() - self.pressv += x_increment - self.events.moved.trigger(self) - self.events.changed.trigger(self) - if self.onmove_callback is not None: - self.onmove_callback(*self._range) - self.draw_patch() - - def mm_on_release(self, event): - if self._button_down is False or self.ignore(event): - return - self._button_down = False - self.canvas.mpl_disconnect(self.on_move_cid) - self.on_move_cid = None - - def turn_off(self): - self.disconnect_events() - if self.on_move_cid is not None: - self.canvas.mpl_disconnect(self.on_move_cid) - self.ax.patches.remove(self.rect) - self.ax.figure.canvas.draw_idle() + self._set_span_extents(*self._get_range()) diff --git a/hyperspy/drawing/_widgets/rectangles.py b/hyperspy/drawing/_widgets/rectangles.py index 25a41a766e..fc6ade3ed8 100644 --- a/hyperspy/drawing/_widgets/rectangles.py +++ b/hyperspy/drawing/_widgets/rectangles.py @@ -1,27 +1,27 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import numpy as np -import matplotlib.pyplot as plt import logging -from hyperspy.drawing.widgets import Widget2DBase, ResizersMixin +import matplotlib.pyplot as plt +import numpy as np +from hyperspy.drawing.widget import ResizersMixin, Widget2DBase _logger = logging.getLogger(__name__) # Track if we have already warned when the widget is out of range @@ -29,7 +29,6 @@ class SquareWidget(Widget2DBase): - """SquareWidget is a symmetric, Rectangle-patch based widget, which can be dragged, and resized by keystrokes/code. As the widget is normally only meant to indicate position, the sizing is deemed purely visual, but there @@ -46,15 +45,24 @@ def _set_patch(self): """ xy = self._get_patch_xy() xs, ys = self.size - self.patch = [plt.Rectangle( - xy, xs, ys, - fill=False, - lw=self.border_thickness, - ec=self.color, - alpha=self.alpha, - picker=True,)] + self._patch = [ + plt.Rectangle( + xy, + xs, + ys, + fill=False, + lw=self.border_thickness, + ec=self.color, + alpha=self.alpha, + picker=True, + ) + ] super(SquareWidget, self)._set_patch() + def _onjumpclick(self, event): + if event.key == "shift" and event.inaxes and self.is_pointer: + self.position = (event.xdata, event.ydata) + def _onmousemove(self, event): """on mouse motion move the patch if picked""" if self.picked is True and event.inaxes: @@ -62,7 +70,6 @@ def _onmousemove(self, event): class RectangleWidget(SquareWidget, ResizersMixin): - """RectangleWidget is a asymmetric, Rectangle-patch based widget, which can be dragged and resized by mouse/keys. For resizing by mouse, it adds a small Rectangle patch on the outer border of the main patch, to serve as @@ -92,19 +99,19 @@ def _parse_bounds_args(self, args, kwargs): return args[0] elif len(args) == 4: return args - elif len(kwargs) == 1 and 'bounds' in kwargs: + elif len(kwargs) == 1 and "bounds" in kwargs: return kwargs.values()[0] else: - x = kwargs.pop('x', kwargs.pop('left', self._pos[0])) - y = kwargs.pop('y', kwargs.pop('top', self._pos[1])) - if 'right' in kwargs: - w = kwargs.pop('right') - x + x = kwargs.pop("x", kwargs.pop("left", self._pos[0])) + y = kwargs.pop("y", kwargs.pop("top", self._pos[1])) + if "right" in kwargs: + w = kwargs.pop("right") - x else: - w = kwargs.pop('w', kwargs.pop('width', self._size[0])) - if 'bottom' in kwargs: - h = kwargs.pop('bottom') - y + w = kwargs.pop("w", kwargs.pop("width", self._size[0])) + if "bottom" in kwargs: + h = kwargs.pop("bottom") - y else: - h = kwargs.pop('h', kwargs.pop('height', self._size[1])) + h = kwargs.pop("h", kwargs.pop("height", self._size[1])) return x, y, w, h def set_ibounds(self, *args, **kwargs): @@ -160,8 +167,10 @@ def set_bounds(self, *args, **kwargs): def warn(obj, parameter, value): global already_warn_out_of_range if not already_warn_out_of_range: - _logger.info('{}: {} is out of range. It is therefore set ' - 'to the value of {}'.format(obj, parameter, value)) + _logger.info( + "{}: {} is out of range. It is therefore set " + "to the value of {}".format(obj, parameter, value) + ) already_warn_out_of_range = True scale = [axis.scale for axis in self.axes] @@ -171,48 +180,48 @@ def warn(obj, parameter, value): in_range = 0 if x < l0: x = l0 - warn(self, '`x`', x) + warn(self, "`x`", x) elif h0 <= x: x = h0 - scale[0] - warn(self, '`x`', x) + warn(self, "`x`", x) else: in_range += 1 if y < l1: y = l1 - warn(self, '`y`', y) + warn(self, "`y`", y) elif h1 <= y: - warn(self, '`y`', y) + warn(self, "`y`", y) y = h1 - scale[1] else: in_range += 1 if w < scale[0]: w = scale[0] - warn(self, '`width` or `right`', w) + warn(self, "`width` or `right`", w) elif not (l0 + scale[0] <= x + w <= h0 + scale[0]): if self.size[0] != w: # resize w = h0 + scale[0] - self.position[0] - warn(self, '`width` or `right`', w) + warn(self, "`width` or `right`", w) if self.position[0] != x: # moved x = h0 + scale[0] - self.size[0] - warn(self, '`x`', x) + warn(self, "`x`", x) else: in_range += 1 if h < scale[1]: h = scale[1] - warn(self, '`height` or `bottom`', h) + warn(self, "`height` or `bottom`", h) elif not (l1 + scale[1] <= y + h <= h1 + scale[1]): if self.size[1] != h: # resize h = h1 + scale[1] - self.position[1] - warn(self, '`height` or `bottom`', h) + warn(self, "`height` or `bottom`", h) if self.position[1] != y: # moved y = h1 + scale[1] - self.size[1] - warn(self, '`y`', y) + warn(self, "`y`", y) else: in_range += 1 # if we are in range again, reset `already_warn_out_of_range` to False if in_range == 4 and already_warn_out_of_range: - _logger.info('{} back in range.'.format(self.__class__.__name__)) + _logger.info("{} back in range.".format(self.__class__.__name__)) already_warn_out_of_range = False old_position, old_size = self.position, self.size @@ -221,12 +230,11 @@ def warn(obj, parameter, value): self._apply_changes(old_size=old_size, old_position=old_position) def _validate_pos(self, value): - """Constrict the position within bounds. - """ - value = (min(value[0], self.axes[0].high_value - self._size[0] + - self.axes[0].scale), - min(value[1], self.axes[1].high_value - self._size[1] + - self.axes[1].scale)) + """Constrict the position within bounds.""" + value = ( + min(value[0], self.axes[0].high_value - self._size[0] + self.axes[0].scale), + min(value[1], self.axes[1].high_value - self._size[1] + self.axes[1].scale), + ) return super(RectangleWidget, self)._validate_pos(value) @property @@ -240,9 +248,11 @@ def width(self, value): ix = self.indices[0] + value il0, ih0 = self.axes[0].low_index, self.axes[0].high_index if value <= 0 or not (il0 < ix <= ih0): - raise ValueError('`width` value is not in range. The ' - '`width` is {} and should be in range ' - '{}-{}.'.format(ix, il0 + 1, ih0)) + raise ValueError( + "`width` value is not in range. The " + "`width` is {} and should be in range " + "{}-{}.".format(ix, il0 + 1, ih0) + ) self._set_a_size(0, value) @property @@ -256,9 +266,11 @@ def height(self, value): iy = self.indices[1] + value il1, ih1 = self.axes[1].low_index, self.axes[1].high_index if value <= 0 or not (il1 < iy <= ih1): - raise ValueError('`height` value is not in range. The ' - '`height` is {} and should be in range ' - '{}-{}.'.format(iy, il1 + 1, ih1)) + raise ValueError( + "`height` value is not in range. The " + "`height` is {} and should be in range " + "{}-{}.".format(iy, il1 + 1, ih1) + ) self._set_a_size(1, value) # --------- Internal functions --------- @@ -288,8 +300,7 @@ def _set_a_size(self, idx, value): self._size_changed() def _increase_xsize(self): - self._set_a_size(0, self._size[0] + - self.axes[0].scale * self.size_step) + self._set_a_size(0, self._size[0] + self.axes[0].scale * self.size_step) def _decrease_xsize(self): new_s = self._size[0] - self.axes[0].scale * self.size_step @@ -297,8 +308,7 @@ def _decrease_xsize(self): self._set_a_size(0, new_s) def _increase_ysize(self): - self._set_a_size(1, self._size[1] + - self.axes[1].scale * self.size_step) + self._set_a_size(1, self._size[1] + self.axes[1].scale * self.size_step) def _decrease_ysize(self): new_s = self._size[1] - self.axes[1].scale * self.size_step @@ -404,9 +414,7 @@ def _onmousemove(self, event): y = event.ydata p = self._get_patch_xy() # Old bounds - bounds = [p[0], p[1], - p[0] + self._size[0], - p[1] + self._size[1]] + bounds = [p[0], p[1], p[0] + self._size[0], p[1] + self._size[1]] # Store geometry for _apply_changes at end old_position, old_size = self.position, self.size @@ -418,62 +426,62 @@ def _onmousemove(self, event): y -= self.pick_offset[1] self._validate_geometry(x, y) else: - posx = None # New x pos. If None, the old pos will be used - posy = None # Same for y + posx = None # New x pos. If None, the old pos will be used + posy = None # Same for y corner = self.resizer_picked # Adjust for resizer position: offset = self._get_resizer_offset() x += offset[0] * (0.5 - 1 * (corner % 2)) y += offset[1] * (0.5 - 1 * (corner // 2)) - if corner % 2 == 0: # Left side start - if x > bounds[2]: # flipped to right - posx = bounds[2] # New left is old right + if corner % 2 == 0: # Left side start + if x > bounds[2]: # flipped to right + posx = bounds[2] # New left is old right # New size is mouse position - new left self._size[0] = x - posx - self.resizer_picked += 1 # Switch pick to right - elif bounds[2] - x < xaxis.scale: # Width too small - posx = bounds[2] - xaxis.scale # So move pos left - self._size[0] = bounds[2] - posx # Should be scale - else: # Moving left edge - posx = x # Set left to mouse position + self.resizer_picked += 1 # Switch pick to right + elif bounds[2] - x < xaxis.scale: # Width too small + posx = bounds[2] - xaxis.scale # So move pos left + self._size[0] = bounds[2] - posx # Should be scale + else: # Moving left edge + posx = x # Set left to mouse position # Keep right still by changing size: self._size[0] = bounds[2] - x - else: # Right side start - if x < bounds[0]: # Flipped to left + else: # Right side start + if x < bounds[0]: # Flipped to left if bounds[0] - x < xaxis.scale: posx = bounds[0] - xaxis.scale else: - posx = x # Set left to mouse + posx = x # Set left to mouse # Set size to old left - new left self._size[0] = bounds[0] - posx - self.resizer_picked -= 1 # Switch pick to left - else: # Moving right edge + self.resizer_picked -= 1 # Switch pick to left + else: # Moving right edge # Left should be left as it is, only size updates: self._size[0] = x - bounds[0] # mouse - old left - if corner // 2 == 0: # Top side start - if y > bounds[3]: # flipped to botton - posy = bounds[3] # New top is old bottom + if corner // 2 == 0: # Top side start + if y > bounds[3]: # flipped to botton + posy = bounds[3] # New top is old bottom # New size is mouse position - new top self._size[1] = y - posy - self.resizer_picked += 2 # Switch pick to bottom + self.resizer_picked += 2 # Switch pick to bottom elif bounds[3] - y < yaxis.scale: # Height too small - posy = bounds[3] - yaxis.scale # So move pos up - self._size[1] = bounds[3] - posy # Should be scale - else: # Moving top edge - posy = y # Set top to mouse index + posy = bounds[3] - yaxis.scale # So move pos up + self._size[1] = bounds[3] - posy # Should be scale + else: # Moving top edge + posy = y # Set top to mouse index # Keep bottom still by changing size: self._size[1] = bounds[3] - y # old bottom - new top - else: # Bottom side start - if y < bounds[1]: # Flipped to top + else: # Bottom side start + if y < bounds[1]: # Flipped to top if bounds[1] - y < yaxis.scale: posy = bounds[1] - yaxis.scale else: - posy = y # Set top to mouse + posy = y # Set top to mouse # Set size to old top - new top self._size[1] = bounds[1] - posy - self.resizer_picked -= 2 # Switch pick to top - else: # Moving bottom edge + self.resizer_picked -= 2 # Switch pick to top + else: # Moving bottom edge self._size[1] = y - bounds[1] # mouse - old top # Bound size to scale: if self._size[0] < xaxis.scale: diff --git a/hyperspy/drawing/_widgets/scalebar.py b/hyperspy/drawing/_widgets/scalebar.py index fb876503f5..125816c1e3 100644 --- a/hyperspy/drawing/_widgets/scalebar.py +++ b/hyperspy/drawing/_widgets/scalebar.py @@ -1,30 +1,40 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import numpy as np + from hyperspy.misc.math_tools import closest_nice_number class ScaleBar(object): - - def __init__(self, ax, units, pixel_size=None, color='white', - position=None, max_size_ratio=0.25, lw=2, length=None, - animated=False): + def __init__( + self, + ax, + units, + pixel_size=None, + color="white", + position=None, + max_size_ratio=0.25, + lw=2, + length=None, + animated=False, + ): """Add a scale bar to an image. Parameters @@ -72,53 +82,54 @@ def __init__(self, ax, units, pixel_size=None, color='white', def get_units_string(self): if self.tex_bold is True: - if (self.units[0] and self.units[-1]) == '$': - return r'$\mathbf{%g\,%s}$' % \ - (self.length, self.units[1:-1]) + if (self.units[0] and self.units[-1]) == "$": + return r"$\mathbf{%g\,%s}$" % (self.length, self.units[1:-1]) else: - return r'$\mathbf{%g\,}$\textbf{%s}' % \ - (self.length, self.units) + return r"$\mathbf{%g\,}$\textbf{%s}" % (self.length, self.units) else: - return r'$%g\,$%s' % (self.length, self.units) + return r"$%g\,$%s" % (self.length, self.units) def calculate_line_position(self, pad=0.05): - return ((1 - pad) * self.xmin + pad * self.xmax, - (1 - pad) * self.ymin + pad * self.ymax) + return ( + (1 - pad) * self.xmin + pad * self.xmax, + (1 - pad) * self.ymin + pad * self.ymax, + ) - def calculate_text_position(self, pad=1 / 100.): + def calculate_text_position(self, pad=1 / 100.0): ps = self.pixel_size if self.pixel_size is not None else 1 x1, y1 = self.position x2, y2 = x1 + self.length / ps, y1 - self.text_position = ((x1 + x2) / 2., - y2 + (self.ymax - self.ymin) / ps * pad) + self.text_position = ((x1 + x2) / 2.0, y2 + (self.ymax - self.ymin) / ps * pad) def calculate_size(self, max_size_ratio=0.25): ps = self.pixel_size if self.pixel_size is not None else 1 - size = closest_nice_number(ps * (self.xmax - self.xmin) * - max_size_ratio) + size = closest_nice_number( + np.abs(ps * (self.xmax - self.xmin) * max_size_ratio) + ) self.length = size def remove(self): if self.line is not None: - self.ax.lines.remove(self.line) + self.line.remove() if self.text is not None: - self.ax.texts.remove(self.text) + self.text.remove() def plot_scale(self, line_width=1): self.remove() ps = self.pixel_size if self.pixel_size is not None else 1 x1, y1 = self.position x2, y2 = x1 + self.length / ps, y1 - self.line, = self.ax.plot([x1, x2], [y1, y2], - linestyle='-', - lw=line_width, - animated=self.animated) - self.text = self.ax.text(*self.text_position, - s=self.get_units_string(), - ha='center', - size='medium', - animated=self.animated) + (self.line,) = self.ax.plot( + [x1, x2], [y1, y2], linestyle="-", lw=line_width, animated=self.animated + ) + self.text = self.ax.text( + *self.text_position, + s=self.get_units_string(), + ha="center", + size="medium", + animated=self.animated, + ) self.ax.set_xlim(self.xmin, self.xmax) self.ax.set_ylim(self.ymin, self.ymax) self.ax.figure.canvas.draw_idle() diff --git a/hyperspy/drawing/_widgets/vertical_line.py b/hyperspy/drawing/_widgets/vertical_line.py index 7250c63d6e..83cddab59b 100644 --- a/hyperspy/drawing/_widgets/vertical_line.py +++ b/hyperspy/drawing/_widgets/vertical_line.py @@ -1,43 +1,51 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy.drawing.widgets import Widget1DBase -from hyperspy.drawing.utils import picker_kwargs from hyperspy.defaults_parser import preferences +from hyperspy.drawing.utils import picker_kwargs +from hyperspy.drawing.widget import Widget1DBase class VerticalLineWidget(Widget1DBase): - - """A draggable, vertical line widget. - """ + """A draggable, vertical line widget.""" def _update_patch_position(self): if self.is_on and self.patch: - self.patch[0].set_xdata(self._pos[0]) + self.patch[0].set_xdata([self._pos[0]]) self.draw_patch() + def _add_patch_to(self, ax): + """Create and add the matplotlib patches to 'ax'""" + self.blit = hasattr(ax, "hspy_fig") and ax.figure.canvas.supports_blit + self._set_patch() + for p in self.patch: + p.set_animated(self.blit) + def _set_patch(self): ax = self.ax kwargs = picker_kwargs(preferences.Plot.pick_tolerance) - self.patch = [ax.axvline(self._pos[0], - color=self.color, - alpha=self.alpha, - **kwargs)] + self._patch = [ + ax.axvline(self._pos[0], color=self.color, alpha=self.alpha, **kwargs) + ] + + def _onjumpclick(self, event): + if event.key == "shift" and event.inaxes and self.is_pointer: + self.position = (event.xdata,) def _onmousemove(self, event): """on mouse motion draw the cursor if picked""" diff --git a/hyperspy/drawing/figure.py b/hyperspy/drawing/figure.py index c1bf758511..5e7d0a5167 100644 --- a/hyperspy/drawing/figure.py +++ b/hyperspy/drawing/figure.py @@ -1,64 +1,77 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import logging import textwrap + +import matplotlib import matplotlib.pyplot as plt -import logging -from hyperspy.events import Event, Events from hyperspy.drawing import utils - +from hyperspy.events import Event, Events _logger = logging.getLogger(__name__) -class BlittedFigure(object): - +class BlittedFigure: def __init__(self): self._draw_event_cid = None self._background = None self.events = Events() - self.events.closed = Event(""" + self.events.closed = Event( + """ Event that triggers when the figure window is closed. - Arguments: - obj: SpectrumFigure instances - The instance that triggered the event. - """, arguments=["obj"]) + Parameters + ---------- + obj: SpectrumFigure instances + The instance that triggered the event. + """, + arguments=["obj"], + ) + # The matplotlib Figure or SubFigure + # To access the matplotlib figure, use `get_mpl_figure` + self.figure = None + # The matplotlib Axis + self.ax = None self.title = "" self.ax_markers = list() def create_figure(self, **kwargs): - """Create matplotlib figure + """ + Create matplotlib figure. Parameters ---------- - **kwargs - All keyword arguments are passed to ``plt.figure``. + **kwargs : dict + Keyword arguments are passed to + :func:`hyperspy.drawing.utils.create_figure`. """ + kwargs.setdefault("_on_figure_window_close", self.close) self.figure = utils.create_figure( - window_title="Figure " + self.title if self.title - else None, **kwargs) - utils.on_figure_window_close(self.figure, self._on_close) + window_title="Figure " + self.title if self.title else None, + **kwargs, + ) if self.figure.canvas.supports_blit: self._draw_event_cid = self.figure.canvas.mpl_connect( - 'draw_event', self._on_blit_draw) + "draw_event", self._on_blit_draw + ) def _on_blit_draw(self, *args): fig = self.figure @@ -71,9 +84,7 @@ def _on_blit_draw(self, *args): self._draw_animated() def _draw_animated(self): - """Draw animated plot elements - - """ + """Draw animated plot elements""" for ax in self.figure.axes: # Create a list of animated artists and draw them. artists = sorted(ax.get_children(), key=lambda x: x.zorder) @@ -82,7 +93,7 @@ def _draw_animated(self): ax.draw_artist(artist) def _update_animated(self): - _logger.debug('Updating animated.') + _logger.debug("Updating animated.") canvas = self.ax.figure.canvas # As the background haven't changed, we can simply restore it. canvas.restore_region(self._background) @@ -90,25 +101,35 @@ def _update_animated(self): self._draw_animated() canvas.blit(self.figure.bbox) + def get_mpl_figure(self): + """Retuns the matplotlib figure""" + if self.figure is None: + return None + else: + # See https://github.com/matplotlib/matplotlib/pull/28177 + figure = self.figure + # matplotlib SubFigure can be nested and we don't support it + if isinstance(figure, matplotlib.figure.SubFigure): + return figure.figure + else: + return figure + def add_marker(self, marker): marker.ax = self.ax - if marker.axes_manager is None: - marker.axes_manager = self.axes_manager self.ax_markers.append(marker) marker.events.closed.connect(lambda obj: self.ax_markers.remove(obj)) def remove_markers(self, render_figure=False): - """ Remove all markers """ + """Remove all markers""" for marker in self.ax_markers: marker.close(render_figure=False) if render_figure: self.render_figure() def _on_close(self): - _logger.debug('Closing `BlittedFigure`.') - if self.figure is None: - _logger.debug('`BlittedFigure` already closed.') - return # Already closed + _logger.debug("Closing `BlittedFigure`.") + self.ax = None + self._background = None for marker in self.ax_markers: marker.close(render_figure=False) self.events.closed.trigger(obj=self) @@ -117,15 +138,13 @@ def _on_close(self): if self._draw_event_cid: self.figure.canvas.mpl_disconnect(self._draw_event_cid) self._draw_event_cid = None - plt.close(self.figure) self.figure = None - self.ax = None - self._background = None - _logger.debug('`BlittedFigure` closed.') + _logger.debug("`BlittedFigure` closed.") def close(self): - _logger.debug('`close` `BlittedFigure` called.') - self._on_close() # Needs to trigger serially for a well defined state + _logger.debug("`close` `BlittedFigure` called.") + self._on_close() # Needs to trigger serially for a well defined state + plt.close(self.get_mpl_figure()) @property def title(self): diff --git a/hyperspy/drawing/image.py b/hyperspy/drawing/image.py index 708ea15afc..eea107f436 100644 --- a/hyperspy/drawing/image.py +++ b/hyperspy/drawing/image.py @@ -1,50 +1,47 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # # This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import copy +import inspect +import logging import math -from distutils.version import LooseVersion -import numpy as np import matplotlib import matplotlib.pyplot as plt -from matplotlib.colors import Normalize, LogNorm, SymLogNorm, PowerNorm +import numpy as np +from matplotlib.colors import LogNorm, Normalize, PowerNorm, SymLogNorm +from matplotlib.figure import SubFigure +from packaging.version import Version +from rsciio.utils import rgb_tools from traits.api import Undefined -import logging -import inspect -import copy -from hyperspy.drawing import widgets -from hyperspy.drawing import utils -from hyperspy.signal_tools import ImageContrastEditor -from hyperspy.misc import math_tools -from hyperspy.misc import rgb_tools -from hyperspy.drawing.figure import BlittedFigure -from hyperspy.ui_registry import DISPLAY_DT, TOOLKIT_DT from hyperspy.docstrings.plot import PLOT2D_DOCSTRING +from hyperspy.drawing import utils, widgets +from hyperspy.drawing.figure import BlittedFigure +from hyperspy.misc import math_tools from hyperspy.misc.test_utils import ignore_warning -from hyperspy.defaults_parser import preferences - +from hyperspy.signal_tools import ImageContrastEditor +from hyperspy.ui_registry import DISPLAY_DT, TOOLKIT_DT _logger = logging.getLogger(__name__) class ImagePlot(BlittedFigure): - """Class to plot an image with the necessary machinery to update the image when the coordinates of an AxesManager change. @@ -62,15 +59,14 @@ class ImagePlot(BlittedFigure): """ % PLOT2D_DOCSTRING - def __init__(self, title=""): - super(ImagePlot, self).__init__() + def __init__(self, title="", **kwargs): + super().__init__() self.data_function = None self.data_function_kwargs = {} # Attribute matching the arguments of # `hyperspy._signal.signal2d.signal2D.plot` self.autoscale = "v" - self.saturated_pixels = None self.norm = "auto" self.vmin = None self.vmax = None @@ -90,7 +86,7 @@ def __init__(self, title=""): # Other attributes self.pixel_units = None self._colorbar = None - self.quantity_label = '' + self.quantity_label = "" self.figure = None self.ax = None self.title = title @@ -102,18 +98,18 @@ def __init__(self, title=""): # user provided percentile values self._vmin_percentile = None self._vmax_percentile = None - # default values used when the numeric and percentile are None - self._vmin_default = f"{preferences.Plot.saturated_pixels / 2}th" - self._vmax_default = f"{100 - preferences.Plot.saturated_pixels / 2}th" # use to store internally the numeric value of contrast self._vmin = None self._vmax = None - self._ylabel = '' - self._xlabel = '' + self._ylabel = "" + self._xlabel = "" self.plot_indices = True self._text = None - self._text_position = (0, 1.05,) + self._text_position = ( + 0, + 1.05, + ) self._aspect = 1 self._extent = None self.xaxis = None @@ -132,7 +128,7 @@ def vmax(self): elif self._vmax_percentile is not None: return self._vmax_percentile else: - return self._vmax_default + return "100th" @vmax.setter def vmax(self, vmax): @@ -145,7 +141,7 @@ def vmax(self, vmax): elif vmax is None: self._vmax_percentile = self._vmax_numeric = None else: - raise TypeError("`vmax` must be a number or a string.") + raise TypeError("`vmax` must be a number, a string or `None`.") @property def vmin(self): @@ -154,7 +150,7 @@ def vmin(self): elif self._vmin_percentile is not None: return self._vmin_percentile else: - return self._vmin_default + return "0th" @vmin.setter def vmin(self, vmin): @@ -167,7 +163,7 @@ def vmin(self, vmin): elif vmin is None: self._vmin_percentile = self._vmin_numeric = None else: - raise TypeError("`vmax` must be a number or a string.") + raise TypeError("`vmin` must be a number, a string or `None`.") @property def axes_ticks(self): @@ -201,9 +197,12 @@ def configure(self): xaxis = self.xaxis yaxis = self.yaxis - if (xaxis.is_uniform and yaxis.is_uniform and - (xaxis.units == yaxis.units) and - (abs(xaxis.scale) == abs(yaxis.scale))): + if ( + xaxis.is_uniform + and yaxis.is_uniform + and (xaxis.units == yaxis.units) + and (abs(xaxis.scale) == abs(yaxis.scale)) + ): self._auto_scalebar = True self._auto_axes_ticks = False self.pixel_units = xaxis.units @@ -212,35 +211,29 @@ def configure(self): self._auto_axes_ticks = True # Signal2D labels - self._xlabel = '{}'.format(xaxis) + self._xlabel = "{}".format(xaxis) if xaxis.units is not Undefined: - self._xlabel += ' ({})'.format(xaxis.units) + self._xlabel += " ({})".format(xaxis.units) - self._ylabel = '{}'.format(yaxis) + self._ylabel = "{}".format(yaxis) if yaxis.units is not Undefined: - self._ylabel += ' ({})'.format(yaxis.units) + self._ylabel += " ({})".format(yaxis.units) # Calibrate the axes of the navigator image if xaxis.is_uniform and yaxis.is_uniform: - xaxis_half_px = xaxis.scale / 2. - yaxis_half_px = yaxis.scale / 2. + xaxis_half_px = xaxis.scale / 2.0 + yaxis_half_px = yaxis.scale / 2.0 else: xaxis_half_px = 0 yaxis_half_px = 0 # Calibrate the axes of the navigator image - self._extent = [xaxis.axis[0] - xaxis_half_px, - xaxis.axis[-1] + xaxis_half_px, - yaxis.axis[-1] + yaxis_half_px, - yaxis.axis[0] - yaxis_half_px] + self._extent = [ + xaxis.axis[0] - xaxis_half_px, + xaxis.axis[-1] + xaxis_half_px, + yaxis.axis[-1] + yaxis_half_px, + yaxis.axis[0] - yaxis_half_px, + ] self._calculate_aspect() - if self.saturated_pixels is not None: - from hyperspy.exceptions import VisibleDeprecationWarning - VisibleDeprecationWarning("`saturated_pixels` is deprecated and will be " - "removed in 2.0. Please use `vmin` and `vmax` " - "instead.") - self._vmin_percentile = self.saturated_pixels / 2 - self._vmax_percentile = self.saturated_pixels / 2 - def _calculate_aspect(self): xaxis = self.xaxis @@ -253,33 +246,32 @@ def _calculate_aspect(self): factor = min_asp * xaxis.size / yaxis.size self._auto_scalebar = False self._auto_axes_ticks = True - elif yaxis.size / xaxis.size > min_asp ** -1: - factor = min_asp ** -1 * xaxis.size / yaxis.size + elif yaxis.size / xaxis.size > min_asp**-1: + factor = min_asp**-1 * xaxis.size / yaxis.size self._auto_scalebar = False self._auto_axes_ticks = True if xaxis.is_uniform and yaxis.is_uniform: - self._aspect = np.abs(factor * xaxis.scale / yaxis.scale) + self._aspect = abs(factor * xaxis.scale / yaxis.scale) else: self._aspect = 1.0 - def _calculate_vmin_max(self, data, auto_contrast=False, - vmin=None, vmax=None): + def _calculate_vmin_max(self, data, auto_contrast=False, vmin=None, vmax=None): + vminprovided = vmin + vmaxprovided = vmax # Calculate vmin and vmax using `utils.contrast_stretching` when: # - auto_contrast is True # - self.vmin or self.vmax is of tpye str - if (auto_contrast and (isinstance(self.vmin, str) or - isinstance(self.vmax, str))): + if auto_contrast and (isinstance(self.vmin, str) or isinstance(self.vmax, str)): with ignore_warning(category=RuntimeWarning): # In case of "All-NaN slices" - vmin, vmax = utils.contrast_stretching( - data, self.vmin, self.vmax) + vmin, vmax = utils.contrast_stretching(data, self.vmin, self.vmax) else: vmin, vmax = self._vmin_numeric, self._vmax_numeric # provided vmin, vmax override the calculated value - if isinstance(vmin, (int, float)): - vmin = vmin - if isinstance(vmax, (int, float)): - vmax = vmax + if isinstance(vminprovided, (int, float)): + vmin = vminprovided + if isinstance(vmaxprovided, (int, float)): + vmax = vmaxprovided if vmin == np.nan: vmin = None if vmax == np.nan: @@ -302,20 +294,22 @@ def create_figure(self, max_size=None, min_size=2, **kwargs): the figure size. **kwargs All keyword arguments are passed to - :py:func:`matplotlib.pyplot.figure`. + :func:`matplotlib.pyplot.figure`. """ if "figsize" not in kwargs: if self.scalebar is True: - wfactor = 1.0 + plt.rcParams['font.size'] / 100 + wfactor = 1.0 + plt.rcParams["font.size"] / 100 else: wfactor = 1 height = abs(self._extent[3] - self._extent[2]) * self._aspect width = abs(self._extent[1] - self._extent[0]) - figsize = np.array((width * wfactor, height)) * \ - max(plt.rcParams['figure.figsize']) / \ - max(width * wfactor, height) + figsize = ( + np.array((width * wfactor, height)) + * max(plt.rcParams["figure.figsize"]) + / max(width * wfactor, height) + ) kwargs["figsize"] = figsize.clip(min_size, max_size) if "disable_xyscale_keys" not in kwargs: kwargs["disable_xyscale_keys"] = True @@ -331,16 +325,18 @@ def create_axis(self): self.ax.set_yticks([]) self.ax.hspy_fig = self if self.axes_off: - self.ax.axis('off') + self.ax.axis("off") def plot(self, data_function_kwargs={}, **kwargs): self.data_function_kwargs = data_function_kwargs self.configure() if self.figure is None: - self.create_figure() + fig = kwargs.pop("fig", None) + _on_figure_window_close = kwargs.pop("_on_figure_window_close", None) + self.create_figure(fig=fig, _on_figure_window_close=_on_figure_window_close) self.create_axis() - if (not self.axes_manager or self.axes_manager.navigation_size == 0): + if not self.axes_manager or self.axes_manager.navigation_size == 0: self.plot_indices = False if self.plot_indices is True: if self._text is not None: @@ -350,11 +346,12 @@ def plot(self, data_function_kwargs={}, **kwargs): s=str(self.axes_manager.indices), transform=self.ax.transAxes, fontsize=12, - color='red', - animated=self.figure.canvas.supports_blit) + color="red", + animated=self.figure.canvas.supports_blit, + ) for marker in self.ax_markers: marker.plot() - for attribute in ['vmin', 'vmax']: + for attribute in ["vmin", "vmax"]: if attribute in kwargs.keys(): setattr(self, attribute, kwargs.pop(attribute)) self.update(data_changed=True, auto_contrast=True, **kwargs) @@ -370,9 +367,9 @@ def plot(self, data_function_kwargs={}, **kwargs): if self.colorbar: self._add_colorbar() - if hasattr(self.figure, 'tight_layout'): + if hasattr(self.figure, "tight_layout"): try: - if self.axes_ticks == 'off' and not self.colorbar: + if self.axes_ticks == "off" and not self.colorbar: plt.subplots_adjust(0, 0, 1, 1) else: self.figure.tight_layout() @@ -382,29 +379,30 @@ def plot(self, data_function_kwargs={}, **kwargs): pass self.connect() - self.figure.canvas.draw() + self.render_figure() def _add_colorbar(self): # Bug extend='min' or extend='both' and power law norm # Use it when it is fixed in matplotlib - self._colorbar = plt.colorbar(self.ax.images[0], ax=self.ax) + ims = self.ax.images if len(self.ax.images) else self.ax.collections + self._colorbar = self.figure.colorbar(ims[0], ax=self.ax) self.set_quantity_label() - self._colorbar.set_label( - self.quantity_label, rotation=-90, va='bottom') - self._colorbar.ax.yaxis.set_animated( - self.figure.canvas.supports_blit) + self._colorbar.set_label(self.quantity_label, rotation=-90, va="bottom") + self._colorbar.ax.yaxis.set_animated(self.figure.canvas.supports_blit) def _update_data(self): # self._current_data caches the displayed data. - data = self.data_function(axes_manager=self.axes_manager, - **self.data_function_kwargs) + data = self.data_function( + axes_manager=self.axes_manager, **self.data_function_kwargs + ) # the colorbar of matplotlib ~< 3.2 doesn't support bool array if data.dtype == bool: data = data.astype(int) self._current_data = data - def update(self, data_changed=True, auto_contrast=None, vmin=None, - vmax=None, **kwargs): + def update( + self, data_changed=True, auto_contrast=None, vmin=None, vmax=None, **kwargs + ): """ Parameters ---------- @@ -419,7 +417,7 @@ def update(self, data_changed=True, auto_contrast=None, vmin=None, vmin, vmax : float or str `vmin` and `vmax` are used to normalise the displayed data. **kwargs : dict - The kwargs are passed to :py:func:`matplotlib.pyplot.imshow`. + The kwargs are passed to :func:`matplotlib.pyplot.imshow`. Raises ------ @@ -428,7 +426,7 @@ def update(self, data_changed=True, auto_contrast=None, vmin=None, compatible with the selected ``norm``. """ if auto_contrast is None: - auto_contrast = 'v' in self.autoscale + auto_contrast = "v" in self.autoscale if data_changed: # When working with lazy signals the following may reread the data # from disk unnecessarily, for example when updating the image just @@ -442,7 +440,8 @@ def update(self, data_changed=True, auto_contrast=None, vmin=None, data = rgb_tools.rgbx2regular_array(data, plot_friendly=True) data = self._current_data = data self._is_rgb = True - ims = self.ax.images + + ims = self.ax.images if len(self.ax.images) else self.ax.collections # Turn on centre_colormap if a diverging colormap is used. if not self._is_rgb and self.centre_colormap == "auto": @@ -458,11 +457,11 @@ def update(self, data_changed=True, auto_contrast=None, vmin=None, self.centre_colormap = False redraw_colorbar = False - for marker in self.ax_markers: marker.update() if not self._is_rgb: + def format_coord(x, y): try: col = self.xaxis.value2index(x) @@ -475,15 +474,15 @@ def format_coord(x, y): if col >= 0 and row >= 0: z = data[row, col] if np.isfinite(z): - return f'x={x:1.4g}, y={y:1.4g}, intensity={z:1.4g}' - return f'x={x:1.4g}, y={y:1.4g}' + return f"x={x:1.4g}, y={y:1.4g}, intensity={z:1.4g}" + return f"x={x:1.4g}, y={y:1.4g}" + self.ax.format_coord = format_coord old_vmin, old_vmax = self._vmin, self._vmax if auto_contrast: - vmin, vmax = self._calculate_vmin_max(data, auto_contrast, - vmin, vmax) + vmin, vmax = self._calculate_vmin_max(data, auto_contrast, vmin, vmax) else: # use the value store internally when not explicitly defined if vmin is None: @@ -493,42 +492,48 @@ def format_coord(x, y): # If there is an image, any of the contrast bounds have changed and # the new contrast bounds are not the same redraw the colorbar. - if (ims and (old_vmin != vmin or old_vmax != vmax) and - vmin != vmax): + if ims and (old_vmin != vmin or old_vmax != vmax) and vmin != vmax: redraw_colorbar = True ims[0].autoscale() if self.centre_colormap: vmin, vmax = utils.centre_colormap_values(vmin, vmax) - if self.norm == 'auto' and self.gamma != 1.0: - self.norm = 'power' + if self.norm == "auto" and self.gamma != 1.0: + self.norm = "power" norm = copy.copy(self.norm) - if norm == 'power': + if norm == "power": # with auto norm, we use the power norm when gamma differs from its # default value. norm = PowerNorm(self.gamma, vmin=vmin, vmax=vmax) - elif norm == 'log': + elif norm == "log": if np.nanmax(data) <= 0: - raise ValueError('All displayed data are <= 0 and can not ' - 'be plotted using `norm="log"`. ' - 'Use `norm="symlog"` to plot on a log scale.') + raise ValueError( + "All displayed data are <= 0 and can not " + 'be plotted using `norm="log"`. ' + 'Use `norm="symlog"` to plot on a log scale.' + ) if np.nanmin(data) <= 0: vmin = np.nanmin(np.where(data > 0, data, np.inf)) norm = LogNorm(vmin=vmin, vmax=vmax) - elif norm == 'symlog': - sym_log_kwargs = {'linthresh':self.linthresh, - 'linscale':self.linscale, - 'vmin':vmin, 'vmax':vmax} - if LooseVersion(matplotlib.__version__) >= LooseVersion("3.2"): - sym_log_kwargs['base'] = 10 + elif norm == "symlog": + sym_log_kwargs = { + "linthresh": self.linthresh, + "linscale": self.linscale, + "vmin": vmin, + "vmax": vmax, + } + if Version(matplotlib.__version__) >= Version("3.2"): + sym_log_kwargs["base"] = 10 norm = SymLogNorm(**sym_log_kwargs) elif inspect.isclass(norm) and issubclass(norm, Normalize): norm = norm(vmin=vmin, vmax=vmax) - elif norm not in ['auto', 'linear']: - raise ValueError("`norm` parameter should be 'auto', 'linear', " - "'log', 'symlog' or a matplotlib Normalize " - "instance or subclass.") + elif norm not in ["auto", "linear"]: + raise ValueError( + "`norm` parameter should be 'auto', 'linear', " + "'log', 'symlog' or a matplotlib Normalize " + "instance or subclass." + ) else: # set back to matplotlib default norm = None @@ -542,18 +547,21 @@ def format_coord(x, y): if self.no_nans: data = np.nan_to_num(data) - if ims: # the images has already been drawn previously - ims[0].set_data(data) + if ims: # the images have already been drawn previously + if len(self.ax.images): # imshow + ims[0].set_data(data) + else: # pcolormesh + ims[0].set_array(data.ravel()) # update extent: - if 'x' in self.autoscale: + if "x" in self.autoscale: self._extent[0] = self.xaxis.axis[0] - self.xaxis.scale / 2 self._extent[1] = self.xaxis.axis[-1] + self.xaxis.scale / 2 self.ax.set_xlim(self._extent[:2]) - if 'y' in self.autoscale: + if "y" in self.autoscale: self._extent[2] = self.yaxis.axis[-1] + self.yaxis.scale / 2 self._extent[3] = self.yaxis.axis[0] - self.yaxis.scale / 2 self.ax.set_ylim(self._extent[2:]) - if 'x' in self.autoscale or 'y' in self.autoscale: + if "x" in self.autoscale or "y" in self.autoscale: ims[0].set_extent(self._extent) self._calculate_aspect() self.ax.set_aspect(self._aspect) @@ -561,31 +569,34 @@ def format_coord(x, y): ims[0].set_norm(norm) ims[0].norm.vmax, ims[0].norm.vmin = vmax, vmin if redraw_colorbar: - self._colorbar.draw_all() - self._colorbar.solids.set_animated( - self.figure.canvas.supports_blit - ) + # `draw_all` is deprecated in matplotlib 3.6.0 + if Version(matplotlib.__version__) <= Version("3.6.0"): + self._colorbar.draw_all() + elif isinstance(self.figure, SubFigure): + self.figure.canvas.draw_idle() # draw without rendering not supported for sub-figures + else: + self.figure.draw_without_rendering() + self._colorbar.solids.set_animated(self.figure.canvas.supports_blit) else: ims[0].changed() - if self.figure.canvas.supports_blit: - self._update_animated() - else: - self.figure.canvas.draw_idle() + self.render_figure() else: # no signal have been drawn yet - new_args = {'extent': self._extent, - 'aspect': self._aspect, - 'animated': self.figure.canvas.supports_blit, - } + new_args = {"animated": self.figure.canvas.supports_blit} if not self._is_rgb: if norm is None: - new_args.update({'vmin': vmin, 'vmax':vmax}) + new_args.update({"vmin": vmin, "vmax": vmax}) else: - new_args['norm'] = norm + new_args["norm"] = norm new_args.update(kwargs) - self.ax.imshow(data, **new_args) - self.figure.canvas.draw_idle() - - if self.axes_ticks == 'off': + if self.xaxis.is_uniform and self.yaxis.is_uniform: + # pcolormesh doesn't have extent and aspect as arguments + # aspect is set earlier via self.ax.set_aspect() anyways + new_args.update({"extent": self._extent, "aspect": self._aspect}) + self.ax.imshow(data, **new_args) + else: + self.ax.pcolormesh(self.xaxis.axis, self.yaxis.axis, data, **new_args) + self.ax.invert_yaxis() + if self.axes_ticks == "off": self.ax.set_axis_off() def _update(self): @@ -596,11 +607,12 @@ def _update(self): def gui_adjust_contrast(self, display=True, toolkit=None): if self._is_rgb: raise NotImplementedError( - "Contrast adjustment of RGB images is not implemented") + "Contrast adjustment of RGB images is not implemented" + ) ceditor = ImageContrastEditor(self) return ceditor.gui(display=display, toolkit=toolkit) - gui_adjust_contrast.__doc__ = \ - """ + + gui_adjust_contrast.__doc__ = """ Display widgets to adjust image contrast if available. Parameters @@ -611,8 +623,9 @@ def gui_adjust_contrast(self, display=True, toolkit=None): """ % (DISPLAY_DT, TOOLKIT_DT) def connect(self): - self.figure.canvas.mpl_connect('key_press_event', - self.on_key_press) + # in case the figure is not displayed + if self.figure is not None: + self.figure.canvas.mpl_connect("key_press_event", self.on_key_press) if self.axes_manager: if self.update not in self.axes_manager.events.indices_changed.connected: self.axes_manager.events.indices_changed.connect(self.update, []) @@ -625,13 +638,13 @@ def disconnect(self): self.axes_manager.events.indices_changed.disconnect(self.update) def on_key_press(self, event): - if event.key == 'h': + if event.key == "h": self.gui_adjust_contrast() - if event.key == 'l': + if event.key == "l": self.toggle_norm() def toggle_norm(self): - self.norm = 'linear' if self.norm == 'log' else 'log' + self.norm = "linear" if self.norm == "log" else "log" self.update(data_changed=False) if self.colorbar: self._colorbar.remove() @@ -639,48 +652,49 @@ def toggle_norm(self): self.figure.canvas.draw_idle() def set_quantity_label(self): - if 'power_spectrum' in self.data_function_kwargs.keys(): - if self.norm == 'log': - if 'FFT' in self.quantity_label: + if "power_spectrum" in self.data_function_kwargs.keys(): + if self.norm == "log": + if "FFT" in self.quantity_label: self.quantity_label = self.quantity_label.replace( - 'Power spectral density', 'FFT') + "Power spectral density", "FFT" + ) else: - of = ' of ' if self.quantity_label else '' - self.quantity_label = 'Power spectral density' + of + \ - self.quantity_label + of = " of " if self.quantity_label else "" + self.quantity_label = ( + "Power spectral density" + of + self.quantity_label + ) else: self.quantity_label = self.quantity_label.replace( - 'Power spectral density of ', '') + "Power spectral density of ", "" + ) self.quantity_label = self.quantity_label.replace( - 'Power spectral density', '') + "Power spectral density", "" + ) def set_contrast(self, vmin, vmax): self.vmin, self.vmax = vmin, vmax self.update(data_changed=False, auto_contrast=True) - def optimize_colorbar(self, - number_of_ticks=5, - tolerance=5, - step_prec_max=1): + def optimize_colorbar(self, number_of_ticks=5, tolerance=5, step_prec_max=1): vmin, vmax = self.vmin, self.vmax _range = vmax - vmin step = _range / (number_of_ticks - 1) step_oom = math_tools.order_of_magnitude(step) def optimize_for_oom(oom): - self.colorbar_step = math.floor(step / 10 ** oom) * 10 ** oom - self.colorbar_vmin = math.floor(vmin / 10 ** oom) * 10 ** oom - self.colorbar_vmax = self.colorbar_vmin + \ - self.colorbar_step * (number_of_ticks - 1) + self.colorbar_step = math.floor(step / 10**oom) * 10**oom + self.colorbar_vmin = math.floor(vmin / 10**oom) * 10**oom + self.colorbar_vmax = self.colorbar_vmin + self.colorbar_step * ( + number_of_ticks - 1 + ) self.colorbar_locs = ( - np.arange(0, number_of_ticks) * - self.colorbar_step + - self.colorbar_vmin) + np.arange(0, number_of_ticks) * self.colorbar_step + self.colorbar_vmin + ) def check_tolerance(): - if abs(self.colorbar_vmax - vmax) / vmax > ( - tolerance / 100.) or abs(self.colorbar_vmin - vmin - ) > (tolerance / 100.): + if abs(self.colorbar_vmax - vmax) / vmax > (tolerance / 100.0) or abs( + self.colorbar_vmin - vmin + ) > (tolerance / 100.0): return True else: return False diff --git a/hyperspy/drawing/marker.py b/hyperspy/drawing/marker.py deleted file mode 100644 index b90f0d1e3c..0000000000 --- a/hyperspy/drawing/marker.py +++ /dev/null @@ -1,263 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import matplotlib.pyplot as plt -from hyperspy.events import Event, Events -import hyperspy.drawing._markers as markers -import logging - -_logger = logging.getLogger(__name__) - - -class MarkerBase(object): - - """Marker that can be added to the signal figure - - Attributes - ---------- - marker_properties : dictionary - Accepts a dictionary of valid (i.e. recognized by mpl.plot) - containing valid line properties. In addition it understands - the keyword `type` that can take the following values: - {'line', 'text'} - """ - - def __init__(self): - # Data attributes - self.data = None - self.axes_manager = None - self.ax = None - self.auto_update = True - - # Properties - self.marker = None - self._marker_properties = {} - self.signal = None - self._plot_on_signal = True - self.name = '' - self.plot_marker = True - - # Events - self.events = Events() - self.events.closed = Event(""" - Event triggered when a marker is closed. - - Arguments - --------- - marker : Marker - The marker that was closed. - """, arguments=['obj']) - self._closing = False - - def __deepcopy__(self, memo): - new_marker = dict2marker( - self._to_dictionary(), - self.name) - return new_marker - - @property - def marker_properties(self): - return self._marker_properties - - @marker_properties.setter - def marker_properties(self, kwargs): - - for key, item in kwargs.items(): - if item is None and key in self._marker_properties: - del self._marker_properties[key] - else: - self._marker_properties[key] = item - if self.marker is not None: - plt.setp(self.marker, **self.marker_properties) - self._render_figure() - - def _to_dictionary(self): - marker_dict = { - 'marker_properties': self.marker_properties, - 'marker_type': self.__class__.__name__, - 'plot_on_signal': self._plot_on_signal, - 'data': {k: self.data[k][()].tolist() for k in ( - 'x1', 'x2', 'y1', 'y2', 'text', 'size')} - } - return marker_dict - - def _get_data_shape(self): - data_shape = None - for key in ('x1', 'x2', 'y1', 'y2'): - ar = self.data[key][()] - if next(ar.flat) is not None: - data_shape = ar.shape - break - if data_shape is None: - raise ValueError("None of the coordinates have value") - else: - return data_shape - - def set_marker_properties(self, **kwargs): - """ - Set the line_properties attribute using keyword - arguments. - """ - self.marker_properties = kwargs - - def set_data(self, x1=None, y1=None, - x2=None, y2=None, text=None, size=None): - """ - Set data to the structured array. Each field of data should have - the same dimensions than the navigation axes. The other fields are - overwritten. - """ - self.data = np.array((np.array(x1), np.array(y1), - np.array(x2), np.array(y2), - np.array(text), np.array(size)), - dtype=[('x1', object), ('y1', object), - ('x2', object), ('y2', object), - ('text', object), ('size', object)]) - self._is_marker_static() - - def add_data(self, **kwargs): - """ - Add data to the structured array. Each field of data should have - the same dimensions than the navigation axes. The other fields are - not changed. - """ - if self.data is None: - self.set_data(**kwargs) - else: - for key in kwargs.keys(): - self.data[key][()] = np.array(kwargs[key]) - self._is_marker_static() - - def isiterable(self, obj): - return not isinstance(obj, (str, bytes)) and hasattr(obj, '__iter__') - - def _is_marker_static(self): - - test = [self.isiterable(self.data[key].item()[()]) is False - for key in self.data.dtype.names] - if np.alltrue(test): - self.auto_update = False - else: - self.auto_update = True - - def get_data_position(self, ind): - data = self.data - if data[ind].item()[()] is None: - return None - elif self.isiterable(data[ind].item()[()]) and self.auto_update: - if self.axes_manager is None: - return self.data['x1'].item().flatten()[0] - indices = self.axes_manager.indices[::-1] - return data[ind].item()[indices] - else: - return data[ind].item()[()] - - def plot(self, render_figure=True): - """ - Plot a marker which has been added to a signal. - - Parameters - ---------- - render_figure : bool, optional, default True - If True, will render the figure after adding the marker. - If False, the marker will be added to the plot, but will the figure - will not be rendered. This is useful when plotting many markers, - since rendering the figure after adding each marker will slow - things down. - """ - if self.ax is None: - raise AttributeError( - "To use this method the marker needs to be first add to a " + - "figure using `s._plot.signal_plot.add_marker(m)` or " + - "`s._plot.navigator_plot.add_marker(m)`") - self._plot_marker() - self.marker.set_animated(self.ax.figure.canvas.supports_blit) - if render_figure: - self._render_figure() - - def _render_figure(self): - self.ax.hspy_fig.render_figure() - - def close(self, render_figure=True): - """Remove and disconnect the marker. - - Parameters - ---------- - render_figure : bool, optional, default True - If True, the figure is rendered after removing the marker. - If False, the figure is not rendered after removing the marker. - This is useful when many markers are removed from a figure, - since rendering the figure after removing each marker will slow - things down. - """ - if self._closing: - return - self._closing = True - self.marker.remove() - self.events.closed.trigger(obj=self) - for f in self.events.closed.connected: - self.events.closed.disconnect(f) - if render_figure: - self._render_figure() - - -def dict2marker(marker_dict, marker_name): - marker_type = marker_dict['marker_type'] - if marker_type == 'Point': - marker = markers.point.Point(0, 0) - elif marker_type == 'HorizontalLine': - marker = markers.horizontal_line.HorizontalLine(0) - elif marker_type == 'HorizontalLineSegment': - marker = markers.horizontal_line_segment.HorizontalLineSegment(0, 0, 0) - elif marker_type == 'LineSegment': - marker = markers.line_segment.LineSegment(0, 0, 0, 0) - elif marker_type == 'Rectangle': - marker = markers.rectangle.Rectangle(0, 0, 0, 0) - elif marker_type == 'Text': - marker = markers.text.Text(0, 0, "") - elif marker_type == 'VerticalLine': - marker = markers.vertical_line.VerticalLine(0) - elif marker_type == 'VerticalLineSegment': - marker = markers.vertical_line_segment.VerticalLineSegment(0, 0, 0) - else: - _log = logging.getLogger(__name__) - _log.warning( - "Marker {} with marker type {} " - "not recognized".format(marker_name, marker_type)) - return(False) - marker.set_data(**marker_dict['data']) - marker.set_marker_properties(**marker_dict['marker_properties']) - marker._plot_on_signal = marker_dict['plot_on_signal'] - marker.name = marker_name - return(marker) - - -def markers_metadata_dict_to_markers(metadata_markers_dict, axes_manager): - markers_dict = {} - for marker_name, m_dict in metadata_markers_dict.items(): - try: - marker = dict2marker(m_dict, marker_name) - if marker is not False: - marker.axes_manager = axes_manager - markers_dict[marker_name] = marker - except Exception as expt: - _logger.warning( - "Marker {} could not be loaded, skipping it. " - "Error: {}".format(marker_name, expt)) - return(markers_dict) diff --git a/hyperspy/drawing/markers.py b/hyperspy/drawing/markers.py new file mode 100644 index 0000000000..c17752d998 --- /dev/null +++ b/hyperspy/drawing/markers.py @@ -0,0 +1,1074 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import logging +from copy import deepcopy + +import dask.array as da +import matplotlib.collections as mpl_collections +import numpy as np +from matplotlib.patches import Patch +from matplotlib.transforms import IdentityTransform + +import hyperspy +from hyperspy.events import Event, Events +from hyperspy.misc.array_tools import _get_navigation_dimension_chunk_slice +from hyperspy.misc.utils import isiterable + +_logger = logging.getLogger(__name__) + + +def convert_positions(peaks, signal_axes): + new_data = np.empty(peaks.shape[:-1] + (len(signal_axes),)) + for i, ax in enumerate(signal_axes[::-1]): + # indexes need to be reversed + new_data[..., (-i - 1)] = ax.scale * peaks[..., i] + ax.offset + return new_data + + +class Markers: + """A set of markers using Matplotlib collections.""" + + # The key defining the position, typically: `offsets`, `segments` or `verts` + _position_key = "offsets" + # For VerticalLines and HorizontalLines, the key to set is different from + # `_position_key` + _position_key_to_set = None + + def __init__( + self, + collection, + offset_transform="data", + transform="display", + shift=None, + plot_on_signal=True, + name="", + ScalarMappable_array=None, + **kwargs, + ): + """ + The markers are defined by a set of arugment required by the collections, + typically, ``offsets``, ``verts`` or ``segments`` will define their + positions. + + To define a non-static marker any argument that can be set with the + :meth:`matplotlib.collections.Collection.set` method can be passed + as an array with `dtype=object` of the constructor and the same size as + the navigation axes of the a signal the markers will be added to. + + Parameters + ---------- + collection : matplotlib.collections.Collection or str + A Matplotlib collection to be initialized. + offset_transform, transform : str + ``offset_transform`` define the transformation used for the + `offsets`` value and ``tranform`` define the transformation for + other arguments, typically to scale the size of the ``Path``. + It can be one of the following: + + - ``"data"``: the offsets are defined in data coordinates and the ``ax.transData`` transformation is used. + - ``"relative"``: The offsets are defined in data coordinates in x and coordinates in y relative to the + data plotted. Only for 1D figure. + - ``"axes"``: the offsets are defined in axes coordinates and the ``ax.transAxes`` transformation is used. + (0, 0) is bottom left of the axes, and (1, 1) is top right of the axes. + - ``"xaxis"``: The offsets are defined in data coordinates in x and axes coordinates in y direction; use + :meth:`matplotlib.axes.Axes.get_xaxis_transform` transformation. + - ``"yaxis"``: The offsets are defined in data coordinates in y and axes coordinates in x direction; use + :meth:`matplotlib.axes.Axes.get_xaxis_transform` transformation.. + - ``"display"``: the offsets are not transformed, i.e. are defined in the display coordinate system. + (0, 0) is the bottom left of the window, and (width, height) is top right of the output in "display units" + :class:`matplotlib.transforms.IdentityTransform`. + + shift : None or float + Only for ``offset_transform="relative"``. This applied a systematic + shift in the y component of the ``offsets`` values. The shift is + defined in the matplotlib ``"axes"`` coordinate system. + This provides a constant shift from the data for labeling + :class:`~.api.signals.Signal1D`. + plot_on_signal : bool + If True, plot on signal figure, otherwise on navigator. + name : str + The name of the markers. + ScalarMappable_array : Array-like + Set the array of the :class:`matplotlib.cm.ScalarMappable` of the + matplotlib collection. + The ``ScalarMappable`` array will overwrite ``facecolor`` and + ``edgecolor``. Default is None. + **kwargs : dict + Keyword arguments passed to the underlying marker collection. Any argument + that is array-like and has ``dtype=object`` is assumed to be an iterating + argument and is treated as such. + + Examples + -------- + Add markers using a :class:`matplotlib.collections.PatchCollection` + which will display the specified subclass of :class:`matplotlib.patches.Patch` + at the position defined by the argument ``offsets`` . + + >>> from matplotlib.collections import PatchCollection + >>> from matplotlib.patches import Circle + + >>> m = hs.plot.markers.Markers( + ... collection=PatchCollection, + ... patches=[Circle((0, 0), 1)], + ... offsets=np.random.rand(10,2)*10, + ... ) + >>> s = hs.signals.Signal2D(np.ones((10, 10, 10, 10))) + >>> s.plot() + >>> s.add_marker(m) + + Adding star "iterating" markers using :class:`matplotlib.collections.StarPolygonCollection` + + >>> from matplotlib.collections import StarPolygonCollection + >>> import numpy as np + >>> rng = np.random.default_rng(0) + >>> data = np.ones((25, 25, 100, 100)) + >>> s = hs.signals.Signal2D(data) + >>> offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + >>> for ind in np.ndindex(offsets.shape): + ... offsets[ind] = rng.random((10, 2)) * 100 + + Every other star has a size of 50/100 + + >>> m = hs.plot.markers.Markers( + ... collection=StarPolygonCollection, + ... offsets=offsets, + ... numsides=5, + ... color="orange", + ... sizes=(50, 100), + ... ) + >>> s.plot() + >>> s.add_marker(m) + + """ + if isinstance(collection, str): + try: + collection = getattr(mpl_collections, collection) + except AttributeError: + raise ValueError( + f"'{collection}' is not the name of a matplotlib collection class." + ) + + if not issubclass(collection, mpl_collections.Collection): + raise ValueError( + f"{collection} is not a subclass of `matplotlib.collection.Collection`." + ) + + if ".".join(collection.__module__.split(".")[:2]) not in [ + "matplotlib.collections", + "hyperspy.external", + ]: + # To be able to load a custom markers, we need to be able to instantiate + # the class and the safe way to do that is to import from + # `matplotlib.collections` or `hyperspy.external` (patched matplotlib collection) + raise ValueError( + "To support loading file saved with custom markers, the collection must be " + "implemented in matplotlib or hyperspy" + ) + + # Data attributes + self.kwargs = kwargs # all keyword arguments. + self.ax = None + self._offset_transform = None + self._transform = None + if self._position_key_to_set is None: + self._position_key_to_set = self._position_key + # The list of keys of iterable argument other than the "_position_key" + self._iterable_argument_keys = [] + + self.dask_kwargs = {} + for key, value in self.kwargs.items(): + # Populate `_iterable_argument_keys` + if ( + isiterable(value) + and not isinstance(value, str) + and key != self._position_key + ): + self._iterable_argument_keys.append(key) + + # Handling dask arrays + if isinstance(value, da.Array) and value.dtype == object: + self.dask_kwargs[key] = self.kwargs[key] + elif isinstance(value, da.Array): # and value.dtype != object: + self.kwargs[key] = value.compute() + # Patches or verts shouldn't be cast to array + elif ( + isinstance(value, list) + and len(value) > 0 + and not isinstance(value[0], Patch) + and not key == "verts" + ): + self.kwargs[key] = np.array(value) + elif isinstance(value, list) and len(value) == 0: + self.kwargs[key] = np.array(value) + + if key in ["sizes", "color"] and ( + not hasattr(value, "__len__") or isinstance(value, str) + ): + self.kwargs[key] = (value,) + + self._cache_dask_chunk_kwargs = {} + self._cache_dask_chunk_kwargs_slice = {} + + self._class_name = self.__class__.__name__ + self.name = name + # Properties + self._collection = None + # used in _initialize_collection + self._collection_class = collection + self._signal = None + self._plot_on_signal = plot_on_signal + self.shift = shift + self.offset_transform = offset_transform + self.transform = transform + self._ScalarMappable_array = ScalarMappable_array + + # Events + self.events = Events() + self.events.closed = Event( + """ + Event triggered when a marker is closed. + + Parameters + ---------- + marker : Marker + The marker that was closed. + """, + arguments=["obj"], + ) + self._closing = False + + @property + def _axes_manager(self): + if self._signal is not None: + return self._signal.axes_manager + else: + return None + + @property + def _is_iterating(self): + if self._plot_on_signal: + return np.any([is_iterating(value) for key, value in self.kwargs.items()]) + else: # currently iterating navigation markers are not supported + return False + + @property + def _signal(self): + return self.__signal + + @_signal.setter + def _signal(self, signal): + if signal is not None: + for key, value in self.kwargs.items(): + nav_shape = value.shape if is_iterating(value) else () + if ( + len(nav_shape) != 0 + and nav_shape != signal.axes_manager.navigation_shape + ): + raise ValueError( + "The shape of the variable length argument must match " + "the navigation shape of the signal." + ) + self.__signal = signal + + def _get_transform(self, attr="_transform"): + if self.ax is not None: # return the transform + transforms = { + "data": self.ax.transData, + "axes": self.ax.transAxes, + "display": IdentityTransform(), + "yaxis": self.ax.get_yaxis_transform(), + "xaxis": self.ax.get_xaxis_transform(), + "relative": self.ax.transData, + } + return transforms[getattr(self, attr)] + else: # return the string value + return getattr(self, attr) + + def _set_transform(self, value, attr="_transform"): + arg_list = ["data", "axes", "xaxis", "yaxis", "display", "relative"] + if value not in arg_list: + str_ = ", ".join([f"`{v}`" for v in arg_list]) + raise ValueError(f"The transform must be one of {str_}.") + setattr(self, attr, value) + if self._collection is not None and self.ax is not None: + getattr(self._collection, f"set{attr}")(getattr(self, attr[1:])) + # Update plot + self.update() + + @property + def offset_transform(self): + """The tranform being used for the ``offsets`` values.""" + return self._get_transform(attr="_offset_transform") + + @offset_transform.setter + def offset_transform(self, value): + self._set_transform(value, attr="_offset_transform") + + @property + def transform(self): + """ + The tranform being used for the values other than ``offsets``, + typically ``sizes``, etc. + """ + return self._get_transform(attr="_transform") + + @transform.setter + def transform(self, value): + self._set_transform(value, attr="_transform") + + def __len__(self): + """ + Return the number of markers in the collection at the current + navigation coordinate. + """ + if self._is_iterating and self._axes_manager is None: + # with variable length markers, the axes_manager is needed to + # know the navigation coordinates of the signal + raise RuntimeError( + "Variable length markers must be added to a signal to provide " + "the numbers of markers at the current navigation coordinates." + ) + + return self.get_current_kwargs()[self._position_key_to_set].shape[0] + + def remove_items(self, indices, keys=None, navigation_indices=None): + """ + Remove items from the markers. + + Parameters + ---------- + indices : slice, int or numpy.ndarray + Indicate indices of sub-arrays to remove along the specified axis. + keys : str, list of str or None + Specify the key of the ``Markers.kwargs`` to remove. If ``None``, + use all compatible keys. Default is ``None``. + navigation_indices : tuple + Only for variable-lenght markers. If ``None``, remove for all + navigation coordinates. + + Examples + -------- + Remove a single item: + + >>> offsets = np.array([[1, 1], [2, 2]]) + >>> m = hs.plot.markers.Points(offsets=offsets) + >>> print(m) + + >>> m.remove_items(indices=(1, )) + >>> print(len(m)) + 1 + + Remove a single item at specific navigation position for variable + length markers: + + >>> offsets = np.empty(4, dtype=object) + >>> texts = np.empty(4, dtype=object) + >>> for i in range(len(offsets)): + ... offsets[i] = np.array([[1, 1], [2, 2]]) + ... texts[i] = ['a' * (i+1)] * 2 + >>> m = hs.plot.markers.Texts(offsets=offsets, texts=texts) + >>> m.remove_items(1, navigation_indices=(1, )) + + Remove several items: + + >>> offsets = np.stack([np.arange(0, 100, 10)]*2).T + np.array([5,]*2) + >>> texts = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'f', 'h', 'i']) + >>> m = hs.plot.markers.Texts(offsets=offsets, texts=texts) + >>> print(m) + + >>> m.remove_items(indices=[1, 2]) + >>> print(m) + + + """ + if keys is None: + keys = self._iterable_argument_keys + [self._position_key] + # keeps value actually in kwargs + elif isinstance(keys, str): + keys = [ + keys, + ] + + if navigation_indices and not self._is_iterating: + raise ValueError( + "`navigation_indices` is only for variable length markers." + ) + + for key in keys: + value = self.kwargs[key] + # Don't remove when it doesn't have the same length as the + # position kwargs because it is a "cycling" argument + if ( + isiterable(value) + and not isinstance(value, str) + and len(value) == len(self.kwargs[self._position_key]) + ): + if isinstance(value, np.ndarray) and value.dtype == object: + # when navigation_indices is not provided + nav_iterator = navigation_indices or np.ndindex( + self.kwargs[self._position_key].shape + ) + for nav_indices in nav_iterator: + self.kwargs[key][nav_indices] = np.delete( + value[nav_indices], indices, axis=0 + ) + else: + self.kwargs[key] = np.delete(value, indices, axis=0) + self._update() + + def add_items(self, navigation_indices=None, **kwargs): + """ + Add items to the markers. + + Parameters + ---------- + navigation_indices : tuple or None + Only for variable-lenght markers. If ``None``, all for all + navigation coordinates. + + **kwargs : dict + Mapping of keys:values to add to the markers + + Examples + -------- + Add a single item: + + >>> offsets = np.array([[1, 1], [2, 2]]) + >>> texts = np.array(["a", "b"]) + >>> m = hs.plot.markers.Texts(offsets=offsets, texts=texts) + >>> print(m) + + >>> m.add_items(offsets=np.array([[0, 1]]), texts=["c"]) + >>> print(m) + + + Add a single item at a defined navigation position of variable + length markers: + + >>> offsets = np.empty(4, dtype=object) + >>> texts = np.empty(4, dtype=object) + >>> for i in range(len(offsets)): + ... offsets[i] = np.array([[1, 1], [2, 2]]) + ... texts[i] = ['a' * (i+1)] * 2 + >>> m = hs.plot.markers.Texts(offsets=offsets, texts=texts) + >>> m.add_items( + ... offsets=np.array([[0, 1]]), texts=["new_text"], + ... navigation_indices=(1, ) + ... ) + """ + + if navigation_indices and not self._is_iterating: + raise ValueError( + "`navigation_indices` is only for variable length markers." + ) + + for key, value in kwargs.items(): + if self.kwargs[key].dtype == object: + nav_iterator = navigation_indices or np.ndindex( + self.kwargs[self._position_key].shape + ) + for nav_indices in nav_iterator: + self.kwargs[key][nav_indices] = np.append( + self.kwargs[key][nav_indices], value, axis=0 + ) + else: + self.kwargs[key] = np.append(self.kwargs[key], value, axis=0) + self._update() + + def _get_cache_dask_kwargs_chunk(self, indices): + """ + Get the kwargs at some index. If the index is cached return the cached value + otherwise compute the kwargs and cache them. + """ + chunks = {key: value.chunks for key, value in self.dask_kwargs.items()} + chunk_slices = { + key: _get_navigation_dimension_chunk_slice(indices, chunk) + for key, chunk in chunks.items() + } + to_compute = {} + for key, value in self.dask_kwargs.items(): + index_slice = chunk_slices[key] + current_slice = self._cache_dask_chunk_kwargs_slice.get(key, None) + if current_slice is None or current_slice != index_slice: + to_compute[key] = value[index_slice] + self._cache_dask_chunk_kwargs_slice[key] = index_slice + + if len(to_compute) > 0: + # values = da.compute([value for value in to_compute.values()]) + # self._cache_dask_chunk_kwargs.update({key: value for + # key, value in zip(to_compute.keys(), values)}) + for key in to_compute: # this should be one da.compute() function... + self._cache_dask_chunk_kwargs[key] = to_compute[key].compute() + + out_kwargs = {} + for key, value in self._cache_dask_chunk_kwargs.items(): + temp_indices = list(indices) + for i, temp_slice in enumerate(chunk_slices[key]): + # add offset to the indices + temp_indices[i] -= temp_slice.start + temp_indices = tuple(temp_indices) + out_kwargs[key] = self._cache_dask_chunk_kwargs[key][temp_indices] + return out_kwargs + + def __repr__(self): + if self.name: + text = "<%s (%s)" % (self.name, self.__class__.__name__) + else: + text = "<%s" % self.__class__.__name__ + + text += ", length: " + if self._is_iterating: + try: + current = len(self) + except RuntimeError: + current = "not plotted" + text += "variable (current: %s)" % current + else: + text += "%s" % len(self) + text += ">" + + return text + + @classmethod + def from_signal( + cls, + signal, + key=None, + signal_axes="metadata", + **kwargs, + ): + """ + Initialize a marker collection from a hyperspy Signal. + + Parameters + ---------- + signal: :class:`~.api.signals.BaseSignal` + A value passed to the Collection as ``{key:signal.data}`` + key: str or None + The key used to create a key value pair to create the subclass of + :class:`matplotlib.collections.Collection`. If ``None`` (default) + the key is set to ``"offsets"``. + signal_axes: str, tuple of :class:`~.axes.UniformDataAxis` or None + If ``"metadata"`` look for signal_axes saved in metadata under + ``s.metadata.Peaks.signal_axes`` and convert from pixel positions + to real units before creating the collection. If a ``tuple`` of + signal axes, those axes will be used otherwise (``None``) + no transformation will happen. + """ + if signal_axes is None or ( + signal_axes == "metadata" + and not signal.metadata.has_item("Peaks.signal_axes") + ): + new_signal = signal + elif signal_axes == "metadata" and signal.metadata.has_item( + "Peaks.signal_axes" + ): + new_signal = signal.map( + convert_positions, + inplace=False, + ragged=True, + output_dtype=object, + signal_axes=signal.metadata.Peaks.signal_axes, + ) + elif isinstance(signal_axes, (tuple, list)): + new_signal = signal.map( + convert_positions, + inplace=False, + ragged=True, + output_dtype=object, + signal_axes=signal_axes, + ) + else: + raise ValueError( + "The keyword argument `signal_axes` must be one of " + "'metadata', a tuple of `DataAxes` or None." + ) + + if key is None: + key = cls._position_key + + # in case ragged array, we need to take the transpose to match the + # navigation shape of the signal, for static marker, there is no + # array dimention match the signal dimension and there is no + # navigation dimension, therefore it shouldn't be transposed + kwargs[key] = new_signal.data.T if new_signal.ragged else new_signal.data + + return cls(**kwargs) + + def __deepcopy__(self, memo): + new_marker = markers_dict_to_markers(self._to_dictionary()) + return new_marker + + def _to_dictionary(self): + class_name = self.__class__.__name__ + marker_dict = { + "class": class_name, + "name": self.name, + "plot_on_signal": self._plot_on_signal, + "offset_transform": self._offset_transform, + "transform": self._transform, + "kwargs": self.kwargs, + "ScalarMappable_array": self._ScalarMappable_array, + } + if class_name == "Markers": + marker_dict["collection"] = self._collection_class.__name__ + + return marker_dict + + def get_current_kwargs(self, only_variable_length=False): + """ + Return the current keyword arguments for updating the collection. + + Parameters + ---------- + only_variable_length : bool + If ``True``, only returns the variable length kwargs. Default is + ``False``. + + Returns + ------- + kwargs : dict + The argument at the current navigation position. + """ + current_keys = {} + if self._is_iterating: + indices = self._axes_manager.indices + for key, value in self.kwargs.items(): + if is_iterating(value): + if key not in self.dask_kwargs: + val = value[indices] + # some keys values need to iterate + if key in ["sizes", "color"] and not hasattr(val, "__len__"): + val = (val,) + current_keys[key] = val + elif not only_variable_length: + current_keys[key] = value + else: # key already set in init + pass + if len(self.dask_kwargs) > 0: + current_keys.update(self._get_cache_dask_kwargs_chunk(indices)) + else: + current_keys = self.kwargs + + # Handling relative markers + if "relative" in [self._offset_transform, self._transform]: + # scale based on current data + current_keys = self._scale_kwarg(current_keys) + + return current_keys + + def _scale_kwarg(self, kwds, key=None): + """ + Scale the kwarg by the current data. This is useful for scaling the + marker position by the current index or data value. + + When self.reference is "data" the kwarg is scaled by the current data value of the + "offset" or "segments" key + + When self.reference is "data_index" the kwarg is scaled by the current data value of the + "offset" or "segments" key and the given value of the index. This is useful when you want + to scale things by some value in the data that is not the same value. + """ + if key is None: + key = self._position_key + + x_positions = kwds[key][..., 0] + if len(x_positions) == 0: + # can't scale as there is no marker at this coordinate + return kwds + + new_kwds = deepcopy(kwds) + current_data = self._signal._get_current_data(as_numpy=True) + axis = self._axes_manager.signal_axes[0] + indexes = np.round((x_positions - axis.offset) / axis.scale).astype(int) + y_positions = new_kwds[key][..., 1] + new_y_positions = current_data[indexes] * y_positions + + if self.shift is not None: + yrange = np.max(current_data) - np.min(current_data) + new_y_positions = new_y_positions + self.shift * yrange + + new_kwds[key][..., 1] = new_y_positions + + return new_kwds + + def update(self): + """Update the markers on the plot.""" + if self._is_iterating or "relative" in [ + self._offset_transform, + self._transform, + ]: + self._update() + + def _update(self): + if self._signal: + kwds = self.get_current_kwargs(only_variable_length=True) + self._collection.set(**kwds) + + def _initialize_collection(self): + self._collection = self._collection_class( + **self.get_current_kwargs(), + offset_transform=self.offset_transform, + ) + self._collection.set_transform(self.transform) + + def plot(self, render_figure=True): + """ + Plot a marker which has been added to a signal. + + Parameters + ---------- + render_figure : bool, optional, default True + If True, will render the figure after adding the marker. + If False, the marker will be added to the plot, but will the figure + will not be rendered. This is useful when plotting many markers, + since rendering the figure after adding each marker will slow + things down. + """ + if self.ax is None: + raise AttributeError( + "To use this method the marker needs to be first add to a " + + "figure using `s._plot.signal_plot.add_marker(m)` or " + + "`s._plot.navigator_plot.add_marker(m)`" + ) + self._initialize_collection() + self._collection.set_animated(self.ax.figure.canvas.supports_blit) + self.ax.add_collection(self._collection) + if render_figure: + self._render_figure() + + def _render_figure(self): + self.ax.hspy_fig.render_figure() + + def close(self, render_figure=True): + """ + Remove and disconnect the marker. + + Parameters + ---------- + render_figure : bool, optional, default True + If True, the figure is rendered after removing the marker. + If False, the figure is not rendered after removing the marker. + This is useful when many markers are removed from a figure, + since rendering the figure after removing each marker will slow + things down. + """ + if self._closing: # pragma: no cover + return + self._closing = True + self._collection.remove() + self._collection = None + self.events.closed.trigger(obj=self) + self._signal = None + for f in self.events.closed.connected: + self.events.closed.disconnect(f) + if render_figure: + self._render_figure() + self._closing = False + + def set_ScalarMappable_array(self, array): + """ + Set the array of the :class:`matplotlib.cm.ScalarMappable` of the + matplotlib collection. + The ``ScalarMappable`` array will overwrite ``facecolor`` and + ``edgecolor``. + + Parameters + ---------- + array : array-like + The value that are mapped to the colors. + + See Also + -------- + plot_colorbar + """ + self._ScalarMappable_array = array + if self._collection is not None: + self._collection.set_array(array) + + def plot_colorbar(self): + """ + Add a colorbar for the collection. + + Returns + ------- + matplotlib.colorbar.Colorbar + The colorbar of the collection. + + See Also + -------- + set_ScalarMappable_array + + Examples + -------- + >>> rng = np.random.default_rng(0) + >>> s = hs.signals.Signal2D(np.ones((100, 100))) + >>> # Define the size of the circles + >>> sizes = rng.random((10, )) * 10 + 20 + >>> # Define the position of the circles + >>> offsets = rng.random((10, 2)) * 100 + >>> m = hs.plot.markers.Circles( + ... sizes=sizes, + ... offsets=offsets, + ... linewidth=2, + ... ) + >>> s.plot() + >>> s.add_marker(m) + >>> m.set_ScalarMappable_array(sizes.ravel() / 2) + >>> cbar = m.plot_colorbar() + >>> cbar.set_label('Circle radius') + """ + if self.ax is None: + raise RuntimeError("The markers needs to be plotted.") + self.set_ScalarMappable_array(self._ScalarMappable_array) + cbar = self.ax.figure.colorbar(self._collection) + + return cbar + + +def is_iterating(arg): + return isinstance(arg, (np.ndarray, da.Array)) and arg.dtype == object + + +def dict2vector(data, keys, return_size=True, dtype=float): + """Take some dictionary of values and create offsets based on the input keys. + For instances like creating a horizontal or vertical line then some key is duplicated. + + Multiple keys can be passed as well. For example to define a rectangle: + + >>> dict2vector(data, keys= [[["x1", "y1"], ["x2", "y2"]]]) # doctest: +SKIP + + In this example the keys will be unpacked to create a line segment + """ + keys = np.array(keys) + # check to see if the array should be ragged + unique_keys = np.unique(keys) + is_key_iter = [ + isiterable(data[key]) and not isinstance(data[key], str) for key in unique_keys + ] + if not any(is_key_iter): # no iterable keys + if dtype is str: + dtype = object + vector = np.empty(keys.shape, dtype=dtype) + for i in np.ndindex( + keys.shape + ): # iterate through keys and create resulting vector + vector[i] = data[keys[i]] + if dtype is object: + vector = np.asarray(vector, dtype=str) + else: + iter_key = unique_keys[is_key_iter][0] + nav_shape = data[iter_key].shape + if not all(is_key_iter): # only some values are iterating + non_iterkeys = unique_keys[np.logical_not(is_key_iter)] + for k in non_iterkeys: + data[k] = np.full(shape=nav_shape, fill_value=data[k]) + vector = np.empty(nav_shape, dtype=object) # Create ragged array + for i in np.ndindex(nav_shape): + if dtype is str: + vect = [] + for j in np.ndindex(keys.shape): + vect.append(data[keys[j]][i]) + vect = np.array(vect) + else: + vect = np.empty(keys.shape, dtype=dtype) + for j in np.ndindex(keys.shape): + vect[j] = data[keys[j]][i] + vector[i] = vect + if return_size: + if not isiterable(data["size"]): + size = data["size"] + else: + size = np.empty(data["size"].shape, dtype=object) + for i in np.ndindex(data["size"].shape): + size[i] = data["size"][i] + return vector, size + else: + return vector + + +def markers_dict_to_markers(marker_dict): + """ + This function maps a marker dict to a Markers object. It supports parsing + old markers API, typically for file saved with hyperspy < 2.0. + """ + # hyperspy 1.x markers uses `marker_type`, 2.x uses name + markers_class = marker_dict.pop("class", marker_dict.pop("marker_type", None)) + if markers_class is None: + raise ValueError("Not a valid marker dictionary.") + + kwargs = { + # in hyperspy >= 2.0, all data and properties are in kwargs + **marker_dict.pop("kwargs", {}), + # in hyperspy < 2.0, "markers properties" are saved in `marker_properties` + **marker_dict.pop("marker_properties", {}), + } + # Parse old markers API: add relevant "data" to kwargs + if "data" in marker_dict: + if "Point" in markers_class: + kwargs["offsets"], kwargs["sizes"] = dict2vector( + marker_dict["data"], keys=["x1", "y1"], return_size=True + ) + kwargs["facecolors"] = kwargs["color"] + kwargs["units"] = "dots" + if "size" not in kwargs: + kwargs["size"] = 20 + kwargs["size"] = kwargs["size"] / np.pi + markers_class = "Points" + elif "HorizontalLineSegment" in markers_class: + kwargs["segments"] = dict2vector( + marker_dict["data"], + keys=[[["x1", "y1"], ["x2", "y1"]]], + return_size=False, + ) + markers_class = "Lines" + + elif "HorizontalLine" in markers_class: + kwargs["offsets"] = dict2vector( + marker_dict["data"], keys=["y1"], return_size=False + ) + markers_class = "HorizontalLines" + + elif "VerticalLineSegment" in markers_class: + kwargs["segments"] = dict2vector( + marker_dict["data"], + keys=[[["x1", "y1"], ["x1", "y2"]]], + return_size=False, + ) + markers_class = "Lines" + elif "VerticalLine" in markers_class: + kwargs["offsets"] = dict2vector( + marker_dict["data"], keys=["x1"], return_size=False + ) + markers_class = "VerticalLines" + elif "Line" in markers_class: + kwargs["segments"] = dict2vector( + marker_dict["data"], + keys=[[["x1", "y1"], ["x2", "y2"]]], + return_size=False, + ) + markers_class = "Lines" + + elif "Arrow" in markers_class: + # check if dx == x2 or dx == x2 - x1, etc. + vectors = dict2vector( + marker_dict["data"], keys=["x1", "y1", "x2", "y2"], return_size=False + ) + if vectors.dtype == object: + offsets = np.empty(vectors.shape, dtype=object) + U = np.empty(vectors.shape, dtype=object) + V = np.empty(vectors.shape, dtype=object) + for i in np.ndindex(vectors.shape): + offsets[i] = np.array( + [ + [vectors[i][0], vectors[i][1]], + ] + ) + U[i] = np.array([vectors[i][0] - vectors[i][2]]) + V[i] = np.array([vectors[i][1] - vectors[i][3]]) + else: + offsets = np.array( + [ + [vectors[0], vectors[1]], + ] + ) + U = np.array([vectors[2] - vectors[0]]) + V = np.array([vectors[3] - vectors[1]]) + + kwargs["offsets"] = offsets + kwargs["U"] = U + kwargs["V"] = V + markers_class = "Arrows" + + elif "Rectangle" in markers_class: + # check if dx == x2 or dx == x2 - x1, etc. + vectors = dict2vector( + marker_dict["data"], keys=["x1", "y1", "x2", "y2"], return_size=False + ) + if vectors.dtype == object: + offsets = np.empty(vectors.shape, dtype=object) + widths = np.empty(vectors.shape, dtype=object) + heights = np.empty(vectors.shape, dtype=object) + for i in np.ndindex(vectors.shape): + offsets[i] = [ + [ + (vectors[i][0] + vectors[i][2]) / 2, + (vectors[i][1] + vectors[i][3]) / 2, + ], + ] + widths[i] = [ + np.abs(vectors[i][0] - vectors[i][2]), + ] + heights[i] = [ + np.abs(vectors[i][1] - vectors[i][3]), + ] + else: + offsets = [ + [((vectors[0] + vectors[2]) / 2), ((vectors[1] + vectors[3]) / 2)], + ] + widths = [ + np.abs(vectors[0] - vectors[2]), + ] + heights = [ + np.abs(vectors[1] - vectors[3]), + ] + kwargs["offsets"] = offsets + kwargs["widths"] = widths + kwargs["heights"] = heights + fill = kwargs.pop("fill") + if not fill: + kwargs["facecolor"] = "none" + markers_class = "Rectangles" + + elif "Ellipse" in markers_class: + kwargs["offsets"] = dict2vector( + marker_dict["data"], + keys=[ + ["x1", "y1"], + ], + return_size=False, + ) + kwargs["widths"] = dict2vector( + marker_dict["data"], keys=["x2"], return_size=False + ) + kwargs["heights"] = dict2vector( + marker_dict["data"], keys=["y2"], return_size=False + ) + fill = kwargs.pop("fill") + if not fill: + kwargs["facecolor"] = "none" + markers_class = "Ellipses" + + elif "Text" in markers_class: + kwargs["offsets"] = dict2vector( + marker_dict["data"], keys=[["x1", "y1"]], return_size=False + ) + kwargs["texts"] = dict2vector( + marker_dict["data"], keys=["text"], return_size=False, dtype=str + ) + kwargs["verticalalignment"] = "bottom" + kwargs["horizontalalignment"] = "left" + markers_class = "Texts" + + # remove "data" key:value + del marker_dict["data"] + if "size" in kwargs: + kwargs["sizes"] = kwargs.pop("size") + + return getattr(hyperspy.utils.markers, markers_class)(**marker_dict, **kwargs) diff --git a/hyperspy/drawing/mpl_he.py b/hyperspy/drawing/mpl_he.py index 49920abc4f..5004e622b8 100644 --- a/hyperspy/drawing/mpl_he.py +++ b/hyperspy/drawing/mpl_he.py @@ -1,38 +1,43 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from functools import partial import logging +import warnings +from functools import partial +import matplotlib from traits.api import Undefined -from hyperspy.drawing import widgets, signal1d, image from hyperspy.defaults_parser import preferences - +from hyperspy.drawing import image, signal1d, widgets +from hyperspy.events import Event, Events _logger = logging.getLogger(__name__) -class MPL_HyperExplorer(object): +def _is_widget_backend(): + backend = matplotlib.get_backend() + # in ipympl 0.9.4/ipython 8.24, the backend name changed from ipympl to widget + return backend.lower() in ["ipympl", "widget", "module://ipympl.backend_nbagg"] - """ - """ +class MPL_HyperExplorer: + """ """ def __init__(self): self.signal_data_function = None @@ -40,24 +45,37 @@ def __init__(self): # args to pass to `__call__` self.signal_data_function_kwargs = {} self.axes_manager = None - self.signal_title = '' - self.navigator_title = '' - self.quantity_label = '' + self.signal_title = "" + self.navigator_title = "" + self.quantity_label = "" self.signal_plot = None self.navigator_plot = None self.axis = None self.pointer = None self._pointer_nav_dim = None + self.events = Events() + self.events.closed = Event( + """ + Event that triggers when the figure window is closed. + + Parameters + ---------- + obj: SpectrumFigure instances + The instance that triggered the event. + """, + arguments=["obj"], + ) + def plot_signal(self, **kwargs): # This method should be implemented by the subclasses. # Doing nothing is good enough for signal_dimension==0 though. if self.axes_manager.signal_dimension == 0: return - if self.signal_data_function_kwargs.get('fft_shift', False): + if self.signal_data_function_kwargs.get("fft_shift", False): self.axes_manager = self.axes_manager.deepcopy() for axis in self.axes_manager.signal_axes: - axis.offset = -axis.high_value / 2. + axis.offset = -axis.high_value / 2.0 def plot_navigator(self, title=None, **kwargs): """ @@ -67,8 +85,8 @@ def plot_navigator(self, title=None, **kwargs): Title of the navigator. The default is None. **kwargs : dict The kwargs are passed to plot method of - :py:meth:`hyperspy.drawing.image.ImagePlot` or - :py:meth:`hyperspy.drawing.signal1d.Signal1DLine`. + :meth:`hyperspy.drawing.image.ImagePlot` or + :meth:`hyperspy.drawing.signal1d.Signal1DLine`. """ if self.axes_manager.navigation_dimension == 0: @@ -82,12 +100,18 @@ def plot_navigator(self, title=None, **kwargs): if len(self.navigator_data_function().shape) == 1: # Create the figure - sf = signal1d.Signal1DFigure(title=title) + fig = kwargs.pop("fig", None) + sf = signal1d.Signal1DFigure( + title=title, + # Passed to figure creation + _on_figure_window_close=self.close, + fig=fig, + ) axis = self.axes_manager.navigation_axes[0] - sf.xlabel = '%s' % str(axis) + sf.xlabel = "%s" % str(axis) if axis.units is not Undefined: - sf.xlabel += ' (%s)' % axis.units - sf.ylabel = r'$\Sigma\mathrm{data\,over\,all\,other\,axes}$' + sf.xlabel += " (%s)" % axis.units + sf.ylabel = r"$\Sigma\mathrm{data\,over\,all\,other\,axes}$" sf.axis = axis sf.axes_manager = self.axes_manager self.navigator_plot = sf @@ -101,8 +125,9 @@ def plot_navigator(self, title=None, **kwargs): for key in list(kwargs.keys()): if hasattr(sl, key): setattr(sl, key, kwargs.pop(key)) - sl.set_line_properties(color='blue', - type='step' if axis.is_uniform else 'line') + sl.set_line_properties( + color="blue", type="step" if axis.is_uniform else "line" + ) # Add the line to the figure sf.add_line(sl) sf.plot() @@ -111,13 +136,15 @@ def plot_navigator(self, title=None, **kwargs): self._get_navigation_sliders() for axis in self.axes_manager.navigation_axes[:-2]: axis.events.index_changed.connect(sf.update, []) - sf.events.closed.connect( - partial(axis.events.index_changed.disconnect, - sf.update), []) + self.events.closed.connect( + partial(axis.events.index_changed.disconnect, sf.update), [] + ) self.navigator_plot = sf elif len(self.navigator_data_function().shape) >= 2: # Create the figure - imf = image.ImagePlot(title=title) + imf = image.ImagePlot( + title=title, + ) imf.data_function = self.navigator_data_function # Set all kwargs value to the image figure before passing the rest @@ -137,24 +164,27 @@ def plot_navigator(self, title=None, **kwargs): self._get_navigation_sliders() for axis in self.axes_manager.navigation_axes[2:]: axis.events.index_changed.connect(imf.update, []) - imf.events.closed.connect( - partial(axis.events.index_changed.disconnect, - imf.update), []) + self.events.closed.connect( + partial(axis.events.index_changed.disconnect, imf.update), + [], + ) - if "cmap" not in kwargs.keys() or kwargs['cmap'] is None: + if "cmap" not in kwargs.keys() or kwargs["cmap"] is None: kwargs["cmap"] = preferences.Plot.cmap_navigator - imf.plot(**kwargs) + imf.plot( + # Passed to figure creation + _on_figure_window_close=self.close, + # Other kwargs + **kwargs, + ) self.pointer.set_mpl_ax(imf.ax) self.navigator_plot = imf - if self.navigator_plot is not None: - self.navigator_plot.events.closed.connect( - self._on_navigator_plot_closing, []) - def _get_navigation_sliders(self): try: self.axes_manager.gui_navigation_sliders( - title=self.signal_title + " navigation sliders") + title=self.signal_title + " navigation sliders" + ) except (ValueError, ImportError) as e: _logger.warning("Navigation sliders not available. " + str(e)) @@ -177,20 +207,70 @@ def is_active(self): def plot(self, **kwargs): # Parse the kwargs for plotting complex data - for key in ['power_spectrum', 'fft_shift']: + for key in ["power_spectrum", "fft_shift"]: if key in kwargs: self.signal_data_function_kwargs[key] = kwargs.pop(key) - if self.pointer is None: - pointer = self.assign_pointer() - if pointer is not None: - self.pointer = pointer(self.axes_manager) - self.pointer.color = 'red' - self.pointer.connect_navigate() - self.plot_navigator(**kwargs.pop('navigator_kwds', {})) - if pointer is not None: - self.navigator_plot.events.closed.connect( - self.pointer.disconnect, []) - self.plot_signal(**kwargs) + if not _is_widget_backend() and "plot_style" in kwargs: + warnings.warn( + "The `plot_style` keyword is only used when the `ipympl` or `widget`" + "plotting backends are used." + ) + plot_style = kwargs.pop("plot_style", None) + + # matplotlib plotting backend + def plot_sig_and_nav(plot_style): + if self.pointer is None: + pointer = self.assign_pointer() + if pointer is not None: + self.pointer = pointer(self.axes_manager) + self.pointer.is_pointer = True + self.pointer.color = "red" + self.pointer.connect_navigate() + self.plot_navigator(**kwargs.pop("navigator_kwds", {})) + if pointer is not None: + self.events.closed.connect(self.pointer.disconnect, []) + self.plot_signal(**kwargs) + if _is_widget_backend() and "fig" not in kwargs: + if plot_style not in ["vertical", "horizontal", None]: + raise ValueError( + "plot_style must be one of ['vertical', 'horizontal', None]" + ) + if plot_style is None: + plot_style = preferences.Plot.widget_plot_style + # If widgets do not already exist, we will `display` them at the end + from IPython.display import display + from ipywidgets.widgets import HBox, VBox + + if self.signal_plot is None and self.navigator_plot is not None: + # in case the signal is navigation only + display(self.navigator_plot.figure.canvas) + elif self.navigator_plot is None: + # in case the signal is signal only + display(self.signal_plot.figure.canvas) + elif plot_style == "horizontal": + display( + HBox( + [ + self.navigator_plot.figure.canvas, + self.signal_plot.figure.canvas, + ] + ) + ) + else: # plot_style == "vertical": + display( + VBox( + [ + self.navigator_plot.figure.canvas, + self.signal_plot.figure.canvas, + ] + ) + ) + + if _is_widget_backend() and "fig" not in kwargs: + with matplotlib.pyplot.ioff(): + plot_sig_and_nav(plot_style) + else: + plot_sig_and_nav(plot_style) def assign_pointer(self): if self.navigator_data_function is None: @@ -212,19 +292,25 @@ def assign_pointer(self): self._pointer_nav_dim = nav_dim return Pointer - def _on_navigator_plot_closing(self): - self.navigator_plot = None - - def _on_signal_plot_closing(self): - self.signal_plot = None - def close(self): - """When closing, we make sure: - - close the matplotlib figure - - drawing events are disconnected - - the attribute 'signal_plot' and 'navigation_plot' are set to None """ - if self.is_active: - if self.signal_plot: - self.signal_plot.close() - self.close_navigator_plot() + Close the plot of the signals. + + This function is called programmatically or on matplotlib close + callback. + When closing, it does the following: + 1. trigger a closed event + 2. disconnect the closed event + 3. run the close method of the signal_plot and navigator_plot + 4. reset the attribute + """ + self.events.closed.trigger(obj=self) + for f in self.events.closed.connected: + self.events.closed.disconnect(f) + + for p in [self.signal_plot, self.navigator_plot]: + if p is not None: + p.close() + + self.navigator_plot = None + self.signal_plot = None diff --git a/hyperspy/drawing/mpl_hie.py b/hyperspy/drawing/mpl_hie.py index a784d96f7d..add19f160b 100644 --- a/hyperspy/drawing/mpl_hie.py +++ b/hyperspy/drawing/mpl_hie.py @@ -1,28 +1,27 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +from hyperspy.defaults_parser import preferences from hyperspy.drawing import image from hyperspy.drawing.mpl_he import MPL_HyperExplorer -from hyperspy.defaults_parser import preferences class MPL_HyperImage_Explorer(MPL_HyperExplorer): - def plot_signal(self, **kwargs): """ Parameters @@ -46,18 +45,23 @@ def plot_signal(self, **kwargs): imf.quantity_label = self.quantity_label - kwargs['data_function_kwargs'] = self.signal_data_function_kwargs - if "cmap" not in kwargs.keys() or kwargs['cmap'] is None: + kwargs["data_function_kwargs"] = self.signal_data_function_kwargs + if "cmap" not in kwargs.keys() or kwargs["cmap"] is None: kwargs["cmap"] = preferences.Plot.cmap_signal - imf.plot(**kwargs) + imf.plot( + # Passed to figure creation + _on_figure_window_close=self.close, + # Other kwargs + **kwargs, + ) self.signal_plot = imf if imf.figure is not None: if self.axes_manager.navigation_axes: self.signal_plot.figure.canvas.mpl_connect( - 'key_press_event', self.axes_manager.key_navigator) + "key_press_event", self.axes_manager.key_navigator + ) if self.navigator_plot is not None: self.navigator_plot.figure.canvas.mpl_connect( - 'key_press_event', self.axes_manager.key_navigator) - imf.events.closed.connect(self.close_navigator_plot, []) - imf.events.closed.connect(self._on_signal_plot_closing, []) + "key_press_event", self.axes_manager.key_navigator + ) diff --git a/hyperspy/drawing/mpl_hse.py b/hyperspy/drawing/mpl_hse.py index da3432f733..7d9a53f99e 100644 --- a/hyperspy/drawing/mpl_hse.py +++ b/hyperspy/drawing/mpl_hse.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import copy @@ -22,12 +22,11 @@ import numpy as np from traits.api import Undefined -from hyperspy.drawing.mpl_he import MPL_HyperExplorer from hyperspy.drawing import signal1d +from hyperspy.drawing.mpl_he import MPL_HyperExplorer class MPL_HyperSignal1D_Explorer(MPL_HyperExplorer): - """Plots the current spectrum to the screen and a map with a cursor to explore the SI. @@ -35,8 +34,8 @@ class MPL_HyperSignal1D_Explorer(MPL_HyperExplorer): def __init__(self): super(MPL_HyperSignal1D_Explorer, self).__init__() - self.xlabel = '' - self.ylabel = '' + self.xlabel = "" + self.ylabel = "" self.right_pointer = None self._right_pointer_on = False self._auto_update_plot = True @@ -49,8 +48,7 @@ def auto_update_plot(self): def auto_update_plot(self, value): if self._auto_update_plot is value: return - for line in self.signal_plot.ax_lines + \ - self.signal_plot.right_ax_lines: + for line in self.signal_plot.ax_lines + self.signal_plot.right_ax_lines: line.auto_update = value if self.pointer is not None: if value is True: @@ -77,18 +75,21 @@ def plot_signal(self, **kwargs): super().plot_signal() # Create the figure self.axis = self.axes_manager.signal_axes[0] - sf = signal1d.Signal1DFigure(title=self.signal_title + - " Signal") + fig = kwargs.pop("fig", None) + sf = signal1d.Signal1DFigure( + title=self.signal_title + " Signal", + # Passed to figure creation + _on_figure_window_close=self.close, + fig=fig, + ) sf.axis = self.axis if sf.ax is None: sf.create_axis() sf.axes_manager = self.axes_manager - self.xlabel = '{}'.format(self.axes_manager.signal_axes[0]) + self.xlabel = "{}".format(self.axes_manager.signal_axes[0]) if self.axes_manager.signal_axes[0].units is not Undefined: - self.xlabel += ' ({})'.format( - self.axes_manager.signal_axes[0].units) - self.ylabel = self.quantity_label if self.quantity_label != '' \ - else 'Intensity' + self.xlabel += " ({})".format(self.axes_manager.signal_axes[0].units) + self.ylabel = self.quantity_label if self.quantity_label != "" else "Intensity" sf.xlabel = self.xlabel sf.ylabel = self.ylabel @@ -97,13 +98,13 @@ def plot_signal(self, **kwargs): sl = signal1d.Signal1DLine() is_complex = np.iscomplexobj(self.signal_data_function()) sl.data_function = self.signal_data_function - kwargs['data_function_kwargs'] = self.signal_data_function_kwargs + kwargs["data_function_kwargs"] = self.signal_data_function_kwargs sl.plot_indices = True if self.pointer is not None: color = self.pointer.color else: - color = 'red' - sl.set_line_properties(color=color, type='step') + color = "red" + sl.set_line_properties(color=color, type="step") # Add the line to the figure: sf.add_line(sl) # If the data is complex create a line in the left axis with the @@ -113,7 +114,7 @@ def plot_signal(self, **kwargs): sl.data_function = self.signal_data_function sl.plot_coordinates = True sl._plot_imag = True - sl.set_line_properties(color="blue", type='step') + sl.set_line_properties(color="blue", type="step") # Add extra line to the figure sf.add_line(sl) @@ -122,16 +123,18 @@ def plot_signal(self, **kwargs): if sf.figure is not None: if self.axes_manager.navigation_axes: self.signal_plot.figure.canvas.mpl_connect( - 'key_press_event', self.axes_manager.key_navigator) + "key_press_event", self.axes_manager.key_navigator + ) if self.navigator_plot is not None: - sf.events.closed.connect(self.close_navigator_plot, []) self.signal_plot.figure.canvas.mpl_connect( - 'key_press_event', self.key2switch_right_pointer) + "key_press_event", self.key2switch_right_pointer + ) self.navigator_plot.figure.canvas.mpl_connect( - 'key_press_event', self.key2switch_right_pointer) + "key_press_event", self.key2switch_right_pointer + ) self.navigator_plot.figure.canvas.mpl_connect( - 'key_press_event', self.axes_manager.key_navigator) - sf.events.closed.connect(self._on_signal_plot_closing, []) + "key_press_event", self.axes_manager.key_navigator + ) def key2switch_right_pointer(self, event): if event.key == "e": @@ -139,33 +142,31 @@ def key2switch_right_pointer(self, event): def add_right_pointer(self, **kwargs): if self.signal_plot.right_axes_manager is None: - self.signal_plot.right_axes_manager = \ - copy.deepcopy(self.axes_manager) + self.signal_plot.right_axes_manager = copy.deepcopy(self.axes_manager) if self.right_pointer is None: pointer = self.assign_pointer() - self.right_pointer = pointer( - self.signal_plot.right_axes_manager) + self.right_pointer = pointer(self.signal_plot.right_axes_manager) # The following is necessary because e.g. a line pointer does not # have size if hasattr(self.pointer, "size"): self.right_pointer.size = self.pointer.size - self.right_pointer.color = 'blue' + self.right_pointer.color = "blue" self.right_pointer.connect_navigate() self.right_pointer.set_mpl_ax(self.navigator_plot.ax) if self.right_pointer is not None: - for axis in self.axes_manager.navigation_axes[ - self._pointer_nav_dim:]: - self.signal_plot.right_axes_manager._axes[ - axis.index_in_array] = axis + for axis in self.axes_manager.navigation_axes[self._pointer_nav_dim :]: + self.signal_plot.right_axes_manager._axes[axis.index_in_array] = axis rl = signal1d.Signal1DLine() rl.data_function = self.signal_data_function - rl.set_line_properties(color=self.right_pointer.color, - type='step') + rl.set_line_properties(color=self.right_pointer.color, type="step") self.signal_plot.create_right_axis() - self.signal_plot.add_line(rl, ax='right', connect_navigation=True) + self.signal_plot.add_line(rl, ax="right", connect_navigation=True) rl.plot_indices = True - rl.text_position = (1., 1.05,) + rl.text_position = ( + 1.0, + 1.05, + ) rl.plot(**kwargs) self.right_pointer_on = True # because we added the right axis, we need to redraw the canvas to diff --git a/hyperspy/drawing/signal.py b/hyperspy/drawing/signal.py index de9c18646c..228568aa35 100644 --- a/hyperspy/drawing/signal.py +++ b/hyperspy/drawing/signal.py @@ -1,34 +1,41 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . # This file contains plotting code generic to the BaseSignal class. -import numpy as np import matplotlib.pyplot as plt +import numpy as np from mpl_toolkits.axes_grid1 import make_axes_locatable from traits.api import Undefined from hyperspy.drawing.utils import set_axes_decor +from hyperspy.misc.utils import to_numpy -def _plot_1D_component(factors, idx, axes_manager, ax=None, - calibrate=True, comp_label=None, - same_window=False): +def _plot_1D_component( + factors, + idx, + axes_manager, + ax=None, + calibrate=True, + comp_label=None, + same_window=False, +): if ax is None: ax = plt.gca() axis = axes_manager.signal_axes[0] @@ -37,33 +44,39 @@ def _plot_1D_component(factors, idx, axes_manager, ax=None, plt.xlabel(axis.units) else: x = np.arange(axis.size) - plt.xlabel('Channel index') - ax.plot(x, factors[:, idx], label='%i' % idx) + plt.xlabel("Channel index") + ax.plot(x, to_numpy(factors[:, idx]), label=f"{idx}") if comp_label and not same_window: - plt.title('%s' % comp_label) + plt.title(f"{comp_label}") return ax -def _plot_2D_component(factors, idx, axes_manager, - calibrate=True, ax=None, - comp_label=None, cmap=plt.cm.gray, - axes_decor='all' - ): +def _plot_2D_component( + factors, + idx, + axes_manager, + calibrate=True, + ax=None, + comp_label=None, + cmap=plt.cm.gray, + axes_decor="all", +): + shape = axes_manager._signal_shape_in_array + factors = to_numpy(factors[:, idx].reshape(shape)) if ax is None: ax = plt.gca() axes = axes_manager.signal_axes[::-1] - shape = axes_manager._signal_shape_in_array extent = None if calibrate: - extent = (axes[1].low_value, - axes[1].high_value, - axes[0].high_value, - axes[0].low_value) + extent = ( + axes[1].low_value, + axes[1].high_value, + axes[0].high_value, + axes[0].low_value, + ) if comp_label: - plt.title('%s' % idx) - im = ax.imshow(factors[:, idx].reshape(shape), - cmap=cmap, interpolation='nearest', - extent=extent) + plt.title(f"{idx}") + im = ax.imshow(factors, cmap=cmap, interpolation="nearest", extent=extent) # Set axes decorations based on user input set_axes_decor(ax, axes_decor) @@ -74,10 +87,19 @@ def _plot_2D_component(factors, idx, axes_manager, return ax -def _plot_loading(loadings, idx, axes_manager, ax=None, - comp_label=None, no_nans=True, - calibrate=True, cmap=plt.cm.gray, - same_window=False, axes_decor='all'): +def _plot_loading( + loadings, + idx, + axes_manager, + ax=None, + comp_label=None, + no_nans=True, + calibrate=True, + cmap=plt.cm.gray, + same_window=False, + axes_decor="all", +): + loadings = to_numpy(loadings[idx]) if ax is None: ax = plt.gca() if no_nans: @@ -88,24 +110,26 @@ def _plot_loading(loadings, idx, axes_manager, ax=None, # get calibration from a passed axes_manager shape = axes_manager._navigation_shape_in_array if calibrate: - extent = (axes[0].low_value, - axes[0].high_value, - axes[1].high_value, - axes[1].low_value) - im = ax.imshow(loadings[idx].reshape(shape), - cmap=cmap, extent=extent, - interpolation='nearest') + extent = ( + axes[0].low_value, + axes[0].high_value, + axes[1].high_value, + axes[1].low_value, + ) + im = ax.imshow( + loadings.reshape(shape), cmap=cmap, extent=extent, interpolation="nearest" + ) if calibrate: plt.xlabel(axes[0].units) plt.ylabel(axes[1].units) else: - plt.xlabel('pixels') - plt.ylabel('pixels') + plt.xlabel("pixels") + plt.ylabel("pixels") if comp_label: if same_window: - plt.title('%s' % idx) + plt.title(f"{idx}") else: - plt.title('%s #%s' % (comp_label, idx)) + plt.title(f"{idx} #{idx}") # Set axes decorations based on user input set_axes_decor(ax, axes_decor) @@ -118,17 +142,16 @@ def _plot_loading(loadings, idx, axes_manager, ax=None, x = axes[0].axis else: x = np.arange(axes[0].size) - ax.step(x, loadings[idx], - label='%s' % idx) + ax.step(x, loadings, label=f"{idx}") if comp_label and not same_window: - plt.title('%s #%s' % (comp_label, idx)) - plt.ylabel('Score (a. u.)') + plt.title(f"{comp_label} #{idx}") + plt.ylabel("Score (a. u.)") if calibrate: if axes[0].units is not Undefined: plt.xlabel(axes[0].units) else: - plt.xlabel('depth') + plt.xlabel("depth") else: - plt.xlabel('depth') + plt.xlabel("depth") else: - raise ValueError('View not supported') + raise ValueError("View not supported") diff --git a/hyperspy/drawing/signal1d.py b/hyperspy/drawing/signal1d.py index 3277ab4edf..a88c131c3d 100644 --- a/hyperspy/drawing/signal1d.py +++ b/hyperspy/drawing/signal1d.py @@ -1,47 +1,43 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import warnings -import numpy as np -import matplotlib.pyplot as plt -import matplotlib as mpl -from mpl_toolkits.axes_grid1 import make_axes_locatable -import logging import inspect +import logging from functools import partial -from hyperspy.drawing.figure import BlittedFigure +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +from mpl_toolkits.axes_grid1 import make_axes_locatable + from hyperspy.drawing import utils +from hyperspy.drawing.figure import BlittedFigure from hyperspy.events import Event, Events -from hyperspy.exceptions import VisibleDeprecationWarning from hyperspy.misc.test_utils import ignore_warning - _logger = logging.getLogger(__name__) class Signal1DFigure(BlittedFigure): + """ """ - """ - """ - - def __init__(self, title=""): - super(Signal1DFigure, self).__init__() + def __init__(self, title="", **kwargs): + super().__init__() self.figure = None self.ax = None self.right_ax = None @@ -51,17 +47,18 @@ def __init__(self, title=""): self.right_axes_manager = None # Labels - self.xlabel = '' - self.ylabel = '' + self.xlabel = "" + self.ylabel = "" self.title = title - self.create_figure() + self.create_figure(**kwargs) self.create_axis() # Color cycles self._color_cycles = { - 'line': utils.ColorCycle(), - 'step': utils.ColorCycle(), - 'scatter': utils.ColorCycle(), } + "line": utils.ColorCycle(), + "step": utils.ColorCycle(), + "scatter": utils.ColorCycle(), + } def create_axis(self): self.ax = self.figure.add_subplot(111) @@ -70,7 +67,7 @@ def create_axis(self): self.ax.xaxis.set_animated(animated) self.ax.hspy_fig = self - def create_right_axis(self, color='black', adjust_layout=True): + def create_right_axis(self, color="black", adjust_layout=True): """ Add an axis on the right hand side of the figure. @@ -90,7 +87,11 @@ def create_right_axis(self, color='black', adjust_layout=True): self.right_ax = self.ax.twinx() self.right_ax.hspy_fig = self self.right_ax.yaxis.set_animated(self.figure.canvas.supports_blit) - self.right_ax.tick_params(axis='y', labelcolor=color) + self.right_ax.tick_params(axis="y", labelcolor=color) + # Needs to set the zorder of the ax to get the mouse event for ax + # See https://github.com/matplotlib/matplotlib/issues/10009 + self.ax.set_zorder(self.right_ax.get_zorder() + 1) + self.ax.patch.set_visible(False) if adjust_layout: plt.tight_layout() @@ -111,12 +112,12 @@ def close_right_axis(self, adjust_layout=True): if self.right_ax is not None: for lines in self.right_ax_lines: lines.close() - self.right_ax.axes.get_yaxis().set_visible(False) + self.right_ax.remove() self.right_ax = None if adjust_layout: plt.tight_layout() - def add_line(self, line, ax='left', connect_navigation=False): + def add_line(self, line, ax="left", connect_navigation=False): """ Add Signal1DLine to figure @@ -137,13 +138,13 @@ def add_line(self, line, ax='left', connect_navigation=False): None. """ - if ax == 'left': + if ax == "left": line.ax = self.ax if line.axes_manager is None: line.axes_manager = self.axes_manager self.ax_lines.append(line) line.sf_lines = self.ax_lines - elif ax == 'right': + elif ax == "right": line.ax = self.right_ax self.right_ax_lines.append(line) line.sf_lines = self.right_ax_lines @@ -153,8 +154,8 @@ def add_line(self, line, ax='left', connect_navigation=False): f = partial(line._auto_update_line, update_ylimits=True) line.axes_manager.events.indices_changed.connect(f, []) line.events.closed.connect( - lambda: line.axes_manager.events.indices_changed.disconnect(f), - []) + lambda: line.axes_manager.events.indices_changed.disconnect(f), [] + ) line.axis = self.axis # Automatically asign the color if not defined if line.color is None: @@ -164,8 +165,7 @@ def add_line(self, line, ax='left', connect_navigation=False): else: rgba_color = mpl.colors.colorConverter.to_rgba(line.color) if rgba_color in self._color_cycles[line.type].color_cycle: - self._color_cycles[line.type].color_cycle.remove( - rgba_color) + self._color_cycles[line.type].color_cycle.remove(rgba_color) def plot(self, data_function_kwargs={}, **kwargs): self.ax.set_xlabel(self.xlabel) @@ -173,36 +173,42 @@ def plot(self, data_function_kwargs={}, **kwargs): self.ax.set_title(self.title) x_axis_upper_lims = [] x_axis_lower_lims = [] + for line in self.ax_lines: line.plot(data_function_kwargs=data_function_kwargs, **kwargs) - x_axis_lower_lims.append(line.axis.axis[0]) - x_axis_upper_lims.append(line.axis.axis[-1]) + if len(line.axis.axis) > 1: + x_axis_lower_lims.append(line.axis.axis[0]) + x_axis_upper_lims.append(line.axis.axis[-1]) + for marker in self.ax_markers: marker.plot(render_figure=False) - plt.xlim(np.min(x_axis_lower_lims), np.max(x_axis_upper_lims)) + + plt.xlim( + min(x_axis_lower_lims, default=None), max(x_axis_upper_lims, default=None) + ) + self.axes_manager.events.indices_changed.connect(self.update, []) self.events.closed.connect( - lambda: self.axes_manager.events.indices_changed.disconnect( - self.update), []) + lambda: self.axes_manager.events.indices_changed.disconnect(self.update), [] + ) - self.ax.figure.canvas.draw_idle() - if hasattr(self.figure, 'tight_layout'): + if hasattr(self.figure, "tight_layout"): try: self.figure.tight_layout() except BaseException: # tight_layout is a bit brittle, we do this just in case it # complains pass - self.figure.canvas.draw() + self.render_figure() def _on_close(self): - _logger.debug('Closing Signal1DFigure.') + _logger.debug("Closing Signal1DFigure.") if self.figure is None: return # Already closed for line in self.ax_lines + self.right_ax_lines: line.close() - super(Signal1DFigure, self)._on_close() - _logger.debug('Signal1DFigure Closed.') + super()._on_close() + _logger.debug("Signal1DFigure Closed.") def update(self): """ @@ -216,8 +222,7 @@ def update_lines(ax, ax_lines): for line in ax_lines: # save on figure rendering and do it at the end # don't update the y limits - line._auto_update_line(render_figure=False, - update_ylimits=False) + line._auto_update_line(render_figure=False, update_ylimits=False) y_min = np.nanmin([y_min, line._y_min]) y_max = np.nanmax([y_max, line._y_max]) ax.set_ylim(y_min, y_max) @@ -235,7 +240,6 @@ def update_lines(ax, ax_lines): class Signal1DLine(object): - """Line that can be added to Signal1DFigure. Attributes @@ -266,13 +270,17 @@ class Signal1DLine(object): def __init__(self): self.events = Events() - self.events.closed = Event(""" + self.events.closed = Event( + """ Event that triggers when the line is closed. - Arguments: - obj: Signal1DLine instance - The instance that triggered the event. - """, arguments=["obj"]) + Parameters + ---------- + obj: Signal1DLine instance + The instance that triggered the event. + """, + arguments=["obj"], + ) self.sf_lines = None self.ax = None # Data attributes @@ -282,40 +290,36 @@ def __init__(self): self.axis = None self.axes_manager = None self._plot_imag = False - self.norm = 'linear' + self.norm = "linear" # Properties self.auto_update = True - self.autoscale = 'v' + self.autoscale = "v" self._y_min = np.nan self._y_max = np.nan self.line = None self.plot_indices = False self.text = None - self.text_position = (-0.1, 1.05,) + self.text_position = ( + -0.1, + 1.05, + ) self._line_properties = {} self.type = "line" - @property - def get_complex(self): - warnings.warn("The `get_complex` attribute is deprecated and will be" - "removed in 2.0, please use `_plot_imag` instead.", - VisibleDeprecationWarning) - return self._plot_imag - @property def line_properties(self): return self._line_properties @line_properties.setter def line_properties(self, kwargs): - if 'type' in kwargs: - self.type = kwargs['type'] - del kwargs['type'] + if "type" in kwargs: + self.type = kwargs["type"] + del kwargs["type"] - if 'color' in kwargs: - color = kwargs['color'] - del kwargs['color'] + if "color" in kwargs: + color = kwargs["color"] + del kwargs["color"] self.color = color for key, item in kwargs.items(): @@ -337,23 +341,24 @@ def type(self): @type.setter def type(self, value): lp = {} - if value == 'scatter': - lp['marker'] = 'o' - lp['linestyle'] = 'None' - lp['markersize'] = 1 - - elif value == 'line': - lp['linestyle'] = '-' - lp['marker'] = "None" - lp['drawstyle'] = "default" - elif value == 'step': - lp['drawstyle'] = 'steps-mid' - lp['marker'] = "None" + if value == "scatter": + lp["marker"] = "o" + lp["linestyle"] = "None" + lp["markersize"] = 1 + + elif value == "line": + lp["linestyle"] = "-" + lp["marker"] = "None" + lp["drawstyle"] = "default" + elif value == "step": + lp["drawstyle"] = "steps-mid" + lp["marker"] = "None" else: raise ValueError( "`type` must be one of " - "{\'scatter\', \'line\', \'step\'}" - "but %s was given" % value) + "{'scatter', 'line', 'step'}" + "but %s was given" % value + ) self._type = value self.line_properties = lp if self.color is not None: @@ -361,24 +366,24 @@ def type(self, value): @property def color(self): - if 'color' in self.line_properties: - return self.line_properties['color'] - elif 'markeredgecolor' in self.line_properties: - return self.line_properties['markeredgecolor'] + if "color" in self.line_properties: + return self.line_properties["color"] + elif "markeredgecolor" in self.line_properties: + return self.line_properties["markeredgecolor"] else: return None @color.setter def color(self, color): - if self._type == 'scatter': + if self._type == "scatter": self.set_line_properties(markeredgecolor=color) - if 'color' in self._line_properties: - del self._line_properties['color'] + if "color" in self._line_properties: + del self._line_properties["color"] else: - if color is None and 'color' in self._line_properties: - del self._line_properties['color'] + if color is None and "color" in self._line_properties: + del self._line_properties["color"] else: - self._line_properties['color'] = color + self._line_properties["color"] = color self.set_line_properties(markeredgecolor=None) if self.line is not None: @@ -395,41 +400,54 @@ def plot(self, data=1, **kwargs): self.line.remove() norm = self.norm - if norm == 'log': + if norm == "log": plot = self.ax.semilogy - elif (isinstance(norm, mpl.colors.Normalize) or - (inspect.isclass(norm) and issubclass(norm, mpl.colors.Normalize)) - ): - raise ValueError("Matplotlib Normalize instance or subclass can " - "be used for Signal2D only.") + elif isinstance(norm, mpl.colors.Normalize) or ( + inspect.isclass(norm) and issubclass(norm, mpl.colors.Normalize) + ): + raise ValueError( + "Matplotlib Normalize instance or subclass can " + "be used for Signal2D only." + ) elif norm not in ["auto", "linear"]: - raise ValueError("`norm` paramater should be 'auto', 'linear' or " - "'log' for Signal1D.") + raise ValueError( + "`norm` paramater should be 'auto', 'linear' or " "'log' for Signal1D." + ) else: plot = self.ax.plot - self.line, = plot(self.axis.axis, data, **self.line_properties, - animated=self.ax.figure.canvas.supports_blit) + # If axis is a DataAxis instance, take the axis attribute + axis = getattr(self.axis, "axis", self.axis) + (self.line,) = plot( + axis, + data, + **self.line_properties, + animated=self.ax.figure.canvas.supports_blit, + ) if not self.axes_manager or self.axes_manager.navigation_size == 0: self.plot_indices = False if self.plot_indices is True: if self.text is not None: self.text.remove() - self.text = self.ax.text(*self.text_position, - s=str(self.axes_manager.indices), - transform=self.ax.transAxes, - fontsize=12, - color=self.line.get_color(), - animated=self.ax.figure.canvas.supports_blit) + self.text = self.ax.text( + *self.text_position, + s=str(self.axes_manager.indices), + transform=self.ax.transAxes, + fontsize=12, + color=self.line.get_color(), + animated=self.ax.figure.canvas.supports_blit, + ) self._y_min, self._y_max = self.ax.get_ylim() self.ax.hspy_fig.render_figure() def _get_data(self, real_part=False): if self._plot_imag and not real_part: - ydata = self.data_function(axes_manager=self.axes_manager, - **self.data_function_kwargs).imag + ydata = self.data_function( + axes_manager=self.axes_manager, **self.data_function_kwargs + ).imag else: - ydata = self.data_function(axes_manager=self.axes_manager, - **self.data_function_kwargs).real + ydata = self.data_function( + axes_manager=self.axes_manager, **self.data_function_kwargs + ).real return ydata def _auto_update_line(self, update_ylimits=False, **kwargs): @@ -439,15 +457,13 @@ def _auto_update_line(self, update_ylimits=False, **kwargs): """ if self.auto_update: - if 'render_figure' not in kwargs.keys(): + if "render_figure" not in kwargs.keys(): # if markers are plotted, we don't render the figure now but # once the markers have been updated - kwargs['render_figure'] = ( - len(self.ax.hspy_fig.ax_markers) == 0) + kwargs["render_figure"] = len(self.ax.hspy_fig.ax_markers) == 0 self.update(self, update_ylimits=update_ylimits, **kwargs) - def update(self, force_replot=False, render_figure=True, - update_ylimits=False): + def update(self, force_replot=False, render_figure=True, update_ylimits=False): """Update the current spectrum figure Parameters @@ -466,41 +482,48 @@ def update(self, force_replot=False, render_figure=True, """ if force_replot is True: self.close() - self.plot(data_function_kwargs=self.data_function_kwargs, - norm=self.norm) + self.plot(data_function_kwargs=self.data_function_kwargs, norm=self.norm) self._y_min, self._y_max = self.ax.get_ylim() ydata = self._get_data() - old_xaxis = self.line.get_xdata() - if len(old_xaxis) != self.axis.size or \ - np.any(np.not_equal(old_xaxis, self.axis.axis)): - self.line.set_data(self.axis.axis, ydata) + + # If axis is a DataAxis instance, take the axis attribute + axis = getattr(self.axis, "axis", self.axis) + if not np.array_equiv(self.line.get_xdata(), axis): + self.line.set_data(axis, ydata) else: self.line.set_ydata(ydata) - if 'x' in self.autoscale: - self.ax.set_xlim(self.axis.axis[0], self.axis.axis[-1]) + # Don't change xlim if axis has 0 length (unnecessary) + if "x" in self.autoscale and len(axis) > 0: + x_min, x_max = axis[0], axis[-1] + if x_min == x_max: + # To avoid matplotlib UserWarning when calling `set_ylim` + x_min, x_max = (x_min - 0.1, x_min + 0.1) + self.ax.set_xlim(x_min, x_max) - if 'v' in self.autoscale: + # Don't change ymin if data has 0 length (unnecessary) + if "v" in self.autoscale and len(ydata) > 0: self.ax.relim() - y1, y2 = np.searchsorted(self.axis.axis, - self.ax.get_xbound()) - y2 += 2 - y1, y2 = np.clip((y1, y2), 0, len(ydata - 1)) - clipped_ydata = ydata[y1:y2] + # Based on the current zoom of the x axis, find the corresponding + # y range of data and calculate the y_min, y_max accordingly + i1, i2 = np.searchsorted(axis, self.ax.get_xbound()) + # Make interval wider on both side and clip to allowed range + i1, i2 = np.clip((i1 - 1, i2 + 1), 0, len(ydata - 1)) + ydata = ydata[i1:i2] + with ignore_warning(category=RuntimeWarning): # In case of "All-NaN slices" - y_max, y_min = (np.nanmax(clipped_ydata), - np.nanmin(clipped_ydata)) + y_max, y_min = np.nanmax(ydata), np.nanmin(ydata) if self._plot_imag: # Add real plot - yreal = self._get_data(real_part=True) - clipped_yreal = yreal[y1:y2] + yreal = self._get_data(real_part=True)[i1:i2] with ignore_warning(category=RuntimeWarning): # In case of "All-NaN slices" - y_min = min(y_min, np.nanmin(clipped_yreal)) - y_max = max(y_max, np.nanmin(clipped_yreal)) + y_min = min(y_min, np.nanmin(yreal)) + y_max = max(y_max, np.nanmin(yreal)) + if y_min == y_max: # To avoid matplotlib UserWarning when calling `set_ylim` y_min, y_max = y_min - 0.1, y_max + 0.1 @@ -525,11 +548,11 @@ def update(self, force_replot=False, render_figure=True, self.ax.hspy_fig.render_figure() def close(self): - _logger.debug('Closing `Signal1DLine`.') + _logger.debug("Closing `Signal1DLine`.") if self.line in self.ax.lines: - self.ax.lines.remove(self.line) + self.line.remove() if self.text and self.text in self.ax.texts: - self.ax.texts.remove(self.text) + self.text.remove() if self.sf_lines and self in self.sf_lines: self.sf_lines.remove(self) self.events.closed.trigger(obj=self) @@ -539,11 +562,10 @@ def close(self): self.ax.figure.canvas.draw_idle() except BaseException: pass - _logger.debug('`Signal1DLine` closed.') + _logger.debug("`Signal1DLine` closed.") -def _plot_component(factors, idx, ax=None, cal_axis=None, - comp_label='PC'): +def _plot_component(factors, idx, ax=None, cal_axis=None, comp_label="PC"): if ax is None: ax = plt.gca() if cal_axis is not None: @@ -551,14 +573,21 @@ def _plot_component(factors, idx, ax=None, cal_axis=None, plt.xlabel(cal_axis.units) else: x = np.arange(factors.shape[0]) - plt.xlabel('Channel index') - ax.plot(x, factors[:, idx], label='%s %i' % (comp_label, idx)) + plt.xlabel("Channel index") + ax.plot(x, factors[:, idx], label="%s %i" % (comp_label, idx)) return ax -def _plot_loading(loadings, idx, axes_manager, ax=None, - comp_label='PC', no_nans=True, calibrate=True, - cmap=plt.cm.gray): +def _plot_loading( + loadings, + idx, + axes_manager, + ax=None, + comp_label="PC", + no_nans=True, + calibrate=True, + cmap=plt.cm.gray, +): if ax is None: ax = plt.gca() if no_nans: @@ -568,12 +597,18 @@ def _plot_loading(loadings, idx, axes_manager, ax=None, # get calibration from a passed axes_manager shape = axes_manager._navigation_shape_in_array if calibrate: - extent = (axes_manager._axes[0].low_value, - axes_manager._axes[0].high_value, - axes_manager._axes[1].high_value, - axes_manager._axes[1].low_value) - im = ax.imshow(loadings[idx].reshape(shape), cmap=cmap, extent=extent, - interpolation='nearest') + extent = ( + axes_manager._axes[0].low_value, + axes_manager._axes[0].high_value, + axes_manager._axes[1].high_value, + axes_manager._axes[1].low_value, + ) + im = ax.imshow( + loadings[idx].reshape(shape), + cmap=cmap, + extent=extent, + interpolation="nearest", + ) div = make_axes_locatable(ax) cax = div.append_axes("right", size="5%", pad=0.05) plt.colorbar(im, cax=cax) @@ -584,4 +619,4 @@ def _plot_loading(loadings, idx, axes_manager, ax=None, x = np.arange(axes_manager._axes[0].size) ax.step(x, loadings[idx]) else: - raise ValueError('View not supported') + raise ValueError("View not supported") diff --git a/hyperspy/drawing/tiles.py b/hyperspy/drawing/tiles.py index f75d5f82b4..43a407a169 100644 --- a/hyperspy/drawing/tiles.py +++ b/hyperspy/drawing/tiles.py @@ -1,38 +1,37 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from __future__ import division -import numpy as np import matplotlib.pyplot as plt +import numpy as np from hyperspy.drawing.figure import BlittedFigure class HistogramTilePlot(BlittedFigure): - def __init__(self): self.figure = None - self.title = '' + self.title = "" self.ax = None - def create_axis(self, ncols=1, nrows=1, number=1, title=''): + def create_axis(self, ncols=1, nrows=1, number=1, title=""): ax = self.figure.add_subplot(ncols, nrows, number) ax.set_title(title) ax.hspy_fig = self @@ -61,7 +60,7 @@ def update(self, db, **kwargs): j += 1 mask = hist > 0 if np.any(mask): - title = c_n + ' ' + p_n + title = c_n + " " + p_n ax = self.create_axis(ncomps, ncols, istart + j, title) self.ax = ax # remove previous @@ -69,15 +68,14 @@ def update(self, db, **kwargs): ax.patches[0].remove() # set new; only draw non-zero height bars ax.bar( - bin_edges[ - :-1][mask], + bin_edges[:-1][mask], hist[mask], np.diff(bin_edges)[mask], # animated=True, - **kwargs) + **kwargs, + ) width = bin_edges[-1] - bin_edges[0] - ax.set_xlim( - bin_edges[0] - width * 0.1, bin_edges[-1] + width * 0.1) + ax.set_xlim(bin_edges[0] - width * 0.1, bin_edges[-1] + width * 0.1) ax.set_ylim(0, np.max(hist) * 1.1) # ax.set_title(c_n + ' ' + p_n) self.figure.canvas.draw_idle() diff --git a/hyperspy/drawing/utils.py b/hyperspy/drawing/utils.py index 78cca300f2..7f9f3e224c 100755 --- a/hyperspy/drawing/utils.py +++ b/hyperspy/drawing/utils.py @@ -1,39 +1,45 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import copy import itertools -from distutils.version import LooseVersion +import logging import textwrap -import traits.api as t -import matplotlib.pyplot as plt -import matplotlib as mpl -from mpl_toolkits.axes_grid1 import make_axes_locatable -from matplotlib.backend_bases import key_press_handler import warnings -import numpy as np -import logging from functools import partial -from matplotlib.colors import LinearSegmentedColormap -from matplotlib.colors import BASE_COLORS, to_rgba -from hyperspy.defaults_parser import preferences +import dask.array as da +import matplotlib as mpl +import matplotlib.colors as mcolors +import matplotlib.patches as patches +import matplotlib.pyplot as plt +import numpy as np +import traits.api as t +from matplotlib.backend_bases import key_press_handler +from mpl_toolkits.axes_grid1 import make_axes_locatable +from packaging.version import Version +from rsciio.utils import rgb_tools +import hyperspy +import hyperspy.api as hs +from hyperspy.defaults_parser import preferences +from hyperspy.docstrings.signal import HISTOGRAM_BIN_ARGS, HISTOGRAM_RANGE_ARGS +from hyperspy.misc.utils import isiterable, to_numpy _logger = logging.getLogger(__name__) @@ -43,20 +49,20 @@ def contrast_stretching(data, vmin=None, vmax=None): Parameters ---------- - data: numpy array - vmin, vmax: scalar, str, None + data : numpy array + vmin, vmax : scalar, str, None If str, formatted as 'xth', use this value to calculate the percentage of pixels that are left out of the lower and upper bounds. For example, for a vmin of '1th', 1% of the lowest will be ignored to estimate the minimum value. Similarly, for a vmax value of '1th', 1% of the highest value will be ignored in the estimation of the maximum - value. See :py:func:`numpy.percentile` for more explanation. + value. See :func:`numpy.percentile` for more explanation. If None, use the percentiles value set in the preferences. If float of integer, keep this value as bounds. Returns ------- - vmin, vmax: scalar + vmin, vmax : scalar The low and high bounds. Raises @@ -66,16 +72,20 @@ def contrast_stretching(data, vmin=None, vmax=None): calculation (in case of string values). """ + if np.issubdtype(data.dtype, bool): + # in case of boolean, simply return 0, 1 + return 0, 1 + def _parse_value(value, value_name): if value is None: if value_name == "vmin": - value = f'{preferences.Plot.saturated_pixels / 2}th' + value = "0th" elif value_name == "vmax": - value = f'{100 - preferences.Plot.saturated_pixels / 2}th' + value = "100th" if isinstance(value, str): value = float(value.split("th")[0]) - if not 0 <= value <= 100: - raise ValueError(f"{value_name} must be in the range[0, 100].") + if not 0 <= value <= 100: + raise ValueError(f"{value_name} must be in the range[0, 100].") return value if np.ma.is_masked(data): @@ -85,9 +95,9 @@ def _parse_value(value, value_name): # If vmin, vmax are float or int, we keep the value, if not we calculate # the precentile value if not isinstance(vmin, (float, int)): - vmin = np.nanpercentile(data, _parse_value(vmin, 'vmin')) + vmin = np.nanpercentile(data, _parse_value(vmin, "vmin")) if not isinstance(vmax, (float, int)): - vmax = np.nanpercentile(data, _parse_value(vmax, 'vmax')) + vmax = np.nanpercentile(data, _parse_value(vmax, "vmax")) return vmin, vmax @@ -104,7 +114,8 @@ def _parse_value(value, value_name): "RdYIBu", "RdYIGn", "seismic", - "Spectral", ] + "Spectral", +] # Add reversed colormaps MPL_DIVERGING_COLORMAPS += [cmap + "_r" for cmap in MPL_DIVERGING_COLORMAPS] @@ -114,12 +125,12 @@ def centre_colormap_values(vmin, vmax): Parameters ---------- - vmin, vmax : scalar + vmin, vmax : float The range of data to display. Returns ------- - cvmin, cvmax : scalar + float The values to obtain a centre colormap. """ @@ -128,10 +139,13 @@ def centre_colormap_values(vmin, vmax): return -absmax, absmax -def create_figure(window_title=None, - _on_figure_window_close=None, - disable_xyscale_keys=False, - **kwargs): +def create_figure( + fig=None, + window_title=None, + _on_figure_window_close=None, + disable_xyscale_keys=False, + **kwargs, +): """Create a matplotlib figure. This function adds the possibility to execute another function @@ -153,31 +167,39 @@ def create_figure(window_title=None, fig : plt.figure """ - fig = plt.figure(**kwargs) + if fig is None: + fig = plt.figure(**kwargs) + else: + if isinstance(fig, mpl.figure.SubFigure) and Version(mpl.__version__) < Version( + "3.9.0" + ): + raise ValueError("Subfigure are only supported for matplotlib>=3.9") + if window_title is not None: # remove non-alphanumeric characters to prevent file saving problems # This is a workaround for: # https://github.com/matplotlib/matplotlib/issues/9056 reserved_characters = r'<>"/\|?*' for c in reserved_characters: - window_title = window_title.replace(c, '') - window_title = window_title.replace('\n', ' ') - window_title = window_title.replace(':', ' -') + window_title = window_title.replace(c, "") + window_title = window_title.replace("\n", " ") + window_title = window_title.replace(":", " -") fig.canvas.manager.set_window_title(window_title) - if disable_xyscale_keys and hasattr(fig.canvas, 'toolbar'): + if disable_xyscale_keys and hasattr(fig.canvas, "toolbar"): # hack the `key_press_handler` to disable the `k`, `l`, `L` shortcuts manager = fig.canvas.manager fig.canvas.mpl_disconnect(manager.key_press_handler_id) manager.key_press_handler_id = manager.canvas.mpl_connect( - 'key_press_event', - lambda event: key_press_handler_custom(event, manager.canvas)) + "key_press_event", + lambda event: key_press_handler_custom(event, manager.canvas), + ) if _on_figure_window_close is not None: on_figure_window_close(fig, _on_figure_window_close) return fig def key_press_handler_custom(event, canvas): - if event.key not in ['k', 'l', 'L']: + if event.key not in ["k", "l", "L"]: key_press_handler(event, canvas, canvas.manager.toolbar) @@ -187,17 +209,19 @@ def on_figure_window_close(figure, function): Parameters ---------- - figure : mpl figure instance - function : function + figure : matplotlib.figure.Figure + The figure to close + function : callable """ + def function_wrapper(evt): function() - figure.canvas.mpl_connect('close_event', function_wrapper) + figure.canvas.mpl_connect("close_event", function_wrapper) -def plot_RGB_map(im_list, normalization='single', dont_plot=False): +def plot_RGB_map(im_list, normalization="single", dont_plot=False): """Plot 2 or 3 maps in RGB. Parameters @@ -213,17 +237,17 @@ def plot_RGB_map(im_list, normalization='single', dont_plot=False): array: RGB matrix """ -# from widgets import cursors + # from widgets import cursors height, width = im_list[0].data.shape[:2] rgb = np.zeros((height, width, 3)) rgb[:, :, 0] = im_list[0].data.squeeze() rgb[:, :, 1] = im_list[1].data.squeeze() if len(im_list) == 3: rgb[:, :, 2] = im_list[2].data.squeeze() - if normalization == 'single': + if normalization == "single": for i in range(len(im_list)): rgb[:, :, i] /= rgb[:, :, i].max() - elif normalization == 'global': + elif normalization == "global": rgb /= rgb.max() rgb = rgb.clip(0, rgb.max()) if not dont_plot: @@ -231,8 +255,8 @@ def plot_RGB_map(im_list, normalization='single', dont_plot=False): ax = figure.add_subplot(111) ax.frameon = False ax.set_axis_off() - ax.imshow(rgb, interpolation='nearest') -# cursors.set_mpl_ax(ax) + ax.imshow(rgb, interpolation="nearest") + # cursors.set_mpl_ax(ax) figure.canvas.draw_idle() else: return rgb @@ -243,7 +267,7 @@ def subplot_parameters(fig): Parameters ---------- - fig : mpl figure + fig : matplotlib.figure.Figure Returns ------- @@ -260,8 +284,9 @@ def subplot_parameters(fig): class ColorCycle: - _color_cycle = [mpl.colors.colorConverter.to_rgba(color) for color - in ('b', 'g', 'r', 'c', 'm', 'y', 'k')] + _color_cycle = [ + mcolors.to_rgba(color) for color in ("b", "g", "r", "c", "m", "y", "k") + ] def __init__(self): self.color_cycle = copy.copy(self._color_cycle) @@ -272,60 +297,59 @@ def __call__(self): return self.color_cycle.pop(0) -def plot_signals(signal_list, sync=True, navigator="auto", - navigator_list=None, **kwargs): +def plot_signals( + signal_list, sync=True, navigator="auto", navigator_list=None, **kwargs +): """Plot several signals at the same time. Parameters ---------- - signal_list : list of BaseSignal instances + signal_list : list of :class:`~.api.signals.BaseSignal` If sync is set to True, the signals must have the same navigation shape, but not necessarily the same signal shape. - sync : True, False, optional + sync : bool, optional If True (default), the signals will share navigation. All the signals must have the same navigation shape for this to work, but not necessarily the same signal shape. - navigator : 'auto', None, 'spectrum', 'slider', BaseSignal, optional - Default 'auto'. See signal.plot docstring for full description. - navigator_list : None, list of navigator arguments, optional + navigator : None, :class:`~.api.signals.BaseSignal` or str + {``'auto'`` | ``'spectrum'`` | ``'slider'`` }, default ``"auto"`` + See signal.plot docstring for full description. + navigator_list : None, list of :class:`~.api.signals.BaseSignal` or list of str, default None Set different navigator options for the signals. Must use valid navigator arguments: 'auto', None, 'spectrum', 'slider', or a HyperSpy Signal. The list must have the same size as signal_list. - If None (default), the argument specified in navigator will be used. - **kwargs - Any extra keyword arguments are passed to each signal `plot` method. + If None, the argument specified in navigator will be used. + **kwargs : dict + Any extra keyword arguments are passed to each signal ``plot`` method. - Example - ------- + Examples + -------- - >>> s_cl = hs.load("coreloss.dm3") - >>> s_ll = hs.load("lowloss.dm3") - >>> hs.plot.plot_signals([s_cl, s_ll]) + >>> s1 = hs.signals.Signal1D(np.arange(100).reshape((10, 10))) + >>> s2 = hs.signals.Signal1D(np.arange(100).reshape((10, 10)) * -1) + >>> hs.plot.plot_signals([s1, s2]) Specifying the navigator: - >>> s_cl = hs.load("coreloss.dm3") - >>> s_ll = hs.load("lowloss.dm3") - >>> hs.plot.plot_signals([s_cl, s_ll], navigator="slider") + >>> hs.plot.plot_signals([s1, s2], navigator="slider") # doctest: +SKIP Specifying the navigator for each signal: - >>> s_cl = hs.load("coreloss.dm3") - >>> s_ll = hs.load("lowloss.dm3") - >>> s_edx = hs.load("edx.dm3") - >>> s_adf = hs.load("adf.dm3") + >>> s3 = hs.signals.Signal1D(np.ones((10, 10))) + >>> s_nav = hs.signals.Signal1D(np.ones((10))) >>> hs.plot.plot_signals( - [s_cl, s_ll, s_edx], navigator_list=["slider",None,s_adf]) + ... [s1, s2, s3], navigator_list=["slider", None, s_nav] + ... ) # doctest: +SKIP """ - import hyperspy.signal + from hyperspy.signal import BaseSignal if navigator_list: if not (len(signal_list) == len(navigator_list)): raise ValueError( - "signal_list and navigator_list must" - " have the same size") + "signal_list and navigator_list must" " have the same size" + ) if sync: axes_manager_list = [] @@ -336,7 +360,7 @@ def plot_signals(signal_list, sync=True, navigator="auto", navigator_list = [] if navigator is None: navigator_list.extend([None] * len(signal_list)) - elif isinstance(navigator, hyperspy.signal.BaseSignal): + elif isinstance(navigator, BaseSignal): navigator_list.append(navigator) navigator_list.extend([None] * (len(signal_list) - 1)) elif navigator == "slider": @@ -348,29 +372,29 @@ def plot_signals(signal_list, sync=True, navigator="auto", navigator_list.extend(["auto"] * len(signal_list)) else: raise ValueError( - "navigator must be one of \"spectrum\",\"auto\"," - " \"slider\", None, a Signal instance") + 'navigator must be one of "spectrum","auto",' + ' "slider", None, a Signal instance' + ) # Check to see if the spectra have the same navigational shapes temp_shape_first = axes_manager_list[0].navigation_shape for i, axes_manager in enumerate(axes_manager_list): temp_shape = axes_manager.navigation_shape if not (temp_shape_first == temp_shape): - raise ValueError( - "The spectra do not have the same navigation shape") + raise ValueError("The spectra do not have the same navigation shape") axes_manager_list[i] = axes_manager.deepcopy() if i > 0: - for axis0, axisn in zip(axes_manager_list[0].navigation_axes, - axes_manager_list[i].navigation_axes): + for axis0, axisn in zip( + axes_manager_list[0].navigation_axes, + axes_manager_list[i].navigation_axes, + ): axes_manager_list[i]._axes[axisn.index_in_array] = axis0 del axes_manager - for signal, navigator, axes_manager in zip(signal_list, - navigator_list, - axes_manager_list): - signal.plot(axes_manager=axes_manager, - navigator=navigator, - **kwargs) + for signal, navigator, axes_manager in zip( + signal_list, navigator_list, axes_manager_list + ): + signal.plot(axes_manager=axes_manager, navigator=navigator, **kwargs) # If sync is False else: @@ -378,13 +402,18 @@ def plot_signals(signal_list, sync=True, navigator="auto", navigator_list = [] navigator_list.extend([navigator] * len(signal_list)) for signal, navigator in zip(signal_list, navigator_list): - signal.plot(navigator=navigator, - **kwargs) + signal.plot(navigator=navigator, **kwargs) -def _make_heatmap_subplot(spectra, **plot_kwargs): +def _make_heatmap_subplot(spectra, normalise, **plot_kwargs): from hyperspy._signals.signal2d import Signal2D + im = Signal2D(spectra.data, axes=spectra.axes_manager._get_axes_dicts()) + if normalise: + im.data = ( + (im.data.T - im.data.min(-1)) / (im.data.max(-1) - im.data.min(-1)) + ).T + im.metadata.General.title = spectra.metadata.General.title im.plot(**plot_kwargs) return im._plot.signal_plot.ax @@ -406,50 +435,60 @@ def set_xaxis_lims(mpl_ax, hs_axis): mpl_ax.set_xlim(x_axis_lower_lim, x_axis_upper_lim) -def _make_overlap_plot(spectra, ax, color="blue", line_style='-'): - if isinstance(color, str): - color = [color] * len(spectra) - if isinstance(line_style, str): - line_style = [line_style] * len(spectra) - for spectrum_index, (spectrum, color, line_style) in enumerate( - zip(spectra, color, line_style)): +def _make_overlap_plot(spectra, ax, color, linestyle, normalise, **kwargs): + for spectrum_index, (spectrum, color, linestyle) in enumerate( + zip(spectra, color, linestyle) + ): x_axis = spectrum.axes_manager.signal_axes[0] spectrum = _transpose_if_required(spectrum, 1) - ax.plot(x_axis.axis, spectrum.data, color=color, ls=line_style) + ax.plot( + x_axis.axis, + _parse_array(spectrum, normalise), + color=color, + ls=linestyle, + **kwargs, + ) set_xaxis_lims(ax, x_axis) _set_spectrum_xlabel(spectra, ax) - ax.set_ylabel('Intensity') ax.autoscale(tight=True) def _make_cascade_subplot( - spectra, ax, color="blue", line_style='-', padding=1): + spectra, ax, color, linestyle, normalise, padding=1, **kwargs +): max_value = 0 - for spectrum in spectra: - spectrum_yrange = (np.nanmax(spectrum.data) - - np.nanmin(spectrum.data)) + factors = [1] * len(spectra) + for i, spectrum in enumerate(spectra): + spectrum_yrange = np.nanmax(spectrum.data) - np.nanmin(spectrum.data) if spectrum_yrange > max_value: max_value = spectrum_yrange - if isinstance(color, str): - color = [color] * len(spectra) - if isinstance(line_style, str): - line_style = [line_style] * len(spectra) - for spectrum_index, (spectrum, color, line_style) in enumerate( - zip(spectra, color, line_style)): + if normalise: + factors[i] = spectrum.data.max() - spectrum.data.min() + if normalise: + # when using normalise, we don't use need `max_value` + max_value = 1 + for i, (spectrum, color, linestyle, factor) in enumerate( + zip(spectra, color, linestyle, factors) + ): x_axis = spectrum.axes_manager.signal_axes[0] - spectrum = _transpose_if_required(spectrum, 1) - data_to_plot = ((spectrum.data - spectrum.data.min()) / - float(max_value) + spectrum_index * padding) - ax.plot(x_axis.axis, data_to_plot, color=color, ls=line_style) + data = _parse_array(_transpose_if_required(spectrum, 1)) + data_to_plot = (data - data.min()) / float(max_value) / factor + i * padding + ax.plot(x_axis.axis, data_to_plot, color=color, ls=linestyle, **kwargs) set_xaxis_lims(ax, x_axis) _set_spectrum_xlabel(spectra, ax) ax.set_yticks([]) ax.autoscale(tight=True) -def _plot_spectrum(spectrum, ax, color="blue", line_style='-'): +def _plot_spectrum(spectrum, ax, normalise, color="blue", linestyle="-", **kwargs): x_axis = spectrum.axes_manager.signal_axes[0] - ax.plot(x_axis.axis, spectrum.data, color=color, ls=line_style) + ax.plot( + x_axis.axis, + _parse_array(spectrum, normalise), + color=color, + ls=linestyle, + **kwargs, + ) set_xaxis_lims(ax, x_axis) @@ -462,60 +501,74 @@ def _set_spectrum_xlabel(spectrum, ax): def _transpose_if_required(signal, expected_dimension): # EDS profiles or maps have signal dimension = 0 and navigation dimension # 1 or 2. For convenience, transpose the signal if possible - if (signal.axes_manager.signal_dimension == 0 and - signal.axes_manager.navigation_dimension == expected_dimension): + if ( + signal.axes_manager.signal_dimension == 0 + and signal.axes_manager.navigation_dimension == expected_dimension + ): return signal.T else: return signal -def plot_images(images, - cmap=None, - no_nans=False, - per_row=3, - label='auto', - labelwrap=30, - suptitle=None, - suptitle_fontsize=18, - colorbar='multi', - centre_colormap='auto', - scalebar=None, - scalebar_color='white', - axes_decor='all', - padding=None, - tight_layout=False, - aspect='auto', - min_asp=0.1, - namefrac_thresh=0.4, - fig=None, - vmin=None, - vmax=None, - overlay=False, - colors='auto', - alphas=1.0, - legend_picking=True, - legend_loc='upper right', - pixel_size_factor=1, - **kwargs): +def _parse_array(signal, normalise=False): + """Convenience function to parse array from a signal.""" + data = signal.data + if isinstance(data, da.Array): + data = data.compute() + if normalise: + data = (data - data.min()) / (data.max() - data.min()) + return to_numpy(data) + + +def plot_images( + images, + cmap=None, + no_nans=False, + per_row=3, + label="auto", + labelwrap=30, + suptitle=None, + suptitle_fontsize=18, + colorbar="default", + centre_colormap="auto", + scalebar=None, + scalebar_color="white", + axes_decor="all", + padding=None, + tight_layout=False, + aspect="auto", + min_asp=0.1, + namefrac_thresh=0.4, + fig=None, + ax=None, + vmin=None, + vmax=None, + overlay=False, + colors="auto", + alphas=1.0, + legend_picking=True, + legend_loc="upper right", + pixel_size_factor=None, + **kwargs, +): """Plot multiple images either as sub-images or overlayed in one figure. Parameters ---------- - images : list of Signal2D or BaseSignal - `images` should be a list of Signals to plot. For `BaseSignal` with - navigation dimensions 2 and signal dimension 0, the signal will be - tranposed to form a `Signal2D`. + images : list of :class:`~.api.signals.Signal2D` or :class:`~.api.signals.BaseSignal` + `images` should be a list of Signals to plot. For + :class:`~.api.signals.BaseSignal` with navigation dimensions 2 and + signal dimension 0, the signal will be transposed to form a `Signal2D`. Multi-dimensional images will have each plane plotted as a separate - image. - If any of the signal shapes is not suitable, a ValueError will be + image. If any of the signal shapes is not suitable, a ValueError will be raised. - cmap : matplotlib colormap, list, 'mpl_colors', optional + cmap : None, (list of) matplotlib.colors.Colormap or str, default None The colormap used for the images, by default uses the setting ``color map signal`` from the plot preferences. A list of colormaps can also be provided, and the images will cycle through them. Optionally, the value ``'mpl_colors'`` will cause the cmap to loop through the default ``matplotlib`` colors (to match with the default output of the - :py:func:`~.drawing.utils.plot_spectra` method). + :func:`~.api.plot.plot_spectra` method). Note: if using more than one colormap, using the ``'single'`` option for ``colorbar`` is disallowed. no_nans : bool, optional @@ -546,18 +599,20 @@ def plot_images(images, this parameter will override the automatically determined title. suptitle_fontsize : int, optional Font size to use for super title at top of figure. - colorbar : 'multi', None, 'single', optional - Controls the type of colorbars that are plotted. - If None, no colorbar is plotted. - If 'multi' (default), individual colorbars are plotted for each - (non-RGB) image + colorbar : 'default', 'multi', 'single', None, optional + Controls the type of colorbars that are plotted, incompatible with + ``overlay=True``. + If 'default', same as 'multi' when ``overlay=False``, otherwise same + as ``None``. + If 'multi', individual colorbars are plotted for each (non-RGB) image. If 'single', all (non-RGB) images are plotted on the same scale, - and one colorbar is shown for all + and one colorbar is shown for all. + If None, no colorbar is plotted. centre_colormap : 'auto', True, False, optional If True, the centre of the color scheme is set to zero. This is particularly useful when using diverging color schemes. If 'auto' (default), diverging color schemes are automatically centred. - scalebar : None, 'all', list of ints, optional + scalebar : None, 'all', list of int, optional If None (or False), no scalebars will be added to the images. If 'all', scalebars will be added to all images. If list of ints, scalebars will be added to each image specified. @@ -575,7 +630,7 @@ def plot_images(images, Otherwise, supply a dictionary with the spacing options as keywords and desired values as values. Values should be supplied as used in - :py:func:`matplotlib.pyplot.subplots_adjust`, + :func:`matplotlib.pyplot.subplots_adjust`, and can be 'left', 'bottom', 'right', 'top', 'wspace' (width) and 'hspace' (height). tight_layout : bool, optional @@ -597,8 +652,14 @@ def plot_images(images, encourage shortening of titles by auto-labeling, while larger values will require more overlap in titles before activing the auto-label code. - fig : mpl figure, optional - If set, the images will be plotted to an existing MPL figure + fig : matplotlib.figure.Figure, default None + If set, the images will be plotted to an existing matplotlib figure. + If the parameter ``ax`` is provided, this parameter will be ignored + and the figure will be obtained from the ``ax`` parameter. + ax : matplotlib.axes.Axes or list of matplotlib.axes.Axes, default None + The matplotlib axes to use to display the images. + When using `overlay=True`, `ax` must be a matplotlib axis. + If None, new matplotlib axes will be created as required. vmin, vmax: scalar, str, None If str, formatted as 'xth', use this value to calculate the percentage of pixels that are left out of the lower and upper bounds. @@ -606,31 +667,33 @@ def plot_images(images, estimate the minimum value. Similarly, for a vmax value of '1th', 1% of the highest value will be ignored in the estimation of the maximum value. It must be in the range [0, 100]. - See :py:func:`numpy.percentile` for more explanation. + See :func:`numpy.percentile` for more explanation. If None, use the percentiles value set in the preferences. If float or integer, keep this value as bounds. Note: vmin is ignored when overlaying images. overlay : bool, optional If True, overlays the images with different colors rather than plotting each image as a subplot. - colors : 'auto', list of char, list of hex str, optional + colors : 'auto', list of str, optional If list, it must contains colors acceptable to matplotlib [1]_. If ``'auto'``, colors will be taken from matplotlib.colors.BASE_COLORS. - alphas : float or list of floats, optional + alphas : float or list of float, optional Float value or a list of floats corresponding to the alpha value of each color. legend_picking: bool, optional - If True (default), a spectrum can be toggled on and off by clicking on - the legended line. + If True (default), an image can be toggled on and off by clicking on + the legended line. For ``overlay=True`` only. legend_loc : str, int, optional This parameter controls where the legend is placed on the figure - see the :py:func:`matplotlib.pyplot.legend` docstring for valid values - pixel_size_factor : int or float, optional - Default value is 1. Sets the size of the figure when plotting an overlay image. The higher - the number the larger the figure and therefore a greater number of - pixels are used. This value will be ignored if a Figure is provided. + see the :func:`matplotlib.pyplot.legend` docstring for valid values + pixel_size_factor : None, int or float, optional + If ``None`` (default), the size of the figure is taken from the + matplotlib ``rcParams``. Otherwise sets the size of the figure when + plotting an overlay image. The higher the number the larger the figure + and therefore a greater number of pixels are used. This value will be + ignored if a Figure is provided. **kwargs, optional - Additional keyword arguments passed to :py:func:`matplotlib.pyplot.imshow`. + Additional keyword arguments passed to :func:`matplotlib.pyplot.imshow`. Returns ------- @@ -658,23 +721,27 @@ def plot_images(images, or try adjusting `label`, `labelwrap`, or `per_row`. """ + def __check_single_colorbar(cbar): - if cbar == 'single': - raise ValueError('Cannot use a single colorbar with multiple ' - 'colormaps. Please check for compatible ' - 'arguments.') + if cbar == "single": + raise ValueError( + "Cannot use a single colorbar with multiple " + "colormaps. Please check for compatible " + "arguments." + ) from hyperspy.drawing.widgets import ScaleBar - from hyperspy.misc import rgb_tools from hyperspy.signal import BaseSignal # Check that we have a hyperspy signal im = [images] if not isinstance(images, (list, tuple)) else images for image in im: if not isinstance(image, BaseSignal): - raise ValueError("`images` must be a list of image signals or a " - "multi-dimensional signal." - " " + repr(type(images)) + " was given.") + raise ValueError( + "`images` must be a list of image signals or a " + "multi-dimensional signal. " + f"{repr(type(images))} was given." + ) # For list of EDS maps, transpose the BaseSignal if isinstance(images, (list, tuple)): @@ -682,49 +749,59 @@ def __check_single_colorbar(cbar): # If input is >= 1D signal (e.g. for multi-dimensional plotting), # copy it and put it in a list so labeling works out as (x,y) when plotting - if isinstance(images, - BaseSignal) and images.axes_manager.navigation_dimension > 0: - images = [images._deepcopy_with_new_data(images.data)] + if isinstance(images, BaseSignal): + images = [im_ for im_ in images] n = 0 for i, sig in enumerate(images): if sig.axes_manager.signal_dimension != 2: - raise ValueError("This method only plots signals that are images. " - "The signal dimension must be equal to 2. " - "The signal at position " + repr(i) + - " was " + repr(sig) + ".") + raise ValueError( + "This method only plots signals that are images. " + "The signal dimension must be equal to 2. " + "The signal at position " + repr(i) + " was " + repr(sig) + "." + ) # increment n by the navigation size, or by 1 if the navigation size is # <= 0 - n += (sig.axes_manager.navigation_size - if sig.axes_manager.navigation_size > 0 - else 1) + n += ( + sig.axes_manager.navigation_size + if sig.axes_manager.navigation_size > 0 + else 1 + ) + + # Check compatibility of colorbar and overlay arguments + if overlay and colorbar != "default": + _logger.info( + f"`colorbar='{colorbar}'` is incompatible with " + "`overlay=True`. Colorbar is disable." + ) + colorbar = None + # Setting the default value + elif colorbar == "default": + colorbar = "multi" # If no cmap given, get default colormap from pyplot: if cmap is None: cmap = [preferences.Plot.cmap_signal] - elif cmap == 'mpl_colors': - cycle = mpl.rcParams['axes.prop_cycle'] - for n_color, c in enumerate(cycle): - name = f'mpl{n_color}' - if name not in plt.colormaps(): - make_cmap(colors=['#000000', c['color']], name=name) - cmap = [f'mpl{i}' for i in range(len(cycle))] + elif cmap == "mpl_colors": + color = mpl.rcParams["axes.prop_cycle"].by_key()["color"] + cmap = _make_cmaps(color) __check_single_colorbar(colorbar) # cmap is list, tuple, or something else iterable (but not string): - elif hasattr(cmap, '__iter__') and not isinstance(cmap, str): + elif hasattr(cmap, "__iter__") and not isinstance(cmap, str): try: cmap = [c.name for c in cmap] # convert colormap to string except AttributeError: - cmap = [c for c in cmap] # c should be string if not colormap + cmap = [c for c in cmap] # c should be string if not colormap __check_single_colorbar(colorbar) elif isinstance(cmap, mpl.colors.Colormap): - cmap = [cmap.name] # convert single colormap to list with string + cmap = [cmap.name] # convert single colormap to list with string elif isinstance(cmap, str): cmap = [cmap] # cmap is single string, so make it a list else: # Didn't understand cmap input, so raise error - raise ValueError('The provided cmap value was not understood. Please ' - 'check input values.') + raise ValueError( + "The provided cmap value was not understood. Please " "check input values." + ) # If any of the cmaps given are diverging, and auto-centering, set the # appropriate flag: @@ -754,7 +831,7 @@ def __check_single_colorbar(cbar): if label is None: pass - elif label == 'auto': + elif label == "auto": # Use some heuristics to try to get base string of similar titles label_list = [x.metadata.General.title for x in images] @@ -781,18 +858,14 @@ def __check_single_colorbar(cbar): all_match = True else: div_num = int(min(np.sum(res, 1))) - basename = label_list[0][:div_num - 1] + basename = label_list[0][: div_num - 1] all_match = False # trim off any '(' or ' ' characters at end of basename if div_num > 1: - while True: - if basename[len(basename) - 1] == '(': - basename = basename[:-1] - elif basename[len(basename) - 1] == ' ': - basename = basename[:-1] - else: - break + basename = basename.strip() + if len(basename) > 1 and basename[len(basename) - 1] == "(": + basename = basename[:-1] # namefrac is ratio of length of basename to the image name # if it is high (e.g. over 0.5), we can assume that all images @@ -814,19 +887,18 @@ def __check_single_colorbar(cbar): else: # there was not much overlap, so default back to 'titles' mode shared_titles = False - label = 'titles' + label = "titles" div_num = 0 - elif label == 'titles': + elif label == "titles": # Set label_list to each image's pre-defined title label_list = [x.metadata.General.title for x in images] elif isinstance(label, str): # Set label_list to an indexed list, based off of label - label_list = [label + " " + repr(num) for num in range(n)] + label_list = [f"{label} {num}" for num in range(n)] - elif isinstance(label, list) and all( - isinstance(x, str) for x in label): + elif isinstance(label, list) and all(isinstance(x, str) for x in label): label_list = label user_labels = True # If list of labels is longer than the number of images, just use the @@ -840,24 +912,62 @@ def __check_single_colorbar(cbar): else: raise ValueError("Did not understand input of labels.") - # Start of non-overlay? - # Determine appropriate number of images per row - rows = int(np.ceil(n / float(per_row))) - if n < per_row: - per_row = n + # Check if we need to add a scalebar for some of the images + if isinstance(scalebar, (list, tuple)) and all( + isinstance(x, int) for x in scalebar + ): + scalelist = True + else: + scalelist = False + if scalebar not in [None, False, "all"] and scalelist is False: + raise ValueError( + "Did not understand scalebar input. Must be None, " + "'all', or list of ints." + ) + + # Determine appropriate number of images per row + if overlay: + # only a single image + per_row = rows = 1 + else: + rows = int(np.ceil(n / float(per_row))) + if n < per_row: + per_row = n + + # Get the figure from ax is provided + if ax is not None: + if isiterable(ax): + fig = ax[0].get_figure() + else: + fig = ax.get_figure() + # Create figure if none has been provided through fig or ax # Set overall figure size and define figure (if not pre-existing) if fig is None: - k = max(plt.rcParams['figure.figsize']) / max(per_row, rows) - if overlay: - shape = images[0].data.shape - dpi = 100 - f = plt.figure(figsize=[pixel_size_factor*v/dpi for v in shape], - dpi=dpi) + w, h = plt.rcParams["figure.figsize"] + dpi = plt.rcParams["figure.dpi"] + if overlay and axes_decor == "off": + shape = images[0].axes_manager.signal_shape + if pixel_size_factor is None: + # Cap the maximum dimension of figure to + # plt.rcParams['figure.figsize'] + aspect_ratio = shape[0] / shape[1] + if aspect_ratio >= w / h: + if label is not None and w / aspect_ratio < 1.0: + # Needs enough space for the labels + w = 1.0 * aspect_ratio + figsize = (w, w / aspect_ratio) + else: + if scalebar is not None and h * aspect_ratio < 2.0: + # Needs enough width for the scalebar + h = 2.0 / aspect_ratio + figsize = (h * aspect_ratio, h) + else: + figsize = [pixel_size_factor * v / dpi for v in shape] else: - f = plt.figure(figsize=(tuple(k * i for i in (per_row, rows)))) - else: - f = fig + k = max(w, h) / max(per_row, rows) + figsize = [k * i for i in (per_row, rows)] + fig = plt.figure(figsize=figsize, dpi=dpi) # Initialize list to hold subplot axes axes_list = [] @@ -877,34 +987,32 @@ def __check_single_colorbar(cbar): colorbar = None warnings.warn("Sorry, colorbar is not implemented for RGB images.") - # Check if we need to add a scalebar for some of the images - if isinstance(scalebar, list) and all(isinstance(x, int) - for x in scalebar): - scalelist = True - else: - scalelist = False - def check_list_length(arg, arg_name): if isinstance(arg, (list, tuple)): if len(arg) != n: - _logger.warning(f'The provided {arg_name} values are ignored ' - 'because the length of the list does not ' - 'match the number of images') + _logger.warning( + f"The provided {arg_name} values are ignored " + "because the length of the list does not " + "match the number of images" + ) arg = [None] * n return arg # Find global min and max values of all the non-rgb images for use with # 'single' scalebar, otherwise define this value later. - if colorbar == 'single': + if colorbar == "single": # check that vmin and vmax are not list if any([isinstance(v, (tuple, list)) for v in [vmin, vmax]]): - _logger.warning('The provided vmin or vmax value are ignored ' - 'because it needs to be a scalar or a str ' - 'to be compatible with a single colorbar. ' - 'The default values are used instead.') + _logger.warning( + "The provided vmin or vmax value are ignored " + "because it needs to be a scalar or a str " + "to be compatible with a single colorbar. " + "The default values are used instead." + ) vmin, vmax = None, None vmin_max = np.array( - [contrast_stretching(i.data, vmin, vmax) for i in non_rgb]) + [contrast_stretching(_parse_array(i), vmin, vmax) for i in non_rgb] + ) _vmin, _vmax = vmin_max[:, 0].min(), vmin_max[:, 1].max() if next(centre_colormaps): _vmin, _vmax = centre_colormap_values(_vmin, _vmax) @@ -920,111 +1028,140 @@ def check_list_length(arg, arg_name): replot_ims = [] def transparent_single_color_cmap(color): - """ Return a single color matplotlib cmap with the transparency increasing + """Return a single color matplotlib cmap with the transparency increasing linearly from 0 to 1.""" - return LinearSegmentedColormap.from_list("", [to_rgba(color, 0), to_rgba(color, 1)]) + return mcolors.LinearSegmentedColormap.from_list( + "", [mcolors.to_rgba(color, 0), mcolors.to_rgba(color, 1)] + ) - #Below is for overlayed images + # Below is for overlayed images if overlay: - - #Check if images all have same scale and therefore can be overlayed. + # Check if images all have same scale and therefore can be overlayed. for im in images: - if (im.axes_manager[0].scale != - images[0].axes_manager[0].scale): - raise ValueError("Images are not the same scale and so should" - "not be overlayed.") + if im.axes_manager[0].scale != images[0].axes_manager[0].scale: + raise ValueError( + "Images are not the same scale and so should" "not be overlayed." + ) - _logger.warning('vmin is ignored when overlaying images.') + if vmin is not None: + _logger.warning("`vmin` is ignored when overlaying images.") import matplotlib.patches as mpatches - if not suptitle and axes_decor == 'off': - ax = f.add_axes([0, 0, 1, 1]) - elif not suptitle: - ax = f.add_axes([0.1, 0.1, 1, 1]) - else: - ax = f.add_axes([0.1, 0.1, 0.9, 0.8]) + + factor = plt.rcParams["font.size"] / 100 + if ax is None: + if not suptitle and axes_decor == "off": + ax = fig.add_axes([0, 0, 1, 1]) + else: + ax = fig.add_subplot() + elif isiterable(ax): + raise ValueError( + "When using `overlay=True`, `ax` must be a matplotlib axis." + ) + patches = [] - #If no colors are selected use BASE_COLORS - if colors == 'auto': + # If no colors are selected use BASE_COLORS + if colors == "auto": colors = [] for i in range(len(images)): - colors.append(list(BASE_COLORS)[i]) + colors.append(list(mcolors.BASE_COLORS)[i]) - #If no alphas are selected use 1.0 + # If no alphas are selected use 1.0 if isinstance(alphas, float): alphas_list = [] for i in range(len(images)): alphas_list.append(alphas) - alphas=alphas_list + alphas = alphas_list - ax.imshow(np.zeros_like(images[0].data), cmap='gray') + ax.imshow(np.zeros_like(images[0].data), cmap="gray") - #Loop through each image + # Loop through each image for i, im in enumerate(images): + # Set vmin and vmax + centre = next(centre_colormaps) # get next value for centreing + data = _parse_array(im) - #Set vmin and vmax - centre = next(centre_colormaps) # get next value for centreing - data = im.data - _vmin = data.min() + _vmin = vmax[idx] if isinstance(vmin, (tuple, list)) else vmin _vmax = vmax[idx] if isinstance(vmax, (tuple, list)) else vmax _vmin, _vmax = contrast_stretching(data, _vmin, _vmax) + if centre: - _logger.warning('Centering is ignored when overlaying images.') + _logger.warning("Centering is ignored when overlaying images.") - ax.imshow(im.data, vmin=_vmin, vmax=_vmax, - cmap=transparent_single_color_cmap(colors[i]), - alpha=alphas[i], **kwargs) + ax.imshow( + data, + vmin=_vmin, + vmax=_vmax, + cmap=transparent_single_color_cmap(colors[i]), + alpha=alphas[i], + **kwargs, + ) if label is not None: if shared_titles: - legend_label = label_list[i][div_num - 1:] + legend_label = label_list[i][div_num - 1 :] else: legend_label = label_list[i] - patches.append(mpatches.Patch(color=colors[i], - label=legend_label)) + patches.append(mpatches.Patch(color=colors[i], label=legend_label)) if label is not None: - plt.legend(handles=patches, loc=legend_loc) - if legend_picking is True: - animate_legend(fig=f, ax=ax, plot_type='images') + ax.legend(handles=patches, loc=legend_loc) + if legend_picking: + animate_legend(fig=fig, ax=ax, plot_type="images") set_axes_decor(ax, axes_decor) - if scalebar=='all': + if scalebar == "all": axes = im.axes_manager.signal_axes ax.scalebar = ScaleBar( - ax=ax, - units=im.axes_manager[0].units, - color=scalebar_color, - ) + ax=ax, + units=im.axes_manager[0].units, + color=scalebar_color, + ) axes_list.append(ax) - #Below is for non-overlayed images - if not overlay: + # Below is for non-overlayed images + else: + if ax is not None: + if not isiterable(ax): + ax = (ax,) + # Loop through each image, adding subplot for each one for i, ims in enumerate(images): # Get handles for the signal axes and axes_manager axes_manager = ims.axes_manager if axes_manager.navigation_dimension > 0: ims = ims._deepcopy_with_new_data(ims.data) + # Use flyback iterpath to get "natural", + # i.e. order the user would except + ims.axes_manager.iterpath = "flyback" for j, im in enumerate(ims): - ax = f.add_subplot(rows, per_row, idx + 1) - axes_list.append(ax) - data = im.data - centre = next(centre_colormaps) # get next value for centreing + # Get `ax_`: the current matplotlib axis used for plotting + if ax is None: + # Create the ax + ax_ = fig.add_subplot(rows, per_row, idx + 1) + else: + # Get the ax when ax is provided + try: + ax_ = ax[idx] + except IndexError: + # in case ax is not long enough + raise ValueError( + "The length of `ax` must match the number of images to plot." + ) + # `axes_list` will be returned + axes_list.append(ax_) + centre = next(centre_colormaps) # get next value for centring + data = _parse_array(im) # Enable RGB plotting if rgb_tools.is_rgbx(data): data = rgb_tools.rgbx2regular_array(data, plot_friendly=True) _vmin, _vmax = None, None - elif colorbar != 'single': - _vmin = vmin[idx] if isinstance(vmin, (tuple, list)) else vmin - _vmax = vmax[idx] if isinstance(vmax, (tuple, list)) else vmax - _vmin, _vmax = contrast_stretching(data, _vmin, _vmax) - if centre: - _vmin, _vmax = centre_colormap_values(_vmin, _vmax) + elif colorbar != "single": + _vmin, _vmax = _parse_vmin_vmax(data, vmin, vmax, idx, centre) # Remove NaNs (if requested) if no_nans: @@ -1038,64 +1175,83 @@ def transparent_single_color_cmap(color): xaxis = axes[0] yaxis = axes[1] - extent = ( - xaxis.low_value, - xaxis.high_value, - yaxis.high_value, - yaxis.low_value, - ) + # Keep extent consistent with `Signal.plot` + if xaxis.is_uniform and yaxis.is_uniform: + xaxis_half_px = xaxis.scale / 2.0 + yaxis_half_px = yaxis.scale / 2.0 + else: + xaxis_half_px = 0 + yaxis_half_px = 0 + extent = [ + xaxis.axis[0] - xaxis_half_px, + xaxis.axis[-1] + xaxis_half_px, + yaxis.axis[-1] + yaxis_half_px, + yaxis.axis[0] - yaxis_half_px, + ] if not isinstance(aspect, (int, float)) and aspect not in [ - 'auto', 'square', 'equal']: - _logger.warning("Did not understand aspect ratio input. " - "Using 'auto' as default.") - aspect = 'auto' + "auto", + "square", + "equal", + ]: + _logger.warning( + "Did not understand aspect ratio input. " + "Using 'auto' as default." + ) + aspect = "auto" - if aspect == 'auto': + if not xaxis.is_uniform or not yaxis.is_uniform: + asp = None + elif aspect == "auto": if float(yaxis.size) / xaxis.size < min_asp: factor = min_asp * float(xaxis.size) / yaxis.size - elif float(yaxis.size) / xaxis.size > min_asp ** -1: - factor = min_asp ** -1 * float(xaxis.size) / yaxis.size + elif float(yaxis.size) / xaxis.size > min_asp**-1: + factor = min_asp**-1 * float(xaxis.size) / yaxis.size else: factor = 1 - asp = np.abs(factor * float(xaxis.scale) / yaxis.scale) - elif aspect == 'square': + asp = abs(factor * float(xaxis.scale) / yaxis.scale) + elif aspect == "square": asp = abs(extent[1] - extent[0]) / abs(extent[3] - extent[2]) - elif aspect == 'equal': + elif aspect == "equal": asp = 1 elif isinstance(aspect, (int, float)): asp = aspect - if 'interpolation' not in kwargs.keys(): - kwargs['interpolation'] = 'nearest' + if "interpolation" not in kwargs.keys(): + kwargs["interpolation"] = "nearest" # Plot image data, using _vmin and _vmax to set bounds, # or allowing them to be set automatically if using individual # colorbars - kwargs.update({'cmap':next(cmap), 'extent':extent, 'aspect':asp}) - axes_im = ax.imshow(data, vmin=_vmin, vmax=_vmax, **kwargs) + kwargs.update({"cmap": next(cmap), "extent": extent, "aspect": asp}) + axes_im = ax_.imshow(data, vmin=_vmin, vmax=_vmax, **kwargs) ax_im_list[i] = axes_im # If an axis trait is undefined, shut off : - if (xaxis.units == t.Undefined or yaxis.units == t.Undefined or - xaxis.name == t.Undefined or yaxis.name == t.Undefined): - if axes_decor == 'all': + if ( + xaxis.units == t.Undefined + or yaxis.units == t.Undefined + or xaxis.name == t.Undefined + or yaxis.name == t.Undefined + ): + if axes_decor == "all": _logger.warning( - 'Axes labels were requested, but one ' - 'or both of the ' - 'axes units and/or name are undefined. ' - 'Axes decorations have been set to ' - '\'ticks\' instead.') - axes_decor = 'ticks' + "Axes labels were requested, but one " + "or both of the " + "axes units and/or name are undefined. " + "Axes decorations have been set to " + "'ticks' instead." + ) + axes_decor = "ticks" # If all traits are defined, set labels as appropriate: else: - ax.set_xlabel(axes[0].name + " axis (" + axes[0].units + ")") - ax.set_ylabel(axes[1].name + " axis (" + axes[1].units + ")") + ax_.set_xlabel(axes[0].name + " axis (" + axes[0].units + ")") + ax_.set_ylabel(axes[1].name + " axis (" + axes[1].units + ")") if label: if all_match: - title = '' + title = "" elif shared_titles: - title = label_list[i][div_num - 1:] + title = label_list[i][div_num - 1 :] else: if len(ims) == n: # This is true if we are plotting just 1 @@ -1109,21 +1265,21 @@ def transparent_single_color_cmap(color): if ims.axes_manager.navigation_size > 1 and not user_labels: title += " %s" % str(ims.axes_manager.indices) - ax.set_title(textwrap.fill(title, labelwrap)) + ax_.set_title(textwrap.fill(title, labelwrap)) # Set axes decorations based on user input - set_axes_decor(ax, axes_decor) + set_axes_decor(ax_, axes_decor) # If using independent colorbars, add them - if colorbar == 'multi' and not isrgb[i]: - div = make_axes_locatable(ax) + if colorbar == "multi" and not isrgb[i]: + div = make_axes_locatable(ax_) cax = div.append_axes("right", size="5%", pad=0.05) plt.colorbar(axes_im, cax=cax) # Add scalebars as necessary - if (scalelist and idx in scalebar) or scalebar == 'all': - ax.scalebar = ScaleBar( - ax=ax, + if (scalelist and idx in scalebar) or scalebar == "all": + ax_.scalebar = ScaleBar( + ax=ax_, units=axes[0].units, color=scalebar_color, ) @@ -1134,19 +1290,19 @@ def transparent_single_color_cmap(color): # If using a single colorbar, add it, and do tight_layout, ensuring that # a colorbar is only added based off of non-rgb Images: - if colorbar == 'single': + if colorbar == "single": foundim = None for i in range(len(isrgb)): if (not isrgb[i]) and foundim is None: foundim = i if foundim is not None: - f.subplots_adjust(right=0.8) - cbar_ax = f.add_axes([0.9, 0.1, 0.03, 0.8]) - f.colorbar(ax_im_list[foundim], cax=cbar_ax) + fig.subplots_adjust(right=0.8) + cbar_ax = fig.add_axes([0.9, 0.1, 0.03, 0.8]) + fig.colorbar(ax_im_list[foundim], cax=cbar_ax) if tight_layout: # tight_layout, leaving room for the colorbar - plt.tight_layout(rect=[0, 0, 0.9, 1]) + fig.tight_layout(rect=[0, 0, 0.9, 1]) elif tight_layout: plt.tight_layout() @@ -1155,22 +1311,8 @@ def transparent_single_color_cmap(color): # Set top bounds for shared titles and add suptitle if suptitle: - f.subplots_adjust(top=0.85) - f.suptitle(suptitle, fontsize=suptitle_fontsize) - - # If we want to plot scalebars, loop through the list of axes and add them - if scalebar is None or scalebar is False: - # Do nothing if no scalebars are called for - pass - elif scalebar == 'all': - # scalebars were taken care of in the plotting loop - pass - elif scalelist: - # scalebars were taken care of in the plotting loop - pass - else: - raise ValueError("Did not understand scalebar input. Must be None, " - "'all', or list of ints.") + fig.subplots_adjust(top=0.85) + fig.suptitle(suptitle, fontsize=suptitle_fontsize) # Adjust subplot spacing according to user's specification if padding is not None: @@ -1179,59 +1321,84 @@ def transparent_single_color_cmap(color): # Replot: connect function def on_dblclick(event): # On the event of a double click, replot the selected subplot - if not event.inaxes: + if not event.inaxes or not event.dblclick: return - if not event.dblclick: - return - subplots = [axi for axi in f.axes if isinstance(axi, mpl.axes.Subplot)] - inx = list(subplots).index(event.inaxes) - im = replot_ims[inx] + + idx_ = axes_list.index(event.inaxes) + ax_ = axes_list[idx_] + im_ = replot_ims[idx_] # Use some of the info in the subplot - cm = subplots[inx].images[0].get_cmap() - clim = subplots[inx].images[0].get_clim() + cm = ax_.images[0].get_cmap() + clim = ax_.images[0].get_clim() sbar = False - if (scalelist and inx in scalebar) or scalebar == 'all': + if (scalelist and idx_ in scalebar) or scalebar == "all": sbar = True - im.plot(colorbar=bool(colorbar), - vmin=clim[0], - vmax=clim[1], - no_nans=no_nans, - aspect=asp, - scalebar=sbar, - scalebar_color=scalebar_color, - cmap=cm) - - f.canvas.mpl_connect('button_press_event', on_dblclick) + im_.plot( + colorbar=bool(colorbar), + vmin=clim[0], + vmax=clim[1], + no_nans=no_nans, + aspect=asp, + scalebar=sbar, + scalebar_color=scalebar_color, + cmap=cm, + ) + + fig.canvas.mpl_connect("button_press_event", on_dblclick) + + def update_image(image, ax, image_index): + data = image.data + im = ax.images[0] + im.set_data(data) + _vmin, _vmax = _parse_vmin_vmax(data, vmin, vmax, image_index, centre) + im.set_clim(vmin=_vmin, vmax=_vmax) + ax.get_figure().canvas.draw() + + for i, (image, ax_) in enumerate(zip(images, axes_list)): + f = partial(update_image, image, ax_, i) + image.events.data_changed.connect(f, []) + # disconnect event when closing figure + disconnect = partial(image.events.data_changed.disconnect, f) + on_figure_window_close(ax_.get_figure(), disconnect) return axes_list +def _parse_vmin_vmax(data, vmin, vmax, index, centre): + _vmin = vmin[index] if isinstance(vmin, (tuple, list)) else vmin + _vmax = vmax[index] if isinstance(vmax, (tuple, list)) else vmax + _vmin, _vmax = contrast_stretching(data, _vmin, _vmax) + if centre: + _vmin, _vmax = centre_colormap_values(_vmin, _vmax) + + return _vmin, _vmax + + def set_axes_decor(ax, axes_decor): - if axes_decor == 'off': - ax.axis('off') - elif axes_decor == 'ticks': - ax.set_xlabel('') - ax.set_ylabel('') - elif axes_decor == 'all': + if axes_decor == "off": + ax.axis("off") + elif axes_decor == "ticks": + ax.set_xlabel("") + ax.set_ylabel("") + elif axes_decor == "all": pass elif axes_decor is None: - ax.set_xlabel('') - ax.set_ylabel('') + ax.set_xlabel("") + ax.set_ylabel("") ax.set_xticklabels([]) ax.set_yticklabels([]) -def make_cmap(colors, name='my_colormap', position=None, - bit=False, register=True): +def make_cmap(colors, name="my_colormap", position=None, bit=False, register=True): """ Create a matplotlib colormap with customized colors, optionally registering it with matplotlib for simplified use. Adapted from Chris Slocum's code at: - https://github.com/CSlocumWX/custom_colormap/blob/master/custom_colormaps.py + https://github.com/CSlocumWX/custom_colormap/blob/master/custom_colormaps/custom_colormaps.py and used under the terms of that code's BSD-3 license Parameters @@ -1265,110 +1432,128 @@ def make_cmap(colors, name='my_colormap', position=None, elif position[0] != 0 or position[-1] != 1: raise ValueError("Position must start with 0 and end with 1") - cdict = {'red': [], 'green': [], 'blue': []} + cdict = {"red": [], "green": [], "blue": []} for pos, color in zip(position, colors): if isinstance(color, str): - color = mpl.colors.to_rgb(color) + color = mcolors.to_rgb(color) elif bit: - color = (bit_rgb[color[0]], - bit_rgb[color[1]], - bit_rgb[color[2]]) + color = (bit_rgb[color[0]], bit_rgb[color[1]], bit_rgb[color[2]]) - cdict['red'].append((pos, color[0], color[0])) - cdict['green'].append((pos, color[1], color[1])) - cdict['blue'].append((pos, color[2], color[2])) + cdict["red"].append((pos, color[0], color[0])) + cdict["green"].append((pos, color[1], color[1])) + cdict["blue"].append((pos, color[2], color[2])) - cmap = mpl.colors.LinearSegmentedColormap(name, cdict, 256) + cmap = mcolors.LinearSegmentedColormap(name, cdict, 256) if register: - mpl.cm.register_cmap(name, cmap) + try: + # Introduced in matplotlib 3.5 + mpl.colormaps.register(cmap, name=name) + except AttributeError: + # Deprecated in matplotlib 3.5 + mpl.cm.register_cmap(name, cmap) return cmap def plot_spectra( - spectra, - style='overlap', - color=None, - line_style=None, - padding=1., - legend=None, - legend_picking=True, - legend_loc='upper right', - fig=None, - ax=None, - auto_update=None, - **kwargs): + spectra, + style="overlap", + color=None, + linestyle=None, + drawstyle="default", + padding=1.0, + legend=None, + legend_picking=True, + legend_loc="upper right", + fig=None, + ax=None, + auto_update=None, + normalise=False, + **kwargs, +): """Plot several spectra in the same figure. Parameters ---------- - spectra : list of Signal1D or BaseSignal + spectra : list of :class:`~.api.signals.Signal1D` or :class:`~.api.signals.BaseSignal` Ordered spectra list of signal to plot. If `style` is "cascade" or - "mosaic", the spectra can have different size and axes. For `BaseSignal` - with navigation dimensions 1 and signal dimension 0, the signal will be - tranposed to form a `Signal1D`. - style : 'overlap', 'cascade', 'mosaic', 'heatmap', optional + "mosaic", the spectra can have different size and axes. + For :class:`~.api.signals.BaseSignal` with navigation dimensions 1 + and signal dimension 0, the signal will be transposed to form a + :class:`~.api.signals.Signal1D`. + style : {``'overlap'`` | ``'cascade'`` | ``'mosaic'`` | ``'heatmap'``}, default 'overlap' The style of the plot: 'overlap' (default), 'cascade', 'mosaic', or 'heatmap'. - color : None, matplotlib color, list of colors, optional + color : None or (list of) matplotlib color, default None Sets the color of the lines of the plots (no action on 'heatmap'). For a list, if its length is less than the number of spectra to plot, the colors will be cycled. If `None` (default), use default matplotlib color cycle. - line_style: None, matplotlib line style, list of line_styles, optional + linestyle : None or (list of) matplotlib line style, default None Sets the line style of the plots (no action on 'heatmap'). - The main line style are '-','--','steps','-.',':'. - For a list, if its length is less than the number of spectra - to plot, `line_style` will be cycled, eg. ('-','--','steps','-.',':'). - If None (default), use continuous lines, eg. '-'. - padding : float, optional - Option for "cascade". 1.0 (default) guarantees that there is no overlapping. + The main line style are ``'-'``, ``'--'``, ``'-.'``, ``':'``. + For a list, if its length is less than the number of + spectra to plot, linestyle will be cycled. + If `None`, use continuous lines (same as ``'-'``). + drawstyle : {``'default'`` | ``'steps'`` | ``'steps-pre'`` | ``'steps-mid'`` | ``'steps-post'``}, + default 'default' + The drawstyle determines how the points are connected, no action with + ``style='heatmap'``. See + :meth:`matplotlib.lines.Line2D.set_drawstyle` for more information. + The ``'default'`` value is defined by matplotlib. + padding : float, default 1.0 + Option for "cascade". 1 guarantees that there is no overlapping. However, in many cases, a value between 0 and 1 can produce a tighter plot without overlapping. Negative values have the same effect but reverse the order of the spectra without reversing the order of the colors. - legend: None, list of str, 'auto', optional + legend : None, list of str, ``'auto'``, default None If list of string, legend for 'cascade' or title for 'mosaic' is displayed. If 'auto', the title of each spectra (metadata.General.title) is used. Default None. - legend_picking: bool, optional + legend_picking : bool, default True If True (default), a spectrum can be toggled on and off by clicking on the legended line. - legend_loc : str, int, optional + legend_loc : str or int, optional This parameter controls where the legend is placed on the figure; - see the pyplot.legend docstring for valid values. Default 'upper right'. - fig : None, matplotlib figure, optional - If None (default), a default figure will be created. Specifying `fig` will - not work for the 'heatmap' style. - ax : none, matplotlib ax (subplot), optional - If None (default), a default ax will be created. Will not work for 'mosaic' - or 'heatmap' style. - auto_update : bool or None + see the pyplot.legend docstring for valid values. Default ``'upper right'``. + fig : None, matplotlib.figure.Figure, default None + If None (default), a default figure will be created. + Not supported for the ``'heatmap'`` style. + ax : None, matplotlib.axes.Axes, default None + If None (default), matplotlib axes will be created when necessary. + Not supported for the ``'heatmap'`` style. + auto_update : bool or None, default None If True, the plot will update when the data are changed. Only supported with style='overlap' and a list of signal with navigation dimension 0. If None (default), update the plot only for style='overlap'. - **kwargs, optional - Keywords arguments passed to :py:func:`matplotlib.pyplot.figure` or - :py:func:`matplotlib.pyplot.subplots` if style='mosaic'. - Has no effect on 'heatmap' style. + normalise : bool, default False + If True, the data are normalised to the [0, 1] interval in the plot. + **kwargs : dict + Depending on the style used, the keyword arguments are passed to different functions - Example - ------- - >>> s = hs.load("some_spectra") + - ``"overlap"``, ``"cascade"`` or ``"mosiac"``: arguments passed to :func:`matplotlib.pyplot.figure` + - ``"heatmap"``: arguments passed to :meth:`~.api.signals.Signal2D.plot`. + + Examples + -------- + >>> s = hs.signals.Signal1D(np.arange(100).reshape((10, 10))) >>> hs.plot.plot_spectra(s, style='cascade', color='red', padding=0.5) + To save the plot as a png-file - >>> hs.plot.plot_spectra(s).figure.savefig("test.png") + >>> ax = hs.plot.plot_spectra(s) + >>> ax.figure.savefig("test.png") # doctest: +SKIP Returns ------- - ax: matplotlib axes or list of matplotlib axes + :class:`matplotlib.axes.Axes` or list of :class:`matplotlib.axes.Axes` An array is returned when `style` is 'mosaic'. """ - import hyperspy.signal + from hyperspy.signal import BaseSignal def _reverse_legend(ax_, legend_loc_): """ @@ -1384,10 +1569,15 @@ def _reverse_legend(ax_, legend_loc_): This parameter controls where the legend is placed on the figure; see the pyplot.legend docstring for valid values. """ - l = ax_.get_legend() - labels = [lb.get_text() for lb in list(l.get_texts())] - handles = l.legendHandles - ax_.legend(handles[::-1], labels[::-1], loc=legend_loc_) + line = ax_.get_legend() + labels = [lb.get_text() for lb in list(line.get_texts())] + # "legendHandles" is deprecated in matplotlib 3.7.0 in favour of + # "legend_handles". + if Version(mpl.__version__) >= Version("3.7"): + handles = line.legend_handles + else: + handles = line.legendHandles + ax_.legend(reversed(handles), reversed(labels), loc=legend_loc_) # Before v1.3 default would read the value from prefereces. if style == "default": @@ -1399,93 +1589,129 @@ def _reverse_legend(ax_, legend_loc_): elif hasattr(color, "__iter__"): color = itertools.cycle(color) else: - raise ValueError("Color must be None, a valid matplotlib color " - "string, or a list of valid matplotlib colors.") + raise ValueError( + "Color must be None, a valid matplotlib color " + "string, or a list of valid matplotlib colors." + ) else: - color = itertools.cycle( - plt.rcParams['axes.prop_cycle'].by_key()["color"]) - - if line_style is not None: - if isinstance(line_style, str): - line_style = itertools.cycle([line_style]) - elif hasattr(line_style, "__iter__"): - line_style = itertools.cycle(line_style) - else: - raise ValueError("line_style must be None, a valid matplotlib " - "line_style string or a list of valid matplotlib " - "line_style.") + color = itertools.cycle(plt.rcParams["axes.prop_cycle"].by_key()["color"]) + + if linestyle is not None: + if isinstance(linestyle, str): + linestyle = itertools.cycle([linestyle]) + elif hasattr(linestyle, "__iter__"): + linestyle = itertools.cycle(linestyle) else: - line_style = ['-'] * len(spectra) + linestyle = ["-"] * len(spectra) if legend is not None: if isinstance(legend, str): - if legend == 'auto': + if legend == "auto": legend = [spec.metadata.General.title for spec in spectra] else: - raise ValueError("legend must be None, 'auto' or a list of " - "strings.") + raise ValueError("legend must be None, 'auto' or a list of " "strings.") - if style == 'overlap': + if normalise: + ylabel = "Normalised Intensity" + else: + ylabel = "Intensity" + + # Get fig and ax + # Try to get fig from ax + if ax is not None: + if style == "heatmap": + raise ValueError("The `ax` parameter is not supported for 'heatmap' style.") + # To avoid ambiguity, don't support iterable with overalp and cascase style + elif style in ["overlap", "cascade"]: + if isiterable(ax): + raise ValueError( + "When using 'overlap' or 'cascade' style, `ax` must be a matplotlib axis." + ) + fig = ax.get_figure() + else: + # use flatten for cases where ax is two dimensional + fig = np.asarray(ax).flatten()[0].get_figure() + # fallback to fig, create when necessary + else: if fig is None: + if style == "mosaic": + default_fsize = plt.rcParams["figure.figsize"] + kwargs.setdefault( + "figsize", (default_fsize[0], default_fsize[1] * len(spectra)) + ) fig = plt.figure(**kwargs) - if ax is None: + elif style == "heatmap": + raise ValueError( + "The `fig` parameter is not supported for 'heatmap' style." + ) + if style in ["overlap", "cascade"]: ax = fig.add_subplot(111) - _make_overlap_plot(spectra, - ax, - color=color, - line_style=line_style,) + else: + ax = fig.subplots(len(spectra), 1) + + if style == "overlap": + _make_overlap_plot( + spectra, ax, color, linestyle, normalise, drawstyle=drawstyle + ) + ax.set_ylabel(ylabel) if legend is not None: ax.legend(legend, loc=legend_loc) _reverse_legend(ax, legend_loc) if legend_picking is True: - animate_legend(fig=fig, ax=ax, plot_type='spectra') - elif style == 'cascade': - if fig is None: - fig = plt.figure(**kwargs) - if ax is None: - ax = fig.add_subplot(111) - _make_cascade_subplot(spectra, - ax, - color=color, - line_style=line_style, - padding=padding) + animate_legend(fig=fig, ax=ax, plot_type="spectra") + elif style == "cascade": + _make_cascade_subplot( + spectra, + ax, + color, + linestyle, + normalise, + padding=padding, + drawstyle=drawstyle, + ) if legend is not None: - plt.legend(legend, loc=legend_loc) + ax.legend(legend, loc=legend_loc) _reverse_legend(ax, legend_loc) - elif style == 'mosaic': - default_fsize = plt.rcParams["figure.figsize"] - figsize = (default_fsize[0], default_fsize[1] * len(spectra)) - fig, subplots = plt.subplots( - len(spectra), 1, figsize=figsize, **kwargs) + if legend_picking is True: + animate_legend(fig=fig, ax=ax) + + elif style == "mosaic": if legend is None: legend = [legend] * len(spectra) - for spectrum, ax, color, line_style, legend in zip( - spectra, subplots, color, line_style, legend): + for spectrum, ax_, color, linestyle, legend in zip( + spectra, ax, color, linestyle, legend + ): spectrum = _transpose_if_required(spectrum, 1) - _plot_spectrum(spectrum, ax, color=color, line_style=line_style) - ax.set_ylabel('Intensity') + _plot_spectrum( + spectrum, + ax_, + normalise, + color=color, + linestyle=linestyle, + drawstyle=drawstyle, + ) + ax_.set_ylabel(ylabel) if legend is not None: - ax.set_title(legend) - if not isinstance(spectra, hyperspy.signal.BaseSignal): - _set_spectrum_xlabel(spectrum, ax) - if isinstance(spectra, hyperspy.signal.BaseSignal): - _set_spectrum_xlabel(spectrum, ax) + ax_.set_title(legend) + if not isinstance(spectra, BaseSignal): + _set_spectrum_xlabel(spectrum, ax_) + if isinstance(spectra, BaseSignal): + _set_spectrum_xlabel(spectrum, ax_) fig.tight_layout() - elif style == 'heatmap': - if not isinstance(spectra, hyperspy.signal.BaseSignal): + elif style == "heatmap": + if not isinstance(spectra, BaseSignal): import hyperspy.utils - spectra = [_transpose_if_required(spectrum, 1) for spectrum in - spectra] + + spectra = [_transpose_if_required(spectrum, 1) for spectrum in spectra] spectra = hyperspy.utils.stack(spectra) with spectra.unfolded(): - ax = _make_heatmap_subplot(spectra) - ax.set_ylabel('Spectra') - ax = ax if style != "mosaic" else subplots + ax = _make_heatmap_subplot(spectra, normalise, **kwargs) + ax.set_ylabel("Spectra") - def update_line(spectrum, line): + def update_line(spectrum, line, normalise): x_axis = spectrum.axes_manager[-1].axis - line.set_data(x_axis, spectrum.data) + line.set_data(x_axis, _parse_array(spectrum, normalise)) fig = line.get_figure() ax = fig.get_axes()[0] # `relim` needs to be called before `autoscale_view` @@ -1493,28 +1719,30 @@ def update_line(spectrum, line): ax.autoscale_view() fig.canvas.draw() - if auto_update is None and style == 'overlap': + if auto_update is None and style == "overlap": auto_update = True if auto_update: - if style != 'overlap': - raise ValueError("auto_update=True is only supported with " - "style='overlap'.") + if style != "overlap": + raise ValueError( + "auto_update=True is only supported with " "style='overlap'." + ) - for spectrum, line in zip(spectra, ax.get_lines()): - f = partial(update_line, spectrum, line) - spectrum.events.data_changed.connect(f, []) + for s, line in zip(spectra, ax.get_lines()): + f = partial(update_line, s, line=line, normalise=normalise) + s.events.data_changed.connect(f, []) # disconnect event when closing figure - disconnect = partial(spectrum.events.data_changed.disconnect, f) + disconnect = partial(s.events.data_changed.disconnect, f) on_figure_window_close(fig, disconnect) return ax -def animate_legend(fig=None, ax=None, plot_type='spectra'): +def animate_legend(fig=None, ax=None, plot_type="spectra"): """Animate the legend of a figure. - A spectrum can be toggled on and off by clicking on the line in the legend. + A spectrum or image can be toggled on and off by clicking on the line in + the legend. Parameters ---------- @@ -1524,9 +1752,8 @@ def animate_legend(fig=None, ax=None, plot_type='spectra'): ax: {None, matplotlib.axes}, optional If None (Default), pick the current axes using "plt.gca". - Note - ---- - + Notes + ----- Code inspired from legend_picking.py in the matplotlib gallery. """ @@ -1537,17 +1764,17 @@ def animate_legend(fig=None, ax=None, plot_type='spectra'): leg = ax.get_legend() - if plot_type=='spectra': + if plot_type == "spectra": lines = ax.lines[::-1] leglines = leg.get_lines() - elif plot_type=='images': + elif plot_type == "images": lines = ax.images[1:] leglines = leg.get_patches() lined = dict() for legline, origline in zip(leglines, lines): - if plot_type=='spectra': + if plot_type == "spectra": legline.set_pickradius(preferences.Plot.pick_tolerance) legline.set_picker(True) lined[legline] = origline @@ -1568,86 +1795,406 @@ def onpick(event): legline.set_alpha(0.2) fig.canvas.draw_idle() - fig.canvas.mpl_connect('pick_event', onpick) + fig.canvas.mpl_connect("pick_event", onpick) -def plot_histograms(signal_list, - bins='fd', - range_bins=None, - color=None, - line_style=None, - legend='auto', - fig=None, - **kwargs): +def plot_histograms( + signal_list, + bins="fd", + range_bins=None, + color=None, + linestyle=None, + legend="auto", + fig=None, + ax=None, + **kwargs, +): """Plot the histogram of every signal in the list in one figure. This function creates a histogram for each signal and plots the list with - the `utils.plot.plot_spectra` function. + the :func:`~.api.plot.plot_spectra` function. Parameters ---------- signal_list : iterable - Ordered list of spectra to plot. If `style` is 'cascade' or 'mosaic', - the spectra can have different size and axes. - bins : int, list, str, optional - If bins is a string, then it must be one of: - 'knuth' : use Knuth's rule to determine bins, - 'scott' : use Scott's rule to determine bins, - 'fd' : use the Freedman-diaconis rule to determine bins (default), - 'blocks' : use bayesian blocks for dynamic bin widths. - range_bins : None, tuple, optional - The minimum and maximum range for the histogram. If not specified, - it will be (x.min(), x.max()). Default None. - color : None, valid matplotlib color, list of colors, optional + Ordered list of spectra to plot. If ``style`` is ``"cascade"`` or + ``"mosaic"``, the spectra can have different size and axes. + %s + %s + color : None, (list of) matplotlib color, optional Sets the color of the lines of the plots. For a list, if its length is less than the number of spectra to plot, the colors will be cycled. - If None (default), use default matplotlib color cycle. - line_style: None, valid matplotlib line style, list of line styles, optional - The main line styles are '-','--','steps','-.',':'. - For a list, if its length is less than the number of spectra - to plot, `line_style` will be cycled, eg. ('-','--','steps','-.',':'). - If None, use continuous lines. - legend: None, list of str, 'auto', optional - Display a legend. If 'auto' (default), the title of each spectra + If `None`, use default matplotlib color cycle. + linestyle : None, (list of) matplotlib line style, optional + The main line styles are ``'-'``, ``'--'``, ``'-.'``, ``':'``. + For a list, if its length is less than the number of + spectra to plot, linestyle will be cycled. + If `None`, use continuous lines (same as ``'-'``). + legend : None, list of str, ``'auto'``, default ``'auto'`` + Display a legend. If 'auto', the title of each spectra (metadata.General.title) is used. - legend_picking: bool, optional + legend_picking : bool, default True If True, a spectrum can be toggled on and off by clicking on the line in the legend. - fig : None, matplotlib figure, optional + fig : None, matplotlib.figure.Figure, default None If None (default), a default figure will be created. **kwargs other keyword arguments (weight and density) are described in - :py:func:`numpy.histogram`. + :func:`numpy.histogram`. - Example - ------- + Examples + -------- Histograms of two random chi-square distributions. - >>> img = hs.signals.Signal2D(np.random.chisquare(1,[10,10,100])) - >>> img2 = hs.signals.Signal2D(np.random.chisquare(2,[10,10,100])) - >>> hs.plot.plot_histograms([img,img2],legend=['hist1','hist2']) + >>> img = hs.signals.Signal2D(np.random.chisquare(1, [10, 10, 100])) + >>> img2 = hs.signals.Signal2D(np.random.chisquare(2, [10, 10, 100])) + >>> hs.plot.plot_histograms([img, img2], legend=['hist1', 'hist2']) + Returns ------- - ax: matplotlib axes or list of matplotlib axes - An array is returned when `style` is 'mosaic'. + matplotlib.axes.Axes or list of matplotlib.axes.Axes + An array is returned when ``style='mosaic'``. """ hists = [] for obj in signal_list: - hists.append(obj.get_histogram(bins=bins, - range_bins=range_bins, **kwargs)) - if line_style is None: - line_style = 'steps' - return plot_spectra(hists, style='overlap', color=color, - line_style=line_style, legend=legend, fig=fig) + hists.append(obj.get_histogram(bins=bins, range_bins=range_bins, **kwargs)) + return plot_spectra( + hists, + style="overlap", + color=color, + linestyle=linestyle, + drawstyle="steps-mid", + legend=legend, + fig=fig, + ax=ax, + ) -def picker_kwargs(value, kwargs={}): +plot_histograms.__doc__ %= (HISTOGRAM_BIN_ARGS, HISTOGRAM_RANGE_ARGS) + + +def picker_kwargs(value, kwargs=None): + if kwargs is None: + kwargs = {} # picker is deprecated in favor of pickradius - if LooseVersion(mpl.__version__) >= LooseVersion("3.3.0"): - kwargs.update({'pickradius': value, 'picker':True}) + if Version(mpl.__version__) >= Version("3.3.0"): + kwargs.update({"pickradius": value, "picker": True}) else: - kwargs['picker'] = value + kwargs["picker"] = value return kwargs + + +def _create_span_roi_group(sig_ax, N): + """ + Creates a set of `N` of :py:class:`~.roi.SpanROI`\\ s that sit along axis at sensible positions. + + Arguments + --------- + sig_ax: DataAxis + The axis over which the ROI will be placed + N: int + The number of ROIs + """ + axis = sig_ax.axis + ax_range = axis[-1] - axis[0] + span_width = ax_range / (2 * N) + + spans = [] + + for i in range(N): + # create a span that has a unique range + span = hs.roi.SpanROI(i * span_width + axis[0], (i + 1) * span_width + axis[0]) + + spans.append(span) + + return spans + + +def _create_rect_roi_group(sig_wax, sig_hax, N): + """ + Creates a set of `N` :py:class:`~roi.RectangularROI`\\ s that sit along + `waxis` and `haxis` at sensible positions. + + Arguments + --------- + sig_wax: DataAxis + The width axis over which the ROI will be placed + sig_hax: DataAxis + The height axis over which the ROI will be placed + N: int + The number of ROIs + """ + waxis = sig_wax.axis + haxis = sig_hax.axis + + w_range = waxis[-1] - waxis[0] + h_range = haxis[-1] - haxis[0] + + span_w_width = w_range / (2 * N) + span_h_width = h_range / (2 * N) + + rects = [] + + for i in range(N): + # create a span that has a unique range + rect = hs.roi.RectangularROI( + left=i * span_w_width + waxis[0], + top=i * span_h_width + haxis[0], + right=(i + 1) * span_w_width + waxis[0], + bottom=(i + 1) * span_h_width + haxis[0], + ) + + rects.append(rect) + + return rects + + +def _make_cmaps(colors): + cmap_name = [] + for n_color, color in enumerate(colors): + color = mcolors.to_hex(color) + name = f"single_color_{color}" + if name not in plt.colormaps(): + make_cmap(colors=["#000000", color], name=name) + cmap_name.append(name) + return cmap_name + + +def _add_colored_frame(ax, color, animated=True): + colored_frame = patches.Rectangle( + (0, 0), + 1, + 1, + linewidth=10, + edgecolor=color, + facecolor="none", + transform=ax.transAxes, + animated=animated and ax.get_figure().canvas.supports_blit, + ) + ax.add_patch(colored_frame) + + +def _roi_sum(signal, roi, axes, out=None): + sliced_signal = roi(signal=signal, axes=axes) + if out is not None: + # use np.sum if the data doesn't contain nan + # ~2x (or more for larger array) faster than nansum + f = np.nansum if np.isnan(sliced_signal.data).any() else np.sum + out.data[:] = f(sliced_signal.data, axis=axes) + out.events.data_changed.trigger(obj=out) + else: + # we don't care if this is not optimised for speed since this is + # expected to be called only when setting up the out signal + with warnings.catch_warnings(): + # Catch warning for non-uniform axis + warnings.filterwarnings("ignore", category=UserWarning, module="hyperspy") + s = sliced_signal.nansum(axis=axes) + # Reset signal to default Signal1D or Signal2D + s.set_signal_type("") + s.metadata.General.title = "Integrated intensity" + return s + + +def plot_roi_map( + signal, + rois=1, + color=None, + cmap=None, + single_figure=False, + single_figure_kwargs=None, + **kwargs, +): + """ + Plot one or multiple ROI maps of a ``signal``. + + Uses regions of interest (ROIs) to select ranges along the signal axis. + + For each ROI, a plot is generated of the summed data values within + this signal ROI at each point in the ``signal``'s navigation space. + + The ROIs can be moved interactively and the corresponding map plots will + update automatically. + + Parameters + ---------- + signal : :class:`~.api.signals.BaseSignal` + The signal to inspect. + rois : int, subclass of :class:`hyperspy.roi.BaseROI` or list of :class:`hyperspy.roi.BaseROI` + ROIs to slice the signal in signal space. If ``int``, define the number of + ROIs to use. + color : list of str or None + Color of the ROIs. Any string supported by matplotlib to define a color + can be used. The length of the list must be equal to the number ROIs. + If None (default), the default matplotlib colors are used. + cmap : str of list of str or None + Only for signals with navigation dimension of 2. Define the colormap of the map(s). + If string, any string supported by matplotlib to define a colormap can be used + and a colored frame matching the ROI color will be added to the map. + If list of str, it must be the same length as the ROIs. + If None (default), the colors from the ``color`` argument are used and no colored + frame is added. + single_figure : bool + Whether to plot on a single figure or several figures. + If True, :func:`~.api.plot.plot_images` or :func:`~.plot.plot_spectra` + will be used, depending on the navigation dimension of the signal. + single_figure_kwargs : dict, None + Only when ``single_figure=True``. Keywords arguments are passed to + :func:`~.api.plot.plot_images` or :func:`~.plot.plot_spectra` + depending on the navigation dimension of the signal. + If None, default ``kwargs`` are used with the following changes + ``scalebar=[0]``, ``axes_decor="off"`` and ``suptitle=""``. + **kwargs : dict + The keyword argument are passed to :meth:`~.api.signals.Signal2D.plot`. + + Returns + ------- + rois : list of :class:`hyperspy.roi.BaseROI` + The ROI objects that slice ``signal``. + roi_sums : :class:`~.api.signals.BaseSignal` + The sums of the signals defined by the ROIs. + + Notes + ----- + Performance consideration: + + - :class:`~.api.roi.RectangularROI` is ~2x faster than :class:`~.api.roi.CircleROI`. + - If the data sliced by the ROI contains :obj:`numpy.nan`, :func:`numpy.nansum` + will be used instead of :func:`numpy.sum` at the cost of a speed penalty (more than + 2 times slower). + - Plotting ROI maps on a single figure is slower than on separate figures. + + Examples + -------- + **3D hyperspectral data** + + For 3D hyperspectral data, the ROIs used will be instances of + :class:`~.api.roi.SpanROI`. Therefore, these ROIs can be used to select + particular spectral ranges, e.g. a particular peak. + + The map generated for a given ROI is therefore the sum of this spectral + region at each point in the hyperspectral map. Therefore, regions of the + sample where this peak is bright will be bright in this map. + + **4D STEM** + + For 4D STEM data, by default, the ROIs used will be instances of + :class:`~.api.roi.RectangularROI`. Other hyperspy ROIs, such as + :class:`~.api.roi.CircleROI` can be used. These ROIs can be used + to select particular regions in reciprocal space, e.g. a particular + diffraction spot. + + The map generated for a given ROI is the intensity of this + region at each point in the scan. Therefore, regions of the + scan where a particular spot is intense will appear bright. + """ + if signal._plot is None or not signal._plot.is_active: + signal.plot() + + sig_dims = len(signal.axes_manager.signal_axes) + nav_dims = len(signal.axes_manager.navigation_axes) + + if sig_dims not in [1, 2]: + raise ValueError("The signal must have signal dimension of 1 or 2.") + + if nav_dims == 0: + raise ValueError("Navigation dimension must be larger than 0.") + + if isinstance(rois, int): + if sig_dims == 1: + rois = _create_span_roi_group(signal.axes_manager.signal_axes[0], rois) + elif sig_dims == 2: + rois = _create_rect_roi_group(*signal.axes_manager.signal_axes, rois) + if isinstance(rois, hyperspy.roi.BaseROI): + rois = [rois] + + if color is None: + color = mpl.rcParams["axes.prop_cycle"].by_key()["color"] + elif isinstance(color, (list, tuple)): + if len(rois) != len(color): + raise ValueError( + f"The number of rois ({len(rois)}) must match " + f"the number of colors ({len(color)})." + ) + else: # pragma: no cover + raise ValueError("Provided value of color is not supported.") + + add_colored_frame = False + if cmap is None: + cmap = _make_cmaps(color) + elif isinstance(cmap, str): + add_colored_frame = True + cmap = [cmap] * len(rois) + elif isinstance(color, (list, tuple)): + if len(rois) != len(cmap): + raise ValueError( + f"The number of rois ({len(rois)}) must match " + f"the number of cmap ({len(cmap)})." + ) + else: # pragma: no cover + raise ValueError("Provided value of cmap is not supported.") + + roi_sums = [] + axes = tuple(axis.index_in_axes_manager for axis in signal.axes_manager.signal_axes) + + for i, (roi, color_, cmap_) in enumerate(zip(rois, color, cmap)): + # add it to the sum over all positions + roi.add_widget(signal, axes=axes, color=color_) + + # create the signal that is the sum slice + roi_sum = _roi_sum(signal, roi=roi, axes=axes) + # Transpose shape to swap these points per nav into signal points. + # Cap to 2, since hyperspy can't handle signal dimension higher than 2. + # Take the two first navigation dimensions by default. + roi_sum = roi_sum.transpose( + signal_axes=min(roi_sum.axes_manager.navigation_dimension, 2) + ) + + # connect the span signal changing range to the value of span_sum + hs.interactive( + _roi_sum, + event=roi.events.changed, + signal=signal, + roi=roi, + axes=axes, + out=roi_sum, + recompute_out_event=None, + ) + + roi_sums.append(roi_sum) + + if not single_figure: + roi_sum.plot(cmap=cmap_, **kwargs) + # Remove widget from signal plot when closing maps figure + roi_sum._plot.signal_plot.events.closed.connect(roi.remove_widget, []) + + if add_colored_frame: + _add_colored_frame(roi_sum._plot.signal_plot.ax, color_) + + if single_figure: + if single_figure_kwargs is None: + single_figure_kwargs = {} + if nav_dims == 1: + axs = plot_spectra(roi_sums, color=color, **single_figure_kwargs) + else: + # default plot kwargs + for k, v in zip(["scalebar", "axes_decor", "suptitle"], [0, "off", ""]): + single_figure_kwargs.setdefault(k, v) + axs = plot_images(roi_sums, cmap=cmap, **single_figure_kwargs) + if add_colored_frame: + # hs.plot.plot_images doesn't use blitting + for ax, color_ in zip(axs, color): + _add_colored_frame(ax, color_, animated=False) + + def remove_widgets(): # pragma: no cover + for roi in rois: + roi.remove_widget() + + if not isinstance(axs, list): + axs = [axs] + on_figure_window_close(axs[0].get_figure(), remove_widgets) + + # return all ya bits for future messing around. + return rois, roi_sums diff --git a/hyperspy/drawing/widget.py b/hyperspy/drawing/widget.py index a1e510ad48..4d9172b706 100644 --- a/hyperspy/drawing/widget.py +++ b/hyperspy/drawing/widget.py @@ -1,34 +1,33 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from __future__ import division import matplotlib.pyplot as plt -from matplotlib.backend_bases import MouseEvent import numpy as np +from matplotlib.backend_bases import MouseEvent, PickEvent -from hyperspy.drawing.utils import on_figure_window_close -from hyperspy.events import Events, Event from hyperspy.defaults_parser import preferences +from hyperspy.drawing.utils import on_figure_window_close +from hyperspy.events import Event, Events class WidgetBase(object): - """Base class for interactive widgets/patches. A widget creates and maintains one or more matplotlib patches, and manages the interaction code so that the user can maniuplate it on the fly. @@ -46,47 +45,57 @@ class WidgetBase(object): needed. """ - def __init__(self, axes_manager=None, color='red', alpha=1.0, **kwargs): + def __init__(self, axes_manager=None, color="red", alpha=1.0, **kwargs): self.axes_manager = axes_manager self._axes = list() self.ax = None self.picked = False self.selected = False self._selected_artist = None - self._size = 1. - self._pos = np.array([0.]) + self._size = 1.0 + self._pos = np.array([0.0]) self._is_on = True self.background = None - self.patch = [] + self._patch = [] self.color = color self.alpha = alpha self.cids = list() self.blit = None self.events = Events() - self.events.changed = Event(doc=""" + self.events.changed = Event( + doc=""" Event that triggers when the widget has a significant change. The event triggers after the internal state of the widget has been updated. - Arguments: + Parameters ---------- - widget: - The widget that changed - """, arguments=['obj']) - self.events.closed = Event(doc=""" + widget : + The widget that changed + """, + arguments=["obj"], + ) + self.events.closed = Event( + doc=""" Event that triggers when the widget closed. The event triggers after the widget has already been closed. - Arguments: + Parameters ---------- - widget: - The widget that closed - """, arguments=['obj']) + widget : + The widget that closed + """, + arguments=["obj"], + ) self._navigating = False super(WidgetBase, self).__init__(**kwargs) + @property + def patch(self): + return tuple(self._patch) + def _get_axes(self): return self._axes @@ -96,17 +105,16 @@ def _set_axes(self, axes): else: self._axes = axes - axes = property(lambda s: s._get_axes(), - lambda s, v: s._set_axes(v)) + axes = property(lambda s: s._get_axes(), lambda s, v: s._set_axes(v)) @property def is_on(self): - """Determines if the widget is set to draw if valid (turned on). - """ + """Determines if the widget is set to draw if valid (turned on).""" return self._is_on def set_on(self, value, render_figure=True): - """Change the on state of the widget. If turning off, all patches will + """ + Change the on state of the widget. If turning off, all patches will be removed from the matplotlib axes and the widget will disconnect from all events. If turning on, the patch(es) will be added to the matplotlib axes, and the widget will connect to its default events. @@ -118,16 +126,10 @@ def set_on(self, value, render_figure=True): self._add_patch_to(self.ax) self.connect(self.ax) elif value is False: - for container in [ - self.ax.patches, - self.ax.lines, - self.ax.artists, - self.ax.texts]: - for p in self.patch: - if p in container: - container.remove(p) + for p in self.patch: + p.remove() self.disconnect() - if hasattr(super(WidgetBase, self), 'set_on'): + if hasattr(super(WidgetBase, self), "set_on"): super(WidgetBase, self).set_on(value) if did_something: if render_figure: @@ -157,21 +159,19 @@ def alpha(self, alpha): p.set_alpha(self._alpha) def _set_patch(self): - """Create the matplotlib patch(es), and store it in self.patch - """ - if hasattr(super(WidgetBase, self), '_set_patch'): + """Create the matplotlib patch(es), and store it in self.patch""" + if hasattr(super(WidgetBase, self), "_set_patch"): super(WidgetBase, self)._set_patch() # Must be provided by the subclass def _add_patch_to(self, ax): - """Create and add the matplotlib patches to 'ax' - """ - self.blit = hasattr(ax, 'hspy_fig') and ax.figure.canvas.supports_blit + """Create and add the matplotlib patches to 'ax'""" + self.blit = hasattr(ax, "hspy_fig") and ax.figure.canvas.supports_blit self._set_patch() for p in self.patch: ax.add_artist(p) p.set_animated(self.blit) - if hasattr(super(WidgetBase, self), '_add_patch_to'): + if hasattr(super(WidgetBase, self), "_add_patch_to"): super(WidgetBase, self)._add_patch_to(ax) def set_mpl_ax(self, ax): @@ -199,18 +199,21 @@ def select(self): if not self.patch or not self.is_on or not self.ax: return - canvas = self.ax.figure.canvas + figure = self.ax.figure # Simulate a pick event x, y = self.patch[0].get_transform().transform_point((0, 0)) - mouseevent = MouseEvent('pick_event', canvas, x, y) - # when the widget is added programatically, mouseevent can be "empty" + mouseevent = MouseEvent("pick_event", figure.canvas, x, y) if mouseevent.button: - canvas.pick_event(mouseevent, self.patch[0]) + try: + # Introduced in matplotlib 3.6 and `pick_event` deprecated + event = PickEvent("pick_event", figure, mouseevent, self.patch[0]) + figure.canvas.callbacks.process("pick_event", event) + except Exception: # Deprecated in matplotlib 3.6 + figure.canvas.pick_event(mouseevent, self.patch[0]) self.picked = False def connect(self, ax): - """Connect to the matplotlib Axes' events. - """ + """Connect to the matplotlib Axes' events.""" on_figure_window_close(ax.figure, self.close) if self._navigating: self.connect_navigate() @@ -222,24 +225,22 @@ def connect_navigate(self): if self._navigating: self.disconnect_navigate() self.axes_manager.events.indices_changed.connect( - self._on_navigate, {'obj': 'axes_manager'}) - self._on_navigate(self.axes_manager) # Update our position + self._on_navigate, {"obj": "axes_manager"} + ) + self._on_navigate(self.axes_manager) # Update our position self._navigating = True def disconnect_navigate(self): - """Disconnect a previous naivgation connection. - """ + """Disconnect a previous naivgation connection.""" self.axes_manager.events.indices_changed.disconnect(self._on_navigate) self._navigating = False def _on_navigate(self, axes_manager): - """Callback for axes_manager's change notification. - """ - pass # Implement in subclass! + """Callback for axes_manager's change notification.""" + pass # Implement in subclass! def disconnect(self): - """Disconnect from all events (both matplotlib and navigation). - """ + """Disconnect from all events (both matplotlib and navigation).""" for cid in self.cids: try: self.ax.figure.canvas.mpl_disconnect(cid) @@ -256,10 +257,9 @@ def close(self, window=None, render_figure=False): self.events.closed.trigger(obj=self) def draw_patch(self, *args): - """Update the patch drawing. - """ + """Update the patch drawing.""" try: - if hasattr(self.ax, 'hspy_fig'): + if hasattr(self.ax, "hspy_fig"): self.ax.hspy_fig.render_figure() elif self.ax.figure is not None: self.ax.figure.canvas.draw_idle() @@ -304,7 +304,6 @@ def __str__(self): class DraggableWidgetBase(WidgetBase): - """Adds the `position` and `indices` properties, and adds a framework for letting the user drag the patch around. Also adds the `moved` event. @@ -320,7 +319,9 @@ class DraggableWidgetBase(WidgetBase): def __init__(self, axes_manager, **kwargs): super(DraggableWidgetBase, self).__init__(axes_manager, **kwargs) - self.events.moved = Event(doc=""" + self.is_pointer = False + self.events.moved = Event( + doc=""" Event that triggers when the widget was moved. The event triggers after the internal state of the widget has been @@ -328,11 +329,13 @@ def __init__(self, axes_manager, **kwargs): the widget was changed, so it is the responsibility of the user to suppress events as neccessary to avoid closed loops etc. - Arguments: + Parameters ---------- - obj: - The widget that was moved. - """, arguments=['obj']) + obj: + The widget that was moved. + """, + arguments=["obj"], + ) self._snap_position = True # Set default axes @@ -342,7 +345,7 @@ def __init__(self, axes_manager, **kwargs): else: self.axes = self.axes_manager.signal_axes[0:1] else: - self._pos = np.array([0.]) + self._pos = np.array([0.0]) def _set_axes(self, axes): super(DraggableWidgetBase, self)._set_axes(axes) @@ -350,8 +353,7 @@ def _set_axes(self, axes): self._pos = np.array([ax.low_value for ax in self.axes]) def _get_indices(self): - """Returns a tuple with the position (indices). - """ + """Returns a tuple with the position (indices).""" idx = [] for i in range(len(self.axes)): idx.append(self.axes[i].value2index(self._pos[i])) @@ -373,18 +375,17 @@ def _set_indices(self, value): p.append(self.axes[i].index2value(value[i])) self.position = p - indices = property(lambda s: s._get_indices(), - lambda s, v: s._set_indices(v)) + indices = property(lambda s: s._get_indices(), lambda s, v: s._set_indices(v)) def _pos_changed(self): """Call when the position of the widget has changed. It triggers the relevant events, and updates the patch position. """ if self._navigating: - with self.axes_manager.events.indices_changed.suppress_callback( - self._on_navigate): + with self.axes_manager.events.indices_changed.suppress(): for i in range(len(self.axes)): self.axes[i].value = self._pos[i] + self.axes_manager.events.indices_changed.trigger(obj=self.axes_manager) self.events.moved.trigger(self) self.events.changed.trigger(self) self._update_patch_position() @@ -406,10 +407,8 @@ def _validate_pos(self, pos): return pos def _get_position(self): - """Provides the position of the widget (by values) in a tuple. - """ - return tuple( - self._pos.tolist()) # Don't pass reference, and make it clear + """Provides the position of the widget (by values) in a tuple.""" + return tuple(self._pos.tolist()) # Don't pass reference, and make it clear def _set_position(self, position): """Sets the position of the widget (by values). The dimensions should @@ -422,8 +421,7 @@ def _set_position(self, position): self._pos = np.array(position) self._pos_changed() - position = property(lambda s: s._get_position(), - lambda s, v: s._set_position(v)) + position = property(lambda s: s._get_position(), lambda s, v: s._set_position(v)) def _do_snap_position(self, value=None): """Snaps position to axes grid. Returns snapped value. If value is @@ -443,30 +441,36 @@ def _set_snap_position(self, value): self._pos = snap_value self._pos_changed() - snap_position = property(lambda s: s._snap_position, - lambda s, v: s._set_snap_position(v)) + snap_position = property( + lambda s: s._snap_position, lambda s, v: s._set_snap_position(v) + ) def connect(self, ax): super(DraggableWidgetBase, self).connect(ax) canvas = ax.figure.canvas + self.cids.append(canvas.mpl_connect("motion_notify_event", self._onmousemove)) + self.cids.append(canvas.mpl_connect("pick_event", self.onpick)) self.cids.append( - canvas.mpl_connect('motion_notify_event', self._onmousemove)) - self.cids.append(canvas.mpl_connect('pick_event', self.onpick)) - self.cids.append(canvas.mpl_connect( - 'button_release_event', self.button_release)) + canvas.mpl_connect("button_release_event", self.button_release) + ) + canvas.mpl_connect("button_press_event", self._onjumpclick) + + def _onjumpclick(self, event): + """This method must be provided by subclasses""" + pass def _on_navigate(self, axes_manager): if axes_manager is self.axes_manager: p = self._pos.tolist() for i, a in enumerate(self.axes): p[i] = a.value - self.position = p # Use property to trigger events + self.position = p # Use property to trigger events def onpick(self, event): # Callback for MPL pick event - self.picked = (event.artist in self.patch) + self.picked = event.artist in self.patch self._selected_artist = event.artist - if hasattr(super(DraggableWidgetBase, self), 'onpick'): + if hasattr(super(DraggableWidgetBase, self), "onpick"): super(DraggableWidgetBase, self).onpick(event) self.selected = self.picked @@ -479,14 +483,12 @@ def _onmousemove(self, event): pass def _update_patch_position(self): - """Updates the position of the patch on the plot. - """ + """Updates the position of the patch on the plot.""" # This method must be provided by the subclass pass def _update_patch_geometry(self): - """Updates all geometrical properties of the patch on the plot. - """ + """Updates all geometrical properties of the patch on the plot.""" self._update_patch_position() def button_release(self, event): @@ -498,7 +500,6 @@ def button_release(self, event): class Widget1DBase(DraggableWidgetBase): - """A base class for 1D widgets. It sets the right dimensions for size and @@ -527,7 +528,6 @@ def _validate_pos(self, pos): class ResizableDraggableWidgetBase(DraggableWidgetBase): - """Adds the `size` property and get_size_in_axes method, and adds a framework for letting the user resize the patch, including resizing by key strokes ('+', '-'). Also adds the 'resized' event. @@ -546,13 +546,13 @@ class ResizableDraggableWidgetBase(DraggableWidgetBase): """ def __init__(self, axes_manager, **kwargs): - super(ResizableDraggableWidgetBase, self).__init__( - axes_manager, **kwargs) + super(ResizableDraggableWidgetBase, self).__init__(axes_manager, **kwargs) if not self.axes: self._size = np.array([1]) - self.size_step = 1 # = one step in index space + self.size_step = 1 # = one step in index space self._snap_size = True - self.events.resized = Event(doc=""" + self.events.resized = Event( + doc=""" Event that triggers when the widget was resized. The event triggers after the internal state of the widget has been @@ -560,11 +560,13 @@ def __init__(self, axes_manager, **kwargs): the widget was changed, so it is the responsibility of the user to suppress events as neccessary to avoid closed loops etc. - Arguments: + Parameters ---------- - obj: - The widget that was resized. - """, arguments=['obj']) + obj: + The widget that was resized. + """, + arguments=["obj"], + ) self.no_events_while_dragging = False self._drag_store = None @@ -575,7 +577,7 @@ def _set_axes(self, axes): def _get_step(self, axis): # TODO: need to check if this is working fine, particularly with - """ Use to determine the size of the widget with support for non + """Use to determine the size of the widget with support for non uniform axis. """ if axis.index >= axis.size - 1: @@ -596,8 +598,7 @@ def _set_size(self, value): """ value = np.minimum(value, [ax.size * ax.scale for ax in self.axes]) - value = np.maximum(value, - self.size_step * [ax.scale for ax in self.axes]) + value = np.maximum(value, self.size_step * [ax.scale for ax in self.axes]) if self.snap_size: value = self._do_snap_size(value) if np.any(self._size != value): @@ -613,6 +614,11 @@ def _do_snap_size(self, value=None): return value def _set_snap_size(self, value): + if value and any(not axis.is_uniform for axis in self.axes): + raise ValueError( + "The snap to axes values feature is not supported " + "for non-uniform axes." + ) self._snap_size = value if value: snap_value = self._do_snap_size(self._size) @@ -620,32 +626,31 @@ def _set_snap_size(self, value): self._size = snap_value self._size_changed() - snap_size = property(lambda s: s._snap_size, - lambda s, v: s._set_snap_size(v)) + snap_size = property(lambda s: s._snap_size, lambda s, v: s._set_snap_size(v)) def _set_snap_all(self, value): # Snap position first, as snapped size can depend on position. self.snap_position = value self.snap_size = value - snap_all = property(lambda s: s.snap_size and s.snap_position, - lambda s, v: s._set_snap_all(v)) + snap_all = property( + lambda s: s.snap_size and s.snap_position, lambda s, v: s._set_snap_all(v) + ) def increase_size(self): - """Increment all sizes by 1. Applied via 'size' property. - """ - self.size = np.array(self.size) + \ - self.size_step * np.array([a.scale for a in self.axes]) + """Increment all sizes by 1. Applied via 'size' property.""" + self.size = np.array(self.size) + self.size_step * np.array( + [a.scale for a in self.axes] + ) def decrease_size(self): - """Decrement all sizes by 1. Applied via 'size' property. - """ - self.size = np.array(self.size) - \ - self.size_step * np.array([a.scale for a in self.axes]) + """Decrement all sizes by 1. Applied via 'size' property.""" + self.size = np.array(self.size) - self.size_step * np.array( + [a.scale for a in self.axes] + ) def _size_changed(self): - """Triggers resize and changed events, and updates the patch. - """ + """Triggers resize and changed events, and updates the patch.""" self.events.resized.trigger(self) self.events.changed.trigger(self) self._update_patch_size() @@ -666,7 +671,7 @@ def set_size_in_indices(self, value): s = list() for i in range(len(self.axes)): s.append(int(round(value[i] * self.axes[i].scale))) - self.size = s # Use property to get full processing + self.size = s # Use property to get full processing def get_centre(self): """Gets the center indices. The default implementation is simply the @@ -687,14 +692,12 @@ def get_centre_index(self): return self.indices + self.get_size_in_indices() / 2.0 def _update_patch_size(self): - """Updates the size of the patch on the plot. - """ + """Updates the size of the patch on the plot.""" # This method must be provided by the subclass pass def _update_patch_geometry(self): - """Updates all geometry of the patch on the plot. - """ + """Updates all geometry of the patch on the plot.""" # This method must be provided by the subclass pass @@ -707,11 +710,10 @@ def on_key_press(self, event): def connect(self, ax): super(ResizableDraggableWidgetBase, self).connect(ax) canvas = ax.figure.canvas - self.cids.append(canvas.mpl_connect('key_press_event', - self.on_key_press)) + self.cids.append(canvas.mpl_connect("key_press_event", self.on_key_press)) def onpick(self, event): - if hasattr(super(ResizableDraggableWidgetBase, self), 'onpick'): + if hasattr(super(ResizableDraggableWidgetBase, self), "onpick"): super(ResizableDraggableWidgetBase, self).onpick(event) if self.picked: self._drag_store = (self.position, self.size) @@ -758,7 +760,6 @@ def button_release(self, event): class Widget2DBase(ResizableDraggableWidgetBase): - """A base class for 2D widgets. It sets the right dimensions for size and position, adds the 'border_thickness' attribute and initalizes the 'axes' attribute to the first two navigation axes if possible, if not, the two @@ -782,8 +783,9 @@ def __init__(self, axes_manager, **kwargs): elif self.axes_manager.signal_dimension > 1: self.axes = self.axes_manager.signal_axes[0:2] elif len(self.axes_manager.shape) > 1: - self.axes = (self.axes_manager.signal_axes + - self.axes_manager.navigation_axes) + self.axes = ( + self.axes_manager.signal_axes + self.axes_manager.navigation_axes + ) else: raise ValueError("2D widget needs at least two axes!") else: @@ -794,7 +796,7 @@ def _get_patch_xy(self): """Returns the xy position of the widget. In this default implementation, the widget is centered on the position. """ - return self._pos - self._size / 2. + return self._pos - self._size / 2.0 def _get_patch_bounds(self): """Returns the bounds of the patch in the form of a tuple in the order @@ -806,7 +808,7 @@ def _get_patch_bounds(self): """ xy = self._get_patch_xy() xs, ys = self.size - return (xy[0], xy[1], xs, ys) # x,y,w,h + return (xy[0], xy[1], xs, ys) # x,y,w,h def _update_patch_position(self): if self.is_on and self.patch: @@ -822,7 +824,7 @@ def _update_patch_geometry(self): self.draw_patch() -class ResizersMixin(object): +class ResizersMixin: """ Widget mix-in for adding resizing manipulation handles. @@ -850,9 +852,12 @@ def __init__(self, resizers=True, **kwargs): super(ResizersMixin, self).__init__(**kwargs) self.resizer_picked = False self.pick_offset = (0, 0) - self.resize_color = 'lime' + self.resize_color = "lime" pick_tol = preferences.Plot.pick_tolerance - self.resize_pixel_size = (pick_tol, pick_tol) # Set to None to make one data pixel + self.resize_pixel_size = ( + pick_tol, + pick_tol, + ) # Set to None to make one data pixel self._resizers = resizers self._resizer_handles = [] self._resizers_on = False @@ -872,8 +877,7 @@ def resizers(self, value): self._set_resizers(value, self.ax) def _update_resizers(self): - """Update resizer handles' patch geometry. - """ + """Update resizer handles' patch geometry.""" pos = self._get_resizer_pos() rsize = self._get_resizer_size() for i, r in enumerate(self._resizer_handles): @@ -891,14 +895,10 @@ def _set_resizers(self, value, ax): ax.add_artist(r) r.set_animated(self.blit) else: - for container in [ - ax.patches, - ax.lines, - ax.artists, - ax.texts]: - for r in self._resizer_handles: - if r in container: - container.remove(r) + for r in self._resizer_handles: + # check that the matplotlib patch is present before removing it + if r in ax.get_children(): + r.remove() self._resizers_on = value def _get_resizer_size(self): @@ -909,8 +909,9 @@ def _get_resizer_size(self): if self.resize_pixel_size is None: rsize = [ax.scale for ax in self.axes] else: - rsize = np.abs(invtrans.transform(self.resize_pixel_size) - - invtrans.transform((0, 0))) + rsize = abs( + invtrans.transform(self.resize_pixel_size) - invtrans.transform((0, 0)) + ) return rsize def _get_resizer_offset(self): @@ -920,31 +921,28 @@ def _get_resizer_offset(self): invtrans = self.ax.transData.inverted() border = self.border_thickness # Transform the border thickness into data values - dl = np.abs(invtrans.transform((border, border)) - - invtrans.transform((0, 0))) / 2 + dl = abs(invtrans.transform((border, border)) - invtrans.transform((0, 0))) / 2 rsize = self._get_resizer_size() return rsize / 2 + dl def _get_resizer_pos(self): - """Get the positions of the resizer handles. - """ + """Get the positions of the resizer handles.""" invtrans = self.ax.transData.inverted() border = self.border_thickness # Transform the border thickness into data values - dl = np.abs(invtrans.transform((border, border)) - - invtrans.transform((0, 0))) / 2 + dl = abs(invtrans.transform((border, border)) - invtrans.transform((0, 0))) / 2 rsize = self._get_resizer_size() xs, ys = self._size positions = [] rp = np.array(self._get_patch_xy()) - p = rp - rsize + dl # Top left + p = rp - rsize + dl # Top left positions.append(p) - p = rp + (xs - dl[0], -rsize[1] + dl[1]) # Top right + p = rp + (xs - dl[0], -rsize[1] + dl[1]) # Top right positions.append(p) - p = rp + (-rsize[0] + dl[0], ys - dl[1]) # Bottom left + p = rp + (-rsize[0] + dl[0], ys - dl[1]) # Bottom left positions.append(p) - p = rp + (xs - dl[0], ys - dl[1]) # Bottom right + p = rp + (xs - dl[0], ys - dl[1]) # Bottom right positions.append(p) return positions @@ -952,7 +950,7 @@ def _set_patch(self): """Creates the resizer handles, regardless of whether they will be used or not. """ - if hasattr(super(ResizersMixin, self), '_set_patch'): + if hasattr(super(ResizersMixin, self), "_set_patch"): super(ResizersMixin, self)._set_patch() if self._resizer_handles: @@ -961,20 +959,27 @@ def _set_patch(self): rsize = self._get_resizer_size() pos = self._get_resizer_pos() for i in range(len(pos)): - r = plt.Rectangle(pos[i], rsize[0], rsize[1], fill=True, lw=0, - fc=self.resize_color, picker=True,) + r = plt.Rectangle( + pos[i], + rsize[0], + rsize[1], + fill=True, + lw=0, + fc=self.resize_color, + picker=True, + ) self._resizer_handles.append(r) def set_on(self, value): - """Turns on/off resizers whet widget is turned on/off. - """ + """Turns on/off resizers whet widget is turned on/off.""" if self.resizers and value != self._resizers_on: self._set_resizers(value, self.ax) - if hasattr(super(ResizersMixin, self), 'set_on'): + if hasattr(super(ResizersMixin, self), "set_on"): super(ResizersMixin, self).set_on(value) def onpick(self, event): - """Picking of main patch is same as for widget base, but this also + """ + Picking of main patch is same as for widget base, but this also handles picking of the resize handles. If a resize handle is picked, `picked` is set to `True`, and `resizer_picked` is set to an integer indicating which handle was picked (0-3 for top left, top right, bottom @@ -999,14 +1004,14 @@ def onpick(self, event): self.resizer_picked = False else: self._set_resizers(False, self.ax) - if hasattr(super(ResizersMixin, self), 'onpick'): + if hasattr(super(ResizersMixin, self), "onpick"): super(ResizersMixin, self).onpick(event) def _add_patch_to(self, ax): """Same as widget base, but also adds resizers if 'resizers' property is True. """ - if self.resizers: + if self.resizers and self._resizers_on: self._set_resizers(True, ax) - if hasattr(super(ResizersMixin, self), '_add_patch_to'): + if hasattr(super(ResizersMixin, self), "_add_patch_to"): super(ResizersMixin, self)._add_patch_to(ax) diff --git a/hyperspy/drawing/widgets.py b/hyperspy/drawing/widgets.py index 1080627834..105520c80b 100644 --- a/hyperspy/drawing/widgets.py +++ b/hyperspy/drawing/widgets.py @@ -1,37 +1,54 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -"""Interactive widgets that can be added to `Signal` plots. +"""Interactive widgets that can be added to :class:`~.api.signals.BaseSignal` plots.""" -Example -------- - -""" - - -from hyperspy.drawing.widget import ( - WidgetBase, DraggableWidgetBase, ResizableDraggableWidgetBase, - Widget2DBase, Widget1DBase, ResizersMixin) +from hyperspy.drawing._widgets.circle import CircleWidget from hyperspy.drawing._widgets.horizontal_line import HorizontalLineWidget -from hyperspy.drawing._widgets.vertical_line import VerticalLineWidget from hyperspy.drawing._widgets.label import LabelWidget -from hyperspy.drawing._widgets.scalebar import ScaleBar -from hyperspy.drawing._widgets.circle import CircleWidget -from hyperspy.drawing._widgets.rectangles import RectangleWidget, SquareWidget -from hyperspy.drawing._widgets.range import ModifiableSpanSelector, RangeWidget from hyperspy.drawing._widgets.line2d import Line2DWidget +from hyperspy.drawing._widgets.range import RangeWidget +from hyperspy.drawing._widgets.rectangles import RectangleWidget, SquareWidget +from hyperspy.drawing._widgets.scalebar import ScaleBar +from hyperspy.drawing._widgets.vertical_line import VerticalLineWidget +from hyperspy.drawing.widget import ( + DraggableWidgetBase, + ResizableDraggableWidgetBase, + ResizersMixin, + Widget1DBase, + Widget2DBase, + WidgetBase, +) + +__all__ = [ + "WidgetBase", + "DraggableWidgetBase", + "ResizableDraggableWidgetBase", + "Widget2DBase", + "Widget1DBase", + "ResizersMixin", + "HorizontalLineWidget", + "VerticalLineWidget", + "LabelWidget", + "CircleWidget", + "ScaleBar", + "RectangleWidget", + "SquareWidget", + "RangeWidget", + "Line2DWidget", +] diff --git a/hyperspy/events.py b/hyperspy/events.py index d9402bb20f..edeecdd594 100644 --- a/hyperspy/events.py +++ b/hyperspy/events.py @@ -1,30 +1,29 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import inspect +import re from collections.abc import Iterable from contextlib import contextmanager -from functools import wraps # Used in exec statement -import re - +from functools import wraps # noqa: F401 Used in exec statement -class Events(object): +class Events: """ Events container. @@ -42,16 +41,16 @@ def suppress(self): all callbacks of all events in the container. When the 'with' lock completes, the old suppression values will be restored. - Example - ------- - >>> with obj.events.suppress(): + Examples + -------- + >>> with obj.events.suppress(): # doctest: +SKIP ... # Any events triggered by assignments are prevented: ... obj.val_a = a ... obj.val_b = b >>> # Trigger one event instead: - >>> obj.events.values_changed.trigger() + >>> obj.events.values_changed.trigger() # doctest: +SKIP - See also + See Also -------- Event.suppress Event.suppress_callback @@ -73,13 +72,13 @@ def _update_doc(self): Updates the doc to reflect the events that are contained """ new_doc = self.__class__.__doc__ - new_doc += '\n\tEvents:\n\t-------\n' + new_doc += "\n\tEvents:\n\t-------\n" for name, e in self._events.items(): - edoc = inspect.getdoc(e) or '' + edoc = inspect.getdoc(e) or "" doclines = edoc.splitlines() e_short = doclines[0] if len(doclines) > 0 else edoc - new_doc += '\t%s :\n\t\t%s\n' % (name, e_short) - new_doc = new_doc.replace('\t', ' ') + new_doc += "\t%s :\n\t\t%s\n" % (name, e_short) + new_doc = new_doc.replace("\t", " ") self.__doc__ = new_doc def __setattr__(self, name, value): @@ -142,14 +141,16 @@ def __repr__(self): return "" -class Event(object): +class Event: + """ + Events class - def __init__(self, doc='', arguments=None): - """ - Create an Event object. + """ - Arguments - --------- + def __init__(self, doc="", arguments=None): + """ + Parameters + ---------- doc : str Optional docstring for the new Event. arguments : iterable @@ -157,8 +158,8 @@ def __init__(self, doc='', arguments=None): element must either be an argument name, or a tuple containing the argument name and the argument's default value. - Example - ------- + Examples + -------- >>> from hyperspy.events import Event >>> Event() @@ -167,7 +168,7 @@ def __init__(self, doc='', arguments=None): >>> e1 = Event() >>> e2 = Event(arguments=('arg1', ('arg2', None))) >>> e1.trigger(arg1=12, arg2=43, arg3='str', arg4=4.3) # Can trigger with whatever - >>> e2.trigger(arg1=11, arg2=22, arg3=3.4) + >>> e2.trigger(arg1=11, arg2=22, arg3=3.4) # doctest: +SKIP Traceback (most recent call last): ... TypeError: trigger() got an unexpected keyword argument 'arg3' @@ -205,32 +206,30 @@ def _trigger_maker(self, arguments): defaults.append(arg[1]) arg = arg[0] elif len(defaults) > 0: - raise SyntaxError( - "non-default argument follows default argument") + raise SyntaxError("non-default argument follows default argument") m = self._re_arg_name.match(arg) if m is None or m.end() != len(arg): raise ValueError("Argument name invalid: %s" % arg) - arguments = [a[0] if isinstance(a, (tuple, list)) - else a for a in arguments] + arguments = [a[0] if isinstance(a, (tuple, list)) else a for a in arguments] # Create the dynamic code: - arglist = ', '.join(arguments) - arg_pass = ', '.join([a + '=' + a for a in arguments]) - wrap_code = u""" + arglist = ", ".join(arguments) + arg_pass = ", ".join([a + "=" + a for a in arguments]) + wrap_code = """ @wraps(f) def trigger(self, %s): return f(%s) """ % (arglist, arg_pass) - wrap_code = wrap_code.replace(" ", "") # Remove indentation + wrap_code = wrap_code.replace(" ", "") # Remove indentation # Execute dynamic code: gl = dict(globals()) gl.update(locals()) - gl.update({'f': orig_f}) # Make sure it keeps the original! + gl.update({"f": orig_f}) # Make sure it keeps the original! exec(wrap_code, gl, locals()) - new_f = locals()['trigger'] + new_f = locals()["trigger"] # Replace the trigger function with the new one if defaults: new_f.__defaults__ = tuple(defaults) - new_f = new_f.__get__(self, self.__class__) # Bind method to self + new_f = new_f.__get__(self, self.__class__) # Bind method to self self.trigger = new_f @contextmanager @@ -240,17 +239,17 @@ def suppress(self): all events in the container. When the 'with' lock completes, the old suppression values will be restored. - Example - ------- - >>> with obj.events.myevent.suppress(): + Examples + -------- + >>> with obj.events.myevent.suppress(): # doctest: +SKIP ... # These would normally both trigger myevent: ... obj.val_a = a ... obj.val_b = b Trigger manually once: - >>> obj.events.myevent.trigger() + >>> obj.events.myevent.trigger() # doctest: +SKIP - See also + See Also -------- suppress_callback Events.suppress @@ -270,17 +269,17 @@ def suppress_callback(self, function): will trigger. When the 'with' lock completes, the old suppression value will be restored. - Example - ------- + Examples + -------- - >>> with obj.events.myevent.suppress_callback(f): + >>> with obj.events.myevent.suppress_callback(f): # doctest: +SKIP ... # Events will trigger as normal, but `f` will not be called ... obj.val_a = a ... obj.val_b = b >>> # Here, `f` will be called as before: - >>> obj.events.myevent.trigger() + >>> obj.events.myevent.trigger() # doctest: +SKIP - See also + See Also -------- suppress Events.suppress @@ -296,24 +295,23 @@ def suppress_callback(self, function): @property def connected(self): - """ Connected functions. - """ + """Connected functions.""" ret = set() ret.update(self._connected_all) ret.update(self._connected_some.keys()) ret.update(self._connected_map.keys()) return ret - def connect(self, function, kwargs='all'): + def connect(self, function, kwargs="all"): """ Connects a function to the event. - Arguments - --------- + Parameters + ---------- function : callable The function to call when the event triggers. - kwargs : {tuple or list, dictionary, 'all', 'auto'}, default "all" - If "all", all the trigger keyword arguments are passed to the + kwargs : tuple or list, dict, str {``'all' | ``'auto'``}, default ``"all"`` + If ``"all"``, all the trigger keyword arguments are passed to the function. If a list or tuple of strings, only those keyword arguments that are in the tuple or list are passed. If empty, no keyword argument is passed. If dictionary, the keyword arguments @@ -321,7 +319,7 @@ def connect(self, function, kwargs='all'): {"a" : "b"} maps the trigger argument "a" to the function argument "b". - See also + See Also -------- disconnect @@ -329,9 +327,8 @@ def connect(self, function, kwargs='all'): if not callable(function): raise TypeError("Only callables can be registered") if function in self.connected: - raise ValueError("Function %s already connected to %s." % - (function, self)) - if kwargs == 'auto': + raise ValueError("Function %s already connected to %s." % (function, self)) + if kwargs == "auto": spec = inspect.signature(function) _has_args = False _has_kwargs = False @@ -344,11 +341,13 @@ def connect(self, function, kwargs='all'): else: _normal_params.append(name) if _has_args and not _has_kwargs: - raise NotImplementedError("Connecting to variable argument " - "functions is not supported in auto " - "connection mode.") + raise NotImplementedError( + "Connecting to variable argument " + "functions is not supported in auto " + "connection mode." + ) elif _has_kwargs: - kwargs = 'all' + kwargs = "all" else: kwargs = _normal_params if kwargs == "all": @@ -377,7 +376,7 @@ def disconnect(self, function): If True, returns the kwargs that would reconnect the function as it was. - See also + See Also -------- connect suppress_callback @@ -389,8 +388,9 @@ def disconnect(self, function): elif function in self._connected_map: self._connected_map.pop(function) else: - raise ValueError("The %s function is not connected to %s." % - (function, self)) + raise ValueError( + "The %s function is not connected to %s." % (function, self) + ) def trigger(self, **kwargs): """ @@ -398,7 +398,7 @@ def trigger(self, **kwargs): Otherwise it calls all the connected functions with the arguments as specified when connected. - See also + See Also -------- suppress suppress_callback @@ -409,8 +409,7 @@ def trigger(self, **kwargs): # Work on copies of collections of connected functions. # Take copies initially, to ensure that all functions connected when # event triggered are called. - connected_all = self._connected_all.difference( - self._suppressed_callbacks) + connected_all = self._connected_all.difference(self._suppressed_callbacks) connected_some = list(self._connected_some.items()) connected_map = list(self._connected_map.items()) @@ -431,11 +430,12 @@ def __deepcopy__(self, memo): def __str__(self): if self.__doc__: - edoc = inspect.getdoc(self) or '' + edoc = inspect.getdoc(self) or "" doclines = edoc.splitlines() e_short = doclines[0] if len(doclines) > 0 else edoc - text = ("") + text = ( + "" + ) else: text = self.__repr__() return text @@ -445,7 +445,6 @@ def __repr__(self): class EventSuppressor(object): - """ Object to enforce a variety of suppression types simultaneously @@ -459,16 +458,16 @@ class EventSuppressor(object): Events where it is connected. * Any iterable collection of the above target types - Example - ------- - >>> es = EventSuppressor((event1, callback1), (event1, callback2)) - >>> es.add(event2, callback2) - >>> es.add(event3) - >>> es.add(events_container1) - >>> es.add(events_container2, callback1) - >>> es.add(event4, (events_container3, callback2)) - >>> - >>> with es.suppress(): + Examples + -------- + >>> es = EventSuppressor((event1, callback1), (event1, callback2)) # doctest: +SKIP + >>> es.add(event2, callback2) # doctest: +SKIP + >>> es.add(event3) # doctest: +SKIP + >>> es.add(events_container1) # doctest: +SKIP + >>> es.add(events_container2, callback1) # doctest: +SKIP + >>> es.add(event4, (events_container3, callback2)) # doctest: +SKIP + + >>> with es.suppress(): # doctest: +SKIP ... do_something() """ @@ -493,16 +492,17 @@ def _add_single(self, target): self._cms.append(cm) def _is_tuple_target(self, candidate): - v = (isinstance(candidate, Iterable) and - not isinstance(candidate, Events) and - len(candidate) == 2 and - isinstance(candidate[0], (Event, Events)) and - callable(candidate[1])) + v = ( + isinstance(candidate, Iterable) + and not isinstance(candidate, Events) + and len(candidate) == 2 + and isinstance(candidate[0], (Event, Events)) + and callable(candidate[1]) + ) return v def _is_target(self, candidate): - v = (isinstance(candidate, (Event, Events)) or - self._is_tuple_target(candidate)) + v = isinstance(candidate, (Event, Events)) or self._is_tuple_target(candidate) return v def add(self, *to_suppress): @@ -518,8 +518,11 @@ def add(self, *to_suppress): - Any iterable collection of the above target types """ # Remove useless layers of iterables: - while (isinstance(to_suppress, Iterable) and - not isinstance(to_suppress, Events) and len(to_suppress) == 1): + while ( + isinstance(to_suppress, Iterable) + and not isinstance(to_suppress, Events) + and len(to_suppress) == 1 + ): to_suppress = to_suppress[0] # If single target passed, add directly: if self._is_target(to_suppress): @@ -540,7 +543,7 @@ def suppress(self): all events added. When the 'with' lock completes, the old suppression values will be restored. - See also + See Also -------- Events.suppress Event.suppress @@ -551,7 +554,7 @@ def suppress(self): try: for cm in self._cms: cm.__enter__() - cms.append(cm) # Only add entered CMs to list + cms.append(cm) # Only add entered CMs to list yield finally: # Completed succefully or exception occured, unwind all diff --git a/hyperspy/exceptions.py b/hyperspy/exceptions.py index 105571dc1b..5d1fbf67a2 100644 --- a/hyperspy/exceptions.py +++ b/hyperspy/exceptions.py @@ -1,105 +1,44 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . # custom exceptions -class MountainsMapFileError(Exception): - - def __init__(self, msg = "Corrupt Mountainsmap file"): - self.error = msg - - def __str__(self): - return repr(self.error) - -class ByteOrderError(Exception): - - def __init__(self, order=''): - self.byte_order = order - - def __str__(self): - return repr(self.byte_order) - - -class DM3FileVersionError(Exception): - - def __init__(self, value=''): - self.dm3_version = value - - def __str__(self): - return repr(self.dm3_version) - - -class DM3TagError(Exception): - - def __init__(self, value=''): - self.dm3_tag = value - - def __str__(self): - return repr(self.dm3_tag) - - -class DM3DataTypeError(Exception): - - def __init__(self, value=''): - self.dm3_dtype = value - - def __str__(self): - return repr(self.dm3_dtype) - - -class DM3TagTypeError(Exception): - - def __init__(self, value=''): - self.dm3_tagtype = value - - def __str__(self): - return repr(self.dm3_tagtype) - - -class DM3TagIDError(Exception): - - def __init__(self, value=''): - self.dm3_tagID = value - - def __str__(self): - return repr(self.dm3_tagID) - +# Unused class ImageIDError(Exception): - - def __init__(self, value=''): + def __init__(self, value=""): self.image_id = value def __str__(self): return repr(self.image_id) +# Unused class ImageModeError(Exception): - - def __init__(self, value=''): + def __init__(self, value=""): self.mode = value def __str__(self): return repr(self.mode) +# Unused class ShapeError(Exception): - def __init__(self, value): self.error = value.shape @@ -107,8 +46,8 @@ def __str__(self): return repr(self.error) +# Unused class NoInteractiveError(Exception): - def __init__(self): self.error = "HyperSpy must run in interactive mode to use this feature" @@ -117,21 +56,21 @@ def __str__(self): class WrongObjectError(Exception): - def __init__(self, is_str, must_be_str): - self.error = ("A object of type %s was given, but a %s" % ( - is_str, must_be_str) + " object is required") + self.error = ( + "A object of type %s was given, but a %s" % (is_str, must_be_str) + + " object is required" + ) def __str__(self): return repr(self.error) class MissingParametersError(Exception): - def __init__(self, parameters): - par_str = '' + par_str = "" for par in parameters: - par_str += '%s,' % par + par_str += "%s," % par self.error = "The following parameters are missing: %s" % par_str # Remove the last comma self.error = self.error[:-1] @@ -141,7 +80,6 @@ def __str__(self): class DataDimensionError(Exception): - def __init__(self, msg): self.msg = msg @@ -150,58 +88,71 @@ def __str__(self): class SignalDimensionError(Exception): - def __init__(self, output_dimension, expected_output_dimension): self.output_dimension = output_dimension self.expected_output_dimension = expected_output_dimension - self.msg = 'output dimension=%i, %i expected' % ( - self.output_dimension, self.expected_output_dimension) + self.msg = "output dimension=%i, %i expected" % ( + self.output_dimension, + self.expected_output_dimension, + ) def __str__(self): return repr(self.msg) class NavigationDimensionError(Exception): - - def __init__(self, - navigation_dimension, - expected_navigation_dimension): + def __init__(self, navigation_dimension, expected_navigation_dimension): self.navigation_dimension = navigation_dimension - self.expected_navigation_dimension = \ - expected_navigation_dimension - self.msg = 'navigation dimension=%i, %s expected' % ( - self.navigation_dimension, self.expected_navigation_dimension) + self.expected_navigation_dimension = expected_navigation_dimension + self.msg = "navigation dimension=%i, %s expected" % ( + self.navigation_dimension, + self.expected_navigation_dimension, + ) def __str__(self): return repr(self.msg) class SignalSizeError(Exception): - def __init__(self, signal_size, expected_signal_size): self.signal_size = signal_size self.expected_signal_size = expected_signal_size - self.msg = 'signal_size=%i, %i expected' % ( - self.signal_size, self.expected_signal_size) + self.msg = "signal_size=%i, %i expected" % ( + self.signal_size, + self.expected_signal_size, + ) def __str__(self): return repr(self.msg) class NavigationSizeError(Exception): - def __init__(self, navigation_size, expected_navigation_size): self.navigation_size = navigation_size self.expected_navigation_size = expected_navigation_size - self.msg = 'navigation_size =%i, %i expected' % ( - self.navigation_size, self.expected_navigation_size) + self.msg = "navigation_size =%i, %i expected" % ( + self.navigation_size, + self.expected_navigation_size, + ) class VisibleDeprecationWarning(UserWarning): - """Visible deprecation warning. By default, python will not show deprecation warnings, so this class provides a visible one. """ + pass + + +class LazyCupyConversion(Exception): + def __init__(self): + self.error = ( + "Automatically converting data to cupy array is not supported " + "for lazy signals. Read the corresponding section in the user " + "guide for more information on how to use GPU with lazy signals." + ) + + def __str__(self): + return repr(self.error) diff --git a/hyperspy/extensions.py b/hyperspy/extensions.py index 2bc368e47e..bb1332bd8e 100644 --- a/hyperspy/extensions.py +++ b/hyperspy/extensions.py @@ -1,34 +1,36 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import logging import copy -import yaml - +import json +import logging from pathlib import Path -import importlib_metadata as metadata +from urllib.parse import urlparse +from urllib.request import url2pathname +import importlib_metadata as metadata +import yaml _logger = logging.getLogger(__name__) # Load hyperspy's own extensions _ext_f = Path(__file__).resolve().parent.joinpath("hyperspy_extension.yaml") -with open(_ext_f, 'r') as stream: +with open(_ext_f, "r") as stream: EXTENSIONS = yaml.safe_load(stream) EXTENSIONS["GUI"]["widgets"] = {} @@ -37,37 +39,61 @@ ALL_EXTENSIONS = copy.deepcopy(EXTENSIONS) _external_extensions = [ - entry_point - for entry_point in metadata.entry_points(group="hyperspy.extensions")] + entry_point for entry_point in metadata.entry_points(group="hyperspy.extensions") +] for _external_extension in _external_extensions: _logger.info("Enabling extension %s" % _external_extension.name) - _files = [file for file in _external_extension.dist.files - if "hyperspy_extension.yaml" in str(file)] + _files = [ + file + for file in _external_extension.dist.files + if "hyperspy_extension.yaml" in str(file) + ] - if _files: - _path = _files.pop() - with _path.locate().open() as stream: + if not _files: # pragma: no cover + # Editable installs for pyproject.toml based builds + # https://peps.python.org/pep-0610/#example-pip-commands-and-their-effect-on-direct-url-json + # https://peps.python.org/pep-0660/#frontend-requirements + _files = [ + file + for file in _external_extension.dist.files + if "direct_url.json" in str(file) + ] + with _files[0].locate().open() as json_data: + _path = url2pathname(urlparse(json.load(json_data)["url"]).path) + _path = Path(_path) / _external_extension.name / "hyperspy_extension.yaml" + else: + _path = _files.pop().locate() + + if _path: + with open(str(_path)) as stream: _external_extension = yaml.safe_load(stream) if "signals" in _external_extension: ALL_EXTENSIONS["signals"].update(_external_extension["signals"]) if "components1D" in _external_extension: ALL_EXTENSIONS["components1D"].update( - _external_extension["components1D"]) + _external_extension["components1D"] + ) if "components2D" in _external_extension: ALL_EXTENSIONS["components2D"].update( - _external_extension["components2D"]) + _external_extension["components2D"] + ) if "GUI" in _external_extension: if "toolkeys" in _external_extension["GUI"]: ALL_EXTENSIONS["GUI"]["toolkeys"].extend( - _external_extension["GUI"]["toolkeys"]) + _external_extension["GUI"]["toolkeys"] + ) if "widgets" in _external_extension["GUI"]: for toolkit, specs in _external_extension["GUI"]["widgets"].items(): if toolkit not in ALL_EXTENSIONS["GUI"]["widgets"]: ALL_EXTENSIONS["GUI"]["widgets"][toolkit] = {} ALL_EXTENSIONS["GUI"]["widgets"][toolkit].update(specs) - else: + else: # pragma: no cover + # When the "hyperspy_extension.yaml" is missing from the package _logger.error( - "Failed to load hyperspy extension from {0}. Please report this issue to the {0} developers".format(_external_extension_mod)) + "Failed to load hyperspy extension from {0}. Please report this issue to the {0} developers".format( + _external_extension.name + ) + ) diff --git a/hyperspy/external/astropy/bayesian_blocks.py b/hyperspy/external/astropy/bayesian_blocks.py index 73e169abb0..ddeabbedb3 100644 --- a/hyperspy/external/astropy/bayesian_blocks.py +++ b/hyperspy/external/astropy/bayesian_blocks.py @@ -1,6 +1,6 @@ """ -Ported first from the astroML project: http://astroml.org/ -Ported again from the astropy project: http://astropy.org/ +Ported first from the astroML project: https://astroml.org/ +Ported again from the astropy project: https://astropy.org/ Bayesian Blocks for Time Series Analysis ======================================== @@ -35,8 +35,8 @@ References ---------- -.. [1] http://adsabs.harvard.edu/abs/2012arXiv1207.5578S -.. [2] http://astroml.org/ https://github.com//astroML/astroML/ +.. [1] https://adsabs.harvard.edu/abs/2012arXiv1207.5578S +.. [2] https://astroml.org/ https://github.com//astroML/astroML/ """ import warnings @@ -136,7 +136,7 @@ def bayesian_blocks(t, x=None, sigma=None, fitness="events", **kwargs): References ---------- .. [1] Scargle, J et al. (2012) - http://adsabs.harvard.edu/abs/2012arXiv1207.5578S + https://adsabs.harvard.edu/abs/2012arXiv1207.5578S See Also -------- @@ -193,7 +193,7 @@ class FitnessFunc: References ---------- .. [1] Scargle, J et al. (2012) - http://adsabs.harvard.edu/abs/2012arXiv1207.5578S + https://adsabs.harvard.edu/abs/2012arXiv1207.5578S """ def __init__(self, p0=0.05, gamma=None, ncp_prior=None): diff --git a/hyperspy/external/astropy/histogram.py b/hyperspy/external/astropy/histogram.py index 3c0598dc0d..99d6f0a41a 100644 --- a/hyperspy/external/astropy/histogram.py +++ b/hyperspy/external/astropy/histogram.py @@ -1,6 +1,6 @@ """ -Ported first from the astroML project: http://astroml.org/ -Ported again from the astropy project: http://astropy.org/ +Ported first from the astroML project: https://astroml.org/ +Ported again from the astropy project: https://astropy.org/ Tools for working with distributions """ @@ -154,6 +154,8 @@ def eval(self, M): smaller values indicate a better fit. """ + if not np.isscalar(M): + M = M[0] M = int(M) if M <= 0: diff --git a/hyperspy/external/matplotlib/_api.py b/hyperspy/external/matplotlib/_api.py new file mode 100644 index 0000000000..5176d3ec9e --- /dev/null +++ b/hyperspy/external/matplotlib/_api.py @@ -0,0 +1,61 @@ +import inspect + + +def nargs_error(name, takes, given): + """Generate a TypeError to be raised by function calls with wrong arity.""" + return TypeError(f"{name}() takes {takes} positional arguments but " + f"{given} were given") + + +def check_in_list(values, /, *, _print_supported_values=True, **kwargs): + """ + For each *key, value* pair in *kwargs*, check that *value* is in *values*; + if not, raise an appropriate ValueError. + + Parameters + ---------- + values : iterable + Sequence of values to check on. + _print_supported_values : bool, default: True + Whether to print *values* when raising ValueError. + **kwargs : dict + *key, value* pairs as keyword arguments to find in *values*. + + Raises + ------ + ValueError + If any *value* in *kwargs* is not found in *values*. + + Examples + -------- + >>> _api.check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg) + """ + if not kwargs: + raise TypeError("No argument to check!") + for key, val in kwargs.items(): + if val not in values: + msg = f"{val!r} is not a valid value for {key}" + if _print_supported_values: + msg += f"; supported values are {', '.join(map(repr, values))}" + raise ValueError(msg) + + +def check_getitem(mapping, /, **kwargs): + """ + *kwargs* must consist of a single *key, value* pair. If *key* is in + *mapping*, return ``mapping[value]``; else, raise an appropriate + ValueError. + + Examples + -------- + >>> _api.check_getitem({"foo": "bar"}, arg=arg) + """ + if len(kwargs) != 1: + raise ValueError("check_getitem takes a single keyword argument") + (k, v), = kwargs.items() + try: + return mapping[v] + except KeyError: + raise ValueError( + f"{v!r} is not a valid value for {k}; supported values are " + f"{', '.join(map(repr, mapping))}") from None diff --git a/hyperspy/external/matplotlib/collections.py b/hyperspy/external/matplotlib/collections.py new file mode 100644 index 0000000000..632a0bfafd --- /dev/null +++ b/hyperspy/external/matplotlib/collections.py @@ -0,0 +1,484 @@ +from matplotlib import artist, path as mpath, transforms +from hyperspy.external.matplotlib.path import Path +from matplotlib.collections import Collection +from matplotlib.cbook import is_math_text +from matplotlib.textpath import TextPath, TextToPath +from matplotlib.font_manager import FontProperties +import numpy as np +import math + + +class _CollectionWithSizes(Collection): + """ + Base class for collections that have an array of sizes. + """ + _factor = 1.0 + + def __init__(self, sizes, units='points', **kwargs): + """ + Parameters + ---------- + sizes : array-like + The lengths of the first axes (e.g., major axis lengths). + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + """ + super().__init__(**kwargs) + self._set_sizes(sizes) + self._units = units + self.set_transform(transforms.IdentityTransform()) + self._transforms = np.empty((0, 3, 3)) + self._paths = [self._path_generator()] + + def get_sizes(self): + """ + Return the sizes ('areas') of the elements in the collection. + + Returns + ------- + array + The 'area' of each element. + """ + return self._sizes + + def _set_sizes(self, sizes): + self._sizes = self._factor * np.asarray(sizes).ravel() + + def set_sizes(self, sizes): + """Set the sizes of the element in the collection.""" + self._set_sizes(sizes) + self.stale = True + + def _set_transforms(self): + """Calculate transforms immediately before drawing.""" + ax = self.axes + fig = self.figure + + if self._units == 'xy': + sc = 1 + elif self._units == 'x': + sc = ax.bbox.width / ax.viewLim.width + elif self._units == 'y': + sc = ax.bbox.height / ax.viewLim.height + elif self._units == 'inches': + sc = fig.dpi + elif self._units == 'points': + sc = fig.dpi / 72.0 + elif self._units == 'width': + sc = ax.bbox.width + elif self._units == 'height': + sc = ax.bbox.height + elif self._units == 'dots': + sc = 1.0 + else: + raise ValueError(f'Unrecognized units: {self._units!r}') + + self._transforms = np.zeros((len(self._sizes), 3, 3)) + sizes = self._sizes * sc + self._transforms[:, 0, 0] = sizes + self._transforms[:, 1, 1] = sizes + self._transforms[:, 2, 2] = 1.0 + + _affine = transforms.Affine2D + if self._units == 'xy': + m = ax.transData.get_affine().get_matrix().copy() + m[:2, 2:] = 0 + self.set_transform(_affine(m)) + + @artist.allow_rasterization + def draw(self, renderer): + self._set_transforms() + super().draw(renderer) + + +class CircleCollection(_CollectionWithSizes): + """A collection of circles, drawn using splines.""" + + _factor = 0.5 + _path_generator = mpath.Path.unit_circle + + +class _CollectionWithWidthAngle(Collection): + """ + Base class for collections that have an array of widths and angles + """ + + _factor = 0.5 + + def __init__(self, widths, angles, units='points', **kwargs): + """ + Parameters + ---------- + widths : array-like + The lengths of the first axes (e.g., major axis lengths). + angles : array-like, optional + The angles of the first axes, degrees CCW from the x-axis. + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + """ + super().__init__(**kwargs) + self._set_widths(widths) + self._set_angles(angles) + self._units = units + self.set_transform(transforms.IdentityTransform()) + self._transforms = np.empty((0, 3, 3)) + self._paths = [self._path_generator()] + + def _set_transforms(self): + """Calculate transforms immediately before drawing.""" + + ax = self.axes + fig = self.figure + + if self._units == 'xy': + sc = 1 + elif self._units == 'x': + sc = ax.bbox.width / ax.viewLim.width + elif self._units == 'y': + sc = ax.bbox.height / ax.viewLim.height + elif self._units == 'inches': + sc = fig.dpi + elif self._units == 'points': + sc = fig.dpi / 72.0 + elif self._units == 'width': + sc = ax.bbox.width + elif self._units == 'height': + sc = ax.bbox.height + elif self._units == 'dots': + sc = 1.0 + else: + raise ValueError(f'Unrecognized units: {self._units!r}') + + self._transforms = np.zeros((len(self._widths), 3, 3)) + widths = self._widths * sc + heights = self._heights * sc + sin_angle = np.sin(self._angles) + cos_angle = np.cos(self._angles) + self._transforms[:, 0, 0] = widths * cos_angle + self._transforms[:, 0, 1] = heights * -sin_angle + self._transforms[:, 1, 0] = widths * sin_angle + self._transforms[:, 1, 1] = heights * cos_angle + self._transforms[:, 2, 2] = 1.0 + + + _affine = transforms.Affine2D + if self._units == 'xy': + m = ax.transData.get_affine().get_matrix().copy() + m[:2, 2:] = 0 + self.set_transform(_affine(m)) + else: # handle different origins + m = ax.transData.get_affine().get_matrix().copy() + m[:2, 2:] = 0 + m = np.sign(m) + self.set_transform(_affine(m)) + + def _set_widths(self, widths): + self._widths = self._factor * np.asarray(widths).ravel() + self._heights = self._factor * np.asarray(widths).ravel() + + def _set_angles(self, angles): + self._angles = np.deg2rad(angles).ravel() + + def set_widths(self, widths): + """Set the lengths of the first axes (e.g., major axis lengths).""" + self._set_widths(widths) + self.stale = True + + def set_angles(self, angles): + """Set the angles of the first axes, degrees CCW from the x-axis.""" + self._set_angles(angles) + self.stale = True + + @artist.allow_rasterization + def draw(self, renderer): + self._set_transforms() + super().draw(renderer) + + +class _CollectionWithWidthHeightAngle(_CollectionWithWidthAngle): + """ + Base class for collections that have an array of widths, heights and angles + """ + + def __init__(self, widths, heights, angles, units='points', **kwargs): + """ + Parameters + ---------- + widths : array-like + The lengths of the first axes (e.g., major axis lengths). + heights : array-like + The lengths of second axes. + angles : array-like, optional + The angles of the first axes, degrees CCW from the x-axis. + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + """ + super().__init__(widths=widths, angles=angles, units=units, **kwargs) + self._set_heights(heights) + + def _set_heights(self, heights): + self._heights = self._factor * np.asarray(heights).ravel() + + def _set_widths(self, widths): + self._widths = self._factor * np.asarray(widths).ravel() + + def set_heights(self, heights): + """Set the lengths of second axes..""" + self._set_heights(heights) + self.stale = True + + +class EllipseCollection(_CollectionWithWidthHeightAngle): + """ + A collection of ellipses, drawn using splines. + + Parameters + ---------- + widths : array-like + The lengths of the first axes (e.g., major axis lengths). + heights : array-like + The lengths of second axes. + angles : array-like + The angles of the first axes, degrees CCW from the x-axis. + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + """ + _factor = 0.5 + _path_generator = mpath.Path.unit_circle + + +class RectangleCollection(_CollectionWithWidthHeightAngle): + """ + A collection of rectangles, drawn using splines. + + Parameters + ---------- + widths : array-like + The lengths of the first axes (e.g., major axis lengths). + heights : array-like + The lengths of second axes. + angles : array-like + The angles of the first axes, degrees CCW from the x-axis. + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + + """ + _factor = 0.5 + _path_generator = Path.unit_rectangle + + +class SquareCollection(_CollectionWithWidthAngle): + """ + A collection of rectangles, drawn using splines. + + Parameters + ---------- + widths : array-like + The lengths of the first axes (e.g., major axis lengths). + angles : array-like + The angles of the first axes, degrees CCW from the x-axis. + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + + """ + _factor = 0.5 + _path_generator = Path.unit_rectangle + + +class TextCollection(Collection): + + _factor = 1.0 + + def __init__(self, + texts, + sizes=None, + rotation=0, + horizontalalignment='center', + verticalalignment='center', + prop=None, + usetex=False, + **kwargs): + """ + Parameters + ---------- + texts : array-like + The texts of the collection. + angles : array-like + The angles of the first axes, degrees CCW from the x-axis. + **kwargs + Forwarded to `Collection`. + """ + super().__init__(**kwargs) + if sizes is None: + sizes = [1] + self._sizes = sizes + self._horizontalalignment = horizontalalignment + self._verticalalignment = verticalalignment + self._transforms = np.empty((0, 3, 3)) # for rotating and shifting the text + self.rotation = rotation + self.usetex = usetex + if prop is None: + self.prop = FontProperties() + else: + self.prop = prop + self._set_texts(texts) + + def set_horizontalalignment(self, horizontalalignment): + self._horizontalalignment = horizontalalignment + self.set_rotation_center_and_sizes(self.figure.dpi) + self.stale = True + + def set_verticalalignment(self, verticalalignment): + self._verticalalignment = verticalalignment + self.set_rotation_center_and_sizes(self.figure.dpi) + self.stale = True + + def _set_texts(self, texts): + self._texts = texts + self._generate_path_from_text() + + def set_texts(self, texts): + self._set_texts(texts) + self.stale = True + + def get_texts(self, texts): + return self._texts + + def set_sizes(self, sizes, dpi=72.0): + self._sizes = sizes + self.set_rotation_center_and_sizes(dpi) + self.stale = True + + def set_rotation(self, rotation): + self.rotation = rotation + self.set_rotation_center_and_sizes(self.figure.dpi) + self.stale = True + + def set_rotation_center_and_sizes(self, dpi=72.0): + """ + Calculate transforms immediately before drawing. + """ + self._transforms = np.zeros((len(self._texts), 3, 3)) + scales = np.sqrt(self._sizes) * dpi / 72.0 * self._factor + scales = [scales[i % len(self._sizes)] for i in range(len(self._texts))] + self._transforms[:, 0, 0] = scales # set the size of the text in x + self._transforms[:, 1, 1] = scales # set the size of the text in y + self._transforms[:, 2, 2] = 1.0 + + text_to_path = TextToPath() + for i, t in enumerate(self._texts): + width, height, decent = text_to_path.get_text_width_height_descent( + t, + prop=self.prop, + ismath=self.usetex or is_math_text(t), + ) + + translation_ = [0, 0] + if self._horizontalalignment == 'center': + translation_[0] = -width/2 * self._transforms[i][0, 0] + elif self._horizontalalignment == 'left': + translation_[0] = 0 + elif self._horizontalalignment == 'right': + translation_[0] = -width * self._transforms[i][0, 0] + else: + raise ValueError(f'Unrecognized horizontalalignment: {self._horizontalalignment!r}') + + if self._verticalalignment == 'center': + translation_[1] = -height/2 * self._transforms[i][0, 0] + elif self._verticalalignment == 'baseline': + translation_[1] = -decent * self._transforms[i][0, 0] + elif self._verticalalignment == 'center_baseline': + translation_[1] = -(height - decent)/2 * self._transforms[i][0, 0] + elif self._verticalalignment == 'bottom': + translation_[1] = 0 + elif self._verticalalignment == 'top': + translation_[1] = -height * self._transforms[i][0, 0] + else: + raise ValueError(f'Unrecognized verticalalignment: {self._verticalalignment!r}') + translation = [0, 0] + translation[1] = math.sin(self.rotation)*translation_[0] + math.cos(self.rotation)*translation_[1] + translation[0] = math.cos(self.rotation)*translation_[0] - math.sin(self.rotation)*translation_[1] + self._transforms[i] = translate_matrix(rotate_matrix(self._transforms[i], self.rotation), + translation[0], + translation[1]) + self.stale = True + + def _generate_path_from_text(self): + # For each TextPath, the position is at (0, 0) because the position + # will be given by the offsets values + self._paths = [TextPath((0, 0), text, prop=self.prop, usetex=self.usetex) for text in self._texts] + + @artist.allow_rasterization + def draw(self, renderer): + self.set_rotation_center_and_sizes(self.figure.dpi) + super().draw(renderer) + + +def rotate_matrix(mat, theta): + a = math.cos(theta) + b = math.sin(theta) + mtx = mat + # Operating and assigning one scalar at a time is much faster. + (xx, xy, x0), (yx, yy, y0), _ = mtx.tolist() + # mtx = [[a -b 0], [b a 0], [0 0 1]] * mtx + mtx[0, 0] = a * xx - b * yx + mtx[0, 1] = a * xy - b * yy + mtx[0, 2] = a * x0 - b * y0 + mtx[1, 0] = b * xx + a * yx + mtx[1, 1] = b * xy + a * yy + mtx[1, 2] = b * x0 + a * y0 + return mtx + + +def translate_matrix(mat, tx, ty): + mat[0, 2] += tx + mat[1, 2] += ty + return mat diff --git a/hyperspy/external/matplotlib/path.py b/hyperspy/external/matplotlib/path.py new file mode 100644 index 0000000000..d7286e6020 --- /dev/null +++ b/hyperspy/external/matplotlib/path.py @@ -0,0 +1,13 @@ +from matplotlib.path import Path as MPLPath + +class Path(MPLPath): + _unit_rectangle = None + @classmethod + def unit_rectangle(cls): + """ + Return a `Path` instance of the unit rectangle from (0, 0) to (1, 1). + """ + if cls._unit_rectangle is None: + cls._unit_rectangle = cls([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, 1]], + closed=True, readonly=True) + return cls._unit_rectangle \ No newline at end of file diff --git a/hyperspy/external/matplotlib/quiver.py b/hyperspy/external/matplotlib/quiver.py new file mode 100644 index 0000000000..c132b1fac2 --- /dev/null +++ b/hyperspy/external/matplotlib/quiver.py @@ -0,0 +1,383 @@ +import matplotlib.collections as mcollections +from matplotlib import transforms +import matplotlib.artist as martist +import hyperspy.external.matplotlib._api as _api +from matplotlib import cbook +import math + +import numpy as np +from numpy import ma + + + +def _parse_args(*args, caller_name='function'): + """ + Helper function to parse positional parameters for colored vector plots. + + This is currently used for Quiver and Barbs. + + Parameters + ---------- + *args : list + list of 2-5 arguments. Depending on their number they are parsed to:: + + U, V + U, V, C + X, Y, U, V + X, Y, U, V, C + + caller_name : str + Name of the calling method (used in error messages). + """ + X = Y = C = None + + nargs = len(args) + if nargs == 2: + # The use of atleast_1d allows for handling scalar arguments while also + # keeping masked arrays + U, V = np.atleast_1d(*args) + elif nargs == 3: + U, V, C = np.atleast_1d(*args) + elif nargs == 4: + X, Y, U, V = np.atleast_1d(*args) + elif nargs == 5: + X, Y, U, V, C = np.atleast_1d(*args) + else: + raise _api.nargs_error(caller_name, takes="from 2 to 5", given=nargs) + + nr, nc = (1, U.shape[0]) if U.ndim == 1 else U.shape + + if X is not None: + X = X.ravel() + Y = Y.ravel() + if len(X) == nc and len(Y) == nr: + X, Y = [a.ravel() for a in np.meshgrid(X, Y)] + elif len(X) != len(Y): + raise ValueError('X and Y must be the same size, but ' + f'X.size is {X.size} and Y.size is {Y.size}.') + else: + indexgrid = np.meshgrid(np.arange(nc), np.arange(nr)) + X, Y = [np.ravel(a) for a in indexgrid] + # Size validation for U, V, C is left to the set_UVC method. + return X, Y, U, V, C + + +class Quiver(mcollections.PolyCollection): + """ + Specialized PolyCollection for arrows. + + The API methods are set_UVC(), set_U(), set_V() and set_C(), which + can be used to change the size, orientation, and color of the + arrows; their locations are fixed when the class is + instantiated. Possibly these methods will be useful + in animations. + + Much of the work in this class is done in the draw() + method so that as much information as possible is available + about the plot. In subsequent draw() calls, recalculation + is limited to things that might have changed, so there + should be no performance penalty from putting the calculations + in the draw() method. + """ + + _PIVOT_VALS = ('tail', 'middle', 'tip') + + def __init__(self, *args, + scale=None, headwidth=3, headlength=5, headaxislength=4.5, + minshaft=1, minlength=1, units='width', scale_units=None, + angles='uv', width=None, color='k', pivot='tail', **kwargs): + """ + The constructor takes one required argument, an Axes + instance, followed by the args and kwargs described + by the following pyplot interface documentation: + %s + """ + X, Y, U, V, C = _parse_args(*args, caller_name='quiver') + self.X = X + self.Y = Y + self.scale = scale + self.headwidth = headwidth + self.headlength = float(headlength) + self.headaxislength = headaxislength + self.minshaft = minshaft + self.minlength = minlength + self.units = units + self.scale_units = scale_units + self.angles = angles + self.width = width + + if pivot.lower() == 'mid': + pivot = 'middle' + self.pivot = pivot.lower() + _api.check_in_list(self._PIVOT_VALS, pivot=self.pivot) + + kwargs.setdefault('facecolors', color) + kwargs.setdefault('linewidths', (0,)) + kwargs.setdefault('offset_transform', kwargs.pop('transform', None)) + super().__init__([], offsets=self.XY, closed=False, **kwargs) + self.polykw = kwargs + self.set_UVC(U, V, C) + self._dpi_at_last_init = None + + def _init(self): + """ + Initialization delayed until first draw; + allow time for axes setup. + """ + # It seems that there are not enough event notifications + # available to have this work on an as-needed basis at present. + if True: # self._dpi_at_last_init != self.axes.figure.dpi + trans = self._set_transform() + self.span = trans.inverted().transform_bbox(self.axes.bbox).width + if self.width is None: + sn = np.clip(math.sqrt(self.N), 8, 25) + self.width = 0.06 * self.span / sn + + # _make_verts sets self.scale if not already specified + if (self._dpi_at_last_init != self.axes.figure.dpi + and self.scale is None): + self._make_verts(self.U, self.V, self.angles) + + self._dpi_at_last_init = self.axes.figure.dpi + + @property + def N(self): + return len(self.X) + + @property + def XY(self): + return np.column_stack((self.X, self.Y)) + + def get_datalim(self, transData): + trans = self.get_transform() + offset_trf = self.get_offset_transform() + full_transform = (trans - transData) + (offset_trf - transData) + XY = full_transform.transform(self.XY) + bbox = transforms.Bbox.null() + bbox.update_from_data_xy(XY, ignore=True) + return bbox + + @martist.allow_rasterization + def draw(self, renderer): + self._init() + verts = self._make_verts(self.U, self.V, self.angles) + self.set_verts(verts, closed=False) + super().draw(renderer) + self.stale = False + + def set_U(self, U): + """Set x direction components of the arrow vectors.""" + self.set_UVC(U, None, None) + + def set_V(self, V): + """Set y direction components of the arrow vectors.""" + self.set_UVC(None, V, None) + + def set_C(self, C): + """Set the arrow colors.""" + self.set_UVC(None, None, C) + + def set_UVC(self, U, V, C=None): + """ + Set the U, V (x and y direction components of the arrow vectors) and + C (arrow colors) values of the arrows. + + Parameters + ---------- + U : ArrayLike | None + The x direction components of the arrows. If None it is unchanged. + V : ArrayLike | None + The y direction components of the arrows. If None it is unchanged. + C : ArrayLike | None, optional + The arrow colors. The default is None. + """ + # We need to ensure we have a copy, not a reference + # to an array that might change before draw(). + U = self.U if U is None else ma.masked_invalid(U, copy=True).ravel() + V = self.V if V is None else ma.masked_invalid(V, copy=True).ravel() + if C is not None: + C = ma.masked_invalid(C, copy=True).ravel() + for name, var in zip(('U', 'V', 'C'), (U, V, C)): + if not (var is None or var.size == self.N or var.size == 1): + raise ValueError(f'Argument {name} has a size {var.size}' + f' which does not match {self.N},' + ' the number of arrow positions') + + mask = ma.mask_or(U.mask, V.mask, copy=False, shrink=True) + if C is not None: + mask = ma.mask_or(mask, C.mask, copy=False, shrink=True) + if mask is ma.nomask: + C = C.filled() + else: + C = ma.array(C, mask=mask, copy=False) + self.U = U.filled(1) + self.V = V.filled(1) + self.Umask = mask + if C is not None: + self.set_array(C) + self.stale = True + + def set_offsets(self, xy): + """ + Set the offsets for the arrows. This saves the offsets passed + in and masks them as appropriate for the existing X/Y data. + + Parameters + ---------- + xy : sequence of pairs of floats + """ + self.X, self.Y = xy[:, 0], xy[:, 1] + super().set_offsets(xy) + self.stale = True + + def _dots_per_unit(self, units): + """Return a scale factor for converting from units to pixels.""" + bb = self.axes.bbox + vl = self.axes.viewLim + return _api.check_getitem({ + 'x': bb.width / vl.width, + 'y': bb.height / vl.height, + 'xy': np.hypot(*bb.size) / np.hypot(*vl.size), + 'width': bb.width, + 'height': bb.height, + 'dots': 1., + 'inches': self.axes.figure.dpi, + }, units=units) + + def _set_transform(self): + """ + Set the PolyCollection transform to go + from arrow width units to pixels. + """ + dx = self._dots_per_unit(self.units) + self._trans_scale = dx # pixels per arrow width unit + trans = transforms.Affine2D().scale(dx) + self.set_transform(trans) + return trans + + def _angles_lengths(self, U, V, eps=1): + xy = self.axes.transData.transform(self.XY) + uv = np.column_stack((U, V)) + xyp = self.axes.transData.transform(self.XY + eps * uv) + dxy = xyp - xy + angles = np.arctan2(dxy[:, 1], dxy[:, 0]) + lengths = np.hypot(*dxy.T) / eps + return angles, lengths + + def _make_verts(self, U, V, angles): + uv = (U + V * 1j) + str_angles = angles if isinstance(angles, str) else '' + if str_angles == 'xy' and self.scale_units == 'xy': + # Here eps is 1 so that if we get U, V by diffing + # the X, Y arrays, the vectors will connect the + # points, regardless of the axis scaling (including log). + angles, lengths = self._angles_lengths(U, V, eps=1) + elif str_angles == 'xy' or self.scale_units == 'xy': + # Calculate eps based on the extents of the plot + # so that we don't end up with roundoff error from + # adding a small number to a large. + eps = np.abs(self.axes.dataLim.extents).max() * 0.001 + angles, lengths = self._angles_lengths(U, V, eps=eps) + if str_angles and self.scale_units == 'xy': + a = lengths + else: + a = np.abs(uv) + if self.scale is None: + sn = max(10, math.sqrt(self.N)) + if self.Umask is not ma.nomask: + amean = a[~self.Umask].mean() + else: + amean = a.mean() + # crude auto-scaling + # scale is typical arrow length as a multiple of the arrow width + scale = 1.8 * amean * sn / self.span + if self.scale_units is None: + if self.scale is None: + self.scale = scale + widthu_per_lenu = 1.0 + else: + if self.scale_units == 'xy': + dx = 1 + else: + dx = self._dots_per_unit(self.scale_units) + widthu_per_lenu = dx / self._trans_scale + if self.scale is None: + self.scale = scale * widthu_per_lenu + length = a * (widthu_per_lenu / (self.scale * self.width)) + X, Y = self._h_arrows(length) + if str_angles == 'xy': + theta = angles + elif str_angles == 'uv': + theta = np.angle(uv) + else: + theta = ma.masked_invalid(np.deg2rad(angles)).filled(0) + theta = theta.reshape((-1, 1)) # for broadcasting + xy = (X + Y * 1j) * np.exp(1j * theta) * self.width + XY = np.stack((xy.real, xy.imag), axis=2) + if self.Umask is not ma.nomask: + XY = ma.array(XY) + XY[self.Umask] = ma.masked + # This might be handled more efficiently with nans, given + # that nans will end up in the paths anyway. + + return XY + + def _h_arrows(self, length): + """Length is in arrow width units.""" + # It might be possible to streamline the code + # and speed it up a bit by using complex (x, y) + # instead of separate arrays; but any gain would be slight. + minsh = self.minshaft * self.headlength + N = len(length) + length = length.reshape(N, 1) + # This number is chosen based on when pixel values overflow in Agg + # causing rendering errors + # length = np.minimum(length, 2 ** 16) + np.clip(length, 0, 2 ** 16, out=length) + # x, y: normal horizontal arrow + x = np.array([0, -self.headaxislength, + -self.headlength, 0], + np.float64) + x = x + np.array([0, 1, 1, 1]) * length + y = 0.5 * np.array([1, 1, self.headwidth, 0], np.float64) + y = np.repeat(y[np.newaxis, :], N, axis=0) + # x0, y0: arrow without shaft, for short vectors + x0 = np.array([0, minsh - self.headaxislength, + minsh - self.headlength, minsh], np.float64) + y0 = 0.5 * np.array([1, 1, self.headwidth, 0], np.float64) + ii = [0, 1, 2, 3, 2, 1, 0, 0] + X = x[:, ii] + Y = y[:, ii] + Y[:, 3:-1] *= -1 + X0 = x0[ii] + Y0 = y0[ii] + Y0[3:-1] *= -1 + shrink = length / minsh if minsh != 0. else 0. + X0 = shrink * X0[np.newaxis, :] + Y0 = shrink * Y0[np.newaxis, :] + short = np.repeat(length < minsh, 8, axis=1) + # Now select X0, Y0 if short, otherwise X, Y + np.copyto(X, X0, where=short) + np.copyto(Y, Y0, where=short) + if self.pivot == 'middle': + X -= 0.5 * X[:, 3, np.newaxis] + elif self.pivot == 'tip': + # numpy bug? using -= does not work here unless we multiply by a + # float first, as with 'mid'. + X = X - X[:, 3, np.newaxis] + elif self.pivot != 'tail': + _api.check_in_list(["middle", "tip", "tail"], pivot=self.pivot) + + tooshort = length < self.minlength + if tooshort.any(): + # Use a heptagonal dot: + th = np.arange(0, 8, 1, np.float64) * (np.pi / 3.0) + x1 = np.cos(th) * self.minlength * 0.5 + y1 = np.sin(th) * self.minlength * 0.5 + X1 = np.repeat(x1[np.newaxis, :], N, axis=0) + Y1 = np.repeat(y1[np.newaxis, :], N, axis=0) + tooshort = np.repeat(tooshort, 8, 1) + np.copyto(X, X1, where=tooshort) + np.copyto(Y, Y1, where=tooshort) + # Mask handling is deferred to the caller, _make_verts. + return X, Y diff --git a/hyperspy/external/matplotlib/widgets.py b/hyperspy/external/matplotlib/widgets.py new file mode 100644 index 0000000000..51d6364145 --- /dev/null +++ b/hyperspy/external/matplotlib/widgets.py @@ -0,0 +1,803 @@ +# Copied from matplotlib 3.6, remove when minimum requirement of matplotlib +# is version >= 3.6 +# Require small changes to work with matplotlib >=3.1 +# - use of _api module is removed because it has been introduced in v3.4 +# - API of cbook.normalize_kwargs has changed +# - remove set_cursor/_hover +# - remove use of _cm_set introduced in +# https://github.com/matplotlib/matplotlib/pull/19369 + + +from contextlib import ExitStack +import copy +from numbers import Integral + +import numpy as np + +from matplotlib import cbook +from matplotlib.lines import Line2D +from matplotlib.patches import Rectangle +from matplotlib.widgets import AxesWidget + + +class _SelectorWidget(AxesWidget): + + def __init__(self, ax, onselect, useblit=False, button=None, + state_modifier_keys=None): + super().__init__(ax) + + self.visible = True + self.onselect = onselect + self.useblit = useblit and self.canvas.supports_blit + self.connect_default_events() + + self.state_modifier_keys = dict(move=' ', clear='escape', + square='shift', center='control') + self.state_modifier_keys.update(state_modifier_keys or {}) + + self.background = None + + if isinstance(button, Integral): + self.validButtons = [button] + else: + self.validButtons = button + + # Set to True when a selection is completed, otherwise is False + self._selection_completed = False + + # will save the data (position at mouseclick) + self._eventpress = None + # will save the data (pos. at mouserelease) + self._eventrelease = None + self._prev_event = None + self._state = set() + + def set_active(self, active): + super().set_active(active) + if active: + self.update_background(None) + + def _get_animated_artists(self): + """ + Convenience method to get all animated artists of a figure, except + those already present in self.artists. 'z_order' is ignored. + """ + return tuple([a for ax_ in self.ax.get_figure().get_axes() + for a in ax_.get_children() + if a.get_animated() and a not in self.artists]) + + def update_background(self, event): + """Force an update of the background.""" + # If you add a call to `ignore` here, you'll want to check edge case: + # `release` can call a draw event even when `ignore` is True. + if not self.useblit: + return + # Make sure that widget artists don't get accidentally included in the + # background, by re-rendering the background if needed (and then + # re-re-rendering the canvas with the visible widget artists). + # z_order needs to be respected when redrawing + artists = sorted(self.artists + self._get_animated_artists(), + key=lambda a: a.zorder) + needs_redraw = any(artist.get_visible() for artist in artists) + with ExitStack() as stack: + if needs_redraw: + for artist in artists: + stack.callback(artist.set_visible, artist.get_visible()) + artist.set_visible(False) + self.canvas.draw() + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + if needs_redraw: + for artist in artists: + self.ax.draw_artist(artist) + + def connect_default_events(self): + """Connect the major canvas events to methods.""" + self.connect_event('motion_notify_event', self.onmove) + self.connect_event('button_press_event', self.press) + self.connect_event('button_release_event', self.release) + self.connect_event('draw_event', self.update_background) + self.connect_event('key_press_event', self.on_key_press) + self.connect_event('key_release_event', self.on_key_release) + self.connect_event('scroll_event', self.on_scroll) + + def ignore(self, event): + # docstring inherited + if not self.active or not self.ax.get_visible(): + return True + # If canvas was locked + if not self.canvas.widgetlock.available(self): + return True + if not hasattr(event, 'button'): + event.button = None + # Only do rectangle selection if event was triggered + # with a desired button + if (self.validButtons is not None + and event.button not in self.validButtons): + return True + # If no button was pressed yet ignore the event if it was out + # of the axes + if self._eventpress is None: + return event.inaxes != self.ax + # If a button was pressed, check if the release-button is the same. + if event.button == self._eventpress.button: + return False + # If a button was pressed, check if the release-button is the same. + return (event.inaxes != self.ax or + event.button != self._eventpress.button) + + def update(self): + """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" + if not self.ax.get_visible() or self.ax.figure._cachedRenderer is None: + return False + if self.useblit: + if self.background is not None: + self.canvas.restore_region(self.background) + else: + self.update_background(None) + # We need to draw all artists, which are not included in the + # background, therefore we also draw self._get_animated_artists() + # and we make sure that we respect z_order + artists = sorted(self.artists + self._get_animated_artists(), + key=lambda a: a.zorder) + for artist in artists: + self.ax.draw_artist(artist) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() + return False + + def _get_data(self, event): + """Get the xdata and ydata for event, with limits.""" + if event.xdata is None: + return None, None + xdata = np.clip(event.xdata, *self.ax.get_xbound()) + ydata = np.clip(event.ydata, *self.ax.get_ybound()) + return xdata, ydata + + def _clean_event(self, event): + """ + Preprocess an event: + + - Replace *event* by the previous event if *event* has no ``xdata``. + - Clip ``xdata`` and ``ydata`` to the axes limits. + - Update the previous event. + """ + if event.xdata is None: + event = self._prev_event + else: + event = copy.copy(event) + event.xdata, event.ydata = self._get_data(event) + self._prev_event = event + return event + + def press(self, event): + """Button press handler and validator.""" + if not self.ignore(event): + event = self._clean_event(event) + self._eventpress = event + self._prev_event = event + key = event.key or '' + key = key.replace('ctrl', 'control') + # move state is locked in on a button press + if key == self.state_modifier_keys['move']: + self._state.add('move') + self._press(event) + return True + return False + + def _press(self, event): + """Button press event handler.""" + + def release(self, event): + """Button release event handler and validator.""" + if not self.ignore(event) and self._eventpress: + event = self._clean_event(event) + self._eventrelease = event + self._release(event) + self._eventpress = None + self._eventrelease = None + self._state.discard('move') + return True + return False + + def _release(self, event): + """Button release event handler.""" + + def onmove(self, event): + """Cursor move event handler and validator.""" + if not self.ignore(event) and self._eventpress: + event = self._clean_event(event) + self._onmove(event) + return True + return False + + def _onmove(self, event): + """Cursor move event handler.""" + + def on_scroll(self, event): + """Mouse scroll event handler and validator.""" + if not self.ignore(event): + self._on_scroll(event) + + def _on_scroll(self, event): + """Mouse scroll event handler.""" + + def on_key_press(self, event): + """Key press event handler and validator for all selection widgets.""" + if self.active: + key = event.key or '' + key = key.replace('ctrl', 'control') + if key == self.state_modifier_keys['clear']: + self.clear() + return + for (state, modifier) in self.state_modifier_keys.items(): + if modifier in key: + self._state.add(state) + self._on_key_press(event) + + def _on_key_press(self, event): + """Key press event handler - for widget-specific key press actions.""" + + def on_key_release(self, event): + """Key release event handler and validator.""" + if self.active: + key = event.key or '' + for (state, modifier) in self.state_modifier_keys.items(): + if modifier in key: + self._state.discard(state) + self._on_key_release(event) + + def _on_key_release(self, event): + """Key release event handler.""" + + def get_visible(self): + """Get the visibility of the selector artists.""" + return self.visible + + def set_visible(self, visible): + """Set the visibility of our artists.""" + self.visible = visible + for artist in self.artists: + artist.set_visible(visible) + + def clear(self): + """Clear the selection and set the selector ready to make a new one.""" + self._selection_completed = False + self.set_visible(False) + self.update() + + @property + def artists(self): + """Tuple of the artists of the selector.""" + handles_artists = getattr(self, '_handles_artists', ()) + return (self._selection_artist,) + handles_artists + + def set_props(self, **props): + """ + Set the properties of the selector artist. See the `props` argument + in the selector docstring to know which properties are supported. + """ + artist = self._selection_artist + props = cbook.normalize_kwargs(props, artist._alias_map) + artist.set(**props) + if self.useblit: + self.update() + self._props.update(props) + + def set_handle_props(self, **handle_props): + """ + Set the properties of the handles selector artist. See the + `handle_props` argument in the selector docstring to know which + properties are supported. + """ + if not hasattr(self, '_handles_artists'): + raise NotImplementedError("This selector doesn't have handles.") + + artist = self._handles_artists[0] + handle_props = cbook.normalize_kwargs(handle_props, artist._alias_map) + for handle in self._handles_artists: + handle.set(**handle_props) + if self.useblit: + self.update() + self._handle_props.update(handle_props) + + +class SpanSelector(_SelectorWidget): + """ + Visually select a min/max range on a single axis and call a function with + those values. + + To guarantee that the selector remains responsive, keep a reference to it. + + In order to turn off the SpanSelector, set ``span_selector.active`` to + False. To turn it back on, set it to True. + + Press and release events triggered at the same coordinates outside the + selection will clear the selector, except when + ``ignore_event_outside=True``. + + Parameters + ---------- + ax : `matplotlib.axes.Axes` + + onselect : callable + A callback function that is called after a release event and the + selection is created, changed or removed. + It must have the signature:: + + def on_select(min: float, max: float) -> Any + + direction : {"horizontal", "vertical"} + The direction along which to draw the span selector. + + minspan : float, default: 0 + If selection is less than or equal to *minspan*, the selection is + removed (when already existing) or cancelled. + + useblit : bool, default: False + If True, use the backend-dependent blitting features for faster + canvas updates. + + props : dict, optional + Dictionary of `matplotlib.patches.Patch` properties. + Default: + + ``dict(facecolor='red', alpha=0.5)`` + + onmove_callback : func(min, max), min/max are floats, default: None + Called on mouse move while the span is being selected. + + span_stays : bool, default: False + If True, the span stays visible after the mouse is released. + Deprecated, use *interactive* instead. + + interactive : bool, default: False + Whether to draw a set of handles that allow interaction with the + widget after it is drawn. + + button : `.MouseButton` or list of `.MouseButton`, default: all buttons + The mouse buttons which activate the span selector. + + handle_props : dict, default: None + Properties of the handle lines at the edges of the span. Only used + when *interactive* is True. See `matplotlib.lines.Line2D` for valid + properties. + + grab_range : float, default: 10 + Distance in pixels within which the interactive tool handles can be + activated. + + drag_from_anywhere : bool, default: False + If `True`, the widget can be moved by clicking anywhere within + its bounds. + + ignore_event_outside : bool, default: False + If `True`, the event triggered outside the span selector will be + ignored. + + snap_values : 1D array-like, default: None + Snap the extents of the selector to the values defined in + ``snap_values``. + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> import matplotlib.widgets as mwidgets + >>> fig, ax = plt.subplots() + >>> ax.plot([1, 2, 3], [10, 50, 100]) + >>> def onselect(vmin, vmax): + ... print(vmin, vmax) + >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal', + ... props=dict(facecolor='blue', alpha=0.5)) + >>> fig.show() + + See also: :doc:`/gallery/widgets/span_selector` + """ + + def __init__(self, ax, onselect, direction, minspan=0, useblit=False, + props=None, onmove_callback=None, interactive=False, + button=None, handle_props=None, grab_range=10, + drag_from_anywhere=False, ignore_event_outside=False, + snap_values=None): + + super().__init__(ax, onselect, useblit=useblit, button=button) + + if props is None: + props = dict(facecolor='red', alpha=0.5) + + props['animated'] = self.useblit + + self.direction = direction + + self.visible = True + self._extents_on_press = None + + # self._pressv is deprecated and we don't use it internally anymore + # but we maintain it until it is removed + self._pressv = None + + self._props = props + self.onmove_callback = onmove_callback + self.minspan = minspan + + self.grab_range = grab_range + self._interactive = interactive + self._edge_handles = None + self.drag_from_anywhere = drag_from_anywhere + self.ignore_event_outside = ignore_event_outside + self.snap_values = snap_values + + # Reset canvas so that `new_axes` connects events. + self.canvas = None + self.new_axes(ax) + + # Setup handles + if handle_props is None: + handle_props = {} + self._handle_props = { + 'color': props.get('facecolor', 'r'), + **cbook.normalize_kwargs(handle_props, Line2D._alias_map)} + + if self._interactive: + self._edge_order = ['min', 'max'] + self._setup_edge_handles(self._handle_props) + + self._active_handle = None + + # prev attribute is deprecated but we still need to maintain it + self._prev = (0, 0) + + def new_axes(self, ax): + """Set SpanSelector to operate on a new Axes.""" + self.ax = ax + if self.canvas is not ax.figure.canvas: + if self.canvas is not None: + self.disconnect_events() + + self.canvas = ax.figure.canvas + self.connect_default_events() + + # Reset + self._selection_completed = False + + if self.direction == 'horizontal': + trans = ax.get_xaxis_transform() + w, h = 0, 1 + else: + trans = ax.get_yaxis_transform() + w, h = 1, 0 + rect_artist = Rectangle((0, 0), w, h, + transform=trans, + visible=False, + **self._props) + + self.ax.add_patch(rect_artist) + self._selection_artist = rect_artist + + def _setup_edge_handles(self, props): + # Define initial position using the axis bounds to keep the same bounds + if self.direction == 'horizontal': + positions = self.ax.get_xbound() + else: + positions = self.ax.get_ybound() + self._edge_handles = ToolLineHandles(self.ax, positions, + direction=self.direction, + line_props=props, + useblit=self.useblit) + + @property + def _handles_artists(self): + if self._edge_handles is not None: + return self._edge_handles.artists + else: + return () + + def _press(self, event): + """Button press event handler.""" + if self._interactive and self._selection_artist.get_visible(): + self._set_active_handle(event) + else: + self._active_handle = None + + if self._active_handle is None or not self._interactive: + # Clear previous rectangle before drawing new rectangle. + self.update() + + v = event.xdata if self.direction == 'horizontal' else event.ydata + # self._pressv and self._prev are deprecated but we still need to + # maintain them + self._pressv = v + self._prev = self._get_data(event) + + if self._active_handle is None and not self.ignore_event_outside: + # when the press event outside the span, we initially set the + # visibility to False and extents to (v, v) + # update will be called when setting the extents + self.visible = False + self._set_extents((v, v)) + # We need to set the visibility back, so the span selector will be + # drawn when necessary (span width > 0) + self.visible = True + else: + self.set_visible(True) + + return False + + @property + def direction(self): + """Direction of the span selector: 'vertical' or 'horizontal'.""" + return self._direction + + @direction.setter + def direction(self, direction): + """Set the direction of the span selector.""" + if hasattr(self, '_direction') and direction != self._direction: + # remove previous artists + self._selection_artist.remove() + if self._interactive: + self._edge_handles.remove() + self._direction = direction + self.new_axes(self.ax) + if self._interactive: + self._setup_edge_handles(self._handle_props) + else: + self._direction = direction + + def _release(self, event): + """Button release event handler.""" + # self._pressv is deprecated but we still need to maintain it + self._pressv = None + + if not self._interactive: + self._selection_artist.set_visible(False) + + if (self._active_handle is None and self._selection_completed and + self.ignore_event_outside): + return + + vmin, vmax = self.extents + span = vmax - vmin + + if span <= self.minspan: + # Remove span and set self._selection_completed = False + self.set_visible(False) + if self._selection_completed: + # Call onselect, only when the span is already existing + self.onselect(vmin, vmax) + self._selection_completed = False + else: + self.onselect(vmin, vmax) + self._selection_completed = True + + self.update() + + self._active_handle = None + + return False + + def _onmove(self, event): + """Motion notify event handler.""" + + # self._prev are deprecated but we still need to maintain it + self._prev = self._get_data(event) + + v = event.xdata if self.direction == 'horizontal' else event.ydata + if self.direction == 'horizontal': + vpress = self._eventpress.xdata + else: + vpress = self._eventpress.ydata + + # move existing span + # When "dragging from anywhere", `self._active_handle` is set to 'C' + # (match notation used in the RectangleSelector) + if self._active_handle == 'C' and self._extents_on_press is not None: + vmin, vmax = self._extents_on_press + dv = v - vpress + vmin += dv + vmax += dv + + # resize an existing shape + elif self._active_handle and self._active_handle != 'C': + vmin, vmax = self._extents_on_press + if self._active_handle == 'min': + vmin = v + else: + vmax = v + # new shape + else: + # Don't create a new span if there is already one when + # ignore_event_outside=True + if self.ignore_event_outside and self._selection_completed: + return + vmin, vmax = vpress, v + if vmin > vmax: + vmin, vmax = vmax, vmin + + self._set_extents((vmin, vmax)) + + if self.onmove_callback is not None: + self.onmove_callback(vmin, vmax) + + return False + + def _draw_shape(self, vmin, vmax): + if vmin > vmax: + vmin, vmax = vmax, vmin + if self.direction == 'horizontal': + self._selection_artist.set_x(vmin) + self._selection_artist.set_width(vmax - vmin) + else: + self._selection_artist.set_y(vmin) + self._selection_artist.set_height(vmax - vmin) + + def _set_active_handle(self, event): + """Set active handle based on the location of the mouse event.""" + # Note: event.xdata/ydata in data coordinates, event.x/y in pixels + e_idx, e_dist = self._edge_handles.closest(event.x, event.y) + + # Prioritise center handle over other handles + # Use 'C' to match the notation used in the RectangleSelector + if 'move' in self._state: + self._active_handle = 'C' + elif e_dist > self.grab_range: + # Not close to any handles + self._active_handle = None + if self.drag_from_anywhere and self._contains(event): + # Check if we've clicked inside the region + self._active_handle = 'C' + self._extents_on_press = self.extents + else: + self._active_handle = None + return + else: + # Closest to an edge handle + self._active_handle = self._edge_order[e_idx] + + # Save coordinates of rectangle at the start of handle movement. + self._extents_on_press = self.extents + + def _contains(self, event): + """Return True if event is within the patch.""" + return self._selection_artist.contains(event, radius=0)[0] + + @staticmethod + def _snap(values, snap_values): + """Snap values to a given array values (snap_values).""" + indices = np.empty_like(values, dtype="uint") + # take into account machine precision + eps = np.min(np.abs(np.diff(snap_values))) * 1e-12 + for i, v in enumerate(values): + indices[i] = np.abs(snap_values - v + np.sign(v) * eps).argmin() + return snap_values[indices] + + @property + def extents(self): + """Return extents of the span selector.""" + if self.direction == 'horizontal': + vmin = self._selection_artist.get_x() + vmax = vmin + self._selection_artist.get_width() + else: + vmin = self._selection_artist.get_y() + vmax = vmin + self._selection_artist.get_height() + return vmin, vmax + + @extents.setter + def extents(self, extents): + self._set_extents(extents) + self._selection_completed = True + + def _set_extents(self, extents): + # Update displayed shape + if self.snap_values is not None: + extents = tuple(self._snap(extents, self.snap_values)) + self._draw_shape(*extents) + if self._interactive: + # Update displayed handles + self._edge_handles.set_data(self.extents) + self.set_visible(self.visible) + self.update() + + +class ToolLineHandles: + """ + Control handles for canvas tools. + + Parameters + ---------- + ax : `matplotlib.axes.Axes` + Matplotlib axes where tool handles are displayed. + positions : 1D array + Positions of handles in data coordinates. + direction : {"horizontal", "vertical"} + Direction of handles, either 'vertical' or 'horizontal' + line_props : dict, optional + Additional line properties. See `matplotlib.lines.Line2D`. + useblit : bool, default: True + Whether to use blitting for faster drawing (if supported by the + backend). + """ + + def __init__(self, ax, positions, direction, line_props=None, + useblit=True): + self.ax = ax + self._direction = direction + + if line_props is None: + line_props = {} + line_props.update({'visible': False, 'animated': useblit}) + + line_fun = ax.axvline if self.direction == 'horizontal' else ax.axhline + + self._artists = [line_fun(p, **line_props) for p in positions] + + @property + def artists(self): + return tuple(self._artists) + + @property + def positions(self): + """Positions of the handle in data coordinates.""" + method = 'get_xdata' if self.direction == 'horizontal' else 'get_ydata' + return [getattr(line, method)()[0] for line in self.artists] + + @property + def direction(self): + """Direction of the handle: 'vertical' or 'horizontal'.""" + return self._direction + + def set_data(self, positions): + """ + Set x or y positions of handles, depending if the lines are vertical + of horizontal. + + Parameters + ---------- + positions : tuple of length 2 + Set the positions of the handle in data coordinates + """ + method = 'set_xdata' if self.direction == 'horizontal' else 'set_ydata' + for line, p in zip(self.artists, positions): + getattr(line, method)([p, p]) + + def set_visible(self, value): + """Set the visibility state of the handles artist.""" + for artist in self.artists: + artist.set_visible(value) + + def set_animated(self, value): + """Set the animated state of the handles artist.""" + for artist in self.artists: + artist.set_animated(value) + + def remove(self): + """Remove the handles artist from the figure.""" + for artist in self._artists: + artist.remove() + + def closest(self, x, y): + """ + Return index and pixel distance to closest handle. + + Parameters + ---------- + x, y : float + x, y position from which the distance will be calculated to + determinate the closest handle + + Returns + ------- + index, distance : index of the handle and its distance from + position x, y + """ + if self.direction == 'horizontal': + p_pts = np.array([ + self.ax.transData.transform((p, 0))[0] for p in self.positions + ]) + dist = abs(p_pts - x) + else: + p_pts = np.array([ + self.ax.transData.transform((0, p))[1] for p in self.positions + ]) + dist = abs(p_pts - y) + index = np.argmin(dist) + return index, dist[index] diff --git a/hyperspy/external/mpfit/mpfit.py b/hyperspy/external/mpfit/mpfit.py index 4f7e15e3f7..3f929dcbff 100644 --- a/hyperspy/external/mpfit/mpfit.py +++ b/hyperspy/external/mpfit/mpfit.py @@ -11,20 +11,20 @@ Craig B. Markwardt, NASA/GSFC Code 662, Greenbelt, MD 20770 craigm@lheamail.gsfc.nasa.gov UPDATED VERSIONs can be found on my WEB PAGE: - http://cow.physics.wisc.edu/~craigm/idl/idl.html + https://cow.physics.wisc.edu/~craigm/idl/idl.html Mark Rivers created this Python version from Craig's IDL version. Mark Rivers, University of Chicago Building 434A, Argonne National Laboratory 9700 South Cass Avenue, Argonne, IL 60439 rivers@cars.uchicago.edu - Updated versions can be found at http://cars.uchicago.edu/software + Updated versions can be found at https://cars.uchicago.edu/software Sergey Koposov converted the Mark's Python version from Numeric to numpy Sergey Koposov, University of Cambridge, Institute of Astronomy, Madingley road, CB3 0HA, Cambridge, UK koposov@ast.cam.ac.uk - Updated versions can be found at http://code.google.com/p/astrolibpy/source/browse/trunk/ + Updated versions can be found at https://code.google.com/p/astrolibpy/source/browse/trunk/ MODIFICATION HISTORY @@ -428,7 +428,7 @@ def myfunct(p, fjac=None, x=None, y=None, err=None): class machar: def __init__(self, double=True): - info = np.finfo(np.float64) if double else np.finfo(np.float32) + info = np.finfo(float) if double else np.finfo(np.float32) self.machep = info.eps self.maxnum = info.max @@ -625,7 +625,7 @@ def iterfunct(myfunct, p, iter, fnorm, functkw=None, """ (blas_enorm32,) = get_blas_funcs(["nrm2"], np.array([0], dtype=np.float32)) - (blas_enorm64,) = get_blas_funcs(["nrm2"], np.array([0], dtype=np.float64)) + (blas_enorm64,) = get_blas_funcs(["nrm2"], np.array([0], dtype=float)) def __init__( self, @@ -711,7 +711,7 @@ def __init__( # In the case if the xall is not float or if is float but has less # than 64 bits we do convert it into double if xall.dtype.kind != "f" or xall.dtype.itemsize <= 4: - xall = xall.astype(np.float) + xall = xall.astype(float) npar = len(xall) self.fnorm = -1.0 @@ -885,7 +885,7 @@ def __init__( return # If parameters were changed (grrr..) then re-tie - if np.max(np.abs(xnew0 - self.params)) > 0: + if np.max(abs(xnew0 - self.params)) > 0: if self.qanytied: self.params = self.tie(self.params, ptied) x = self.params[ifree] @@ -988,7 +988,7 @@ def __init__( l_ = ipvt[j] if wa2[l_] != 0: sum0 = sum(fjac[0 : j + 1, j] * qtf[0 : j + 1]) / self.fnorm - gnorm = np.max([gnorm, np.abs(sum0 / wa2[l_])]) + gnorm = np.max([gnorm, abs(sum0 / wa2[l_])]) # Test for convergence of the gradient norm if gnorm <= gtol: @@ -1031,7 +1031,7 @@ def __init__( if nupeg > 0: wa1[whupeg] = np.clip(wa1[whupeg], np.min(wa1), 0.0) - dwa1 = np.abs(wa1) > machep + dwa1 = abs(wa1) > machep whl = ( np.nonzero(((dwa1 != 0.0) & qllim) & ((x + wa1) < llim)) )[0] @@ -1053,7 +1053,7 @@ def __init__( )[0] if len(whmax) > 0: mrat = np.max( - np.abs(nwa1[whmax]) / np.abs(maxstep[ifree[whmax]]) + abs(nwa1[whmax]) / abs(maxstep[ifree[whmax]]) ) if mrat > 1: alpha /= mrat @@ -1144,12 +1144,12 @@ def __init__( self.niter += 1 # Tests for convergence - if (np.abs(actred) <= ftol) and (prered <= ftol) and (0.5 * ratio <= 1): + if (abs(actred) <= ftol) and (prered <= ftol) and (0.5 * ratio <= 1): self.status = 1 if delta <= xtol * xnorm: self.status = 2 if ( - (np.abs(actred) <= ftol) + (abs(actred) <= ftol) and (prered <= ftol) and (0.5 * ratio <= 1) and (self.status == 2) @@ -1162,7 +1162,7 @@ def __init__( if self.niter >= maxiter: self.status = 5 if ( - (np.abs(actred) <= machep) + (abs(actred) <= machep) and (prered <= machep) and (0.5 * ratio <= 1) ): @@ -1404,7 +1404,7 @@ def fdjac2( fjac = np.zeros([m, n]) - h = eps * np.abs(x) + h = eps * abs(x) # if STEP is given, use that # STEP includes the fixed parameters @@ -1420,7 +1420,7 @@ def fdjac2( dstepi = dstep[ifree] wh = (np.nonzero(dstepi > 0))[0] if len(wh) > 0: - h[wh] = np.abs(dstepi[wh] * x[wh]) + h[wh] = abs(dstepi[wh] * x[wh]) # In case any of the step values are zero h[h == 0] = eps @@ -1443,7 +1443,7 @@ def fdjac2( if status < 0: return None - if np.abs(dside[ifree[j]]) <= 1: + if abs(dside[ifree[j]]) <= 1: # COMPUTE THE ONE-SIDED DERIVATIVE # Note optimization fjac(0:*,j) fjac[0:, j] = (fp - fvec) / h[j] @@ -1682,7 +1682,7 @@ def qrsolv(self, r, ipvt, diag, qtb, sdiag): for k in range(j, n): if sdiag[k] == 0: break - if np.abs(r[k, k]) < np.abs(sdiag[k]): + if abs(r[k, k]) < abs(sdiag[k]): cotan = r[k, k] / sdiag[k] sine = 0.5 / np.sqrt(0.25 + 0.25 * cotan * cotan) cosine = sine * cotan @@ -1818,7 +1818,7 @@ def lmpar(self, r, ipvt, diag, qtb, delta, x, sdiag, par=None): # jacobian is rank-deficient, obtain a least-squares solution nsing = n wa1 = qtb.copy() - rdiagabs = np.abs(np.diagonal(r)) + rdiagabs = abs(np.diagonal(r)) rthresh = np.max(rdiagabs) * machep wh = (np.nonzero(rdiagabs < rthresh))[0] if len(wh) > 0: @@ -1888,7 +1888,7 @@ def lmpar(self, r, ipvt, diag, qtb, delta, x, sdiag, par=None): temp = fp fp = dxnorm - delta - if (np.abs(fp) <= 0.1 * delta) or ( + if (abs(fp) <= 0.1 * delta) or ( (parl == 0) and (fp <= temp) and (temp < 0) ): break @@ -1992,9 +1992,9 @@ def calc_covar(self, rr, ipvt=None, tol=1e-14): # For the inverse of r in the full upper triangle of r l_ = -1 - tolr = tol * np.abs(r[0, 0]) + tolr = tol * abs(r[0, 0]) for k in range(n): - if np.abs(r[k, k]) <= tolr: + if abs(r[k, k]) <= tolr: break r[k, k] = 1.0 / r[k, k] for j in range(k): diff --git a/hyperspy/external/progressbar.py b/hyperspy/external/progressbar.py index 479fa4c22b..740163c9aa 100644 --- a/hyperspy/external/progressbar.py +++ b/hyperspy/external/progressbar.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from distutils.version import LooseVersion +from packaging.version import Version from tqdm import __version__ as tqdm_version -if LooseVersion(tqdm_version) >= LooseVersion("4.36.0"): +if Version(tqdm_version) >= Version("4.36.0"): # API change for 5.0 https://github.com/tqdm/tqdm/pull/800 from tqdm import tqdm from tqdm.notebook import tqdm as tqdm_notebook diff --git a/hyperspy/hyperspy_extension.yaml b/hyperspy/hyperspy_extension.yaml index c1d7e4da9e..4e4eb4cf8d 100644 --- a/hyperspy/hyperspy_extension.yaml +++ b/hyperspy/hyperspy_extension.yaml @@ -71,78 +71,10 @@ signals: dtype: complex lazy: True module: hyperspy._signals.complex_signal2d - HologramImage: - signal_type: hologram - signal_dimension: 2 - dtype: real - lazy: False - module: hyperspy._signals.hologram_image - LazyHologramImage: - signal_type: hologram - signal_dimension: 2 - dtype: real - lazy: True - module: hyperspy._signals.hologram_image - EELSSpectrum: - signal_type: EELS - signal_type_aliases: - - TEM EELS - signal_dimension: 1 - dtype: real - lazy: False - module: hyperspy._signals.eels - LazyEELSSpectrum: - signal_type: EELS - signal_type_aliases: - - TEM EELS - signal_dimension: 1 - dtype: real - lazy: True - module: hyperspy._signals.eels - EDSTEMSpectrum: - signal_type: EDS_TEM - signal_dimension: 1 - dtype: real - lazy: False - module: hyperspy._signals.eds_tem - LazyEDSTEMSpectrum: - signal_type: EDS_TEM - signal_dimension: 1 - dtype: real - lazy: True - module: hyperspy._signals.eds_tem - EDSSEMSpectrum: - signal_type: EDS_SEM - signal_dimension: 1 - dtype: real - lazy: False - module: hyperspy._signals.eds_sem - LazyEDSSEMSpectrum: - signal_type: EDS_SEM - signal_dimension: 1 - dtype: real - lazy: True - module: hyperspy._signals.eds_sem - DielectricFunction: - signal_type: DielectricFunction - signal_type_aliases: - - dielectric function - signal_dimension: 1 - dtype: complex - lazy: False - module: hyperspy._signals.dielectric_function - LazyDielectricFunction: - signal_type: DielectricFunction - signal_type_aliases: - - dielectric function - signal_dimension: 1 - dtype: complex - lazy: True - module: hyperspy._signals.dielectric_function components1D: Arctan: - module: hyperspy._components.eels_arctan + module: hyperspy._components.arctan class: Arctan Bleasdale: module: hyperspy._components.bleasdale @@ -150,15 +82,6 @@ components1D: Doniach: module: hyperspy._components.doniach class: Doniach - DoublePowerLaw: - module: hyperspy._components.eels_double_power_law - class: DoublePowerLaw - EELSArctan: - module: hyperspy._components.eels_arctan - class: EELSArctan - EELSCLEdge: - module: hyperspy._components.eels_cl_edge - class: EELSCLEdge Erf: module: hyperspy._components.error_function class: Erf @@ -186,16 +109,7 @@ components1D: Offset: module: hyperspy._components.offset class: Offset - PESCoreLineShape: - module: hyperspy._components.pes_core_line_shape - class: PESCoreLineShape - PESVoigt: - module: hyperspy._components.pes_voigt - class: PESVoigt Polynomial: - module: hyperspy._components.polynomial_deprecated - class: Polynomial - eab91275-88db-4855-917a-cdcbe7209592: module: hyperspy._components.polynomial class: Polynomial PowerLaw: @@ -204,9 +118,6 @@ components1D: RC: module: hyperspy._components.rc class: RC - SEE: - module: hyperspy._components.pes_see - class: SEE ScalableFixedPattern: module: hyperspy._components.scalable_fixed_pattern class: ScalableFixedPattern @@ -216,15 +127,9 @@ components1D: SplitVoigt: module: hyperspy._components.split_voigt class: SplitVoigt - Vignetting: - module: hyperspy._components.eels_vignetting - class: Vignetting Voigt: - module: hyperspy._components.pes_voigt + module: hyperspy._components.voigt class: Voigt - VolumePlasmonDrude: - module: hyperspy._components.volume_plasmon_drude - class: VolumePlasmonDrude components2D: Expression: @@ -244,8 +149,6 @@ GUI: - hyperspy.AxesManager - hyperspy.Parameter - hyperspy.Component - - hyperspy.EELSCLEdge_Component - - hyperspy.EELSSpectrum.print_edges_table - hyperspy.ScalableFixedPattern_Component - hyperspy.Signal1D.calibrate - hyperspy.Signal1D.smooth_savitzky_golay @@ -253,11 +156,11 @@ GUI: - hyperspy.Signal1D.smooth_total_variation - hyperspy.Signal1D.smooth_butterworth - hyperspy.Signal1D.contrast_editor - - hyperspy.Signal1D.integrate_in_range - hyperspy.Signal1D.remove_background - hyperspy.SimpleMessage - hyperspy.Signal1D.spikes_removal_tool - hyperspy.Signal2D.find_peaks + - hyperspy.Signal2D.calibrate - hyperspy.Point1DROI - hyperspy.Point2DROI - hyperspy.SpanROI @@ -266,6 +169,3 @@ GUI: - hyperspy.Line2DROI - hyperspy.Model - hyperspy.Model1D.fit_component - - hyperspy.microscope_parameters_EELS - - hyperspy.microscope_parameters_EDS_TEM - - hyperspy.microscope_parameters_EDS_SEM diff --git a/hyperspy/interactive.py b/hyperspy/interactive.py index 0b20c4979c..c22cea10ed 100644 --- a/hyperspy/interactive.py +++ b/hyperspy/interactive.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import inspect @@ -30,58 +30,55 @@ def _connect_events(event, to_connect): class Interactive: - - """Chainable operations on Signals that update on events. + r""" + Chainable operations on Signals that update on events. The operation + result will be updated when a given event is triggered. + + Parameters + ---------- + f : callable + A function that returns an object and that optionally can place the + result in an object given through the ``out`` keyword. + event : (list of) :class:`~hyperspy.events.Event`, str ("auto") or None + Update the result of the operation when the event is triggered. + If ``"auto"`` and ``f`` is a method of a Signal class instance its + ``data_changed`` event is selected if the function takes an ``out`` + argument. If None, ``update`` is not connected to any event. The + default is ``"auto"``. It is also possible to pass an iterable of + events, in which case all the events are connected. + recompute_out_event : (list of) :class:`~hyperspy.events.Event`, str ("auto") or None + Optional argument. If supplied, this event causes a full + recomputation of a new object. Both the data and axes of the new + object are then copied over to the existing `out` object. Only + useful for signals or other objects that have an attribute + ``axes_manager``. If ``"auto"`` and ``f`` is a method of a Signal class + instance its ``AxesManager`` ``any_axis_changed`` event is selected. + Otherwise, the signal ``data_changed`` event is selected. + If None, ``recompute_out`` is not connected to any event. + The default is ``"auto"``. It is also possible to pass an iterable of + events, in which case all the events are connected. + *args : + Arguments to be passed to ``f``. + **kwargs : dict + Keyword arguments to be passed to ``f``. """ - def __init__(self, f, event="auto", - recompute_out_event="auto", - *args, **kwargs): - """Update operation result when a given event is triggered. - - Parameters - ---------- - f : function or method - A function that returns an object and that optionally can place the - result in an object given through the `out` keyword. - event : {Event, "auto", None, iterable of events} - Update the result of the operation when the event is triggered. - If "auto" and `f` is a method of a Signal class instance its - `data_changed` event is selected if the function takes an `out` - argument. If None, `update` is not connected to any event. The - default is "auto". It is also possible to pass an iterable of - events, in which case all the events are connected. - recompute_out_event : {Event, "auto", None, iterable of events} - Optional argument. If supplied, this event causes a full - recomputation of a new object. Both the data and axes of the new - object are then copied over to the existing `out` object. Only - useful for `Signal` or other objects that have an attribute - `axes_manager`. If "auto" and `f` is a method of a Signal class - instance its `AxesManager` `any_axis_changed` event is selected. - Otherwise the `Signal` `data_changed` event is selected. - If None, `recompute_out` is not connected to any event. - The default is "auto". It is also possible to pass an iterable of - events, in which case all the events are connected. - *args - Arguments to be passed to `f`. - **kwargs - Keyword arguments to be passed to `f`. - - """ + def __init__(self, f, event="auto", recompute_out_event="auto", *args, **kwargs): from hyperspy.signal import BaseSignal + self.f = f self.args = args self.kwargs = kwargs - _plot_kwargs = self.kwargs.pop('_plot_kwargs', None) - if 'out' in self.kwargs: + _plot_kwargs = self.kwargs.pop("_plot_kwargs", None) + if "out" in self.kwargs: self.f(*self.args, **self.kwargs) - self.out = self.kwargs.pop('out') + self.out = self.kwargs.pop("out") else: self.out = self.f(*self.args, **self.kwargs) # Reuse the `_plot_kwargs` for the roi if available - if _plot_kwargs and 'signal' in self.kwargs: - self.out._plot_kwargs = self.kwargs['signal']._plot_kwargs + if _plot_kwargs and "signal" in self.kwargs: + self.out._plot_kwargs = self.kwargs["signal"]._plot_kwargs try: fargs = list(inspect.signature(self.f).parameters.keys()) except TypeError: @@ -94,12 +91,14 @@ def __init__(self, f, event="auto", if event == "auto": event = self.f.__self__.events.data_changed if recompute_out_event == "auto": - recompute_out_event = \ + recompute_out_event = ( self.f.__self__.axes_manager.events.any_axis_changed + ) else: event = None if event == "auto" else event - recompute_out_event = (None if recompute_out_event == "auto" - else recompute_out_event) + recompute_out_event = ( + None if recompute_out_event == "auto" else recompute_out_event + ) if recompute_out_event: _connect_events(recompute_out_event, self.recompute_out) if event: @@ -118,8 +117,7 @@ def recompute_out(self): self.out.data[:] = out.data[:] else: self.out.data = out.data - self.out.axes_manager.update_axes_attributes_from( - out.axes_manager._axes) + self.out.axes_manager.update_axes_attributes_from(out.axes_manager._axes) self.out.events.data_changed.trigger(self.out) def update(self): @@ -127,8 +125,17 @@ def update(self): def interactive(f, event="auto", recompute_out_event="auto", *args, **kwargs): + """%s + + Returns + ------- + :class:`~hyperspy.signal.BaseSignal` or subclass + Signal updated with the operation result when a given event is + triggered. + + """ cls = Interactive(f, event, recompute_out_event, *args, **kwargs) return cls.out -interactive.__doc__ = Interactive.__init__.__doc__ +interactive.__doc__ %= Interactive.__doc__ diff --git a/hyperspy/io.py b/hyperspy/io.py index eb0248d0ad..955ee717ed 100644 --- a/hyperspy/io.py +++ b/hyperspy/io.py @@ -1,64 +1,75 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import os import glob -import warnings -import logging import importlib +import logging +import os +import warnings +from collections.abc import MutableMapping +from datetime import datetime +from inspect import isgenerator +from pathlib import Path import numpy as np from natsort import natsorted -from inspect import isgenerator -from pathlib import Path +from rsciio import IO_PLUGINS +from rsciio.utils.tools import ensure_directory +from rsciio.utils.tools import overwrite as overwrite_method -from hyperspy.drawing.marker import markers_metadata_dict_to_markers +from hyperspy.api import __version__ as hs_version +from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG +from hyperspy.docstrings.utils import STACK_METADATA_ARG +from hyperspy.drawing.markers import markers_dict_to_markers from hyperspy.exceptions import VisibleDeprecationWarning -from hyperspy.misc.io.tools import ensure_directory -from hyperspy.misc.io.tools import overwrite as overwrite_method -from hyperspy.misc.utils import strlist2enumeration +from hyperspy.extensions import ALL_EXTENSIONS +from hyperspy.misc.utils import get_object_package_info, strlist2enumeration from hyperspy.misc.utils import stack as stack_method -from hyperspy.io_plugins import io_plugins, default_write_ext from hyperspy.ui_registry import get_gui -from hyperspy.extensions import ALL_EXTENSIONS -from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG -from hyperspy.docstrings.utils import STACK_METADATA_ARG - _logger = logging.getLogger(__name__) # Utility string: -f_error_fmt = ( - "\tFile %d:\n" - "\t\t%d signals\n" - "\t\tPath: %s") +f_error_fmt = "\tFile %d:\n" "\t\t%d signals\n" "\t\tPath: %s" -def _infer_file_reader(extension): - """Return a file reader from the plugins list based on the file extension. +def _format_name_to_reader(format_name): + for reader in IO_PLUGINS: + if format_name.lower() == reader["name"].lower(): + return reader + elif reader.get("name_aliases"): + aliases = [s.lower() for s in reader["name_aliases"]] + if format_name.lower() in aliases: + return reader + raise ValueError("The `format_name` given does not match any format available.") + + +def _infer_file_reader(string): + """Return a file reader from the plugins list based on the format name or + the file extension. If the extension is not found or understood, returns the Python imaging library as the file reader. Parameters ---------- - extension : str + string : str File extension, without initial "." separator Returns @@ -67,25 +78,88 @@ def _infer_file_reader(extension): The inferred file reader. """ - rdrs = [rdr for rdr in io_plugins if extension.lower() in rdr.file_extensions] + try: + reader = _format_name_to_reader(string) + return reader + except ValueError: + pass + + rdrs = [rdr for rdr in IO_PLUGINS if string.lower() in rdr["file_extensions"]] if not rdrs: # Try to load it with the python imaging library _logger.warning( - f"Unable to infer file type from extension '{extension}'. " + f"Unable to infer file type from extension '{string}'. " "Will attempt to load the file with the Python imaging library." ) - from hyperspy.io_plugins import image - - reader = image + (reader,) = [ + reader for reader in IO_PLUGINS if reader["name"].lower() == "image" + ] + elif len(rdrs) > 1: + names = [rdr["name"] for rdr in rdrs] + raise ValueError( + f"There are multiple file readers that could read the file. " + f"Please select one from the list below with the `reader` keyword. " + f"File readers for your file: {names}" + ) else: - # Just take the first match for now reader = rdrs[0] return reader +def _infer_file_writer(string): + """Return a file reader from the plugins list based on the file extension. + + If the extension is not found or understood, returns + the Python imaging library as the file reader. + + Parameters + ---------- + string : str + File extension, without initial "." separator + + Returns + ------- + reader : func + The inferred file reader. + + """ + plugins = [ + plugin for plugin in IO_PLUGINS if string.lower() in plugin["file_extensions"] + ] + writers = [plugin for plugin in plugins if plugin["writes"]] + if not writers: + extensions = [ + plugin["file_extensions"][plugin["default_extension"]] + for plugin in IO_PLUGINS + if plugin["writes"] + ] + if not plugins: + raise ValueError( + f"The .{string} extension does not correspond to any supported format. " + f"Supported file extensions are: {strlist2enumeration(extensions)}." + ) + else: + raise ValueError( + "Writing to this format is not supported. " + f"Supported file extensions are: {strlist2enumeration(extensions)}." + ) + + elif len(writers) > 1: + names = [writer["name"] for writer in writers] + raise ValueError( + f"There are multiple file formats matching the extension of your file. " + f"Please select one from the list below with the `format` keyword. " + f"File formats for your file: {names}" + ) + else: + writer = writers[0] + + return writer + + def _escape_square_brackets(text): """Escapes pairs of square brackets in strings for glob.glob(). @@ -99,8 +173,8 @@ def _escape_square_brackets(text): str The escaped string - Example - ------- + Examples + -------- >>> # Say there are two files like this: >>> # /home/data/afile[1x1].txt >>> # /home/data/afile[1x2].txt @@ -108,7 +182,7 @@ def _escape_square_brackets(text): >>> path = "/home/data/afile[*].txt" >>> glob.glob(path) [] - >>> glob.glob(_escape_square_brackets(path)) + >>> glob.glob(_escape_square_brackets(path)) # doctest: +SKIP ['/home/data/afile[1x2].txt', '/home/data/afile[1x1].txt'] """ @@ -119,22 +193,35 @@ def _escape_square_brackets(text): return pattern.sub(lambda m: rep[re.escape(m.group(0))], text) -def load(filenames=None, - signal_type=None, - stack=False, - stack_axis=None, - new_axis_name='stack_element', - lazy=False, - convert_units=False, - escape_square_brackets=False, - stack_metadata=True, - load_original_metadata=True, - show_progressbar=None, - **kwds): +def _parse_path(arg): + """Convenience function to get the path from zarr store or string.""" + # In case of zarr store, get the path + if isinstance(arg, MutableMapping): + fname = arg.path + else: + fname = arg + + return fname + + +def load( + filenames=None, + signal_type=None, + stack=False, + stack_axis=None, + new_axis_name="stack_element", + lazy=False, + convert_units=False, + escape_square_brackets=False, + stack_metadata=True, + load_original_metadata=True, + show_progressbar=None, + **kwds, +): """Load potentially multiple supported files into HyperSpy. Supported formats: hspy (HDF5), msa, Gatan dm3, Ripple (rpl+raw), - Bruker bcf and spx, FEI ser and emi, SEMPER unf, EMD, EDAX spd/spc, + Bruker bcf and spx, FEI ser and emi, SEMPER unf, EMD, EDAX spd/spc, CEOS prz tif, and a number of image formats. Depending on the number of datasets to load in the file, this function will @@ -145,7 +232,7 @@ def load(filenames=None, Parameters ---------- - filenames : None, str, list(str), pathlib.Path, list(pathlib.Path) + filenames : None, (list of) str or (list of) pathlib.Path, default None The filename to be loaded. If None, a window will open to select a file to load. If a valid filename is passed, that single file is loaded. If multiple file names are passed in @@ -157,7 +244,7 @@ def load(filenames=None, by 'my_file' and have the '.msa' extension. Alternatively, regular expression type character classes can be used (e.g. ``[a-z]`` matches lowercase letters). See also the `escape_square_brackets` parameter. - signal_type : None, str, '', optional + signal_type : None, str, default None The acronym that identifies the signal type. May be any signal type provided by HyperSpy or by installed extensions as listed by `hs.print_known_signal_types()`. The value provided may determines the @@ -167,13 +254,13 @@ def load(filenames=None, For example, for electron energy-loss spectroscopy use 'EELS'. If '' (empty string) the value is not read from the file and is considered undefined. - stack : bool, optional + stack : bool, default False Default False. If True and multiple filenames are passed, stacking all the data into a single object is attempted. All files must match in shape. If each file contains multiple (N) signals, N stacks will be created, with the requirement that each file contains the same number of signals. - stack_axis : None, int, str, optional + stack_axis : None, int or str, default None If None (default), the signals are stacked over a new axis. The data must have the same dimensions. Otherwise, the signals are stacked over the axis given by its integer index or its name. The data must have the @@ -182,13 +269,12 @@ def load(filenames=None, The name of the new axis (default 'stack_element'), when `axis` is None. If an axis with this name already exists, it automatically appends '-i', where `i` are integers, until it finds a name that is not yet in use. - lazy : None, bool, optional + lazy : bool, default False Open the data lazily - i.e. without actually reading the data from the - disk until required. Allows opening arbitrary-sized datasets. The default - is False. - convert_units : bool, optional + disk until required. Allows opening arbitrary-sized datasets. + convert_units : bool, default False If True, convert the units using the `convert_to_units` method of - the `axes_manager`. If False (default), does nothing. + the `axes_manager`. If False, does nothing. escape_square_brackets : bool, default False If True, and ``filenames`` is a str containing square brackets, then square brackets are escaped before wildcard matching with @@ -196,16 +282,16 @@ def load(filenames=None, character classes (e.g. ``[a-z]`` matches lowercase letters). %s %s Only used with ``stack=True``. - load_original_metadata : bool + load_original_metadata : bool, default True If ``True``, all metadata contained in the input file will be added to ``original_metadata``. This does not affect parsing the metadata to ``metadata``. - reader : None, str, custom file reader object, optional + reader : None, str, module, optional Specify the file reader to use when loading the file(s). If None (default), will use the file extension to infer the file type and appropriate reader. If str, will select the appropriate file reader - from the list of available readers in HyperSpy. If a custom reader - object, it should implement the ``file_reader`` function, which returns + from the list of available readers in HyperSpy. If module, it must + implement the ``file_reader`` function, which returns a dictionary containing the data and metadata for conversion to a HyperSpy signal. print_info: bool, optional @@ -219,10 +305,18 @@ def load(filenames=None, pixel. This allows to improve signal and conserve the memory with the cost of lower resolution. cutoff_at_kV : None, int, float, optional - For Bruker bcf files, if set to numerical (default is None), - bcf is parsed into array with depth cutoff at coresponding given energy. + For Bruker bcf files and Jeol, if set to numerical (default is None), + hypermap is parsed into array with depth cutoff at set energy value. This allows to conserve the memory by cutting-off unused spectral tails, or force enlargement of the spectra size. + Bruker bcf reader accepts additional values for semi-automatic cutoff. + "zealous" value truncates to the last non zero channel (this option + should not be used for stacks, as low beam current EDS can have different + last non zero channel per slice). + "auto" truncates channels to SEM/TEM acceleration voltage or + energy at last channel, depending which is smaller. + In case the hv info is not there or hv is off (0 kV) then it fallbacks to + full channel range. select_type : 'spectrum_image', 'image', 'single_spectrum', None, optional If None (default), all data are loaded. For Bruker bcf and Velox emd files: if one of 'spectrum_image', 'image' @@ -274,52 +368,60 @@ def load(filenames=None, Returns ------- - Signal instance or list of signal instances + (list of) :class:`~.api.signals.BaseSignal` or subclass Examples -------- Loading a single file providing the signal type: - >>> d = hs.load('file.dm3', signal_type="EDS_TEM") + >>> d = hs.load('file.dm3', signal_type="EDS_TEM") # doctest: +SKIP Loading multiple files: - >>> d = hs.load(['file1.hspy','file2.hspy']) + >>> d = hs.load(['file1.hspy','file2.hspy']) # doctest: +SKIP Loading multiple files matching the pattern: - >>> d = hs.load('file*.hspy') + >>> d = hs.load('file*.hspy') # doctest: +SKIP Loading multiple files containing square brackets in the filename: - >>> d = hs.load('file[*].hspy', escape_square_brackets=True) + >>> d = hs.load('file[*].hspy', escape_square_brackets=True) # doctest: +SKIP Loading multiple files containing character classes (regular expression): - >>> d = hs.load('file[0-9].hspy') + >>> d = hs.load('file[0-9].hspy') # doctest: +SKIP Loading (potentially larger than the available memory) files lazily and stacking: - >>> s = hs.load('file*.blo', lazy=True, stack=True) + >>> s = hs.load('file*.blo', lazy=True, stack=True) # doctest: +SKIP Specify the file reader to use - >>> s = hs.load('a_nexus_file.h5', reader='nxs') + >>> s = hs.load('a_nexus_file.h5', reader='nxs') # doctest: +SKIP + + Loading a file containing several datasets: + + >>> s = hs.load("spameggsandham.nxs") # doctest: +SKIP + >>> s # doctest: +SKIP + [, + , + ] + + Use list indexation to access single signal + + >>> s[0] # doctest: +SKIP + """ - deprecated = ['mmap_dir', 'load_to_memory'] - warn_str = "'{}' argument is deprecated, please use 'lazy' instead" - for k in deprecated: - if k in kwds: - lazy = True - warnings.warn(warn_str.format(k), VisibleDeprecationWarning) - del kwds[k] - kwds['signal_type'] = signal_type - kwds['convert_units'] = convert_units - kwds['load_original_metadata'] = load_original_metadata + + kwds["signal_type"] = signal_type + kwds["convert_units"] = convert_units + kwds["load_original_metadata"] = load_original_metadata if filenames is None: from hyperspy.signal_tools import Load + load_ui = Load() get_gui(load_ui, toolkey="hyperspy.load") if load_ui.filename: @@ -328,40 +430,51 @@ def load(filenames=None, if filenames is None: raise ValueError("No file provided to reader") + pattern = None if isinstance(filenames, str): pattern = filenames if escape_square_brackets: filenames = _escape_square_brackets(filenames) - filenames = natsorted([f for f in glob.glob(filenames) - if os.path.isfile(f)]) - - if not filenames: - raise ValueError(f'No filename matches the pattern "{pattern}"') + filenames = natsorted( + [ + f + for f in glob.glob(filenames) + if os.path.isfile(f) + or (os.path.isdir(f) and os.path.splitext(f)[1] == ".zspy") + ] + ) elif isinstance(filenames, Path): + pattern = filenames # Just convert to list for now, pathlib.Path not # fully supported in io_plugins - filenames = [f for f in [filenames] if f.is_file()] + filenames = [ + f for f in [filenames] if f.is_file() or (f.is_dir() and ".zspy" in f.name) + ] elif isgenerator(filenames): filenames = list(filenames) - elif not isinstance(filenames, (list, tuple)): + elif not isinstance(filenames, (list, tuple, MutableMapping)): raise ValueError( - 'The filenames parameter must be a list, tuple, ' - f'string or None, not {type(filenames)}' + "The filenames parameter must be a list, tuple, " + f"string or None, not {type(filenames)}" ) if not filenames: - raise ValueError('No file(s) provided to reader.') + # in case, the file doesn't exist + raise ValueError(f'No filename matches the pattern "{pattern}"') - # pathlib.Path not fully supported in io_plugins, - # so convert to str here to maintain compatibility - filenames = [str(f) if isinstance(f, Path) else f for f in filenames] + if isinstance(filenames, MutableMapping): + filenames = [filenames] + else: + # pathlib.Path not fully supported in io_plugins, + # so convert to str here to maintain compatibility + filenames = [str(f) if isinstance(f, Path) else f for f in filenames] if len(filenames) > 1: - _logger.info('Loading individual files') + _logger.info("Loading individual files") if stack is True: # We are loading a stack! @@ -383,15 +496,15 @@ def load(filenames=None, if isinstance(obj, (list, tuple)): if n != len(obj): raise ValueError( - "The number of sub-signals per file does not match:\n" + - (f_error_fmt % (1, n, filenames[0])) + - (f_error_fmt % (i, len(obj), filename)) + "The number of sub-signals per file does not match:\n" + + (f_error_fmt % (1, n, filenames[0])) + + (f_error_fmt % (i, len(obj), filename)) ) elif n != 1: raise ValueError( - "The number of sub-signals per file does not match:\n" + - (f_error_fmt % (1, n, filenames[0])) + - (f_error_fmt % (i, len(obj), filename)) + "The number of sub-signals per file does not match:\n" + + (f_error_fmt % (1, n, filenames[0])) + + (f_error_fmt % (i, len(obj), filename)) ) # Append loaded signals to 2D list: @@ -405,7 +518,7 @@ def load(filenames=None, # When each file had N signals, we create N stacks! objects = [] for i in range(n): - signal = signals[i] # Sublist, with len = len(filenames) + signal = signals[i] # Sublist, with len = len(filenames) signal = stack_method( signal, axis=stack_axis, @@ -415,18 +528,21 @@ def load(filenames=None, show_progressbar=show_progressbar, ) signal.metadata.General.title = Path(filenames[0]).parent.stem - _logger.info('Individual files loaded correctly') + _logger.info("Individual files loaded correctly") _logger.info(signal._summary()) objects.append(signal) else: # No stack, so simply we load all signals in all files separately - objects = [load_single_file(filename, lazy=lazy, **kwds) for filename in filenames] + objects = [ + load_single_file(filename, lazy=lazy, **kwds) for filename in filenames + ] if len(objects) == 1: objects = objects[0] return objects + load.__doc__ %= (STACK_METADATA_ARG, SHOW_PROGRESSBAR_ARG) @@ -449,11 +565,16 @@ def load_single_file(filename, **kwds): Data loaded from the file. """ - if not os.path.isfile(filename): - raise FileNotFoundError(f"File: {filename} not found!") + # in case filename is a zarr store, we want to the path and not the store + path = _parse_path(filename) + + if not os.path.isfile(path) and not ( + os.path.isdir(path) and os.path.splitext(path)[1] == ".zspy" + ): + raise FileNotFoundError(f"File: {path} not found!") # File extension without "." separator - file_ext = os.path.splitext(filename)[1][1:] + file_ext = os.path.splitext(path)[1][1:] reader = kwds.pop("reader", None) if reader is None: @@ -467,8 +588,7 @@ def load_single_file(filename, **kwds): pass else: raise ValueError( - "`reader` should be one of None, str, " - "or a custom file reader object" + "`reader` should be one of None, str, " "or a custom file reader object" ) try: @@ -478,49 +598,66 @@ def load_single_file(filename, **kwds): except BaseException: _logger.error( "If this file format is supported, please " - "report this error to the HyperSpy developers." + "report this error to the RosettaSciIO developers at " + "https://github.com/hyperspy/rosettasciio/issues" ) raise def load_with_reader( - filename, - reader, - signal_type=None, - convert_units=False, - load_original_metadata=True, - **kwds - ): + filename, + reader, + signal_type=None, + convert_units=False, + load_original_metadata=True, + **kwds, +): """Load a supported file with a given reader.""" - lazy = kwds.get('lazy', False) - file_data_list = reader.file_reader(filename, **kwds) + lazy = kwds.get("lazy", False) + if isinstance(reader, dict): + file_data_list = importlib.import_module(reader["api"]).file_reader( + filename, **kwds + ) + else: + # We assume it is a module + file_data_list = reader.file_reader(filename, **kwds) signal_list = [] for signal_dict in file_data_list: - if 'metadata' in signal_dict: + if "metadata" in signal_dict: if "Signal" not in signal_dict["metadata"]: signal_dict["metadata"]["Signal"] = {} if signal_type is not None: - signal_dict['metadata']["Signal"]['signal_type'] = signal_type + signal_dict["metadata"]["Signal"]["signal_type"] = signal_type signal = dict2signal(signal_dict, lazy=lazy) - folder, filename = os.path.split(os.path.abspath(filename)) + signal = _add_file_load_save_metadata("load", signal, reader) + path = _parse_path(filename) + folder, filename = os.path.split(os.path.abspath(path)) filename, extension = os.path.splitext(filename) signal.tmp_parameters.folder = folder signal.tmp_parameters.filename = filename - signal.tmp_parameters.extension = extension.replace('.', '') + signal.tmp_parameters.extension = extension.replace(".", "") + # original_filename and original_file are used to keep track of + # where is the file which has been open lazily + signal.tmp_parameters.original_folder = folder + signal.tmp_parameters.original_filename = filename + signal.tmp_parameters.original_extension = extension.replace(".", "") # test if binned attribute is still in metadata - if signal.metadata.has_item('Signal.binned'): + if signal.metadata.has_item("Signal.binned"): for axis in signal.axes_manager.signal_axes: axis.is_binned = signal.metadata.Signal.binned del signal.metadata.Signal.binned - warnings.warn('Loading old file version. The binned attribute ' - 'has been moved from metadata.Signal to ' - 'axis.is_binned. Setting this attribute for all ' - 'signal axes instead.', VisibleDeprecationWarning) + warnings.warn( + "Loading old file version. The binned attribute " + "has been moved from metadata.Signal to " + "axis.is_binned. Setting this attribute for all " + "signal axes instead.", + VisibleDeprecationWarning, + ) if convert_units: signal.axes_manager.convert_units() if not load_original_metadata: - signal.original_metadata = type(signal.original_metadata)() + signal._original_metadata = type(signal.original_metadata)() signal_list.append(signal) else: # it's a standalone model @@ -557,32 +694,44 @@ def assign_signal_subclass(dtype, signal_dimension, signal_type="", lazy=False): """ # Check if parameter values are allowed: if np.issubdtype(dtype, np.complexfloating): - dtype = 'complex' - elif ('float' in dtype.name or 'int' in dtype.name or - 'void' in dtype.name or 'bool' in dtype.name or - 'object' in dtype.name): - dtype = 'real' + dtype = "complex" + elif ( + "float" in dtype.name + or "int" in dtype.name + or "void" in dtype.name + or "bool" in dtype.name + or "object" in dtype.name + ): + dtype = "real" else: raise ValueError(f'Data type "{dtype.name}" not understood!') if not isinstance(signal_dimension, int) or signal_dimension < 0: raise ValueError("signal_dimension must be a positive integer") - signals = {key: value for key, value in ALL_EXTENSIONS["signals"].items() - if value["lazy"] == lazy} - dtype_matches = {key: value for key, value in signals.items() - if value["dtype"] == dtype} - dtype_dim_matches = {key: value for key, value in dtype_matches.items() - if signal_dimension == value["signal_dimension"]} - dtype_dim_type_matches = {key: value for key, value in dtype_dim_matches.items() - if signal_type == value["signal_type"] or - "signal_type_aliases" in value and - signal_type in value["signal_type_aliases"]} + signals = { + key: value + for key, value in ALL_EXTENSIONS["signals"].items() + if value["lazy"] == lazy + } + dtype_matches = { + key: value for key, value in signals.items() if value["dtype"] == dtype + } + dtype_dim_matches = { + key: value + for key, value in dtype_matches.items() + if signal_dimension == value["signal_dimension"] + } + dtype_dim_type_matches = { + key: value + for key, value in dtype_dim_matches.items() + if signal_type == value["signal_type"] + or "signal_type_aliases" in value + and signal_type in value["signal_type_aliases"] + } valid_signal_types = [v["signal_type"] for v in signals.values()] valid_signal_aliases = [ - v["signal_type_aliases"] - for v in signals.values() - if "signal_type_aliases" in v + v["signal_type_aliases"] for v in signals.values() if "signal_type_aliases" in v ] valid_signal_aliases = [i for j in valid_signal_aliases for i in j] valid_signal_types.extend(valid_signal_aliases) @@ -602,14 +751,19 @@ def assign_signal_subclass(dtype, signal_dimension, signal_type="", lazy=False): # If the following dict is not empty, only signal_dimension and dtype match. # The dict should contain a general class for the given signal # dimension. - signal_dict = {key: value for key, value in dtype_dim_matches.items() - if value["signal_type"] == ""} + signal_dict = { + key: value + for key, value in dtype_dim_matches.items() + if value["signal_type"] == "" + } if not signal_dict: # no signal_dimension match either, hence select the general subclass for # correct dtype - signal_dict = {key: value for key, value in dtype_matches.items() - if value["signal_dimension"] == -1 - and value["signal_type"] == ""} + signal_dict = { + key: value + for key, value in dtype_matches.items() + if value["signal_dimension"] == -1 and value["signal_type"] == "" + } # Sanity check if len(signal_dict) > 1: _logger.warning( @@ -649,44 +803,50 @@ def dict2signal(signal_dict, lazy=False): 'currently installed. The signal will be loaded into a ' 'generic HyperSpy signal. Consider installing ' f'{signal_dict["package"]} to load this dataset into its ' - 'original signal class.') + 'original signal class.' + ) signal_dimension = -1 # undefined signal_type = "" - if "metadata" in signal_dict: - mp = signal_dict["metadata"] + mp = signal_dict.get("metadata") + + if mp is not None: if "Signal" in mp and "record_by" in mp["Signal"]: - record_by = mp["Signal"]['record_by'] + record_by = mp["Signal"]["record_by"] if record_by == "spectrum": signal_dimension = 1 elif record_by == "image": signal_dimension = 2 - del mp["Signal"]['record_by'] + del mp["Signal"]["record_by"] if "Signal" in mp and "signal_type" in mp["Signal"]: - signal_type = mp["Signal"]['signal_type'] + signal_type = mp["Signal"]["signal_type"] if "attributes" in signal_dict and "_lazy" in signal_dict["attributes"]: lazy = signal_dict["attributes"]["_lazy"] # "Estimate" signal_dimension from axes. It takes precedence over record_by - if ("axes" in signal_dict and - len(signal_dict["axes"]) == len( - [axis for axis in signal_dict["axes"] if "navigate" in axis])): - # If navigate is defined for all axes + if "axes" in signal_dict and len(signal_dict["axes"]) == len( + [axis for axis in signal_dict["axes"] if "navigate" in axis] + ): + # If navigate is defined for all axes signal_dimension = len( - [axis for axis in signal_dict["axes"] if not axis["navigate"]]) + [axis for axis in signal_dict["axes"] if not axis["navigate"]] + ) elif signal_dimension == -1: # If not defined, all dimension are categorised as signal signal_dimension = signal_dict["data"].ndim - signal = assign_signal_subclass(signal_dimension=signal_dimension, - signal_type=signal_type, - dtype=signal_dict['data'].dtype, - lazy=lazy)(**signal_dict) + signal = assign_signal_subclass( + signal_dimension=signal_dimension, + signal_type=signal_type, + dtype=signal_dict["data"].dtype, + lazy=lazy, + )(**signal_dict) if signal._lazy: signal._make_lazy() - if signal.axes_manager.signal_dimension != signal_dimension: - # This may happen when the signal dimension couldn't be matched with - # any specialised subclass - signal.axes_manager.set_signal_dimension(signal_dimension) + + # This may happen when the signal dimension couldn't be matched with + # any specialised subclass + signal.axes_manager._set_signal_dimension(signal_dimension) + if "post_process" in signal_dict: - for f in signal_dict['post_process']: + for f in signal_dict["post_process"]: signal = f(signal) if "mapping" in signal_dict: for opattr, (mpattr, function) in signal_dict["mapping"].items(): @@ -696,16 +856,15 @@ def dict2signal(signal_dict, lazy=False): value = function(value) if value is not None: signal.metadata.set_item(mpattr, value) - if "metadata" in signal_dict and "Markers" in mp: - markers_dict = markers_metadata_dict_to_markers( - mp['Markers'], - axes_manager=signal.axes_manager) - del signal.metadata.Markers - signal.metadata.Markers = markers_dict + if mp is not None and "Markers" in mp: + for key in mp["Markers"]: + signal.metadata.Markers[key] = markers_dict_to_markers(mp["Markers"][key]) + signal.metadata.Markers[key]._signal = signal + return signal -def save(filename, signal, overwrite=None, **kwds): +def save(filename, signal, overwrite=None, file_format=None, **kwds): """Save hyperspy signal to a file. A list of plugins supporting file saving can be found here: @@ -725,79 +884,129 @@ def save(filename, signal, overwrite=None, **kwds): to overwrite. If False and a file exists, the file will not be written. If True and a file exists, the file will be overwritten without prompting + file_format: string + The file format of choice to save the file. If not given, it is inferred + from the file extension. Returns ------- None """ - filename = Path(filename).resolve() - extension = filename.suffix - if extension == '': - extension = ".hspy" - filename = filename.with_suffix(extension) - writer = None - for plugin in io_plugins: - # Drop the "." separator from the suffix - if extension[1:].lower() in plugin.file_extensions: - writer = plugin - break - - if writer is None: - raise ValueError( - f"{extension} does not correspond to any supported format. " - f"Supported file extensions are: {strlist2enumeration(default_write_ext)}" - ) + if isinstance(filename, MutableMapping): + extension = ".zspy" + writer = _format_name_to_reader("ZSPY") + else: + filename = Path(filename).resolve() + extension = filename.suffix + if extension == "": + if file_format: + writer = _format_name_to_reader(file_format) + extension = "." + writer["file_extensions"][writer["default_extension"]] + else: + extension = ".hspy" + writer = _format_name_to_reader("HSPY") + filename = filename.with_suffix(extension) + else: + if file_format: + writer = _format_name_to_reader(file_format) + else: + writer = _infer_file_writer(extension[1:]) # Check if the writer can write sd = signal.axes_manager.signal_dimension nd = signal.axes_manager.navigation_dimension - if writer.writes is False: - raise ValueError( - "Writing to this format is not supported. " - f"Supported file extensions are: {strlist2enumeration(default_write_ext)}" - ) - - if writer.writes is not True and (sd, nd) not in writer.writes: - compatible_writers = [plugin.format_name for plugin in io_plugins - if plugin.writes is True or - plugin.writes is not False and - (sd, nd) in plugin.writes] + if writer["writes"] is not True and [sd, nd] not in writer["writes"]: + compatible_writers = [ + plugin["name"] + for plugin in IO_PLUGINS + if plugin["writes"] is True + or plugin["writes"] is not False + and [sd, nd] in plugin["writes"] + ] raise TypeError( "This file format does not support this data. " f"Please try one of {strlist2enumeration(compatible_writers)}" ) - if not writer.non_uniform_axis and not signal.axes_manager.all_uniform: - compatible_writers = [plugin.format_name for plugin in io_plugins - if plugin.non_uniform_axis is True] - raise TypeError("Writing to this format is not supported for " - "non-uniform axes. Use one of the following " - f"formats: {strlist2enumeration(compatible_writers)}" + if not writer["non_uniform_axis"] and not signal.axes_manager.all_uniform: + compatible_writers = [ + plugin["name"] + for plugin in IO_PLUGINS + if plugin["non_uniform_axis"] is True + ] + raise TypeError( + "Writing to this format is not supported for " + "non-uniform axes. Use one of the following " + f"formats: {strlist2enumeration(compatible_writers)}" ) # Create the directory if it does not exist - ensure_directory(filename.parent) - is_file = filename.is_file() - - if overwrite is None: - write = overwrite_method(filename) # Ask what to do - elif overwrite is True or (overwrite is False and not is_file): - write = True # Write the file - elif overwrite is False and is_file: - write = False # Don't write the file + if not isinstance(filename, MutableMapping): + ensure_directory(filename.parent) + is_file = filename.is_file() or ( + filename.is_dir() and os.path.splitext(filename)[1] == ".zspy" + ) + + if overwrite is None: + write = overwrite_method(filename) # Ask what to do + elif overwrite is True or (overwrite is False and not is_file): + write = True # Write the file + elif overwrite is False and is_file: + write = False # Don't write the file + else: + raise ValueError( + "`overwrite` parameter can only be None, True or " "False." + ) else: - raise ValueError("`overwrite` parameter can only be None, True or " - "False.") + write = True # file does not exist (creating it) if write: # Pass as a string for now, pathlib.Path not # properly supported in io_plugins - writer.file_writer(str(filename), signal, **kwds) + signal = _add_file_load_save_metadata("save", signal, writer) + signal_dic = signal._to_dictionary(add_models=True) + signal_dic["package_info"] = get_object_package_info(signal) + if not isinstance(filename, MutableMapping): + importlib.import_module(writer["api"]).file_writer( + str(filename), signal_dic, **kwds + ) + _logger.info(f"{filename} was created") + signal.tmp_parameters.set_item("folder", filename.parent) + signal.tmp_parameters.set_item("filename", filename.stem) + signal.tmp_parameters.set_item("extension", extension) + else: + importlib.import_module(writer["api"]).file_writer( + filename, signal_dic, **kwds + ) + if hasattr(filename, "path"): + file = Path(filename.path).resolve() + signal.tmp_parameters.set_item("folder", file.parent) + signal.tmp_parameters.set_item("filename", file.stem) + signal.tmp_parameters.set_item("extension", extension) + + +def _add_file_load_save_metadata(operation, signal, io_plugin): + mdata_dict = { + "operation": operation, + "io_plugin": io_plugin["api"] + if isinstance(io_plugin, dict) + else io_plugin.__loader__.name, + "hyperspy_version": hs_version, + "timestamp": datetime.now().astimezone().isoformat(), + } + # get the largest integer key present under General.FileIO, returning 0 + # as default if none are present + largest_index = max( + [ + int(i.replace("Number_", "")) + 1 + for i in signal.metadata.get_item("General.FileIO", {}).keys() + ] + + [0] + ) + + signal.metadata.set_item(f"General.FileIO.{largest_index}", mdata_dict) - _logger.info(f'{filename} was created') - signal.tmp_parameters.set_item('folder', filename.parent) - signal.tmp_parameters.set_item('filename', filename.stem) - signal.tmp_parameters.set_item('extension', extension) + return signal diff --git a/hyperspy/io_plugins/README b/hyperspy/io_plugins/README deleted file mode 100644 index e115837399..0000000000 --- a/hyperspy/io_plugins/README +++ /dev/null @@ -1,32 +0,0 @@ -Definition of a read/write plugin ---------------------------------- - -All the read/write plugins must provide a python file containing: - - - The characteristics of the IO plugin as the following python variables: - - # Plugin characteristics - # ---------------------- - format_name = - description = - full_support = # Whether all the Hyperspy features are supported - # Recognised file extension - file_extensions = - default_extension = # Index of the extension that will be used by default - # Reading capabilities - reads_images = - reads_spectrum = - reads_spectrum_image = - # Writing capabilities - writes_images = - writes_spectrum = - writes_spectrum_image = - # Support for non-uniform axis - non_uniform_axis = - - - A function called file_reader with at least one attribute: filename - - - A function called file_writer with at least two attributes: - filename and object2save in that order. - -They must also be declared in io.py diff --git a/hyperspy/io_plugins/__init__.py b/hyperspy/io_plugins/__init__.py deleted file mode 100644 index 6e4224512c..0000000000 --- a/hyperspy/io_plugins/__init__.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - - -import logging - -from hyperspy.io_plugins import ( - blockfile, - bruker, - dens, - digital_micrograph, - edax, - emd, - empad, - fei, - hspy, - image, - jeol, - mrc, - msa, - nexus, - phenom, - protochips, - ripple, - semper_unf, - sur, - tiff, -) - -io_plugins = [ - blockfile, - bruker, - dens, - digital_micrograph, - edax, - emd, - empad, - fei, - hspy, - image, - jeol, - mrc, - msa, - nexus, - phenom, - protochips, - ripple, - semper_unf, - sur, - tiff, -] - - -_logger = logging.getLogger(__name__) - - -try: - from hyperspy.io_plugins import netcdf - - io_plugins.append(netcdf) -except ImportError: - # NetCDF is obsolete and is only provided for users who have - # old EELSLab files. Therefore, we silently ignore if missing. - pass - -try: - from hyperspy.io_plugins import usid_hdf5 - - io_plugins.append(usid_hdf5) -except ImportError: - _logger.info( - "The USID IO plugin is not available because " - "the pyUSID or sidpy packages are not installed." - ) - -try: - from hyperspy.io_plugins import mrcz - - io_plugins.append(mrcz) -except ImportError: - _logger.info( - "The mrcz IO plugin is not available because " - "the mrcz package is not installed." - ) - - -default_write_ext = set() -for plugin in io_plugins: - if plugin.writes: - default_write_ext.add(plugin.file_extensions[plugin.default_extension]) diff --git a/hyperspy/io_plugins/blockfile.py b/hyperspy/io_plugins/blockfile.py deleted file mode 100644 index 8acbe77f57..0000000000 --- a/hyperspy/io_plugins/blockfile.py +++ /dev/null @@ -1,436 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import os -from traits.api import Undefined -import numpy as np -import dask -from dask.diagnostics import ProgressBar -from skimage.exposure import rescale_intensity -from skimage import dtype_limits -from tqdm import tqdm -import logging -import warnings -import datetime -import dateutil - -from hyperspy.misc.array_tools import sarray2dict, dict2sarray -from hyperspy.misc.date_time_tools import ( - serial_date_to_ISO_format, - datetime_to_serial_date, -) -from hyperspy.misc.utils import dummy_context_manager -from hyperspy.defaults_parser import preferences - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = "Blockfile" -description = "Read/write support for ASTAR blockfiles" -full_support = False -# Recognised file extension -file_extensions = ["blo", "BLO"] -default_extension = 0 -# Writing capabilities: -writes = [(2, 2), (2, 1), (2, 0)] -non_uniform_axis = False -magics = [0x0102] -# ---------------------- - - -mapping = { - "blockfile_header.Beam_energy": ( - "Acquisition_instrument.TEM.beam_energy", - lambda x: x * 1e-3, - ), - "blockfile_header.Camera_length": ( - "Acquisition_instrument.TEM.camera_length", - lambda x: x * 1e-4, - ), - "blockfile_header.Scan_rotation": ( - "Acquisition_instrument.TEM.rotation", - lambda x: x * 1e-2, - ), -} - - -def get_header_dtype_list(endianess="<"): - end = endianess - dtype_list = ( - [ - ("ID", (bytes, 6)), - ("MAGIC", end + "u2"), - ("Data_offset_1", end + "u4"), # Offset VBF - ("Data_offset_2", end + "u4"), # Offset DPs - ("UNKNOWN1", end + "u4"), # Flags for ASTAR software? - ("DP_SZ", end + "u2"), # Pixel dim DPs - ("DP_rotation", end + "u2"), # [degrees ( * 100 ?)] - ("NX", end + "u2"), # Scan dim 1 - ("NY", end + "u2"), # Scan dim 2 - ("Scan_rotation", end + "u2"), # [100 * degrees] - ("SX", end + "f8"), # Pixel size [nm] - ("SY", end + "f8"), # Pixel size [nm] - ("Beam_energy", end + "u4"), # [V] - ("SDP", end + "u2"), # Pixel size [100 * ppcm] - ("Camera_length", end + "u4"), # [10 * mm] - ("Acquisition_time", end + "f8"), # [Serial date] - ] - + [("Centering_N%d" % i, "f8") for i in range(8)] - + [("Distortion_N%02d" % i, "f8") for i in range(14)] - ) - - return dtype_list - - -def get_default_header(endianess="<"): - """Returns a header pre-populated with default values.""" - dt = np.dtype(get_header_dtype_list()) - header = np.zeros((1,), dtype=dt) - header["ID"][0] = "IMGBLO".encode() - header["MAGIC"][0] = magics[0] - header["Data_offset_1"][0] = 0x1000 # Always this value observed - header["UNKNOWN1"][0] = 131141 # Very typical value (always?) - header["Acquisition_time"][0] = datetime_to_serial_date( - datetime.datetime.fromtimestamp(86400, dateutil.tz.tzutc()) - ) - # Default to UNIX epoch + 1 day - # Have to add 1 day, as dateutil's timezones dont work before epoch - return header - - -def get_header_from_signal(signal, endianess="<"): - header = get_default_header(endianess) - if "blockfile_header" in signal.original_metadata: - header = dict2sarray( - signal.original_metadata["blockfile_header"], sarray=header - ) - note = signal.original_metadata["blockfile_header"]["Note"] - else: - note = "" - # The navigation and signal units are 'nm' and 'cm', respectively, so we - # convert the units accordingly before saving the signal - axes_manager = signal.axes_manager.deepcopy() - try: - axes_manager.convert_units("navigation", "nm") - axes_manager.convert_units("signal", "cm") - except Exception: - warnings.warn( - "BLO file expects cm units in signal dimensions and nm in navigation dimensions. " - "Existing units could not be converted; saving " - "axes scales as is. Beware that scales " - "will likely be incorrect in the file.", - UserWarning, - ) - - if axes_manager.navigation_dimension == 2: - NX, NY = axes_manager.navigation_shape - SX = axes_manager.navigation_axes[0].scale - SY = axes_manager.navigation_axes[1].scale - elif axes_manager.navigation_dimension == 1: - NX = axes_manager.navigation_shape[0] - NY = 1 - SX = axes_manager.navigation_axes[0].scale - SY = SX - elif axes_manager.navigation_dimension == 0: - NX = NY = SX = SY = 1 - - DP_SZ = axes_manager.signal_shape - if DP_SZ[0] != DP_SZ[1]: - raise ValueError("Blockfiles require signal shape to be square!") - DP_SZ = DP_SZ[0] - SDP = 100.0 / axes_manager.signal_axes[0].scale - - offset2 = NX * NY + header["Data_offset_1"] - # Based on inspected files, the DPs are stored at 16-bit boundary... - # Normally, you'd expect word alignment (32-bits) ¯\_(°_o)_/¯ - offset2 += offset2 % 16 - - header = dict2sarray( - { - "NX": NX, - "NY": NY, - "DP_SZ": DP_SZ, - "SX": SX, - "SY": SY, - "SDP": SDP, - "Data_offset_2": offset2, - }, - sarray=header, - ) - return header, note - - -def file_reader(filename, endianess="<", mmap_mode=None, lazy=False, **kwds): - _logger.debug("Reading blockfile: %s" % filename) - metadata = {} - if mmap_mode is None: - mmap_mode = "r" if lazy else "c" - # Makes sure we open in right mode: - if "+" in mmap_mode or ("write" in mmap_mode and "copyonwrite" != mmap_mode): - if lazy: - raise ValueError("Lazy loading does not support in-place writing") - f = open(filename, "r+b") - else: - f = open(filename, "rb") - _logger.debug("File opened") - - # Get header - header = np.fromfile(f, dtype=get_header_dtype_list(endianess), count=1) - if header["MAGIC"][0] not in magics: - warnings.warn( - "Blockfile has unrecognized header signature. " - "Will attempt to read, but correcteness not guaranteed!", - UserWarning, - ) - header = sarray2dict(header) - note = f.read(header["Data_offset_1"] - f.tell()) - # It seems it uses "\x00" for padding, so we remove it - try: - header["Note"] = note.decode("latin1").strip("\x00") - except BaseException: - # Not sure about the encoding so, if it fails, we carry on - _logger.warning( - "Reading the Note metadata of this file failed. " - "You can help improving " - "HyperSpy by reporting the issue in " - "https://github.com/hyperspy/hyperspy" - ) - _logger.debug("File header: " + str(header)) - NX, NY = int(header["NX"]), int(header["NY"]) - DP_SZ = int(header["DP_SZ"]) - if header["SDP"]: - SDP = 100.0 / header["SDP"] - else: - SDP = Undefined - original_metadata = {"blockfile_header": header} - - # Get data: - - # TODO A Virtual BF/DF is stored first, may be loaded as navigator in future - # offset1 = header['Data_offset_1'] - # f.seek(offset1) - # navigator = np.fromfile(f, dtype=endianess+"u1", shape=(NX, NY)).T - - # Then comes actual blockfile - offset2 = header["Data_offset_2"] - if not lazy: - f.seek(offset2) - data = np.fromfile(f, dtype=endianess + "u1") - else: - data = np.memmap(f, mode=mmap_mode, offset=offset2, dtype=endianess + "u1") - try: - data = data.reshape((NY, NX, DP_SZ * DP_SZ + 6)) - except ValueError: - warnings.warn( - "Blockfile header dimensions larger than file size! " - "Will attempt to load by zero padding incomplete frames." - ) - # Data is stored DP by DP: - pw = [(0, NX * NY * (DP_SZ * DP_SZ + 6) - data.size)] - data = np.pad(data, pw, mode="constant") - data = data.reshape((NY, NX, DP_SZ * DP_SZ + 6)) - - # Every frame is preceeded by a 6 byte sequence (AA 55, and then a 4 byte - # integer specifying frame number) - data = data[:, :, 6:] - data = data.reshape((NY, NX, DP_SZ, DP_SZ), order="C").squeeze() - - units = ["nm", "nm", "cm", "cm"] - names = ["y", "x", "dy", "dx"] - scales = [header["SY"], header["SX"], SDP, SDP] - date, time, time_zone = serial_date_to_ISO_format(header["Acquisition_time"]) - metadata = { - "General": { - "original_filename": os.path.split(filename)[1], - "date": date, - "time": time, - "time_zone": time_zone, - "notes": header["Note"], - }, - "Signal": { - "signal_type": "diffraction", - "record_by": "image", - }, - } - # Create the axis objects for each axis - dim = data.ndim - axes = [ - { - "size": data.shape[i], - "index_in_array": i, - "name": names[i], - "scale": scales[i], - "offset": 0.0, - "units": units[i], - } - for i in range(dim) - ] - - dictionary = { - "data": data, - "axes": axes, - "metadata": metadata, - "original_metadata": original_metadata, - "mapping": mapping, - } - - f.close() - return [ - dictionary, - ] - - -def file_writer(filename, signal, **kwds): - """ - Write signal to blockfile. - - Parameters - ---------- - file : str - Filename of the file to write to - signal : instance of hyperspy Signal2D - The signal to save. - endianess : str - '<' (default) or '>' determining how the bits are written to the file - intensity_scaling : str or 2-Tuple of float/int - If the signal datatype is not uint8 this argument provides intensity - linear scaling strategies. If 'dtype', the entire dtype range is mapped - to 0-255, if 'minmax' the range between the minimum and maximum intensity is - mapped to 0-255, if 'crop' the range between 0-255 is conserved without - overflow, if a tuple of values the values between this range is mapped - to 0-255. If None (default) no rescaling is performed and overflow is - permitted. - navigator_signal : str or Signal2D - A blo file also saves a virtual bright field image for navigation. - This option determines what kind of data is stored for this image. - The default option "navigator" uses the navigator image if it was - previously calculated, else it is calculated here which can take - some time for large datasets. Alternatively, a Signal2D of the right - shape may also be provided. If set to None, a zero array is stored - in the file. - """ - endianess = kwds.pop("endianess", "<") - scale_strategy = kwds.pop("intensity_scaling", None) - vbf_strategy = kwds.pop("navigator_signal", "navigator") - show_progressbar = kwds.pop("show_progressbar", None) - if scale_strategy is None: - # to distinguish from the tuple case - if signal.data.dtype != "u1": - warnings.warn( - "Data does not have uint8 dtype: values outside the " - "range 0-255 may result in overflow. To avoid this " - "use the 'intensity_scaling' keyword argument.", - UserWarning, - ) - elif scale_strategy == "dtype": - original_scale = dtype_limits(signal.data) - if original_scale[1] == 1.0: - raise ValueError("Signals with float dtype can not use 'dtype'") - elif scale_strategy == "minmax": - minimum = signal.data.min() - maximum = signal.data.max() - if signal._lazy: - minimum, maximum = dask.compute(minimum, maximum) - original_scale = (minimum, maximum) - elif scale_strategy == "crop": - original_scale = (0, 255) - else: - # we leave the error checking for incorrect tuples to skimage - original_scale = scale_strategy - - header, note = get_header_from_signal(signal, endianess=endianess) - with open(filename, "wb") as f: - # Write header - header.tofile(f) - # Write header note field: - if len(note) > int(header["Data_offset_1"]) - f.tell(): - note = note[: int(header["Data_offset_1"]) - f.tell() - len(note)] - f.write(note.encode()) - # Zero pad until next data block - zero_pad = int(header["Data_offset_1"]) - f.tell() - np.zeros((zero_pad,), np.byte).tofile(f) - # Write virtual bright field - if vbf_strategy is None: - vbf = np.zeros((signal.data.shape[0], signal.data.shape[1])) - elif isinstance(vbf_strategy, str) and (vbf_strategy == "navigator"): - if signal.navigator is not None: - vbf = signal.navigator.data - else: - if signal._lazy: - signal.compute_navigator() - vbf = signal.navigator.data - else: - # TODO workaround for non-lazy datasets - sigints = signal.axes_manager.signal_indices_in_array[:2] - vbf = signal.mean(axis=sigints).data - else: - vbf = vbf_strategy.data - # check that the shape is ok - if vbf.shape != signal.data.shape[:-2]: - raise ValueError( - "Size of the provided VBF does not match the " - "navigation dimensions of the dataset." - ) - if scale_strategy is not None: - vbf = rescale_intensity(vbf, in_range=original_scale, out_range=np.uint8) - vbf = vbf.astype(endianess + "u1") - vbf.tofile(f) - # Zero pad until next data block - if f.tell() > int(header["Data_offset_2"]): - raise ValueError( - "Signal navigation size does not match " "data dimensions." - ) - zero_pad = int(header["Data_offset_2"]) - f.tell() - np.zeros((zero_pad,), np.byte).tofile(f) - file_location = f.tell() - - if scale_strategy is not None: - signal = signal.map( - rescale_intensity, - in_range=original_scale, - out_range=np.uint8, - inplace=False, - ) - array_data = signal.data.astype(endianess + "u1") - # Write full data stack: - # We need to pad each image with magic 'AA55', then a u32 serial - pixels = array_data.shape[-2:] - records = array_data.shape[:-2] - record_dtype = [ - ("MAGIC", endianess + "u2"), - ("ID", endianess + "u4"), - ("IMG", endianess + "u1", pixels), - ] - magics = np.full(records, 0x55AA, dtype=endianess + "u2") - ids = np.arange(np.product(records), dtype=endianess + "u4").reshape(records) - file_memmap = np.memmap( - filename, dtype=record_dtype, mode="r+", offset=file_location, shape=records - ) - file_memmap["MAGIC"] = magics - file_memmap["ID"] = ids - if signal._lazy: - if show_progressbar is None: - show_progressbar = preferences.General.show_progressbar - cm = ProgressBar if show_progressbar else dummy_context_manager - with cm(): - signal.data.store(file_memmap["IMG"]) - else: - file_memmap["IMG"] = signal.data - file_memmap.flush() diff --git a/hyperspy/io_plugins/bruker.py b/hyperspy/io_plugins/bruker.py deleted file mode 100644 index 0e3e815085..0000000000 --- a/hyperspy/io_plugins/bruker.py +++ /dev/null @@ -1,1441 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2016 Petras Jokubauskas -# Copyright 2016 The HyperSpy developers -# -# This library is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with any project and source this library is coupled. -# If not, see . -# -# This python library subset provides read functionality of -# Bruker bcf files. -# The basic reading capabilities of proprietary AidAim Software(tm) -# SFS (Single File System) (used in bcf technology) is present in -# the same library. - - -# Plugin characteristics -# ---------------------- -from os.path import splitext, basename -from math import ceil -import re -import logging -from zlib import decompress as unzip_block -from struct import unpack as strct_unp -import dask.delayed as dd -import dask.array as da -import numpy as np -from datetime import datetime, timedelta -from ast import literal_eval -import codecs -import xml.etree.ElementTree as ET -from collections import defaultdict -import io -format_name = 'bruker composite file bcf' -description = """the proprietary format used by Bruker's -Esprit(R) software to save hypermaps together with 16bit SEM imagery, -EDS spectra and metadata describing the dimentions of the data and -SEM/TEM (limited) parameters""" -full_support = False -# Recognised file extension -file_extensions = ('bcf', 'spx') -default_extension = 0 -# Reading capabilities -reads_images = True -reads_spectrum = True -reads_spectrum_image = True -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -_logger = logging.getLogger(__name__) - -warn_once = True - -try: - from hyperspy.io_plugins import unbcf_fast - fast_unbcf = True - _logger.info("The fast cython based bcf unpacking library were found") -except ImportError: # pragma: no cover - fast_unbcf = False - _logger.info("""unbcf_fast library is not present... -Falling back to slow python only backend.""") - -# define re with two capturing groups with comma in between -# firstgroup looks for numeric value after (the '>' char) with or -# without minus sign, second group looks for numeric value with following -# closing <\tag> (the '<' char); '([Ee]-?\d*)' part (optionally a third group) -# checks for scientific notation (e.g. 8,843E-7 -> 'E-7'); -# compiled pattern is binary, as raw xml string is binary.: -fix_dec_patterns = re.compile(b'(>-?\\d+),(\\d*([Ee]-?\\d*)?<)') - - -class Container(object): - pass - - -class SFSTreeItem(object): - """Class to manage one internal sfs file. - - Reading, reading in chunks, reading and extracting, reading without - extracting even if compression is pressent. - - Attributes: - item_raw_string -- the bytes from sfs file table describing the file - parent -- the item higher hierarchicaly in the sfs file tree - - Methods: - read_piece, setup_compression_metadata, get_iter_and_properties, - get_as_BytesIO_string - """ - - def __init__(self, item_raw_string, parent): - self.sfs = parent - self._pointer_to_pointer_table, self.size, create_time, \ - mod_time, some_time, self.permissions, \ - self.parent, _, self.is_dir, _, name, _ = strct_unp( - ' 1: - next_chunk = self._pointer_to_pointer_table - temp_string = io.BytesIO() - for dummy1 in range(n_of_chunks): - fn.seek(self.sfs.chunksize * next_chunk + 0x118) - next_chunk = strct_unp(' 0: - fn.seek(self.pointers[lb_idx]) - data.write(fn.read(lbco)) - else: - fn.seek(self.pointers[fb_idx] + fbo) - data.write(fn.read(length)) - data.seek(0) - return data.read() - - def _iter_read_chunks(self, first=0): - """Generate and return iterator for reading and returning - sfs internal file in chunks. - - By default it creates iterator for whole file, however - with kwargs 'first' and 'chunks' the range of chunks - for iterator can be set. - - Keyword arguments: - first -- the index of first chunk from which to read. (default 0) - chunks -- the number of chunks to read. (default False) - """ - last = self.size_in_chunks - with open(self.sfs.filename, 'rb') as fn: - for idx in range(first, last - 1): - fn.seek(self.pointers[idx]) - yield fn.read(self.sfs.usable_chunk) - fn.seek(self.pointers[last - 1]) - last_stuff = self.size % self.sfs.usable_chunk - if last_stuff != 0: - yield fn.read(last_stuff) - else: - yield fn.read(self.sfs.usable_chunk) - - def setup_compression_metadata(self): - """ parse and setup the number of compression chunks - - and uncompressed chunk size as class attributes. - - Sets up attributes: - self.uncompressed_blk_size, self.no_of_compr_blk - - """ - with open(self.sfs.filename, 'rb') as fn: - fn.seek(self.pointers[0]) - # AACS signature, uncompressed size, undef var, number of blocks - aacs, uc_size, _, n_of_blocks = strct_unp('>> instance_of_SFSReader.get_file('catz/kitten.png') - - See also - -------- - SFSTreeItem - """ - item = self.vfs - - for i in path.split('/'): - item = item[i] - return item - - -def interpret(string): - """interpret any string and return casted to appropriate - dtype python object - """ - try: - return literal_eval(string) - except (ValueError, SyntaxError): - # SyntaxError due to: - # literal_eval have problems with strings like this '8842_80' - return string - - -def dictionarize(t): - d = {t.tag: {} if t.attrib else None} - children = list(t) - if children: - dd = defaultdict(list) - for dc in map(dictionarize, children): - for k, v in dc.items(): - dd[k].append(v) - d = {t.tag: {k: interpret(v[0]) if len( - v) == 1 else v for k, v in dd.items()}} - if t.attrib: - d[t.tag].update(('XmlClass' + k if list(t) else k, interpret(v)) - for k, v in t.attrib.items()) - if t.text: - text = t.text.strip() - if children or t.attrib: - if text: - d[t.tag]['#text'] = interpret(text) - else: - d[t.tag] = interpret(text) - if 'ClassInstance' in d: - return d['ClassInstance'] - else: - return d - - -class EDXSpectrum(object): - - def __init__(self, spectrum): - """ - Wrap the objectified bruker EDS spectrum xml part - to the python object, leaving all the xml and bruker clutter behind. - - Parameters - ---------- - spectrum : etree xml object - etree xml object, where spectrum.attrib['Type'] should - be 'TRTSpectrum'. - """ - TRTHeader = spectrum.find('./TRTHeaderedClass') - hardware_header = TRTHeader.find( - "./ClassInstance[@Type='TRTSpectrumHardwareHeader']") - detector_header = TRTHeader.find( - "./ClassInstance[@Type='TRTDetectorHeader']") - esma_header = TRTHeader.find( - "./ClassInstance[@Type='TRTESMAHeader']") - # what TRT means? - # ESMA could stand for Electron Scanning Microscope Analysis - spectrum_header = spectrum.find( - "./ClassInstance[@Type='TRTSpectrumHeader']") - xrf_header = TRTHeader.find("./ClassInstance[@Type='TRTXrfHeader']") - - # map stuff from harware xml branch: - self.hardware_metadata = dictionarize(hardware_header) - self.amplification = self.hardware_metadata['Amplification'] # USED - - # map stuff from detector xml branch - self.detector_metadata = dictionarize(detector_header) - self.detector_type = self.detector_metadata['Type'] # USED - - # decode silly hidden detector layer info: - det_l_str = self.detector_metadata['DetLayers'] - dec_det_l_str = codecs.decode(det_l_str.encode('ascii'), 'base64') - mini_xml = ET.fromstring(unzip_block(dec_det_l_str)) - self.detector_metadata['DetLayers'] = {} # Overwrite with dict - for i in list(mini_xml): - self.detector_metadata['DetLayers'][i.tag] = dict(i.attrib) - - # map stuff from esma xml branch: - if esma_header: - self.esma_metadata = dictionarize(esma_header) - if xrf_header: - xrf_header_dict = dictionarize(xrf_header) - self.esma_metadata = { - 'PrimaryEnergy':xrf_header_dict['Voltage'], - 'ElevationAngle':xrf_header_dict['ExcitationAngle'] - } - # USED: - self.hv = self.esma_metadata['PrimaryEnergy'] - self.elev_angle = self.esma_metadata['ElevationAngle'] - date_time = gen_iso_date_time(spectrum_header) - if date_time is not None: - self.date, self.time = date_time - - self.spectrum_metadata = dictionarize(spectrum_header) - self.offset = self.spectrum_metadata['CalibAbs'] - self.scale = self.spectrum_metadata['CalibLin'] - - # main data: - self.data = np.fromstring(spectrum.find('./Channels').text, - dtype='Q', sep=",") - - def energy_to_channel(self, energy, kV=True): - """ convert energy to channel index, - optional kwarg 'kV' (default: True) should be set to False - if given energy units is in V""" - if not kV: - en_temp = energy / 1000. - else: - en_temp = energy - return int(round((en_temp - self.offset) / self.scale)) - - -class HyperHeader(object): - """Wrap Bruker HyperMaping xml header into python object. - - Arguments: - xml_str -- the uncompressed to be provided with extracted Header xml - from bcf. - indexes -- list of indexes of available datasets - - Methods: - estimate_map_channels, estimate_map_depth - - If Bcf is version 2, the bcf can contain stacks - of hypermaps - thus header part can contain multiply sum eds spectras - and it's metadata per hypermap slice which can be selected using index. - Bcf can record number of imagery from different - imagining detectors (BSE, SEI, ARGUS, etc...): access to imagery - is throught image index. - """ - - def __init__(self, xml_str, indexes, instrument=None): - root = ET.fromstring(xml_str) - root = root.find("./ClassInstance[@Type='TRTSpectrumDatabase']") - try: - self.name = str(root.attrib['Name']) - except KeyError: - self.name = 'Undefinded' - _logger.info("hypermap have no name. Giving it 'Undefined' name") - hd = root.find("./Header") - self.date, self.time = gen_iso_date_time(hd) - self.version = int(literal_eval(hd.find('./FileVersion').text)) - # fill the sem and stage attributes: - self._set_microscope(root) - self._set_mode(instrument) - self._set_images(root) - self.elements = {} - self._set_elements(root) - self.line_counter = interpret(root.find('./LineCounter').text) - self.channel_count = int(root.find('./ChCount').text) - self.mapping_count = int(root.find('./DetectorCount').text) - #self.channel_factors = {} - self.spectra_data = {} - self._set_sum_edx(root, indexes) - - def _set_microscope(self, root): - """set microscope metadata from objectified xml part (TRTSEMData, - TRTSEMStageData, TRTDSPConfiguration). - - BCF can contain basic parameters of SEM column, and optionaly - the stage. This metadata can be not fully or at all availbale to - Esprit and saved into bcf file as it depends from license and - the link and implementation state between the microscope's - software and Bruker system. - """ - - semData = root.find("./ClassInstance[@Type='TRTSEMData']") - self.sem_metadata = dictionarize(semData) - # parse values for use in hspy metadata: - self.hv = self.sem_metadata.get('HV', 0.0) # in kV - # image/hypermap resolution in um/pixel: - if 'DX' in self.sem_metadata: - self.units = 'µm' - else: - self.units = 'pix' - self.x_res = self.sem_metadata.get('DX', 1.0) - self.y_res = self.sem_metadata.get('DY', 1.0) - # stage position: - semStageData = root.find("./ClassInstance[@Type='TRTSEMStageData']") - self.stage_metadata = dictionarize(semStageData) - # DSP configuration (always present, part of Bruker system): - DSPConf = root.find("./ClassInstance[@Type='TRTDSPConfiguration']") - self.dsp_metadata = dictionarize(DSPConf) - - def _set_mode(self, instrument=None): - if instrument is not None: - self.mode = instrument - else: - self.mode = guess_mode(self.hv) - - def get_acq_instrument_dict(self, detector=False, **kwargs): - """return python dictionary with aquisition instrument - mandatory data - """ - acq_inst = {'beam_energy': self.hv} - if 'Mag' in self.sem_metadata: - acq_inst['magnification'] = self.sem_metadata['Mag'] - if detector: - eds_metadata = self.get_spectra_metadata(**kwargs) - det = gen_detector_node(eds_metadata) - det['EDS']['real_time'] = self.calc_real_time() - acq_inst['Detector'] = det - return acq_inst - - def _parse_image(self, xml_node, overview=False): - """parse image from bruker xml image node.""" - if overview: - rect_node = xml_node.find("./ChildClassInstances" - "/ClassInstance[" - #"@Type='TRTRectangleOverlayElement' and " - "@Name='Map']/TRTSolidOverlayElement/" - "TRTBasicLineOverlayElement/TRTOverlayElement") - if rect_node is not None: - over_rect = dictionarize(rect_node)[ - 'TRTOverlayElement']['Rect'] - rect = {'y1': over_rect['Top'] * self.y_res, - 'x1': over_rect['Left'] * self.x_res, - 'y2': over_rect['Bottom'] * self.y_res, - 'x2': over_rect['Right'] * self.x_res} - over_dict = {'marker_type': 'Rectangle', - 'plot_on_signal': True, - 'data': rect, - 'marker_properties': {'color': 'yellow', - 'linewidth': 2}} - image = Container() - image.width = int(xml_node.find('./Width').text) # in pixels - image.height = int(xml_node.find('./Height').text) # in pixels - # in bytes ('u1','u2','u4') - image.dtype = 'u' + xml_node.find('./ItemSize').text - image.plane_count = int(xml_node.find('./PlaneCount').text) - image.images = [] - for i in range(image.plane_count): - img = xml_node.find("./Plane" + str(i)) - raw = codecs.decode( - (img.find('./Data').text).encode('ascii'), 'base64') - array1 = np.frombuffer(raw, dtype=image.dtype) - if any(array1): - item = self.gen_hspy_item_dict_basic() - data = array1.reshape((image.height, image.width)) - desc = img.find('./Description') - item['data'] = data - item['axes'][0]['size'] = image.height - item['axes'][1]['size'] = image.width - item['metadata']['Signal'] = {'record_by': 'image'} - item['metadata']['General'] = {} - if desc is not None: - item['metadata']['General']['title'] = str(desc.text) - if overview and (rect_node is not None): - item['metadata']['Markers'] = {'overview': over_dict} - image.images.append(item) - return image - - def _set_images(self, root): - """Wrap objectified xml part with image to class attributes - for self.image. - """ - image_nodes = root.findall("./ClassInstance[@Type='TRTImageData']") - for n in image_nodes: - if not(n.get('Name')): - image_node = n - self.image = self._parse_image(image_node) - if self.version == 2: - overview_node = root.findall( - "./ClassInstance[@Type='TRTContainerClass']" - "/ChildClassInstances" - "/ClassInstance[" - #"@Type='TRTContainerClass' and " - "@Name='OverviewImages']" - "/ChildClassInstances" - "/ClassInstance[@Type='TRTImageData']") - if len(overview_node) > 0: # in case there is no image - self.overview = self._parse_image( - overview_node[0], overview=True) - - def _set_elements(self, root): - """wrap objectified xml part with selection of elements to - self.elements list - """ - try: - elements = root.find( - "./ClassInstance[@Type='TRTContainerClass']" - "/ChildClassInstances" - "/ClassInstance[@Type='TRTElementInformationList']" - "/ClassInstance[@Type='TRTSpectrumRegionList']" - "/ChildClassInstances") - for j in elements.findall( - "./ClassInstance[@Type='TRTSpectrumRegion']"): - tmp_d = dictionarize(j) - self.elements[tmp_d['XmlClassName']] = {'line': tmp_d['Line'], - 'energy': tmp_d['Energy']} - except AttributeError: - _logger.info('no element selection present in the spectra..') - - def _set_sum_edx(self, root, indexes): - for i in indexes: - spec_node = root.find( - "./SpectrumData{0}/ClassInstance".format(str(i))) - self.spectra_data[i] = EDXSpectrum(spec_node) - - def estimate_map_channels(self, index=0): - """Estimate minimal size of energy axis so any spectra from any pixel - would not be truncated. - - Parameters - ---------- - index : int - Index of the map if multiply hypermaps are present in the same bcf. - - Returns - ------- - optimal channel number - """ - bruker_hv_range = self.spectra_data[index].amplification / 1000 - if self.hv >= bruker_hv_range: - return self.spectra_data[index].data.shape[0] - else: - return self.spectra_data[index].energy_to_channel(self.hv) - - def estimate_map_depth(self, index=0, downsample=1, for_numpy=False): - """Estimate minimal dtype of array using cumulative spectra - of the all pixels so that no data would be truncated. - - The method estimates the value from sum eds spectra, dividing - the maximum energy pulse value from raster x and y and to be on the - safe side multiplying by 2. - - Parameters - ---------- - index : int - Index of the hypermap if multiply hypermaps are present in the - same bcf. (default 0) - downsample : int - Downsample factor. (default 1) - for_numpy : bool - If False produce unsigned, otherwise signed types: if hypermap - will be loaded using the pure python function where numpy's inplace - integer addition will be used, the dtype should be signed; - If cython implementation will be used (default), then any returned - dtypes can be safely unsigned. (default False) - - Returns - ------- - depth : numpy.dtype - numpy dtype large enought to use in final hypermap numpy array. - - """ - sum_eds = self.spectra_data[index].data - # the most intensive peak is Bruker reference peak at 0kV: - roof = np.max(sum_eds) // self.image.width // self.image.height * 2 *\ - downsample * downsample - # this complicated nonsence bellow is due to numpy regression in adding - # integer inplace to unsigned integer array. (python integers is - # signed) - if roof > 0xFF: - if roof > 0xFFFF: - if for_numpy and (downsample > 1): - if roof > 0xEFFFFFFF: - depth = np.int64 - else: - depth = np.int32 - else: - depth = np.uint32 - else: - if for_numpy and (downsample > 1): - if roof > 0xEFFF: - depth = np.int32 - else: - depth = np.int16 - else: - depth = np.uint16 - else: - if for_numpy and (downsample > 1): - if roof > 0xEF: - depth = np.int16 - else: - depth = np.int8 - else: - depth = np.uint8 - return depth - - def get_spectra_metadata(self, index=0): - """return objectified xml with spectra metadata - Arguments: - index -- index of hypermap/spectra (default 0) - """ - return self.spectra_data[index] - - def calc_real_time(self): - """calculate and return real time for whole hypermap - in seconds - """ - line_cnt_sum = np.sum(self.line_counter, dtype=np.float64) - line_avg = self.dsp_metadata['LineAverage'] - pix_avg = self.dsp_metadata['PixelAverage'] - pix_time = self.dsp_metadata['PixelTime'] - width = self.image.width - real_time = line_cnt_sum * line_avg * pix_avg * pix_time * width * 1E-6 - return float(real_time) - - def gen_hspy_item_dict_basic(self): - i = {'axes': [{'name': 'height', - 'offset': 0, - 'scale': self.y_res, - 'units': self.units}, - {'name': 'width', - 'offset': 0, - 'scale': self.x_res, - 'units': self.units}], - 'metadata': { - 'Acquisition_instrument': - {self.mode: self.get_acq_instrument_dict()}, - 'Sample': {'name': self.name}, - }, - 'original_metadata': { - 'Microscope': self.sem_metadata, - 'DSP Configuration': self.dsp_metadata, - 'Stage': self.stage_metadata - } - } - return i - - -class BCF_reader(SFS_reader): - - """Class to read bcf (Bruker hypermapping) file. - - Inherits SFS_reader and all its attributes and methods. - - Attributes: - filename - - Methods: - check_index_valid, parse_hypermap - - The class instantiates HyperHeader class as self.header attribute - where all metadata, sum eds spectras, (SEM) images are stored. - """ - - def __init__(self, filename, instrument=None): - SFS_reader.__init__(self, filename) - header_file = self.get_file('EDSDatabase/HeaderData') - self.available_indexes = [] - # get list of presented indexes from file tree of binary sfs container - # while looking for file names containg the hypercube data: - for i in self.vfs['EDSDatabase'].keys(): - if 'SpectrumData' in i: - self.available_indexes.append(int(i[-1])) - self.def_index = min(self.available_indexes) - header_byte_str = header_file.get_as_BytesIO_string().getvalue() - hd_bt_str = fix_dec_patterns.sub(b'\\1.\\2', header_byte_str) - self.header = HyperHeader( - hd_bt_str, self.available_indexes, instrument=instrument) - self.hypermap = {} - - def check_index_valid(self, index): - """check and return if index is valid""" - if not isinstance(index, int): - raise TypeError("provided index should be integer") - if index not in self.available_indexes: - raise IndexError("requisted index is not in the list of available indexes. " - "Available maps are under indexes: {0}".format(str(self.available_indexes))) - return index - - def parse_hypermap(self, index=None, - downsample=1, cutoff_at_kV=None, - lazy=False): - """Unpack the Delphi/Bruker binary spectral map and return - numpy array in memory efficient way. - - Pure python/numpy implementation -- slow, or - cython/memoryview/numpy implimentation if compilied and present - (fast) is used. - - Parameters - ---------- - index : None or int - The index of hypermap in bcf if there is more than one - hyper map in file. - downsample : int - Downsampling factor. Differently than block_reduce from - skimage.measure, the parser populates reduced array by suming - results of pixels, thus having lower memory requiriments. Default - is 1. - cutoff_at_kV : None or float - Value in keV to truncate the array at. Helps reducing size of - array. Default is None. - lazy : bool - It True, returns dask.array otherwise a numpy.array. Default is - False. - - Returns - ------- - result : numpy.ndarray or dask.array.array - Bruker hypermap, with (y,x,E) shape. - """ - if index is None: - index = self.def_index - if type(cutoff_at_kV) in (int, float): - eds = self.header.spectra_data[index] - max_chan = eds.energy_to_channel(cutoff_at_kV) - else: - max_chan = self.header.estimate_map_channels(index=index) - shape = (ceil(self.header.image.height / downsample), - ceil(self.header.image.width / downsample), - max_chan) - sfs_file = SFS_reader(self.filename) - vrt_file_hand = sfs_file.get_file( - 'EDSDatabase/SpectrumData' + str(index)) - if fast_unbcf: - parse_func = unbcf_fast.parse_to_numpy - dtype = self.header.estimate_map_depth(index=index, - downsample=downsample, - for_numpy=False) - else: - parse_func = py_parse_hypermap - dtype = self.header.estimate_map_depth(index=index, - downsample=downsample, - for_numpy=True) - if lazy: - value = dd(parse_func)(vrt_file_hand, shape, - dtype, downsample=downsample) - result = da.from_delayed(value, shape=shape, dtype=dtype) - else: - result = parse_func(vrt_file_hand, shape, - dtype, downsample=downsample) - return result - - def add_filename_to_general(self, item): - """hypy helper method""" - item['metadata']['General']['original_filename'] = \ - basename(self.filename) - - -def spx_reader(filename, lazy=False): - with open(filename, 'br') as fn: - xml_str = fn.read() - root = ET.fromstring(xml_str) - sp_node = root.find("./ClassInstance[@Type='TRTSpectrum']") - try: - name = str(sp_node.attrib['Name']) - except KeyError: - name = 'Undefinded' - _logger.info("spectra have no name. Giving it 'Undefined' name") - spectrum = EDXSpectrum(sp_node) - mode = guess_mode(spectrum.hv) - results_xml = sp_node.find("./ClassInstance[@Type='TRTResult']") - elements_xml = sp_node.find("./ClassInstance[@Type='TRTPSEElementList']") - hy_spec = {'data': spectrum.data, - 'axes': [{'name': 'Energy', - 'size': len(spectrum.data), - 'offset': spectrum.offset, - 'scale': spectrum.scale, - 'units': 'keV'}], - 'metadata': - # where is no way to determine what kind of instrument was used: - # TEM or SEM - {'Acquisition_instrument': { - mode: {'Detector': - gen_detector_node(spectrum), - 'beam_energy': spectrum.hv} - }, - 'General': {'original_filename': basename(filename), - 'title': 'EDX', - 'date': spectrum.date, - 'time': spectrum.time}, - 'Sample': {'name': name}, - 'Signal': {'signal_type': 'EDS_%s' % mode, - 'record_by': 'spectrum', - 'quantity': 'X-rays (Counts)'} - }, - 'original_metadata': {'Hardware': spectrum.hardware_metadata, - 'Detector': spectrum.detector_metadata, - 'Analysis': spectrum.esma_metadata, - 'Spectrum': spectrum.spectrum_metadata, } - } - if results_xml is not None: - hy_spec['original_metadata']['Results'] = dictionarize(results_xml) - if elements_xml is not None: - elem = dictionarize(elements_xml)['ChildClassInstances'] - hy_spec['original_metadata']['Selected_elements'] = elem - hy_spec['metadata']['Sample']['elements'] = elem['XmlClassName'] - return [hy_spec] - - -# dict of nibbles to struct notation for reading: -st = {1: 'B', 2: 'B', 4: 'H', 8: 'I', 16: 'Q'} - - -def py_parse_hypermap(virtual_file, shape, dtype, downsample=1): - """Unpack the Delphi/Bruker binary spectral map and return - numpy array in memory efficient way using pure python implementation. - (Slow!) - - The function is long and complicated due to complexity of Delphi packed - array. - Whole parsing is placed in one function to reduce overhead of - python function calls. For cleaner parsing logic, please, see - fast cython implementation at hyperspy/io_plugins/unbcf_fast.pyx - - The method is only meant to be used if for some - reason c (generated with cython) version of the parser is not compiled. - - Parameters - ---------- - virtual_file -- virtual file handle returned by SFS_reader instance - or by object inheriting it (e.g. BCF_reader instance) - shape -- numpy shape - dtype -- numpy dtype - downsample -- downsample factor - - note!: downsample, shape and dtype are interconnected and needs - to be properly calculated otherwise wrong output or segfault - is expected - - Return - ------ - numpy array of bruker hypermap, with (y, x, E) shape. - """ - iter_data, size_chnk = virtual_file.get_iter_and_properties()[:2] - dwn_factor = downsample - max_chan = shape[2] - buffer1 = next(iter_data) - height, width = strct_unp('= size: - size = size_chnk + size - offset - buffer1 = buffer1[offset:] + next(iter_data) - offset = 0 - line_head = strct_unp('= size: - size = size_chnk + size - offset - buffer1 = buffer1[offset:] + next(iter_data) - offset = 0 - # the pixel header contains such information: - # x index of pixel (uint32); - # number of channels for whole mapping (unit16); - # number of channels for pixel (uint16); - # dummy placehollder (same value in every known bcf) (32bit); - # flag distinguishing packing data type (16bit): - # 0 - 16bit packed pulses, 1 - 12bit packed pulses, - # >1 - instructively packed spectra; - # value which sometimes shows the size of packed data (uint16); - # number of pulses if pulse data are present (uint16) or - # additional pulses to the instructively packed data; - # packed data size (32bit) (without additional pulses) \ - # next header is after that amount of bytes; - x_pix, chan1, chan2, dummy1, flag, dummy_size1, n_of_pulses,\ - data_size2 = strct_unp('= size: - buffer1 = buffer1[offset:] + next(iter_data) - size = size_chnk + size - offset - offset = 0 - if flag == 0: - data1 = buffer1[offset:offset + data_size2] - arr16 = np.frombuffer(data1, dtype=np.uint16) - pixel = np.bincount(arr16, minlength=chan1 - 1) - offset += data_size2 - elif flag == 1: # and (chan1 != chan2) - # Unpack packed 12-bit data to 16-bit uints: - data1 = buffer1[offset:offset + data_size2] - switched_i2 = np.frombuffer(data1, - dtype='>= 4 # Shift every second short by 4 - exp16 &= np.uint16(0x0FFF) # Mask all shorts to 12bit - pixel = np.bincount(exp16, minlength=chan1 - 1) - offset += data_size2 - else: # flag > 1 - # Unpack instructively packed data to pixel channels: - pixel = [] - the_end = offset + data_size2 - 4 - while offset < the_end: - # this would work on py3 - #size_p, channels = buffer1[offset:offset + 2] - # this is needed on py2: - size_p, channels = strct_unp('> 4) + gain - pixel += g[:channels] - else: - length = int(channels * size_p / 2) - temp = strct_unp('<' + channels * st[size_p], - buffer1[offset:offset + length]) - pixel += [l + gain for l in temp] - offset += length - if chan2 < chan1: - rest = chan1 - chan2 - pixel += rest * [0] - # additional data size: - if n_of_pulses > 0: - add_s = strct_unp('= size: - buffer1 = buffer1[offset:] + next(iter_data) - size = size_chnk + size - offset - offset = 0 - # the additional pulses: - add_pulses = strct_unp('<' + 'H' * n_of_pulses, - buffer1[offset:offset + add_s]) - offset += add_s - for i in add_pulses: - pixel[i] += 1 - else: - offset += 4 - # if no downsampling is needed, or if it is first - # pixel encountered with downsampling on, then - # use assigment, which is ~4 times faster, than inplace add - if max_chan < chan1: # if pixel have more channels than we need - chan1 = max_chan - if (dwn_factor == 1): - vfa[max_chan * pix_idx:chan1 + max_chan * pix_idx] =\ - pixel[:chan1] - else: - vfa[max_chan * pix_idx:chan1 + max_chan * pix_idx] +=\ - pixel[:chan1] - vfa.resize((ceil(height / dwn_factor), - ceil(width / dwn_factor), - max_chan)) - # check if array is signed, and convert to unsigned - if str(vfa.dtype)[0] == 'i': - new_dtype = ''.join(['u', str(vfa.dtype)]) - vfa.dtype = new_dtype - return vfa - - -def file_reader(filename, *args, **kwds): - ext = splitext(filename)[1][1:] - if ext == 'bcf': - return bcf_reader(filename, *args, **kwds) - elif ext == 'spx': - return spx_reader(filename, *args, **kwds) - - -def bcf_reader(filename, select_type=None, index=None, # noqa - downsample=1, cutoff_at_kV=None, instrument=None, lazy=False): - """Reads a bruker bcf file and loads the data into the appropriate class, - then wraps it into appropriate hyperspy required list of dictionaries - used by hyperspy.api.load() method. - - Parameters - ---------- - select_type : str or None - One of: spectrum_image, image. If none specified, then function - loads everything, else if specified, loads either just sem imagery, - or just hyper spectral mapping data (default None). - index : int, None or str - Index of dataset in bcf v2 can be None integer and 'all' - (default None); None will select first available mapping if more than - one. 'all' will return all maps if more than one present; - integer will return only selected map. - downsample : int - the downsample ratio of hyperspectral array (downsampling - height and width only), can be integer from 1 to inf, where '1' means - no downsampling will be applied. (default 1) - cutoff_at_kV : int, float or None - if set (can be int of float >= 0) can be used either, to - crop or enlarge energy range at max values. (default None) - instrument : str or None - Can be either 'TEM' or 'SEM'. Default is None. - """ - - # objectified bcf file: - obj_bcf = BCF_reader(filename, instrument=instrument) - if select_type == 'spectrum': - select_type = 'spectrum_image' - from hyperspy.misc.utils import deprecation_warning - msg = ( - "The 'spectrum' option for the `select_type` parameter is " - "deprecated and will be removed in v2.0. Use 'spectrum_image' " - "instead.") - deprecation_warning(msg) - if select_type == 'image': - return bcf_images(obj_bcf) - elif select_type == 'spectrum_image': - return bcf_hyperspectra(obj_bcf, index=index, - downsample=downsample, - cutoff_at_kV=cutoff_at_kV, - lazy=lazy) - else: - return bcf_images(obj_bcf) + bcf_hyperspectra( - obj_bcf, - index=index, - downsample=downsample, - cutoff_at_kV=cutoff_at_kV, - lazy=lazy) - - -def bcf_images(obj_bcf): - """ return hyperspy required list of dict with sem - images and metadata. - """ - images_list = [] - for img in obj_bcf.header.image.images: - obj_bcf.add_filename_to_general(img) - images_list.append(img) - if hasattr(obj_bcf.header, 'overview'): - for img2 in obj_bcf.header.overview.images: - obj_bcf.add_filename_to_general(img2) - images_list.append(img2) - return images_list - - -def bcf_hyperspectra(obj_bcf, index=None, downsample=None, cutoff_at_kV=None, # noqa - lazy=False): - """ Return hyperspy required list of dict with eds - hyperspectra and metadata. - """ - global warn_once - if (fast_unbcf == False) and warn_once: - _logger.warning("""unbcf_fast library is not present... -Parsing BCF with Python-only backend, which is slow... please wait. -If parsing is uncomfortably slow, first install cython, then reinstall hyperspy. -For more information, check the 'Installing HyperSpy' section in the documentation.""") - warn_once = False - if index is None: - indexes = [obj_bcf.def_index] - elif index == 'all': - indexes = obj_bcf.available_indexes - else: - indexes = [obj_bcf.check_index_valid(index)] - hyperspectra = [] - mode = obj_bcf.header.mode - mapping = get_mapping(mode) - for index in indexes: - hypermap = obj_bcf.parse_hypermap(index=index, - downsample=downsample, - cutoff_at_kV=cutoff_at_kV, - lazy=lazy) - eds_metadata = obj_bcf.header.get_spectra_metadata(index=index) - hyperspectra.append( - {'data': hypermap, - 'axes': [{'name': 'height', - 'size': hypermap.shape[0], - 'offset': 0, - 'scale': obj_bcf.header.y_res * downsample, - 'units': obj_bcf.header.units}, - {'name': 'width', - 'size': hypermap.shape[1], - 'offset': 0, - 'scale': obj_bcf.header.y_res * downsample, - 'units': obj_bcf.header.units}, - {'name': 'Energy', - 'size': hypermap.shape[2], - 'offset': eds_metadata.offset, - 'scale': eds_metadata.scale, - 'units': 'keV'}], - 'metadata': - # where is no way to determine what kind of instrument was used: - # TEM or SEM - {'Acquisition_instrument': { - mode: obj_bcf.header.get_acq_instrument_dict( - detector=True, - index=index) - }, - 'General': {'original_filename': basename(obj_bcf.filename), - 'title': 'EDX', - 'date': obj_bcf.header.date, - 'time': obj_bcf.header.time}, - 'Sample': {'name': obj_bcf.header.name, - 'elements': sorted(list(obj_bcf.header.elements)), - 'xray_lines': sorted(gen_elem_list(obj_bcf.header.elements))}, - 'Signal': {'signal_type': 'EDS_%s' % mode, - 'record_by': 'spectrum', - 'quantity': 'X-rays (Counts)'} - }, - 'original_metadata': {'Hardware': eds_metadata.hardware_metadata, - 'Detector': eds_metadata.detector_metadata, - 'Analysis': eds_metadata.esma_metadata, - 'Spectrum': eds_metadata.spectrum_metadata, - 'DSP Configuration': obj_bcf.header.dsp_metadata, - 'Line counter': obj_bcf.header.line_counter, - 'Stage': obj_bcf.header.stage_metadata, - 'Microscope': obj_bcf.header.sem_metadata}, - 'mapping': mapping, - }) - return hyperspectra - - -def gen_elem_list(the_dict): - return ['_'.join([i, parse_line(the_dict[i]['line'])]) for i in the_dict] - - -def parse_line(line_string): - """standardize line describtion. - - Bruker saves line description in all caps - and omits the type if only one exists instead of - using alfa""" - if len(line_string) == 1: - line_string = line_string + 'a' - elif len(line_string) > 2: - line_string = line_string[:2] - return line_string.capitalize() - - -def get_mapping(mode): - return { - 'Stage.Rotation': - ("Acquisition_instrument.%s.Stage.rotation" % mode, None), - 'Stage.Tilt': - ("Acquisition_instrument.%s.Stage.tilt_alpha" % mode, None), - 'Stage.X': - ("Acquisition_instrument.%s.Stage.x" % mode, None), - 'Stage.Y': - ("Acquisition_instrument.%s.Stage.y" % mode, None), - 'Stage.Z': - ("Acquisition_instrument.%s.Stage.z" % mode, None), - } - - -def guess_mode(hv): - """there is no way to determine what kind of instrument - was used from metadata: TEM or SEM. - However simple guess can be made using the acceleration - voltage, assuming that SEM is <= 30kV or TEM is >30kV""" - if hv > 30.0: - mode = 'TEM' - else: - mode = 'SEM' - _logger.info( - "Guessing that the acquisition instrument is %s " % mode + - "because the beam energy is %i keV. If this is wrong, " % hv + - "please provide the right instrument using the 'instrument' " + - "keyword.") - return mode - - -def gen_detector_node(spectrum): - eds_dict = {'EDS': {'elevation_angle': spectrum.elev_angle, - 'detector_type': spectrum.detector_type, }} - if 'AzimutAngle' in spectrum.esma_metadata: - eds_dict['EDS']['azimuth_angle'] = spectrum.esma_metadata['AzimutAngle'] - if 'RealTime' in spectrum.hardware_metadata: - eds_dict['EDS']['real_time'] = spectrum.hardware_metadata['RealTime'] / 1000 - eds_dict['EDS']['live_time'] = spectrum.hardware_metadata['LifeTime'] / 1000 - return eds_dict - - -def gen_iso_date_time(node): - date_xml = node.find('./Date') - time_xml = node.find('./Time') - if date_xml is not None: - dt = datetime.strptime(' '.join([date_xml.text, time_xml.text]), - "%d.%m.%Y %H:%M:%S") - date = dt.date().isoformat() - time = dt.time().isoformat() - return date, time diff --git a/hyperspy/io_plugins/dens.py b/hyperspy/io_plugins/dens.py deleted file mode 100644 index fedd6fcb49..0000000000 --- a/hyperspy/io_plugins/dens.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - - -import numpy as np -import os -import scipy -from datetime import datetime - - -# Plugin characteristics -# ---------------------- -format_name = 'DENS' -description = 'Reads heater log from a DENS heating holder' -version = '3.1' -full_support = False -# Recognised file extension -file_extensions = ['dens', 'DENS'] -default_extension = 0 -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -def _cnv_time(timestr): - try: - t = datetime.strptime(timestr.decode(), '%H:%M:%S.%f') - dt = t - datetime(t.year, t.month, t.day) - r = float(dt.seconds) + float(dt.microseconds) * 1e-6 - except ValueError: - r = float(timestr) - return r - - -def _bad_file(filename): - raise AssertionError("Cannot interpret as DENS heater log: %s" % filename) - - -def file_reader(filename, *args, **kwds): - with open(filename, 'rt') as f: - # Strip leading, empty lines - line = str(f.readline()) - while line.strip() == '' and not f.closed: - line = str(f.readline()) - try: - date, version = line.split('\t') - except ValueError: - _bad_file(filename) - if version.strip() != 'Digiheater 3.1': - _bad_file(filename) - calib = str(f.readline()).split('\t') - str(f.readline()) # delta_t - header_line = str(f.readline()) - try: - R0, a, b, c = [float(v.split('=')[1]) for v in calib] - date0 = datetime.strptime(date, "%d/%m/'%y %H:%M") - date = '%s' % date0.date() - time = '%s' % date0.time() - except ValueError: - _bad_file(filename) - original_metadata = dict(R0=R0, a=a, b=b, c=c, date=date0, - version=version) - - if header_line.strip() != ( - 'sample\ttime\tTset[C]\tTmeas[C]\tRheat[ohm]\tVheat[V]\t' - 'Iheat[mA]\tPheat [mW]\tc'): - _bad_file(filename) - try: - rawdata = np.loadtxt(f, converters={1: _cnv_time}, usecols=(1, 3), - unpack=True) - except ValueError: - _bad_file(filename) - - times = rawdata[0] - # Add a day worth of seconds to any values after a detected rollover - # Hopefully unlikely that there is more than one, but we can handle it - for rollover in 1 + np.where(np.diff(times) < 0)[0]: - times[rollover:] += 60 * 60 * 24 - # Raw data is not necessarily grid aligned. Interpolate onto grid. - dt = np.diff(times).mean() - temp = rawdata[1] - interp = scipy.interpolate.interp1d(times, temp, copy=False, - assume_sorted=True, bounds_error=False) - interp_axis = times[0] + dt * np.array(range(len(times))) - temp_interp = interp(interp_axis) - - metadata = {'General': {'original_filename': os.path.split(filename)[1], - 'date': date, - 'time': time}, - "Signal": {'signal_type': "", - 'quantity': "Temperature (Celsius)"}, } - - axes = [{ - 'size': len(temp_interp), - 'index_in_array': 0, - 'name': 'Time', - 'scale': dt, - 'offset': times[0], - 'units': 's', - 'navigate': False, - }] - - dictionary = {'data': temp_interp, - 'axes': axes, - 'metadata': metadata, - 'original_metadata': {'DENS_header': original_metadata}, - } - - return [dictionary, ] diff --git a/hyperspy/io_plugins/digital_micrograph.py b/hyperspy/io_plugins/digital_micrograph.py deleted file mode 100644 index ec2893e527..0000000000 --- a/hyperspy/io_plugins/digital_micrograph.py +++ /dev/null @@ -1,1140 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2010 Stefano Mazzucco -# Copyright 2011-2020 The HyperSpy developers -# -# This file is part of HyperSpy. It is a fork of the original PIL dm3 plugin -# written by Stefano Mazzucco. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -# Plugin to read the Gatan Digital Micrograph(TM) file format - - -import os -import logging -import dateutil.parser - -import numpy as np -import traits.api as t -from copy import deepcopy - -import hyperspy.misc.io.utils_readfile as iou -from hyperspy.exceptions import DM3TagIDError, DM3DataTypeError, DM3TagTypeError -import hyperspy.misc.io.tools -from hyperspy.misc.utils import DictionaryTreeBrowser -from hyperspy.docstrings.signal import OPTIMIZE_ARG - - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = 'Digital Micrograph dm3' -description = 'Read data from Gatan Digital Micrograph (TM) files' -full_support = False -# Recognised file extension -file_extensions = ('dm3', 'DM3', 'dm4', 'DM4') -default_extension = 0 -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -class DigitalMicrographReader(object): - - """ Class to read Gatan Digital Micrograph (TM) files. - - Currently it supports versions 3 and 4. - - Attributes - ---------- - dm_version, endian, tags_dict - - Methods - ------- - parse_file, parse_header, get_image_dictionaries - - """ - - _complex_type = (15, 18, 20) - simple_type = (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) - - def __init__(self, f): - self.dm_version = None - self.endian = None - self.tags_dict = None - self.f = f - - def parse_file(self): - self.f.seek(0) - self.parse_header() - self.tags_dict = {"root": {}} - number_of_root_tags = self.parse_tag_group()[2] - _logger.info('Total tags in root group: %s', number_of_root_tags) - self.parse_tags( - number_of_root_tags, - group_name="root", - group_dict=self.tags_dict) - - def parse_header(self): - self.dm_version = iou.read_long(self.f, "big") - if self.dm_version not in (3, 4): - raise NotImplementedError( - "Currently we only support reading DM versions 3 and 4 but " - "this file " - "seems to be version %s " % self.dm_version) - filesizeB = self.read_l_or_q(self.f, "big") - is_little_endian = iou.read_long(self.f, "big") - - _logger.info('DM version: %i', self.dm_version) - _logger.info('size %i B', filesizeB) - _logger.info('Is file Little endian? %s', bool(is_little_endian)) - if bool(is_little_endian): - self.endian = 'little' - else: - self.endian = 'big' - - def parse_tags(self, ntags, group_name='root', group_dict={}): - """Parse the DM file into a dictionary. - - """ - unnammed_data_tags = 0 - unnammed_group_tags = 0 - for tag in range(ntags): - _logger.debug('Reading tag name at address: %s', self.f.tell()) - tag_header = self.parse_tag_header() - tag_name = tag_header['tag_name'] - - skip = True if (group_name == "ImageData" and - tag_name == "Data") else False - _logger.debug('Tag name: %s', tag_name[:20]) - _logger.debug('Tag ID: %s', tag_header['tag_id']) - - if tag_header['tag_id'] == 21: # it's a TagType (DATA) - if not tag_name: - tag_name = 'Data%i' % unnammed_data_tags - unnammed_data_tags += 1 - - _logger.debug('Reading data tag at address: %s', self.f.tell()) - - # Start reading the data - # Raises IOError if it is wrong - self.check_data_tag_delimiter() - infoarray_size = self.read_l_or_q(self.f, 'big') - _logger.debug("Infoarray size: %s", infoarray_size) - if infoarray_size == 1: # Simple type - _logger.debug("Reading simple data") - etype = self.read_l_or_q(self.f, "big") - data = self.read_simple_data(etype) - elif infoarray_size == 2: # String - _logger.debug("Reading string") - enctype = self.read_l_or_q(self.f, "big") - if enctype != 18: - raise IOError("Expected 18 (string), got %i" % enctype) - string_length = self.parse_string_definition() - data = self.read_string(string_length, skip=skip) - elif infoarray_size == 3: # Array of simple type - _logger.debug("Reading simple array") - # Read array header - enctype = self.read_l_or_q(self.f, "big") - if enctype != 20: # Should be 20 if it is an array - raise IOError("Expected 20 (string), got %i" % enctype) - size, enc_eltype = self.parse_array_definition() - data = self.read_array(size, enc_eltype, skip=skip) - elif infoarray_size > 3: - enctype = self.read_l_or_q(self.f, "big") - if enctype == 15: # It is a struct - _logger.debug("Reading struct") - definition = self.parse_struct_definition() - _logger.debug("Struct definition %s", definition) - data = self.read_struct(definition, skip=skip) - elif enctype == 20: # It is an array of complex type - # Read complex array info - # The structure is - # 20 <4>, ? <4>, enc_dtype <4>, definition , - # size <4> - enc_eltype = self.read_l_or_q(self.f, "big") - if enc_eltype == 15: # Array of structs - _logger.debug("Reading array of structs") - definition = self.parse_struct_definition() - size = self.read_l_or_q(self.f, "big") - _logger.debug("Struct definition: %s", definition) - _logger.debug("Array size: %s", size) - data = self.read_array( - size=size, - enc_eltype=enc_eltype, - extra={"definition": definition}, - skip=skip) - elif enc_eltype == 18: # Array of strings - _logger.debug("Reading array of strings") - string_length = \ - self.parse_string_definition() - size = self.read_l_or_q(self.f, "big") - data = self.read_array( - size=size, - enc_eltype=enc_eltype, - extra={"length": string_length}, - skip=skip) - elif enc_eltype == 20: # Array of arrays - _logger.debug("Reading array of arrays") - el_length, enc_eltype = \ - self.parse_array_definition() - size = self.read_l_or_q(self.f, "big") - data = self.read_array( - size=size, - enc_eltype=enc_eltype, - extra={"size": el_length}, - skip=skip) - - else: # Infoarray_size < 1 - raise IOError("Invalided infoarray size ", infoarray_size) - - group_dict[tag_name] = data - - elif tag_header['tag_id'] == 20: # it's a TagGroup (GROUP) - if not tag_name: - tag_name = 'TagGroup%i' % unnammed_group_tags - unnammed_group_tags += 1 - _logger.debug( - 'Reading Tag group at address: %s', - self.f.tell()) - ntags = self.parse_tag_group(size=True)[2] - group_dict[tag_name] = {} - self.parse_tags( - ntags=ntags, - group_name=tag_name, - group_dict=group_dict[tag_name]) - else: - _logger.debug('File address:', self.f.tell()) - raise DM3TagIDError(tag_header['tag_id']) - - def get_data_reader(self, enc_dtype): - # _data_type dictionary. - # The first element of the InfoArray in the TagType - # will always be one of _data_type keys. - # the tuple reads: ('read bytes function', 'number of bytes', 'type') - - dtype_dict = { - 2: (iou.read_short, 2, 'h'), - 3: (iou.read_long, 4, 'l'), - 4: (iou.read_ushort, 2, 'H'), # dm3 uses ushorts for unicode chars - 5: (iou.read_ulong, 4, 'L'), - 6: (iou.read_float, 4, 'f'), - 7: (iou.read_double, 8, 'd'), - 8: (iou.read_boolean, 1, 'B'), - # dm3 uses chars for 1-Byte signed integers - 9: (iou.read_char, 1, 'b'), - 10: (iou.read_byte, 1, 'b'), # 0x0a - 11: (iou.read_long_long, 8, 'q'), # long long, new in DM4 - # unsigned long long, new in DM4 - 12: (iou.read_ulong_long, 8, 'Q'), - 15: (self.read_struct, None, 'struct',), # 0x0f - 18: (self.read_string, None, 'c'), # 0x12 - 20: (self.read_array, None, 'array'), # 0x14 - } - return dtype_dict[enc_dtype] - - def skipif4(self, n=1): - if self.dm_version == 4: - self.f.seek(4 * n, 1) - - @property - def read_l_or_q(self): - if self.dm_version == 4: - return iou.read_long_long - else: - return iou.read_long - - def parse_array_definition(self): - """Reads and returns the element type and length of the array. - - The position in the file must be just after the - array encoded dtype. - - """ - enc_eltype = self.read_l_or_q(self.f, "big") - length = self.read_l_or_q(self.f, "big") - return length, enc_eltype - - def parse_string_definition(self): - """Reads and returns the length of the string. - - The position in the file must be just after the - string encoded dtype. - """ - return self.read_l_or_q(self.f, "big") - - def parse_struct_definition(self): - """Reads and returns the struct definition tuple. - - The position in the file must be just after the - struct encoded dtype. - - """ - length = self.read_l_or_q(self.f, "big") - nfields = self.read_l_or_q(self.f, "big") - definition = () - for ifield in range(nfields): - length2 = self.read_l_or_q(self.f, "big") - definition += (self.read_l_or_q(self.f, "big"),) - - return definition - - def read_simple_data(self, etype): - """Parse the data of the given DM3 file f - with the given endianness (byte order). - The infoArray iarray specifies how to read the data. - Returns the tuple (file address, data). - The tag data is stored in the platform's byte order: - 'little' endian for Intel, PC; 'big' endian for Mac, Motorola. - If skip != 0 the data is actually skipped. - """ - data = self.get_data_reader(etype)[0](self.f, self.endian) - if isinstance(data, str): - data = hyperspy.misc.utils.ensure_unicode(data) - return data - - def read_string(self, length, skip=False): - """Read a string defined by the infoArray iarray from file f with a - given endianness (byte order). endian can be either 'big' or 'little'. - - If it's a tag name, each char is 1-Byte; - if it's a tag data, each char is 2-Bytes Unicode, - """ - if skip is True: - offset = self.f.tell() - self.f.seek(length, 1) - return {'size': length, - 'size_bytes': size_bytes, - 'offset': offset, - 'endian': self.endian, } - data = b'' - if self.endian == 'little': - s = iou.L_char - elif self.endian == 'big': - s = iou.B_char - for char in range(length): - data += s.unpack(self.f.read(1))[0] - try: - data = data.decode('utf8') - except BaseException: - # Sometimes the dm3 file strings are encoded in latin-1 - # instead of utf8 - data = data.decode('latin-1', errors='ignore') - return data - - def read_struct(self, definition, skip=False): - """Read a struct, defined by iarray, from file f - with a given endianness (byte order). - Returns a list of 2-tuples in the form - (fieldAddress, fieldValue). - endian can be either 'big' or 'little'. - - """ - field_value = [] - size_bytes = 0 - offset = self.f.tell() - for dtype in definition: - if dtype in self.simple_type: - if skip is False: - data = self.get_data_reader(dtype)[0](self.f, self.endian) - field_value.append(data) - else: - sbytes = self.get_data_reader(dtype)[1] - self.f.seek(sbytes, 1) - size_bytes += sbytes - else: - raise DM3DataTypeError(dtype) - if skip is False: - return tuple(field_value) - else: - return {'size': len(definition), - 'size_bytes': size_bytes, - 'offset': offset, - 'endian': self.endian, } - - def read_array(self, size, enc_eltype, extra=None, skip=False): - """Read an array, defined by iarray, from file f - with a given endianness (byte order). - endian can be either 'big' or 'little'. - - """ - eltype = self.get_data_reader(enc_eltype)[0] # same for all elements - if skip is True: - if enc_eltype not in self._complex_type: - size_bytes = self.get_data_reader(enc_eltype)[1] * size - data = {"size": size, - "endian": self.endian, - "size_bytes": size_bytes, - "offset": self.f.tell()} - self.f.seek(size_bytes, 1) # Skipping data - else: - data = eltype(skip=skip, **extra) - self.f.seek(data['size_bytes'] * (size - 1), 1) - data['size'] = size - data['size_bytes'] *= size - else: - if enc_eltype in self.simple_type: # simple type - data = [eltype(self.f, self.endian) - for element in range(size)] - if enc_eltype == 4 and data: # it's actually a string - data = "".join([chr(i) for i in data]) - elif enc_eltype in self._complex_type: - data = [eltype(**extra) - for element in range(size)] - return data - - def parse_tag_group(self, size=False): - """Parse the root TagGroup of the given DM3 file f. - Returns the tuple (is_sorted, is_open, n_tags). - endian can be either 'big' or 'little'. - """ - is_sorted = iou.read_byte(self.f, "big") - is_open = iou.read_byte(self.f, "big") - if self.dm_version == 4 and size: - # Just guessing that this is the size - size = self.read_l_or_q(self.f, "big") - n_tags = self.read_l_or_q(self.f, "big") - return bool(is_sorted), bool(is_open), n_tags - - def find_next_tag(self): - while iou.read_byte(self.f, "big") not in (20, 21): - continue - location = self.f.tell() - 1 - self.f.seek(location) - tag_id = iou.read_byte(self.f, "big") - self.f.seek(location) - tag_header = self.parse_tag_header() - if tag_id == 20: - _logger.debug("Tag header length", tag_header['tag_name_length']) - if not 20 > tag_header['tag_name_length'] > 0: - _logger.debug("Skipping id 20") - self.f.seek(location + 1) - self.find_next_tag() - else: - self.f.seek(location) - return - else: - try: - self.check_data_tag_delimiter() - self.f.seek(location) - return - except DM3TagTypeError: - self.f.seek(location + 1) - _logger.debug("Skipping id 21") - self.find_next_tag() - - def find_next_data_tag(self): - while iou.read_byte(self.f, "big") != 21: - continue - position = self.f.tell() - 1 - self.f.seek(position) - self.parse_tag_header() - try: - self.check_data_tag_delimiter() - self.f.seek(position) - except DM3TagTypeError: - self.f.seek(position + 1) - self.find_next_data_tag() - - def parse_tag_header(self): - tag_id = iou.read_byte(self.f, "big") - tag_name_length = iou.read_short(self.f, "big") - tag_name = self.read_string(tag_name_length) - return {'tag_id': tag_id, - 'tag_name_length': tag_name_length, - 'tag_name': tag_name, } - - def check_data_tag_delimiter(self): - self.skipif4(2) - delimiter = self.read_string(4) - if delimiter != '%%%%': - raise DM3TagTypeError(delimiter) - - def get_image_dictionaries(self): - """Returns the image dictionaries of all images in the file except - the thumbnails. - - Returns - ------- - dict, None - - """ - if 'ImageList' not in self.tags_dict: - return None - if "Thumbnails" in self.tags_dict: - thumbnail_idx = [tag['ImageIndex'] for key, tag in - self.tags_dict['Thumbnails'].items()] - else: - thumbnail_idx = [] - images = [image for key, image in - self.tags_dict['ImageList'].items() - if not int(key.replace("TagGroup", "")) in - thumbnail_idx] - return images - - -class ImageObject(object): - - def __init__(self, imdict, file, order="C", record_by=None): - self.imdict = DictionaryTreeBrowser(imdict) - self.file = file - self._order = order if order else "C" - self._record_by = record_by - - @property - def shape(self): - dimensions = self.imdict.ImageData.Dimensions - shape = tuple([dimension[1] for dimension in dimensions]) - return shape[::-1] # DM uses image indexing X, Y, Z... - - # For some image stacks created using plugins in Digital Micrograph - # the metadata under Calibrations.Dimension would not reflect the - # actual dimensions in the dataset, leading to these images not - # loading properly. To allow HyperSpy to load these files, any missing - # dimensions in the metadata is appended with "dummy" values. - # This is done for the offsets, scales and units properties, using - # the len_diff variable - @property - def offsets(self): - dimensions = self.imdict.ImageData.Calibrations.Dimension - len_diff = len(self.shape) - len(dimensions) - origins = np.array([dimension[1].Origin for dimension in dimensions]) - origins = np.append(origins, (0.0,) * len_diff) - return -1 * origins[::-1] * self.scales - - @property - def scales(self): - dimensions = self.imdict.ImageData.Calibrations.Dimension - len_diff = len(self.shape) - len(dimensions) - scales = np.array([dimension[1].Scale for dimension in dimensions]) - scales = np.append(scales, (1.0,) * len_diff) - return scales[::-1] - - @property - def units(self): - dimensions = self.imdict.ImageData.Calibrations.Dimension - len_diff = len(self.shape) - len(dimensions) - return (tuple([dimension[1].Units - if dimension[1].Units else "" - for dimension in dimensions]) + ('',) * len_diff)[::-1] - - @property - def names(self): - names = [t.Undefined] * len(self.shape) - indices = list(range(len(self.shape))) - if self.signal_type == "EELS": - if "eV" in self.units: - names[indices.pop(self.units.index("eV"))] = "Energy loss" - elif self.signal_type in ("EDS", "EDX"): - if "keV" in self.units: - names[indices.pop(self.units.index("keV"))] = "Energy" - elif self.signal_type in ("CL"): - if "nm" in self.units: - names[indices.pop(self.units.index("nm"))] = "Wavelength" - for index, name in zip(indices[::-1], ("x", "y", "z")): - names[index] = name - return names - - @property - def title(self): - title = self.imdict.get_item("Name", "") - # ``if title else ""`` below is there to account for when Name - # contains an empty list. - # See https://github.com/hyperspy/hyperspy/issues/1937 - return title if title else "" - - @property - def record_by(self): - if self._record_by is not None: - return self._record_by - if len(self.scales) == 1: - return "spectrum" - elif (('ImageTags.Meta_Data.Format' in self.imdict and - self.imdict.ImageTags.Meta_Data.Format in ("Spectrum image", - "Spectrum")) or ( - "ImageTags.spim" in self.imdict)) and len(self.scales) == 2: - return "spectrum" - else: - return "image" - - @property - def to_spectrum(self): - if (('ImageTags.Meta_Data.Format' in self.imdict and - self.imdict.ImageTags.Meta_Data.Format == "Spectrum image") or - ("ImageTags.spim" in self.imdict)) and len(self.scales) > 2: - return True - else: - return False - - @property - def order(self): - return self._order - - @property - def intensity_calibration(self): - ic = self.imdict.ImageData.Calibrations.Brightness.as_dictionary() - if not ic['Units']: - ic['Units'] = "" - return ic - - @property - def dtype(self): - # Signal2D data types (Signal2D Object chapter on DM help)# - # key = DM data type code - # value = numpy data type - if self.imdict.ImageData.DataType == 4: - raise NotImplementedError( - "Reading data of this type is not implemented.") - - imdtype_dict = { - 0: 'not_implemented', # null - 1: 'int16', - 2: 'float32', - 3: 'complex64', - 5: 'float32', # not numpy: 8-Byte packed complex (FFT data) - 6: 'uint8', - 7: 'int32', - 8: np.dtype({'names': ['B', 'G', 'R', 'A'], - 'formats': ['u1', 'u1', 'u1', 'u1']}), - 9: 'int8', - 10: 'uint16', - 11: 'uint32', - 12: 'float64', - 13: 'complex128', - 14: 'bool', - 23: np.dtype({'names': ['B', 'G', 'R', 'A'], - 'formats': ['u1', 'u1', 'u1', 'u1']}), - 27: 'complex64', # not numpy: 8-Byte packed complex (FFT data) - 28: 'complex128', # not numpy: 16-Byte packed complex (FFT data) - } - return imdtype_dict[self.imdict.ImageData.DataType] - - @property - def signal_type(self): - md_signal = self.imdict.get_item('ImageTags.Meta_Data.Signal', "") - if md_signal == 'X-ray': - return "EDS_TEM" - elif md_signal == 'CL': - return "CL" - # 'ImageTags.spim.eels' is Orsay's tag group - elif md_signal == 'EELS' or 'ImageTags.spim.eels' in self.imdict: - return "EELS" - else: - return "" - - def _get_data_array(self): - need_to_close = False - if self.file.closed: - self.file = open(self.filename, "rb") - need_to_close = True - self.file.seek(self.imdict.ImageData.Data.offset) - count = self.imdict.ImageData.Data.size - if self.imdict.ImageData.DataType in (27, 28): # Packed complex - count = int(count / 2) - data = np.fromfile(self.file, - dtype=self.dtype, - count=count) - if need_to_close: - self.file.close() - return data - - @property - def size(self): - if self.imdict.ImageData.DataType in (27, 28): # Packed complex - if self.imdict.ImageData.Data.size % 2: - raise IOError( - "ImageData.Data.size should be an even integer for " - "this datatype.") - else: - return int(self.imdict.ImageData.Data.size / 2) - else: - return self.imdict.ImageData.Data.size - - def get_data(self): - if isinstance(self.imdict.ImageData.Data, np.ndarray): - return self.imdict.ImageData.Data - data = self._get_data_array() - if self.imdict.ImageData.DataType in (27, 28): # New packed complex - return self.unpack_new_packed_complex(data) - elif self.imdict.ImageData.DataType == 5: # Old packed compled - return self.unpack_packed_complex(data) - elif self.imdict.ImageData.DataType in (8, 23): # ABGR - # Reorder the fields - data = data[['R', 'G', 'B', 'A']].astype( - [('R', 'u1'), ('G', 'u1'), ('B', 'u1'), ('A', 'u1')]) - return data.reshape(self.shape, order=self.order) - - def unpack_new_packed_complex(self, data): - packed_shape = (self.shape[0], int(self.shape[1] / 2 + 1)) - data = data.reshape(packed_shape, order=self.order) - return np.hstack((data[:, ::-1], np.conjugate(data[:, 1:-1]))) - - def unpack_packed_complex(self, tmpdata): - shape = self.shape - if shape[0] != shape[1] or len(shape) > 2: - raise IOError( - 'Unable to read this DM file in packed complex format. ' - 'Please report the issue to the HyperSpy developers providing ' - 'the file if possible') - N = int(self.shape[0] / 2) # think about a 2Nx2N matrix - # create an empty 2Nx2N ndarray of complex - data = np.zeros(shape, dtype="complex64") - - # fill in the real values: - data[N, 0] = tmpdata[0] - data[0, 0] = tmpdata[1] - data[N, N] = tmpdata[2 * N ** 2] # Nyquist frequency - data[0, N] = tmpdata[2 * N ** 2 + 1] # Nyquist frequency - - # fill in the non-redundant complex values: - # top right quarter, except 1st column - for i in range(N): # this could be optimized - start = 2 * i * N + 2 - stop = start + 2 * (N - 1) - 1 - step = 2 - realpart = tmpdata[start:stop:step] - imagpart = tmpdata[start + 1:stop + 1:step] - data[i, N + 1:2 * N] = realpart + imagpart * 1j - # 1st column, bottom left quarter - start = 2 * N - stop = start + 2 * N * (N - 1) - 1 - step = 2 * N - realpart = tmpdata[start:stop:step] - imagpart = tmpdata[start + 1:stop + 1:step] - data[N + 1:2 * N, 0] = realpart + imagpart * 1j - # 1st row, bottom right quarter - start = 2 * N ** 2 + 2 - stop = start + 2 * (N - 1) - 1 - step = 2 - realpart = tmpdata[start:stop:step] - imagpart = tmpdata[start + 1:stop + 1:step] - data[N, N + 1:2 * N] = realpart + imagpart * 1j - # bottom right quarter, except 1st row - start = stop + 1 - stop = start + 2 * N * (N - 1) - 1 - step = 2 - realpart = tmpdata[start:stop:step] - imagpart = tmpdata[start + 1:stop + 1:step] - complexdata = realpart + imagpart * 1j - data[ - N + - 1:2 * - N, - N:2 * - N] = complexdata.reshape( - N - - 1, - N, - order=self.order) - - # fill in the empty pixels: A(i)(j) = A(2N-i)(2N-j)* - # 1st row, top left quarter, except 1st element - data[0, 1:N] = np.conjugate(data[0, -1:-N:-1]) - # 1st row, bottom left quarter, except 1st element - data[N, 1:N] = np.conjugate(data[N, -1:-N:-1]) - # 1st column, top left quarter, except 1st element - data[1:N, 0] = np.conjugate(data[-1:-N:-1, 0]) - # 1st column, top right quarter, except 1st element - data[1:N, N] = np.conjugate(data[-1:-N:-1, N]) - # top left quarter, except 1st row and 1st column - data[1:N, 1:N] = np.conjugate(data[-1:-N:-1, -1:-N:-1]) - # bottom left quarter, except 1st row and 1st column - data[N + 1:2 * N, 1:N] = np.conjugate(data[-N - 1:-2 * N:-1, -1:-N:-1]) - - return data - - def get_axes_dict(self): - return [{'name': name, - 'size': size, - 'index_in_array': i, - 'scale': scale, - 'offset': offset, - 'units': str(units), } - for i, (name, size, scale, offset, units) in enumerate( - zip(self.names, self.shape, self.scales, self.offsets, - self.units))] - - def get_metadata(self, metadata={}): - if "General" not in metadata: - metadata['General'] = {} - if "Signal" not in metadata: - metadata['Signal'] = {} - metadata['General']['title'] = self.title - metadata["Signal"]['record_by'] = self.record_by - metadata["Signal"]['signal_type'] = self.signal_type - return metadata - - def _get_quantity(self, units): - quantity = "Intensity" - if len(units) == 0: - units = "" - elif units == 'e-': - units = "Counts" - quantity = "Electrons" - if self.signal_type == 'EDS_TEM': - quantity = "X-rays" - if len(units) != 0: - units = " (%s)" % units - return "%s%s" % (quantity, units) - - def _get_mode(self, mode): - if 'STEM' in mode: - return 'STEM' - elif 'SEM' in mode: - return 'SEM' - else: - return 'TEM' - - def _get_time(self, time): - try: - dt = dateutil.parser.parse(time) - return dt.time().isoformat() - except BaseException: - _logger.warning(f"Time string '{time}' could not be parsed.") - - def _get_date(self, date): - try: - dt = dateutil.parser.parse(date) - return dt.date().isoformat() - except BaseException: - _logger.warning(f"Date string '{date}' could not be parsed.") - - def _get_microscope_name(self, ImageTags): - locations = ( - "Session_Info.Microscope", - "Microscope_Info.Name", - "Microscope_Info.Microscope", - ) - for loc in locations: - mic = ImageTags.get_item(loc) - if mic and mic != "[]": - return mic - _logger.info("Microscope name not present") - return None - - def _parse_string(self, tag, convert_to_float=False, tag_name=None): - if len(tag) == 0: - return None - elif convert_to_float: - try: - return float(tag) - # In case the string can't be converted to float - except BaseException: - if tag_name is None: - warning = "Metadata could not be parsed." - else: - warning = f"Metadata '{tag_name}' could not be parsed." - _logger.warning(warning) - return None - else: - return tag - - def _get_EELS_exposure_time(self, tags): - # for GMS 2 and quantum/enfinium, the "Integration time (s)" tag is - # only present for single spectrum acquisition; for maps we need to - # compute exposure * number of frames - if 'Integration_time_s' in tags.keys(): - return float(tags["Integration_time_s"]) - elif 'Exposure_s' in tags.keys(): - frame_number = 1 - if "Number_of_frames" in tags.keys(): - frame_number = float(tags["Number_of_frames"]) - return float(tags["Exposure_s"]) * frame_number - else: - _logger.info("EELS exposure time can't be read.") - - def get_mapping(self): - if 'source' in self.imdict.ImageTags.keys(): - # For stack created with the stack builder plugin - tags_path = 'ImageList.TagGroup0.ImageTags.source.Tags at creation' - image_tags_dict = self.imdict.ImageTags.source['Tags at creation'] - else: - # Standard tags - tags_path = 'ImageList.TagGroup0.ImageTags' - image_tags_dict = self.imdict.ImageTags - is_scanning = "DigiScan" in image_tags_dict.keys() - mapping = { - "{}.DataBar.Acquisition Date".format(tags_path): ( - "General.date", self._get_date), - "{}.DataBar.Acquisition Time".format(tags_path): ( - "General.time", self._get_time), - "{}.Microscope Info.Voltage".format(tags_path): ( - "Acquisition_instrument.TEM.beam_energy", lambda x: x / 1e3), - "{}.Microscope Info.Stage Position.Stage Alpha".format(tags_path): ( - "Acquisition_instrument.TEM.Stage.tilt_alpha", None), - "{}.Microscope Info.Stage Position.Stage Beta".format(tags_path): ( - "Acquisition_instrument.TEM.Stage.tilt_beta", None), - "{}.Microscope Info.Stage Position.Stage X".format(tags_path): ( - "Acquisition_instrument.TEM.Stage.x", lambda x: x * 1e-3), - "{}.Microscope Info.Stage Position.Stage Y".format(tags_path): ( - "Acquisition_instrument.TEM.Stage.y", lambda x: x * 1e-3), - "{}.Microscope Info.Stage Position.Stage Z".format(tags_path): ( - "Acquisition_instrument.TEM.Stage.z", lambda x: x * 1e-3), - "{}.Microscope Info.Illumination Mode".format(tags_path): ( - "Acquisition_instrument.TEM.acquisition_mode", self._get_mode), - "{}.Microscope Info.Probe Current (nA)".format(tags_path): ( - "Acquisition_instrument.TEM.beam_current", None), - "{}.Session Info.Operator".format(tags_path): ( - "General.authors", self._parse_string), - "{}.Session Info.Specimen".format(tags_path): ( - "Sample.description", self._parse_string), - } - - if "Microscope_Info" in image_tags_dict.keys(): - is_TEM = is_diffraction = None - if "Illumination_Mode" in image_tags_dict['Microscope_Info'].keys( - ): - is_TEM = ( - 'TEM' == image_tags_dict.Microscope_Info.Illumination_Mode) - if "Imaging_Mode" in image_tags_dict['Microscope_Info'].keys(): - is_diffraction = ( - 'DIFFRACTION' == image_tags_dict.Microscope_Info.Imaging_Mode) - - if is_TEM: - if is_diffraction: - mapping.update({ - "{}.Microscope Info.Indicated Magnification".format(tags_path): ( - "Acquisition_instrument.TEM.camera_length", - None), - }) - else: - mapping.update({ - "{}.Microscope Info.Indicated Magnification".format(tags_path): ( - "Acquisition_instrument.TEM.magnification", - None), - }) - else: - mapping.update({ - "{}.Microscope Info.STEM Camera Length".format(tags_path): ( - "Acquisition_instrument.TEM.camera_length", - None), - "{}.Microscope Info.Indicated Magnification".format(tags_path): ( - "Acquisition_instrument.TEM.magnification", - None), - }) - - mapping.update({ - tags_path: ( - "Acquisition_instrument.TEM.microscope", - self._get_microscope_name), - }) - if "SI" in self.imdict.ImageTags.keys(): - mapping.update({ - "{}.SI.Acquisition.Date".format(tags_path): ( - "General.date", - self._get_date), - "{}.SI.Acquisition.Start time".format(tags_path): ( - "General.time", - self._get_time), - }) - if self.signal_type == "EELS": - if is_scanning: - mapped_attribute = 'dwell_time' - else: - mapped_attribute = 'exposure' - mapping.update({ - "{}.EELS.Acquisition.Date".format(tags_path): ( - "General.date", - self._get_date), - "{}.EELS.Acquisition.Start time".format(tags_path): ( - "General.time", - self._get_time), - "{}.EELS.Experimental Conditions.".format(tags_path) + - "Collection semi-angle (mrad)": ( - "Acquisition_instrument.TEM.Detector.EELS.collection_angle", - None), - "{}.EELS.Experimental Conditions.".format(tags_path) + - "Convergence semi-angle (mrad)": ( - "Acquisition_instrument.TEM.convergence_angle", - None), - "{}.EELS.Acquisition".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EELS.%s" % mapped_attribute, - self._get_EELS_exposure_time), - "{}.EELS.Acquisition.Number_of_frames".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EELS.frame_number", - None), - "{}.EELS_Spectrometer.Aperture_label".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EELS.aperture_size", - lambda string: self._parse_string(string.replace('mm', ''), - convert_to_float=True, - tag_name='Aperture_label')), - "{}.EELS Spectrometer.Instrument name".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EELS.spectrometer", - None), - }) - elif self.signal_type == "EDS_TEM": - mapping.update({ - "{}.EDS.Acquisition.Date".format(tags_path): ( - "General.date", - self._get_date), - "{}.EDS.Acquisition.Start time".format(tags_path): ( - "General.time", - self._get_time), - "{}.EDS.Detector_Info.Azimuthal_angle".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EDS.azimuth_angle", - None), - "{}.EDS.Detector_Info.Elevation_angle".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EDS.elevation_angle", - None), - "{}.EDS.Solid_angle".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EDS.solid_angle", - None), - "{}.EDS.Live_time".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EDS.live_time", - None), - "{}.EDS.Real_time".format(tags_path): ( - "Acquisition_instrument.TEM.Detector.EDS.real_time", - None), - }) - elif self.signal_type == "CL": - mapping.update({ - "{}.CL.Acquisition.Date".format(tags_path): ( - "General.date", self._get_date), - "{}.CL.Acquisition.Start_time".format(tags_path): ( - "General.time", self._get_time), - "{}.Meta_Data.Acquisition_Mode".format(tags_path): ( - "Acquisition_instrument.CL.acquisition_mode", None), - "{}.Meta_Data.Format".format(tags_path): ( - "Signal.format", None), - "{}.CL.Acquisition.Dispersion_grating_(lines/mm)".format(tags_path): ( - "Acquisition_instrument.CL.dispersion_grating", None), - # Parallel spectrum - "{}.CL.Acquisition.Central_wavelength_(nm)".format(tags_path): ( - "Acquisition_instrument.CL.central_wavelength", None), - "{}.CL.Acquisition.Exposure_(s)".format(tags_path): ( - "Acquisition_instrument.CL.exposure", None), - "{}.CL.Acquisition.Number_of_frames".format(tags_path): ( - "Acquisition_instrument.CL.frame_number", None), - "{}.CL.Acquisition.Integration_time_(s)".format(tags_path): ( - "Acquisition_instrument.CL.integration_time", None), - "{}.CL.Acquisition.Saturation_fraction".format(tags_path): ( - "Acquisition_instrument.CL.saturation_fraction", None), - "{}.Acquisition.Parameters.High_Level.Binning".format(tags_path): ( - "Acquisition_instrument.CL.CCD.binning", None), - "{}.Acquisition.Parameters.High_Level.CCD_Read_Area".format(tags_path): ( - "Acquisition_instrument.CL.CCD.read_area", None), - "{}.Acquisition.Parameters.High_Level.Processing".format(tags_path): ( - "Acquisition_instrument.CL.CCD.processing", None), - # Serial Spectrum - "{}.CL.Acquisition.Acquisition_begin".format(tags_path): ( - "General.date", self._get_date), - "{}.CL.Acquisition.Detector_type".format(tags_path): ( - "Acquisition_instrument.CL.detector_type", None), - "{}.CL.Acquisition.Dwell_time_(s)".format(tags_path): ( - "Acquisition_instrument.CL.dwell_time", None), - "{}.CL.Acquisition.Start_wavelength_(nm)".format(tags_path): ( - "Acquisition_instrument.CL.start_wavelength", None), - "{}.CL.Acquisition.Step-size_(nm)".format(tags_path): ( - "Acquisition_instrument.CL.step_size", None), - # SI - "{}.SI.Acquisition.Artefact_Correction.Spatial_Drift.Periodicity".format(tags_path): ( - "Acquisition_instrument.CL.SI.drift_correction_periodicity", None), - "{}.SI.Acquisition.Artefact_Correction.Spatial_Drift.Units".format(tags_path): ( - "Acquisition_instrument.CL.SI.drift_correction_units", None), - "{}.SI.Acquisition.SI_Application_Mode.Name".format(tags_path): ( - "Acquisition_instrument.CL.SI.mode", None), - - }) - elif "DigiScan" in image_tags_dict.keys(): - mapping.update({ - "{}.DigiScan.Sample Time".format(tags_path): ( - "Acquisition_instrument.TEM.dwell_time", - lambda x: x / 1e6), - }) - else: - mapping.update({ - "{}.Acquisition.Parameters.Detector.".format(tags_path) + - "exposure_s": ( - "Acquisition_instrument.TEM.Camera.exposure", - None), - }) - mapping.update({ - "ImageList.TagGroup0.ImageData.Calibrations.Brightness.Units": ( - "Signal.quantity", - self._get_quantity), - "ImageList.TagGroup0.ImageData.Calibrations.Brightness.Scale": ( - "Signal.Noise_properties.Variance_linear_model.gain_factor", - None), - "ImageList.TagGroup0.ImageData.Calibrations.Brightness.Origin": ( - "Signal.Noise_properties.Variance_linear_model.gain_offset", - None), - }) - return mapping - - -def file_reader(filename, record_by=None, order=None, lazy=False, - optimize=True): - """Reads a DM3/4 file and loads the data into the appropriate class. - data_id can be specified to load a given image within a DM3/4 file that - contains more than one dataset. - - Parameters - ---------- - record_by: Str - One of: SI, Signal2D - order : Str - One of 'C' or 'F' - lazy : bool, default False - Load the signal lazily. - %s - """ - - with open(filename, "rb") as f: - dm = DigitalMicrographReader(f) - dm.parse_file() - images = [ImageObject(imdict, f, order=order, record_by=record_by) - for imdict in dm.get_image_dictionaries()] - imd = [] - del dm.tags_dict['ImageList'] - dm.tags_dict['ImageList'] = {} - - for image in images: - dm.tags_dict['ImageList'][ - 'TagGroup0'] = image.imdict.as_dictionary() - axes = image.get_axes_dict() - mp = image.get_metadata() - mp['General']['original_filename'] = os.path.split(filename)[1] - post_process = [] - if image.to_spectrum is True: - post_process.append(lambda s: s.to_signal1D(optimize=optimize)) - post_process.append(lambda s: s.squeeze()) - if lazy: - image.filename = filename - from dask.array import from_delayed - import dask.delayed as dd - val = dd(image.get_data, pure=True)() - data = from_delayed(val, shape=image.shape, - dtype=image.dtype) - else: - data = image.get_data() - # in the event there are multiple signals contained within this - # DM file, it is important to make a "deepcopy" of the metadata - # and original_metadata, since they are changed in each iteration - # of the "for image in images" loop, and using shallow copies - # will result in the final signal's metadata being used for all - # of the contained signals - imd.append( - {'data': data, - 'axes': axes, - 'metadata': deepcopy(mp), - 'original_metadata': deepcopy(dm.tags_dict), - 'post_process': post_process, - 'mapping': image.get_mapping(), - }) - - return imd - file_reader.__doc__ %= (OPTIMIZE_ARG.replace('False', 'True')) diff --git a/hyperspy/io_plugins/edax.py b/hyperspy/io_plugins/edax.py deleted file mode 100644 index d1bdd4873f..0000000000 --- a/hyperspy/io_plugins/edax.py +++ /dev/null @@ -1,1024 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -# The details of the format were taken from -# http://www.biochem.mpg.de/doc_tom/TOM_Release_2008/IOfun/tom_mrcread.html -# and http://ami.scripps.edu/software/mrctools/mrc_specification.php - -import os -import logging -import numpy as np -from hyperspy.misc.array_tools import sarray2dict -import traits.api as t -from hyperspy.misc.elements import atomic_number2name - -_logger = logging.getLogger(__name__) - -# Plugin characteristics -# ---------------------- -format_name = 'EDAX TEAM' -description = 'Reader for EDS maps and spectra saved by the EDAX TEAM' \ - 'software: \n' \ - 'An SPD file contains map data. The spectral information is ' \ - '\nheld in an SPC file with the same name, while the spatial ' \ - '\ncalibration is held in a related IPR file. If an SPD file ' \ - '\nis loaded, the result will be a Hyperspy EDSSpectrum map, ' \ - '\nand the calibration will be loaded from appropriate SPC ' \ - '\nand IPR files (if available). \n' \ - 'If an SPC file is loaded, the result will be a single \n' \ - 'EDSSpectrum with no other files needed for calibration.' -full_support = False -# Recognised file extension -file_extensions = ['spd', 'SPD', 'spc', 'SPC'] -default_extension = 0 -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -spd_extensions = ('spd', 'SPD', 'Spd') -spc_extensions = ('spc', 'SPC', 'Spc') - - -def get_spd_dtype_list(endianess='<'): - """ - Get the data type list for an SPD map. - Further information about the file format is available `here - `__. - - Table of header tags: - - tag: 16 byte char array; *File ID tag ("MAPSPECTRA_DATA")* - - version: 4 byte long; *File version* - - nSpectra: 4 byte long; *Number of spectra in file* - - nPoints: 4 byte long; *Number of map pixels in X direction* - - nLines: 4 byte long; *Number of map pixels in Y direction* - - nChannels: 4 byte long; *Number of channels per spectrum* - - countBytes: 4 byte long; *Number of count bytes per channel* - - dataOffset: 4 byte long; *File offset in bytes for data start* - - nFrames: 4 byte long; *Number of frames in live spectrum mapping* - - fName: 120 byte char array; *File name of electron image acquired during mapping* - - - Parameters - ---------- - endianess : byte-order used to read the data - - Returns - ------- - dtype_list : list - List of the data tags and data types that will be used by numpy to - read an SPD file header. - """ - end = endianess - dtype_list = \ - [ - ('tag', '16i1'), - ('version', end + 'i4'), - ('nSpectra', end + 'i4'), - ('nPoints', end + 'i4'), - ('nLines', end + 'u4'), - ('nChannels', end + 'u4'), - ('countBytes', end + 'u4'), - ('dataOffset', end + 'u4'), - ('nFrames', end + 'u4'), - ('fName', end + '120i1'), - ('filler', end + 'V900'), - ] - return dtype_list - - -def __get_spc_header(f, endianess, load_all_spc): - """ - Get the header of an spc file, checking for the file version as necessary - - Parameters - ---------- - f : file - A file object for the .spc file to be read (i.e. file should be - already opened with ``open()``) - endianess : char - Byte-order of data to read - load_all_spc : bool - Switch to control if all of the .spc header is read, or just the parts - relevant to HyperSpy - - Returns - ------- - spc_header : np.ndarray - Array containing the binary information read from the .spc file - """ - version = np.fromfile(f, - dtype=[('version', '{}f4'.format(endianess))], - count=1) - version = round(float(version.item()[0]), 2) # convert to scalar - f.seek(0) - - spc_header = np.fromfile(f, - dtype=get_spc_dtype_list( - load_all=load_all_spc, - endianess=endianess, - version=version), - count=1) - - _logger.debug(' .spc version is {}'.format(version)) - - return spc_header - - -def get_spc_dtype_list(load_all=False, endianess='<', version=0.61): - """ - Get the data type list for an SPC spectrum. - Further information about the file format is available `here - `__. - - Parameters - ---------- - load_all : bool - Switch to control if all the data is loaded, or if just the - important pieces of the signal will be read (speeds up loading time) - endianess : char - byte-order used to read the data - version : float - version of spc file to read (only 0.61 and 0.70 have been tested) - Default is 0.61 to be as backwards-compatible as possible, but the - file version can be read from the file anyway, so this parameter - should always be set programmatically - - Table of header tags: - - fVersion: 4 byte float; *File format Version* - - aVersion: 4 byte float; *Application Version* - - fileName: 8 array of 1 byte char; *File name w/o '.spc' extension (OLD)* - - collectDateYear: 2 byte short; *Year the spectrum was collected* - - collectDateDay: 1 byte char; *Day the spectrum was collected* - - collectDateMon: 1 byte char; *Month the spectrum was collected* - - collectTimeMin: 1 byte char; *Minute the spectrum was collected* - - collectTimeHour: 1 byte char; *Hour the spectrum was collected* - - collectTimeHund: 1 byte char; *Hundredth second the spectrum was collected* - - collectTimeSec: 1 byte char; *Second the spectrum was collected* - - fileSize: 4 byte long; *Size of spectrum file in bytes* - - dataStart: 4 byte long; *Start of spectrum data in bytes offset from 0 of file* - - numPts: 2 byte short; *Number of spectrum pts* - - intersectingDist: 2 byte short; *Intersecting distance * 100 (mm)* - - workingDist: 2 byte short; *Working distance * 100* - - scaleSetting: 2 byte short; *Scale setting distance * 100* - - - filler1: 24 byte; - - - spectrumLabel: 256 array of 1 byte char; *Type label for spectrum, 0-39=material type, 40-255=sample* - - imageFilename: 8 array of 1 byte char; *Parent Image filename* - - spotX: 2 byte short; *Spot X in parent image file* - - spotY: 2 byte short; *Spot Y in parent image file* - - imageADC: 2 byte short; *Image ADC value 0-4095* - - discrValues: 5 array of 4 byte long; *Analyzer Discriminator Values* - - discrEnabled: 5 array of 1 byte unsigned char; *Discriminator Flags (0=Disabled,1=Enabled)* - - pileupProcessed: 1 byte char; *Pileup Processed Flag (0=No PU,1=Static PU, 2=Dynamic PU,...)* - - fpgaVersion: 4 byte long; *Firmware Version.* - - pileupProcVersion: 4 byte long; *Pileup Processing Software Version* - - NB5000CFG: 4 byte long; *Defines Hitachi NB5000 Dual Stage Cfg 0=None, 10=Eucentric Crossx,11= Eucentric Surface 12= Side Entry - Side 13 = Side Entry - Top* - - - filler2: 12 byte; - - - evPerChan: 4 byte long; *EV/channel* - - ADCTimeConstant: 2 byte short; *ADC Time constant* - - analysisType: 2 byte short; *Preset mode 1=clock, 2=count, 3=none, 4=live, 5=resume* - - preset: 4 byte float; *Analysis Time Preset value* - - maxp: 4 byte long; *Maximum counts of the spectrum* - - maxPeakCh: 4 byte long; *Max peak channel number* - - xRayTubeZ: 2 byte short; *XRF* - - filterZ: 2 byte short; *XRF* - - current: 4 byte float; *XRF* - - sampleCond: 2 byte short; *XRF Air= 0, Vacuum= 1, Helium= 2* - - sampleType: 2 byte short; *Bulk or thin* - - xrayCollimator: 2 byte unsigned short; *0=None, 1=Installed* - - xrayCapilaryType: 2 byte unsigned short; *0=Mono, 1=Poly* - - xrayCapilarySize: 2 byte unsigned short; *Range : 20 – 5000 Microns* - - xrayFilterThickness: 2 byte unsigned short; *Range : 0 – 10000 Microns* - - spectrumSmoothed: 2 byte unsigned short; *1= Spectrum Smoothed, Else 0* - - detector_Size_SiLi: 2 byte unsigned short; *Eagle Detector 0=30mm, 1=80mm* - - spectrumReCalib: 2 byte unsigned short; *1= Peaks Recalibrated, Else 0* - - eagleSystem: 2 byte unsigned short; *0=None, 2=Eagle2, 3=Eagle3, 4-Xscope* - - sumPeakRemoved: 2 byte unsigned short; *1= Sum Peaks Removed, Else 0* - - edaxSoftwareType: 2 byte unsigned short; *1= Team Spectrum, Else 0* - - - filler3: 6 byte; - - - escapePeakRemoved: 2 byte unsigned short; *1=Escape Peak Was Removed, Else 0* - - analyzerType: 4 byte unsigned long; *Hardware type 1=EDI1, 2=EDI2, 3=DPP2, 31=DPP-FR, 32=DPP-FR2, 4=DPP3, 5= APOLLO XLT/XLS/DPP-4 (EDPP)* - - startEnergy: 4 byte float; *Starting energy of spectrum* - - endEnergy: 4 byte float; *Ending energy of spectrum* - - liveTime: 4 byte float; *LiveTime* - - tilt: 4 byte float; *Tilt angle* - - takeoff: 4 byte float; *Take off angle* - - beamCurFact: 4 byte float; *Beam current factor* - - detReso: 4 byte float; *Detector resolution* - - detectType: 4 byte unsigned long; *Detector Type: 1=Std-BE, 2=UTW, 3=Super UTW, 4=ECON 3/4 Open, 5=ECON 3/4 Closed, 6=ECON 5/6 Open, 7=ECON 5/6 Closed, 8=TEMECON; Add + 10 For Sapphire SiLi Detectors, (11-18), which started shipping in 1996. 30 = APOLLO 10 SDD, 31=APOLLO XV, 32 = APOLLO 10+, 40 = APOLLO 40 SDD ,50 = APOLLO-X, 51=APOLLO-XP, 52 = APOLLO-XL, 53 = APOLLO XL-XRF, 60 =APOLLO-XLT-LS, 61 =APOLLO-XLT-NW, 62 =APOLLO-XLT-SUTW* - - parThick: 4 byte float; *Parlodion light shield thickness* - - alThick: 4 byte float; *Aluminum light shield thickness* - - beWinThick: 4 byte float; *Be window thickness* - - auThick: 4 byte float; *Gold light shield thickness* - - siDead: 4 byte float; *Si dead layer thickness* - - siLive: 4 byte float; *Si live layer thickness* - - xrayInc: 4 byte float; *X-ray incidence angle* - - azimuth: 4 byte float; *Azimuth angle of detector* - - elevation: 4 byte float; *Elevation angle of detector* - - bCoeff: 4 byte float; *K-line B coefficient* - - cCoeff: 4 byte float; *K-line C coefficient* - - tailMax: 4 byte float; *Tail function maximum channel* - - tailHeight: 4 byte float; *Tail height adjustment percentage* - - kV: 4 byte float; *Acc voltage* - - apThick: 4 byte float; *Ap window thickness* - - xTilt: 4 byte float; *x tilt angle for mDX* - - yTilt: 4 byte float; *y tilt angle for mDX* - - yagStatus: 4 byte unsigned long; *0 = N/A, 1 = YAG OUT, 2 = YAG IN* - - - filler4: 24 byte; - - - rawDataType: 2 byte unsigned short; *TEM or SEM data* - - totalBkgdCount: 4 byte float; *Accumulated background counts* - - totalSpectralCount: 4 byte unsigned long; *Accumulated spectrum counts* - - avginputCount: 4 byte float; *Average spectral counts* - - stdDevInputCount: 4 byte float; *Standard deviation of spectral counts* - - peakToBack: 2 byte unsigned short; *Peak to background setting. 0 = off, 1 = low, 2 = medium, 3 = high, 4 = user selected* - - peakToBackValue: 4 byte float; *Peak to back value* - - - filler5: 38 byte; - - - numElem: 2 byte short; *Number of peak id elements 0-48* - - at: 48 array of 2 byte unsigned short; *atomic numbers for peak id elems* - - line: 48 array of 2 byte unsigned short; *line numbers for peak id elems* - - energy: 48 array of 4 byte float; *float energy of identified peaks* - - height: 48 array of 4 byte unsigned long; *height in counts of id' ed peaks* - - spkht: 48 array of 2 byte short; *sorted peak height of id' ed peaks* - - - filler5_1: 30 byte; - - - numRois: 2 byte short; *Number of ROI's defined 0-48* - - st: 48 array of 2 byte short; *Start channel # for each ROI* - - end: 48 array of 2 byte short; *End channel # for each ROI* - - roiEnable: 48 array of 2 byte short; *ROI enable/disable flags* - - roiNames: (24 x 8) array of 1 byte char; *8 char name for eah ROI* - - - filler5_2: 1 byte; - - - userID: 80 array of 1 byte char; *User ID (Vision S/W) - Overlapping* - - - filler5_3: 111 byte; - - - sRoi: 48 array of 2 byte short; *sorted ROI heights* - - scaNum: 48 array of 2 byte short; *SCA number assigned for each ROI* - - - filler6: 12 byte; - - - backgrdWidth: 2 byte short; *Background width* - - manBkgrdPerc: 4 byte float; *Percentage to move manual background down* - - numBkgrdPts: 2 byte short; *Number of background points (2-64)* - - backMethod: 4 byte unsigned long; *Background method 1=auto, 2=manual* - - backStEng: 4 byte float; *Starting energy of background* - - backEndEng: 4 byte float; *Ending energy of background* - - bg: 64 array of 2 byte short; *Channel # of background point* - - bgType: 4 byte unsigned long; *Background type. 1 = curve, 2 = linear.* - - concenKev1: 4 byte float; *First concentration background point* - - concenKev2: 4 byte float; *Second concentration background point* - - concenMethod: 2 byte short; *0 = Off, 1 = On* - - jobFilename: 32 array of 1 byte char; *Vision Job Filename* - - - filler7: 16 byte; - - - numLabels: 2 byte short; *Number of displayed labels* - - label: (10 x 32) array 1 byte char; *32 character labels on the spectrum* - - labelx: 10 array of 2 byte short; *x position of label in terms of channel #* - - labely: 10 array of 4 byte long; *y position of label in terms of counts* - - zListFlag: 4 byte long; *Flag to indicate if Z List was written* - - bgPercents: 64 array of 4 byte float; *Percentage to move background point up and down.* - - IswGBg: 2 byte short; *= 1 if new backgrd pts exist* - - BgPoints: 5 array of 4 byte float; *Background points* - - IswGConc: 2 byte short; *= 1 if given concentrations exist* - - numConcen: 2 byte short; *Number of elements (up to 24)* - - ZList: 24 array of 2 byte short; *Element list for which given concentrations exist* - - GivenConc: 24 array of 4 byte float; *Given concentrations for each element in Zlist* - - - filler8: 598 byte; - - - s: 4096 array of 4 byte long; *counts for each channel* - - longFileName: 256 array of 1 byte char; *Long filename for 32 bit version* - - longImageFileName: 256 array of 1 byte char; *Associated long image file name* - - ADCTimeConstantNew: 4 byte float; *Time constant: 2.5… 100 OR 1.6… 102.4 us* - - # the following datatypes are only included for version 0.70: - - - filler9: 60 byte; - - - numZElements: 2 byte short; *number of Z List elements for quant* - - zAtoms: 48 array of 2 byte short; *Z List Atomic numbers* - - zShells: 48 array of 2 byte short; *Z List Shell numbers* - - Returns - ------- - dtype_list : list - List of the data tags and data types that will be used by numpy to - read an SPC file header. - """ - end = endianess - # important parameters are marked by "**" in comment - if load_all: - dtype_list = \ - [ # data offset (bytes) - ('fVersion', end + 'f4'), # 0 - ('aVersion', end + 'f4'), # 4 - ('fileName', '8i1'), # 8 - ('collectDateYear', end + 'i2'), # 16 - ('collectDateDay', end + 'i1'), # 17 - ('collectDateMon', end + 'i1'), - ('collectTimeMin', end + 'i1'), - ('collectTimeHour', end + 'i1'), - ('collectTimeHund', end + 'i1'), - ('collectTimeSec', end + 'i1'), - ('fileSize', end + 'i4'), # 24 - ('dataStart', end + 'i4'), # 28 - ('numPts', end + 'i2'), # 32 - ('intersectingDist', end + 'i2'), # 34 - ('workingDist', end + 'i2'), # 36 - ('scaleSetting', end + 'i2'), # 38 - - ('filler1', 'V24'), # 40 - - ('spectrumLabel', '256i1'), # 64 - ('imageFilename', '8i1'), # 320 - ('spotX', end + 'i2'), # 328 - ('spotY', end + 'i2'), # 330 - ('imageADC', end + 'i2'), # 332 - ('discrValues', end + '5i4'), # 334 - ('discrEnabled', end + '5i1'), # 354 - ('pileupProcessed', end + 'i1'), # 359 - ('fpgaVersion', end + 'i4'), # 360 - ('pileupProcVersion', end + 'i4'), # 364 - ('NB5000CFG', end + 'i4'), # 368 - - ('filler2', 'V12'), # 380 - - ('evPerChan', end + 'i4'), # 384 ** - ('ADCTimeConstant', end + 'i2'), # 388 - ('analysisType', end + 'i2'), # 390 - ('preset', end + 'f4'), # 392 - ('maxp', end + 'i4'), # 396 - ('maxPeakCh', end + 'i4'), # 400 - ('xRayTubeZ', end + 'i2'), # 404 - ('filterZ', end + 'i2'), # 406 - ('current', end + 'f4'), # 408 - ('sampleCond', end + 'i2'), # 412 - ('sampleType', end + 'i2'), # 414 - ('xrayCollimator', end + 'u2'), # 416 - ('xrayCapilaryType', end + 'u2'), # 418 - ('xrayCapilarySize', end + 'u2'), # 420 - ('xrayFilterThickness', end + 'u2'), # 422 - ('spectrumSmoothed', end + 'u2'), # 424 - ('detector_Size_SiLi', end + 'u2'), # 426 - ('spectrumReCalib', end + 'u2'), # 428 - ('eagleSystem', end + 'u2'), # 430 - ('sumPeakRemoved', end + 'u2'), # 432 - ('edaxSoftwareType', end + 'u2'), # 434 - - ('filler3', 'V6'), # 436 - - ('escapePeakRemoved', end + 'u2'), # 442 - ('analyzerType', end + 'u4'), # 444 - ('startEnergy', end + 'f4'), # 448 ** - ('endEnergy', end + 'f4'), # 452 - ('liveTime', end + 'f4'), # 456 ** - ('tilt', end + 'f4'), # 460 ** - ('takeoff', end + 'f4'), # 464 - ('beamCurFact', end + 'f4'), # 468 - ('detReso', end + 'f4'), # 472 ** - ('detectType', end + 'u4'), # 476 - ('parThick', end + 'f4'), # 480 - ('alThick', end + 'f4'), # 484 - ('beWinThick', end + 'f4'), # 488 - ('auThick', end + 'f4'), # 492 - ('siDead', end + 'f4'), # 496 - ('siLive', end + 'f4'), # 500 - ('xrayInc', end + 'f4'), # 504 - ('azimuth', end + 'f4'), # 508 ** - ('elevation', end + 'f4'), # 512 ** - ('bCoeff', end + 'f4'), # 516 - ('cCoeff', end + 'f4'), # 520 - ('tailMax', end + 'f4'), # 524 - ('tailHeight', end + 'f4'), # 528 - ('kV', end + 'f4'), # 532 ** - ('apThick', end + 'f4'), # 536 - ('xTilt', end + 'f4'), # 540 - ('yTilt', end + 'f4'), # 544 - ('yagStatus', end + 'u4'), # 548 - - ('filler4', 'V24'), # 552 - - ('rawDataType', end + 'u2'), # 576 - ('totalBkgdCount', end + 'f4'), # 578 - ('totalSpectralCount', end + 'u4'), # 582 - ('avginputCount', end + 'f4'), # 586 - ('stdDevInputCount', end + 'f4'), # 590 - ('peakToBack', end + 'u2'), # 594 - ('peakToBackValue', end + 'f4'), # 596 - - ('filler5', 'V38'), # 600 - - ('numElem', end + 'i2'), # 638 ** - ('at', end + '48u2'), # 640 ** - ('line', end + '48u2'), # 736 - ('energy', end + '48f4'), # 832 - ('height', end + '48u4'), # 1024 - ('spkht', end + '48i2'), # 1216 - - ('filler5_1', 'V30'), # 1312 - - ('numRois', end + 'i2'), # 1342 - ('st', end + '48i2'), # 1344 - ('end', end + '48i2'), # 1440 - ('roiEnable', end + '48i2'), # 1536 - ('roiNames', '(24,8)i1'), # 1632 - - ('filler5_2', 'V1'), # 1824 - - ('userID', '80i1'), # 1825 - - ('filler5_3', 'V111'), # 1905 - - ('sRoi', end + '48i2'), # 2016 - ('scaNum', end + '48i2'), # 2112 - - ('filler6', 'V12'), # 2208 - - ('backgrdWidth', end + 'i2'), # 2220 - ('manBkgrdPerc', end + 'f4'), # 2222 - ('numBkgrdPts', end + 'i2'), # 2226 - ('backMethod', end + 'u4'), # 2228 - ('backStEng', end + 'f4'), # 2232 - ('backEndEng', end + 'f4'), # 2236 - ('bg', end + '64i2'), # 2240 - ('bgType', end + 'u4'), # 2368 - ('concenKev1', end + 'f4'), # 2372 - ('concenKev2', end + 'f4'), # 2376 - ('concenMethod', end + 'i2'), # 2380 - ('jobFilename', end + '32i1'), # 2382 - - ('filler7', 'V16'), # 2414 - - ('numLabels', end + 'i2'), # 2430 - ('label', end + '(10,32)i1'), # 2432 - ('labelx', end + '10i2'), # 2752 - ('labely', end + '10i4'), # 2772 - ('zListFlag', end + 'i4'), # 2812 - ('bgPercents', end + '64f4'), # 2816 - ('IswGBg', end + 'i2'), # 3072 - ('BgPoints', end + '5f4'), # 3074 - ('IswGConc', end + 'i2'), # 3094 - ('numConcen', end + 'i2'), # 3096 - ('ZList', end + '24i2'), # 3098 - ('GivenConc', end + '24f4'), # 3146 - - ('filler8', 'V598'), # 3242 - - ('s', end + '4096i4'), # 3840 - ('longFileName', end + '256i1'), # 20224 - ('longImageFileName', end + '256i1'), # 20480 - ] - - if version >= 0.7: - dtype_list.extend([ - ('ADCTimeConstantNew', end + 'f4'), # 20736 - - ('filler9', 'V60'), # 20740 - - ('numZElements', end + 'i2'), # 20800 - ('zAtoms', end + '48i2'), # 20802 - ('zShells', end + '48i2'), # 20898 - ]) - - else: - dtype_list = \ - [ - ('filler1', 'V28'), # 0 - - ('dataStart', end + 'i4'), # 28 - ('numPts', end + 'i2'), # 32 ** - - ('filler1_1', 'V350'), # 34 - - ('evPerChan', end + 'i4'), # 384 ** - - ('filler2', 'V60'), # 388 - - ('startEnergy', end + 'f4'), # 448 ** - ('endEnergy', end + 'f4'), # 452 - ('liveTime', end + 'f4'), # 456 ** - ('tilt', end + 'f4'), # 460 ** - - ('filler3', 'V8'), # 464 - - ('detReso', end + 'f4'), # 472 ** - - ('filler4', 'V32'), # 476 - - ('azimuth', end + 'f4'), # 508 ** - ('elevation', end + 'f4'), # 512 ** - - ('filler5', 'V16'), # 516 - - ('kV', end + 'f4'), # 532 ** - - ('filler6', 'V102'), # 536 - - ('numElem', end + 'i2'), # 638 ** - ('at', end + '48u2'), # 640 ** - - ('filler7', 'V20004'), # 736 - - ] - return dtype_list - - -def __get_ipr_header(f, endianess): - """ - Get the header of an spc file, checking for the file version as necessary - - Parameters - ---------- - f : file - A file object for the .spc file to be read (i.e. file should be - already opened with ``open()``) - endianess : char - Byte-order of data to read - - Returns - ------- - ipr_header : np.ndarray - Array containing the binary information read from the .ipr file - """ - version = np.fromfile(f, - dtype=[('version', '{}i2'.format(endianess))], - count=1) - version = version.item()[0] # convert to scalar - f.seek(0) - _logger.debug(' .ipr version is {}'.format(version)) - - ipr_header = np.fromfile(f, - dtype=get_ipr_dtype_list( - endianess=endianess, - version=version), - count=1) - - return ipr_header - - -def get_ipr_dtype_list(endianess='<', version=333): - """ - Get the data type list for an IPR image description file. - Further information about the file format is available `here - `__. - - Table of header tags: - - - version: 2 byte unsigned short; *Current version number: 334* - - imageType: 2 byte unsigned short; *0=empty; 1=electron; 2=xmap; 3=disk; 4=overlay* - - label: 8 byte char array; *Image label* - - sMin: 2 byte unsigned short; *Min collected signal* - - sMax: 2 byte unsigned short; *Max collected signal* - - color: 2 byte unsigned short; *color: 0=gray; 1=R; 2=G; 3=B; 4=Y; 5=M; 6=C; 8=overlay* - - presetMode: 2 byte unsigned short; *0=clock; 1=live* - - presetTime: 4 byte unsigned long; *Dwell time for x-ray (millisec)* - - dataType: 2 byte unsigned short; *0=ROI; 1=Net intensity; 2=K ratio; 3=Wt%; 4=Mthin2* - - timeConstantOld: 2 byte unsigned short; *Amplifier pulse processing time [usec]* - - reserved1: 2 byte short; *Not used* - - roiStartChan: 2 byte unsigned short; *ROI starting channel* - - roiEndChan: 2 byte unsigned short; *ROI ending channel* - - userMin: 2 byte short; *User Defined Min signal range* - - userMax: 2 byte short; *User Defined Max signal range* - - iADC: 2 byte unsigned short; *Electron detector number: 1; 2; 3; 4* - - reserved2: 2 byte short; *Not used* - - iBits: 2 byte unsigned short; *conversion type: 8; 12 (not used)* - - nReads: 2 byte unsigned short; *No. of reads per point* - - nFrames: 2 byte unsigned short; *No. of frames averaged (not used)* - - fDwell: 4 byte float; *Dwell time (not used)* - - accV: 2 byte unsigned short; *V_acc in units of 100V* - - tilt: 2 byte short; *Sample tilt [deg]* - - takeoff: 2 byte short; *Takeoff angle [deg]* - - mag: 4 byte unsigned long; *Magnification* - - wd: 2 byte unsigned short; *Working distance [mm]* - - mppX: 4 byte float; *Microns per pixel in X direction* - - mppY: 4 byte float; *Microns per pixel in Y direction* - - nTextLines: 2 byte unsigned short; *No. of comment lines* - - charText: (4 x 32) byte character array; *Comment text* - - reserved3: 4 byte float; *Not used* - - nOverlayElements: 2 byte unsigned short; *No. of overlay elements* - - overlayColors: 16 array of 2 byte unsigned short; *Overlay colors* - - These two are specific to V334 of the file format, and are omitted - for compatibility with V333 of the IPR format: - - - timeConstantNew: 4 byte float; *Amplifier time constant [usec]* - - reserved4: 2 array of 4 byte float; *Not used* - - - Parameters - ---------- - endianess : char - byte-order used to read the data - version : float - version of .ipr file to read (only 333 and 334 have been tested) - Default is 333 to be as backwards-compatible as possible, but the - file version can be read from the file anyway, so this parameter - should always be set programmatically - - Returns - ------- - dtype_list : list - List of the data tags and data types that will be used by numpy to - read an IPR file. - """ - end = endianess - dtype_list = \ - [ - ('version', end + 'u2'), - ('imageType', end + 'u2'), - ('label', end + 'a8'), - ('sMin', end + 'u2'), - ('sMax', end + 'u2'), - ('color', end + 'u2'), - ('presetMode', end + 'u2'), - ('presetTime', end + 'u4'), - ('dataType', end + 'u2'), - ('timeConstantOld', end + 'u2'), - ('reserved1', end + 'i2'), - ('roiStartChan', end + 'u2'), - ('roiEndChan', end + 'u2'), - ('userMin', end + 'i2'), - ('userMax', end + 'i2'), - ('iADC', end + 'u2'), - ('reserved2', end + 'i2'), - ('iBits', end + 'u2'), - ('nReads', end + 'u2'), - ('nFrames', end + 'u2'), - ('fDwell', end + 'f4'), - ('accV', end + 'u2'), - ('tilt', end + 'i2'), - ('takeoff', end + 'i2'), - ('mag', end + 'u4'), - ('wd', end + 'u2'), - ('mppX', end + 'f4'), - ('mppY', end + 'f4'), - ('nTextLines', end + 'u2'), - ('charText', end + '4a32'), - ('reserved3', end + '4f4'), - ('nOverlayElements', end + 'u2'), - ('overlayColors', end + '16u2')] - - if version >= 334: - dtype_list.extend([ - ('timeConstantNew', end + 'f4'), - ('reserved4', end + '2f4'), - ]) - - return dtype_list - - -def _add_spc_metadata(metadata, spc_header): - """ - Return metadata with information from the .spc header added - - Parameters - ---------- - metadata : dict - current metadata of signal without spectral calibration information - added - spc_header : dict - header of .spc file that contains spectral information such as - azimuth and elevation angles, energy resolution, etc. - - Returns - ------- - metadata : dict - copy of original dictionary with spectral calibration added - """ - metadata['Acquisition_instrument'] = { - 'SEM': - {'Detector': - {'EDS': {'azimuth_angle': spc_header['azimuth'], - 'elevation_angle': spc_header['elevation'], - 'energy_resolution_MnKa': spc_header['detReso'], - 'live_time': spc_header['liveTime']}}, - 'beam_energy': spc_header['kV'], - 'Stage': {'tilt_alpha': spc_header['tilt']}} - } - - # Get elements stored in spectrum: - num_elem = spc_header['numElem'] - if num_elem > 0: - element_list = sorted([atomic_number2name[i] for - i in spc_header['at'][:num_elem]]) - metadata['Sample'] = {'elements': element_list} - _logger.info(" Elemental information found in the spectral metadata " - "was added to the signal.\n" - "Elements found were: {}\n".format(element_list)) - - return metadata - - -def spc_reader(filename, - endianess='<', - load_all_spc=False, - **kwargs): - """ - Read data from an SPC spectrum specified by filename. - - Parameters - ---------- - filename : str - Name of SPC file to read - endianess : char - Byte-order of data to read - load_all_spc : bool - Switch to control if all of the .spc header is read, or just the - important parts for import into HyperSpy - **kwargs - Remaining arguments are passed to the Numpy ``memmap`` function - - Returns - ------- - list - list with dictionary of signal information to be passed back to - hyperspy.io.load_with_reader - """ - with open(filename, 'rb') as f: - _logger.debug(' Reading {}'.format(filename)) - spc_header = __get_spc_header(f, endianess, load_all_spc) - - spc_dict = sarray2dict(spc_header) - original_metadata = {'spc_header': spc_dict} - - nz = original_metadata['spc_header']['numPts'] - data_offset = original_metadata['spc_header']['dataStart'] - - mode = kwargs.pop('mode', 'c') - lazy = kwargs.pop('lazy', False) - if lazy: - mode = 'r' - - # Read data from file into a numpy memmap object - data = np.memmap(f, mode=mode, offset=data_offset, - dtype='u4', shape=(1, nz), **kwargs).squeeze() - - # create the energy axis dictionary: - energy_axis = { - 'size': data.shape[0], - 'index_in_array': 0, - 'name': 'Energy', - 'scale': original_metadata['spc_header']['evPerChan'] / 1000.0, - 'offset': original_metadata['spc_header']['startEnergy'], - 'units': 'keV' - } - - # Assign metadata for spectrum: - metadata = {'General': {'original_filename': os.path.split(filename)[1], - 'title': 'EDS Spectrum'}, - "Signal": {'signal_type': "EDS_SEM", - 'record_by': 'spectrum', }, } - metadata = _add_spc_metadata(metadata, spc_dict) - - dictionary = {'data': data, - 'axes': [energy_axis], - 'metadata': metadata, - 'original_metadata': original_metadata} - - return [dictionary, ] - - -def spd_reader(filename, - endianess='<', - spc_fname=None, - ipr_fname=None, - load_all_spc=False, - **kwargs): - """ - Read data from an SPD spectral map specified by filename. - - Parameters - ---------- - filename : str - Name of SPD file to read - endianess : char - Byte-order of data to read - spc_fname : None or str - Name of file from which to read the spectral calibration. If data - was exported fully from EDAX TEAM software, an .spc file with the - same name as the .spd should be present. - If `None`, the default filename will be searched for. - Otherwise, the name of the .spc file to use for calibration can - be explicitly given as a string. - ipr_fname : None or str - Name of file from which to read the spatial calibration. If data - was exported fully from EDAX TEAM software, an .ipr file with the - same name as the .spd (plus a "_Img" suffix) should be present. - If `None`, the default filename will be searched for. - Otherwise, the name of the .ipr file to use for spatial calibration - can be explicitly given as a string. - load_all_spc : bool - Switch to control if all of the .spc header is read, or just the - important parts for import into HyperSpy - **kwargs - Remaining arguments are passed to the Numpy ``memmap`` function - - Returns - ------- - list - list with dictionary of signal information to be passed back to - hyperspy.io.load_with_reader - """ - with open(filename, 'rb') as f: - spd_header = np.fromfile(f, - dtype=get_spd_dtype_list(endianess), - count=1) - - original_metadata = {'spd_header': sarray2dict(spd_header)} - - # dimensions of map data: - nx = original_metadata['spd_header']['nPoints'] - ny = original_metadata['spd_header']['nLines'] - nz = original_metadata['spd_header']['nChannels'] - data_offset = original_metadata['spd_header']['dataOffset'] - data_type = {'1': 'u1', - '2': 'u2', - '4': 'u4'}[str(original_metadata['spd_header'][ - 'countBytes'])] - lazy = kwargs.pop('lazy', False) - mode = kwargs.pop('mode', 'c') - if lazy: - mode = 'r' - - # Read data from file into a numpy memmap object - data = np.memmap(f, mode=mode, offset=data_offset, dtype=data_type, - **kwargs).squeeze().reshape((nz, nx, ny), order='F').T - - # Convert char arrays to strings: - original_metadata['spd_header']['tag'] = \ - spd_header['tag'][0].view('S16')[0] - # fName is the name of the .bmp (and .ipr) file of the map - original_metadata['spd_header']['fName'] = \ - spd_header['fName'][0].view('S120')[0] - - # Get name of .spc file from the .spd map (if not explicitly given): - if spc_fname is None: - spc_path = os.path.dirname(filename) - spc_basename = os.path.splitext(os.path.basename(filename))[ - 0] + '.spc' - spc_fname = os.path.join(spc_path, spc_basename) - - # Get name of .ipr file from bitmap image (if not explicitly given): - if ipr_fname is None: - ipr_basename = os.path.splitext( - os.path.basename( - original_metadata['spd_header'][ - 'fName']))[0].decode() + '.ipr' - ipr_path = os.path.dirname(filename) - ipr_fname = os.path.join(ipr_path, ipr_basename) - - # Flags to control reading of files - read_spc = os.path.isfile(spc_fname) - read_ipr = os.path.isfile(ipr_fname) - - # Read the .ipr header (if possible) - if read_ipr: - with open(ipr_fname, 'rb') as f: - _logger.debug(' From .spd reader - ' - 'reading .ipr {}'.format(ipr_fname)) - ipr_header = __get_ipr_header(f, endianess) - original_metadata['ipr_header'] = sarray2dict(ipr_header) - - # Workaround for type error when saving hdf5: - # save as list of strings instead of numpy unicode array - # see https://github.com/hyperspy/hyperspy/pull/2007 and - # https://github.com/h5py/h5py/issues/289 for context - original_metadata['ipr_header']['charText'] = \ - [np.string_(i) for i in - original_metadata['ipr_header']['charText']] - else: - _logger.warning('Could not find .ipr file named {}.\n' - 'No spatial calibration will be loaded.' - '\n'.format(ipr_fname)) - - # Read the .spc header (if possible) - if read_spc: - with open(spc_fname, 'rb') as f: - _logger.debug(' From .spd reader - ' - 'reading .spc {}'.format(spc_fname)) - spc_header = __get_spc_header(f, endianess, load_all_spc) - spc_dict = sarray2dict(spc_header) - original_metadata['spc_header'] = spc_dict - else: - _logger.warning('Could not find .spc file named {}.\n' - 'No spectral metadata will be loaded.' - '\n'.format(spc_fname)) - - # create the energy axis dictionary: - energy_axis = { - 'size': data.shape[2], - 'index_in_array': 2, - 'name': 'Energy', - 'scale': original_metadata['spc_header']['evPerChan'] / 1000.0 if - read_spc else 1, - 'offset': original_metadata['spc_header']['startEnergy'] if - read_spc else 1, - 'units': 'keV' if read_spc else t.Undefined, - } - - nav_units = 'µm' - # Create navigation axes dictionaries: - x_axis = { - 'size': data.shape[1], - 'index_in_array': 1, - 'name': 'x', - 'scale': original_metadata['ipr_header']['mppX'] if read_ipr - else 1, - 'offset': 0, - 'units': nav_units if read_ipr else t.Undefined, - } - - y_axis = { - 'size': data.shape[0], - 'index_in_array': 0, - 'name': 'y', - 'scale': original_metadata['ipr_header']['mppY'] if read_ipr - else 1, - 'offset': 0, - 'units': nav_units if read_ipr else t.Undefined, - } - - # Assign metadata for spectrum image: - metadata = {'General': {'original_filename': os.path.split(filename)[1], - 'title': 'EDS Spectrum Image'}, - "Signal": {'signal_type': "EDS_SEM", - 'record_by': 'spectrum', }, } - - # Add spectral calibration and elements (if present): - if read_spc: - metadata = _add_spc_metadata(metadata, spc_dict) - - # Define navigation and signal axes: - axes = [y_axis, x_axis, energy_axis] - - dictionary = {'data': data, - 'axes': axes, - 'metadata': metadata, - 'original_metadata': original_metadata} - - return [dictionary, ] - - -def file_reader(filename, - record_by='spectrum', - endianess='<', - **kwargs): - """ - - Parameters - ---------- - filename : str - Name of file to read - record_by : str - EDAX EDS data is always recorded by 'spectrum', so this parameter - is not used - endianess : char - Byte-order of data to read - **kwargs - Additional keyword arguments supplied to the readers - - Returns - ------- - - """ - ext = os.path.splitext(filename)[1][1:] - if ext in spd_extensions: - return spd_reader(filename, - endianess, - **kwargs) - elif ext in spc_extensions: - return spc_reader(filename, - endianess, - **kwargs) - else: - raise IOError("Did not understand input file format.") diff --git a/hyperspy/io_plugins/emd.py b/hyperspy/io_plugins/emd.py deleted file mode 100644 index aeac092490..0000000000 --- a/hyperspy/io_plugins/emd.py +++ /dev/null @@ -1,1853 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -# The EMD format is a hdf5 standard proposed at Lawrence Berkeley -# National Lab (see http://emdatasets.com/ for more information). -# FEI later developed another EMD format, also based on the hdf5 standard. This -# reader first checked if the file have been saved by Velox (FEI EMD format) -# and use either the EMD class or the FEIEMDReader class to read the file. -# Writing file is only supported for EMD Berkeley file. - - -import re -import json -import os -from datetime import datetime -import time -import warnings -import math -import logging -import traits.api as t - -import h5py -import numpy as np -import dask.array as da -from dateutil import tz -import pint - -from hyperspy.exceptions import VisibleDeprecationWarning -from hyperspy.misc.elements import atomic_number2name -import hyperspy.misc.io.fei_stream_readers as stream_readers -from hyperspy.io_plugins.hspy import get_signal_chunks - - -# Plugin characteristics -# ---------------------- -format_name = 'Electron Microscopy Data (EMD)' -description = 'Read data from Berkeleys EMD files.' -full_support = False # Hopefully? -# Recognised file extension -file_extensions = ('emd', 'EMD') -default_extension = 0 -# Reading capabilities -reads_images = True -reads_spectrum = True -reads_spectrum_image = True -# Writing capabilities -writes = True # Only Berkeley emd -non_uniform_axis = False -EMD_VERSION = '0.2' -# ---------------------- - -_logger = logging.getLogger(__name__) - - -class EMD(object): - - """Class for storing electron microscopy datasets. - - The :class:`~.EMD` class can hold an arbitrary amount of datasets in the - `signals` dictionary. These are saved as HyperSpy - :class:`~hyperspy.signal.Signal` instances. Global metadata are saved in - four dictionaries (`user`, `microscope`, `sample`, `comments`). To print - relevant information about the EMD instance use the :func:`~.log_info` - function. EMD instances can be loaded from and saved to emd-files, an - hdf5 standard developed at Lawrence - Berkeley National Lab (https://emdatasets.com/). - - Attributes - ---------- - signals: dictionary - Dictionary which contains all datasets as - :class:`~hyperspy.signal.Signal` instances. - user : dictionary - Dictionary which contains user related metadata. - microscope : dictionary - Dictionary which contains microscope related metadata. - sample : dictionary - Dictionary which contains sample related metadata. - comments : dictionary - Dictionary which contains additional commentary metadata. - - """ - - _log = logging.getLogger(__name__) - - def __init__(self, signals=None, user=None, - microscope=None, sample=None, comments=None): - msg = ( - "Direct instantiation of the EMD class is deprecated and will be " - "removed in HyperSpy v2.0. Please use the `hs.load` function " - "instead.") - warnings.warn(msg, VisibleDeprecationWarning) - self._log.debug('Calling __init__') - # Create dictionaries if not present: - if signals is None: - signals = {} - if user is None: - user = {} - if microscope is None: - microscope = {} - if sample is None: - sample = {} - if comments is None: - comments = {} - # Make sure some default keys are present in user: - for key in ['name', 'institution', 'department', 'email']: - if key not in user: - user[key] = '' - self.user = user - # Make sure some default keys are present in microscope: - for key in ['name', 'voltage']: - if key not in microscope: - microscope[key] = '' - self.microscope = microscope - # Make sure some default keys are present in sample: - for key in ['material', 'preparation']: - if key not in sample: - sample[key] = '' - self.sample = sample - # Add comments: - self.comments = comments - # Make sure the signals are added properly to signals: - self.signals = {} - for name, signal in signals.items(): - self.add_signal(signal, name) - - def __getitem__(self, key): - # This is for accessing the raw data easily. For the signals use - # emd.signals[key]! - return self.signals[key].data - - def _write_signal_to_group(self, signal_group, signal): - self._log.debug('Calling _write_signal_to_group') - # Save data: - dataset = signal_group.require_group(signal.metadata.General.title) - maxshape = tuple(None for _ in signal.data.shape) - dataset.create_dataset( - 'data', - data=signal.data, - chunks=True, - maxshape=maxshape) - # Iterate over all dimensions: - for i in range(len(signal.data.shape)): - key = 'dim{}'.format(i + 1) - axis = signal.axes_manager._axes[i] - offset = axis.offset - scale = axis.scale - dim = dataset.create_dataset(key, data=[offset, offset + scale]) - name = axis.name - from traits.trait_base import _Undefined - if isinstance(name, _Undefined): - name = '' - dim.attrs['name'] = name - units = axis.units - if isinstance(units, _Undefined): - units = '' - else: - units = '[{}]'.format('_'.join(list(units))) - dim.attrs['units'] = units - # Write metadata: - dataset.attrs['emd_group_type'] = 1 - for key, value in signal.metadata.Signal: - try: # If something h5py can't handle is saved in the metadata... - dataset.attrs[key] = value - except Exception: # ...let the user know what could not be added! - self._log.exception( - 'The hdf5 writer could not write the following ' - 'information in the file: %s : %s', key, value) - - def _read_signal_from_group(self, name, group, lazy=False): - self._log.debug('Calling _read_signal_from_group') - from hyperspy import signals - # Extract essential data: - data = group.get('data') - if lazy: - data = da.from_array(data, chunks=data.chunks) - else: - data = np.asanyarray(data) - # EMD does not have a standard way to describe the signal axis. - # Therefore we return a BaseSignal - signal = signals.BaseSignal(data) - # Set signal properties: - signal.set_signal_origin = group.attrs.get('signal_origin', '') - signal.set_signal_type = group.attrs.get('signal_type', '') - # Iterate over all dimensions: - for i in range(len(data.shape)): - dim = group.get('dim{}'.format(i + 1)) - axis = signal.axes_manager._axes[i] - axis_name = dim.attrs.get('name', '') - if isinstance(axis_name, bytes): - axis_name = axis_name.decode('utf-8') - axis.name = axis_name - - axis_units = dim.attrs.get('units', '') - if isinstance(axis_units, bytes): - axis_units = axis_units.decode('utf-8') - units = re.findall(r'[^_\W]+', axis_units) - axis.units = ''.join(units) - try: - if len(dim) == 1: - axis.scale = 1. - self._log.warning( - 'Could not calculate scale of axis {}. ' - 'Setting scale to 1'.format(i)) - else: - axis.scale = dim[1] - dim[0] - axis.offset = dim[0] - # HyperSpy then uses defaults (1.0 and 0.0)! - except (IndexError, TypeError) as e: - self._log.warning( - 'Could not calculate scale/offset of ' - 'axis {}: {}'.format(i, e)) - # Extract metadata: - metadata = {} - for key, value in group.attrs.items(): - metadata[key] = value - if signal.data.dtype == np.object: - self._log.warning('HyperSpy could not load the data in {}, ' - 'skipping it'.format(name)) - else: - # Add signal: - self.add_signal(signal, name, metadata) - - def add_signal(self, signal, name=None, metadata=None): - """Add a HyperSpy signal to the EMD instance and make sure all - metadata is present. - - Parameters - ---------- - signal : :class:`~hyperspy.signal.Signal` - HyperSpy signal which should be added to the EMD instance. - name : string, optional - Name of the (used as a key for the `signals` dictionary). If not - specified, `signal.metadata.General.title` will be used. If this - is an empty string, both name and signal title are set to 'dataset' - per default. If specified, `name` overwrites the - signal title. - metadata : dictionary - Dictionary which holds signal specific metadata which will - be added to the signal. - - Returns - ------- - None - - Notes - ----- - This is the preferred way to add signals to the EMD instance. - Directly adding to the `signals` dictionary is possible but does not - make sure all metadata are correct. This method is also called in - the standard constructor on all entries in the `signals` dictionary! - - """ - self._log.debug('Calling add_signal') - # Create metadata if not present: - if metadata is None: - metadata = {} - # Check and save title: - if name is not None: # Overwrite Signal title! - signal.metadata.General.title = name - else: - # Take title of Signal! - if signal.metadata.General.title != '': - name = signal.metadata.General.title - else: # Take default! - name = '__unnamed__' - signal.metadata.General.title = name - # Save signal metadata: - signal.metadata.Signal.add_dictionary(metadata) - # Save global metadata: - signal.metadata.General.add_node('user') - signal.metadata.General.user.add_dictionary(self.user) - signal.metadata.General.add_node('microscope') - signal.metadata.General.microscope.add_dictionary(self.microscope) - signal.metadata.General.add_node('sample') - signal.metadata.General.sample.add_dictionary(self.sample) - signal.metadata.General.add_node('comments') - signal.metadata.General.comments.add_dictionary(self.comments) - # Also save metadata as original_metadata: - signal.original_metadata.add_dictionary( - signal.metadata.as_dictionary()) - # Add signal: - self.signals[name] = signal - - @classmethod - def load_from_emd(cls, filename, lazy=False, dataset_name=None): - """Construct :class:`~.EMD` object from an emd-file. - - Parameters - ---------- - filename : str - The name of the emd-file from which to load the signals. Standard - file extesnion is '.emd'. - False : bool, optional - If False (default) loads data to memory. If True, enables loading - only if requested. - dataset_name : str or iterable, optional - Only add dataset with specific name. Note, this has to be the full - group path in the file. For example '/experimental/science_data'. - If the dataset is not found, an IOError with the possible - datasets will be raised. Several names can be specified - in the form of a list. - - Returns - ------- - emd : :class:`~.EMD` - A :class:`~.EMD` object containing the loaded signals. - - """ - cls._log.debug('Calling load_from_emd') - # Read in file: - emd_file = h5py.File(filename, 'r') - # Creat empty EMD instance: - emd = cls() - # Extract user: - user_group = emd_file.get('user') - if user_group is not None: - for key, value in user_group.attrs.items(): - emd.user[key] = value - # Extract microscope: - microscope_group = emd_file.get('microscope') - if microscope_group is not None: - for key, value in microscope_group.attrs.items(): - emd.microscope[key] = value - # Extract sample: - sample_group = emd_file.get('sample') - if sample_group is not None: - for key, value in sample_group.attrs.items(): - emd.sample[key] = value - # Extract comments: - comments_group = emd_file.get('comments') - if comments_group is not None: - for key, value in comments_group.attrs.items(): - emd.comments[key] = value - # Extract signals: - node_list = list(emd_file.keys()) - for key in ['user', 'microscope', - 'sample', 'comments']: # Nodes which are not the data! - if key in node_list: - node_list.pop(node_list.index(key)) # Pop all unwanted nodes! - dataset_in_file_list = [] - for node in node_list: - data_group = emd_file.get(node) - if data_group is not None: - for group in data_group.values(): - name = group.name - if isinstance(group, h5py.Group): - if group.attrs.get('emd_group_type') == 1: - dataset_in_file_list.append(name) - if len(dataset_in_file_list) == 0: - raise IOError("No datasets found in {0}".format(filename)) - dataset_read_list = [] - if dataset_name is not None: - if isinstance(dataset_name, str): - dataset_name = [dataset_name] - - for temp_dataset_name in dataset_name: - if temp_dataset_name in dataset_in_file_list: - dataset_read_list.append(temp_dataset_name) - else: - raise IOError( - "Dataset with name {0} not found in the file. " - "Possible datasets are {1}.".format( - temp_dataset_name, - ', '.join(dataset_in_file_list))) - else: - dataset_read_list = dataset_in_file_list - for dataset_read in dataset_read_list: - group = emd_file[dataset_read] - emd._read_signal_from_group(dataset_read, group, lazy) - - # Close file and return EMD object: - if not lazy: - emd_file.close() - return emd - - def save_to_emd(self, filename='datacollection.emd'): - """Save :class:`~.EMD` data in a file with emd(hdf5)-format. - - Parameters - ---------- - filename : string, optional - The name of the emd-file in which to store the signals. - The default is 'datacollection.emd'. - - Returns - ------- - None - - """ - self._log.debug('Calling save_to_emd') - # Open file: - emd_file = h5py.File(filename, 'w') - # Write version: - ver_maj, ver_min = EMD_VERSION.split('.') - emd_file.attrs['version_major'] = ver_maj - emd_file.attrs['version_minor'] = ver_min - # Write user: - user_group = emd_file.require_group('user') - for key, value in self.user.items(): - user_group.attrs[key] = value - # Write microscope: - microscope_group = emd_file.require_group('microscope') - for key, value in self.microscope.items(): - microscope_group.attrs[key] = value - # Write sample: - sample_group = emd_file.require_group('sample') - for key, value in self.sample.items(): - sample_group.attrs[key] = value - # Write comments: - comments_group = emd_file.require_group('comments') - for key, value in self.comments.items(): - comments_group.attrs[key] = value - # Write signals: - signal_group = emd_file.require_group('signals') - for signal in self.signals.values(): - self._write_signal_to_group(signal_group, signal) - # Close file and return EMD object: - emd_file.close() - - def log_info(self): - """( all relevant information about the EMD instance.""" - self._log.debug('Calling log_info') - pad_string0 = '-------------------------\n' - pad_string1 = '\n-------------------------\n' - info_str = '\nUser:' + pad_string1 - for key, value in self.user.items(): - info_str += '{:<15}: {}\n'.format(key, value) - info_str += pad_string0 + '\nMicroscope:' + pad_string1 - for key, value in self.microscope.items(): - info_str += '{:<15}: {}\n'.format(key, value) - info_str += pad_string0 + '\nSample:' + pad_string1 - for key, value in self.sample.items(): - info_str += '{:<15}: {}\n'.format(key, value) - info_str += pad_string0 + '\nComments:' + pad_string1 - for key, value in self.comments.items(): - info_str += '{:<15}: {}\n'.format(key, value) - info_str += pad_string0 + '\nData:' + pad_string1 - for key, value in self.signals.items(): - info_str += '{:<15}: {}\n'.format(key, value) - sig_dict = value.metadata.Signal - for k in sig_dict.keys(): - info_str += ' |-- {}: {}\n'.format(k, sig_dict[k]) - info_str += pad_string0 - self._log.info(info_str) - - -class EMD_NCEM: - - """Class for reading and writing the Berkeley variant of the electron - microscopy datasets (EMD) file format. It reads files EMD NCEM, including - files generated by the prismatic software. - - Attributes - ---------- - dictionaries: list - List of dictionaries which are passed to the file_reader. - """ - - def __init__(self): - self._ureg = pint.UnitRegistry() - - def read_file(self, file, lazy=None, dataset_path=None, stack_group=None): - """ - Read the data from an emd file - - Parameters - ---------- - file : file handle - Handle of the file to read the data from. - lazy : bool, optional - Load the data lazily. The default is False. - dataset_path : None, str or list of str - Path of the dataset. If None, load all supported datasets, - otherwise the specified dataset. The default is None. - stack_group : bool, optional - Stack datasets of groups with common name. Relevant for emd file - version >= 0.5 where groups can be named 'group0000', 'group0001', - etc. - """ - self.file = file - self.lazy = lazy - - if isinstance(dataset_path, list): - if stack_group: - _logger.warning("The argument 'dataset_path' and " - "'stack_group' are not compatible.") - stack_group = False - dataset_path = dataset_path.copy() - elif isinstance(dataset_path, str): - dataset_path = [dataset_path] - # if 'datasets' is not provided, we load all valid datasets - elif dataset_path is None: - dataset_path = self.find_dataset_paths(file) - if stack_group is None: - stack_group = True - - self.dictionaries = [] - - while len(dataset_path) > 0: - path = dataset_path.pop(0) - group_paths = [os.path.dirname(path)] - dataset_name = os.path.basename(path) - - if stack_group: - # Find all the datasets in this group which are also listed - # in dataset_path: - # 1. add them to 'group_paths' - # 2. remove them from 'dataset_path' - group_basename = group_paths[0] - if self._is_prismatic_file and 'ppotential' not in path: - # In prismatic file, the group name have '0000' except - # for 'ppotential' - group_basename = group_basename[:-4] - for _path in dataset_path[:]: - if path != _path and group_basename in _path: - group_paths.append(os.path.dirname(_path)) - dataset_path.remove(_path) - title = os.path.basename(group_basename) - else: - title = os.path.basename(group_paths[0]) - - _logger.debug(f'Loading dataset: {path}') - - om = self._parse_original_metadata() - data, axes = self._read_data_from_groups( - group_paths, - dataset_name, - title, - om) - - md = self._parse_metadata(group_paths[0], title=title) - d = {'data': data, - 'axes': axes, - 'metadata': md, - 'original_metadata': om, - } - self.dictionaries.append(d) - - @classmethod - def find_dataset_paths(cls, file, supported_dataset=True): - """ - Find the paths of all groups containing valid EMD data. - - Parameters - ---------- - file : hdf5 file handle - supported_dataset : bool, optional - If True (default), returns the paths of all supported datasets, - otherwise returns the path of the non-supported other dataset. - This is relevant for groups containing auxiliary dataset(s) which - are not supported by HyperSpy or described in the EMD NCEM dataset - specification. - - Returns - ------- - datasets : list - List of path to these group. - - """ - def print_dataset_only(item_name, item, dataset_only): - if supported_dataset is os.path.basename(item_name).startswith( - ('data', 'counted_datacube', 'datacube', 'diffractionslice', - 'realslice', 'pointlistarray', 'pointlist')): - if isinstance(item, h5py.Dataset): - grp = file.get(os.path.dirname(item_name)) - if cls._get_emd_group_type(grp): - dataset_path.append(item_name) - - f = lambda item_name, item: print_dataset_only(item_name, item, - supported_dataset) - - dataset_path = [] - file.visititems(f) - - return dataset_path - - @property - def _is_prismatic_file(self): - return True if '4DSTEM_simulation' in self.file.keys() else False - - @property - def _is_py4DSTEM_file(self): - return True if '4DSTEM_experiment' in self.file.keys() else False - - @staticmethod - def _get_emd_group_type(group): - """ Return the value of the 'emd_group_type' attribute if it exist, - otherwise returns False - """ - return group.attrs.get('emd_group_type', False) - - @staticmethod - def _read_dataset(dataset): - """Read dataset and use the h5py AsStrWrapper when the dataset is of - string type (h5py 3.0 and newer) - """ - chunks = dataset.chunks - if chunks is None: - chunks = 'auto' - if (h5py.check_string_dtype(dataset.dtype) and - hasattr(dataset, 'asstr')): - # h5py 3.0 and newer - # https://docs.h5py.org/en/3.0.0/strings.html - dataset = dataset.asstr()[:] - return dataset, chunks - - def _read_emd_version(self, group): - """ Return the group version if the group is an EMD group, otherwise - return None. - """ - if 'version_major' in group.attrs.keys(): - version = [str(group.attrs.get(v)) - for v in ['version_major', 'version_minor']] - version = ".".join(version) - return version - - def _read_data_from_groups(self, group_path, dataset_name, stack_key=None, - original_metadata={}): - axes = [] - transpose_required = True if dataset_name != 'datacube' else False - - array_list = [self.file.get(f'{key}/{dataset_name}') for key in group_path] - - if None in array_list: - raise IOError("Dataset can't be found.") - - if len(array_list) > 1: - # Squeeze the data only when - if self.lazy: - data_list = [da.from_array(*self._read_dataset(d)) - for d in array_list] - if transpose_required: - data_list = [da.transpose(d) for d in data_list] - data = da.stack(data_list) - data = da.squeeze(data) - else: - data_list = [np.asanyarray(self._read_dataset(d)[0]) - for d in array_list] - if transpose_required: - data_list = [np.transpose(d) for d in data_list] - data = np.stack(data_list).squeeze() - else: - d = array_list[0] - if self.lazy: - data = da.from_array(*self._read_dataset(d)) - else: - data = np.asanyarray(self._read_dataset(d)[0]) - if transpose_required: - data = data.transpose() - - shape = data.shape - - if len(array_list) > 1: - offset, scale, units = 0, 1, t.Undefined - if self._is_prismatic_file and 'depth' in stack_key: - simu_om = original_metadata.get('simulation_parameters', {}) - if 'numSlices' in simu_om.keys(): - scale = simu_om['numSlices'] - scale *= simu_om.get('sliceThickness', 1.0) - if 'zStart' in simu_om.keys(): - offset = simu_om['zStart'] - # when zStart = 0, the first image is not at zero but - # the first output: numSlices * sliceThickness (=scale) - if offset == 0: - offset = scale - units = 'Å' - total_thickness = (simu_om.get('tile', 0)[2] * - simu_om.get('cellDimension', 0)[0]) - if not math.isclose(total_thickness, len(array_list) * scale, - rel_tol=1e-4): - _logger.warning("Depth axis is non-uniform and its offset " - "and scale can't be set accurately.") - # When non-uniform/non-linear axis are implemented, adjust - # the final depth to the "total_thickness" - offset, scale, units = 0, 1, t.Undefined - axes.append({'index_in_array': 0, - 'name': stack_key if stack_key is not None else t.Undefined, - 'offset': offset, - 'scale': scale, - 'size': len(array_list), - 'units': units, - 'navigate': True}) - - array_indices = np.arange(1, len(shape)) - dim_indices = array_indices - else: - array_indices = np.arange(0, len(shape)) - # dim indices start form 1 - dim_indices = array_indices + 1 - - if transpose_required: - dim_indices = dim_indices[::-1] - - for arr_index, dim_index in zip(array_indices, dim_indices): - dim = self.file.get(f'{group_path[0]}/dim{dim_index}') - offset, scale = self._parse_axis(dim) - if self._is_prismatic_file: - if dataset_name == 'datacube': - # For datacube (4D STEM), the signal is detector coordinate - sig_dim = ['dim3', 'dim4'] - else: - sig_dim = ['dim1', 'dim2'] - - navigate = dim.name.split('/')[-1] not in sig_dim - - else: - navigate = False - axes.append({'index_in_array': arr_index, - 'name': self._parse_attribute(dim, 'name'), - 'units': self._parse_attribute(dim, 'units'), - 'size': shape[arr_index], - 'offset': offset, - 'scale': scale, - 'navigate': navigate, - }) - return data, axes - - def _parse_attribute(self, obj, key): - value = obj.attrs.get(key) - if value is None: - value = t.Undefined - else: - if not isinstance(value, str): - value = value.decode() - if key == 'units': - # Get all the units - units_list = re.findall(r"(\[.+?\])", value) - units_list = [u[1:-1].replace("_", "") for u in units_list] - value = ' * '.join(units_list) - try: - units = self._ureg.parse_units(value) - value = f"{units:~}" - except: - pass - return value - - def _parse_metadata(self, group_basename, title=''): - filename = self.file if isinstance(self.file, str) else self.file.filename - md = { - 'General': {'title': title.replace('_depth', ''), - 'original_filename': os.path.split(filename)[1]}, - "Signal": {'signal_type': ""} - } - if 'CBED' in group_basename: - md['Signal']['signal_type'] = 'electron_diffraction' - return md - - def _parse_original_metadata(self): - f = self.file - om = {'EMD_version':self._read_emd_version(self.file.get('/'))} - for group_name in ['microscope', 'sample', 'user', 'comments']: - group = f.get(group_name) - if group is not None: - om.update({group_name:{key:value for key, value in group.attrs.items()}}) - - if self._is_prismatic_file: - md_mapping = {'i':'filenameAtoms', 'a': 'algorithm', - 'fx':'interpolationFactorX', 'fy':'interpolationFactorY', - 'F':'numFP', 'ns':'numSlices', 'te':'includeThermalEffects', - 'oc':'includeOccupancy', '3D':'save3DOutput', '4D': 'save3DOutput', - 'DPC':'saveDPC_CoM', 'ps':'savePotentialSlices', 'nqs':'nyquistSampling', - 'px':'realspacePixelSizeX', 'py':'realspacePixelSizeY', - 'P':'potBound', 's':'sliceThickness', 'zs': 'zStart', 'E':'E0', - 'A':'alphaBeamMax', 'rx':'probeStepX', 'ry':'probeStepY', - 'df':'probeDefocus', 'sa':'probeSemiangle', 'd':'detectorAngleStep', - 'tx':'probeXtilt', 'ty':'probeYtilt', 'c':'cellDimension', - 't':'tile', 'wx':'scanWindowX', 'wy':'scanWindowY', - 'wxr':'scanWindowX_r', 'wyr':'scanWindowY_r','2D':'integrationAngle'} - simu_md = f.get( - '4DSTEM_simulation/metadata/metadata_0/original/simulation_parameters') - om['simulation_parameters'] = {md_mapping.get(k, k):v for k, v in - simu_md.attrs.items()} - - return om - - @staticmethod - def _parse_axis(axis_data): - """ - Estimate, offset, scale from a 1D array - """ - if axis_data.ndim > 0 and np.issubdtype(axis_data.dtype, np.number): - offset, scale = axis_data[0], np.diff(axis_data).mean() - else: - # This is a string, return default values - # When non-uniform axis is supported we should be able to parse - # string - offset, scale = 0, 1 - return offset, scale - - def write_file(self, file, signal, **kwargs): - """ - Write signal to file. - - Parameters - ---------- - file : str of h5py file handle - If str, filename of the file to write, otherwise a h5py file handle - signal : instance of hyperspy signal - The signal to save. - **kwargs : dict - Keyword argument are passed to the ``h5py.Group.create_dataset`` - method. - - """ - if isinstance(file, str): - emd_file = h5py.File(file, 'w') - # Write version: - ver_maj, ver_min = EMD_VERSION.split('.') - emd_file.attrs['version_major'] = ver_maj - emd_file.attrs['version_minor'] = ver_min - - # Write attribute from the original_metadata - om = signal.original_metadata - for group_name in ['microscope', 'sample', 'user', 'comments']: - group = emd_file.require_group(group_name) - d = om.get_item(group_name, None) - if d is not None: - for key, value in d.as_dictionary().items(): - group.attrs[key] = value - - # Write signals: - signal_group = emd_file.require_group('signals') - signal_group.attrs['emd_group_type'] = 1 - self._write_signal_to_group(signal_group, signal, **kwargs) - emd_file.close() - - def _write_signal_to_group(self, signal_group, signal, chunks=None, - **kwargs): - # Save data: - title = signal.metadata.General.title or '__unnamed__' - dataset = signal_group.require_group(title) - data = signal.data.T - maxshape = tuple(None for _ in data.shape) - if np.issubdtype(data.dtype, np.dtype('U')): - # Saving numpy unicode type is not supported in h5py - data = data.astype(np.dtype('S')) - if chunks is None: - if isinstance(data, da.Array): - # For lazy dataset, by default, we use the current dask chunking - chunks = tuple([c[0] for c in data.chunks]) - else: - signal_axes = signal.axes_manager.signal_indices_in_array - chunks = get_signal_chunks(data.shape, data.dtype, signal_axes) - # when chunks=True, we leave it to h5py `guess_chunk` - elif chunks is not True: - # Need to reverse since the data is transposed when saving - chunks = chunks[::-1] - - dataset.create_dataset('data', data=data, maxshape=maxshape, - chunks=chunks, **kwargs) - - array_indices = np.arange(0, len(data.shape)) - dim_indices = (array_indices + 1)[::-1] - # Iterate over all dimensions: - for i, dim_index in zip(array_indices, dim_indices): - key = f'dim{dim_index}' - axis = signal.axes_manager._axes[i] - offset = axis.offset - scale = axis.scale - dim = dataset.create_dataset(key, data=[offset, offset + scale]) - name = axis.name - if name is t.Undefined: - name = '' - dim.attrs['name'] = name - units = axis.units - if units is t.Undefined: - units = '' - else: - units = '[{}]'.format('_'.join(list(units))) - dim.attrs['units'] = units - # Write metadata: - dataset.attrs['emd_group_type'] = 1 - for key, value in signal.metadata.Signal: - try: # If something h5py can't handle is saved in the metadata... - dataset.attrs[key] = value - except Exception: # ...let the user know what could not be added! - _logger.warning("The following information couldn't be " - f"written in the file: {key}: {value}") - - -def _get_keys_from_group(group): - # Return a list of ids of items contains in the group - return list(group.keys()) - - -def _parse_sub_data_group_metadata(sub_data_group): - metadata_array = sub_data_group['Metadata'][:, 0].T - mdata_string = metadata_array.tobytes().decode("utf-8") - return json.loads(mdata_string.rstrip('\x00')) - - -def _parse_metadata(data_group, sub_group_key): - return _parse_sub_data_group_metadata(data_group[sub_group_key]) - - -def _get_detector_metadata_dict(om, detector_name): - detectors_dict = om['Detectors'] - # find detector dict from the detector_name - for key in detectors_dict: - if detectors_dict[key]['DetectorName'] == detector_name: - return detectors_dict[key] - - -class FeiEMDReader(object): - """ - Class for reading FEI electron microscopy datasets. - - The :class:`~.FeiEMDReader` reads EMD files saved by the FEI Velox - software package. - - Attributes - ---------- - dictionaries: list - List of dictionaries which are passed to the file_reader. - im_type : string - String specifying whether the data is an image, spectrum or - spectrum image. - - """ - - def __init__(self, filename=None, select_type=None, first_frame=0, - last_frame=None, sum_frames=True, sum_EDS_detectors=True, - rebin_energy=1, SI_dtype=None, load_SI_image_stack=False, - lazy=False): - # TODO: Finish lazy implementation using the `FrameLocationTable` - # Parallelise streams reading - self.filename = filename - self.select_type = select_type - self.ureg = pint.UnitRegistry() - self.dictionaries = [] - self.first_frame = first_frame - self.last_frame = last_frame - self.sum_frames = sum_frames - self.sum_EDS_detectors = sum_EDS_detectors - self.rebin_energy = rebin_energy - self.SI_data_dtype = SI_dtype - self.load_SI_image_stack = load_SI_image_stack - self.lazy = lazy - self.detector_name = None - self.original_metadata = {} - - def read_file(self, f): - self.filename = f.filename - self.d_grp = f.get('Data') - self._check_im_type() - self._parse_metadata_group(f.get('Operations'), 'Operations') - if self.im_type == 'SpectrumStream': - self.p_grp = f.get('Presentation') - self._parse_image_display() - self._read_data(self.select_type) - - def _read_data(self, select_type): - self.load_images = self.load_SI = self.load_single_spectrum = True - if select_type == 'single_spectrum': - self.load_images = self.load_SI = False - elif select_type == 'images': - self.load_SI = self.load_single_spectrum = False - elif select_type == 'spectrum_image': - self.load_images = self.load_single_spectrum = False - elif select_type is None: - pass - else: - raise ValueError("`select_type` parameter takes only: `None`, " - "'single_spectrum', 'images' or 'spectrum_image'.") - - if self.im_type == 'Image': - _logger.info('Reading the images.') - self._read_images() - elif self.im_type == 'Spectrum': - self._read_single_spectrum() - self._read_images() - elif self.im_type == 'SpectrumStream': - self._read_single_spectrum() - _logger.info('Reading the spectrum image.') - t0 = time.time() - self._read_images() - t1 = time.time() - self._read_spectrum_stream() - t2 = time.time() - _logger.info('Time to load images: {} s.'.format(t1 - t0)) - _logger.info('Time to load spectrum image: {} s.'.format(t2 - t1)) - - def _check_im_type(self): - if 'Image' in self.d_grp: - if 'SpectrumImage' in self.d_grp: - self.im_type = 'SpectrumStream' - else: - self.im_type = 'Image' - else: - self.im_type = 'Spectrum' - - def _read_single_spectrum(self): - if not self.load_single_spectrum: - return - spectrum_grp = self.d_grp.get("Spectrum") - if spectrum_grp is None: - return # No spectra in the file - self.detector_name = 'EDS' - for spectrum_sub_group_key in _get_keys_from_group(spectrum_grp): - self.dictionaries.append( - self._read_spectrum(spectrum_grp, spectrum_sub_group_key)) - - def _read_spectrum(self, spectrum_group, spectrum_sub_group_key): - spectrum_sub_group = spectrum_group[spectrum_sub_group_key] - dataset = spectrum_sub_group['Data'] - if self.lazy: - data = da.from_array(dataset, chunks=dataset.chunks).T - else: - data = dataset[:].T - original_metadata = _parse_metadata(spectrum_group, - spectrum_sub_group_key) - original_metadata.update(self.original_metadata) - - # Can be used in more recent version of velox emd files - self.detector_information = self._get_detector_information( - original_metadata) - - dispersion, offset, unit = self._get_dispersion_offset( - original_metadata) - axes = [] - if len(data.shape) == 2: - if data.shape[0] == 1: - # squeeze - data = data[0, :] - else: - axes = [{ - 'name': 'Stack', - 'offset': 0, - 'scale': 1, - 'size': data.shape[0], - 'navigate': True, - } - ] - axes.append({ - 'name': 'Energy', - 'offset': offset, - 'scale': dispersion, - 'size': data.shape[-1], - 'units': 'keV', - 'navigate': False}, - ) - - md = self._get_metadata_dict(original_metadata) - md['Signal']['signal_type'] = 'EDS_TEM' - - return {'data': data, - 'axes': axes, - 'metadata': md, - 'original_metadata': original_metadata, - 'mapping': self._get_mapping()} - - def _read_images(self): - # We need to read the image to get the shape of the spectrum image - if not self.load_images and not self.load_SI: - return - # Get the image data group - image_group = self.d_grp.get("Image") - if image_group is None: - return # No images in the file - # Get all the subgroup of the image data group and read the image for - # each of them - for image_sub_group_key in _get_keys_from_group(image_group): - image = self._read_image(image_group, image_sub_group_key) - if not self.load_images: - # If we don't want to load the images, we stop here - return - self.dictionaries.append(image) - - def _read_image(self, image_group, image_sub_group_key): - """ Return a dictionary ready to parse of return to io module""" - image_sub_group = image_group[image_sub_group_key] - original_metadata = _parse_metadata(image_group, image_sub_group_key) - original_metadata.update(self.original_metadata) - - # Can be used in more recent version of velox emd files - self.detector_information = self._get_detector_information( - original_metadata) - self.detector_name = self._get_detector_name(image_sub_group_key) - - read_stack = (self.load_SI_image_stack or self.im_type == 'Image') - h5data = image_sub_group['Data'] - # Get the scanning area shape of the SI from the images - self.spatial_shape = h5data.shape[:-1] - # For Velox FFT data, dtype must be specified and lazy is not - # supported due to special dtype. The data is loaded as-is; to get - # a traditional view the negative half must be created and the data - # must be re-centered - # Similar story for DPC signal - fft_dtype = [('realFloatHalfEven', ' self.number_of_frames: - raise ValueError( - "The `last_frame` cannot be greater than" - " the number of frames, %i for this file." - % self.number_of_frames - ) - - spectrum_stream_group = self.d_grp.get("SpectrumStream") - if spectrum_stream_group is None: - _logger.warning("No spectrum stream is present in the file. It " - "is possible that the file has been pruned: use " - "Velox to read the spectrum image (proprietary " - "format). If you want to open FEI emd file with " - "HyperSpy don't prune the file when saving it in " - "Velox.") - return - - def _read_stream(key): - stream = FeiSpectrumStream(spectrum_stream_group[key], self) - return stream - - subgroup_keys = _get_keys_from_group(spectrum_stream_group) - if self.sum_EDS_detectors: - if len(subgroup_keys) == 1: - _logger.warning("The file contains only one spectrum stream") - # Read the first stream - s0 = _read_stream(subgroup_keys[0]) - streams = [s0] - # add other stream streams - if len(subgroup_keys) > 1: - for key in subgroup_keys[1:]: - stream_data = spectrum_stream_group[key]['Data'][:].T[0] - if self.lazy: - s0.spectrum_image = ( - s0.spectrum_image + - s0.stream_to_sparse_array(stream_data=stream_data) - ) - else: - s0.stream_to_array(stream_data=stream_data, - spectrum_image=s0.spectrum_image) - else: - streams = [_read_stream(key) for key in subgroup_keys] - if self.lazy: - for stream in streams: - sa = stream.spectrum_image.astype(self.SI_data_dtype) - stream.spectrum_image = sa - - spectrum_image_shape = streams[0].shape - original_metadata = streams[0].original_metadata - original_metadata.update(self.original_metadata) - - # Can be used in more recent version of velox emd files - self.detector_information = self._get_detector_information( - original_metadata) - - pixel_size, offsets, original_units = \ - streams[0].get_pixelsize_offset_unit() - dispersion, offset, unit = self._get_dispersion_offset( - original_metadata) - - scale_x = self._convert_scale_units( - pixel_size['width'], original_units, spectrum_image_shape[1]) - scale_y = self._convert_scale_units( - pixel_size['height'], original_units, spectrum_image_shape[0]) - offset_x = self._convert_scale_units( - offsets['x'], original_units, spectrum_image_shape[1]) - offset_y = self._convert_scale_units( - offsets['y'], original_units, spectrum_image_shape[0]) - - i = 0 - axes = [] - # add a supplementary axes when we import all frames individualy - if not self.sum_frames: - frame_time, time_unit = self._parse_frame_time(original_metadata, - spectrum_image_shape[i]) - axes.append({'index_in_array': i, - 'name': 'Time', - 'offset': 0, - 'scale': frame_time, - 'size': spectrum_image_shape[i], - 'units': time_unit, - 'navigate': True}) - i = 1 - axes.extend([{'index_in_array': i, - 'name': 'y', - 'offset': offset_y[0], - 'scale': scale_y[0], - 'size': spectrum_image_shape[i], - 'units': scale_y[1], - 'navigate': True}, - {'index_in_array': i + 1, - 'name': 'x', - 'offset': offset_x[0], - 'scale': scale_x[0], - 'size': spectrum_image_shape[i + 1], - 'units': scale_x[1], - 'navigate': True}, - {'index_in_array': i + 2, - 'name': 'X-ray energy', - 'offset': offset, - 'scale': dispersion, - 'size': spectrum_image_shape[i + 2], - 'units': unit, - 'navigate': False}]) - - md = self._get_metadata_dict(original_metadata) - md['Signal']['signal_type'] = 'EDS_TEM' - - for stream in streams: - original_metadata = stream.original_metadata - original_metadata.update(self.original_metadata) - self.dictionaries.append({'data': stream.spectrum_image, - 'axes': axes, - 'metadata': md, - 'original_metadata': original_metadata, - 'mapping': self._get_mapping( - parse_individual_EDS_detector_metadata=not self.sum_frames)}) - - def _get_dispersion_offset(self, original_metadata): - try: - for detectorname, detector in original_metadata['Detectors'].items( - ): - if original_metadata['BinaryResult']['Detector'] in detector['DetectorName']: - dispersion = float( - detector['Dispersion']) / 1000.0 * self.rebin_energy - offset = float( - detector['OffsetEnergy']) / 1000.0 - return dispersion, offset, 'keV' - except KeyError: - _logger.warning("The spectrum calibration can't be loaded.") - return 1, 0, t.Undefined - - def _convert_scale_units(self, value, units, factor=1): - if units == t.Undefined: - return value, units - factor /= 2 - v = float(value) * self.ureg(units) - converted_v = (factor * v).to_compact() - converted_value = float(converted_v.magnitude / factor) - converted_units = '{:~}'.format(converted_v.units) - return converted_value, converted_units - - def _get_metadata_dict(self, om): - meta_gen = {} - meta_gen['original_filename'] = os.path.split(self.filename)[1] - if self.detector_name is not None: - meta_gen['title'] = self.detector_name - # We have only one entry in the original_metadata, so we can't use - # the mapping of the original_metadata to set the date and time in - # the metadata: need to set it manually here - try: - if 'AcquisitionStartDatetime' in om['Acquisition'].keys(): - unix_time = om['Acquisition']['AcquisitionStartDatetime']['DateTime'] - # Workaround when the 'AcquisitionStartDatetime' key is missing - # This timestamp corresponds to when the data is stored - elif (not isinstance(om['CustomProperties'], str) and - 'Detectors[BM-Ceta].TimeStamp' in om['CustomProperties'].keys()): - unix_time = float( - om['CustomProperties']['Detectors[BM-Ceta].TimeStamp']['value']) / 1E6 - date, time = self._convert_datetime(unix_time).split('T') - meta_gen['date'] = date - meta_gen['time'] = time - meta_gen['time_zone'] = self._get_local_time_zone() - except (UnboundLocalError): - pass - - meta_sig = {} - meta_sig['signal_type'] = '' - - return {'General': meta_gen, 'Signal': meta_sig} - - def _get_mapping(self, map_selected_element=True, - parse_individual_EDS_detector_metadata=True): - mapping = { - 'Acquisition.AcquisitionStartDatetime.DateTime': ( - "General.time_zone", lambda x: self._get_local_time_zone()), - 'Optics.AccelerationVoltage': ( - "Acquisition_instrument.TEM.beam_energy", lambda x: float(x) / 1e3), - 'Optics.CameraLength': ( - "Acquisition_instrument.TEM.camera_length", lambda x: float(x) * 1e3), - 'CustomProperties.StemMagnification.value': ( - "Acquisition_instrument.TEM.magnification", lambda x: float(x)), - 'Instrument.InstrumentClass': ( - "Acquisition_instrument.TEM.microscope", None), - 'Stage.AlphaTilt': ( - "Acquisition_instrument.TEM.Stage.tilt_alpha", - lambda x: round(np.degrees(float(x)), 3)), - 'Stage.BetaTilt': ( - "Acquisition_instrument.TEM.Stage.tilt_beta", - lambda x: round(np.degrees(float(x)), 3)), - 'Stage.Position.x': ( - "Acquisition_instrument.TEM.Stage.x", - lambda x: round(float(x), 6)), - 'Stage.Position.y': ( - "Acquisition_instrument.TEM.Stage.y", - lambda x: round(float(x), 6)), - 'Stage.Position.z': ( - "Acquisition_instrument.TEM.Stage.z", - lambda x: round(float(x), 6)), - 'ImportedDataParameter.Number_of_frames': ( - "Acquisition_instrument.TEM.Detector.EDS.number_of_frames", None), - 'DetectorMetadata.ElevationAngle': ( - "Acquisition_instrument.TEM.Detector.EDS.elevation_angle", - lambda x: round(float(x), 3)), - 'DetectorMetadata.Gain': ( - "Signal.Noise_properties.Variance_linear_model.gain_factor", - lambda x: float(x)), - 'DetectorMetadata.Offset': ( - "Signal.Noise_properties.Variance_linear_model.gain_offset", - lambda x: float(x)), - } - - # Parse individual metadata for each EDS detector - if parse_individual_EDS_detector_metadata: - mapping.update({ - 'DetectorMetadata.AzimuthAngle': ( - "Acquisition_instrument.TEM.Detector.EDS.azimuth_angle", - lambda x: '{:.3f}'.format(np.degrees(float(x)))), - 'DetectorMetadata.LiveTime': ( - "Acquisition_instrument.TEM.Detector.EDS.live_time", - lambda x: '{:.6f}'.format(float(x))), - 'DetectorMetadata.RealTime': ( - "Acquisition_instrument.TEM.Detector.EDS.real_time", - lambda x: '{:.6f}'.format(float(x))), - 'DetectorMetadata.DetectorName': ( - "General.title", None), - }) - - # Add selected element - if map_selected_element: - mapping.update({'Operations.ImageQuantificationOperation': ( - 'Sample.elements', - self._convert_element_list), - }) - - return mapping - - def _convert_element_list(self, d): - atomic_number_list = d[d.keys()[0]]['elementSelection'] - return [atomic_number2name[int(atomic_number)] - for atomic_number in atomic_number_list] - - def _convert_datetime(self, unix_time): - # Since we don't know the actual time zone of where the data have been - # acquired, we convert the datetime to the local time for convenience - dt = datetime.fromtimestamp(float(unix_time), tz=tz.tzutc()) - return dt.astimezone(tz.tzlocal()).isoformat().split('+')[0] - - def _get_local_time_zone(self): - return tz.tzlocal().tzname(datetime.today()) - - -# Below some information we have got from FEI about the format of the stream: -# -# The SI data is stored as a spectrum stream, ‘65535’ means next pixel -# (these markers are also called `Gate pulse`), other numbers mean a spectrum -# count in that bin for that pixel. -# For the size of the spectrum image and dispersion you have to look in -# AcquisitionSettings. -# The spectrum image cube itself stored in a compressed format, that is -# not easy to decode. - -class FeiSpectrumStream(object): - """Read spectrum image stored in FEI's stream format - - Once initialized, the instance of this class supports numpy style - indexing and slicing of the data stored in the stream format. - """ - - def __init__(self, stream_group, reader): - self.reader = reader - self.stream_group = stream_group - # Parse acquisition settings to get bin_count and dtype - acquisition_settings_group = stream_group['AcquisitionSettings'] - acquisition_settings = json.loads( - acquisition_settings_group[0].decode('utf-8')) - self.bin_count = int(acquisition_settings['bincount']) - if self.bin_count % self.reader.rebin_energy != 0: - raise ValueError('The `rebin_energy` needs to be a divisor of the', - ' total number of channels.') - if self.reader.SI_data_dtype is None: - self.reader.SI_data_dtype = acquisition_settings['StreamEncoding'] - # Parse the rest of the metadata for storage - self.original_metadata = _parse_sub_data_group_metadata(stream_group) - # If last_frame is None, compute it - stream_data = self.stream_group['Data'][:].T[0] - if self.reader.last_frame is None: - # The information could not be retrieved from metadata - # we compute, which involves iterating once over the whole stream. - # This is required to support the `last_frame` feature without - # duplicating the functions as currently numba does not support - # parametetrization. - spatial_shape = self.reader.spatial_shape - last_frame = int( - np.ceil((stream_data == 65535).sum() / - (spatial_shape[0] * spatial_shape[1]))) - self.reader.last_frame = last_frame - self.reader.number_of_frames = last_frame - self.original_metadata['ImportedDataParameter'] = { - 'First_frame': self.reader.first_frame, - 'Last_frame': self.reader.last_frame, - 'Number_of_frames': self.reader.number_of_frames, - 'Rebin_energy': self.reader.rebin_energy, - 'Number_of_channels': self.bin_count, } - # Convert stream to spectrum image - if self.reader.lazy: - self.spectrum_image = self.stream_to_sparse_array( - stream_data=stream_data - ) - else: - self.spectrum_image = self.stream_to_array( - stream_data=stream_data - ) - - @property - def shape(self): - return self.spectrum_image.shape - - def get_pixelsize_offset_unit(self): - om_br = self.original_metadata['BinaryResult'] - return om_br['PixelSize'], om_br['Offset'], om_br['PixelUnitX'] - - def stream_to_sparse_array(self, stream_data): - """Convert stream in sparse array - - Parameters - ---------- - stream_data: array - - """ - # Here we load the stream data into memory, which is fine is the - # arrays are small. We could load them lazily when lazy. - stream_data = self.stream_group['Data'][:].T[0] - sparse_array = stream_readers.stream_to_sparse_COO_array( - stream_data=stream_data, - spatial_shape=self.reader.spatial_shape, - first_frame=self.reader.first_frame, - last_frame=self.reader.last_frame, - channels=self.bin_count, - sum_frames=self.reader.sum_frames, - rebin_energy=self.reader.rebin_energy, - ) - return sparse_array - - def stream_to_array(self, stream_data, spectrum_image=None): - """Convert stream to array. - - Parameters - ---------- - stream_data: array - spectrum_image: array or None - If array, the data from the stream are added to the array. - Otherwise it creates a new array and returns it. - - """ - spectrum_image = stream_readers.stream_to_array( - stream=stream_data, - spatial_shape=self.reader.spatial_shape, - channels=self.bin_count, - first_frame=self.reader.first_frame, - last_frame=self.reader.last_frame, - rebin_energy=self.reader.rebin_energy, - sum_frames=self.reader.sum_frames, - spectrum_image=spectrum_image, - dtype=self.reader.SI_data_dtype, - ) - return spectrum_image - - -def read_emd_version(group): - """Function to read the emd file version from a group. The EMD version is - saved in the attributes 'version_major' and 'version_minor'. - - Parameters - ---------- - group : hdf5 group - The group to extract the version from. - - Returns - ------- - file version : str - Empty string if the file version is not defined in this group - - """ - major = group.attrs.get('version_major', None) - minor = group.attrs.get('version_minor', None) - if major is not None and minor is not None: - return f"{major}.{minor}" - else: - return "" - - -def is_EMD_NCEM(file): - """ - Parameters - ---------- - file : h5py file handle - DESCRIPTION. - - Returns - ------- - bool - DESCRIPTION. - - """ - def _is_EMD_NCEM(file): - for group in file: - if read_emd_version != '': - return True - return False - - if isinstance(file, str): - with h5py.File(file, 'r') as f: - return _is_EMD_NCEM(f) - else: - return _is_EMD_NCEM(file) - - -def is_EMD_Velox(file): - """Function to check if the EMD file is an Velox file. - - Parameters - ---------- - file : string or HDF5 file handle - The name of the emd-file from which to load the signals. Standard - file extension is 'emd'. - - Returns - ------- - True if the file is a Velox file, otherwise False - - """ - def _is_EMD_velox(file): - if 'Version' in list(file.keys()): - version = file.get('Version') - v_dict = json.loads(version[0].decode('utf-8')) - if v_dict['format'] in ['Velox', 'DevelopersKit']: - return True - return False - - if isinstance(file, str): - with h5py.File(file, 'r') as f: - return _is_EMD_velox(f) - else: - return _is_EMD_velox(file) - - -def file_reader(filename, lazy=False, **kwds): - """ - Read EMD file, which can be a NCEM or a Velox variant of the EMD format. - - Parameters - ---------- - filename : str - Filename of the file to write. - lazy : bool - Open the data lazily. Default is False. - **kwds : dict - Keyword argument pass to the EMD NCEM or EMD Velox reader. See user - guide or the docstring of the `load` function for more information. - """ - file = h5py.File(filename, 'r') - dictionaries = [] - try: - if is_EMD_Velox(file): - _logger.debug('EMD file is a Velox variant.') - emd_reader = FeiEMDReader(lazy=lazy, **kwds) - emd_reader.read_file(file) - elif is_EMD_NCEM(file): - _logger.debug('EMD file is a Berkeley variant.') - dataset_name = kwds.pop('dataset_name', None) - if dataset_name is not None: - msg = ( - "Using 'dataset_name' is deprecated and will be removed " - "in HyperSpy 2.0, use 'dataset_path' instead.") - warnings.warn(msg, VisibleDeprecationWarning) - dataset_path = f"{dataset_name}/data" - dataset_path = kwds.pop('dataset_path', None) - stack_group = kwds.pop('stack_group', None) - emd_reader = EMD_NCEM(**kwds) - emd_reader.read_file(file, lazy=lazy, dataset_path=dataset_path, - stack_group=stack_group) - else: - raise IOError("The file is not a supported EMD file.") - except Exception as e: - raise e - finally: - if not lazy: - file.close() - - dictionaries = emd_reader.dictionaries - - return dictionaries - - -def file_writer(filename, signal, **kwds): - """ - Write signal to EMD NCEM file. - - Parameters - ---------- - file : str of h5py file handle - If str, filename of the file to write, otherwise a h5py file handle - signal : instance of hyperspy signal - The signal to save. - **kwargs : dict - Dictionary containing metadata which will be written as attribute - of the root group. - """ - EMD_NCEM().write_file(filename, signal, **kwds) diff --git a/hyperspy/io_plugins/empad.py b/hyperspy/io_plugins/empad.py deleted file mode 100644 index 6a7e84e3b4..0000000000 --- a/hyperspy/io_plugins/empad.py +++ /dev/null @@ -1,188 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import os -import ast -import xml.etree.ElementTree as ET -import numpy as np -import logging -import pint -import traits.api as t - -from hyperspy.misc.io.tools import convert_xml_to_dict - - -_logger = logging.getLogger(__name__) -_ureg = pint.UnitRegistry() - - -# Plugin characteristics -# ---------------------- -format_name = 'empad' -description = '' -full_support = False -# Recognised file extension -file_extensions = ['xml', 'XML'] -default_extension = 0 - - # Writing capabilities: -writes = False -non_uniform_axis = False -# ---------------------- - - -def _read_raw(info, fp, lazy=False): - - raw_height = info['raw_height'] - width = info['width'] - height = info['height'] - - if lazy: - data = np.memmap(fp, dtype=' 1: - axes.append({ - 'size': sizes[i], - 'index_in_array': index_in_array, - 'name': names[i], - 'scale': scales[i], - 'offset': origins[i] * scales[i], - 'units': units[i], - }) - index_in_array += 1 - - data = _read_raw(info, - os.path.join(dname, info['raw_filename']), - lazy=lazy) - - dictionary = { - 'data': data.squeeze(), - 'axes': axes, - 'metadata': md, - 'original_metadata': om.as_dictionary() - } - - return [dictionary, ] diff --git a/hyperspy/io_plugins/fei.py b/hyperspy/io_plugins/fei.py deleted file mode 100644 index 2b8e108838..0000000000 --- a/hyperspy/io_plugins/fei.py +++ /dev/null @@ -1,779 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import struct -import warnings -from glob import glob -import os -from dateutil import parser -import logging -import xml.etree.ElementTree as ET -from collections import OrderedDict - -import numpy as np -import traits.api as t - -from hyperspy.misc.array_tools import sarray2dict -from hyperspy.misc.utils import DictionaryTreeBrowser, multiply - - -_logger = logging.getLogger(__name__) - - -ser_extensions = ('ser', 'SER') -emi_extensions = ('emi', 'EMI') -# Plugin characteristics -# ---------------------- -format_name = 'FEI TIA' -description = '' -full_support = False -# Recognised file extension -file_extensions = ser_extensions + emi_extensions -default_extension = 0 -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - -data_types = { - '1': ' and . It is standard xml :) - # xml chunks are identified using UUID, if we can find how these UUID are - # generated then, it will possible to match to the corresponding ser file - # and add the detector information in the metadata - objects = get_xml_info_from_emi(filename) - orig_fname = filename - filename = os.path.splitext(filename)[0] - if dump_xml is True: - for i, obj in enumerate(objects): - with open(filename + '-object-%s.xml' % i, 'w') as f: - f.write(obj) - - ser_files = sorted(glob(filename + '_[0-9].ser')) - sers = [] - for f in ser_files: - _logger.info("Opening %s", f) - try: - sers.append(ser_reader(f, objects, **kwds)) - except IOError: # Probably a single spectrum that we don't support - continue - - index = int(os.path.splitext(f)[0].split("_")[-1]) - 1 - op = DictionaryTreeBrowser(sers[-1]['original_metadata']) - - # defend against condition where more ser files are present than object - # metadata defined in emi - if index < len(objects): - emixml2dtb(ET.fromstring(objects[index]), op) - else: - _logger.warning(f'{orig_fname} did not contain any metadata for ' - f'{f}, so only .ser header information was read') - sers[-1]['original_metadata'] = op.as_dictionary() - return sers - - -def file_reader(filename, *args, **kwds): - ext = os.path.splitext(filename)[1][1:] - if ext in ser_extensions: - return [ser_reader(filename, *args, **kwds), ] - elif ext in emi_extensions: - return emi_reader(filename, *args, **kwds) - - -def load_ser_file(filename): - _logger.info("Opening the file: %s", filename) - with open(filename, 'rb') as f: - header = np.fromfile(f, - dtype=np.dtype(get_header_dtype_list(f)), - count=1) - _logger.info("Header info:") - log_struct_array_values(header[0]) - - if header['ValidNumberElements'] == 0: - raise IOError( - "The file does not contains valid data. " - "If it is a single spectrum, the data is contained in the " - ".emi file but HyperSpy cannot currently extract this " - "information.") - - # Read the first element of data offsets - f.seek(header["OffsetArrayOffset"][0]) - # OffsetArrayOffset can contain 4 or 8 bytes integer depending if the - # data have been acquired using a 32 or 64 bits platform. - if header['SeriesVersion'] <= 528: - data_offset = readLELong(f) - data_offset_array = np.fromfile(f, - dtype="', i_start) - i_end = tx.find(b'', i_start) - objects.append(tx[i_start:i_end + 13].decode('utf-8')) - return objects[:-1] - - -def get_calibration_from_position(position): - """Compute the size, scale and offset of a linear axis from coordinates. - - This function assumes rastering on a regular grid for the full size of - each dimension before rastering over another one. Fox example: a11, a12, - a13, a21, a22, a23 for a 2x3 grid. - - Parameters - ---------- - position: numpy array. - Position coordinates of the axis. Normally as in PositionX/Y of the - ser file. - - Returns - ------- - axis_attr: dictionary with `size`, `scale`, `offeset` keys. - - """ - offset = position[0] - for i, x in enumerate(position): - if x != position[0]: - break - if i == len(position) - 1: - # No scanning over this axis - scale = 0 - size = 0 - else: - scale = x - position[0] - if i == 1: # Rastering over this dimension first - for j, x in enumerate(position[1:]): - if x == position[0]: - break - size = j + 1 - else: # Second rastering dimension - size = len(position) / i - axis_attr = {"size": size, "scale": scale, "offset": offset} - return axis_attr - - -def get_axes_from_position(header, data): - array_shape = [] - axes = [] - array_size = int(header["ValidNumberElements"]) - if data[b"TagTypeID"][0] == XY_TAG_ID: - xcal = get_calibration_from_position(data[b"PositionX"]) - ycal = get_calibration_from_position(data[b"PositionY"]) - if xcal[b"size"] == 0 and ycal[b"size"] != 0: - # Vertical line scan - axes.append({ - 'name': "x", - 'units': "meters", - 'index_in_array': 0, - }) - axes[-1].update(xcal) - array_shape.append(axes[-1]["size"]) - - elif xcal[b"size"] != 0 and ycal[b"size"] == 0: - # Horizontal line scan - axes.append({ - 'name': "y", - 'units': "meters", - 'index_in_array': 0, - }) - axes[-1].update(ycal) - array_shape.append(axes[-1]["size"]) - - elif xcal[b"size"] * ycal[b"size"] == array_size: - # Signal2D - axes.append({ - 'name': "y", - 'units': "meters", - 'index_in_array': 0, - }) - axes[-1].update(ycal) - array_shape.append(axes[-1]["size"]) - axes.append({ - 'name': "x", - 'units': "meters", - 'index_in_array': 1, - }) - axes[-1].update(xcal) - array_shape.append(axes[-1]["size"]) - elif xcal[b"size"] == ycal[b"size"] == array_size: - # Oblique line scan - scale = np.sqrt(xcal["scale"] ** 2 + ycal["scale"] ** 2) - axes.append({ - 'name': "x", - 'units': "meters", - 'index_in_array': 0, - "offset": 0, - "scale": scale, - "size": xcal["size"] - }) - array_shape.append(axes[-1]["size"]) - else: - raise IOError - else: - array_shape = [header["ValidNumberElements"]] - axes.append({ - 'name': "Unknown dimension", - 'offset': 0, - 'scale': 1, - 'units': "", - 'size': header["ValidNumberElements"], - 'index_in_array': 0 - }) - return array_shape, axes - - -def convert_xml_to_dict(xml_object): - op = DictionaryTreeBrowser() - emixml2dtb(ET.fromstring(xml_object), op) - return op - - -def ser_reader(filename, objects=None, lazy=False, only_valid_data=False): - """Reads the information from the file and returns it in the HyperSpy - required format. - - """ - header, data = load_ser_file(filename) - record_by = guess_record_by(header['DataTypeID']) - ndim = int(header['NumberDimensions']) - date, time = None, None - if objects is not None: - objects_dict = convert_xml_to_dict(objects[0]) - try: - acq_date = objects_dict.ObjectInfo.AcquireDate - date, time = _get_date_time(acq_date) - except AttributeError: - _logger.warning(f'AcquireDate not found in metadata of {filename};' - ' Not setting metadata date or time') - if "PositionY" in data.dtype.names and len(data['PositionY']) > 1 and \ - (data['PositionY'][0] == data['PositionY'][1]): - # The spatial dimensions are stored in F order i.e. X, Y, ... - order = "F" - else: - # The spatial dimensions are stored in C order i.e. ..., Y, X - order = "C" - if ndim == 0 and header["ValidNumberElements"] != 0: - # The calibration of the axes are not stored in the header. - # We try to guess from the position coordinates. - array_shape, axes = get_axes_from_position(header=header, - data=data) - else: - axes = [] - array_shape = [None, ] * int(ndim) - spatial_axes = ["x", "y"][:ndim] - for i in range(ndim): - idim = 1 + i if order == "C" else ndim - i - if (record_by == "spectrum" or - header['Dim-%i_DimensionSize' % (i + 1)][0] != 1): - units = (header['Dim-%i_Units' % (idim)][0].decode('utf-8') - if header['Dim-%i_UnitsLength' % (idim)] > 0 - else t.Undefined) - if units == "meters": - name = (spatial_axes.pop() if order == "F" - else spatial_axes.pop(-1)) - else: - name = t.Undefined - axes.append({ - 'offset': header['Dim-%i_CalibrationOffset' % idim][0], - 'scale': header['Dim-%i_CalibrationDelta' % idim][0], - 'units': units, - 'size': header['Dim-%i_DimensionSize' % idim][0], - 'name': name, - }) - array_shape[i] = header['Dim-%i_DimensionSize' % idim][0] - - # Deal with issue when TotalNumberElements does not equal - # ValidNumberElements for ndim==1. - if ndim == 1 and (header['TotalNumberElements'] - != header['ValidNumberElements'][0]) and only_valid_data: - if header['ValidNumberElements'][0] == 1: - # no need for navigation dimension - array_shape = [] - axes = [] - else: - array_shape[0] = header['ValidNumberElements'][0] - axes[0]['size'] = header['ValidNumberElements'][0] - - # Spectral dimension - if record_by == "spectrum": - axes.append({ - 'offset': data['CalibrationOffset'][0], - 'scale': data['CalibrationDelta'][0], - 'size': data['ArrayLength'][0], - 'index_in_array': header['NumberDimensions'][0] - }) - - # FEI seems to use the international system of units (SI) for the - # energy scale (eV). - axes[-1]['units'] = 'eV' - axes[-1]['name'] = 'Energy' - - array_shape.append(data['ArrayLength'][0]) - - elif record_by == 'image': - if objects is not None: - units = _guess_units_from_mode(objects_dict, header) - else: - units = "meters" - # Y axis - axes.append({ - 'name': 'y', - 'offset': data['CalibrationOffsetY'][0] - - data['CalibrationElementY'][0] * data['CalibrationDeltaY'][0], - 'scale': data['CalibrationDeltaY'][0], - 'units': units, - 'size': data['ArraySizeY'][0], - }) - array_shape.append(data['ArraySizeY'][0]) - # X axis - axes.append({ - 'name': 'x', - 'offset': data['CalibrationOffsetX'][0] - - data['CalibrationElementX'][0] * data['CalibrationDeltaX'][0], - 'scale': data['CalibrationDeltaX'][0], - 'size': data['ArraySizeX'][0], - 'units': units, - }) - array_shape.append(data['ArraySizeX'][0]) - - # FEI seems to use the international system of units (SI) for the - # spatial scale. However, we prefer to work in nm - for axis in axes: - if axis['units'] == 'meters': - axis['units'] = 'nm' - axis['scale'] *= 10 ** 9 - elif axis['units'] == '1/meters': - axis['units'] = '1 / nm' - axis['scale'] /= 10 ** 9 - - # Remove Nones from array_shape caused by squeezing size 1 dimensions - array_shape = [dim for dim in array_shape if dim is not None] - if lazy: - from dask import delayed - from dask.array import from_delayed - val = delayed(load_only_data, pure=True)(filename, array_shape, - record_by, len(axes), - only_valid_data=only_valid_data) - dc = from_delayed(val, shape=array_shape, - dtype=data['Array'].dtype) - else: - dc = load_only_data(filename, array_shape, record_by, len(axes), - data=data, header=header, - only_valid_data=only_valid_data) - - original_metadata = OrderedDict() - header_parameters = sarray2dict(header) - sarray2dict(data, header_parameters) - # We remove the Array key to save memory avoiding duplication - del header_parameters['Array'] - original_metadata['ser_header_parameters'] = header_parameters - metadata = {'General': { - 'original_filename': os.path.split(filename)[1], - }, - "Signal": { - 'signal_type': "", - 'record_by': record_by, - }, - } - if date is not None and time is not None: - metadata['General']['date'] = date - metadata['General']['time'] = time - dictionary = { - 'data': dc, - 'metadata': metadata, - 'axes': axes, - 'original_metadata': original_metadata, - 'mapping': mapping} - return dictionary - - -def load_only_data(filename, array_shape, record_by, num_axes, data=None, - header=None, only_valid_data=False): - if data is None: - header, data = load_ser_file(filename) - # If the acquisition stops before finishing the job, the stored file will - # report the requested size even though no values are recorded. Therefore - # if the shapes of the retrieved array does not match that of the data - # dimensions we must fill the rest with zeros or (better) nans if the - # dtype is float - if multiply(array_shape) != multiply(data['Array'].shape): - if int(header['NumberDimensions']) == 1 and only_valid_data: - # No need to fill with zeros if `TotalNumberElements != - # ValidNumberElements` for series data. - # The valid data is always `0:ValidNumberElements` - dc = data['Array'][0:header['ValidNumberElements'][0], ...] - array_shape[0] = header['ValidNumberElements'][0] - else: - # Maps will need to be filled with zeros or nans - dc = np.zeros(multiply(array_shape), - dtype=data['Array'].dtype) - if dc.dtype is np.dtype('f') or dc.dtype is np.dtype('f8'): - dc[:] = np.nan - dc[:data['Array'].ravel().shape[0]] = data['Array'].ravel() - else: - dc = data['Array'] - - dc = dc.reshape(array_shape) - if record_by == 'image': - dc = dc[..., ::-1, :] - if num_axes != len(dc.shape): - dc = dc.squeeze() - if num_axes != len(dc.shape): - raise IOError("Please report this issue to the HyperSpy developers.") - return dc - - -def _guess_units_from_mode(objects_dict, header): - # in case the xml file doesn't contain the "Mode" or the header doesn't - # contain 'Dim-1_UnitsLength', return "meters" as default, which will be - # OK most of the time - warn_str = "The navigation axes units could not be determined. " \ - "Setting them to `nm`, but this may be wrong." - try: - mode = objects_dict.ObjectInfo.ExperimentalDescription.Mode - isCamera = ( - "CameraNamePath" in objects_dict.ObjectInfo.AcquireInfo.keys()) - except AttributeError: # in case the xml chunk doesn't contain the Mode - warnings.warn(warn_str) - return 'meters' # Most of the time, the unit will be meters! - if 'Dim-1_UnitsLength' in header.dtype.fields: - # assuming that for an image stack, the UnitsLength of the "3rd" - # dimension is 0 - isImageStack = (header['Dim-1_UnitsLength'][0] == 0) - # Workaround: if this is not an image stack and not a STEM image, then - # we assume that it should be a diffraction - isDiffractionScan = (header['Dim-1_DimensionSize'][0] > 1 and not - isImageStack) - else: - warnings.warn(warn_str) - return 'meters' # Most of the time, the unit will be meters! - - _logger.info(objects_dict.ObjectInfo.AcquireInfo) - _logger.info("mode: %s", mode) - _logger.info("isCamera: %s", isCamera) - _logger.info("isImageStack: %s", isImageStack) - _logger.info("isImageStack: %s", isDiffractionScan) - if 'STEM' in mode: - # data recorded in STEM with a camera, so we assume, it's a diffraction - # in case we can't make use the detector is a camera, use a workaround - if isCamera or isDiffractionScan: - return "1/meters" - else: - return "meters" - elif 'Diffraction' in mode: - return "1/meters" - else: - return 'meters' - - -def _get_simplified_mode(mode): - if "STEM" in mode: - return "STEM" - else: - return "TEM" - - -def _get_date_time(value): - dt = parser.parse(value) - return dt.date().isoformat(), dt.time().isoformat() - - -def _get_microscope_name(value): - return value.replace('Microscope ', '') - - -mapping = { - "ObjectInfo.ExperimentalDescription.High_tension_kV": ( - "Acquisition_instrument.TEM.beam_energy", - None), - "ObjectInfo.ExperimentalDescription.Microscope": ( - "Acquisition_instrument.TEM.microscope", - _get_microscope_name), - "ObjectInfo.ExperimentalDescription.Mode": ( - "Acquisition_instrument.TEM.acquisition_mode", - _get_simplified_mode), - "ObjectInfo.ExperimentalDescription.Camera length_m": ( - "Acquisition_instrument.TEM.camera_length", - lambda x: x * 1e3), - "ObjectInfo.ExperimentalDescription.Magnification_x": ( - "Acquisition_instrument.TEM.magnification", - None), - "ObjectInfo.AcquireInfo.CameraNamePath": ( - "Acquisition_instrument.TEM.Detector.Camera.Name", - None), - "ObjectInfo.AcquireInfo.DwellTimePath": ( - "Acquisition_instrument.TEM.Detector.Camera.exposure", - None), - "ObjectInfo.ExperimentalDescription.Stage_A_deg": ( - "Acquisition_instrument.TEM.Stage.tilt_alpha", - None), - "ObjectInfo.ExperimentalDescription.Stage_B_deg": ( - "Acquisition_instrument.TEM.Stage.tilt_beta", - None), - "ObjectInfo.ExperimentalDescription.Stage_X_um": ( - "Acquisition_instrument.TEM.Stage.x", - lambda x: x * 1e-3), - "ObjectInfo.ExperimentalDescription.Stage_Y_um": ( - "Acquisition_instrument.TEM.Stage.y", - lambda x: x * 1e-3), - "ObjectInfo.ExperimentalDescription.Stage_Z_um": ( - "Acquisition_instrument.TEM.Stage.z", - lambda x: x * 1e-3), - "ObjectInfo.ExperimentalDescription.User": ( - "General.authors", - None), -} diff --git a/hyperspy/io_plugins/hspy.py b/hyperspy/io_plugins/hspy.py deleted file mode 100644 index b799868e6a..0000000000 --- a/hyperspy/io_plugins/hspy.py +++ /dev/null @@ -1,806 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from distutils.version import LooseVersion -import warnings -import logging -import datetime -import ast - -import h5py -import numpy as np -import dask.array as da -from traits.api import Undefined -from hyperspy.misc.utils import ensure_unicode, multiply, get_object_package_info -from hyperspy.axes import AxesManager - - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = 'HSPY' -description = \ - 'The default file format for HyperSpy based on the HDF5 standard' -full_support = False -# Recognised file extension -file_extensions = ['hspy', 'hdf5'] -default_extension = 0 -# Writing capabilities -writes = True -non_uniform_axis = True -version = "3.1" -# ---------------------- - -# ----------------------- -# File format description -# ----------------------- -# The root must contain a group called Experiments. -# The experiments group can contain any number of subgroups. -# Each subgroup is an experiment or signal. -# Each subgroup must contain at least one dataset called data. -# The data is an array of arbitrary dimension. -# In addition, a number equal to the number of dimensions of the data -# dataset + 1 of empty groups called coordinates followed by a number -# must exist with the following attributes: -# 'name' -# 'offset' -# 'scale' -# 'units' -# 'size' -# 'index_in_array' -# Alternatively to 'offset' and 'scale', the coordinate groups may -# contain an 'axis' vector attribute defining the axis points. -# The experiment group contains a number of attributes that will be -# directly assigned as class attributes of the Signal instance. In -# addition the experiment groups may contain 'original_metadata' and -# 'metadata'-subgroup that will be assigned to the same name attributes -# of the Signal instance as a Dictionary Browser. -# The Experiments group can contain attributes that may be common to all -# the experiments and that will be accessible as attributes of the -# Experiments instance. -# -# CHANGES -# -# v3.1 -# - add read support for non-uniform DataAxis defined by 'axis' vector -# - move metadata.Signal.binned attribute to axes.is_binned parameter -# -# v3.0 -# - add Camera and Stage node -# - move tilt_stage to Stage.tilt_alpha -# -# v2.2 -# - store more metadata as string: date, time, notes, authors and doi -# - store quantity for intensity axis -# -# v2.1 -# - Store the navigate attribute -# - record_by is stored only for backward compatibility but the axes navigate -# attribute takes precendence over record_by for files with version >= 2.1 -# v1.3 -# ---- -# - Added support for lists, tuples and binary strings - -not_valid_format = 'The file is not a valid HyperSpy hdf5 file' - -current_file_version = None # Format version of the file being read -default_version = LooseVersion(version) - - -def get_hspy_format_version(f): - if "file_format_version" in f.attrs: - version = f.attrs["file_format_version"] - if isinstance(version, bytes): - version = version.decode() - if isinstance(version, float): - version = str(round(version, 2)) - elif "Experiments" in f: - # Chances are that this is a HSpy hdf5 file version 1.0 - version = "1.0" - elif "Analysis" in f: - # Starting version 2.0 we have "Analysis" field as well - version = "2.0" - else: - raise IOError(not_valid_format) - return LooseVersion(version) - - -def file_reader(filename, backing_store=False, - lazy=False, **kwds): - """Read data from hdf5 files saved with the hyperspy hdf5 format specification - - Parameters - ---------- - filename: str - lazy: bool - Load image lazily using dask - **kwds, optional - """ - try: - # in case blosc compression is used - import hdf5plugin - except ImportError: - pass - mode = kwds.pop('mode', 'r') - f = h5py.File(filename, mode=mode, **kwds) - # Getting the format version here also checks if it is a valid HSpy - # hdf5 file, so the following two lines must not be deleted or moved - # elsewhere. - global current_file_version - current_file_version = get_hspy_format_version(f) - global default_version - if current_file_version > default_version: - warnings.warn( - "This file was written using a newer version of the " - "HyperSpy hdf5 file format. I will attempt to load it, but, " - "if I fail, it is likely that I will be more successful at " - "this and other tasks if you upgrade me.") - - models_with_signals = [] - standalone_models = [] - if 'Analysis/models' in f: - try: - m_gr = f.require_group('Analysis/models') - for model_name in m_gr: - if '_signal' in m_gr[model_name].attrs: - key = m_gr[model_name].attrs['_signal'] - # del m_gr[model_name].attrs['_signal'] - res = hdfgroup2dict( - m_gr[model_name], - lazy=lazy) - del res['_signal'] - models_with_signals.append((key, {model_name: res})) - else: - standalone_models.append( - {model_name: hdfgroup2dict( - m_gr[model_name], lazy=lazy)}) - except TypeError: - raise IOError(not_valid_format) - - experiments = [] - exp_dict_list = [] - if 'Experiments' in f: - for ds in f['Experiments']: - if isinstance(f['Experiments'][ds], h5py.Group): - if 'data' in f['Experiments'][ds]: - experiments.append(ds) - # Parse the file - for experiment in experiments: - exg = f['Experiments'][experiment] - exp = hdfgroup2signaldict(exg, lazy) - # assign correct models, if found: - _tmp = {} - for (key, _dict) in reversed(models_with_signals): - if key == exg.name: - _tmp.update(_dict) - models_with_signals.remove((key, _dict)) - exp['models'] = _tmp - - exp_dict_list.append(exp) - - for _, m in models_with_signals: - standalone_models.append(m) - - exp_dict_list.extend(standalone_models) - if not len(exp_dict_list): - raise IOError('This is not a valid HyperSpy HDF5 file. ' - 'You can still load the data using a hdf5 reader, ' - 'e.g. h5py, and manually create a Signal. ' - 'Please, refer to the User Guide for details') - if not lazy: - f.close() - return exp_dict_list - - -def hdfgroup2signaldict(group, lazy=False): - global current_file_version - global default_version - if current_file_version < LooseVersion("1.2"): - metadata = "mapped_parameters" - original_metadata = "original_parameters" - else: - metadata = "metadata" - original_metadata = "original_metadata" - - exp = {'metadata': hdfgroup2dict( - group[metadata], lazy=lazy), - 'original_metadata': hdfgroup2dict( - group[original_metadata], lazy=lazy), - 'attributes': {} - } - if "package" in group.attrs: - # HyperSpy version is >= 1.5 - exp["package"] = group.attrs["package"] - exp["package_version"] = group.attrs["package_version"] - else: - # Prior to v1.4 we didn't store the package information. Since there - # were already external package we cannot assume any package provider so - # we leave this empty. - exp["package"] = "" - exp["package_version"] = "" - - data = group['data'] - if lazy: - data = da.from_array(data, chunks=data.chunks) - exp['attributes']['_lazy'] = True - else: - data = np.asanyarray(data) - exp['data'] = data - axes = [] - for i in range(len(exp['data'].shape)): - try: - axes.append(hdfgroup2dict(group['axis-%i' % i])) - axis = axes[-1] - for key, item in axis.items(): - if isinstance(item, np.bool_): - axis[key] = bool(item) - else: - axis[key] = ensure_unicode(item) - except KeyError: - break - if len(axes) != len(exp['data'].shape): # broke from the previous loop - try: - axes = [i for k, i in sorted(iter(hdfgroup2dict( - group['_list_' + str(len(exp['data'].shape)) + '_axes'], - lazy=lazy).items()))] - except KeyError: - raise IOError(not_valid_format) - exp['axes'] = axes - if 'learning_results' in group.keys(): - exp['attributes']['learning_results'] = \ - hdfgroup2dict( - group['learning_results'], - lazy=lazy) - if 'peak_learning_results' in group.keys(): - exp['attributes']['peak_learning_results'] = \ - hdfgroup2dict( - group['peak_learning_results'], - lazy=lazy) - - # If the title was not defined on writing the Experiment is - # then called __unnamed__. The next "if" simply sets the title - # back to the empty string - if "General" in exp["metadata"] and "title" in exp["metadata"]["General"]: - if '__unnamed__' == exp['metadata']['General']['title']: - exp['metadata']["General"]['title'] = '' - - if current_file_version < LooseVersion("1.1"): - # Load the decomposition results written with the old name, - # mva_results - if 'mva_results' in group.keys(): - exp['attributes']['learning_results'] = hdfgroup2dict( - group['mva_results'], lazy=lazy) - if 'peak_mva_results' in group.keys(): - exp['attributes']['peak_learning_results'] = hdfgroup2dict( - group['peak_mva_results'], lazy=lazy) - # Replace the old signal and name keys with their current names - if 'signal' in exp['metadata']: - if "Signal" not in exp["metadata"]: - exp["metadata"]["Signal"] = {} - exp['metadata']["Signal"]['signal_type'] = \ - exp['metadata']['signal'] - del exp['metadata']['signal'] - - if 'name' in exp['metadata']: - if "General" not in exp["metadata"]: - exp["metadata"]["General"] = {} - exp['metadata']['General']['title'] = \ - exp['metadata']['name'] - del exp['metadata']['name'] - - if current_file_version < LooseVersion("1.2"): - if '_internal_parameters' in exp['metadata']: - exp['metadata']['_HyperSpy'] = \ - exp['metadata']['_internal_parameters'] - del exp['metadata']['_internal_parameters'] - if 'stacking_history' in exp['metadata']['_HyperSpy']: - exp['metadata']['_HyperSpy']["Stacking_history"] = \ - exp['metadata']['_HyperSpy']['stacking_history'] - del exp['metadata']['_HyperSpy']["stacking_history"] - if 'folding' in exp['metadata']['_HyperSpy']: - exp['metadata']['_HyperSpy']["Folding"] = \ - exp['metadata']['_HyperSpy']['folding'] - del exp['metadata']['_HyperSpy']["folding"] - if 'Variance_estimation' in exp['metadata']: - if "Noise_properties" not in exp["metadata"]: - exp["metadata"]["Noise_properties"] = {} - exp['metadata']['Noise_properties']["Variance_linear_model"] = \ - exp['metadata']['Variance_estimation'] - del exp['metadata']['Variance_estimation'] - if "TEM" in exp["metadata"]: - if "Acquisition_instrument" not in exp["metadata"]: - exp["metadata"]["Acquisition_instrument"] = {} - exp["metadata"]["Acquisition_instrument"]["TEM"] = \ - exp["metadata"]["TEM"] - del exp["metadata"]["TEM"] - tem = exp["metadata"]["Acquisition_instrument"]["TEM"] - if "EELS" in tem: - if "dwell_time" in tem: - tem["EELS"]["dwell_time"] = tem["dwell_time"] - del tem["dwell_time"] - if "dwell_time_units" in tem: - tem["EELS"]["dwell_time_units"] = tem["dwell_time_units"] - del tem["dwell_time_units"] - if "exposure" in tem: - tem["EELS"]["exposure"] = tem["exposure"] - del tem["exposure"] - if "exposure_units" in tem: - tem["EELS"]["exposure_units"] = tem["exposure_units"] - del tem["exposure_units"] - if "Detector" not in tem: - tem["Detector"] = {} - tem["Detector"] = tem["EELS"] - del tem["EELS"] - if "EDS" in tem: - if "Detector" not in tem: - tem["Detector"] = {} - if "EDS" not in tem["Detector"]: - tem["Detector"]["EDS"] = {} - tem["Detector"]["EDS"] = tem["EDS"] - del tem["EDS"] - del tem - if "SEM" in exp["metadata"]: - if "Acquisition_instrument" not in exp["metadata"]: - exp["metadata"]["Acquisition_instrument"] = {} - exp["metadata"]["Acquisition_instrument"]["SEM"] = \ - exp["metadata"]["SEM"] - del exp["metadata"]["SEM"] - sem = exp["metadata"]["Acquisition_instrument"]["SEM"] - if "EDS" in sem: - if "Detector" not in sem: - sem["Detector"] = {} - if "EDS" not in sem["Detector"]: - sem["Detector"]["EDS"] = {} - sem["Detector"]["EDS"] = sem["EDS"] - del sem["EDS"] - del sem - - if "Sample" in exp["metadata"] and "Xray_lines" in exp[ - "metadata"]["Sample"]: - exp["metadata"]["Sample"]["xray_lines"] = exp[ - "metadata"]["Sample"]["Xray_lines"] - del exp["metadata"]["Sample"]["Xray_lines"] - - for key in ["title", "date", "time", "original_filename"]: - if key in exp["metadata"]: - if "General" not in exp["metadata"]: - exp["metadata"]["General"] = {} - exp["metadata"]["General"][key] = exp["metadata"][key] - del exp["metadata"][key] - for key in ["record_by", "signal_origin", "signal_type"]: - if key in exp["metadata"]: - if "Signal" not in exp["metadata"]: - exp["metadata"]["Signal"] = {} - exp["metadata"]["Signal"][key] = exp["metadata"][key] - del exp["metadata"][key] - - if current_file_version < LooseVersion("3.0"): - if "Acquisition_instrument" in exp["metadata"]: - # Move tilt_stage to Stage.tilt_alpha - # Move exposure time to Detector.Camera.exposure_time - if "TEM" in exp["metadata"]["Acquisition_instrument"]: - tem = exp["metadata"]["Acquisition_instrument"]["TEM"] - exposure = None - if "tilt_stage" in tem: - tem["Stage"] = {"tilt_alpha": tem["tilt_stage"]} - del tem["tilt_stage"] - if "exposure" in tem: - exposure = "exposure" - # Digital_micrograph plugin was parsing to 'exposure_time' - # instead of 'exposure': need this to be compatible with - # previous behaviour - if "exposure_time" in tem: - exposure = "exposure_time" - if exposure is not None: - if "Detector" not in tem: - tem["Detector"] = {"Camera": { - "exposure": tem[exposure]}} - tem["Detector"]["Camera"] = {"exposure": tem[exposure]} - del tem[exposure] - # Move tilt_stage to Stage.tilt_alpha - if "SEM" in exp["metadata"]["Acquisition_instrument"]: - sem = exp["metadata"]["Acquisition_instrument"]["SEM"] - if "tilt_stage" in sem: - sem["Stage"] = {"tilt_alpha": sem["tilt_stage"]} - del sem["tilt_stage"] - - return exp - - -def dict2hdfgroup(dictionary, group, **kwds): - "Recursive writer of dicts and signals" - - from hyperspy.misc.utils import DictionaryTreeBrowser - from hyperspy.signal import BaseSignal - - def parse_structure(key, group, value, _type, **kwds): - try: - # Here we check if there are any signals in the container, as - # casting a long list of signals to a numpy array takes a very long - # time. So we check if there are any, and save numpy the trouble - if np.any([isinstance(t, BaseSignal) for t in value]): - tmp = np.array([[0]]) - else: - tmp = np.array(value) - except ValueError: - tmp = np.array([[0]]) - if tmp.dtype == np.dtype('O') or tmp.ndim != 1: - dict2hdfgroup(dict(zip( - [str(i) for i in range(len(value))], value)), - group.create_group(_type + str(len(value)) + '_' + key), - **kwds) - elif tmp.dtype.type is np.unicode_: - if _type + key in group: - del group[_type + key] - group.create_dataset(_type + key, - tmp.shape, - dtype=h5py.special_dtype(vlen=str), - **kwds) - group[_type + key][:] = tmp[:] - else: - if _type + key in group: - del group[_type + key] - group.create_dataset( - _type + key, - data=tmp, - **kwds) - - for key, value in dictionary.items(): - if isinstance(value, dict): - dict2hdfgroup(value, group.create_group(key), - **kwds) - elif isinstance(value, DictionaryTreeBrowser): - dict2hdfgroup(value.as_dictionary(), - group.create_group(key), - **kwds) - elif isinstance(value, BaseSignal): - kn = key if key.startswith('_sig_') else '_sig_' + key - write_signal(value, group.require_group(kn)) - elif isinstance(value, (np.ndarray, h5py.Dataset, da.Array)): - overwrite_dataset(group, value, key, **kwds) - elif value is None: - group.attrs[key] = '_None_' - elif isinstance(value, bytes): - try: - # binary string if has any null characters (otherwise not - # supported by hdf5) - value.index(b'\x00') - group.attrs['_bs_' + key] = np.void(value) - except ValueError: - group.attrs[key] = value.decode() - elif isinstance(value, str): - group.attrs[key] = value - elif isinstance(value, AxesManager): - dict2hdfgroup(value.as_dictionary(), - group.create_group('_hspy_AxesManager_' + key), - **kwds) - elif isinstance(value, list): - if len(value): - parse_structure(key, group, value, '_list_', **kwds) - else: - group.attrs['_list_empty_' + key] = '_None_' - elif isinstance(value, tuple): - if len(value): - parse_structure(key, group, value, '_tuple_', **kwds) - else: - group.attrs['_tuple_empty_' + key] = '_None_' - - elif value is Undefined: - continue - else: - try: - group.attrs[key] = value - except BaseException: - _logger.exception( - "The hdf5 writer could not write the following " - "information in the file: %s : %s", key, value) - - -def get_signal_chunks(shape, dtype, signal_axes=None): - """Function that calculates chunks for the signal, preferably at least one - chunk per signal space. - - Parameters - ---------- - shape : tuple - the shape of the dataset to be sored / chunked - dtype : {dtype, string} - the numpy dtype of the data - signal_axes: {None, iterable of ints} - the axes defining "signal space" of the dataset. If None, the default - h5py chunking is performed. - """ - typesize = np.dtype(dtype).itemsize - if signal_axes is None: - return h5py._hl.filters.guess_chunk(shape, None, typesize) - - # largely based on the guess_chunk in h5py - CHUNK_MAX = 1024 * 1024 - want_to_keep = multiply([shape[i] for i in signal_axes]) * typesize - if want_to_keep >= CHUNK_MAX: - chunks = [1 for _ in shape] - for i in signal_axes: - chunks[i] = shape[i] - return tuple(chunks) - - chunks = [i for i in shape] - idx = 0 - navigation_axes = tuple(i for i in range(len(shape)) if i not in - signal_axes) - nchange = len(navigation_axes) - while True: - chunk_bytes = multiply(chunks) * typesize - - if chunk_bytes < CHUNK_MAX: - break - - if multiply([chunks[i] for i in navigation_axes]) == 1: - break - change = navigation_axes[idx % nchange] - chunks[change] = np.ceil(chunks[change] / 2.0) - idx += 1 - return tuple(int(x) for x in chunks) - - -def overwrite_dataset(group, data, key, signal_axes=None, chunks=None, **kwds): - if chunks is None: - if isinstance(data, da.Array): - # For lazy dataset, by default, we use the current dask chunking - chunks = tuple([c[0] for c in data.chunks]) - else: - # If signal_axes=None, use automatic h5py chunking, otherwise - # optimise the chunking to contain at least one signal per chunk - chunks = get_signal_chunks(data.shape, data.dtype, signal_axes) - - if np.issubdtype(data.dtype, np.dtype('U')): - # Saving numpy unicode type is not supported in h5py - data = data.astype(np.dtype('S')) - if data.dtype == np.dtype('O'): - # For saving ragged array - # http://docs.h5py.org/en/stable/special.html#arbitrary-vlen-data - group.require_dataset(key, - chunks, - dtype=h5py.special_dtype(vlen=data[0].dtype), - **kwds) - group[key][:] = data[:] - - maxshape = tuple(None for _ in data.shape) - - got_data = False - while not got_data: - try: - these_kwds = kwds.copy() - these_kwds.update(dict(shape=data.shape, - dtype=data.dtype, - exact=True, - maxshape=maxshape, - chunks=chunks, - shuffle=True,)) - - # If chunks is True, the `chunks` attribute of `dset` below - # contains the chunk shape guessed by h5py - dset = group.require_dataset(key, **these_kwds) - got_data = True - except TypeError: - # if the shape or dtype/etc do not match, - # we delete the old one and create new in the next loop run - del group[key] - if dset == data: - # just a reference to already created thing - pass - else: - _logger.info(f"Chunks used for saving: {dset.chunks}") - if isinstance(data, da.Array): - if data.chunks != dset.chunks: - data = data.rechunk(dset.chunks) - da.store(data, dset) - elif data.flags.c_contiguous: - dset.write_direct(data) - else: - dset[:] = data - - -def hdfgroup2dict(group, dictionary=None, lazy=False): - if dictionary is None: - dictionary = {} - for key, value in group.attrs.items(): - if isinstance(value, bytes): - value = value.decode() - if isinstance(value, (np.string_, str)): - if value == '_None_': - value = None - elif isinstance(value, np.bool_): - value = bool(value) - elif isinstance(value, np.ndarray) and value.dtype.char == "S": - # Convert strings to unicode - value = value.astype("U") - if value.dtype.str.endswith("U1"): - value = value.tolist() - # skip signals - these are handled below. - if key.startswith('_sig_'): - pass - elif key.startswith('_list_empty_'): - dictionary[key[len('_list_empty_'):]] = [] - elif key.startswith('_tuple_empty_'): - dictionary[key[len('_tuple_empty_'):]] = () - elif key.startswith('_bs_'): - dictionary[key[len('_bs_'):]] = value.tobytes() - # The following two elif stataments enable reading date and time from - # v < 2 of HyperSpy's metadata specifications - elif key.startswith('_datetime_date'): - date_iso = datetime.date( - *ast.literal_eval(value[value.index("("):])).isoformat() - dictionary[key.replace("_datetime_", "")] = date_iso - elif key.startswith('_datetime_time'): - date_iso = datetime.time( - *ast.literal_eval(value[value.index("("):])).isoformat() - dictionary[key.replace("_datetime_", "")] = date_iso - else: - dictionary[key] = value - if not isinstance(group, h5py.Dataset): - for key in group.keys(): - if key.startswith('_sig_'): - from hyperspy.io import dict2signal - dictionary[key[len('_sig_'):]] = ( - dict2signal(hdfgroup2signaldict( - group[key], lazy=lazy))) - elif isinstance(group[key], h5py.Dataset): - dat = group[key] - kn = key - if key.startswith("_list_"): - if (h5py.check_string_dtype(dat.dtype) and - hasattr(dat, 'asstr')): - # h5py 3.0 and newer - # https://docs.h5py.org/en/3.0.0/strings.html - dat = dat.asstr()[:] - ans = np.array(dat) - ans = ans.tolist() - kn = key[6:] - elif key.startswith("_tuple_"): - ans = np.array(dat) - ans = tuple(ans.tolist()) - kn = key[7:] - elif dat.dtype.char == "S": - ans = np.array(dat) - try: - ans = ans.astype("U") - except UnicodeDecodeError: - # There are some strings that must stay in binary, - # for example dill pickles. This will obviously also - # let "wrong" binary string fail somewhere else... - pass - elif lazy: - ans = da.from_array(dat, chunks=dat.chunks) - else: - ans = np.array(dat) - dictionary[kn] = ans - elif key.startswith('_hspy_AxesManager_'): - dictionary[key[len('_hspy_AxesManager_'):]] = AxesManager( - [i for k, i in sorted(iter( - hdfgroup2dict( - group[key], lazy=lazy).items() - ))]) - elif key.startswith('_list_'): - dictionary[key[7 + key[6:].find('_'):]] = \ - [i for k, i in sorted(iter( - hdfgroup2dict( - group[key], lazy=lazy).items() - ))] - elif key.startswith('_tuple_'): - dictionary[key[8 + key[7:].find('_'):]] = tuple( - [i for k, i in sorted(iter( - hdfgroup2dict( - group[key], lazy=lazy).items() - ))]) - else: - dictionary[key] = {} - hdfgroup2dict( - group[key], - dictionary[key], - lazy=lazy) - return dictionary - - -def write_signal(signal, group, **kwds): - "Writes a hyperspy signal to a hdf5 group" - - group.attrs.update(get_object_package_info(signal)) - if default_version < LooseVersion("1.2"): - metadata = "mapped_parameters" - original_metadata = "original_parameters" - else: - metadata = "metadata" - original_metadata = "original_metadata" - - if 'compression' not in kwds: - kwds['compression'] = 'gzip' - - for axis in signal.axes_manager._axes: - axis_dict = axis.get_axis_dictionary() - coord_group = group.create_group( - 'axis-%s' % axis.index_in_array) - dict2hdfgroup(axis_dict, coord_group, **kwds) - mapped_par = group.create_group(metadata) - metadata_dict = signal.metadata.as_dictionary() - overwrite_dataset(group, signal.data, 'data', - signal_axes=signal.axes_manager.signal_indices_in_array, - **kwds) - if default_version < LooseVersion("1.2"): - metadata_dict["_internal_parameters"] = \ - metadata_dict.pop("_HyperSpy") - # Remove chunks from the kwds since it wouldn't have the same rank as the - # dataset and can't be used - kwds.pop('chunks', None) - dict2hdfgroup(metadata_dict, mapped_par, **kwds) - original_par = group.create_group(original_metadata) - dict2hdfgroup(signal.original_metadata.as_dictionary(), original_par, - **kwds) - learning_results = group.create_group('learning_results') - dict2hdfgroup(signal.learning_results.__dict__, - learning_results, **kwds) - if hasattr(signal, 'peak_learning_results'): - peak_learning_results = group.create_group( - 'peak_learning_results') - dict2hdfgroup(signal.peak_learning_results.__dict__, - peak_learning_results, **kwds) - - if len(signal.models): - model_group = group.file.require_group('Analysis/models') - dict2hdfgroup(signal.models._models.as_dictionary(), - model_group, **kwds) - for model in model_group.values(): - model.attrs['_signal'] = group.name - - -def file_writer(filename, signal, *args, **kwds): - """Writes data to hyperspy's hdf5 format - - Parameters - ---------- - filename: str - signal: a BaseSignal instance - *args, optional - **kwds, optional - """ - with h5py.File(filename, mode='w') as f: - f.attrs['file_format'] = "HyperSpy" - f.attrs['file_format_version'] = version - exps = f.create_group('Experiments') - group_name = signal.metadata.General.title if \ - signal.metadata.General.title else '__unnamed__' - # / is a invalid character, see #942 - if "/" in group_name: - group_name = group_name.replace("/", "-") - expg = exps.create_group(group_name) - - # Add record_by metadata for backward compatibility - smd = signal.metadata.Signal - if signal.axes_manager.signal_dimension == 1: - smd.record_by = "spectrum" - elif signal.axes_manager.signal_dimension == 2: - smd.record_by = "image" - else: - smd.record_by = "" - try: - write_signal(signal, expg, **kwds) - except BaseException: - raise - finally: - del smd.record_by diff --git a/hyperspy/io_plugins/image.py b/hyperspy/io_plugins/image.py deleted file mode 100644 index d4297a2e74..0000000000 --- a/hyperspy/io_plugins/image.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import os -import logging - -from imageio import imread, imwrite -from matplotlib.figure import Figure -import traits.api as t -import pint - -from hyperspy.misc import rgb_tools - -# Plugin characteristics -# ---------------------- -format_name = 'Signal2D' -description = 'Import/Export standard image formats using pillow, freeimage or matplotlib (with scalebar)' -full_support = False -file_extensions = ['png', 'bmp', 'dib', 'gif', 'jpeg', 'jpe', 'jpg', - 'msp', 'pcx', 'ppm', "pbm", "pgm", 'xbm', 'spi', ] -default_extension = 0 # png -# Writing capabilities -writes = [(2, 0), ] -non_uniform_axis = False -# ---------------------- - -_ureg = pint.UnitRegistry() -_logger = logging.getLogger(__name__) - - -def file_writer(filename, signal, scalebar=False, - scalebar_kwds={'box_alpha':0.75, 'location':'lower left'}, - **kwds): - """Writes data to any format supported by PIL - - Parameters - ---------- - filename: {str, pathlib.Path, bytes, file} - The resource to write the image to, e.g. a filename, pathlib.Path or - file object, see the docs for more info. The file format is defined by - the file extension that is any one supported by imageio. - signal: a Signal instance - scalebar : bool, optional - Export the image with a scalebar. - scalebar_kwds : dict - Dictionary of keyword arguments for the scalebar. Useful to set - formattiong, location, etc. of the scalebar. See the documentation of - the 'matplotlib-scalebar' library for more information. - **kwds: keyword arguments - Allows to pass keyword arguments supported by the individual file - writers as documented at https://imageio.readthedocs.io/en/stable/formats.html - - """ - data = signal.data - if rgb_tools.is_rgbx(data): - data = rgb_tools.rgbx2regular_array(data) - if scalebar: - try: - from matplotlib_scalebar.scalebar import ScaleBar - export_scalebar = True - except ImportError: # pragma: no cover - export_scalebar = False - _logger.warning("Exporting image with scalebar requires the " - "matplotlib-scalebar library.") - dpi = 100 - fig = Figure(figsize=[v/dpi for v in signal.axes_manager.signal_shape], - dpi=dpi) - - try: - # List of format supported by matplotlib - supported_format = sorted(fig.canvas.get_supported_filetypes()) - if os.path.splitext(filename)[1].replace('.', '') not in supported_format: - export_scalebar = False - _logger.warning("Exporting image with scalebar is supported " - f"only with {', '.join(supported_format)}.") - except AttributeError: # pragma: no cover - export_scalebar = False - _logger.warning("Exporting image with scalebar requires the " - "matplotlib 3.1 or newer.") - - if scalebar and export_scalebar: - ax = fig.add_axes([0, 0, 1, 1]) - ax.axis('off') - ax.imshow(data, cmap='gray') - - # Add scalebar - axis = signal.axes_manager.signal_axes[0] - if axis.units == t.Undefined: - axis.units = "px" - scalebar_kwds['dimension'] = "pixel-length" - if _ureg.Quantity(axis.units).check('1/[length]'): - scalebar_kwds['dimension'] = "si-length-reciprocal" - scalebar = ScaleBar(axis.scale, axis.units, **scalebar_kwds) - ax.add_artist(scalebar) - fig.savefig(filename, dpi=dpi, pil_kwargs=kwds) - else: - imwrite(filename, data, **kwds) - - -def file_reader(filename, **kwds): - """Read data from any format supported by imageio (PIL/pillow). - For a list of formats see https://imageio.readthedocs.io/en/stable/formats.html - - Parameters - ---------- - filename: {str, pathlib.Path, bytes, file} - The resource to load the image from, e.g. a filename, pathlib.Path, - http address or file object, see the docs for more info. The file format - is defined by the file extension that is any one supported by imageio. - format: str, optional - The format to use to read the file. By default imageio selects the - appropriate for you based on the filename and its contents. - **kwds: keyword arguments - Allows to pass keyword arguments supported by the individual file - readers as documented at https://imageio.readthedocs.io/en/stable/formats.html - - """ - dc = _read_data(filename, **kwds) - lazy = kwds.pop('lazy', False) - if lazy: - # load the image fully to check the dtype and shape, should be cheap. - # Then store this info for later re-loading when required - from dask.array import from_delayed - from dask import delayed - val = delayed(_read_data, pure=True)(filename) - dc = from_delayed(val, shape=dc.shape, dtype=dc.dtype) - return [{'data': dc, - 'metadata': - { - 'General': {'original_filename': os.path.split(filename)[1]}, - "Signal": {'signal_type': "", - 'record_by': 'image', }, - } - }] - - -def _read_data(filename, **kwds): - dc = imread(filename) - if len(dc.shape) > 2: - # It may be a grayscale image that was saved in the RGB or RGBA - # format - if (dc[:, :, 1] == dc[:, :, 2]).all() and \ - (dc[:, :, 1] == dc[:, :, 2]).all(): - dc = dc[:, :, 0] - else: - dc = rgb_tools.regular_array2rgbx(dc) - return dc diff --git a/hyperspy/io_plugins/jeol.py b/hyperspy/io_plugins/jeol.py deleted file mode 100644 index 41e457b47d..0000000000 --- a/hyperspy/io_plugins/jeol.py +++ /dev/null @@ -1,729 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2016 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import os -from collections.abc import Iterable -from datetime import datetime, timedelta -import logging - -import numpy as np -import numba - - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = "JEOL" -description = "Read JEOL files output by Analysis Station software" -full_support = False # Whether all the Hyperspy features are supported -# Recognised file extension -file_extensions = ("ASW", "asw", "img", "map", "pts", "eds") -default_extension = 0 # Index of the extension that will be used by default -# Reading capabilities -reads_images = True -reads_spectrum = True -reads_spectrum_image = True -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -jTYPE = { - 1: "B", - 2: "H", - 3: "i", - 4: "f", - 5: "d", - 6: "B", - 7: "H", - 8: "i", - 9: "f", - 10: "d", - 11: "?", - 12: "c", - 13: "c", - 14: "H", - 20: "c", - 65553: "?", - 65552: "?", -} - - -def file_reader(filename, **kwds): - dictionary = [] - file_ext = os.path.splitext(filename)[-1][1:].lower() - if file_ext == "asw": - fd = open(filename, "br") - file_magic = np.fromfile(fd, " 1 and number % factor != 0: - raise ValueError(f'`{string}` must be a multiple of {number}.') - - check_multiple(rebin_energy, 4096, 'rebin_energy') - rebin_energy = int(rebin_energy) - - if file_magic == 304: - # fileformat - _ = fd.read(8).rstrip(b"\x00").decode("utf-8") - a, b, head_pos, head_len, data_pos, data_len = np.fromfile(fd, " 2: - raise ValueError("`downsample` can't be an iterable of length " - "different from 2.") - downsample_width = downsample[0] - downsample_height = downsample[1] - check_multiple(downsample_width, width, 'downsample[0]') - check_multiple(downsample_height, height, 'downsample[1]') - else: - downsample_width = downsample_height = int(downsample) - check_multiple(downsample_width, width, 'downsample') - check_multiple(downsample_height, height, 'downsample') - - check_multiple(downsample_width, width, 'downsample[0]') - check_multiple(downsample_height, height, 'downsample[1]') - - # Normalisation factor for the x and y position in the stream; depends - # on the downsampling and the size of the navigation space - width_norm = int(4096 / width * downsample_width) - height_norm = int(4096 / height * downsample_height) - - width = int(width / downsample_width) - height = int(height / downsample_height) - - channel_number = int(4096 / rebin_energy) - - fd.seek(data_pos) - # read spectrum image - rawdata = np.fromfile(fd, dtype="u2") - - if scale is not None: - xscale = -scale[2] / width - yscale = scale[3] / height - units = "µm" - else: - xscale = 1 - yscale = 1 - units = "px" - - ch_mod = meas_data_header["Meas Cond"]["Tpl"] - ch_res = meas_data_header["Doc"]["CoefA"] - ch_ini = meas_data_header["Doc"]["CoefB"] - ch_pos = header["PTTD Param"]["Params"]["PARAMPAGE1_EDXRF"]["Tpl"][ch_mod][ - "DigZ" - ] - - energy_offset = ch_ini - ch_res * ch_pos - energy_scale = ch_res * rebin_energy - - if cutoff_at_kV is not None: - channel_number = int( - np.round((cutoff_at_kV - energy_offset) / energy_scale) - ) - - axes = [ - { - "name": "y", - "size": height, - "offset": 0, - "scale": yscale, - "units": units, - }, - { - "name": "x", - "size": width, - "offset": 0, - "scale": xscale, - "units": units, - }, - { - "name": "Energy", - "size": channel_number, - "offset": energy_offset, - "scale": energy_scale, - "units": "keV", - }, - ] - # pixel time in milliseconds - pixel_time = meas_data_header["Doc"]["DwellTime(msec)"] - - data_shape = [height, width, channel_number] - if not sum_frames: - sweep = meas_data_header["Doc"]["Sweep"] - data_shape.insert(0, sweep) - axes.insert(0, { - "index_in_array": 0, - "name": "Frame", - "size": sweep, - "offset": 0, - "scale": pixel_time*height*width/1E3, - "units": 's', - }) - - data = np.zeros(data_shape, dtype=SI_dtype) - datacube_reader = readcube if sum_frames else readcube_frames - data = datacube_reader(rawdata, data, rebin_energy, channel_number, - width_norm, height_norm, np.iinfo(SI_dtype).max) - - hv = meas_data_header["MeasCond"]["AccKV"] - if hv <= 30.0: - mode = "SEM" - else: - mode = "TEM" - - detector_hearder = header["PTTD Param"]["Params"]["PARAMPAGE0_SEM"] - metadata = { - "Acquisition_instrument": { - mode: { - "beam_energy": hv, - "magnification": meas_data_header["MeasCond"]["Mag"], - "Detector": { - "EDS": { - "azimuth_angle": detector_hearder["DirAng"], - "detector_type": detector_hearder["DetT"], - "elevation_angle": detector_hearder["ElevAng"], - "energy_resolution_MnKa": detector_hearder["MnKaRES"], - "real_time": meas_data_header["Doc"]["RealTime"], - }, - }, - }, - }, - "General": { - "original_filename": os.path.basename(filename), - "date": datefile.date().isoformat(), - "time": datefile.time().isoformat(), - "title": "EDX", - }, - "Signal": { - "record_by": "spectrum", - "quantity": "X-rays (Counts)", - "signal_type": "EDS_" + mode, - }, - } - - dictionary = { - "data": data, - "axes": axes, - "metadata": metadata, - "original_metadata": header, - } - else: - _logger.warning("Not a valid JEOL pts format") - - fd.close() - - return dictionary - - -def parsejeol(fd): - final_dict = {} - tmp_list = [] - tmp_dict = final_dict - - mark = 1 - while abs(mark) == 1: - mark = np.fromfile(fd, "b", 1)[0] - if mark == 1: - str_len = np.fromfile(fd, "= 32768 and value < 36864: - x = int((value - 32768) / width_norm) - elif value >= 36864 and value < 40960: - y = int((value - 36864) / height_norm) - elif value >= 45056 and value < 49152: - z = int((value - 45056) / rebin_energy) - if z < channel_number: - hypermap[y, x, z] += 1 - if hypermap[y, x, z] == max_value: - raise ValueError("The range of the dtype is too small, " - "use `SI_dtype` to set a dtype with " - "higher range.") - return hypermap - - -@numba.njit(cache=True) -def readcube_frames(rawdata, hypermap, rebin_energy, channel_number, - width_norm, height_norm, max_value): # pragma: no cover - """ - We need to create a separate function, because numba.njit doesn't play well - with an array having its shape depending on something else - """ - frame_idx = 0 - previous_y = 0 - for value in rawdata: - if value >= 32768 and value < 36864: - x = int((value - 32768) / width_norm) - elif value >= 36864 and value < 40960: - y = int((value - 36864) / height_norm) - if y < previous_y: - frame_idx += 1 - previous_y = y - elif value >= 45056 and value < 49152: - z = int((value - 45056) / rebin_energy) - if z < channel_number: - hypermap[frame_idx, y, x, z] += 1 - if hypermap[frame_idx, y, x, z] == max_value: - raise ValueError("The range of the dtype is too small, " - "use `SI_dtype` to set a dtype with " - "higher range.") - return hypermap - - -def read_eds(filename, **kwargs): - header = {} - fd = open(filename, "br") - # file_magic - _ = np.fromfile(fd, ". - -# The details of the format were taken from -# http://www.biochem.mpg.de/doc_tom/TOM_Release_2008/IOfun/tom_mrcread.html -# and http://ami.scripps.edu/software/mrctools/mrc_specification.php - -import os -import logging - -import numpy as np -from traits.api import Undefined - -from hyperspy.misc.array_tools import sarray2dict - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = 'MRC' -description = '' -full_support = False -# Recognised file extension -file_extensions = ['mrc', 'MRC', 'ALI', 'ali'] -default_extension = 0 -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -def get_std_dtype_list(endianess='<'): - end = endianess - dtype_list = \ - [ - ('NX', end + 'u4'), - ('NY', end + 'u4'), - ('NZ', end + 'u4'), - ('MODE', end + 'u4'), - ('NXSTART', end + 'u4'), - ('NYSTART', end + 'u4'), - ('NZSTART', end + 'u4'), - ('MX', end + 'u4'), - ('MY', end + 'u4'), - ('MZ', end + 'u4'), - ('Xlen', end + 'f4'), - ('Ylen', end + 'f4'), - ('Zlen', end + 'f4'), - ('ALPHA', end + 'f4'), - ('BETA', end + 'f4'), - ('GAMMA', end + 'f4'), - ('MAPC', end + 'u4'), - ('MAPR', end + 'u4'), - ('MAPS', end + 'u4'), - ('AMIN', end + 'f4'), - ('AMAX', end + 'f4'), - ('AMEAN', end + 'f4'), - ('ISPG', end + 'u2'), - ('NSYMBT', end + 'u2'), - ('NEXT', end + 'u4'), - ('CREATID', end + 'u2'), - ('EXTRA', (np.void, 30)), - ('NINT', end + 'u2'), - ('NREAL', end + 'u2'), - ('EXTRA2', (np.void, 28)), - ('IDTYPE', end + 'u2'), - ('LENS', end + 'u2'), - ('ND1', end + 'u2'), - ('ND2', end + 'u2'), - ('VD1', end + 'u2'), - ('VD2', end + 'u2'), - ('TILTANGLES', (np.float32, 6)), - ('XORIGIN', end + 'f4'), - ('YORIGIN', end + 'f4'), - ('ZORIGIN', end + 'f4'), - ('CMAP', (bytes, 4)), - ('STAMP', (bytes, 4)), - ('RMS', end + 'f4'), - ('NLABL', end + 'u4'), - ('LABELS', (bytes, 800)), - ] - - return dtype_list - - -def get_fei_dtype_list(endianess='<'): - end = endianess - dtype_list = [ - ('a_tilt', end + 'f4'), # Alpha tilt (deg) - ('b_tilt', end + 'f4'), # Beta tilt (deg) - # Stage x position (Unit=m. But if value>1, unit=???m) - ('x_stage', end + 'f4'), - # Stage y position (Unit=m. But if value>1, unit=???m) - ('y_stage', end + 'f4'), - # Stage z position (Unit=m. But if value>1, unit=???m) - ('z_stage', end + 'f4'), - # Signal2D shift x (Unit=m. But if value>1, unit=???m) - ('x_shift', end + 'f4'), - # Signal2D shift y (Unit=m. But if value>1, unit=???m) - ('y_shift', end + 'f4'), - ('defocus', end + 'f4'), # Defocus Unit=m. But if value>1, unit=???m) - ('exp_time', end + 'f4'), # Exposure time (s) - ('mean_int', end + 'f4'), # Mean value of image - ('tilt_axis', end + 'f4'), # Tilt axis (deg) - ('pixel_size', end + 'f4'), # Pixel size of image (m) - ('magnification', end + 'f4'), # Magnification used - # Not used (filling up to 128 bytes) - ('empty', (np.void, 128 - 13 * 4)), - ] - return dtype_list - - -def get_data_type(index, endianess='<'): - end = endianess - data_type = [ - end + 'u2', # 0 = Signal2D unsigned bytes - end + 'i2', # 1 = Signal2D signed short integer (16 bits) - end + 'f4', # 2 = Signal2D float - (end + 'i2', 2), # 3 = Complex short*2 - end + 'c8', # 4 = Complex float*2 - ] - return data_type[index] - - -def file_reader(filename, endianess='<', **kwds): - metadata = {} - f = open(filename, 'rb') - std_header = np.fromfile(f, dtype=get_std_dtype_list(endianess), - count=1) - fei_header = None - if std_header['NEXT'] / 1024 == 128: - _logger.info("%s seems to contain an extended FEI header", filename) - fei_header = np.fromfile(f, dtype=get_fei_dtype_list(endianess), - count=1024) - if f.tell() == 1024 + std_header['NEXT']: - _logger.debug("The FEI header was correctly loaded") - else: - _logger.warning("There was a problem reading the extended header") - f.seek(1024 + std_header['NEXT']) - fei_header = None - NX, NY, NZ = std_header['NX'], std_header['NY'], std_header['NZ'] - mmap_mode = kwds.pop('mmap_mode', 'c') - lazy = kwds.pop('lazy', False) - if lazy: - mmap_mode = 'r' - data = np.memmap(f, mode=mmap_mode, offset=f.tell(), - dtype=get_data_type(std_header['MODE'][0], endianess) - ).squeeze().reshape((NX[0], NY[0], NZ[0]), order='F').T - - original_metadata = {'std_header': sarray2dict(std_header)} - # Convert bytes to unicode - for key in ["CMAP", "STAMP", "LABELS"]: - original_metadata["std_header"][key] = \ - original_metadata["std_header"][key].decode() - if fei_header is not None: - fei_dict = sarray2dict(fei_header,) - del fei_dict['empty'] - original_metadata['fei_header'] = fei_dict - - dim = len(data.shape) - if fei_header is None: - # The scale is in Amstrongs, we convert it to nm - scales = [10 * float(std_header['Zlen'] / std_header['MZ']) - if float(std_header['MZ']) != 0 else 1, - 10 * float(std_header['Ylen'] / std_header['MY']) - if float(std_header['MY']) != 0 else 1, - 10 * float(std_header['Xlen'] / std_header['MX']) - if float(std_header['MX']) != 0 else 1, ] - offsets = [10 * float(std_header['ZORIGIN']), - 10 * float(std_header['YORIGIN']), - 10 * float(std_header['XORIGIN']), ] - - else: - # FEI does not use the standard header to store the scale - # It does store the spatial scale in pixel_size, one per angle in - # meters - scales = [1, ] + [fei_header['pixel_size'][0] * 10 ** 9, ] * 2 - offsets = [0, ] * 3 - - units = [Undefined, 'nm', 'nm'] - names = ['z', 'y', 'x'] - metadata = {'General': {'original_filename': os.path.split(filename)[1]}, - "Signal": {'signal_type': "", - 'record_by': 'image', }, - } - # create the axis objects for each axis - axes = [ - { - 'size': data.shape[i], - 'index_in_array': i, - 'name': names[i + 3 - dim], - 'scale': scales[i + 3 - dim], - 'offset': offsets[i + 3 - dim], - 'units': units[i + 3 - dim], } - for i in range(dim)] - - dictionary = {'data': data, - 'axes': axes, - 'metadata': metadata, - 'original_metadata': original_metadata, - 'mapping': mapping} - - return [dictionary, ] - - -mapping = { - 'fei_header.a_tilt': - ("Acquisition_instrument.TEM.Stage.tilt_alpha", None), - 'fei_header.b_tilt': - ("Acquisition_instrument.TEM.Stage.tilt_beta", None), - 'fei_header.x_stage': - ("Acquisition_instrument.TEM.Stage.x", None), - 'fei_header.y_stage': - ("Acquisition_instrument.TEM.Stage.y", None), - 'fei_header.z_stage': - ("Acquisition_instrument.TEM.Stage.z", None), - 'fei_header.exp_time': - ("Acquisition_instrument.TEM.Detector.Camera.exposure", None), - 'fei_header.magnification': - ("Acquisition_instrument.TEM.magnification", None), -} diff --git a/hyperspy/io_plugins/mrcz.py b/hyperspy/io_plugins/mrcz.py deleted file mode 100644 index 7d9ee3b26d..0000000000 --- a/hyperspy/io_plugins/mrcz.py +++ /dev/null @@ -1,146 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from distutils.version import LooseVersion -import mrcz as _mrcz -import logging - - -_logger = logging.getLogger(__name__) -# Plugin characteristics -# ---------------------- -format_name = 'MRCZ' -description = 'Compressed MRC file format extension with blosc meta-compression' -full_support = False -# Recognised file extension -file_extensions = ['mrc', 'MRC', 'mrcz', 'MRCZ'] -default_extension = 2 -# Writing capabilities: -writes = True -non_uniform_axis = False -# ---------------------- - - -_POP_FROM_HEADER = ['compressor', 'MRCtype', 'C3', 'dimensions', 'dtype', - 'extendedBytes', 'gain', 'maxImage', 'minImage', 'meanImage', - 'metaId', 'packedBytes', 'pixelsize', 'pixelunits', 'voltage'] -# Hyperspy uses an unusual mixed Fortran- and C-ordering scheme -_READ_ORDER = [1, 2, 0] -_WRITE_ORDER = [0, 2, 1] - - -# API changes in mrcz 0.5 -def _parse_metadata(metadata): - if LooseVersion(_mrcz.__version__) < LooseVersion("0.5"): - return metadata[0] - else: - return metadata - - -mapping = { - 'mrcz_header.voltage': - ("Acquisition_instrument.TEM.beam_energy", - _parse_metadata), - 'mrcz_header.gain': - ("Signal.Noise_properties.Variance_linear_model.gain_factor", - _parse_metadata), - # There is no metadata field for spherical aberration - #'mrcz_header.C3': - #("Acquisition_instrument.TEM.C3", lambda x: x), -} - - -def file_reader(filename, endianess='<', lazy=False, mmap_mode='c', - **kwds): - _logger.debug("Reading MRCZ file: %s" % filename) - - if mmap_mode != 'c': - # Note also that MRCZ does not support memory-mapping of compressed data. - # Perhaps we could use the zarr package for that - raise ValueError('MRCZ supports only C-ordering memory-maps') - - mrcz_endian = 'le' if endianess == '<' else 'be' - data, mrcz_header = _mrcz.readMRC(filename, endian=mrcz_endian, - useMemmap=lazy, - pixelunits='nm', - **kwds) - - # Create the axis objects for each axis - names = ['y', 'x', 'z'] - navigate = [False, False, True] - axes = [{'size': data.shape[hsIndex], - 'index_in_array': hsIndex, - 'name': names[index], - 'scale': mrcz_header['pixelsize'][hsIndex], - 'offset': 0.0, - 'units': mrcz_header['pixelunits'], - 'navigate': nav} - for index, (hsIndex, nav) in enumerate(zip(_READ_ORDER, navigate))] - axes.insert(0, axes.pop(2)) # re-order the axes - - metadata = mrcz_header.copy() - # Remove non-standard fields - for popTarget in _POP_FROM_HEADER: - metadata.pop(popTarget) - - dictionary = {'data': data, - 'axes': axes, - 'metadata': metadata, - 'original_metadata': {'mrcz_header': mrcz_header}, - 'mapping': mapping, } - - return [dictionary, ] - - -def file_writer(filename, signal, do_async=False, compressor=None, clevel=1, - n_threads=None, **kwds): - import hyperspy.signals - if not isinstance(signal, - (hyperspy.signals.Signal2D, hyperspy.signals.ComplexSignal2D)): - raise TypeError("MRCZ supports 2D and 3D data only. type(signal) is " - "{}".format(type(signal))) - - endianess = kwds.pop('endianess', '<') - mrcz_endian = 'le' if endianess == '<' else 'be' - - meta = signal.metadata.as_dictionary() - - # Get pixelsize and pixelunits from the axes - pixelunits = signal.axes_manager[-1].units - - pixelsize = [signal.axes_manager[I].scale for I in _WRITE_ORDER] - - # Strip out voltage from meta-data - voltage = signal.metadata.get_item( - 'Acquisition_instrument.TEM.beam_energy') - # There aren't hyperspy fields for spherical aberration or detector gain - C3 = 0.0 - gain = signal.metadata.get_item("Signal.Noise_properties." - "Variance_linear_model.gain_factor", 1.0) - if do_async: - _mrcz.asyncWriteMRC(signal.data, filename, meta=meta, endian=mrcz_endian, - pixelsize=pixelsize, pixelunits=pixelunits, - voltage=voltage, C3=C3, gain=gain, - compressor=compressor, clevel=clevel, - n_threads=n_threads) - else: - _mrcz.writeMRC(signal.data, filename, meta=meta, endian=mrcz_endian, - pixelsize=pixelsize, pixelunits=pixelunits, - voltage=voltage, C3=C3, gain=gain, - compressor=compressor, clevel=clevel, - n_threads=n_threads) diff --git a/hyperspy/io_plugins/msa.py b/hyperspy/io_plugins/msa.py deleted file mode 100644 index bd8cbc4464..0000000000 --- a/hyperspy/io_plugins/msa.py +++ /dev/null @@ -1,435 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from datetime import datetime as dt -import codecs -import os -import logging - -import numpy as np -from traits.api import Undefined - -from hyperspy import Release -from hyperspy.misc.utils import DictionaryTreeBrowser - -_logger = logging.getLogger(__name__) - -# Plugin characteristics -# ---------------------- -format_name = 'MSA' -description = '' -full_support = False -file_extensions = ('msa', 'ems', 'mas', 'emsa', 'EMS', 'MAS', 'EMSA', 'MSA') -default_extension = 0 -# Writing capabilities -writes = [(1, 0), ] -non_uniform_axis = False -# ---------------------- - -# For a description of the EMSA/MSA format, incluiding the meaning of the -# following keywords: -# http://www.amc.anl.gov/ANLSoftwareLibrary/02-MMSLib/XEDS/EMMFF/EMMFF.IBM/Emmff.Total - -US_MONTHS_D2A = { - "01" : "JAN", - "02" : "FEB", - "03": "MAR", - "04": "APR", - "05": "MAY", - "06": "JUN", - "07": "JUL", - "08": "AUG", - "09": "SEP", - "10": "OCT", - "11": "NOV", - "12": "DEC", } - -US_MONTH_A2D = dict([reversed(i) for i in US_MONTHS_D2A.items()]) - -keywords = { - # Required parameters - 'FORMAT': {'dtype': str, 'mapped_to': None}, - 'VERSION': {'dtype': str, 'mapped_to': None}, - 'TITLE': {'dtype': str, 'mapped_to': 'General.title'}, - 'DATE': {'dtype': str, 'mapped_to': None}, - 'TIME': {'dtype': str, 'mapped_to': None}, - 'OWNER': {'dtype': str, 'mapped_to': None}, - 'NPOINTS': {'dtype': float, 'mapped_to': None}, - 'NCOLUMNS': {'dtype': float, 'mapped_to': None}, - 'DATATYPE': {'dtype': str, 'mapped_to': None}, - 'XPERCHAN': {'dtype': float, 'mapped_to': None}, - 'OFFSET': {'dtype': float, 'mapped_to': None}, - # Optional parameters - # Signal1D characteristics - 'SIGNALTYPE': {'dtype': str, 'mapped_to': - 'Signal.signal_type'}, - 'XLABEL': {'dtype': str, 'mapped_to': None}, - 'YLABEL': {'dtype': str, 'mapped_to': None}, - 'XUNITS': {'dtype': str, 'mapped_to': None}, - 'YUNITS': {'dtype': str, 'mapped_to': None}, - 'CHOFFSET': {'dtype': float, 'mapped_to': None}, - 'COMMENT': {'dtype': str, 'mapped_to': None}, - # Microscope - 'BEAMKV': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.beam_energy'}, - 'EMISSION': {'dtype': float, 'mapped_to': None}, - 'PROBECUR': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.beam_current'}, - 'BEAMDIAM': {'dtype': float, 'mapped_to': None}, - 'MAGCAM': {'dtype': float, 'mapped_to': None}, - 'OPERMODE': {'dtype': str, 'mapped_to': None}, - 'CONVANGLE': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.convergence_angle'}, - - # Specimen - 'THICKNESS': {'dtype': float, 'mapped_to': - 'Sample.thickness'}, - 'XTILTSTGE': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Stage.tilt_alpha'}, - 'YTILTSTGE': {'dtype': float, 'mapped_to': None}, - 'XPOSITION': {'dtype': float, 'mapped_to': None}, - 'YPOSITION': {'dtype': float, 'mapped_to': None}, - 'ZPOSITION': {'dtype': float, 'mapped_to': None}, - - # EELS - # in ms: - 'INTEGTIME': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EELS.exposure'}, - # in ms: - 'DWELLTIME': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EELS.dwell_time'}, - 'COLLANGLE': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EELS.collection_angle'}, - 'ELSDET': {'dtype': str, 'mapped_to': None}, - - # EDS - 'ELEVANGLE': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EDS.elevation_angle'}, - 'AZIMANGLE': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EDS.azimuth_angle'}, - 'SOLIDANGLE': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EDS.solid_angle'}, - 'LIVETIME': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EDS.live_time'}, - 'REALTIME': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EDS.real_time'}, - 'FWHMMNKA': {'dtype': float, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EDS.' + - 'energy_resolution_MnKa'}, - 'TBEWIND': {'dtype': float, 'mapped_to': None}, - 'TAUWIND': {'dtype': float, 'mapped_to': None}, - 'TDEADLYR': {'dtype': float, 'mapped_to': None}, - 'TACTLYR': {'dtype': float, 'mapped_to': None}, - 'TALWIND': {'dtype': float, 'mapped_to': None}, - 'TPYWIND': {'dtype': float, 'mapped_to': None}, - 'TBNWIND': {'dtype': float, 'mapped_to': None}, - 'TDIWIND': {'dtype': float, 'mapped_to': None}, - 'THCWIND': {'dtype': float, 'mapped_to': None}, - 'EDSDET': {'dtype': str, 'mapped_to': - 'Acquisition_instrument.TEM.Detector.EDS.EDS_det'}, -} - - -def parse_msa_string(string, filename=None): - """Parse an EMSA/MSA file content. - - Parameters - ---------- - string: string or file object - It must complain with the EMSA/MSA standard. - filename: string or None - The filename. - - Returns: - -------- - file_data_list: list - The list containts a dictionary that contains the parsed - information. It can be used to create a `:class:BaseSignal` - using `:func:hyperspy.io.dict2signal`. - - """ - if not hasattr(string, "readlines"): - string = string.splitlines() - parameters = {} - mapped = DictionaryTreeBrowser({}) - y = [] - # Read the keywords - data_section = False - for line in string: - if data_section is False: - if line[0] == "#": - try: - key, value = line.split(': ') - value = value.strip() - except ValueError: - key = line - value = None - key = key.strip('#').strip() - - if key != 'SPECTRUM': - parameters[key] = value - else: - data_section = True - else: - # Read the data - if line[0] != "#" and line.strip(): - if parameters['DATATYPE'] == 'XY': - xy = line.replace(',', ' ').strip().split() - y.append(float(xy[1])) - elif parameters['DATATYPE'] == 'Y': - data = [ - float(i) for i in line.replace(',', ' ').strip().split()] - y.extend(data) - # We rewrite the format value to be sure that it complies with the - # standard, because it will be used by the writer routine - parameters['FORMAT'] = "EMSA/MAS Spectral Data File" - - # Convert the parameters to the right type and map some - # TODO: the msa format seems to support specifying the units of some - # parametes. We should add this feature here - for parameter, value in parameters.items(): - # Some parameters names can contain the units information - # e.g. #AZIMANGLE-dg: 90. - if '-' in parameter: - clean_par, units = parameter.split('-') - clean_par, units = clean_par.strip(), units.strip() - else: - clean_par, units = parameter, None - if clean_par in keywords: - try: - parameters[parameter] = keywords[clean_par]['dtype'](value) - except BaseException: - # Normally the offending mispelling is a space in the scientic - # notation, e.g. 2.0 E-06, so we try to correct for it - try: - parameters[parameter] = keywords[clean_par]['dtype']( - value.replace(' ', '')) - except BaseException: - _logger.exception( - "The %s keyword value, %s could not be converted to " - "the right type", parameter, value) - - if keywords[clean_par]['mapped_to'] is not None: - mapped.set_item(keywords[clean_par]['mapped_to'], - parameters[parameter]) - if units is not None: - mapped.set_item(keywords[clean_par]['mapped_to'] + - '_units', units) - if 'TIME' in parameters and parameters['TIME']: - try: - time = dt.strptime(parameters['TIME'], "%H:%M") - mapped.set_item('General.time', time.time().isoformat()) - except ValueError as e: - _logger.warning('Possible malformed TIME field in msa file. The time information could not be retrieved.: %s' % e) - else: - _logger.warning('TIME information missing.') - - malformed_date_error = 'Possibly malformed DATE in msa file. The date information could not be retrieved.' - if "DATE" in parameters and parameters["DATE"]: - try: - day, month, year = parameters["DATE"].split("-") - if month.upper() in US_MONTH_A2D: - month = US_MONTH_A2D[month.upper()] - date = dt.strptime("-".join((day, month, year)), "%d-%m-%Y") - mapped.set_item('General.date', date.date().isoformat()) - else: - _logger.warning(malformed_date_error) - except ValueError as e: # Error raised if split does not return 3 elements in this case - _logger.warning(malformed_date_error + ": %s" % e) - - - axes = [{ - 'size': len(y), - 'index_in_array': 0, - 'name': parameters['XLABEL'] if 'XLABEL' in parameters else '', - 'scale': parameters['XPERCHAN'] if 'XPERCHAN' in parameters else 1, - 'offset': parameters['OFFSET'] if 'OFFSET' in parameters else 0, - 'units': parameters['XUNITS'] if 'XUNITS' in parameters else '', - }] - if filename is not None: - mapped.set_item('General.original_filename', - os.path.split(filename)[1]) - mapped.set_item('Signal.record_by', 'spectrum') - if mapped.has_item('Signal.signal_type'): - if mapped.Signal.signal_type == 'ELS': - mapped.Signal.signal_type = 'EELS' - if mapped.Signal.signal_type in ['EDX', 'XEDS']: - mapped.Signal.signal_type = 'EDS' - else: - # Defaulting to EELS looks reasonable - mapped.set_item('Signal.signal_type', 'EELS') - if 'YUNITS' in parameters.keys(): - yunits = "(%s)" % parameters['YUNITS'] - else: - yunits = "" - if 'YLABEL' in parameters.keys(): - quantity = "%s" % parameters['YLABEL'] - else: - if mapped.Signal.signal_type == 'EELS': - quantity = 'Electrons' - if not yunits: - yunits = "(Counts)" - elif 'EDS' in mapped.Signal.signal_type: - quantity = 'X-rays' - if not yunits: - yunits = "(Counts)" - else: - quantity = "" - if quantity or yunits: - quantity_units = "%s %s" % (quantity, yunits) - mapped.set_item('Signal.quantity', quantity_units.strip()) - - dictionary = { - 'data': np.array(y), - 'axes': axes, - 'metadata': mapped.as_dictionary(), - 'original_metadata': parameters - } - file_data_list = [dictionary, ] - return file_data_list - - -def file_reader(filename, encoding='latin-1', **kwds): - with codecs.open( - filename, - encoding=encoding, - errors='replace') as spectrum_file: - return parse_msa_string(string=spectrum_file, - filename=filename) - - -def file_writer(filename, signal, format=None, separator=', ', - encoding='latin-1'): - loc_kwds = {} - FORMAT = "EMSA/MAS Spectral Data File" - md = signal.metadata - if hasattr(signal.original_metadata, 'FORMAT') and \ - signal.original_metadata.FORMAT == FORMAT: - loc_kwds = signal.original_metadata.as_dictionary() - if format is not None: - loc_kwds['DATATYPE'] = format - else: - if 'DATATYPE' in loc_kwds: - format = loc_kwds['DATATYPE'] - else: - if format is None: - format = 'Y' - if md.has_item("General.date"): - date = dt.strptime(md.General.date, "%Y-%m-%d") - date_str = date.strftime("%d-%m-%Y") - day, month, year = date_str.split("-") - month = US_MONTHS_D2A[month] - loc_kwds['DATE'] = "-".join((day, month, year)) - if md.has_item("General.time"): - time = dt.strptime(md.General.time, "%H:%M:%S") - loc_kwds['TIME'] = time.strftime("%H:%M") - keys_from_signal = { - # Required parameters - 'FORMAT': FORMAT, - 'VERSION': '1.0', - # 'TITLE' : signal.title[:64] if hasattr(signal, "title") else '', - 'DATE': '', - 'TIME': '', - 'OWNER': '', - 'NPOINTS': signal.axes_manager._axes[0].size, - 'NCOLUMNS': 1, - 'DATATYPE': format, - 'SIGNALTYPE': signal.metadata.Signal.signal_type, - 'XPERCHAN': signal.axes_manager._axes[0].scale, - 'OFFSET': signal.axes_manager._axes[0].offset, - # Signal1D characteristics - - 'XLABEL': signal.axes_manager._axes[0].name - if signal.axes_manager._axes[0].name is not Undefined - else "", - - # 'YLABEL' : '', - 'XUNITS': signal.axes_manager._axes[0].units - if signal.axes_manager._axes[0].units is not Undefined - else "", - # 'YUNITS' : '', - 'COMMENT': 'File created by HyperSpy version %s' % Release.version, - # Microscope - # 'BEAMKV' : , - # 'EMISSION' : , - # 'PROBECUR' : , - # 'BEAMDIAM' : , - # 'MAGCAM' : , - # 'OPERMODE' : , - # 'CONVANGLE' : , - # Specimen - # 'THICKNESS' : , - # 'XTILTSTGE' : , - # 'YTILTSTGE' : , - # 'XPOSITION' : , - # 'YPOSITION' : , - # 'ZPOSITION' : , - # - # EELS - # 'INTEGTIME' : , # in ms - # 'DWELLTIME' : , # in ms - # 'COLLANGLE' : , - # 'ELSDET' : , - } - - # Update the loc_kwds with the information retrieved from the signal class - for key, value in keys_from_signal.items(): - if key not in loc_kwds or value != '': - loc_kwds[key] = value - - for key, dic in keywords.items(): - - if dic['mapped_to'] is not None: - if 'SEM' in signal.metadata.Signal.signal_type: - dic['mapped_to'] = dic['mapped_to'].replace('TEM', 'SEM') - if signal.metadata.has_item(dic['mapped_to']): - loc_kwds[key] = eval('signal.metadata.%s' % - dic['mapped_to']) - - with codecs.open( - filename, - 'w', - encoding=encoding, - errors='ignore') as f: - # Remove the following keys from loc_kwds if they are in - # (although they shouldn't) - for key in ['SPECTRUM', 'ENDOFDATA']: - if key in loc_kwds: - del(loc_kwds[key]) - - f.write('#%-12s: %s\u000D\u000A' % ('FORMAT', loc_kwds.pop('FORMAT'))) - f.write( - '#%-12s: %s\u000D\u000A' % - ('VERSION', loc_kwds.pop('VERSION'))) - for keyword, value in loc_kwds.items(): - f.write('#%-12s: %s\u000D\u000A' % (keyword, value)) - - f.write('#%-12s: Spectral Data Starts Here\u000D\u000A' % 'SPECTRUM') - - if format == 'XY': - for x, y in zip(signal.axes_manager._axes[0].axis, signal.data): - f.write("%g%s%g" % (x, separator, y)) - f.write('\u000D\u000A') - elif format == 'Y': - for y in signal.data: - f.write('%f%s' % (y, separator)) - f.write('\u000D\u000A') - else: - raise ValueError('format must be one of: None, \'XY\' or \'Y\'') - - f.write('#%-12s: End Of Data and File' % 'ENDOFDATA') diff --git a/hyperspy/io_plugins/netcdf.py b/hyperspy/io_plugins/netcdf.py deleted file mode 100644 index 6c8907351e..0000000000 --- a/hyperspy/io_plugins/netcdf.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import os -import logging - -import numpy as np - -_logger = logging.getLogger(__name__) - -no_netcdf = False -try: - from netCDF4 import Dataset - which_netcdf = 'netCDF4' -except BaseException: - try: - from netCDF3 import Dataset - which_netcdf = 'netCDF3' - except BaseException: - try: - from Scientific.IO.NetCDF import NetCDFFile as Dataset - which_netcdf = 'Scientific Python' - except BaseException: - no_netcdf = True - -# Plugin characteristics -# ---------------------- -format_name = 'netCDF' -description = '' -full_support = True -file_extensions = ('nc', 'NC') -default_extension = 0 -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -attrib2netcdf = \ - { - 'energyorigin': 'energy_origin', - 'energyscale': 'energy_scale', - 'energyunits': 'energy_units', - 'xorigin': 'x_origin', - 'xscale': 'x_scale', - 'xunits': 'x_units', - 'yorigin': 'y_origin', - 'yscale': 'y_scale', - 'yunits': 'y_units', - 'zorigin': 'z_origin', - 'zscale': 'z_scale', - 'zunits': 'z_units', - 'exposure': 'exposure', - 'title': 'title', - 'binning': 'binning', - 'readout_frequency': 'readout_frequency', - 'ccd_height': 'ccd_height', - 'blanking': 'blanking' - } - -acquisition2netcdf = \ - { - 'exposure': 'exposure', - 'binning': 'binning', - 'readout_frequency': 'readout_frequency', - 'ccd_height': 'ccd_height', - 'blanking': 'blanking', - 'gain': 'gain', - 'pppc': 'pppc', - } - -treatments2netcdf = \ - { - 'dark_current': 'dark_current', - 'readout': 'readout', - } - - -def file_reader(filename, *args, **kwds): - if no_netcdf is True: - raise ImportError("No netCDF library installed. " - "To read EELSLab netcdf files install " - "one of the following packages:" - "netCDF4, netCDF3, netcdf, scientific") - - ncfile = Dataset(filename, 'r') - - if hasattr(ncfile, 'file_format_version'): - if ncfile.file_format_version == 'EELSLab 0.1': - dictionary = nc_hyperspy_reader_0dot1( - ncfile, - filename, - *args, - **kwds) - else: - ncfile.close() - raise IOError('Unsupported netCDF file') - - return dictionary, - - -def nc_hyperspy_reader_0dot1(ncfile, filename, *args, **kwds): - calibration_dict, acquisition_dict, treatments_dict = {}, {}, {} - dc = ncfile.variables['data_cube'] - data = dc[:] - if 'history' in calibration_dict: - calibration_dict['history'] = eval(ncfile.history) - for attrib in attrib2netcdf.items(): - if hasattr(dc, attrib[1]): - value = eval('dc.' + attrib[1]) - if isinstance(value, np.ndarray): - calibration_dict[attrib[0]] = value[0] - else: - calibration_dict[attrib[0]] = value - else: - _logger.warning("Warning: the attribute '%s' is not defined in " - "the file '%s'", attrib[0], filename) - for attrib in acquisition2netcdf.items(): - if hasattr(dc, attrib[1]): - value = eval('dc.' + attrib[1]) - if isinstance(value, np.ndarray): - acquisition_dict[attrib[0]] = value[0] - else: - acquisition_dict[attrib[0]] = value - else: - _logger.warning("Warning: the attribute '%s' is not defined in " - "the file '%s'", attrib[0], filename) - for attrib in treatments2netcdf.items(): - if hasattr(dc, attrib[1]): - treatments_dict[attrib[0]] = eval('dc.' + attrib[1]) - else: - _logger.warning("Warning: the attribute '%s' is not defined in " - "the file '%s'", attrib[0], filename) - original_metadata = {'record_by': ncfile.type, - 'calibration': calibration_dict, - 'acquisition': acquisition_dict, - 'treatments': treatments_dict} - ncfile.close() - # Now we'll map some parameters - record_by = 'image' if original_metadata[ - 'record_by'] == 'image' else 'spectrum' - if record_by == 'image': - dim = len(data.shape) - names = ['Z', 'Y', 'X'][3 - dim:] - scaleskeys = ['zscale', 'yscale', 'xscale'] - originskeys = ['zorigin', 'yorigin', 'xorigin'] - unitskeys = ['zunits', 'yunits', 'xunits'] - - elif record_by == 'spectrum': - dim = len(data.shape) - names = ['Y', 'X', 'Energy'][3 - dim:] - scaleskeys = ['yscale', 'xscale', 'energyscale'] - originskeys = ['yorigin', 'xorigin', 'energyorigin'] - unitskeys = ['yunits', 'xunits', 'energyunits'] - - # The images are recorded in the Fortran order - data = data.T.copy() - try: - scales = [calibration_dict[key] for key in scaleskeys[3 - dim:]] - except KeyError: - scales = [1, 1, 1][3 - dim:] - try: - origins = [calibration_dict[key] for key in originskeys[3 - dim:]] - except KeyError: - origins = [0, 0, 0][3 - dim:] - try: - units = [calibration_dict[key] for key in unitskeys[3 - dim:]] - except KeyError: - units = ['', '', ''] - axes = [ - { - 'size': int(data.shape[i]), - 'index_in_array': i, - 'name': names[i], - 'scale': scales[i], - 'offset': origins[i], - 'units': units[i], } - for i in range(dim)] - metadata = {'General': {}, 'Signal': {}} - metadata['General']['original_filename'] = os.path.split(filename)[1] - metadata["Signal"]['record_by'] = record_by - metadata["General"]['signal_type'] = "" - dictionary = { - 'data': data, - 'axes': axes, - 'metadata': metadata, - 'original_metadata': original_metadata, - } - - return dictionary diff --git a/hyperspy/io_plugins/nexus.py b/hyperspy/io_plugins/nexus.py deleted file mode 100644 index c27514a084..0000000000 --- a/hyperspy/io_plugins/nexus.py +++ /dev/null @@ -1,1324 +0,0 @@ -"""Nexus file reading, writing and inspection.""" -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . -# -import logging -import warnings -import numpy as np -import dask.array as da -import os -import h5py -import pprint -import traits.api as t -from hyperspy.io_plugins.hspy import overwrite_dataset, get_signal_chunks -from hyperspy.misc.utils import DictionaryTreeBrowser -from hyperspy.exceptions import VisibleDeprecationWarning -_logger = logging.getLogger(__name__) - -# Plugin characteristics -# ---------------------- -format_name = 'Nexus' -description = \ - 'Read NXdata sets from Nexus files and metadata. Data and metadata can '\ - 'also be examined from general hdf5 files' -full_support = False -# Recognised file extension -file_extensions = ['nxs', 'NXS'] -default_extension = 0 -# Writing capabilities: -writes = True -non_uniform_axis = False -# ---------------------- - - -def _byte_to_string(value): - """Decode a byte string. - - Parameters - ---------- - value : byte str - - Returns - ------- - str - decoded version of input value - - """ - try: - text = value.decode("utf-8") - except UnicodeDecodeError: - text = value.decode('latin-1') - return text.replace('\x00', '').rstrip() - - -def _parse_from_file(value, lazy=False): - """To convert values from the hdf file to compatible formats. - - When reading string arrays we convert or keep string arrays as - byte_strings (some io_plugins only supports byte-strings arrays so this - ensures inter-compatibility across io_plugins) - Arrays of length 1 - return the single value stored. - Large datasets are returned as dask arrays if lazy=True. - - Parameters - ---------- - value : input read from hdf file (array,list,tuple,string,int,float) - lazy : bool {default: False} - The lazy flag is only applied to values of size >=2 - - Returns - ------- - str,int, float, ndarray dask Array - parsed value. - - """ - toreturn = value - if isinstance(value, h5py.Dataset): - if value.size < 2: - toreturn = value[...].item() - else: - if lazy: - if value.chunks: - toreturn = da.from_array(value, value.chunks) - else: - chunks = get_signal_chunks(value.shape, value.dtype) - toreturn = da.from_array(value, chunks) - else: - toreturn = np.array(value) - - if isinstance(toreturn, np.ndarray) and value.shape == (1,): - toreturn = toreturn[0] - if isinstance(toreturn, bytes): - toreturn = _byte_to_string(toreturn) - if isinstance(toreturn, (int, float)): - toreturn = toreturn - if isinstance(toreturn, (np.ndarray)) and toreturn.dtype.char == "U": - toreturn = toreturn.astype("S") - return toreturn - - -def _parse_to_file(value): - """Convert to a suitable format for writing to HDF5. - - For example unicode values are not compatible with hdf5 so conversion to - byte strings is required. - - Parameters - ---------- - value - input object to write to the hdf file - - Returns - ------- - parsed value - - """ - totest = value - toreturn = totest - if isinstance(totest, (bytes, int, float)): - toreturn = value - if isinstance(totest, (list, tuple)): - totest = np.array(value) - if isinstance(totest, np.ndarray) and totest.dtype.char == "U": - toreturn = np.array(totest).astype("S") - elif isinstance(totest, (np.ndarray, da.Array)): - toreturn = totest - if isinstance(totest, str): - toreturn = totest.encode("utf-8") - toreturn = np.string_(toreturn) - return toreturn - - -def _text_split(s, sep): - """Split a string based of list of seperators. - - Parameters - ---------- - s : str - sep : str - seperator or list of seperators e.g. '.' or ['_','/'] - - Returns - ------- - list - String sections split based on the seperators - - """ - stack = [s] - for char in sep: - pieces = [] - for substr in stack: - pieces.extend(substr.split(char)) - stack = pieces - if '' in stack: - stack.remove('') - return stack - - -def _getlink(h5group, rootkey, key): - """Return the link target path. - - If a hdf group is a soft link or has a target attribute - this method will return the target path. If no link is found - return None. - - Returns - ------- - str - Soft link path if it exists, otherwise None - - """ - _target = None - if rootkey != '/': - if isinstance(h5group, h5py.Group): - _link = h5group.get(key, getlink=True) - if isinstance(_link, h5py.SoftLink): - _target = _link.path - if 'target' in h5group.attrs.keys(): - _target = _parse_from_file(h5group.attrs['target']) - if not _target.startswith('/'): - _target = '/' + _target - if _target == rootkey: - _target = None - - return _target - - -def _get_nav_list(data, dataentry): - """Get the list with information of each axes of the dataset - - Parameters - ---------- - data : hdf dataset - the dataset to be loaded. - dataentry : hdf group - the group with corresponding attributes. - - Returns - ------- - nav_list : list - contains information about each axes. - """ - - detector_index = 0 - nav_list = [] - # list indices... - axis_index_list = [] - if "axes" in dataentry.attrs.keys(): - axes_key = dataentry.attrs["axes"] - axes_list = ["."]*data.ndim - if isinstance(axes_key, np.ndarray): - axes_keys = axes_key[:data.ndim] - for i, num in enumerate(axes_keys): - axes_list[i] = _parse_from_file(num) - elif isinstance(axes_key, (str, bytes)): - axes_list = _parse_from_file(axes_key).split(',')[:data.ndim] - else: - axes_list[0] = _parse_from_file(axes_key) - - named_axes = list(range(len(axes_list))) - for i, ax in enumerate(axes_list): - if ax != ".": - index_name = ax + "_indices" - if index_name in dataentry.attrs: - ind_in_array = int(dataentry.attrs[index_name]) - else: - ind_in_array = i - axis_index_list.append(ind_in_array) - if "units" in dataentry[ax].attrs: - units = _parse_from_file(dataentry[ax].attrs["units"]) - else: - units = "" - - navigation = True - named_axes.remove(ind_in_array) - - if _is_numeric_data(dataentry[ax]): - if dataentry[ax].size > 1: - if _is_linear_axis(dataentry[ax]): - nav_list.append({ - 'size': data.shape[ind_in_array], - 'index_in_array': ind_in_array, - 'name': ax, - 'scale': abs(dataentry[ax][1] - - dataentry[ax][0]), - 'offset': min(dataentry[ax][0], - dataentry[ax][-1]), - 'units': units, - 'navigate': navigation - }) - else: - nav_list.append({ - 'size': data.shape[ind_in_array], - 'index_in_array': ind_in_array, - 'name': ax, - 'scale': 1, - 'offset': 0, - 'navigate': navigation - }) - else: - nav_list.append({ - 'size': 1, - 'index_in_array': ind_in_array, - 'name': ax, - 'scale': 1, - 'offset': dataentry[ax][0], - 'units': units, - 'navigate': True - }) - else: - if len(data.shape) == len(axes_list): - nav_list.append({ - 'size': data.shape[named_axes[detector_index]], - 'index_in_array': named_axes[detector_index], - 'scale': 1, - 'offset': 0.0, - 'units': '', - 'navigate': False - }) - detector_index = detector_index+1 - - return nav_list - - -def _extract_hdf_dataset(group, dataset, lazy=False): - """Import data from hdf path. - - Parameters - ---------- - group : hdf group - group from which to load the dataset - dataset : str - path to the dataset within the group - lazy : bool {default:True} - If true use lazy opening, if false read into memory - - Returns - ------- - dict - A signal dictionary which can be used to instantiate a signal. - - """ - - data = group[dataset] - - # exclude the dataset tagged by the signal attribute to avoid extracting - # duplicated dataset, which is already loaded when loading NeXus data - if 'signal' in data.parent.attrs.keys(): - data_key = data.parent.attrs['signal'] - if isinstance(data_key, bytes): - data_key = data_key.decode() - if dataset.split('/')[-1] == data_key: - return - - nav_list = _get_nav_list(data, data.parent) - - if lazy: - if "chunks" in data.attrs.keys(): - chunks = data.attrs["chunks"] - else: - signal_axes = [d['index_in_array'] for d in nav_list - if not d['navigate']] - chunks = get_signal_chunks(data.shape, data.dtype, signal_axes) - data_lazy = da.from_array(data, chunks=chunks) - else: - data_lazy = np.array(data) - - dictionary = {'data': data_lazy, 'metadata': {}, 'original_metadata': {}, - 'axes': nav_list} - - return dictionary - - -def _nexus_dataset_to_signal(group, nexus_dataset_path, lazy=False): - """Load an NXdata set as a hyperspy signal. - - Parameters - ---------- - group : hdf group containing the NXdata - nexus_data_path : str - Path to the NXdata set in the group - lazy : bool, default : True - lazy loading of data - - Returns - ------- - dict - A signal dictionary which can be used to instantiate a signal. - - """ - - interpretation = None - dataentry = group[nexus_dataset_path] - if "signal" in dataentry.attrs.keys(): - if _is_int(dataentry.attrs["signal"]): - data_key = "data" - else: - data_key = dataentry.attrs["signal"] - else: - _logger.info("No signal attr associated with NXdata will\ - try assume signal name is data") - if "data" not in dataentry.keys(): - raise ValueError("Signal attribute not found in NXdata and " - "attempt to find a default \"data\" key failed") - else: - data_key = "data" - - if "interpretation" in dataentry.attrs.keys(): - interpretation = _parse_from_file(dataentry.attrs["interpretation"]) - - data = dataentry[data_key] - nav_list = _get_nav_list(data, dataentry) - - if lazy: - if "chunks" in data.attrs.keys(): - chunks = data.attrs["chunks"] - else: - signal_axes = [d['index_in_array'] for d in nav_list - if not d['navigate']] - chunks = get_signal_chunks(data.shape, data.dtype, signal_axes) - data_lazy = da.from_array(data, chunks=chunks) - else: - data_lazy = np.array(data) - - if not nav_list: - for i in range(data.ndim): - nav_list.append({ - 'size': data_lazy.shape[i], - 'index_in_array': i, - 'scale': 1, - 'offset': 0.0, - 'units': '', - 'navigate': True - }) - title = _text_split(nexus_dataset_path, '/')[-1] - metadata = {'General': {'title': title}} - - # - # if interpretation - reset the nav axes - # assume the last dimensions are the signal - # - if interpretation: - for x in nav_list: - x["navigate"] = True - if interpretation == "spectrum": - nav_list[-1]["navigate"] = False - elif interpretation == "image": - nav_list[-1]["navigate"] = False - nav_list[-2]["navigate"] = False - - dictionary = {'data': data_lazy, - 'axes': nav_list, - 'metadata': metadata} - return dictionary - - -def file_reader(filename, lazy=False, dataset_key=None, dataset_path=None, - metadata_key=None, - skip_array_metadata=False, - nxdata_only=False, - hardlinks_only=False, - use_default=False, - **kwds): - """Read NXdata class or hdf datasets from a file and return signal(s). - - Note - ---- - Loading all datasets can result in a large number of signals - Please review your datasets and use the dataset_key to target - the datasets of interest. - "keys" is a special keywords and prepended with "fix" in the metadata - structure to avoid any issues. - - Datasets are all arrays with size>2 (arrays, lists) - - Parameters - ---------- - filename : str - Input filename - dataset_key : None, str, list of strings, default : None - If None all datasets are returned. - If a string or list of strings is provided only items - whose path contain the string(s) are returned. For example - dataset_key = ["instrument", "Fe"] will return - data entries with instrument or Fe in their hdf path. - dataset_path : None, str, list of strings, default : None - If None, no absolute path is searched. - If a string or list of strings is provided items with the absolute - paths specified will be returned. For example, dataset_path = - ['/data/spectrum/Mn'], it returns the exact dataset with this path. - It is not filtered by dataset_key, i.e. with dataset_key = ['Fe'], - it still returns the specific dataset at '/data/spectrum/Mn'. It is - empty if no dataset matching the absolute path provided is present. - metadata_key: : None, str, list of strings, default : None - Only return items from the original metadata whose path contain the - strings .e.g metadata_key = ["instrument", "Fe"] will return - all metadata entries with "instrument" or "Fe" in their hdf path. - skip_array_metadata : bool, default : False - Whether to skip loading metadata with an array entry. This is useful - as metadata may contain large array that is redundant with the data. - nxdata_only : bool, default : False - If True only NXdata will be converted into a signal - if False NXdata and any hdf datasets will be loaded as signals - hardlinks_only : bool, default : False - If True any links (soft or External) will be ignored when loading. - use_default : bool, default : False - If True and a default NXdata is defined in the file load this as a - signal. This will ignore the other keyword options. If True and no - default is defined the file will be loaded according to - the keyword options. - - Returns - ------- - dict : signal dictionary or list of signal dictionaries - - - See Also - -------- - * :py:meth:`~.io_plugins.nexus.list_datasets_in_file` - * :py:meth:`~.io_plugins.nexus.read_metadata_from_file` - - - """ - # search for NXdata sets... - - mapping = kwds.get('mapping', {}) - original_metadata = {} - learning = {} - fin = h5py.File(filename, "r") - signal_dict_list = [] - - if 'dataset_keys' in kwds: - warnings.warn("The `dataset_keys` keyword is deprecated. " - "Use `dataset_key` instead.", VisibleDeprecationWarning) - dataset_key = kwds['dataset_keys'] - - if 'dataset_paths' in kwds: - warnings.warn("The `dataset_paths` keyword is deprecated. " - "Use `dataset_path` instead.", VisibleDeprecationWarning) - dataset_path = kwds['dataset_paths'] - - if 'metadata_keys' in kwds: - warnings.warn("The `metadata_keys` keyword is deprecated. " - "Use `metadata_key` instead.", VisibleDeprecationWarning) - metadata_key = kwds['metadata_keys'] - - dataset_key = _check_search_keys(dataset_key) - dataset_path = _check_search_keys(dataset_path) - metadata_key = _check_search_keys(metadata_key) - original_metadata = _load_metadata(fin, lazy=lazy, - skip_array_metadata=skip_array_metadata) - # some default values... - nexus_data_paths = [] - hdf_data_paths = [] - # check if a default dataset is defined - if use_default: - nexus_data_paths, hdf_data_paths = _find_data(fin, - search_keys=None, - hardlinks_only=False) - nxentry = None - nxdata = None - if "attrs" in original_metadata: - if "default" in original_metadata["attrs"]: - nxentry = original_metadata["attrs"]["default"] - else: - rootlist = list(original_metadata.keys()) - rootlist.remove("attrs") - if rootlist and len(rootlist) == 1: - nxentry == rootlist[0] - if nxentry: - if "default" in original_metadata[nxentry]["attrs"]: - nxdata = original_metadata[nxentry]["attrs"]["default"] - if nxentry and nxdata: - nxdata = "/"+nxentry+"/"+nxdata - if nxdata: - hdf_data_paths = [] - nexus_data_paths = [nxpath for nxpath in nexus_data_paths - if nxdata in nxpath] - - # if no default found then search for the data as normal - if not nexus_data_paths and not hdf_data_paths: - nexus_data_paths, hdf_data_paths = \ - _find_data(fin, search_keys=dataset_key, - hardlinks_only=hardlinks_only, - absolute_path=dataset_path) - - for data_path in nexus_data_paths: - dictionary = _nexus_dataset_to_signal(fin, data_path, lazy=lazy) - entryname = _text_split(data_path, "/")[0] - dictionary["mapping"] = mapping - title = dictionary["metadata"]["General"]["title"] - if entryname in original_metadata: - if metadata_key is None: - dictionary["original_metadata"] = \ - original_metadata[entryname] - else: - dictionary["original_metadata"] = \ - _find_search_keys_in_dict(original_metadata, - search_keys=metadata_key) - # test if it's a hyperspy_nexus format and update metadata - # as appropriate. - if "attrs" in original_metadata and \ - "file_writer" in original_metadata["attrs"]: - if original_metadata["attrs"]["file_writer"] == \ - "hyperspy_nexus_v3": - orig_metadata = original_metadata[entryname] - if "auxiliary" in orig_metadata: - oma = orig_metadata["auxiliary"] - if "learning_results" in oma: - learning = oma["learning_results"] - dictionary["attributes"] = {} - dictionary["attributes"]["learning_results"] = \ - learning - if "original_metadata" in oma: - if metadata_key is None: - dictionary["original_metadata"] = \ - (oma["original_metadata"]) - else: - dictionary["original_metadata"] = \ - _find_search_keys_in_dict( - (oma["original_metadata"]), - search_keys=metadata_key) - # reconstruct the axes_list for axes_manager - for k, v in oma['original_metadata'].items(): - if k.startswith('_sig_'): - hyper_ax = [ax_v for ax_k, ax_v in v.items() - if ax_k.startswith('_axes')] - oma['original_metadata'][k]['axes'] = hyper_ax - if "hyperspy_metadata" in oma: - hyper_metadata = oma["hyperspy_metadata"] - hyper_metadata.update(dictionary["metadata"]) - dictionary["metadata"] = hyper_metadata - else: - dictionary["original_metadata"] = {} - - signal_dict_list.append(dictionary) - - if not nxdata_only: - for data_path in hdf_data_paths: - datadict = _extract_hdf_dataset(fin, data_path, lazy=lazy) - if datadict: - title = data_path[1:].replace('/', '_') - basic_metadata = {'General': - {'original_filename': - os.path.split(filename)[1], - 'title': title}} - datadict["metadata"].update(basic_metadata) - signal_dict_list.append(datadict) - - return signal_dict_list - - -def _is_linear_axis(data): - """Check if the data is linearly incrementing. - - Parameters - ---------- - data : dask or numpy array - - Returns - ------- - bool - True or False - - """ - steps = np.diff(data) - est_steps = np.array([steps[0]]*len(steps)) - return np.allclose(est_steps, steps, rtol=1.0e-5) - - -def _is_numeric_data(data): - """Check that data contains numeric data. - - Parameters - ---------- - data : dask or numpy array - - Returns - ------- - bool - True or False - - """ - try: - data.astype(float) - return True - except ValueError: - return False - - -def _is_int(s): - """Check that s in an integer. - - Parameters - ---------- - s : python object to test - - Returns - ------- - bool - True or False - - """ - try: - int(s) - return True - except ValueError: - return False - - -def _check_search_keys(search_keys): - if type(search_keys) is str: - return [search_keys] - elif type(search_keys) is list: - if all(isinstance(key, str) for key in search_keys): - return search_keys - else: - raise ValueError("key list provided is not a list of strings") - elif search_keys is None: - return search_keys - else: - raise ValueError("search keys must be None, a string, " - "or a list of strings") - - -def _find_data(group, search_keys=None, hardlinks_only=False, - absolute_path=None): - """Read from a nexus or hdf file and return a list of the dataset entries. - - The method iterates through group attributes and returns NXdata or - hdf datasets of size >=2 if they're not already NXdata blocks - and returns a list of the entries - This is a convenience method to inspect a file to see which datasets - are present rather than loading all the sets in the file as signals - h5py.visit or visititems does not visit soft - links or external links so an implementation of a recursive - search is required. See https://github.com/h5py/h5py/issues/671 - - - Parameters - ---------- - group : hdf group or File - search_keys : string, list of strings or None, default: None - Only return items which contain the strings - .e.g search_list = ["instrument","Fe"] will return - hdf entries with instrument or Fe in their hdf path. - hardlinks_only : bool , default : False - Option to ignore links (soft or External) within the file. - absolute_path : string, list of strings or None, default: None - Return items with the exact specified absolute path - - Returns - ------- - nx_dataset_list, hdf_dataset_list - nx_dataset_list is a list of all NXdata paths - hdf_dataset_list is a list of all hdf_datasets not linked to an - NXdata set. - - """ - _check_search_keys(search_keys) - _check_search_keys(absolute_path) - all_hdf_datasets = [] - unique_hdf_datasets = [] - all_nx_datasets = [] - unique_nx_datasets = [] - rootname = "" - - def find_data_in_tree(group, rootname): - for key, value in group.items(): - if rootname != "": - rootkey = rootname + "/" + key - else: - rootkey = "/" + key - if isinstance(value, h5py.Group): - target = _getlink(group, rootkey, key) - if "NX_class" in value.attrs: - if value.attrs["NX_class"] == b"NXdata" \ - and "signal" in value.attrs.keys(): - all_nx_datasets.append(rootkey) - if target is None: - unique_nx_datasets.append(rootkey) - if hardlinks_only: - if target is None: - find_data_in_tree(value, rootkey) - else: - find_data_in_tree(value, rootkey) - else: - if isinstance(value, h5py.Dataset): - if value.size >= 2: - target = _getlink(group, rootkey, key) - if not(value.dtype.type is np.string_ or - value.dtype.type is np.object_): - all_hdf_datasets.append(rootkey) - if target is None: - unique_hdf_datasets.append(rootkey) - # need to use custom recursive function as visititems in h5py - # does not visit links - find_data_in_tree(group, rootname) - - if search_keys is None and absolute_path is None: - # return all datasets - if hardlinks_only: - # return only the stored data, no linked data - return unique_nx_datasets, unique_hdf_datasets - else: - return all_nx_datasets, all_hdf_datasets - - elif type(search_keys) is list or type(absolute_path) is list: - if hardlinks_only: - # return only the stored data, no linked data - nx_datasets = unique_nx_datasets - hdf_datasets = unique_hdf_datasets - else: - nx_datasets = all_nx_datasets - hdf_datasets = all_hdf_datasets - - matched_hdf = set() - matched_nexus = set() - # return data having the specified absolute paths - if absolute_path is not None: - matched_hdf.update([j for j in hdf_datasets - if any(s==j for s in absolute_path)]) - matched_nexus.update([j for j in nx_datasets - if any(s==j for s in absolute_path)]) - # return data which contains a search string - if search_keys is not None: - matched_hdf.update([j for j in hdf_datasets - if any(s in j for s in search_keys)]) - matched_nexus.update([j for j in nx_datasets - if any(s in j for s in search_keys)]) - - matched_nexus = list(matched_nexus) - matched_hdf = list(matched_hdf) - - return matched_nexus, matched_hdf - - -def _load_metadata(group, lazy=False, skip_array_metadata=False): - """Search through a hdf group and return the group structure. - - h5py.visit or visititems does not visit soft - links or external links so an implementation of a recursive - search is required. See https://github.com/h5py/h5py/issues/671 - - Parameters - ---------- - group : hdf group - location to load the metadata from - lazy : bool , default : False - Option for lazy loading - skip_array_metadata : bool, default : False - whether to skip loading array metadata - - Returns - ------- - dict - dictionary of group contents - - - """ - rootname = "" - - def find_meta_in_tree(group, rootname, lazy=False, - skip_array_metadata=False): - tree = {} - for key, item in group.attrs.items(): - new_key = _fix_exclusion_keys(key) - if "attrs" not in tree.keys(): - tree["attrs"] = {} - tree["attrs"][new_key] = _parse_from_file(item, lazy=lazy) - - for key, item in group.items(): - if rootname != "": - rootkey = rootname + "/" + key - else: - rootkey = "/" + key - new_key = _fix_exclusion_keys(key) - if type(item) is h5py.Dataset: - if item.attrs: - if new_key not in tree.keys(): - tree[new_key] = {} - # avoid loading array as metadata - if skip_array_metadata: - if item.size < 2 and item.ndim == 0: - tree[new_key]["value"] = _parse_from_file(item, - lazy=lazy) - else: - tree[new_key]["value"] = _parse_from_file(item, - lazy=lazy) - - for k, v in item.attrs.items(): - if "attrs" not in tree[new_key].keys(): - tree[new_key]["attrs"] = {} - tree[new_key]["attrs"][k] = _parse_from_file(v, - lazy=lazy) - else: - # this is to support hyperspy where datasets are not saved - # with attributes - if skip_array_metadata: - if item.size < 2 and item.ndim == 0: - tree[new_key] = _parse_from_file(item, lazy=lazy) - else: - tree[new_key] = _parse_from_file(item, lazy=lazy) - - elif type(item) is h5py.Group: - if "NX_class" in item.attrs: - if item.attrs["NX_class"] != b"NXdata": - tree[new_key] = find_meta_in_tree(item, rootkey, - lazy=lazy, - skip_array_metadata=skip_array_metadata) - else: - tree[new_key] = find_meta_in_tree(item, rootkey, - lazy=lazy, - skip_array_metadata=skip_array_metadata) - - return tree - extracted_tree = find_meta_in_tree(group, rootname, lazy=lazy, - skip_array_metadata=skip_array_metadata) - return extracted_tree - - -def _fix_exclusion_keys(key): - """Exclude hyperspy specific keys. - - Signal and DictionaryBrowser break if a - a key is a dict method - e.g. {"keys":2.0}. - - This method prepends the key with ``fix_`` so the information is - still present to work around this issue - - Parameters - ---------- - key : str - - Returns - ------- - str - - """ - if key.startswith("keys"): - return "fix_"+key - else: - return key - - -def _find_search_keys_in_dict(tree, search_keys=None): - """Search through a dict for search keys. - - This is a convenience method to inspect a file for a value - rather than loading the file as a signal - - Parameters - ---------- - tree : h5py File object - search_keys : string or list of strings - Only return items which contain the strings - .e.g search_keys = ["instrument","Fe"] will return - hdf entries with instrument or Fe in their hdf path. - - Returns - ------- - dict - When search_list is specified only full paths - containing one or more search_keys will be returned - - """ - _check_search_keys(search_keys) - metadata_dict = {} - rootname = "" - - # recursive function - def find_searchkeys_in_tree(myDict, rootname): - for key, value in myDict.items(): - if rootname != "": - rootkey = rootname + "/" + key - else: - rootkey = key - if type(search_keys) is list \ - and any([s1 in rootkey for s1 in search_keys]): - mod_keys = _text_split(rootkey, (".", "/")) - # create the key, values in the dict - p = metadata_dict - for d in mod_keys[:-1]: - p = p.setdefault(d, {}) - p[mod_keys[-1]] = value - if isinstance(value, dict): - find_searchkeys_in_tree(value, rootkey) - - if search_keys is None: - return tree - else: - find_searchkeys_in_tree(tree, rootname) - return metadata_dict - - -def _write_nexus_groups(dictionary, group, skip_keys=None, **kwds): - """Recursively iterate throuh dictionary and write groups to nexus. - - Parameters - ---------- - dictionary : dict - dictionary contents to store to hdf group - group : hdf group - location to store dictionary - skip_keys : str or list of str - the key(s) to skip when writing into the group - **kwds : additional keywords - additional keywords to pass to h5py.create_dataset method - - """ - if skip_keys is None: - skip_keys = [] - elif isinstance(skip_keys, str): - skip_keys = [skip_keys] - - for key, value in dictionary.items(): - if key == 'attrs' or key in skip_keys: - # we will handle attrs later... and skip unwanted keys - continue - if isinstance(value, dict): - if "attrs" in value: - if "NX_class" in value["attrs"] and \ - value["attrs"]["NX_class"] == "NXdata": - continue - if 'value' in value.keys() \ - and not isinstance(value["value"], dict) \ - and len(set(list(value.keys()) + ["attrs", "value"])) == 2: - value = value["value"] - else: - _write_nexus_groups(value, group.require_group(key), - skip_keys=skip_keys, **kwds) - if isinstance(value, (list, tuple, np.ndarray, da.Array)): - if all(isinstance(v, dict) for v in value): - # a list of dictionary is from the axes of HyperSpy signal - for i, ax_dict in enumerate(value): - ax_key = '_axes_' + str(i) - _write_nexus_groups(ax_dict, group.require_group(ax_key), - skip_keys=skip_keys, **kwds) - else: - data = _parse_to_file(value) - overwrite_dataset(group, data, key, chunks=None, **kwds) - elif isinstance(value, (int, float, str, bytes)): - group.create_dataset(key, data=_parse_to_file(value)) - else: - if value is not None and value != t.Undefined and key not in group: - _write_nexus_groups(value, group.require_group(key), - skip_keys=skip_keys, **kwds) - - -def _write_nexus_attr(dictionary, group, skip_keys=None): - """Recursively iterate through dictionary and write "attrs" dictionaries. - - This step is called after the groups and datasets have been created - - Parameters - ---------- - dictionary : dict - Input dictionary to be written to the hdf group - group : hdf group - location to store the attrs sections of the dictionary - - """ - if skip_keys is None: - skip_keys = [] - elif isinstance(skip_keys, str): - skip_keys = [skip_keys] - - for key, value in dictionary.items(): - if key == 'attrs': - - for k, v in value.items(): - group.attrs[k] = _parse_to_file(v) - else: - if isinstance(value, dict): - if "attrs" in value: - if "NX_class" in value["attrs"] and \ - value["attrs"]["NX_class"] == "NXdata": - continue - if key not in skip_keys: - _write_nexus_attr(dictionary[key], group[key], - skip_keys=skip_keys) - - -def read_metadata_from_file(filename, metadata_key=None, - lazy=False, verbose=False, - skip_array_metadata=False): - """Read the metadata from a nexus or hdf file. - - This method iterates through the file and returns a dictionary of - the entries. - This is a convenience method to inspect a file for a value - rather than loading the file as a signal. - - Parameters - ---------- - filename : str - path of the file to read - metadata_key : None,str or list_of_strings , default : None - None will return all datasets found including linked data. - Providing a string or list of strings will only return items - which contain the string(s). - For example, search_keys = ["instrument","Fe"] will return - hdf entries with "instrument" or "Fe" in their hdf path. - verbose: bool, default : False - Pretty Print the results to screen - skip_array_metadata : bool, default : False - Whether to skip loading array metadata. This is useful as a lot of - large array may be present in the metadata and it is redundant with - dataset itself. - - Returns - ------- - dict - Metadata dictionary. - - See Also - -------- - * :py:meth:`~.io_plugins.nexus.file_reader` - * :py:meth:`~.io_plugins.nexus.file_writer` - * :py:meth:`~.io_plugins.nexus.list_datasets_in_file` - - - """ - search_keys = _check_search_keys(metadata_key) - fin = h5py.File(filename, "r") - # search for NXdata sets... - # strip out the metadata (basically everything other than NXdata) - stripped_metadata = _load_metadata(fin, lazy=lazy, - skip_array_metadata=skip_array_metadata) - stripped_metadata = _find_search_keys_in_dict(stripped_metadata, - search_keys=search_keys) - if verbose: - pprint.pprint(stripped_metadata) - - fin.close() - return stripped_metadata - - -def list_datasets_in_file(filename, dataset_key=None, - hardlinks_only=False, - verbose=True): - """Read from a nexus or hdf file and return a list of the dataset paths. - - This method is used to inspect the contents of a Nexus file. - The method iterates through group attributes and returns NXdata or - hdf datasets of size >=2 if they're not already NXdata blocks - and returns a list of the entries. - This is a convenience method to inspect a file to list datasets - present rather than loading all the datasets in the file as signals. - - Parameters - ---------- - filename : str - path of the file to read - dataset_key : str, list of strings or None , default: None - If a str or list of strings is provided only return items whose - path contain the strings. - For example, dataset_key = ["instrument", "Fe"] will only return - hdf entries with "instrument" or "Fe" somewhere in their hdf path. - hardlinks_only : bool, default : False - If true any links (soft or External) will be ignored when loading. - verbose : boolean, default : True - Prints the results to screen - - - Returns - ------- - list - list of paths to datasets - - - See Also - -------- - * :py:meth:`~.io_plugins.nexus.file_reader` - * :py:meth:`~.io_plugins.nexus.file_writer` - * :py:meth:`~.io_plugins.nexus.read_metadata_from_file` - - - """ - search_keys = _check_search_keys(dataset_key) - fin = h5py.File(filename, "r") - # search for NXdata sets... - # strip out the metadata (basically everything other than NXdata) - nexus_data_paths, hdf_dataset_paths = \ - _find_data(fin, search_keys=search_keys, hardlinks_only=hardlinks_only) - if verbose: - if nexus_data_paths: - print("NXdata found") - for nxd in nexus_data_paths: - print(nxd) - else: - print("NXdata not found") - if hdf_dataset_paths: - print("Unique HDF datasets found") - for hdfd in hdf_dataset_paths: - print(hdfd, fin[hdfd].shape) - else: - print("No HDF datasets not found or data is captured by NXdata") - fin.close() - return nexus_data_paths, hdf_dataset_paths - - -def _write_signal(signal, nxgroup, signal_name, **kwds): - """Store the signal data as an NXdata dataset. - - Parameters - ---------- - signal : Hyperspy signal - nxgroup : HDF group - Entry at which to save signal data - signal_name : str - Name under which to store the signal entry in the file - - """ - smd = signal.metadata.Signal - if signal.axes_manager.signal_dimension == 1: - smd.record_by = "spectrum" - elif signal.axes_manager.signal_dimension == 2: - smd.record_by = "image" - else: - smd.record_by = "" - - nxdata = nxgroup.require_group(signal_name) - nxdata.attrs["NX_class"] = _parse_to_file("NXdata") - nxdata.attrs["signal"] = _parse_to_file("data") - if smd.record_by: - nxdata.attrs["interpretation"] = _parse_to_file(smd.record_by) - overwrite_dataset(nxdata, signal.data, "data", chunks=None, - signal_axes=signal.axes_manager.signal_indices_in_array, - **kwds) - axis_names = [_parse_to_file(".")] * len(signal.axes_manager.shape) - for i, axis in enumerate(signal.axes_manager._axes): - if axis.name != t.Undefined: - axname = axis.name - axindex = [axis.index_in_array] - indices = _parse_to_file(axis.name + "_indices") - nxdata.attrs[indices] = _parse_to_file(axindex) - ax = nxdata.require_dataset(axname, data=axis.axis, - shape=axis.axis.shape, - dtype=axis.axis.dtype) - if axis.units != t.Undefined: - ax.attrs['units'] = axis.units - axis_names[axis.index_in_array] = axname - - nxdata.attrs["axes"] = _parse_to_file(axis_names) - return nxdata - - -def file_writer(filename, - signals, - save_original_metadata=True, - skip_metadata_key=None, - use_default=False, - *args, **kwds): - """Write the signal and metadata as a nexus file. - - This will save the signal in NXdata format in the file. - As the form of the metadata can vary and is not validated it will - be stored as an NXcollection (an unvalidated collection) - - Parameters - ---------- - filename : str - Path of the file to write - signals : signal or list of signals - Signal(s) to be written - save_original_metadata : bool , default : False - Option to save hyperspy.original_metadata with the signal. - A loaded Nexus file may have a large amount of data - when loaded which you may wish to omit on saving - skip_metadata_key : str or list of str, default : None - the key(s) to skip when it is saving original metadata. This is useful - when some metadata's keys are to be ignored. - use_default : bool , default : False - Option to define the default dataset in the file. - If set to True the signal or first signal in the list of signals - will be defined as the default (following Nexus v3 data rules). - - See Also - -------- - * :py:meth:`~.io_plugins.nexus.file_reader` - * :py:meth:`~.io_plugins.nexus.list_datasets_in_file` - * :py:meth:`~.io_plugins.nexus.read_metadata_from_file` - - """ - if not isinstance(signals, list): - signals = [signals] - - with h5py.File(filename, mode='w') as f: - f.attrs['file_format'] = "nexus" - f.attrs['file_writer'] = "hyperspy_nexus_v3" - if 'compression' not in kwds: - kwds['compression'] = 'gzip' - if use_default: - f.attrs["default"] = "entry1" - # - # write the signals - # - - for i, sig in enumerate(signals): - nxentry = f.create_group("entry%d" % (i + 1)) - nxentry.attrs["NX_class"] = _parse_to_file("NXentry") - - if isinstance(sig.metadata, dict): - sig.metadata = DictionaryTreeBrowser(sig.metadata) - if isinstance(sig.original_metadata, dict): - sig.original_metadata = DictionaryTreeBrowser( - sig.original_metadata) - - signal_name = sig.metadata.General.title \ - if sig.metadata.General.title else 'unnamed__%d' % i - if "/" in signal_name: - signal_name = signal_name.replace("/", "_") - if signal_name.startswith("__"): - signal_name = signal_name[2:] - - if i == 0 and use_default: - nxentry.attrs["default"] = signal_name - - nxaux = nxentry.create_group("auxiliary") - nxaux.attrs["NX_class"] = _parse_to_file("NXentry") - _write_signal(sig, nxentry, signal_name, **kwds) - - if sig.learning_results: - nxlearn = nxaux.create_group('learning_results') - nxlearn.attrs["NX_class"] = _parse_to_file("NXcollection") - learn = sig.learning_results.__dict__ - _write_nexus_groups(learn, nxlearn, **kwds) - _write_nexus_attr(learn, nxlearn) - # - # write metadata - # - if save_original_metadata: - if sig.original_metadata: - ometa = sig.original_metadata.as_dictionary() - - nxometa = nxaux.create_group('original_metadata') - nxometa.attrs["NX_class"] = _parse_to_file("NXcollection") - # write the groups and structure - _write_nexus_groups(ometa, nxometa, - skip_keys=skip_metadata_key, **kwds) - _write_nexus_attr(ometa, nxometa, - skip_keys=skip_metadata_key) - - if sig.metadata: - meta = sig.metadata.as_dictionary() - - nxometa = nxaux.create_group('hyperspy_metadata') - nxometa.attrs["NX_class"] = _parse_to_file("NXcollection") - # write the groups and structure - _write_nexus_groups(meta, nxometa, **kwds) - _write_nexus_attr(meta, nxometa) diff --git a/hyperspy/io_plugins/phenom.py b/hyperspy/io_plugins/phenom.py deleted file mode 100644 index 0a9a3013ab..0000000000 --- a/hyperspy/io_plugins/phenom.py +++ /dev/null @@ -1,683 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2016 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . -# -# -# Install this as hyperspy/io_plugins/phenom.py -# -# If you get the error "cannot decompress LZW", run -# pip install imagecodecs -# -# Edit hyperspy/io_plugins/__init__.py and add phenom to both the -# list of imports and io_plugins -# -# You should now be able to load Phenom EID .elid files. -# -# This reader supports the ELID file format used in Phenom ProSuite -# Element Identification version 3.8.0 and later. You can convert -# older ELID files by loading the file into a recent Element -# Identification release and then save the ELID file into the newer -# file format. - -import bz2 -import math -import numpy as np -import copy -import os -import struct -import io -from datetime import datetime -from dateutil import tz -import tifffile -from hyperspy.misc import rgb_tools -import xml.etree.ElementTree as ET - -# Plugin characteristics -# ---------------------- -format_name = 'Phenom Element Identification (ELID)' -description = 'Read data from Phenom ELID files.' -full_support = False -# Recognised file extension -file_extensions = ('elid', 'ELID') -default_extension = 0 -# Reading capabilities -reads_images = False -reads_spectrum = True -reads_spectrum_image = True -# Writing features -writes = False -non_uniform_axis = False -# ---------------------- - -def element_symbol(z): - elements = ['', - 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', - 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', - 'Kr', 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', - 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', - 'Yb', 'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn', - 'Fr', 'Ra', 'Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr', - 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv', 'Ts', 'Og'] - if z < 1 or z >= len(elements): - raise Exception('Invalid atomic number') - return elements[z] - -def family_symbol(i): - families = ['', 'K', 'L', 'M', 'N', 'O', 'P'] - if i < 1 or i >= len(families): - raise Exception('Invalid atomic number') - return families[i] - -def IsGZip(pathname): - with open(pathname, 'rb') as f: - (magic,) = struct.unpack('2s', f.read(2)) - return magic == b'\x1f\x8b' - -def IsBZip2(pathname): - with open(pathname, 'rb') as f: - (magic, _, bytes) = struct.unpack('2s2s6s', f.read(10)) - return magic == b'BZ' and bytes == b'\x31\x41\x59\x26\x53\x59' - - -class ElidReader: - - def __init__(self, pathname, block_size=1024*1024): - if IsGZip(pathname): - raise Exception('pre EID 3.8 files are not supported') - if not IsBZip2(pathname): - raise Exception('not an ELID file') - - self._pathname = pathname - self._file = open(pathname, 'rb') - self._decompressor = bz2.BZ2Decompressor() - self._block_size = block_size - (id, version) = struct.unpack('<4si', self._read(8)) - if id != b'EID2': - raise Exception('not an ELID file') - if version > 2: - raise Exception('unsupported ELID format') - self._version = version - self.dictionaries = self._read_Project() - self._file.close() - - def _read(self, size=1): - data = self._decompressor.decompress(b'', size) - while self._decompressor.needs_input: - data += self._decompressor.decompress(self._file.read(self._block_size), size - len(data)) - return data - - def _read_bool(self): - return struct.unpack('?', self._read(1))[0] - - def _read_uint8(self): - return struct.unpack('B', self._read(1))[0] - - def _read_int32(self): - return struct.unpack(' 0: - dict[element.tag] = { 'value': element.text } - for attrib, value in element.items(): - dict[element.tag].update({ attrib: value }) - else: - dict[element.tag] = element.text - else: - dict[element.tag] = {} - for child in element: - dict[element.tag].update(xml_element_to_dict(child)) - return dict - - def make_metadata_dict(xml): - dict = xml_element_to_dict(ET.fromstring(xml)) - return dict['FeiImage'] if dict else {} - - n = self._read_uint32() - if n == 0: - return (None, None) - bytes = io.BytesIO(self._read(n)) - with tifffile.TiffFile(bytes) as tiff: - data = tiff.asarray() - if len(data.shape) > 2: - data = rgb_tools.regular_array2rgbx(data) - tags = tiff.pages[0].tags - if 'FEI_TITAN' in tags: - metadata = make_metadata_dict(tags['FEI_TITAN'].value) - metadata['acquisition']['scan']['fieldSize'] = max(self._get_value_with_unit(metadata['pixelHeight']) * data.shape[0], self._get_value_with_unit(metadata['pixelWidth']) * data.shape[1]) - else: - metadata = {} - return (metadata, data) - - def _read_int32s(self): - n = self._read_uint32() - return [self._read_int32() for _ in range(n)] - - def _read_float64(self): - return struct.unpack(' 1: - oxide += str(num_element) - oxide += 'O' - if num_oxygen > 1: - oxide += str(num_oxygen) - return oxide - - def _read_oxides(self): - n = self._read_uint32() - return [self._read_oxide() for _ in range(n)] - - def _read_element_family(self): - element = element_symbol(self._read_uint8()) - family = family_symbol(self._read_uint8()) - return (element, family) - - def _read_element_families(self): - n = self._read_uint32() - return [self._read_element_family() for _ in range(n)] - - def _read_drift_correction(self): - dc = self._read_uint8() - if dc == 1: - return 'on' - elif dc == 2: - return 'off' - else: - return 'unknown' - - def _read_eds_metadata(self, om): - metadata = {} - metadata['high_tension'] = self._read_float64() - detector_elevation = self._read_float64() - if self._version == 0: - detector_elevation = math.radians(detector_elevation) - metadata['detector_elevation'] = detector_elevation - metadata['detector_azimuth'] = self._read_float64() - metadata['live_time'] = self._read_float64() - metadata['real_time'] = self._read_float64() - metadata['slow_peaking_time'] = self._read_float64() - metadata['fast_peaking_time'] = self._read_float64() - metadata['detector_resolution'] = self._read_float64() - metadata['instrument_id'] = self._read_string() - if self._version == 0 and 'workingDistance' in om: - metadata['working_distance'] = self._get_value_with_unit(om['workingDistance']) - metadata['slow_peaking_time'] = 11.2e-6 if float(om['acquisition']['scan']['spotSize']) < 4.5 else 2e-6 - metadata['fast_peaking_time'] = 100e-9; - metadata['detector_surface_area'] = 25e-6; - elif self._version > 0: - metadata['optical_working_distance'] = self._read_float64() - metadata['working_distance'] = self._read_float64() - metadata['detector_surface_area'] = self._read_float64() - metadata['detector_distance'] = self._read_float64() - metadata['sample_tilt_angle'] = self._read_float64() - return metadata - - def _read_CommonAnalysis(self, am): - (metadata, cutout) = self._read_tiff() - sum_spectrum = self._read_spectrum() - eds_metadata = self._read_eds_metadata(am) - eds_metadata['offset'] = sum_spectrum[0] - eds_metadata['dispersion'] = sum_spectrum[1] - data = sum_spectrum[2] - eds_metadata['included_elements'] = [element_symbol(z) for z in self._read_uint8s()] - eds_metadata['excluded_elements'] = [element_symbol(z) for z in self._read_uint8s()] - eds_metadata['background_fit_bins'] = self._read_int32s() - eds_metadata['selected_oxides'] = self._read_oxides() - eds_metadata['auto_id'] = self._read_bool() - eds_metadata['order_nr'] = self._read_int32() - eds_metadata['family_overrides'] = self._read_element_families() - if self._version >= 2: - eds_metadata['drift_correction'] = self._read_drift_correction() - else: - eds_metadata['drift_correction'] = 'unknown' - if metadata: - metadata['acquisition']['scan']['detectors']['EDS'] = eds_metadata - else: - metadata = {} - metadata.update({'acquisition': {'scan': {'detectors': {}}}}) - metadata['acquisition']['scan']['detectors']['EDS'] = eds_metadata - return (metadata, data) - - def _make_metadata_dict(self, signal_type=None, title="", datetime=None): - metadata_dict = { - 'General': { - 'original_filename': os.path.split(self._pathname)[1], - 'title': title - } - } - if signal_type: - metadata_dict['Signal'] = { - 'signal_type': signal_type - } - if datetime: - metadata_dict['General'].update({ - 'date': datetime[0], - 'time': datetime[1], - 'time_zone': self._get_local_time_zone() - }) - return metadata_dict - - def _get_local_time_zone(self): - return tz.tzlocal().tzname(datetime.today()) - - def _make_mapping(self): - return { - 'acquisition.scan.detectors.EDS.detector_azimuth': ('Acquisition_instrument.SEM.Detector.EDS.azimuth_angle', lambda x: math.degrees(float(x))), - 'acquisition.scan.detectors.EDS.detector_elevation': ('Acquisition_instrument.SEM.Detector.EDS.elevation_angle', lambda x: math.degrees(float(x))), - 'acquisition.scan.detectors.EDS.detector_resolution': ('Acquisition_instrument.SEM.Detector.EDS.energy_resolution_MnKa', lambda x: float(x)), - 'acquisition.scan.detectors.EDS.live_time': ('Acquisition_instrument.SEM.Detector.EDS.live_time', lambda x: float(x)), - 'acquisition.scan.detectors.EDS.real_time': ('Acquisition_instrument.SEM.Detector.EDS.real_time', lambda x: float(x)), - 'acquisition.scan.detectors.EDS.high_tension': ('Acquisition_instrument.SEM.beam_energy', lambda x: float(x) / 1e3), - 'acquisition.scan.highVoltage.value': ('Acquisition_instrument.SEM.beam_energy', lambda x: -float(x)), - 'instrument.uniqueID': ('Acquisition_instrument.SEM.microscope', lambda x: x), - 'samplePosition.x': ('Acquisition_instrument.SEM.Stage.x', lambda x: float(x) / 1e-3), - 'samplePosition.y': ('Acquisition_instrument.SEM.Stage.y', lambda x: float(x) / 1e-3), - 'acquisition.scan.detectors.EDS.sample_tilt_angle': ('Acquisition_instrument.SEM.Stage.tilt_alpha', lambda x: math.degrees(float(x))), - 'acquisition.scan.detectors.EDS.working_distance': ('Acquisition_instrument.SEM.working_distance', lambda x: float(x) / 1e-3) - } - - def _make_spot_spectrum_dict(self, om, offset, dispersion, data, title): - axes = [{ - 'name': 'Energy', - 'offset': offset / 1e3, - 'scale': dispersion / 1e3, - 'size': len(data), - 'units': 'keV', - 'navigate': False}] - dict = { - 'data': data, - 'axes': axes, - 'metadata': self._make_metadata_dict('EDS_SEM', title, self._get_datetime(om)), - 'original_metadata': om, - 'mapping': self._make_mapping() - } - return dict - - def _get_datetime(self, metadata): - if 'time' in metadata: - return metadata['time'].split('T') - - def _make_line_spectrum_dict(self, om, offset, dispersion, data, title): - axes = [ - { - 'index_in_array': 0, - 'name': 'i', - 'offset': 0, - 'scale': 1, - 'size': data.shape[0], - 'units': 'points', - 'navigate': True - }, - { - 'index_in_array': 1, - 'name': 'X-ray energy', - 'offset': offset / 1e3, - 'scale': dispersion / 1e3, - 'size': data.shape[1], - 'units': 'keV', - 'navigate': False - }] - dict = { - 'data': data, - 'axes': axes, - 'metadata': self._make_metadata_dict('EDS_SEM', title, self._get_datetime(om)), - 'original_metadata': om, - 'mapping': self._make_mapping() - } - return dict - - def _get_unit(self, value): - if value > 1: - return (1, '') - elif value > 1e-3: - return (1e-3, 'm') - elif value > 1e-6: - return (1e-6, 'µ') - elif value > 1e-9: - return (1e-9, 'n') - else: - return (1, '') - - def _make_map_spectrum_dict(self, om, offset, dispersion, data, title): - size = om['acquisition']['scan']['fieldSize'] * float(om['acquisition']['scan']['scanScale']) - (scale, prefix) = self._get_unit(size) - unit = prefix + 'm' - size = size / scale - axes = [ - { - 'index_in_array': 0, - 'name': 'y', - 'offset': 0, - 'scale': size / data.shape[0], - 'size': data.shape[0], - 'units': unit, - 'navigate': True - }, - { - 'index_in_array': 1, - 'name': 'x', - 'offset': 0, - 'scale': size / data.shape[1], - 'size': data.shape[1], - 'units': unit, - 'navigate': True - }, - { - 'index_in_array': 2, - 'name': 'X-ray energy', - 'offset': offset / 1e3, - 'scale': dispersion / 1e3, - 'size': data.shape[2], - 'units': 'keV', - 'navigate': False - }] - dict = { - 'data': data, - 'axes': axes, - 'metadata': self._make_metadata_dict('EDS_SEM', title, self._get_datetime(om)), - 'original_metadata': om, - 'mapping': self._make_mapping() - } - return dict - - def _make_image_dict(self, om, data, title): - if om: - (scale, prefix) = self._get_unit(0.2 * om['acquisition']['scan']['fieldSize']) - scale_x = self._get_value_with_unit(om['pixelWidth']) / scale - scale_y = self._get_value_with_unit(om['pixelHeight']) / scale - unit = prefix + 'm' - else: - scale_x = 1 - scale_y = 1 - unit = 'points' - axes = [ - { - 'index_in_array': 0, - 'name': 'y', - 'offset': 0, - 'scale': scale_y, - 'size': data.shape[0], - 'units': unit, - 'navigate': True - }, - { - 'index_in_array': 1, - 'name': 'x', - 'offset': 0, - 'scale': scale_x, - 'size': data.shape[1], - 'units': unit, - 'navigate': True - }] - dict = { - 'data': data, - 'axes': axes, - 'metadata': self._make_metadata_dict('', title, self._get_datetime(om)), - 'original_metadata': om, - 'mapping': self._make_mapping() - } - return dict - - def _read_MsaAnalysis(self, label, am): - (om, sum_spectrum) = self._read_CommonAnalysis(am) - original_metadata = copy.deepcopy(am) - original_metadata.update(om) - return self._make_spot_spectrum_dict(original_metadata, om['acquisition']['scan']['detectors']['EDS']['offset'], om['acquisition']['scan']['detectors']['EDS']['dispersion'], np.array(sum_spectrum), '{}, MSA {}'.format(label, om['acquisition']['scan']['detectors']['EDS']['order_nr'])) - - def _read_SpotAnalysis(self, label, am): - (om, sum_spectrum) = self._read_CommonAnalysis(am) - x = self._read_float64() - y = self._read_float64() - original_metadata = copy.deepcopy(am) - original_metadata['acquisition']['scan']['detectors']['EDS'] = om['acquisition']['scan']['detectors']['EDS'] - original_metadata['acquisition']['scan']['detectors']['EDS']['position'] = {'x': x, 'y': y} - return self._make_spot_spectrum_dict(original_metadata, om['acquisition']['scan']['detectors']['EDS']['offset'], om['acquisition']['scan']['detectors']['EDS']['dispersion'], np.array(sum_spectrum), '{}, Spot {}'.format(label, om['acquisition']['scan']['detectors']['EDS']['order_nr'])) - - def _read_LineScanAnalysis(self, label, am): - (om, sum_spectrum) = self._read_CommonAnalysis(am) - x1 = self._read_float64() - y1 = self._read_float64() - x2 = self._read_float64() - y2 = self._read_float64() - size = self._read_uint32() - bins = self._read_uint32() - offset = self._read_float64() - dispersion = self._read_float64() - eds_metadata = self._read_eds_metadata(am) - eds_metadata['live_time'] = om['acquisition']['scan']['detectors']['EDS']['live_time'] - eds_metadata['real_time'] = om['acquisition']['scan']['detectors']['EDS']['real_time'] - eds_metadata['begin'] = { 'x': x1, 'y': y1 } - eds_metadata['end'] = { 'x': x2, 'y': y2 } - eds_metadata['offset'] = offset - eds_metadata['dispersion'] = dispersion - has_variable_real_time = self._read_bool() - has_variable_live_time = self._read_bool() - data = np.empty([size, bins], dtype=np.uint32) - for i in range(size): - for bin in range(bins): - data[i, bin] = self._read_varuint32() - if has_variable_real_time: - eds_metadata['real_time_values'] = [self._read_float64() for _ in range(size)] - else: - eds_metadata['real_time_values'] = [self._read_float64()] * size - if has_variable_live_time: - eds_metadata['live_time_values'] = [self._read_float64() for _ in range(size)] - else: - eds_metadata['live_time_values'] = [self._read_float64()] * size - eds_metadata['high_accuracy_quantification'] = self._read_bool() - original_metadata = copy.deepcopy(am) - original_metadata['acquisition']['scan']['detectors']['EDS'] = eds_metadata - return self._make_line_spectrum_dict(original_metadata, om['acquisition']['scan']['detectors']['EDS']['offset'], om['acquisition']['scan']['detectors']['EDS']['dispersion'], data, '{}, Line {}'.format(label, om['acquisition']['scan']['detectors']['EDS']['order_nr'])) - - def _read_MapAnalysis(self, label, am): - (om, sum_spectrum) = self._read_CommonAnalysis(am) - left = self._read_float64() - top = self._read_float64() - right = self._read_float64() - bottom = self._read_float64() - color_intensities = self._read_float64s() - width = self._read_uint32() - height = self._read_uint32() - bins = self._read_uint32() - offset = self._read_float64() - dispersion = self._read_float64() - original_metadata = copy.deepcopy(am) - eds_metadata = self._read_eds_metadata(am) - eds_metadata['live_time'] = om['acquisition']['scan']['detectors']['EDS']['live_time'] - eds_metadata['real_time'] = om['acquisition']['scan']['detectors']['EDS']['real_time'] - original_metadata['acquisition']['scan']['detectors']['EDS'] = eds_metadata - has_variable_real_time = self._read_bool() - has_variable_live_time = self._read_bool() - data = np.empty([height, width, bins], dtype=np.uint32) - for y in range(height): - for x in range(width): - for bin in range(bins): - data[y, x, bin] = self._read_varuint32() - if has_variable_real_time: - real_time_values = np.empty([height, width], dtype=np.float64) - for y in range(height): - for x in range(width): - real_time_values[y, x] = self._read_float64() - eds_metadata['real_time_values'] = real_time_values - else: - eds_metadata['real_time_values'] = np.full([height, width], self._read_float64()) - if has_variable_live_time: - live_time_values = np.empty([height, width], dtype=np.float64) - for y in range(height): - for x in range(width): - live_time_values[y, x] = self._read_float64() - eds_metadata['live_time_values'] = live_time_values - else: - eds_metadata['live_time_values'] = np.full([height, width], self._read_float64()) - return self._make_map_spectrum_dict(original_metadata, om['acquisition']['scan']['detectors']['EDS']['offset'], om['acquisition']['scan']['detectors']['EDS']['dispersion'], data, '{}, Map {}'.format(label, om['acquisition']['scan']['detectors']['EDS']['order_nr'])) - - def _read_DifferenceAnalysis(self, label, am): - (om, sum_spectrum) = self._read_CommonAnalysis(am) - minuend = self._read_uint32() - subtrahend = self._read_uint32() - original_metadata = copy.deepcopy(am) - original_metadata['acquisition']['scan']['detectors']['EDS'] = om['acquisition']['scan']['detectors']['EDS'] - return self._make_spot_spectrum_dict(original_metadata, om['acquisition']['scan']['detectors']['EDS']['offset'], om['acquisition']['scan']['detectors']['EDS']['dispersion'], np.array(sum_spectrum), '{}, Difference {} - {}'.format(label, minuend, subtrahend)) - - def _read_RegionAnalysis(self, label, am): - (om, sum_spectrum) = self._read_CommonAnalysis(am) - left = self._read_float64() - top = self._read_float64() - right = self._read_float64() - bottom = self._read_float64() - original_metadata = copy.deepcopy(am) - original_metadata['acquisition']['scan']['detectors']['EDS'] = om['acquisition']['scan']['detectors']['EDS'] - original_metadata['acquisition']['scan']['detectors']['EDS']['rectangle'] = { - 'left': left, - 'top': top, - 'right': right, - 'bottom': bottom - } - return self._make_spot_spectrum_dict(original_metadata, om['acquisition']['scan']['detectors']['EDS']['offset'], om['acquisition']['scan']['detectors']['EDS']['dispersion'], np.array(sum_spectrum), '{}, Region {}'.format(label, om['acquisition']['scan']['detectors']['EDS']['order_nr'])) - - def _read_ConstructiveAnalysisSource(self): - analysis_index = self._read_uint32() - weight_factor = self._read_float64() - return (analysis_index, weight_factor) - - def _read_ConstructiveAnalysisSources(self): - n = self._read_uint32() - return [self._read_ConstructiveAnalysisSource() for _ in range(n)] - - def _read_ConstructiveAnalysis(self, label, metadata): - self._read_CommonAnalysis() - description = self._read_string() - sources = self._read_ConstructiveAnalysisSources() - - def _read_ConstructiveAnalyses(self): - n = self._read_uint32() - return [self._read_ConstructiveAnalysis('', {}) for _ in range(n)] - - def _read_Analysis(self, label, am): - type = self._read_uint8() - if type == 1: - return self._read_MsaAnalysis(label, am) - elif type == 2: - return self._read_SpotAnalysis(label, am) - elif type == 3: - return self._read_LineScanAnalysis(label, am) - elif type == 4: - return self._read_MapAnalysis(label, am) - elif type == 5: - return self._read_DifferenceAnalysis(label, am) - elif type == 6: - return self._read_RegionAnalysis(label, am) - elif type == 7: - return self._read_ConstructiveAnalysis(label, am) - else: - raise Exception('Unknown Analysis type') - - def _read_Analyses(self, label, metadata): - n = self._read_uint32() - return [self._read_Analysis(label, metadata) for _ in range(n)] - - def _read_Image(self): - tiff = self._read_tiff() - label = self._read_string() - dictionaries = [] - if tiff: - dictionaries.append(self._make_image_dict(tiff[0], tiff[1], label)) - dictionaries.extend(self._read_Analyses(label, tiff[0])) - return dictionaries - - def _read_Images(self): - n = self._read_uint32() - dictionaries = [] - for _ in range(n): - dictionaries.extend(self._read_Image()) - return dictionaries - - def _read_Project(self): - dictionaries = self._read_Images() - if self._version >= 1: - self._read_Analyses('', {}) - self._read_ConstructiveAnalyses() - return [dict for dict in dictionaries if dict] - - -def file_reader(filename, log_info=False, lazy=False, **kwds): - reader = ElidReader(filename) - return reader.dictionaries diff --git a/hyperspy/io_plugins/protochips.py b/hyperspy/io_plugins/protochips.py deleted file mode 100644 index 0b18d06447..0000000000 --- a/hyperspy/io_plugins/protochips.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - - -import numpy as np -import os -from datetime import datetime as dt -import warnings -import logging - - -# Plugin characteristics -# ---------------------- -format_name = 'Protochips' -description = 'Reads Protochips log files (heating/baising and gas cell)' -full_support = False -# Recognised file extension -file_extensions = ['csv', 'CSV'] -default_extension = 0 -# Reading capabilities -reads_images = False -reads_spectrum = False -reads_spectrum_image = False -# Writing capabilities -writes = False -non_uniform_axis = False -# ---------------------- - - -_logger = logging.getLogger(__name__) - - -# At some point, if there is another readerw, whith also use csv file, it will -# be necessary to mention the other reader in this message (and to add an -# argument in the load function to specify the correct reader) -invalid_file_error = "The Protochips csv reader can't import the file, please"\ - " make sure, this is a valid Protochips log file." - - -def file_reader(filename, *args, **kwds): - csv_file = ProtochipsCSV(filename) - return _protochips_log_reader(csv_file) - - -def _protochips_log_reader(csv_file): - csvs = [] - for key in csv_file.logged_quantity_name_list: - try: - csvs.append(csv_file.get_dictionary(key)) - except BaseException: - raise IOError(invalid_file_error) - return csvs - - -class ProtochipsCSV(object): - - def __init__(self, filename, ): - self.filename = filename - self._parse_header() - self._read_data() - - def _parse_header(self): - with open(self.filename, 'r') as f: - s = f.readline() - self.column_name = s.replace(', ', ',').replace('\n', '').split(',') - if not self._is_protochips_csv_file(): - raise IOError(invalid_file_error) - self._read_all_metadata_header(f) - self.logged_quantity_name_list = self.column_name[2:] - - def _is_protochips_csv_file(self): - # This check is not great, but it's better than nothing... - if 'Time' in self.column_name and 'Notes' in self.column_name and len( - self.column_name) >= 3: - return True - else: - return False - - def get_dictionary(self, quantity): - return {'data': self._data_dictionary[quantity], - 'axes': self._get_axes(), - 'metadata': self._get_metadata(quantity), - 'mapping': self._get_mapping(), - 'original_metadata': {'Protochips_header': - self._get_original_metadata()}} - - def _get_original_metadata(self): - d = {'Start time': self.start_datetime} - d['Time units'] = self.time_units - for quantity in self.logged_quantity_name_list: - d['%s_units' % quantity] = self._parse_quantity_units(quantity) - if self.user: - d['User'] = self.user - d['Calibration file path'] = self._parse_calibration_filepath() - d['Time axis'] = self._get_metadata_time_axis() - # Add the notes here, because there are not well formatted enough to - # go in metadata - d['Original notes'] = self._parse_notes() - return d - - def _get_metadata(self, quantity): - date, time = np.datetime_as_string(self.start_datetime).split('T') - return {'General': {'original_filename': os.path.split(self.filename)[1], - 'title': '%s (%s)' % (quantity, - self._parse_quantity_units(quantity)), - 'date': date, - 'time': time}, - "Signal": {'signal_type': '', - 'quantity': self._parse_quantity(quantity)}} - - def _get_mapping(self): - mapping = { - "Protochips_header.Calibration file path": ( - "General.notes", - self._parse_calibration_file_name), - "Protochips_header.User": ( - "General.authors", - None), - } - return mapping - - def _get_metadata_time_axis(self): - return {'value': self.time_axis, - 'units': self.time_units} - - def _read_data(self): - names = [name.replace(' ', '_') for name in self.column_name] - data = np.genfromtxt(self.filename, delimiter=',', dtype=None, - names=names, - skip_header=self.header_last_line_number, - encoding='latin1') - - self._data_dictionary = dict() - for i, name, name_dtype in zip(range(len(names)), self.column_name, - names): - if name == 'Notes': - self.notes = data[name_dtype].astype(str) - elif name == 'Time': - self.time_axis = data[name_dtype] - else: - self._data_dictionary[name] = data[name_dtype] - - def _parse_notes(self): - arr = np.vstack((self.time_axis, self.notes)) - return np.compress(arr[1] != '', arr, axis=1) - - def _parse_calibration_filepath(self): - # for the gas cell, the calibration is saved in the notes colunm - if hasattr(self, "calibration_file"): - calibration_file = self.calibration_file - else: - calibration_file = "The calibration files names are saved in the"\ - " 'Original notes' array of the original metadata." - return calibration_file - - def _parse_calibration_file_name(self, path): - basename = os.path.basename(path) - return "Calibration file name: %s" % basename.split('\\')[-1] - - def _get_axes(self): - scale = np.diff(self.time_axis[1:-1]).mean() - max_diff = np.diff(self.time_axis[1:-1]).max() - units = 's' - offset = 0 - if self.time_units == 'Milliseconds': - scale /= 1000 - max_diff /= 1000 - # Once we support non-uniform axis, don't forgot to update the - # documentation of the protochips reader - _logger.warning("The time axis is not uniform, the time step is " - "thus extrapolated to {0} {1}. The maximal step in time step is {2} {1}".format( - scale, units, max_diff)) - else: - warnings.warn("Time units not recognised, assuming second.") - - return [{'size': self.time_axis.shape[0], - 'index_in_array': 0, - 'name': 'Time', - 'scale': scale, - 'offset': offset, - 'units': units, - 'navigate': False - }] - - def _parse_quantity(self, quantity): - quantity_name = quantity.split(' ')[-1] - return '%s (%s)' % (quantity_name, - self._parse_quantity_units(quantity)) - - def _parse_quantity_units(self, quantity): - quantity = quantity.split(' ')[-1].lower() - return self.__dict__['%s_units' % quantity] - - def _read_all_metadata_header(self, f): - param, value = self._parse_metadata_header(f.readline()) - i = 2 - while 'User' not in param: # user should be the last of the header - if 'Calibration file' in param: - self.calibration_file = value - elif 'Date (yyyy.mm.dd)' in param: - date = value - elif 'Time (hh:mm:ss.ms)' in param: - time = value - else: - attr_name = param.replace(' ', '_').lower() - self.__dict__[attr_name] = value - i += 1 - try: - param, value = self._parse_metadata_header(f.readline()) - except ValueError: - # when the last line of header does not contain 'User', - # possibly some old file. - self.user = None - break - except IndexError: - _logger.warning("The metadata may not be parsed properly.") - break - else: - self.user = value - self.header_last_line_number = i - self.start_datetime = np.datetime64(dt.strptime(date + time, - "%Y.%m.%d%H:%M:%S.%f")) - - def _parse_metadata_header(self, line): - return line.replace(', ', ',').split(',')[1].split(' = ') diff --git a/hyperspy/io_plugins/ripple.py b/hyperspy/io_plugins/ripple.py deleted file mode 100644 index 0a1f040855..0000000000 --- a/hyperspy/io_plugins/ripple.py +++ /dev/null @@ -1,722 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -# for more information on the RPL/RAW format, see -# http://www.nist.gov/lispix/ -# and -# http://www.nist.gov/lispix/doc/image-file-formats/raw-file-format.htm - -import codecs -import os.path -from io import StringIO -import logging -import traits.api as t - -import numpy as np - -from hyperspy import Release -from hyperspy.misc.utils import DictionaryTreeBrowser - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = 'Ripple' -description = 'RPL file contains the information on how to read\n' -description += 'the RAW file with the same name.' -description += '\nThis format may not provide information on the calibration.' -description += '\nIf so, you should add that after loading the file.' -full_support = False # but maybe True -# Recognised file extension -file_extensions = ['rpl', 'RPL'] -default_extension = 0 -# Writing capabilities -writes = [(1, 0), (1, 1), (1, 2), (2, 0), (2, 1), ] -non_uniform_axis = False -# ---------------------- - -# The format only support the followng data types -newline = ('\n', '\r\n') -comment = ';' -sep = '\t' - -dtype2keys = { - 'float64': ('float', 8), - 'float32': ('float', 4), - 'uint8': ('unsigned', 1), - 'uint16': ('unsigned', 2), - 'int32': ('signed', 4), - 'int64': ('signed', 8), } - -endianess2rpl = { - '=': 'dont-care', - '<': 'little-endian', - '>': 'big-endian'} - -# Warning: for selection lists use tuples not lists. -rpl_keys = { - # spectrum/image keys - 'width': int, - 'height': int, - 'depth': int, - 'offset': int, - 'data-length': ('1', '2', '4', '8'), - 'data-type': ('signed', 'unsigned', 'float'), - 'byte-order': ('little-endian', 'big-endian', 'dont-care'), - 'record-by': ('image', 'vector', 'dont-care'), - # X-ray keys - 'ev-per-chan': float, # usually 5 or 10 eV - 'detector-peak-width-ev': float, # usually 150 eV - # HyperSpy-specific keys - 'depth-origin': float, - 'depth-scale': float, - 'depth-units': str, - 'width-origin': float, - 'width-scale': float, - 'width-units': str, - 'height-origin': float, - 'height-scale': float, - 'height-units': str, - 'signal': str, - # EELS HyperSpy keys - 'collection-angle': float, - # TEM HyperSpy keys - 'convergence-angle': float, - 'beam-energy': float, - # EDS HyperSpy keys - 'elevation-angle': float, - 'azimuth-angle': float, - 'live-time': float, - # From 0.8.5 energy-resolution is deprecated as it is a duplicate of - # detector-peak-width-ev of the ripple standard format. We keep it here - # to keep compatibility with rpl file written by HyperSpy < 0.8.4 - 'energy-resolution': float, - 'tilt-stage': float, - 'date': str, - 'time': str, - 'title': str, -} - - -def correct_INCA_format(fp): - fp_list = list() - fp.seek(0) - if '(' in fp.readline(): - for line in fp: - line = line.replace( - "(MLX::", - "").replace( - " : ", - "\t").replace( - " :", - "\t").replace( - " ", - "\t").lower().strip().replace( - ")", - "\n") - if "record-by" in line: - if "image" in line: - line = "record-by\timage" - if "vector" in line: - line = "record-by\tvector" - if "dont-care" in line: - line = "record-by\tdont-care" - fp_list.append(line) - fp = StringIO() - fp.writelines(fp_list) - fp.seek(0) - return fp - - -def parse_ripple(fp): - """Parse information from ripple (.rpl) file. - Accepts file object 'fp. Returns dictionary rpl_info. - """ - - fp = correct_INCA_format(fp) - - rpl_info = {} - for line in fp.readlines(): - # correct_brucker_format - line = line.replace('data-Length', 'data-length') - if line[:2] not in newline and line[0] != comment: - line = line.strip('\r\n') - if comment in line: - line = line[:line.find(comment)] - if sep not in line: - err = 'Separator in line "%s" is wrong, ' % line - err += 'it should be a ("\\t")' - raise IOError(err) - line = [seg.strip() for seg in line.split(sep)] # now it's a list - if (line[0] in rpl_keys) is True: - value_type = rpl_keys[line[0]] - if isinstance(value_type, tuple): # is selection list - if line[1] not in value_type: - err = \ - 'Wrong value for key %s.\n' \ - 'Value read is %s' \ - ' but it should be one of %s' % \ - (line[0], line[1], str(value_type)) - raise IOError(err) - else: - # rpl_keys[line[0]] must then be a type - line[1] = value_type(line[1]) - - rpl_info[line[0]] = line[1] - - if rpl_info['depth'] == 1 and rpl_info['record-by'] != 'dont-care': - err = '"depth" and "record-by" keys mismatch.\n' - err += '"depth" cannot be "1" if "record-by" is "dont-care" ' - err += 'and vice versa.' - err += 'Check %s' % fp.name - raise IOError(err) - if rpl_info['data-type'] == 'float' and int(rpl_info['data-length']) < 4: - err = '"data-length" for float "data-type" must be "4" or "8".\n' - err += 'Check %s' % fp.name - raise IOError(err) - if (rpl_info['data-length'] == '1' and - rpl_info['byte-order'] != 'dont-care'): - err = '"data-length" and "byte-order" mismatch.\n' - err += '"data-length" cannot be "1" if "byte-order" is not "dont-care"' - err += ' and vice versa.' - err += 'Check %s' % fp.name - raise IOError(err) - return rpl_info - - -def read_raw(rpl_info, fp, mmap_mode='c'): - """Read the raw file object 'fp' based on the information given in the - 'rpl_info' dictionary. - - Parameters - ---------- - rpl_info: dict - A dictionary containing the keywords as parsed by read_rpl - fp: - mmap_mode: {None, 'r+', 'r', 'w+', 'c'}, optional - If not None, then memory-map the file, using the given mode - (see `numpy.memmap`). The mode has no effect for pickled or - zipped files. - A memory-mapped array is stored on disk, and not directly loaded - into memory. However, it can be accessed and sliced like any - ndarray. Memory mapping is especially useful for accessing - small fragments of large files without reading the entire file - into memory. - - - """ - width = rpl_info['width'] - height = rpl_info['height'] - depth = rpl_info['depth'] - offset = rpl_info['offset'] - data_length = rpl_info['data-length'] - data_type = rpl_info['data-type'] - endian = rpl_info['byte-order'] - record_by = rpl_info['record-by'] - - if data_type == 'signed': - data_type = 'int' - elif data_type == 'unsigned': - data_type = 'uint' - elif data_type == 'float': - pass - else: - raise TypeError('Unknown "data-type" string.') - - if endian == 'big-endian': - endian = '>' - elif endian == 'little-endian': - endian = '<' - else: - endian = '=' - - data_type += str(int(data_length) * 8) - data_type = np.dtype(data_type) - data_type = data_type.newbyteorder(endian) - - data = np.memmap(fp, - offset=offset, - dtype=data_type, - mode=mmap_mode) - - if record_by == 'vector': # spectral image - size = (height, width, depth) - data = data.reshape(size) - elif record_by == 'image': # stack of images - size = (depth, height, width) - data = data.reshape(size) - elif record_by == 'dont-care': # stack of images - size = (height, width) - data = data.reshape(size) - return data - - -def file_reader(filename, rpl_info=None, encoding="latin-1", - mmap_mode='c', *args, **kwds): - """Parses a Lispix (http://www.nist.gov/lispix/) ripple (.rpl) file - and reads the data from the corresponding raw (.raw) file; - or, read a raw file if the dictionary rpl_info is provided. - - This format is often uses in EDS/EDX experiments. - - Images and spectral images or data cubes that are written in the - (Lispix) raw file format are just a continuous string of numbers. - - Data cubes can be stored image by image, or spectrum by spectrum. - Single images are stored row by row, vector cubes are stored row by row - (each row spectrum by spectrum), image cubes are stored image by image. - - All of the numbers are in the same format, such as 16 bit signed integer, - IEEE 8-byte real, 8-bit unsigned byte, etc. - - The "raw" file should be accompanied by text file with the same name and - ".rpl" extension. This file lists the characteristics of the raw file so - that it can be loaded without human intervention. - - Alternatively, dictionary 'rpl_info' containing the information can - be given. - - Some keys are specific to HyperSpy and will be ignored by other software. - - RPL stands for "Raw Parameter List", an ASCII text, tab delimited file in - which HyperSpy reads the image parameters for a raw file. - - ======================== ======= ================================================= - Key Type Description - ======================== ======= ================================================= - width int pixels per row - height int number of rows - depth int number of images or spectral pts - offset int bytes to skip - data-type str 'signed', 'unsigned', or 'float' - data-length str bytes per pixel '1', '2', '4', or '8' - byte-order str 'big-endian', 'little-endian', or 'dont-care' - record-by str 'image', 'vector', or 'dont-care' - # X-ray keys: - ev-per-chan int optional, eV per channel - detector-peak-width-ev int optional, FWHM for the Mn K-alpha line - # HyperSpy-specific keys - depth-origin int energy offset in pixels - depth-scale float energy scaling (units per pixel) - depth-units str energy units, usually eV - depth-name str Name of the magnitude stored as depth - width-origin int column offset in pixels - width-scale float column scaling (units per pixel) - width-units str column units, usually nm - width-name str Name of the magnitude stored as width - height-origin int row offset in pixels - height-scale float row scaling (units per pixel) - height-units str row units, usually nm - height-name str Name of the magnitude stored as height - signal str Type of the signal stored, e.g. EDS_SEM - convergence-angle float TEM convergence angle in mrad - collection-angle float EELS spectrometer collection semi-angle in mrad - beam-energy float TEM beam energy in keV - elevation-angle float Elevation angle of the EDS detector - azimuth-angle float Elevation angle of the EDS detector - live-time float Live time per spectrum - energy-resolution float Resolution of the EDS (FHWM of MnKa) - tilt-stage float The tilt of the stage - date str date in ISO 8601 - time str time in ISO 8601 - title str title of the signal to be stored - ======================== ======= ================================================= - - - NOTE - ---- - - When 'data-length' is 1, the 'byte order' is not relevant as there is only - one byte per datum, and 'byte-order' should be 'dont-care'. - - When 'depth' is 1, the file has one image, 'record-by' is not relevant and - should be 'dont-care'. For spectral images, 'record-by' is 'vector'. - For stacks of images, 'record-by' is 'image'. - - Floating point numbers can be IEEE 4-byte, or IEEE 8-byte. Therefore if - data-type is float, data-length MUST be 4 or 8. - - The rpl file is read in a case-insensitive manner. However, when providing - a dictionary as input, the keys MUST be lowercase. - - Comment lines, beginning with a semi-colon ';' are allowed anywhere. - - The first non-comment in the rpl file line MUST have two column names: - 'name_1''name_2'; any name would do e.g. 'key''value'. - - Parameters can be in ANY order. - - In the rpl file, the parameter name is followed by ONE tab (spaces are - ignored) e.g.: 'data-length''2' - - In the rpl file, other data and more tabs can follow the two items on - each row, and are ignored. - - Other keys and values can be included and are ignored. - - Any number of spaces can go along with each tab. - - """ - - if not rpl_info: - if filename[-3:] in file_extensions: - with codecs.open(filename, encoding=encoding, - errors='replace') as f: - rpl_info = parse_ripple(f) - else: - raise IOError('File has wrong extension: "%s"' % filename[-3:]) - for ext in ['raw', 'RAW']: - rawfname = filename[:-3] + ext - if os.path.exists(rawfname): - break - else: - rawfname = '' - if not rawfname: - raise IOError('RAW file "%s" does not exists' % rawfname) - else: - lazy = kwds.pop('lazy', False) - if lazy: - mmap_mode = 'r' - data = read_raw(rpl_info, rawfname, mmap_mode=mmap_mode) - - if rpl_info['record-by'] == 'vector': - _logger.info('Loading as Signal1D') - record_by = 'spectrum' - elif rpl_info['record-by'] == 'image': - _logger.info('Loading as Signal2D') - record_by = 'image' - else: - if len(data.shape) == 1: - _logger.info('Loading as Signal1D') - record_by = 'spectrum' - else: - _logger.info('Loading as Signal2D') - record_by = 'image' - - if rpl_info['record-by'] == 'vector': - idepth, iheight, iwidth = 2, 0, 1 - names = ['height', 'width', 'depth', ] - else: - idepth, iheight, iwidth = 0, 1, 2 - names = ['depth', 'height', 'width'] - - scales = [1, 1, 1] - origins = [0, 0, 0] - units = ['', '', ''] - sizes = [rpl_info[names[i]] for i in range(3)] - - if 'date' not in rpl_info: - rpl_info['date'] = "" - - if 'time' not in rpl_info: - rpl_info['time'] = "" - - if 'signal' not in rpl_info: - rpl_info['signal'] = "" - - if 'title' not in rpl_info: - rpl_info['title'] = "" - - if 'depth-scale' in rpl_info: - scales[idepth] = rpl_info['depth-scale'] - # ev-per-chan is the only calibration supported by the original ripple - # format - elif 'ev-per-chan' in rpl_info: - scales[idepth] = rpl_info['ev-per-chan'] - - if 'depth-origin' in rpl_info: - origins[idepth] = rpl_info['depth-origin'] - - if 'depth-units' in rpl_info: - units[idepth] = rpl_info['depth-units'] - - if 'depth-name' in rpl_info: - names[idepth] = rpl_info['depth-name'] - - if 'width-origin' in rpl_info: - origins[iwidth] = rpl_info['width-origin'] - - if 'width-scale' in rpl_info: - scales[iwidth] = rpl_info['width-scale'] - - if 'width-units' in rpl_info: - units[iwidth] = rpl_info['width-units'] - - if 'width-name' in rpl_info: - names[iwidth] = rpl_info['width-name'] - - if 'height-origin' in rpl_info: - origins[iheight] = rpl_info['height-origin'] - - if 'height-scale' in rpl_info: - scales[iheight] = rpl_info['height-scale'] - - if 'height-units' in rpl_info: - units[iheight] = rpl_info['height-units'] - - if 'height-name' in rpl_info: - names[iheight] = rpl_info['height-name'] - - mp = DictionaryTreeBrowser({ - 'General': {'original_filename': os.path.split(filename)[1], - 'date': rpl_info['date'], - 'time': rpl_info['time'], - 'title': rpl_info['title'] - }, - "Signal": {'signal_type': rpl_info['signal'], - 'record_by': record_by}, - }) - if 'convergence-angle' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.convergence_angle', - rpl_info['convergence-angle']) - if 'tilt-stage' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.Stage.tilt_alpha', - rpl_info['tilt-stage']) - if 'collection-angle' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.Detector.EELS.' + - 'collection_angle', - rpl_info['collection-angle']) - if 'beam-energy' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.beam_energy', - rpl_info['beam-energy']) - if 'elevation-angle' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.Detector.EDS.elevation_angle', - rpl_info['elevation-angle']) - if 'azimuth-angle' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.Detector.EDS.azimuth_angle', - rpl_info['azimuth-angle']) - if 'energy-resolution' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.Detector.EDS.' + - 'energy_resolution_MnKa', - rpl_info['energy-resolution']) - if 'detector-peak-width-ev' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.Detector.EDS.' + - 'energy_resolution_MnKa', - rpl_info['detector-peak-width-ev']) - if 'live-time' in rpl_info: - mp.set_item('Acquisition_instrument.TEM.Detector.EDS.live_time', - rpl_info['live-time']) - - units = [t.Undefined if unit == '' else unit for unit in units] - - axes = [] - index_in_array = 0 - for i in range(3): - if sizes[i] > 1: - axes.append({ - 'size': sizes[i], - 'index_in_array': index_in_array, - 'name': names[i], - 'scale': scales[i], - 'offset': origins[i], - 'units': units[i], - }) - index_in_array += 1 - - dictionary = { - 'data': data.squeeze(), - 'axes': axes, - 'metadata': mp.as_dictionary(), - 'original_metadata': rpl_info - } - return [dictionary, ] - - -def file_writer(filename, signal, encoding='latin-1', *args, **kwds): - - # Set the optional keys to None - ev_per_chan = None - - # Check if the dtype is supported - dc = signal.data - dtype_name = signal.data.dtype.name - if dtype_name not in dtype2keys.keys(): - err = 'The ripple format does not support writting data of %s type' % ( - dtype_name) - raise IOError(err) - # Check if the dimensions are supported - dimension = len(signal.data.shape) - if dimension > 3: - err = 'This file format does not support %i dimension data' % ( - dimension) - raise IOError(err) - - # Gather the information to write the rpl - data_type, data_length = dtype2keys[dc.dtype.name] - byte_order = endianess2rpl[dc.dtype.byteorder.replace('|', '=')] - offset = 0 - if signal.metadata.has_item("Signal.signal_type"): - signal_type = signal.metadata.Signal.signal_type - else: - signal_type = "" - if signal.metadata.has_item("General.date"): - date = signal.metadata.General.date - else: - date = "" - if signal.metadata.has_item("General.time"): - time = signal.metadata.General.time - else: - time = "" - if signal.metadata.has_item("General.title"): - title = signal.metadata.General.title - else: - title = "" - if signal.axes_manager.signal_dimension == 1: - record_by = 'vector' - depth_axis = signal.axes_manager.signal_axes[0] - ev_per_chan = int(round(depth_axis.scale)) - if dimension == 3: - width_axis = signal.axes_manager.navigation_axes[0] - height_axis = signal.axes_manager.navigation_axes[1] - depth, width, height = \ - depth_axis.size, width_axis.size, height_axis.size - elif dimension == 2: - width_axis = signal.axes_manager.navigation_axes[0] - depth, width, height = depth_axis.size, width_axis.size, 1 - elif dimension == 1: - record_by == 'dont-care' - depth, width, height = depth_axis.size, 1, 1 - - elif signal.axes_manager.signal_dimension == 2: - width_axis = signal.axes_manager.signal_axes[0] - height_axis = signal.axes_manager.signal_axes[1] - if dimension == 3: - depth_axis = signal.axes_manager.navigation_axes[0] - record_by = 'image' - depth, width, height = \ - depth_axis.size, width_axis.size, height_axis.size - elif dimension == 2: - record_by = 'dont-care' - width, height, depth = width_axis.size, height_axis.size, 1 - elif dimension == 1: - record_by = 'dont-care' - depth, width, height = width_axis.size, 1, 1 - else: - _logger.info("Only Signal1D and Signal2D objects can be saved") - return - - # Fill the keys dictionary - keys_dictionary = { - 'width': width, - 'height': height, - 'depth': depth, - 'offset': offset, - 'data-type': data_type, - 'data-length': data_length, - 'byte-order': byte_order, - 'record-by': record_by, - 'signal': signal_type, - 'date': date, - 'time': time, - 'title': title - } - if ev_per_chan is not None: - keys_dictionary['ev-per-chan'] = ev_per_chan - keys = ['depth', 'height', 'width'] - for key in keys: - if eval(key) > 1: - keys_dictionary['%s-scale' % key] = eval( - '%s_axis.scale' % key) - keys_dictionary['%s-origin' % key] = eval( - '%s_axis.offset' % key) - keys_dictionary['%s-units' % key] = eval( - '%s_axis.units' % key) - keys_dictionary['%s-name' % key] = eval( - '%s_axis.name' % key) - if signal.metadata.Signal.signal_type == "EELS": - if "Acquisition_instrument.TEM" in signal.metadata: - mp = signal.metadata.Acquisition_instrument.TEM - if mp.has_item('beam_energy'): - keys_dictionary['beam-energy'] = mp.beam_energy - if mp.has_item('convergence_angle'): - keys_dictionary['convergence-angle'] = mp.convergence_angle - if mp.has_item('Detector.EELS.collection_angle'): - keys_dictionary[ - 'collection-angle'] = mp.Detector.EELS.collection_angle - if "EDS" in signal.metadata.Signal.signal_type: - if signal.metadata.Signal.signal_type == "EDS_SEM": - mp = signal.metadata.Acquisition_instrument.SEM - elif signal.metadata.Signal.signal_type == "EDS_TEM": - mp = signal.metadata.Acquisition_instrument.TEM - if mp.has_item('beam_energy'): - keys_dictionary['beam-energy'] = mp.beam_energy - if mp.has_item('Detector.EDS.elevation_angle'): - keys_dictionary[ - 'elevation-angle'] = mp.Detector.EDS.elevation_angle - if mp.has_item('Stage.tilt_alpha'): - keys_dictionary['tilt-stage'] = mp.Stage.tilt_alpha - if mp.has_item('Detector.EDS.azimuth_angle'): - keys_dictionary['azimuth-angle'] = mp.Detector.EDS.azimuth_angle - if mp.has_item('Detector.EDS.live_time'): - keys_dictionary['live-time'] = mp.Detector.EDS.live_time - if mp.has_item('Detector.EDS.energy_resolution_MnKa'): - keys_dictionary[ - 'detector-peak-width-ev'] = \ - mp.Detector.EDS.energy_resolution_MnKa - - write_rpl(filename, keys_dictionary, encoding) - write_raw(filename, signal, record_by) - - -def write_rpl(filename, keys_dictionary, encoding='ascii'): - f = codecs.open(filename, 'w', encoding=encoding, - errors='ignore') - f.write(';File created by HyperSpy version %s\n' % Release.version) - f.write('key\tvalue\n') - # Even if it is not necessary, we sort the keywords when writing - # to make the rpl file more human friendly - for key, value in iter(sorted(keys_dictionary.items())): - if not isinstance(value, str): - value = str(value) - f.write(key + '\t' + value + '\n') - f.close() - - -def write_raw(filename, signal, record_by): - """Writes the raw file object - - Parameters - ---------- - filename : str - the filename, either with the extension or without it - record_by : str - 'vector' or 'image' - - """ - filename = os.path.splitext(filename)[0] + '.raw' - dshape = signal.data.shape - data = signal.data - if len(dshape) == 3: - if record_by == 'vector': - np.rollaxis( - data, signal.axes_manager.signal_axes[0].index_in_array, 3 - ).ravel().tofile(filename) - elif record_by == 'image': - data = np.rollaxis( - data, signal.axes_manager.navigation_axes[0].index_in_array, 0 - ).ravel().tofile(filename) - elif len(dshape) == 2: - if record_by == 'vector': - np.rollaxis( - data, signal.axes_manager.signal_axes[0].index_in_array, 2 - ).ravel().tofile(filename) - elif record_by in ('image', 'dont-care'): - data.ravel().tofile(filename) - elif len(dshape) == 1: - data.ravel().tofile(filename) diff --git a/hyperspy/io_plugins/semper_unf.py b/hyperspy/io_plugins/semper_unf.py deleted file mode 100644 index d02656490c..0000000000 --- a/hyperspy/io_plugins/semper_unf.py +++ /dev/null @@ -1,713 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -# INFORMATION ABOUT THE SEMPER FORMAT LABEL: -# Picture labels consist of a sequence of bytes -# --------------------------------------------- -# v61:256B | v7:at least 256B, rounded up to multiple of block size 64, -# with max set by LNLAB in params.f -# The versions have the same contents for the first 256B, as set out below, -# and referred to as the label 'base'; beyond this, in the label 'extension', -# the structure is deliberately undefined, allowing you to make your own use -# of the additional storage. -# -# From Aug14: -# B1-6 S e m p e r (ic chars) -# 7,8 ncol msb,lsb (BIG-ended) -# 9,10 nrow msb,lsb -# 11,12 nlay msb,lsb -# 13,14 ccol msb,lsb -# 15,16 crow msb,lsb -# 17,18 clay msb,lsb -# 19 class: 1-20 -# for image,macro,fou,spec,correln,undef,walsh,histog,plist,lut -# 20 form: 0,1,2,3,4 = byte,i*2,fp,com,i*4 from Aug08 -# 21 wp: non-zero if prot -# 22-27 creation year(-1900?),month,day,hour,min,sec -# 28 v61|v7 # chars in range record | 255 -# 29-55 v61: min,max values present (ic chars for decimal repn) -# v7: min,max vals as two Fp values in B29-36 (LE ordered) -# followed by 19 unused bytes B37-55 -# 56 plist type: 1,2,3 = list,opencurve,closedcurve -# acc to EXAMPD - code appears to use explicit numbers -# 57,58,59 ncol,nrow,nlay hsb -# 60,61,62 ccol,crow,clay hsb -# 63 RealCoords flag (non-zero -> DX,DY,DZ,X0,Y0,Z0,units held as below) -# 64 v61:0 | v7: # blocks in (this) picture label (incl extn) -# 65-67 0 (free at present) -# 68-71 DATA cmd V7 4-byte Fp values, order LITTLE-ended -# 72-75 DATA cmd V6 -# 76-79 RealCoord DZ / V5 RealCoord pars as 4-byte Fp values, LITTLE-ended -# 80-83 ... Z0 / V4 -# 84-87 ... DY / V3 -# 88-91 ... Y0 / V2 -# 92-95 ... DX / V1 -# 96-99 ... X0 / V0 -# 100 # chars in title -# 101-244 title (ic chars) -# 245-248 RealCoord X unit (4 ic chars) -# 249-252 RealCoord Y unit (4 ic chars) -# 253-256 RealCoord Z unit (4 ic chars) -# -# Aug08-Aug14 labels held no RealCoord information, so: -# B63 'free' and zero - flagging absence of RealCoord information -# 101-256 title (12 chars longer than now) -# -# Before Aug08 labels held no hsb for pic sizes, so: -# 57-99 all free/zero except for use by DATA cmd -# 101-256 title (ic chars) - -from collections import OrderedDict -import struct -from functools import partial -import logging -import warnings -from datetime import datetime - -import numpy as np -from traits.api import Undefined - -from hyperspy.misc.array_tools import sarray2dict - - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = 'SEMPER UNF (unformatted)' -description = 'Read data from SEMPER UNF files.' -full_support = True # Hopefully? -# Recognised file extension -file_extensions = ('unf', 'UNF') -default_extension = 0 -# Writing capabilities -writes = [(1, 0), (1, 1), (1, 2), (2, 0), (2, 1)] # All up to 3D -non_uniform_axis = False -# ---------------------- - - -class SemperFormat(object): - - """Class for importing and exporting SEMPER `.unf`-files. - - The :class:`~.SemperFormat` class represents a SEMPER binary file format - with a header, which holds additional information. `.unf`-files can be - saved and read from files. - - Attributes - ---------- - data : :class:`~numpy.ndarray` (N=3) - The phase map or magnetization information in a 3D array (with one - slice). - title : string - Title of the file (not to be confused with the filename). - offsets : tuple (N=3) of floats - Offset shifts (in nm) of the grid origin (does not have to start at 0) - in x, y, z. - scales : tuple (N=3) of floats - Grid spacing (nm per pixel) in x, y, z. - units : tuple (N=3) of strings - Units of the grid in x, y, z. - metadata : dictionary - A dictionary of all flags and metadata present in the `.unf`-file. - - """ - - ICLASS_DICT = {1: 'image', 2: 'macro', 3: 'fourier', 4: 'spectrum', - 5: 'correlation', 6: Undefined, 7: 'walsh', - 8: 'position list', 9: 'histogram', - 10: 'display look-up table'} - - ICLASS_DICT_INV = {v: k for k, v in ICLASS_DICT.items()} - - IFORM_DICT = { - 0: np.byte, - 1: np.int16, - 2: np.float32, - 3: np.complex64, - 4: np.int32} - - IFORM_DICT_INV = {v: k for k, v in IFORM_DICT.items()} - - HEADER_DTYPES = [('NCOL', ' 0: - assert np.fromfile( - f, - dtype=' 0: - f.write( - struct.pack( - '. - -# Plugin to read the mountainsmap surface format (sur) -#Current state can bring support to the surface format if the file is an -#attolight hyperspectral map, but cannot bring write nor support for other -#mountainsmap files (.pro etc.). I need to write some tests, check whether the -#comments can be systematically parsed into metadata and write a support for -#original_metadata or other - -import logging -#Dateutil allows to parse date but I don't think it's useful here -#import dateutil.parser - -import numpy as np -#Commented for now because I don't know what purpose it serves -#import traits.api as t - -from copy import deepcopy -import struct -import sys -import zlib -import os -import warnings - -#Maybe later we can implement reading the class with the io utils tools instead -#of re-defining read functions in the class -#import hyperspy.misc.io.utils_readfile as iou - -#This module will prove useful when we write the export function -#import hyperspy.misc.io.tools - -#DictionaryTreeBrowser class handles the fancy metadata dictionnaries -#from hyperspy.misc.utils import DictionaryTreeBrowser -from hyperspy.exceptions import MountainsMapFileError - -_logger = logging.getLogger(__name__) - -# Plugin characteristics -# ---------------------- -format_name = 'Digital Surf Surface sur' -description = """Read data from the proprietary .sur file format from Digital -Surf. Allows hyperspy to interact with the mountains map software""" -full_support = False #Check with the boys once this is done -# Recognised file extension -file_extensions = ('sur', 'SUR','pro','PRO') -default_extension = 0 -# Writing features -writes = False #First we will check with the load -non_uniform_axis = False -# ---------------------- - -class DigitalSurfHandler(object): - """ Class to read Digital Surf MountainsMap files. - - Attributes - ---------- - filename, signal_dict, _work_dict, _list_sur_file_content, _Object_type, - _N_data_object, _N_data_channels, _initialized - - Methods - ------- - parse_file, parse_header, get_image_dictionaries - - Class Variables - --------------- - _object_type : dict key: int containing the mountainsmap object types - - """ - #Object types - _mountains_object_types = { - -1: "_ERROR" , - 0: "_UNKNOWN" , - 1: "_PROFILE" , - 2: "_SURFACE" , - 3: "_BINARYIMAGE" , - 4: "_PROFILESERIE" , - 5: "_SURFACESERIE" , - 6: "_MERIDIANDISC" , - 7: "_MULTILAYERPROFILE" , - 8: "_MULTILAYERSURFACE" , - 9: "_PARALLELDISC" , - 10: "_INTENSITYIMAGE" , - 11: "_INTENSITYSURFACE" , - 12: "_RGBIMAGE" , - 13: "_RGBSURFACE" , - 14: "_FORCECURVE" , - 15: "_SERIEOFFORCECURVE" , - 16: "_RGBINTENSITYSURFACE", - 20: "_SPECTRUM" , - 21: "_HYPCARD" , - } - - def __init__(self, filename=None): - - #We do not need to check for file existence here because - #io module implements it in the load function - self.filename = filename - - #The signal_dict dictionnary has to be returned by the - #file_reader function. Apparently original_metadata needs - #to be set - self.signal_dict = {'data': np.empty((0,0,0)), - 'axes': [], - 'metadata': {}, - 'original_metadata': {} - } - - #Dictionary to store, read and write fields in the binary file - #defined in the MountainsMap SDK. Structure is - # _work_dict['Field']['value'] : access field value - # _work_dict['Field']['b_unpack_fn'](f) : unpack value from file f - # _work_dict['Field']['b_pack_fn'](f,v): pack value v in file f - self._work_dict = \ - { - "_01_Signature": - { - 'value':"DSCOMPRESSED", - 'b_unpack_fn': lambda f: self._get_str(f,12,"DSCOMPRESSED"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,12), - }, - "_02_Format": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_03_Number_of_Objects": - { - 'value':1, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_04_Version": - { - 'value':1, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_05_Object_Type": - { - 'value':2, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_06_Object_Name": - { - 'value':"", - 'b_unpack_fn': lambda f: self._get_str(f,30,"DOSONLY"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,30), - }, - "_07_Operator_Name": - { - 'value':"", - 'b_unpack_fn': lambda f: self._get_str(f,30,""), - 'b_pack_fn': lambda f,v: self._set_str(f,v,30), - }, - "_08_P_Size": - { - 'value':1, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_09_Acquisition_Type": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_10_Range_Type": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_11_Special_Points": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_12_Absolute": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_13_Gauge_Resolution": - { - 'value':0.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_14_W_Size": - { - 'value':0, - 'b_unpack_fn': self._get_int32, - 'b_pack_fn': self._set_int32, - }, - "_15_Size_of_Points": - { - 'value':16, - 'b_unpack_fn':lambda f: self._get_int16(f,32), - 'b_pack_fn': self._set_int16, - }, - "_16_Zmin": - { - 'value':0, - 'b_unpack_fn':self._get_int32, - 'b_pack_fn':self._set_int32, - }, - "_17_Zmax": - { - 'value':0, - 'b_unpack_fn':self._get_int32, - 'b_pack_fn': self._set_int32, - }, - "_18_Number_of_Points": - { - 'value':0, - 'b_unpack_fn': self._get_int32, - 'b_pack_fn': self._set_int32, - }, - "_19_Number_of_Lines": - { - 'value':0, - 'b_unpack_fn':self._get_int32, - 'b_pack_fn':self._set_int32, - }, - "_20_Total_Nb_of_Pts": - { - 'value':0, - 'b_unpack_fn': self._get_int32, - 'b_pack_fn': self._set_int32 - }, - "_21_X_Spacing": - { - 'value':1.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_22_Y_Spacing": - { - 'value':1.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_23_Z_Spacing": - { - 'value':1.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_24_Name_of_X_Axis": - { - 'value':'X', - 'b_unpack_fn': lambda f: self._get_str(f,16,"X"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_25_Name_of_Y_Axis": - { - 'value':'Y', - 'b_unpack_fn': lambda f: self._get_str(f,16,"Y"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_26_Name_of_Z_Axis": - { - 'value':'Z', - 'b_unpack_fn': lambda f: self._get_str(f,16,"Z"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_27_X_Step_Unit": - { - 'value':'um', - 'b_unpack_fn': lambda f: self._get_str(f,16,"um"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_28_Y_Step_Unit": - { - 'value':'um', - 'b_unpack_fn': lambda f: self._get_str(f,16,"um"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_29_Z_Step_Unit": - { - 'value':'um', - 'b_unpack_fn': lambda f: self._get_str(f,16,"um"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_30_X_Length_Unit": - { - 'value':'um', - 'b_unpack_fn': lambda f: self._get_str(f,16,"um"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_31_Y_Length_Unit": - { - 'value':'um', - 'b_unpack_fn': lambda f: self._get_str(f,16,"um"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_32_Z_Length_Unit": - { - 'value':'um', - 'b_unpack_fn': lambda f: self._get_str(f,16,"um"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,16), - }, - "_33_X_Unit_Ratio": - { - 'value':1.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_34_Y_Unit_Ratio": - { - 'value':1.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_35_Z_Unit_Ratio": - { - 'value':1.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_36_Imprint": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_37_Inverted": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_38_Levelled": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_39_Obsolete": - { - 'value':0, - 'b_unpack_fn': lambda f: self._get_bytes(f,12), - 'b_pack_fn': lambda f,v: self._set_bytes(f,v,12), - }, - "_40_Seconds": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_41_Minutes": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_42_Hours": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_43_Day": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_44_Month": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_45_Year": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_46_Day_of_week": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_47_Measurement_duration": - { - 'value':0.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_48_Compressed_data_size": - { - 'value':0, - 'b_unpack_fn':self._get_uint32, - 'b_pack_fn':self._set_uint32, - }, - "_49_Obsolete": - { - 'value':0, - 'b_unpack_fn': lambda f: self._get_bytes(f,6), - 'b_pack_fn': lambda f,v: self._set_bytes(f,v,6), - }, - "_50_Comment_size": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_51_Private_size": - { - 'value':0, - 'b_unpack_fn': self._get_int16, - 'b_pack_fn': self._set_int16, - }, - "_52_Client_zone": - { - 'value':0, - 'b_unpack_fn': lambda f: self._get_bytes(f,128), - 'b_pack_fn': lambda f,v: self._set_bytes(f,v,128), - }, - "_53_X_Offset": - { - 'value':0.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_54_Y_Offset": - { - 'value':0.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_55_Z_Offset": - { - 'value':0.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_56_T_Spacing":\ - { - 'value':0.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_57_T_Offset": - { - 'value':0.0, - 'b_unpack_fn': self._get_float, - 'b_pack_fn': self._set_float, - }, - "_58_T_Axis_Name": - { - 'value':'T', - 'b_unpack_fn': lambda f: self._get_str(f,13,"Wavelength"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,13), - }, - "_59_T_Step_Unit": - { - 'value':'um', - 'b_unpack_fn': lambda f: self._get_str(f,13,"nm"), - 'b_pack_fn': lambda f,v: self._set_str(f,v,13), - }, - "_60_Comment": - { - 'value':0, - 'b_unpack_fn': self._unpack_comment, - 'b_pack_fn': self._pack_comment, - }, - "_61_Private_zone": - { - 'value':0, - 'b_unpack_fn': self._unpack_private, - 'b_pack_fn': self._pack_private, - }, - "_62_points": - { - 'value':0, - 'b_unpack_fn': self._unpack_data, - 'b_pack_fn': lambda f,v: 0, #Not implemented - }, - } - - #List of all measurement - self._list_sur_file_content = [] - - #The surface files convention is that when saving multiple data - #objects at once, they are all packed in the same binary file. - #Every single object contains a full header with all the sections, - # but only the first one contains the relevant infos about - #object type, the number of objects in the file and other. - #Hence they will be made attributes. - #Object type - self._Object_type = "_UNKNOWN" - - #Number of data objects in the file. - self._N_data_object = 1 - self._N_data_channels = 1 - - ### Read methods - def _read_sur_file(self): - """Read the binary, possibly compressed, content of the surface - file. Surface files can be encoded as single or a succession - of objects. The file is thus read iteratively and from metadata of the - first file """ - - with open(self.filename,'rb') as f: - #We read the first object - self._read_single_sur_object(f) - #We append the first object to the content list - self._append_work_dict_to_content() - #Lookup how many objects are stored in the file and save - self._N_data_object = self._get_work_dict_key_value("_03_Number_of_Objects") - self._N_data_channels = self._get_work_dict_key_value('_08_P_Size') - - #Determine how many objects we need to read - if self._N_data_channels>0 and self._N_data_object>0: - N_objects_to_read = self._N_data_channels*self._N_data_object - elif self._N_data_channels>0: - N_objects_to_read = self._N_data_channels - elif self._N_data_object>0: - N_objects_to_read = self._N_data_object - else: - N_objects_to_read = 1 - - #Lookup what object type we are dealing with and save - self._Object_type = \ - DigitalSurfHandler._mountains_object_types[ \ - self._get_work_dict_key_value("_05_Object_Type")] - - #if more than 1 - if N_objects_to_read > 1: - #continue reading until everything is done - for i in range(1,N_objects_to_read): - #We read an object - self._read_single_sur_object(f) - #We append it to content list - self._append_work_dict_to_content() - - def _read_single_sur_object(self,file): - for key,val in self._work_dict.items(): - self._work_dict[key]['value'] = val['b_unpack_fn'](file) - - def _append_work_dict_to_content(self): - """Save the values stored in the work dict in the surface file list""" - datadict = deepcopy( \ - {key:val['value'] for key,val in self._work_dict.items()}) - self._list_sur_file_content.append(datadict) - - def _get_work_dict_key_value(self,key): - return self._work_dict[key]['value'] - - ### Signal dictionary methods - def _build_sur_dict(self): - """Create a signal dict with an unpacked object""" - - #If the signal is of the type spectrum or hypercard - if self._Object_type in ["_HYPCARD",]: - self._build_hyperspectral_map() - elif self._Object_type in ["_SPECTRUM"]: - self._build_spectrum() - elif self._Object_type in ["_PROFILE"]: - self._build_general_1D_data() - elif self._Object_type in ["_PROFILESERIE"]: - self._build_1D_series() - elif self._Object_type in ["_SURFACE"]: - self._build_surface() - elif self._Object_type in ["_SURFACESERIE"]: - self._build_surface_series() - elif self._Object_type in ["_MULTILAYERSURFACE"]: - self._build_surface_series() - elif self._Object_type in ["_RGBSURFACE"]: - self._build_RGB_surface() - elif self._Object_type in ["_RGBIMAGE"]: - self._build_RGB_image() - elif self._Object_type in ["_RGBINTENSITYSURFACE"]: - self._build_RGB_surface() - elif self._Object_type in ["_BINARYIMAGE"]: - self._build_surface() - else: - raise MountainsMapFileError(self._Object_type \ - + "is not a supported mountain object.") - - return self.signal_dict - - def _build_Xax(self,unpacked_dict,ind=0,nav=False,binned=False): - """Return X axis dictionary from an unpacked dict. index int and navigate - boolean can be optionally passed. Default 0 and False respectively.""" - Xax = { 'name': unpacked_dict['_24_Name_of_X_Axis'], - 'size': unpacked_dict['_18_Number_of_Points'], - 'index_in_array': ind, - 'scale': unpacked_dict['_21_X_Spacing'], - 'offset': unpacked_dict['_53_X_Offset'], - 'units': unpacked_dict['_27_X_Step_Unit'], - 'navigate':nav, - 'is_binned':binned, - } - return Xax - - def _build_Yax(self,unpacked_dict,ind=1,nav=False,binned=False): - """Return X axis dictionary from an unpacked dict. index int and navigate - boolean can be optionally passed. Default 1 and False respectively.""" - Yax = { 'name': unpacked_dict['_25_Name_of_Y_Axis'], - 'size': unpacked_dict['_19_Number_of_Lines'], - 'index_in_array': ind, - 'scale': unpacked_dict['_22_Y_Spacing'], - 'offset': unpacked_dict['_54_Y_Offset'], - 'units': unpacked_dict['_28_Y_Step_Unit'], - 'navigate':nav, - 'is_binned':binned, - } - return Yax - - def _build_Tax(self,unpacked_dict,size_key,ind=0,nav=True,binned=False): - """Return T axis dictionary from an unpacked surface object dict. - Unlike x and y axes, the size key can be determined from various keys: - _14_W_Size, _15_Size_of_Points or _03_Number_of_Objects. index int - and navigate boolean can be optionally passed. Default 0 and - True respectively.""" - - #The T axis is somewhat special because it is only defined on series - #and is thus only navigation. It is only defined on the first object - #in a serie. - #Here it needs to be checked that the T axis scale is not 0 Otherwise - #it raises hyperspy errors - scale = unpacked_dict['_56_T_Spacing'] - if scale == 0: - scale =1 - - Tax = { 'name': unpacked_dict['_58_T_Axis_Name'], - 'size': unpacked_dict[size_key], - 'index_in_array': ind, - 'scale': scale, - 'offset': unpacked_dict['_57_T_Offset'], - 'units': unpacked_dict['_59_T_Step_Unit'], - 'navigate':nav, - 'is_binned':binned, - } - return Tax - - ### Build methods for individual surface objects - def _build_hyperspectral_map(self,): - """Build a hyperspectral map. Hyperspectral maps are single-object - files with datapoints of _14_W_Size length""" - - #Check that the object contained only one object. - #Probably overkill at this point but better safe than sorry - if len(self._list_sur_file_content) != 1: - raise MountainsMapFileError("Input {:s} File is not of Hyperspectral type".format(self._Object_type)) - - #We get the dictionary with all the data - hypdic = self._list_sur_file_content[0] - - #Add all the axes to the signal dict - self.signal_dict['axes'].append(\ - self._build_Yax(hypdic,ind=0,nav=True)) - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=1,nav=True)) - #Wavelength axis in hyperspectral surface files are stored as T Axis - #with length set as _14_W_Size - self.signal_dict['axes'].append(\ - self._build_Tax(hypdic,'_14_W_Size',ind=2,nav=False)) - - #We reshape the data in the correct format - self.signal_dict['data'] = hypdic['_62_points'].reshape(\ - hypdic['_19_Number_of_Lines'], - hypdic['_18_Number_of_Points'], - hypdic['_14_W_Size'], - ) - - self.signal_dict['metadata'] = self._build_metadata(hypdic) - - self.signal_dict['original_metadata'] = self._build_original_metadata() - - def _build_general_1D_data(self,): - """Build general 1D Data objects. Currently work with spectra""" - - #Check that the object contained only one object. - #Probably overkill at this point but better safe than sorry - if len(self._list_sur_file_content) != 1: - raise MountainsMapFileError("Corrupt file") - - #We get the dictionary with all the data - hypdic = self._list_sur_file_content[0] - - #Add the axe to the signal dict - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=0,nav=False)) - - #We reshape the data in the correct format - self.signal_dict['data'] = hypdic['_62_points'] - - #Build the metadata - self._set_metadata_and_original_metadata(hypdic) - - def _build_spectrum(self,): - """Build spectra objects. Spectra and 1D series of spectra are - saved in the same object.""" - - #We get the dictionary with all the data - hypdic = self._list_sur_file_content[0] - - #Add the signal axis_src to the signal dict - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=1,nav=False)) - - #If there is more than 1 spectrum also add the navigation axis - if hypdic['_19_Number_of_Lines'] != 1: - self.signal_dict['axes'].append(\ - self._build_Yax(hypdic,ind=0,nav=True)) - - #We reshape the data in the correct format. - #Edit: the data is now squeezed for unneeded dimensions - self.signal_dict['data'] = np.squeeze(hypdic['_62_points'].reshape(\ - hypdic['_19_Number_of_Lines'], - hypdic['_18_Number_of_Points'], - )) - - self._set_metadata_and_original_metadata(hypdic) - - def _build_1D_series(self,): - """Build a series of 1D objects. The T axis is navigation and set from - the first object""" - - #First object dictionary - hypdic = self._list_sur_file_content[0] - - #Metadata are set from first dictionary - self._set_metadata_and_original_metadata(hypdic) - - #Add the series-axis to the signal dict - self.signal_dict['axes'].append(\ - self._build_Tax(hypdic,'_03_Number_of_Objects',ind=0,nav=True)) - - #All objects must share the same signal axis - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=1,nav=False)) - - #We put all the data together - data = [] - for obj in self._list_sur_file_content: - data.append(obj['_62_points']) - - self.signal_dict['data'] = np.stack(data) - - def _build_surface(self,): - """Build a surface""" - - #Check that the object contained only one object. - #Probably overkill at this point but better safe than sorry - if len(self._list_sur_file_content) != 1: - raise MountainsMapFileError("CORRUPT {:s} FILE".format(self._Object_type)) - - #We get the dictionary with all the data - hypdic = self._list_sur_file_content[0] - - #Add all the axes to the signal dict - self.signal_dict['axes'].append(\ - self._build_Yax(hypdic,ind=0,nav=False)) - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=1,nav=False)) - - - - #We reshape the data in the correct format - shape = (hypdic['_19_Number_of_Lines'],hypdic['_18_Number_of_Points']) - self.signal_dict['data'] = hypdic['_62_points'].reshape(shape) - - self._set_metadata_and_original_metadata(hypdic) - - def _build_surface_series(self,): - """Build a series of surfaces. The T axis is navigation and set from - the first object""" - - #First object dictionary - hypdic = self._list_sur_file_content[0] - - #Metadata are set from first dictionary - self._set_metadata_and_original_metadata(hypdic) - - #Add the series-axis to the signal dict - self.signal_dict['axes'].append(\ - self._build_Tax(hypdic,'_03_Number_of_Objects',ind=0,nav=True)) - - #All objects must share the same signal axes - self.signal_dict['axes'].append(\ - self._build_Yax(hypdic,ind=1,nav=False)) - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=2,nav=False)) - - #shape of the surfaces in the series - shape = (hypdic['_19_Number_of_Lines'],hypdic['_18_Number_of_Points']) - #We put all the data together - data = [] - for obj in self._list_sur_file_content: - data.append(obj['_62_points'].reshape(shape)) - - self.signal_dict['data'] = np.stack(data) - - def _build_RGB_surface(self,): - """Build a series of surfaces. The T axis is navigation and set from - P Size""" - - #First object dictionary - hypdic = self._list_sur_file_content[0] - - #Metadata are set from first dictionary - self._set_metadata_and_original_metadata(hypdic) - - #Add the series-axis to the signal dict - self.signal_dict['axes'].append(\ - self._build_Tax(hypdic,'_08_P_Size',ind=0,nav=True)) - - #All objects must share the same signal axes - self.signal_dict['axes'].append(\ - self._build_Yax(hypdic,ind=1,nav=False)) - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=2,nav=False)) - - #shape of the surfaces in the series - shape = (hypdic['_19_Number_of_Lines'],hypdic['_18_Number_of_Points']) - #We put all the data together - data = [] - for obj in self._list_sur_file_content: - data.append(obj['_62_points'].reshape(shape)) - - #Pushing data into the dictionary - self.signal_dict['data'] = np.stack(data) - - def _build_RGB_image(self,): - """Build an RGB image. The T axis is navigation and set from - P Size""" - - #First object dictionary - hypdic = self._list_sur_file_content[0] - - #Metadata are set from first dictionary - self._set_metadata_and_original_metadata(hypdic) - - #Add the series-axis to the signal dict - self.signal_dict['axes'].append(\ - self._build_Tax(hypdic,'_08_P_Size',ind=0,nav=True)) - - #All objects must share the same signal axes - self.signal_dict['axes'].append(\ - self._build_Yax(hypdic,ind=1,nav=False)) - self.signal_dict['axes'].append(\ - self._build_Xax(hypdic,ind=2,nav=False)) - - #shape of the surfaces in the series - shape = (hypdic['_19_Number_of_Lines'],hypdic['_18_Number_of_Points']) - #We put all the data together - data = [] - for obj in self._list_sur_file_content: - data.append(obj['_62_points'].reshape(shape)) - - #Pushing data into the dictionary - self.signal_dict['data'] = np.stack(data) - - self.signal_dict.update({'post_process':[self.post_process_RGB]}) - - ### Metadata utility methods - def _build_metadata(self,unpacked_dict): - """Return a minimalistic metadata dictionary according to hyperspy - format. Accept a dictionary as an input because dictionary with the - headers of a mountians object. - - Parameters - ---------- - unpacked_dict: dictionary from the header of a surface file - - Returns - ------- - metadict: dictionnary in the hyperspy metadata format - - """ - - #Formatting for complicated strings. We add parentheses to units - qty_unit = unpacked_dict['_29_Z_Step_Unit'] - #We strip unit from any character that might pre-format it - qty_unit = qty_unit.strip(' \t\n()[]') - #If unit string is still truthy after strip we add parentheses - if qty_unit: - qty_unit = "({:s})".format(qty_unit) - - - quantity_str = " ".join([ - unpacked_dict['_26_Name_of_Z_Axis'],qty_unit]).strip() - - #Date and time are set in metadata only if all values are not set to 0 - - date = [unpacked_dict['_45_Year'], - unpacked_dict['_44_Month'], - unpacked_dict['_43_Day']] - if not all(v == 0 for v in date): - date_str = "{:4d}-{:2d}-{:2d}".format(date[0],date[1],date[2]) - else: - date_str = "" - - time = [unpacked_dict['_42_Hours'], - unpacked_dict['_41_Minutes'], - unpacked_dict['_40_Seconds']] - - if not all(v == 0 for v in time): - time_str = "{:d}:{:d}:{:d}".format(time[0],time[1],time[2]) - else: - time_str = "" - - #Metadata dictionary initialization - metadict = { - "General":{ - "authors": unpacked_dict['_07_Operator_Name'], - "date":date_str, - "original_filename": os.path.split(self.filename)[1], - "time": time_str, - }, - "Signal": { - "quantity": quantity_str, - "signal_type": "", - }, - } - - return metadict - - def _build_original_metadata(self,): - """Builds a metadata dictionnary from the header""" - original_metadata_dict = {} - - Ntot = (self._N_data_object+1)*(self._N_data_channels+1) - - #Iteration over Number of data objects - for i in range(self._N_data_object): - #Iteration over the Number of Data channels - for j in range(self._N_data_channels): - #Creating a dictionary key for each object - k = (i+1)*(j+1) - key = "Object_{:d}_Channel_{:d}".format(i,j) - original_metadata_dict.update({key:{}}) - - #We load one full object header - a = self._list_sur_file_content[k-1] - - #Save it as original metadata dictionary - headerdict = {"H"+l.lstrip('_'):a[l] for l in a if l not in \ - ("_62_points",'_61_Private_zone')} - original_metadata_dict[key].update({"Header" : headerdict}) - - #The second dictionary might contain custom mountainsmap params - parsedict = {} - - #Check if it is the case and append it to - #original metadata if yes - - valid_comment = self._check_comments(a["_60_Comment"],'$','=') - if valid_comment: - parsedict = self._MS_parse(a["_60_Comment"],'$','=') - parsedict = {l.lstrip('_'):m for l,m in parsedict.items()} - original_metadata_dict[key].update({"Parsed" : parsedict}) - - return original_metadata_dict - - def _set_metadata_and_original_metadata(self,unpacked_dict): - """Run successively _build_metadata and _build_original_metadata - and set signal dictionary with results""" - - self.signal_dict['metadata'] = self._build_metadata(unpacked_dict) - self.signal_dict['original_metadata'] = self._build_original_metadata() - - def _check_comments(self,commentsstr,prefix,delimiter): - """Check if comment string is parsable into metadata dictionary. - Some specific lines (empty or starting with @@) will be ignored, - but any non-ignored line must conform to being a title line (beginning - with the TITLESTART indicator) or being parsable (starting with Prefix - and containing the key data delimiter). At the end, the comment is - considered parsable if it contains minimum 1 parsable line and no - non-ignorable non-parsable non-title line. - - Parameters - ---------- - commentstr: string containing comments - prefix: string (or char) character assumed to start each line. - '$' if a .sur file. - delimiter: string that delimits the keyword from value. always '=' - - Returns - ------- - valid: boolean - """ - - #Titlestart markers start with Prefix ($) followed by underscore - TITLESTART = '{:s}_'.format(prefix) - - #We start by assuming that the comment string is valid - #but contains 0 valid (= parsable) lines - valid = True - N_valid_lines = 0 - - for line in commentsstr.splitlines(): - #Here we ignore any empty line or line starting with @@ - ignore = False - if not line.strip() or line.startswith('@@'): - ignore = True - #If the line must not be ignored - if not ignore: - #If line starts with a titlestart marker we it counts as valid - if line.startswith(TITLESTART): - N_valid_lines += 1 - # if it does not we check that it has the delimiter and - # starts with prefix - else: - #We check that line contains delimiter and prefix - #if it does the count of valid line is increased - if delimiter in line and line.startswith(prefix): - N_valid_lines += 1 - #Otherwise the whole comment string is thrown out - else: - valid = False - - #finally, it total number of valid line is 0 we throw out this comments - if N_valid_lines ==0: - valid = False - - #return falsiness of the string. - return valid - - def _MS_parse(self, strMS, prefix, delimiter): - """ Parses a string containing metadata information. The string can be - read from the comment section of a .sur file, or, alternatively, a file - containing them with a similar formatting. - - Parameters - ---------- - strMS: string containing metadata - prefix: string (or char) character assumed to start each line. - '$' if a .sur file. - delimiter: string that delimits the keyword from value. always '=' - - Returns - ------- - dictMS: dictionnary in the correct hyperspy metadata format - - """ - #dictMS is created as an empty dictionnary - dictMS = {} - #Title lines start with an underscore - TITLESTART = '{:s}_'.format(prefix) - - for line in strMS.splitlines() : - #Here we ignore any empty line or line starting with @@ - ignore = False - if not line.strip() or line.startswith('@@'): - ignore = True - #If the line must not be ignored - if not ignore: - if line.startswith(TITLESTART): - #We strip keys from whitespace at the end and beginning - keyMain = line[len(TITLESTART):].strip() - dictMS[keyMain] = {} - elif line.startswith(prefix): - key, *liValue = line.split(delimiter) - #Key is also stripped from beginning or end whitespace - key = key[len(prefix):].strip() - strValue = liValue[0] if len(liValue)>0 else "" - # remove whitespace at the beginning of value - strValue = strValue.strip() - liValue = strValue.split(' ') - try : - if key == "Grating": - dictMS[keyMain][key] = liValue[0] # we don't want to eval this one - else : - dictMS[keyMain][key] = eval(liValue[0]) - except : - dictMS[keyMain][key] = liValue[0] - if len(liValue) > 1: - dictMS[keyMain][key+'_units'] = liValue[1] - return dictMS - - ### Post processing - def post_process_RGB(self,signal): - signal = signal.transpose() - max_data = np.nanmax(signal.data) - if max_data <=256: - signal.change_dtype('uint8') - signal.change_dtype('rgb8') - elif max_data <=65536: - signal.change_dtype('uint8') - signal.change_dtype('rgb8') - else: - warnings.warn("""RGB-announced data could not be converted to - uint8 or uint16 datatype""") - pass - - return signal - - ### pack/unpack binary quantities - def _get_int16(self,file, default=None, signed=True): - """Read a 16-bits int with a user-definable default value if - no file is given""" - if file is None : - return default - b = file.read(2) - if sys.byteorder == 'big' : - return struct.unpack('>h', b)[0] - else : - return struct.unpack('i', b)[0] - else : - return struct.unpack('I', b)[0] - else : - return struct.unpack('. - -import os -import logging -from datetime import datetime, timedelta -from dateutil import parser -import pint -from tifffile import imwrite, TiffFile, TIFF -import tifffile -import traits.api as t -import numpy as np -from distutils.version import LooseVersion - -from hyperspy.misc import rgb_tools -from hyperspy.misc.date_time_tools import get_date_time_from_metadata - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = 'TIFF' -description = ('Import/Export standard image formats Christoph Gohlke\'s ' - 'tifffile library') -full_support = False -file_extensions = ['tif', 'tiff'] -default_extension = 0 # tif -# Writing capabilities -writes = [(2, 0), (2, 1)] -non_uniform_axis = False -# ---------------------- - - -axes_label_codes = { - 'X': "width", - 'Y': "height", - 'S': "sample", - 'P': "plane", - 'I': "image series", - 'Z': "depth", - 'C': "color|em-wavelength|channel", - 'E': "ex-wavelength|lambda", - 'T': "time", - 'R': "region|tile", - 'A': "angle", - 'F': "phase", - 'H': "lifetime", - 'L': "exposure", - 'V': "event", - 'Q': t.Undefined, - '_': t.Undefined} - - -ureg = pint.UnitRegistry() - - -def file_writer(filename, signal, export_scale=True, extratags=[], **kwds): - """Writes data to tif using Christoph Gohlke's tifffile library - - Parameters - ---------- - filename: str - signal: a BaseSignal instance - export_scale: bool - default: True - Export the scale and the units (compatible with DM and ImageJ) to - appropriate tags. - """ - - data = signal.data - if signal.is_rgbx is True: - data = rgb_tools.rgbx2regular_array(data) - photometric = "RGB" - else: - photometric = "MINISBLACK" - if 'description' in kwds.keys() and export_scale: - kwds.pop('description') - _logger.warning( - "Description and export scale cannot be used at the same time, " - "because it is incompability with the 'ImageJ' tiff format") - if export_scale: - kwds.update(_get_tags_dict(signal, extratags=extratags)) - _logger.debug(f"kwargs passed to tifffile.py imsave: {kwds}") - - if 'metadata' not in kwds.keys(): - # Because we write the calibration to the ImageDescription tag - # for imageJ, we need to disable tiffile from also writing JSON - # metadata if not explicitely requested - # (https://github.com/cgohlke/tifffile/issues/21) - kwds['metadata'] = None - - if 'date' in signal.metadata['General']: - dt = get_date_time_from_metadata(signal.metadata, - formatting='datetime') - kwds['datetime'] = dt - - imwrite(filename, - data, - software="hyperspy", - photometric=photometric, - **kwds) - - -def file_reader(filename, force_read_resolution=False, lazy=False, **kwds): - """ - Read data from tif files using Christoph Gohlke's tifffile library. - The units and the scale of images saved with ImageJ or Digital - Micrograph is read. There is limited support for reading the scale of - files created with Zeiss and FEI SEMs. - - Parameters - ---------- - filename: str - Name of the file to read - force_read_resolution: bool - Force reading the x_resolution, y_resolution and the resolution_unit - of the tiff tags. - See http://www.awaresystems.be/imaging/tiff/tifftags/resolutionunit.html - Default is False. - lazy : bool - Load the data lazily. Default is False - **kwds, optional - """ - - - with TiffFile(filename, **kwds) as tiff: - dict_list = [_read_serie(tiff, serie, filename, force_read_resolution, - lazy=lazy, **kwds) - for serie in tiff.series] - - return dict_list - - - -def _read_serie(tiff, serie, filename, force_read_resolution=False, - lazy=False, memmap=None, **kwds): - - axes = serie.axes - page = serie.pages[0] - if hasattr(serie, 'shape'): - shape = serie.shape - dtype = serie.dtype - else: - shape = serie['shape'] - dtype = serie['dtype'] - - is_rgb = page.photometric == TIFF.PHOTOMETRIC.RGB - _logger.debug("Is RGB: %s" % is_rgb) - if is_rgb: - axes = axes[:-1] - names = ['R', 'G', 'B', 'A'] - lastshape = shape[-1] - dtype = np.dtype({'names': names[:lastshape], - 'formats': [dtype] * lastshape}) - shape = shape[:-1] - - if LooseVersion(tifffile.__version__) >= LooseVersion("2020.2.16"): - op = {tag.name: tag.value for tag in page.tags} - else: - op = {key: tag.value for key, tag in page.tags.items()} - - names = [axes_label_codes[axis] for axis in axes] - - _logger.debug('Tiff tags list: %s' % op) - _logger.debug("Photometric: %s" % op['PhotometricInterpretation']) - _logger.debug('is_imagej: {}'.format(page.is_imagej)) - - scales = [1.0] * len(names) - offsets = [0.0] * len(names) - units = [t.Undefined] * len(names) - intensity_axis = {} - try: - scales_d, units_d, offsets_d, intensity_axis = _parse_scale_unit( - tiff, page, op, shape, force_read_resolution) - for i, name in enumerate(names): - if name == 'height': - scales[i], units[i] = scales_d['x'], units_d['x'] - offsets[i] = offsets_d['x'] - elif name == 'width': - scales[i], units[i] = scales_d['y'], units_d['y'] - offsets[i] = offsets_d['y'] - elif name in ['depth', 'image series', 'time']: - scales[i], units[i] = scales_d['z'], units_d['z'] - offsets[i] = offsets_d['z'] - except BaseException: - _logger.info("Scale and units could not be imported") - - axes = [{'size': size, - 'name': str(name), - 'scale': scale, - 'offset': offset, - 'units': unit, - } - for size, name, scale, offset, unit in zip(shape, names, - scales, offsets, - units)] - - md = {'General': {'original_filename': os.path.split(filename)[1]}, - 'Signal': {'signal_type': "", - 'record_by': "image", - }, - } - - if 'DateTime' in op: - dt = datetime.strptime(op['DateTime'], "%Y:%m:%d %H:%M:%S") - md['General']['date'] = dt.date().isoformat() - md['General']['time'] = dt.time().isoformat() - if 'units' in intensity_axis: - md['Signal']['quantity'] = intensity_axis['units'] - if 'scale' in intensity_axis and 'offset' in intensity_axis: - dic = {'gain_factor': intensity_axis['scale'], - 'gain_offset': intensity_axis['offset']} - md['Signal']['Noise_properties'] = {'Variance_linear_model': dic} - - data_args = serie, is_rgb - if lazy: - from dask import delayed - from dask.array import from_delayed - memmap = 'memmap' - val = delayed(_load_data, pure=True)(*data_args, memmap=memmap, **kwds) - dc = from_delayed(val, dtype=dtype, shape=shape) - # TODO: maybe just pass the memmap from tiffile? - else: - dc = _load_data(*data_args, memmap=memmap, **kwds) - - metadata_mapping = get_metadata_mapping(page, op) - - return {'data': dc, - 'original_metadata': op, - 'axes': axes, - 'metadata': md, - 'mapping': metadata_mapping, - } - - -def _load_data(serie, is_rgb, sl=None, memmap=None, **kwds): - dc = serie.asarray(out=memmap) - _logger.debug("data shape: {0}".format(dc.shape)) - if is_rgb: - dc = rgb_tools.regular_array2rgbx(dc) - if sl is not None: - dc = dc[tuple(sl)] - return dc - - -def _parse_scale_unit(tiff, page, op, shape, force_read_resolution): - axes_l = ['x', 'y', 'z'] - scales = {axis: 1.0 for axis in axes_l} - offsets = {axis: 0.0 for axis in axes_l} - units = {axis: t.Undefined for axis in axes_l} - intensity_axis = {} - - if force_read_resolution and 'ResolutionUnit' in op \ - and 'XResolution' in op: - res_unit_tag = op['ResolutionUnit'] - if res_unit_tag != TIFF.RESUNIT.NONE: - _logger.debug("Resolution unit: %s" % res_unit_tag) - scales['x'], scales['y'] = _get_scales_from_x_y_resolution(op) - # conversion to µm: - if res_unit_tag == TIFF.RESUNIT.INCH: - for key in ['x', 'y']: - units[key] = 'µm' - scales[key] = scales[key] * 25400 - elif res_unit_tag == TIFF.RESUNIT.CENTIMETER: - for key in ['x', 'y']: - units[key] = 'µm' - scales[key] = scales[key] * 10000 - - return scales, units, offsets, intensity_axis - - # for files created with FEI, ZEISS, TVIPS or Olympus (no DM or ImageJ metadata) - if 'fei' in tiff.flags: - _logger.debug("Reading FEI tif metadata") - op['fei_metadata'] = tiff.fei_metadata - try: - del op['FEI_HELIOS'] - except KeyError: - del op['FEI_SFEG'] - scales['x'] = float(op['fei_metadata']['Scan']['PixelWidth']) - scales['y'] = float(op['fei_metadata']['Scan']['PixelHeight']) - units.update({'x': 'm', 'y': 'm'}) - return scales, units, offsets, intensity_axis - - elif 'sem' in tiff.flags: - _logger.debug("Reading Zeiss tif pixel_scale") - # op['CZ_SEM'][''] is containing structure of primary - # not described SEM parameters in SI units. - # tifffiles returns flattened version of the structure (as tuple) - # and the scale in it is at index 3. - # The scale is tied with physical display and needs to be multiplied - # with factor, which is the 1024 (1k) divide by horizontal pixel n. - # CZ_SEM tiff can contain reslution of lesser precission - # in the described tags as 'ap_image_pixel_size' and/or - # 'ap_pixel_size', which depending from ZEISS software version - # can be absent and thus is not used here. - scale_in_m = op['CZ_SEM'][''][3] * 1024 / tiff.pages[0].shape[1] - scales.update({'x': scale_in_m, 'y': scale_in_m}) - units.update({'x': 'm', 'y': 'm'}) - return scales, units, offsets, intensity_axis - - elif 'tvips' in tiff.flags: - _logger.debug("Reading TVIPS tif metadata") - if 'PixelSizeX' in op['TVIPS'] and 'PixelSizeY' in op['TVIPS']: - _logger.debug("getting TVIPS scale from PixelSizeX") - scales['x'] = op['TVIPS']['PixelSizeX'] - scales['y'] = op['TVIPS']['PixelSizeY'] - units.update({'x': 'nm', 'y': 'nm'}) - else: - _logger.debug("getting TVIPS scale from XYResolution") - scales['x'], scales['y'] = _get_scales_from_x_y_resolution( - op, factor=1e-2) - units.update({'x': 'm', 'y': 'm'}) - return scales, units, offsets, intensity_axis - - elif page.is_sis: - _logger.debug("Reading Olympus SIS tif metadata") - for tag_number in [33471, 33560]: - try: - sis_metadata = page.tags[tag_number].value - except Exception: - pass - - op['Olympus_SIS_metadata'] = sis_metadata - scales['x'] = round(float(sis_metadata['pixelsizex']), 15) - scales['y'] = round(float(sis_metadata['pixelsizey']), 15) - units.update({'x': 'm', 'y': 'm'}) - - - # for files containing DM metadata - if '65003' in op: - _logger.debug("Reading Gatan DigitalMicrograph tif metadata") - units['y'] = op['65003'] # x units - if '65004' in op: - units['x'] = op['65004'] # y units - if '65005' in op: - units['z'] = op['65005'] # z units - if '65009' in op: - scales['y'] = op['65009'] # x scales - if '65010' in op: - scales['x'] = op['65010'] # y scales - if '65011' in op: - scales['z'] = op['65011'] # z scales - if '65006' in op: - offsets['y'] = op['65006'] # x offset - if '65007' in op: - offsets['x'] = op['65007'] # y offset - if '65008' in op: - offsets['z'] = op['65008'] # z offset - if '65022' in op: - intensity_axis['units'] = op['65022'] # intensity units - if '65024' in op: - intensity_axis['offset'] = op['65024'] # intensity offset - if '65025' in op: - intensity_axis['scale'] = op['65025'] # intensity scale - - # for files containing ImageJ metadata - if 'imagej' in tiff.flags: - imagej_metadata = tiff.imagej_metadata - if 'ImageJ' in imagej_metadata: - _logger.debug("Reading ImageJ tif metadata") - # ImageJ write the unit in the image description - if 'unit' in imagej_metadata: - if imagej_metadata['unit'] == 'micron': - units.update({'x': 'µm', 'y': 'µm'}) - scales['x'], scales['y'] = _get_scales_from_x_y_resolution(op) - if 'spacing' in imagej_metadata: - scales['z'] = imagej_metadata['spacing'] - - return scales, units, offsets, intensity_axis - - -def _get_scales_from_x_y_resolution(op, factor=1): - scales = (op["YResolution"][1] / op["YResolution"][0] * factor, - op["XResolution"][1] / op["XResolution"][0] * factor) - return scales - - -def _get_tags_dict(signal, extratags=[], factor=int(1E8)): - """ Get the tags to export the scale and the unit to be used in - Digital Micrograph and ImageJ. - """ - scales, units, offsets = _get_scale_unit(signal, encoding=None) - _logger.debug("{0}".format(units)) - tags_dict = _get_imagej_kwargs(signal, scales, units, factor=factor) - scales, units, offsets = _get_scale_unit(signal, encoding='latin-1') - - tags_dict["extratags"].extend( - _get_dm_kwargs_extratag( - signal, - scales, - units, - offsets)) - tags_dict["extratags"].extend(extratags) - return tags_dict - - -def _get_imagej_kwargs(signal, scales, units, factor=int(1E8)): - resolution = ((factor, int(scales[-1] * factor)), - (factor, int(scales[-2] * factor))) - if len(signal.axes_manager.navigation_axes) == 1: # For stacks - spacing = '%s' % scales[0] - else: - spacing = None - description_string = _imagej_description(unit=units[1], spacing=spacing) - _logger.debug("Description tag: %s" % description_string) - extratag = [(270, 's', 1, description_string, False)] - return {"resolution": resolution, "extratags": extratag} - - -def _get_dm_kwargs_extratag(signal, scales, units, offsets): - extratags = [(65003, 's', 3, units[-1], False), # x unit - (65004, 's', 3, units[-2], False), # y unit - (65006, 'd', 1, offsets[-1], False), # x origin - (65007, 'd', 1, offsets[-2], False), # y origin - (65009, 'd', 1, float(scales[-1]), False), # x scale - (65010, 'd', 1, float(scales[-2]), False)] # y scale -# (65012, 's', 3, units[-1], False), # x unit full name -# (65013, 's', 3, units[-2], False)] # y unit full name -# (65015, 'i', 1, 1, False), # don't know -# (65016, 'i', 1, 1, False), # don't know -# (65026, 'i', 1, 1, False)] # don't know - md = signal.metadata - if md.has_item('Signal.quantity'): - try: - intensity_units = md.Signal.quantity - except BaseException: - _logger.info("The units of the 'intensity axes' couldn't be" - "retrieved, please report the bug.") - intensity_units = "" - extratags.extend([(65022, 's', 3, intensity_units, False), - (65023, 's', 3, intensity_units, False)]) - if md.has_item('Signal.Noise_properties.Variance_linear_model'): - try: - dic = md.Signal.Noise_properties.Variance_linear_model - intensity_offset = dic.gain_offset - intensity_scale = dic.gain_factor - except BaseException: - _logger.info("The scale or the offset of the 'intensity axes'" - "couldn't be retrieved, please report the bug.") - intensity_offset = 0.0 - intensity_scale = 1.0 - extratags.extend([(65024, 'd', 1, intensity_offset, False), - (65025, 'd', 1, intensity_scale, False)]) - if signal.axes_manager.navigation_dimension > 0: - extratags.extend([(65005, 's', 3, units[0], False), # z unit - (65008, 'd', 1, offsets[0], False), # z origin - (65011, 'd', 1, float(scales[0]), False), # z scale - # (65014, 's', 3, units[0], False), # z unit full name - (65017, 'i', 1, 1, False)]) - return extratags - - -def _get_scale_unit(signal, encoding=None): - """ Return a list of scales and units, the length of the list is equal to - the signal dimension. """ - signal_axes = signal.axes_manager._axes - scales = [signal_axis.scale for signal_axis in signal_axes] - units = [signal_axis.units for signal_axis in signal_axes] - offsets = [signal_axis.offset for signal_axis in signal_axes] - for i, unit in enumerate(units): - if unit == t.Undefined: - units[i] = '' - if encoding is not None: - units[i] = units[i].encode(encoding) - return scales, units, offsets - - -def _imagej_description(version='1.11a', **kwargs): - """ Return a string that will be used by ImageJ to read the unit when - appropriate arguments are provided """ - result = ['ImageJ=%s' % version] - - append = [] - if kwargs['spacing'] is None: - kwargs.pop('spacing') - for key, value in list(kwargs.items()): - if value == 'µm': - value = 'micron' - if value == 'Å': - value = 'angstrom' - append.append(f'{key.lower()}={value}') - - return '\n'.join(result + append + ['']) - - -def _parse_beam_current_FEI(value): - try: - return float(value) * 1e9 - except ValueError: - return None - - -def _parse_tuple_Zeiss(tup): - value = tup[1] - try: - return float(value) - except ValueError: - return value - - -def _parse_tuple_Zeiss_with_units(tup, to_units=None): - (value, parse_units) = tup[1:] - if to_units is not None: - v = value * ureg(parse_units) - value = float("%.6e" % v.to(to_units).magnitude) - return value - - -def _parse_tvips_time(value): - # assuming this is the time in second - return str(timedelta(seconds=int(value))) - - -def _parse_tvips_date(value): - # get a number, such as 132122901, no idea, what it is... this is not - # an excel serial, nor an unix time... - return None - - -def _parse_string(value): - if value == '': - return None - return value - - -mapping_fei = { - 'fei_metadata.Beam.HV': - ("Acquisition_instrument.SEM.beam_energy", lambda x: float(x) * 1e-3), - 'fei_metadata.Stage.StageX': - ("Acquisition_instrument.SEM.Stage.x", None), - 'fei_metadata.Stage.StageY': - ("Acquisition_instrument.SEM.Stage.y", None), - 'fei_metadata.Stage.StageZ': - ("Acquisition_instrument.SEM.Stage.z", None), - 'fei_metadata.Stage.StageR': - ("Acquisition_instrument.SEM.Stage.rotation", None), - 'fei_metadata.Stage.StageT': - ("Acquisition_instrument.SEM.Stage.tilt", None), - 'fei_metadata.Stage.WorkingDistance': - ("Acquisition_instrument.SEM.working_distance", lambda x: float(x) * 1e3), - 'fei_metadata.Scan.Dwelltime': - ("Acquisition_instrument.SEM.dwell_time", None), - 'fei_metadata.EBeam.BeamCurrent': - ("Acquisition_instrument.SEM.beam_current", _parse_beam_current_FEI), - 'fei_metadata.System.SystemType': - ("Acquisition_instrument.SEM.microscope", None), - 'fei_metadata.User.Date': - ("General.date", lambda x: parser.parse(x).date().isoformat()), - 'fei_metadata.User.Time': - ("General.time", lambda x: parser.parse(x).time().isoformat()), - 'fei_metadata.User.User': - ("General.authors", None) -} - - -mapping_cz_sem = { - 'CZ_SEM.ap_actualkv': - ("Acquisition_instrument.SEM.beam_energy", _parse_tuple_Zeiss), - 'CZ_SEM.ap_mag': - ("Acquisition_instrument.SEM.magnification", _parse_tuple_Zeiss), - 'CZ_SEM.ap_stage_at_x': - ("Acquisition_instrument.SEM.Stage.x", - lambda tup: _parse_tuple_Zeiss_with_units(tup, to_units='mm')), - 'CZ_SEM.ap_stage_at_y': - ("Acquisition_instrument.SEM.Stage.y", - lambda tup: _parse_tuple_Zeiss_with_units(tup, to_units='mm')), - 'CZ_SEM.ap_stage_at_z': - ("Acquisition_instrument.SEM.Stage.z", - lambda tup: _parse_tuple_Zeiss_with_units(tup, to_units='mm')), - 'CZ_SEM.ap_stage_at_r': - ("Acquisition_instrument.SEM.Stage.rotation", _parse_tuple_Zeiss), - 'CZ_SEM.ap_stage_at_t': - ("Acquisition_instrument.SEM.Stage.tilt", _parse_tuple_Zeiss), - 'CZ_SEM.ap_wd': - ("Acquisition_instrument.SEM.working_distance", - lambda tup: _parse_tuple_Zeiss_with_units(tup, to_units='mm')), - 'CZ_SEM.dp_dwell_time': - ("Acquisition_instrument.SEM.dwell_time", - lambda tup: _parse_tuple_Zeiss_with_units(tup, to_units='s')), - 'CZ_SEM.ap_iprobe': - ("Acquisition_instrument.SEM.beam_current", - lambda tup: _parse_tuple_Zeiss_with_units(tup, to_units='nA')), - 'CZ_SEM.dp_detector_type': - ("Acquisition_instrument.SEM.Detector.detector_type", - lambda tup: _parse_tuple_Zeiss(tup)), - 'CZ_SEM.sv_serial_number': - ("Acquisition_instrument.SEM.microscope", _parse_tuple_Zeiss), - 'CZ_SEM.ap_date': - ("General.date", lambda tup: parser.parse(tup[1]).date().isoformat()), - 'CZ_SEM.ap_time': - ("General.time", lambda tup: parser.parse(tup[1]).time().isoformat()), - 'CZ_SEM.sv_user_name': - ("General.authors", _parse_tuple_Zeiss), -} - - -def get_tvips_mapping(mapped_magnification): - mapping_tvips = { - 'TVIPS.TemMagnification': - ("Acquisition_instrument.TEM.%s" % mapped_magnification, None), - 'TVIPS.CameraType': - ("Acquisition_instrument.TEM.Detector.Camera.name", None), - 'TVIPS.ExposureTime': - ("Acquisition_instrument.TEM.Detector.Camera.exposure", - lambda x: float(x) * 1e-3), - 'TVIPS.TemHighTension': - ("Acquisition_instrument.TEM.beam_energy", lambda x: float(x) * 1e-3), - 'TVIPS.Comment': - ("General.notes", _parse_string), - 'TVIPS.Date': - ("General.date", _parse_tvips_date), - 'TVIPS.Time': - ("General.time", _parse_tvips_time), - 'TVIPS.TemStagePosition': - ("Acquisition_instrument.TEM.Stage", lambda stage: { - 'x': stage[0] * 1E3, - 'y': stage[1] * 1E3, - 'z': stage[2] * 1E3, - 'tilt_alpha': stage[3], - 'tilt_beta': stage[4] - } - ) - } - return mapping_tvips - - -mapping_olympus_sis = { - 'Olympus_SIS_metadata.magnification': - ("Acquisition_instrument.TEM.magnification", None), - 'Olympus_SIS_metadata.cameraname': - ("Acquisition_instrument.TEM.Detector.Camera.name", None), - } - -def get_metadata_mapping(tiff_page, op): - if tiff_page.is_fei: - return mapping_fei - - elif tiff_page.is_sem: - return mapping_cz_sem - - elif tiff_page.is_tvips: - try: - if op['TVIPS']['TemMode'] == 3: - mapped_magnification = 'camera_length' - else: - mapped_magnification = 'magnification' - except KeyError: - mapped_magnification = 'magnification' - return get_tvips_mapping(mapped_magnification) - elif tiff_page.is_sis: - return mapping_olympus_sis - else: - return {} diff --git a/hyperspy/io_plugins/unbcf_fast.pyx b/hyperspy/io_plugins/unbcf_fast.pyx deleted file mode 100644 index dd5ae3ef2f..0000000000 --- a/hyperspy/io_plugins/unbcf_fast.pyx +++ /dev/null @@ -1,331 +0,0 @@ -import cython -import numpy as np -import sys - -cdef int byte_order -if sys.byteorder == 'little': - byte_order = 0 -else: - byte_order = 1 - -from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t - -# fused unsigned integer type for generalised programing: - -ctypedef fused channel_t: - uint8_t - uint16_t - uint32_t - - -# instructivelly packed array structs: - -cdef packed struct Bunch_head: # size 2bytes - uint8_t size - uint8_t channels - -# endianess agnostic reading functions... probably very slow: - -@cython.boundscheck(False) -cdef uint16_t read_16(unsigned char *pointer): - - return ((pointer[1]<<8) & 0xff00) | pointer[0] - -@cython.boundscheck(False) -cdef uint32_t read_32(unsigned char *pointer): - - return ((pointer[3]<<24) & 0xff000000) |\ - ((pointer[2]<<16) & 0xff0000) |\ - ((pointer[1]<<8) & 0xff00) |\ - pointer[0] - -@cython.boundscheck(False) -cdef uint64_t read_64(unsigned char *pointer): - # skiping the most high bits, as such a huge values is impossible - # for present bruker technology. If it would change - uncomment bellow and recompile. - #return ((pointer[7]<<56) & 0xff00000000000000) |\ - # ((pointer[6]<<48) & 0xff000000000000) |\ - # ((pointer[5]<<40) & 0xff0000000000) |\ - return ((pointer[4]<<32) & 0xff00000000) |\ - ((pointer[3]<<24) & 0xff000000) |\ - ((pointer[2]<<16) & 0xff0000) |\ - ((pointer[1]<<8) & 0xff00) |\ - pointer[0] - - -# datastream class: - -@cython.boundscheck(False) -cdef class DataStream: - - cdef unsigned char *buffer2 - cdef int size, size_chnk - cdef int offset - cdef bytes raw_bytes - cdef public object blocks # public - because it is python object - - def __cinit__(self, blocks, int size_chnk): - self.size_chnk = size_chnk - self.size = size_chnk - self.offset = 0 - - def __init__(self, blocks, int size_chnk): - self.blocks = blocks - self.raw_bytes = next(self.blocks) # python bytes buffer - self.buffer2 = self.raw_bytes # C unsigned char buffer - - cdef void seek(self, int value): - """move offset to given value. - NOTE: it do not check if value is in bounds of buffer!""" - self.offset = value - - cdef void skip(self, int length): - """increase offset by given value, - check if new offset is in bounds of buffer length - else load up next block""" - if (self.offset + length) > self.size: - self.load_next_block() - self.offset = self.offset + length - - cdef uint8_t read_8(self): - if (self.offset + 1) > self.size: - self.load_next_block() - self.offset += 1 - return self.buffer2[self.offset-1] - - cdef uint16_t read_16(self): - if (self.offset + 2) > self.size: - self.load_next_block() - self.offset += 2 - # endianess agnostic way... probably very slow: - return read_16(&self.buffer2[self.offset-2]) - - cdef uint32_t read_32(self): - if (self.offset + 4) > self.size: - self.load_next_block() - self.offset += 4 - # endianess agnostic way... probably very slow: - return read_32(&self.buffer2[self.offset-4]) - - cdef uint64_t read_64(self): - if (self.offset + 8) > self.size: - self.load_next_block() - self.offset += 8 - return read_64(&self.buffer2[self.offset-8]) - - cdef unsigned char *ptr_to(self, int length): - """get the pointer to the raw buffer, - making sure the array have the required length - counting from the offset, increase the internal offset - by given length""" - if (self.offset + length) > self.size: - self.load_next_block() - self.offset += length - return &self.buffer2[self.offset-length] - - cdef void load_next_block(self): - """take the reminder of buffer (offset:end) and - append new block of raw data, and overwrite old buffer - handle with new, set offset to 0""" - self.size = self.size_chnk + self.size - self.offset - self.buffer2 = b'' - self.raw_bytes = self.raw_bytes[self.offset:] + next(self.blocks) - self.offset = 0 - self.buffer2 = self.raw_bytes - - -# function for looping throught the bcf pixels: - -@cython.cdivision(True) -@cython.boundscheck(False) -cdef bin_to_numpy(DataStream data_stream, - channel_t[:, :, :] hypermap, - int max_chan, - int downsample): - - cdef uint32_t height, width, pix_in_line, pixel_x, add_pulse_size - cdef uint32_t dummy1, line_cnt, data_size2 - cdef uint16_t chan1, chan2, flag, data_size1, n_of_pulses - cdef uint16_t add_val, j - - height = data_stream.read_32() - width = data_stream.read_32() - data_stream.seek(0x1A0) #the begining of the array - for line_cnt in range(height): - pix_in_line = data_stream.read_32() - for dummy1 in range(pix_in_line): - pixel_x = data_stream.read_32() - chan1 = data_stream.read_16() - chan2 = data_stream.read_16() - data_stream.skip(4) # unknown static value - flag = data_stream.read_16() - data_size1 = data_stream.read_16() - n_of_pulses = data_stream.read_16() - data_size2 = data_stream.read_32() - if flag == 0: - unpack16bit(hypermap, - pixel_x // downsample, - line_cnt // downsample, - data_stream.ptr_to(data_size2), - n_of_pulses, - max_chan) - elif flag == 1: - unpack12bit(hypermap, - pixel_x // downsample, - line_cnt // downsample, - data_stream.ptr_to(data_size2), - n_of_pulses, - max_chan) - else: - unpack_instructed(hypermap, - pixel_x // downsample, - line_cnt // downsample, - data_stream.ptr_to(data_size2 - 4), - data_size2 - 4, - max_chan) - if n_of_pulses > 0: - add_pulse_size = data_stream.read_32() - for j in range(n_of_pulses): - add_val = data_stream.read_16() - if add_val < max_chan: - hypermap[line_cnt // downsample, - pixel_x // downsample, - add_val] += 1 - else: - data_stream.skip(4) - - -#functions to extract pixel spectrum: - -@cython.cdivision(True) -@cython.boundscheck(False) -cdef void unpack_instructed(channel_t[:, :, :] dest, int x, int y, - unsigned char * src, uint16_t data_size, - int cutoff): - """ - unpack instructivelly packed delphi array into selection - of memoryview - """ - cdef int offset = 0 - cdef int channel = 0 - cdef int i, j, length - cdef int gain = 0 - cdef Bunch_head* head - cdef uint16_t val16 - cdef uint32_t val32 - while (offset < data_size): - head =&src[offset] - offset +=2 - if head.size == 0: # empty channels (zero counts) - channel += head.channels - else: - if head.size == 1: - gain = (src[offset]) - elif head.size == 2: - gain = read_16(&src[offset]) - elif head.size == 4: - gain = read_32(&src[offset]) - else: - gain = read_64(&src[offset]) - offset += head.size - if head.size == 1: # special nibble switching case - for i in range(head.channels): - if (i+channel) < cutoff: - #reverse the nibbles: - if i % 2 == 0: - dest[y, x, i+channel] += ((src[offset +(i//2)] & 15) + gain) - else: - dest[y, x, i+channel] += ((src[offset +(i//2)] >> 4) + gain) - if head.channels % 2 == 0: - length = (head.channels // 2) - else: - length = ((head.channels // 2) +1) - elif head.size == 2: - for i in range(head.channels): - if (i+channel) < cutoff: - dest[y, x, i+channel] += (src[offset + i] + gain) - length = (head.channels * head.size // 2) - elif head.size == 4: - for i in range(head.channels): - if (i+channel) < cutoff: - val16 = read_16(&src[offset + i*2]) - dest[y, x, i+channel] += (val16 + gain) - length = (head.channels * head.size // 2) - else: - for i in range(head.channels): - if (i+channel) < cutoff: - val32 = read_32(&src[offset + i*2]) - dest[y, x, i+channel] += (val32 + gain) - length = (head.channels * head.size // 2) - offset += length - channel += head.channels - - -@cython.cdivision(True) -@cython.boundscheck(False) -cdef void unpack12bit(channel_t[:, :, :] dest, int x, int y, - unsigned char * src, - uint16_t no_of_pulses, - int cutoff): - """unpack 12bit packed array into selection of memoryview""" - cdef int i, channel - for i in range(no_of_pulses): - if i % 4 == 0: - channel = ((src[6*(i//4)] >> 4)+(src[6*(i//4)+1] << 4)) - elif i % 4 == 1: - channel = (((src[6*(i//4)] << 8 ) + (src[6*(i//4)+3])) & 0xFFF) - elif i % 4 == 2: - channel = ((src[6*(i//4)+2] << 4) + (src[6*(i//4)+5] >> 4)) - else: - channel = (((src[6*(i//4)+5] << 8) + src[6*(i//4)+4]) & 0xFFF) - if channel < cutoff: - dest[y, x, channel] += 1 - - -@cython.cdivision(True) -@cython.boundscheck(False) -cdef void unpack16bit(channel_t[:, :, :] dest, int x, int y, - unsigned char * src, - uint16_t no_of_pulses, - int cutoff): - """unpack 16bit packed array into selection of memoryview""" - cdef int i, channel - for i in range(no_of_pulses): - channel = (src[2*i] + ((src[2*i+1] << 8) & 0xff00)) - if channel < cutoff: - dest[y, x, channel] += 1 - - -#the main function: - -def parse_to_numpy(virtual_file, shape, dtype, downsample=1): - """Parse the hyperspectral cube from brukers bcf binary file - and return it as numpy array - - Parameters - ---------- - virtual_file : SFSTreeItem - Virtual file handle returned by SFS_reader instance. - shape : tuple - Shape of the dataset. - dtype : numpy.dtype - Data type of the dataset. - downsample : int, optional - Value for downsampling in navigation space. Default is 1. - - """ - blocks, block_size = virtual_file.get_iter_and_properties()[:2] - map_depth = shape[2] - hypermap = np.zeros(shape, dtype=dtype) - cdef DataStream data_stream = DataStream(blocks, block_size) - if dtype == np.uint8: - bin_to_numpy[uint8_t](data_stream, hypermap, map_depth, downsample) - return hypermap - elif dtype == np.uint16: - bin_to_numpy[uint16_t](data_stream, hypermap, map_depth, downsample) - return hypermap - elif dtype == np.uint32: - bin_to_numpy[uint32_t](data_stream, hypermap, map_depth, downsample) - return hypermap - else: - raise NotImplementedError('64bit array not implemented!') diff --git a/hyperspy/io_plugins/usid_hdf5.py b/hyperspy/io_plugins/usid_hdf5.py deleted file mode 100644 index ab1cc1a3f7..0000000000 --- a/hyperspy/io_plugins/usid_hdf5.py +++ /dev/null @@ -1,518 +0,0 @@ -import os -import logging -from warnings import warn -from functools import partial -from collections.abc import MutableMapping -import h5py -import numpy as np -import pyUSID as usid -import sidpy - -_logger = logging.getLogger(__name__) - - -# Plugin characteristics -# ---------------------- -format_name = 'USID' -description = \ - 'Data structured according to the Universal Spectroscopic and Imaging ' \ - 'Data (USID) model written into Hierarchical Data Format (HDF5) files' -full_support = False -# Recognised file extension -file_extensions = ['h5', 'hdf5'] -default_extension = 0 -# Reading capabilities -reads_images = True -reads_spectrum = True -reads_spectrum_image = True -# Writing capabilities -writes_images = True -writes_spectrum = True -writes_spectrum_image = True -# Writing capabilities -writes = True -non_uniform_axis = False -version = usid.__version__ - -# ######### UTILITIES THAT SIMPLIFY READING FROM H5USID FILES ################# - - -def _get_dim_dict(labels, units, val_func, ignore_non_uniform_dims=True): - """ - Gets a list of dictionaries that correspond to axes for HyperSpy Signal - objects - - Parameters - ---------- - labels : list - List of strings denoting the names of the dimension - units : list - List of strings denoting the units for the dimensions - val_func : callable - Function that will return the values over which a dimension was varied - ignore_non_uniform_dims : bool, Optional. Default = True - If set to True, a warning will be raised instead of a ValueError when a - dimension is encountered which was non-uniformly. - - Returns - ------- - dict - Dictionary of dictionaries that correspond to axes for HyperSpy Signal - objects - - Notes - ----- - For a future release of HyperSpy: - If a dimension was varied non-uniformly, one would need to set the - appropriate quantity in the quantity equal to dim_vals. At that point, - the typical offset and scale parameters would be (hopefully) ignored. - """ - dim_dict = dict() - for dim_name, units in zip(labels, units): - # dim_vals below contains the full 1D tensor that shows how a dimension - # was varied. If the parameter was varied uniformly, the offset, size, - # and scale can be extracted easily. - dim_vals = val_func(dim_name) - if len(dim_vals) == 1: - # Empty dimension! - continue - else: - try: - step_size = sidpy.base.num_utils.get_slope(dim_vals) - except ValueError: - # non-uniform dimension! - see notes above - if ignore_non_uniform_dims: - warn('Ignoring non-uniformity of dimension: ' - '{}'.format(dim_name)) - step_size = 1 - dim_vals[0] = 0 - else: - raise ValueError('Cannot load provided dataset. ' - 'Parameter: {} was varied ' - 'non-uniformly. Supply keyword ' - 'argument "ignore_non_uniform_dims=' - 'True" to ignore this ' - 'error'.format(dim_name)) - - dim_dict[dim_name] = {'size': len(dim_vals), - 'name': dim_name, - 'units': units, - 'scale': step_size, - 'offset': dim_vals[0]} - return dim_dict - - -def _assemble_dim_list(dim_dict, dim_names): - """ - Assembles a list of dictionary objects (axes) in the same order as - specified in dim_names - - Parameters - ---------- - dim_dict : dict - Dictionary of dictionaries that correspond to axes for HyperSpy Signal - objects - dim_names : list - List of strings denoting the names of the dimension - - Returns - ------- - list - List of dictionaries that correspond to axes for HyperSpy Signal - objects - """ - dim_list = [] - for dim_name in dim_names: - try: - dim_list.append(dim_dict[dim_name]) - except KeyError: - pass - return dim_list - - -def _split_descriptor(desc): - """ - Splits a string such as "Quantity [units]" or "Quantity (units)" into the - quantity and unit strings - - Parameters - ---------- - desc : str - Descriptor of a dimension or the main dataset itself - - Returns - ------- - quant : str - Name of the physical quantity - units : str - Units corresponding to the physical quantity - """ - desc = desc.strip() - ind = desc.rfind('(') - if ind < 0: - ind = desc.rfind('[') - if ind < 0: - return desc, '' - - quant = desc[:ind].strip() - units = desc[ind:] - for item in '()[]': - units = units.replace(item, '') - return quant, units - - -def _convert_to_signal_dict(ndim_form, quantity, units, dim_dict_list, - h5_path, h5_dset_path, name, sig_type='', - group_attrs={}): - """ - Packages required components that make up a Signal object - - Parameters - ---------- - ndim_form : numpy.ndarray - N-dimensional form of the main dataset - quantity : str - Physical quantity of the measurement - units : str - Corresponding units - dim_dict_list : list - List of dictionaries that instruct the axes corresponding to the main - dataset - h5_path : str - Absolute path of the original USID HDF5 file - h5_dset_path : str - Absolute path of the USIDataset within the HDF5 file - name : str - Name of the HDF5 dataset - sig_type : str, Optional - Type of measurement - group_attrs : dict, Optional. Default = {} - Any attributes at the channel and group levels - - Returns - ------- - - """ - - sig = {'data': ndim_form, - 'axes': dim_dict_list, - 'metadata': { - 'Signal': {'signal_type': sig_type}, - 'General': {'original_filename': h5_path, - 'title': name} - }, - 'original_metadata': {'quantity': quantity, - 'units': units, - 'dataset_path': h5_dset_path, - 'original_file_type': 'USID HDF5', - 'pyUSID_version': usid.__version__, - 'parameters': group_attrs - }, - } - return sig - - -def _usidataset_to_signal(h5_main, ignore_non_uniform_dims=True, lazy=True, - *kwds): - """ - Converts a single specified USIDataset object to one or more Signal objects - - Parameters - ---------- - h5_main : pyUSID.USIDataset object - USID Main dataset - ignore_non_uniform_dims : bool, Optional - If True, parameters that were varied non-uniformly in the desired - dataset will result in Exceptions. - Else, all such non-uniformly varied parameters will be treated as - uniformly varied parameters and - a Signal object will be generated. - lazy : bool, Optional - If set to True, data will be read as a Dask array. - Else, data will be read in as a numpy array - - Returns - ------- - list of hyperspy.signals.BaseSignal objects - USIDatasets with compound datatypes are broken down to multiple Signal - objects. - """ - h5_main = usid.USIDataset(h5_main) - # TODO: Cannot handle data without N-dimensional form yet - # First get dictionary of axes that HyperSpy likes to see. Ignore singular - # dimensions - pos_dict = _get_dim_dict(h5_main.pos_dim_labels, - usid.hdf_utils.get_attr(h5_main.h5_pos_inds, - 'units'), - h5_main.get_pos_values, - ignore_non_uniform_dims=ignore_non_uniform_dims) - spec_dict = _get_dim_dict(h5_main.spec_dim_labels, - usid.hdf_utils.get_attr(h5_main.h5_spec_inds, - 'units'), - h5_main.get_spec_values, - ignore_non_uniform_dims=ignore_non_uniform_dims) - - num_spec_dims = len(spec_dict) - num_pos_dims = len(pos_dict) - _logger.info('Dimensions: Positions: {}, Spectroscopic: {}' - '.'.format(num_pos_dims, num_spec_dims)) - - ret_vals = usid.hdf_utils.reshape_to_n_dims(h5_main, get_labels=True, - lazy=lazy) - ds_nd, success, dim_labs = ret_vals - - if success is not True: - raise ValueError('Dataset could not be reshaped!') - ds_nd = ds_nd.squeeze() - _logger.info('N-dimensional shape: {}'.format(ds_nd.shape)) - _logger.info('N-dimensional labels: {}'.format(dim_labs)) - - # Capturing metadata present in conventional h5USID files: - group_attrs = dict() - h5_chan_grp = h5_main.parent - if isinstance(h5_chan_grp, h5py.Group): - if 'Channel' in h5_chan_grp.name.split('/')[-1]: - group_attrs = sidpy.hdf_utils.get_attributes(h5_chan_grp) - h5_meas_grp = h5_main.parent - if isinstance(h5_meas_grp, h5py.Group): - if 'Measurement' in h5_meas_grp.name.split('/')[-1]: - temp = sidpy.hdf_utils.get_attributes(h5_meas_grp) - group_attrs.update(temp) - - """ - Normally, we might have been done but the order of the dimensions may be - different in N-dim form and - attributes in ancillary dataset - """ - num_pos_dims = len(h5_main.pos_dim_labels) - pos_dim_list = _assemble_dim_list(pos_dict, dim_labs[:num_pos_dims]) - spec_dim_list = _assemble_dim_list(spec_dict, dim_labs[num_pos_dims:]) - dim_list = pos_dim_list + spec_dim_list - - _, is_complex, is_compound, _, _ = sidpy.hdf.dtype_utils.check_dtype(h5_main) - - trunc_func = partial(_convert_to_signal_dict, - dim_dict_list=dim_list, - h5_path=h5_main.file.filename, - h5_dset_path=h5_main.name, - name=h5_main.name.split('/')[-1], - group_attrs=group_attrs) - - # Extracting the quantity and units of the main dataset - quant, units = _split_descriptor(h5_main.data_descriptor) - - if is_compound: - sig = [] - # Iterate over each dimension name: - for name in ds_nd.dtype.names: - q_sub, u_sub = _split_descriptor(name) - sig.append(trunc_func(ds_nd[name], q_sub, u_sub, sig_type=quant)) - else: - sig = [trunc_func(ds_nd, quant, units)] - - return sig - - -# ######## UTILITIES THAT SIMPLIFY WRITING TO H5USID FILES #################### - - -def _flatten_dict(nested_dict, parent_key='', sep='-'): - """ - Flattens a nested dictionary - - Parameters - ---------- - nested_dict : dict - Nested dictionary - parent_key : str, Optional - Name of current parent - sep : str, Optional. Default='-' - Separator between the keys of different levels - - Returns - ------- - dict - Dictionary whose keys are flattened to a single level - Notes - ----- - Taken from https://stackoverflow.com/questions/6027558/flatten-nested- - dictionaries-compressing-keys - """ - items = [] - for k, v in nested_dict.items(): - new_key = parent_key + sep + k if parent_key else k - if isinstance(v, MutableMapping): - items.extend(_flatten_dict(v, new_key, - sep=sep).items()) - else: - items.append((new_key, v)) - return dict(items) - - -def _axes_list_to_dimensions(axes_list, data_shape, is_spec): - dim_list = [] - dim_type = 'Pos' - if is_spec: - dim_type = 'Spec' - # for dim_ind, (dim_size, dim) in enumerate(zip(data_shape, axes_list)): - # we are going by data_shape for order (slowest to fastest) - # so the order in axes_list does not matter - for dim_ind, dim in enumerate(axes_list): - dim = axes_list[dim_ind] - dim_name = dim_type + '_Dim_' + str(dim_ind) - if isinstance(dim.name, str): - temp = dim.name.strip() - if len(temp) > 0: - dim_name = temp - dim_units = 'a. u.' - if isinstance(dim.units, str): - temp = dim.units.strip() - if len(temp) > 0: - dim_units = temp - # use REAL dimension size rather than what is presented in the - # axes manager - dim_size = data_shape[len(data_shape) - 1 - dim_ind] - ar = np.arange(dim_size) * dim.scale + dim.offset - dim_list.append(usid.Dimension(dim_name, dim_units, ar)) - if len(dim_list) == 0: - return usid.Dimension('Arb', 'a. u.', 1) - return dim_list[::-1] - -# ####### REQUIRED FUNCTIONS FOR AN IO PLUGIN ################################# - - -def file_reader(filename, dataset_path=None, ignore_non_uniform_dims=True, - lazy=False, **kwds): - """ - Reads a USID Main dataset present in an HDF5 file into a HyperSpy Signal - - Parameters - ---------- - filename : str - path to HDF5 file - dataset_path : str, Optional - Absolute path of USID Main HDF5 dataset. - Default - None - all Main Datasets will be read. Given that HDF5 files - can accommodate very large datasets, lazy reading is strongly - recommended. - If a string like ``'/Measurement_000/Channel_000/My_Dataset'`` is - provided, the specific dataset will be loaded. - ignore_non_uniform_dims : bool, Optional - If True, parameters that were varied non-uniformly in the desired - dataset will result in Exceptions. - Else, all such non-uniformly varied parameters will be treated as - uniformly varied parameters and a Signal object will be generated. - - Returns - ------- - list of hyperspy.signals.Signal object - """ - if not isinstance(filename, str): - raise TypeError('filename should be a string') - if not os.path.isfile(filename): - raise FileNotFoundError(f'No file found at: {filename}') - - # Need to keep h5 file handle open indefinitely if lazy - # Using "with" will cause the file to be closed - h5_f = h5py.File(filename, 'r') - if dataset_path is None: - all_main_dsets = usid.hdf_utils.get_all_main(h5_f) - signals = [] - for h5_dset in all_main_dsets: - # Note that the function returns a list already. - # Should not append - signals += _usidataset_to_signal(h5_dset, - ignore_non_uniform_dims= - ignore_non_uniform_dims, - lazy=lazy, **kwds) - return signals - else: - if not isinstance(dataset_path, str): - raise TypeError("'dataset_path' should be a string") - h5_dset = h5_f[dataset_path] - return _usidataset_to_signal(h5_dset, - ignore_non_uniform_dims= - ignore_non_uniform_dims, - lazy=lazy, **kwds) - - # At least close the file handle if not lazy load - if not lazy: - h5_f.close() - - -def file_writer(filename, object2save, **kwds): - """ - Writes a HyperSpy Signal object to a HDF5 file formatted according to USID - - Parameters - ---------- - filename : str - Path to target HDF5 file - object2save : hyperspy.signals.Signal - A HyperSpy signal - """ - append = False - if os.path.exists(filename): - append = True - - hs_shape = object2save.data.shape - - # Not sure how to safely ignore spurious / additional dimensions - if len(object2save.axes_manager.shape) != len(hs_shape): - raise ValueError('Number of dimensions in data (shape: {}) does not ' - 'match number of axes: ({})' - '.'.format(hs_shape, - len(object2save.axes_manager.shape))) - - parm_dict = _flatten_dict(object2save.metadata.as_dictionary()) - temp = object2save.original_metadata.as_dictionary() - parm_dict.update(_flatten_dict(temp, parent_key='Original')) - - num_pos_dims = object2save.axes_manager.navigation_dimension - nav_axes = object2save.axes_manager.navigation_axes - sig_axes = object2save.axes_manager.signal_axes - - data_2d = object2save.data - # data_2d is assumed to have dimensions arranged from slowest to fastest - # varying dimensions - if num_pos_dims > 0 and object2save.axes_manager.signal_dimension > 0: - # now flatten to 2D: - data_2d = data_2d.reshape(np.prod(hs_shape[:num_pos_dims]), - np.prod(hs_shape[num_pos_dims:])) - pos_dims = _axes_list_to_dimensions(nav_axes, - hs_shape[:num_pos_dims], False) - spec_dims = _axes_list_to_dimensions(sig_axes, - hs_shape[num_pos_dims:], True) - elif num_pos_dims == 0: - # only spectroscopic: - # now flatten to 2D: - data_2d = data_2d.reshape(1, -1) - pos_dims = _axes_list_to_dimensions(nav_axes, [], False) - spec_dims = _axes_list_to_dimensions(sig_axes, hs_shape, True) - else: - # now flatten to 2D: - data_2d = data_2d.reshape(-1, 1) - pos_dims = _axes_list_to_dimensions(nav_axes, hs_shape, False) - spec_dims = _axes_list_to_dimensions(sig_axes, [], True) - - # Does HyperSpy store the physical quantity and units somewhere? - phy_quant = 'Unknown Quantity' - phy_units = 'Unknown Units' - dset_name = 'Raw_Data' - - - - if not append: - tran = usid.NumpyTranslator() - _ = tran.translate(filename, dset_name, data_2d, phy_quant, phy_units, - pos_dims, spec_dims, parm_dict=parm_dict, - slow_to_fast=True, **kwds) - else: - with h5py.File(filename, mode='r+') as h5_f: - h5_grp = usid.hdf_utils.create_indexed_group(h5_f, 'Measurement') - usid.hdf_utils.write_simple_attrs(h5_grp, parm_dict) - h5_grp = usid.hdf_utils.create_indexed_group(h5_grp, 'Channel') - _ = usid.hdf_utils.write_main_dataset(h5_grp, data_2d, dset_name, - phy_quant, phy_units, - pos_dims, spec_dims, - slow_to_fast=True, **kwds) diff --git a/hyperspy/learn/__init__.py b/hyperspy/learn/__init__.py index c41a8ae211..3d813a7dda 100644 --- a/hyperspy/learn/__init__.py +++ b/hyperspy/learn/__init__.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . diff --git a/hyperspy/learn/mlpca.py b/hyperspy/learn/mlpca.py index 84ada7fedd..bb32299683 100644 --- a/hyperspy/learn/mlpca.py +++ b/hyperspy/learn/mlpca.py @@ -6,22 +6,22 @@ # Analytica Chimica Acta 350, no. 3 (September 19, 1997): 341-352. # # Copyright 1997 Darren T. Andrews and Peter D. Wentzell -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging @@ -51,16 +51,16 @@ def mlpca( Parameters ---------- - X : numpy array, shape (m, n) - Matrix of observations. - varX : numpy array + X : numpy.ndarray + Matrix of observations with shape (m, n). + varX : numpy.ndarray Matrix of variances associated with X (zeros for missing measurements). output_dimension : int The model dimensionality. - svd_solver : {"auto", "full", "arpack", "randomized"}, default "auto" + svd_solver : {``"auto"``, ``"full"``, ``"arpack"``, ``"randomized"``}, default ``"auto"`` If auto: - The solver is selected by a default policy based on `data.shape` and + The solver is selected by a default policy based on ``data.shape`` and `output_dimension`: if the input data is larger than 500x500 and the number of components to extract is lower than 80% of the smallest dimension of the data, then the more efficient "randomized" @@ -68,13 +68,13 @@ def mlpca( optionally truncated afterwards. If full: run exact SVD, calling the standard LAPACK solver via - :py:func:`scipy.linalg.svd`, and select the components by postprocessing + :func:`scipy.linalg.svd`, and select the components by postprocessing If arpack: use truncated SVD, calling ARPACK solver via - :py:func:`scipy.sparse.linalg.svds`. It requires strictly + :func:`scipy.sparse.linalg.svds`. It requires strictly `0 < output_dimension < min(data.shape)` If randomized: - use truncated SVD, calling :py:func:`sklearn.utils.extmath.randomized_svd` + use truncated SVD, calling :func:`sklearn.utils.extmath.randomized_svd` to estimate a limited number of components tol : float Tolerance of the stopping condition. @@ -83,9 +83,9 @@ def mlpca( Returns ------- - U, S, V: numpy array + numpy.ndarray The pseudo-SVD parameters. - s_obj : float + float Value of the objective function. References @@ -131,7 +131,7 @@ def mlpca( # Every second iteration, check the stop criterion if itr > 0 and itr % 2 == 0: - stop_criterion = np.abs(s_old - s_obj) / s_obj + stop_criterion = abs(s_old - s_obj) / s_obj _logger.info(f"Iteration: {itr // 2}, convergence: {stop_criterion}") if stop_criterion < tol: diff --git a/hyperspy/learn/mva.py b/hyperspy/learn/mva.py index ec2ab8a2d8..1ea76f0542 100644 --- a/hyperspy/learn/mva.py +++ b/hyperspy/learn/mva.py @@ -1,32 +1,35 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging import types import warnings + import dask.array as da import matplotlib.pyplot as plt import numpy as np +import rsciio.utils.tools as io_tools from matplotlib.ticker import FuncFormatter, MaxNLocator -import hyperspy.misc.io.tools as io_tools -from hyperspy.exceptions import VisibleDeprecationWarning +from hyperspy.defaults_parser import preferences +from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG +from hyperspy.external.progressbar import progressbar from hyperspy.learn.mlpca import mlpca from hyperspy.learn.ornmf import ornmf from hyperspy.learn.orthomax import orthomax @@ -34,11 +37,16 @@ from hyperspy.learn.svd_pca import svd_pca from hyperspy.learn.whitening import whiten_data from hyperspy.misc.machine_learning import import_sklearn -from hyperspy.misc.utils import ordinal, stack,is_hyperspy_signal -from hyperspy.external.progressbar import progressbar +from hyperspy.misc.utils import ( + is_cupy_array, + is_hyperspy_signal, + ordinal, + stack, +) try: import mdp + mdp_installed = True except ImportError: mdp_installed = False @@ -56,21 +64,20 @@ "sklearn_fastica": import_sklearn.sklearn.decomposition.FastICA, } cluster_algorithms = { - None : import_sklearn.sklearn.cluster.KMeans, - 'kmeans' : import_sklearn.sklearn.cluster.KMeans, - 'agglomerative' : import_sklearn.sklearn.cluster.AgglomerativeClustering, - 'minibatchkmeans' : import_sklearn.sklearn.cluster.MiniBatchKMeans, - 'spectralclustering' : import_sklearn.sklearn.cluster.SpectralClustering + None: import_sklearn.sklearn.cluster.KMeans, + "kmeans": import_sklearn.sklearn.cluster.KMeans, + "agglomerative": import_sklearn.sklearn.cluster.AgglomerativeClustering, + "minibatchkmeans": import_sklearn.sklearn.cluster.MiniBatchKMeans, + "spectralclustering": import_sklearn.sklearn.cluster.SpectralClustering, } cluster_preprocessing_algorithms = { None: None, "norm": import_sklearn.sklearn.preprocessing.Normalizer, "standard": import_sklearn.sklearn.preprocessing.StandardScaler, - "minmax": import_sklearn.sklearn.preprocessing.MinMaxScaler + "minmax": import_sklearn.sklearn.preprocessing.MinMaxScaler, } - def _get_derivative(signal, diff_axes, diff_order): """Calculate the derivative of a signal.""" if signal.axes_manager.signal_dimension == 1: @@ -135,15 +142,17 @@ def decomposition( ---------- normalize_poissonian_noise : bool, default False If True, scale the signal to normalize Poissonian noise using - the approach described in [Keenan2004]_. - algorithm : {"SVD", "MLPCA", "sklearn_pca", "NMF", "sparse_pca", "mini_batch_sparse_pca", "RPCA", "ORPCA", "ORNMF", custom object}, default "SVD" + the approach described in [*]. + algorithm : str {``"SVD"``, ``"MLPCA"``, ``"sklearn_pca"``, ``"NMF"``, ``"sparse_pca"``, + ``"mini_batch_sparse_pca"``, ``"RPCA"``, ``"ORPCA"``, ``"ORNMF"``} or object, default ``"SVD"`` The decomposition algorithm to use. If algorithm is an object, it must implement a ``fit_transform()`` method or ``fit()`` and ``transform()`` methods, in the same manner as a scikit-learn estimator. + For cupy arrays, only "SVD" is supported. output_dimension : None or int Number of components to keep/calculate. Default is None, i.e. ``min(data.shape)``. - centre : {None, "navigation", "signal"}, default None + centre : None or str {``"navigation"``, ``"signal"``}, default None * If None, the data is not centered prior to decomposition. * If "navigation", the data is centered along the navigation axis. Only used by the "SVD" algorithm. @@ -152,23 +161,23 @@ def decomposition( auto_transpose : bool, default True If True, automatically transposes the data to boost performance. Only used by the "SVD" algorithm. - navigation_mask : boolean numpy array or BaseSignal + navigation_mask : numpy.ndarray or :class:`~hyperspy.api.signals.BaseSignal` The navigation locations marked as True are not used in the decomposition. - signal_mask : boolean numpy array or BaseSignal + signal_mask : numpy.ndarray or :class:`~hyperspy.api.signals.BaseSignal` The signal locations marked as True are not used in the decomposition. - var_array : numpy array + var_array : numpy.ndarray Array of variance for the maximum likelihood PCA algorithm. Only used by the "MLPCA" algorithm. - var_func : None or function or numpy array, default None - * If None, ignored - * If function, applies the function to the data to obtain ``var_array``. - Only used by the "MLPCA" algorithm. - * If numpy array, creates ``var_array`` by applying a polynomial function - defined by the array of coefficients to the data. Only used by - the "MLPCA" algorithm. - reproject : {None, "signal", "navigation", "both"}, default None + var_func : None, callable or numpy.ndarray, default None + If None, ignored + If callable, applies the function to the data to obtain ``var_array``. + Only used by the "MLPCA" algorithm. + If numpy array, creates ``var_array`` by applying a polynomial function + defined by the array of coefficients to the data. Only used by + the "MLPCA" algorithm. + reproject : None or str {"signal", "navigation", "both"}, default None If not None, the results of the decomposition will be projected in the selected masked area. return_info: bool, default False @@ -182,35 +191,34 @@ def decomposition( In the case of sklearn.decomposition objects, this includes the values of all arguments of the chosen sklearn algorithm. svd_solver : {"auto", "full", "arpack", "randomized"}, default "auto" - If auto: - The solver is selected by a default policy based on `data.shape` and - `output_dimension`: if the input data is larger than 500x500 and the - number of components to extract is lower than 80% of the smallest - dimension of the data, then the more efficient "randomized" - method is enabled. Otherwise the exact full SVD is computed and - optionally truncated afterwards. - If full: - run exact SVD, calling the standard LAPACK solver via - :py:func:`scipy.linalg.svd`, and select the components by postprocessing - If arpack: - use truncated SVD, calling ARPACK solver via - :py:func:`scipy.sparse.linalg.svds`. It requires strictly - `0 < output_dimension < min(data.shape)` - If randomized: - use truncated SVD, calling :py:func:`sklearn.utils.extmath.randomized_svd` - to estimate a limited number of components + * If ``"auto"``: the solver is selected by a default policy based on ``data.shape`` and + ``output_dimension``: if the input data is larger than 500x500 and the + number of components to extract is lower than 80% of the smallest + dimension of the data, then the more efficient ``"randomized"`` + method is enabled. Otherwise the exact full SVD is computed and + optionally truncated afterwards. + * If ``"full"``: run exact SVD, calling the standard LAPACK solver via + :func:`scipy.linalg.svd`, and select the components by postprocessing + * If ``"arpack"``: use truncated SVD, calling ARPACK solver via + :func:`scipy.sparse.linalg.svds`. It strictly requires + ``0 < output_dimension < min(data.shape)`` + * If ``"randomized"``: use truncated SVD, call + :func:`sklearn.utils.extmath.randomized_svd` to estimate a + limited number of components + + For cupy arrays, only "full" is supported. copy : bool, default True - * If True, stores a copy of the data before any pre-treatments + * If ``True``, stores a copy of the data before any pre-treatments such as normalization in ``s._data_before_treatments``. The original data can then be restored by calling ``s.undo_treatments()``. - * If False, no copy is made. This can be beneficial for memory + * If ``False``, no copy is made. This can be beneficial for memory usage, but care must be taken since data will be overwritten. - **kwargs : extra keyword arguments + **kwargs : dict Any keyword arguments are passed to the decomposition algorithm. Returns ------- - return_info : tuple(numpy array, numpy array) or sklearn.Estimator or None + tuple of numpy.ndarray or sklearn.base.BaseEstimator or None * If True and 'algorithm' in ['RPCA', 'ORPCA', 'ORNMF'], returns the low-rank (X) and sparse (E) matrices from robust PCA/NMF. * If True and 'algorithm' is an sklearn Estimator, returns the @@ -219,19 +227,30 @@ def decomposition( References ---------- - .. [Keenan2004] M. Keenan and P. Kotula, "Accounting for Poisson noise + .. [*] M. Keenan and P. Kotula, "Accounting for Poisson noise in the multivariate analysis of ToF-SIMS spectrum images", Surf. Interface Anal 36(3) (2004): 203-212. See Also -------- - * :py:meth:`~.signal.MVATools.plot_decomposition_factors` - * :py:meth:`~.signal.MVATools.plot_decomposition_loadings` - * :py:meth:`~.signal.MVATools.plot_decomposition_results` - * :py:meth:`~.learn.mva.MVA.plot_explained_variance_ratio` - * :py:meth:`~._signals.lazy.LazySignal.decomposition` for lazy signals + plot_decomposition_factors, plot_decomposition_loadings, + plot_decomposition_results, plot_explained_variance_ratio """ + if is_cupy_array(self.data): # pragma: no cover + if algorithm == "SVD": + if svd_solver == "randomized": + raise ValueError( + "`svd_solver='randomized'` is not supported with " "cupy array." + ) + elif svd_solver == "auto": + svd_solver = "full" + else: + raise TypeError( + "Only `algorithm='SVD'` is supported with cupy arrays. " + f"Provided algorithm is '{algorithm}'." + ) + from hyperspy.signal import BaseSignal # Check data is suitable for decomposition @@ -250,35 +269,6 @@ def decomposition( "It is not possible to decompose a dataset with navigation_size < 2" ) - # Check for deprecated algorithm arguments - algorithms_deprecated = { - "fast_svd": "SVD", - "svd": "SVD", - "fast_mlpca": "MLPCA", - "mlpca": "MLPCA", - "nmf": "NMF", - "RPCA_GoDec": "RPCA", - } - new_algo = algorithms_deprecated.get(algorithm, None) - if new_algo: - if "fast" in algorithm: - warnings.warn( - f"The algorithm name `{algorithm}` has been deprecated and will be " - f"removed in HyperSpy 2.0. Please use `{new_algo}` along with the " - "argument `svd_solver='randomized'` instead.", - VisibleDeprecationWarning, - ) - svd_solver = "randomized" - else: - warnings.warn( - f"The algorithm name `{algorithm}` has been deprecated and will be " - f"removed in HyperSpy 2.0. Please use `{new_algo}` instead.", - VisibleDeprecationWarning, - ) - - # Update algorithm name - algorithm = new_algo - # Check algorithms requiring output_dimension algorithms_require_dimension = [ "MLPCA", @@ -327,16 +317,6 @@ def decomposition( ) normalize_poissonian_noise = False - # Check for deprecated polyfit - polyfit = kwargs.get("polyfit", False) - if polyfit: - warnings.warn( - "The `polyfit` argument has been deprecated and will be " - "removed in HyperSpy 2.0. Please use `var_func` instead.", - VisibleDeprecationWarning, - ) - var_func = polyfit - # Initialize return_info and print_info to_return = None to_print = [ @@ -347,8 +327,6 @@ def decomposition( f" centre={centre}", ] - from hyperspy.signal import BaseSignal - self._check_navigation_mask(navigation_mask) self._check_signal_mask(signal_mask) @@ -387,7 +365,8 @@ def decomposition( ) self.normalize_poissonian_noise( - navigation_mask=navigation_mask, signal_mask=signal_mask, + navigation_mask=navigation_mask, + signal_mask=signal_mask, ) # The rest of the code assumes that the first data axis @@ -462,19 +441,23 @@ def decomposition( ) U, S, V, Sobj = mlpca( - data_, var_array, output_dimension, svd_solver=svd_solver, **kwargs, + data_, + var_array, + output_dimension, + svd_solver=svd_solver, + **kwargs, ) loadings = U * S factors = V - explained_variance = S ** 2 / len(factors) + explained_variance = S**2 / len(factors) elif algorithm == "RPCA": X, E, U, S, V = rpca_godec(data_, rank=output_dimension, **kwargs) loadings = U * S factors = V - explained_variance = S ** 2 / len(factors) + explained_variance = S**2 / len(factors) if return_info: to_return = (X, E) @@ -487,7 +470,7 @@ def decomposition( loadings = U * S factors = V - explained_variance = S ** 2 / len(factors) + explained_variance = S**2 / len(factors) to_return = (X, E) @@ -500,7 +483,10 @@ def decomposition( elif algorithm == "ORNMF": if return_info: X, E, W, H = ornmf( - data_, rank=output_dimension, store_error=True, **kwargs, + data_, + rank=output_dimension, + store_error=True, + **kwargs, ) to_return = (X, E) else: @@ -551,9 +537,9 @@ def decomposition( else: raise ValueError( f"algorithm={algorithm}' not recognised. Expected one of: " - '"SVD", "MLPCA", "sklearn_pca", "NMF", "sparse_pca", ' - '"mini_batch_sparse_pca", "RPCA", "ORPCA", "ORNMF", custom object.' - ) + '"SVD", "MLPCA", "sklearn_pca", "NMF", "sparse_pca", ' + '"mini_batch_sparse_pca", "RPCA", "ORPCA", "ORNMF", custom object.' + ) # We must calculate the ratio here because otherwise the sum # information can be lost if the user subsequently calls @@ -688,14 +674,15 @@ def blind_source_separation( number_of_components : int or None Number of principal components to pass to the BSS algorithm. If None, you must specify the ``comp_list`` argument. - algorithm : {"sklearn_fastica", "orthomax", "FastICA", "JADE", "CuBICA", "TDSEP", custom object}, default "sklearn_fastica" + algorithm : {``"sklearn_fastica"`` | ``"orthomax"`` | ``"FastICA"`` | ``"JADE"`` | + ``"CuBICA"`` | ``"TDSEP"``} or object, default "sklearn_fastica" The BSS algorithm to use. If algorithm is an object, it must implement a ``fit_transform()`` method or ``fit()`` and ``transform()`` methods, in the same manner as a scikit-learn estimator. diff_order : int, default 1 Sometimes it is convenient to perform the BSS on the derivative of the signal. If ``diff_order`` is 0, the signal is not differentiated. - diff_axes : None or list of ints or strings + diff_axes : None, list of int, list of str * If None and `on_loadings` is False, when `diff_order` is greater than 1 and `signal_dimension` is greater than 1, the differences are calculated across all signal axes @@ -703,14 +690,14 @@ def blind_source_separation( and `navigation_dimension` is greater than 1, the differences are calculated across all navigation axes * Otherwise the axes can be specified in a list. - factors : :py:class:`~hyperspy.signal.BaseSignal` or numpy array + factors : :class:`~hyperspy.signal.BaseSignal` or numpy.ndarray Factors to decompose. If None, the BSS is performed on the factors of a previous decomposition. If a Signal instance, the navigation dimension must be 1 and the size greater than 1. - comp_list : None or list or numpy array + comp_list : None or list or numpy.ndarray Choose the components to apply BSS to. Unlike ``number_of_components``, this argument permits non-contiguous components. - mask : :py:class:`~hyperspy.signal.BaseSignal` or subclass + mask : :class:`~hyperspy.signal.BaseSignal` or subclass If not None, the signal locations marked as True are masked. The mask shape must be equal to the signal shape (navigation shape) when `on_loadings` is False (True). @@ -720,9 +707,9 @@ def blind_source_separation( reverse_component_criterion : {"factors", "loadings"}, default "factors" Use either the factors or the loadings to determine if the component needs to be reversed. - whiten_method : {"PCA", "ZCA", None}, default "PCA" + whiten_method : {``"PCA"`` | ``"ZCA"``} or None, default "PCA" How to whiten the data prior to blind source separation. - If None, no whitening is applied. See :py:func:`~.learn.whitening.whiten_data` + If None, no whitening is applied. See :func:`~.learn.whitening.whiten_data` for more details. return_info: bool, default False The result of the decomposition is stored internally. However, @@ -734,21 +721,18 @@ def blind_source_separation( If True, print information about the decomposition being performed. In the case of sklearn.decomposition objects, this includes the values of all arguments of the chosen sklearn algorithm. - **kwargs : extra keyword arguments + **kwargs : dict Any keyword arguments are passed to the BSS algorithm. Returns ------- - return_info : sklearn.Estimator or None - * If True and 'algorithm' is an sklearn Estimator, returns the + None or subclass of sklearn.base.BaseEstimator + If True and 'algorithm' is an sklearn Estimator, returns the Estimator object. - * Otherwise, returns None See Also -------- - * :py:meth:`~.signal.MVATools.plot_bss_factors` - * :py:meth:`~.signal.MVATools.plot_bss_loadings` - * :py:meth:`~.signal.MVATools.plot_bss_results` + plot_bss_factors, plot_bss_loadings, plot_bss_results """ from hyperspy.signal import BaseSignal @@ -809,7 +793,7 @@ def blind_source_separation( ) else: raise ValueError("`mask` must be a HyperSpy signal.") - + mask = mask.deepcopy() # Avoid changing the original mask if hasattr(mask, "compute"): # if the mask is lazy, we compute them, which should be fine # since we already reduce the dimensionality of the data. @@ -843,7 +827,7 @@ def blind_source_separation( number_of_components = lr.output_dimension comp_list = range(number_of_components) else: - raise ValueError("No `number_of_components` or `comp_list` provided") + raise ValueError("No `number_of_components` or `comp_list` provided.") factors = stack([factors.inav[i] for i in comp_list]) @@ -851,8 +835,12 @@ def blind_source_separation( is_sklearn_like = False algorithms_sklearn = ["sklearn_fastica"] if algorithm in algorithms_sklearn: + if is_cupy_array(self.data): # pragma: no cover + raise TypeError( + "cupy arrays are not supported with scikit-learn " "algorithms." + ) if not import_sklearn.sklearn_installed: - raise ImportError(f"algorithm='{algorithm}' requires scikit-learn") + raise ImportError(f"algorithm='{algorithm}' requires scikit-learn.") # Set smaller convergence tolerance than sklearn default if not kwargs.get("tol", False): @@ -867,7 +855,7 @@ def blind_source_separation( _logger.warning( "HyperSpy already performs its own data whitening " f"(whiten_method='{whiten_method}'), so it is ignored " - f"for algorithm='{algorithm}'" + f"for algorithm='{algorithm}'." ) estim.whiten = False @@ -1007,7 +995,7 @@ def blind_source_separation( # following code is an experimental attempt to sort them in a # more predictable way sorting_indices = np.argsort( - lr.explained_variance[:number_of_components] @ np.abs(w.T) + lr.explained_variance[:number_of_components] @ abs(w.T) )[::-1] w[:] = w[sorting_indices, :] @@ -1031,10 +1019,10 @@ def normalize_decomposition_components(self, target="factors", function=np.sum): ---------- target : {"factors", "loadings"} Normalize components based on the scale of either the factors or loadings. - function : numpy universal function, default np.sum + function : numpy callable, default numpy.sum Each target component is divided by the output of ``function(target)``. The function must return a scalar when operating on numpy arrays and - must have an `axis` argument. + must have an ``axis`` argument. """ if target == "factors": @@ -1058,10 +1046,10 @@ def normalize_bss_components(self, target="factors", function=np.sum): ---------- target : {"factors", "loadings"} Normalize components based on the scale of either the factors or loadings. - function : numpy universal function, default np.sum + function : numpy callable, default numpy.sum Each target component is divided by the output of ``function(target)``. The function must return a scalar when operating on numpy arrays and - must have an `axis` argument. + must have an ``axis`` argument. """ if target == "factors": @@ -1090,10 +1078,16 @@ def reverse_decomposition_component(self, component_number): Examples -------- - >>> s = hs.load('some_file') - >>> s.decomposition(True) # perform PCA - >>> s.reverse_decomposition_component(1) # reverse IC 1 - >>> s.reverse_decomposition_component((0, 2)) # reverse ICs 0 and 2 + >>> s = hs.load('some_file') # doctest: +SKIP + >>> s.decomposition(True) # doctest: +SKIP + + Reverse component 1 + + >>> s.reverse_decomposition_component(1) # doctest: +SKIP + + Reverse components 0 and 2 + + >>> s.reverse_decomposition_component((0, 2)) # doctest: +SKIP """ if hasattr(self.learning_results.factors, "compute"): @@ -1119,11 +1113,17 @@ def reverse_bss_component(self, component_number): Examples -------- - >>> s = hs.load('some_file') - >>> s.decomposition(True) # perform PCA - >>> s.blind_source_separation(3) # perform ICA on 3 PCs - >>> s.reverse_bss_component(1) # reverse IC 1 - >>> s.reverse_bss_component((0, 2)) # reverse ICs 0 and 2 + >>> s = hs.load('some_file') # doctest: +SKIP + >>> s.decomposition(True) # doctest: +SKIP + >>> s.blind_source_separation(3) # doctest: +SKIP + + Reverse component 1 + + >>> s.reverse_bss_component(1) # doctest: +SKIP + + Reverse components 0 and 2 + + >>> s.reverse_bss_component((0, 2)) # doctest: +SKIP """ if hasattr(self.learning_results.bss_factors, "compute"): @@ -1252,14 +1252,14 @@ def get_decomposition_model(self, components=None): Parameters ---------- - components : {None, int, list of ints}, default None + components : None, int or list of int, default None * If None, rebuilds signal instance from all components * If int, rebuilds signal instance from components in range 0-given int * If list of ints, rebuilds signal instance from only components in given list Returns ------- - Signal instance + :class:`~hyperspy.api.signals.BaseSignal` or subclass A model built from the given components. """ @@ -1271,14 +1271,14 @@ def get_bss_model(self, components=None, chunks="auto"): Parameters ---------- - components : {None, int, list of ints}, default None - * If None, rebuilds signal instance from all components - * If int, rebuilds signal instance from components in range 0-given int - * If list of ints, rebuilds signal instance from only components in given list + components : None, int or list of int, default None + If None, rebuilds signal instance from all components + If int, rebuilds signal instance from components in range 0-given int + If list of ints, rebuilds signal instance from only components in given list Returns ------- - Signal instance + :class:`~hyperspy.api.signals.BaseSignal` or subclass A model built from the given components. """ @@ -1303,10 +1303,8 @@ def get_explained_variance_ratio(self): See Also -------- - * :py:meth:`~.learn.mva.MVA.decomposition` - * :py:meth:`~.learn.mva.MVA.plot_explained_variance_ratio` - * :py:meth:`~.learn.mva.MVA.get_decomposition_loadings` - * :py:meth:`~.learn.mva.MVA.get_decomposition_factors` + decomposition, plot_explained_variance_ratio, + get_decomposition_loadings, get_decomposition_factors """ from hyperspy._signals.signal1d import Signal1D @@ -1392,44 +1390,42 @@ def plot_explained_variance_ratio( noise_fmt : dict Dictionary of matplotlib formatting values for the noise components - fig : matplotlib figure or None + fig : matplotlib.figure.Figure or None If None, a default figure will be created, otherwise will plot into fig - ax : matplotlib ax (subplot) or None + ax : matplotlib.axes.Axes or None If None, a default ax will be created, otherwise will plot into ax **kwargs - remaining keyword arguments are passed to ``matplotlib.figure()`` + remaining keyword arguments are passed to :class:`matplotlib.figure.Figure` Returns ------- - ax : matplotlib.axes + matplotlib.axes.Axes Axes object containing the scree plot - Example - ------- + Examples + -------- To generate a scree plot with customized symbols for signal vs. noise components and a modified cutoff threshold value: - >>> s = hs.load("some_spectrum_image") - >>> s.decomposition() - >>> s.plot_explained_variance_ratio(n=40, - >>> threshold=0.005, - >>> signal_fmt={'marker': 'v', - >>> 's': 150, - >>> 'c': 'pink'} - >>> noise_fmt={'marker': '*', - >>> 's': 200, - >>> 'c': 'green'}) + >>> s = hs.load("some_spectrum_image") # doctest: +SKIP + >>> s.decomposition() # doctest: +SKIP + >>> s.plot_explained_variance_ratio( + ... n=40, + ... threshold=0.005, + ... signal_fmt={'marker': 'v', 's': 150, 'c': 'pink'}, + ... noise_fmt={'marker': '*', 's': 200, 'c': 'green'} + ... ) # doctest: +SKIP See Also -------- - * :py:meth:`~.learn.mva.MVA.decomposition` - * :py:meth:`~.learn.mva.MVA.get_explained_variance_ratio` - * :py:meth:`~.signal.MVATools.get_decomposition_loadings` - * :py:meth:`~.signal.MVATools.get_decomposition_factors` + decomposition, get_explained_variance_ratio, get_decomposition_loadings, + get_decomposition_factors """ s = self.get_explained_variance_ratio() + if is_cupy_array(s.data): + s.to_host() n_max = len(self.learning_results.explained_variance_ratio) if n is None: @@ -1583,7 +1579,7 @@ def plot_cumulative_explained_variance_ratio(self, n=50): See Also -------- - :py:meth:`~.learn.mva.MVA.plot_explained_variance_ratio`, + plot_explained_variance_ratio """ target = self.learning_results @@ -1603,7 +1599,7 @@ def normalize_poissonian_noise(self, navigation_mask=None, signal_mask=None): """Normalize the signal under the assumption of Poisson noise. Scales the signal using to "normalize" the Poisson data for - subsequent decomposition analysis [Keenan2004]_. + subsequent decomposition analysis [*]. Parameters ---------- @@ -1612,6 +1608,12 @@ def normalize_poissonian_noise(self, navigation_mask=None, signal_mask=None): signal_mask : {None, boolean numpy array}, default None Optional mask applied in the signal axis. + References + ---------- + .. [*] M. Keenan and P. Kotula, "Accounting for Poisson noise + in the multivariate analysis of ToF-SIMS spectrum images", Surf. + Interface Anal 36(3) (2004): 203-212. + """ _logger.info("preprocessing the data to normalize Poissonian noise") with self.unfolded(): @@ -1630,7 +1632,7 @@ def normalize_poissonian_noise(self, navigation_mask=None, signal_mask=None): if signal_mask is None: signal_mask = slice(None) else: - signal_mask = ~signal_mask + signal_mask = ~signal_mask.ravel() if dc[:, signal_mask][navigation_mask, :].size == 0: raise ValueError("All the data are masked, change the mask.") @@ -1652,10 +1654,25 @@ def normalize_poissonian_noise(self, navigation_mask=None, signal_mask=None): # We ignore numpy's warning when the result of an # operation produces nans - instead we set 0/0 = 0 with np.errstate(divide="ignore", invalid="ignore"): - dc[:, signal_mask][navigation_mask, :] /= self._root_aG * self._root_bH - dc[:, signal_mask][navigation_mask, :] = np.nan_to_num( - dc[:, signal_mask][navigation_mask, :] - ) + # Boolean indexing always makes a copy of data. Therefore, we cannot modify the + # data using `data[nav_mask, :][:, sig_mask] /= [...]` that would make + # the code compact but doesn't work. Instead, we treat each case differently. + if type(signal_mask) is slice and type(navigation_mask) is not slice: + dc[navigation_mask, :] /= self._root_aG * self._root_bH + dc[navigation_mask, :] = np.nan_to_num(dc[navigation_mask, :]) + elif type(signal_mask) is not slice and type(navigation_mask) is slice: + dc[:, signal_mask] /= self._root_aG * self._root_bH + dc[:, signal_mask] = np.nan_to_num(dc[:, signal_mask]) + elif ( + type(signal_mask) is not slice + and type(navigation_mask) is not slice + ): + mask = navigation_mask[:, np.newaxis] & signal_mask[np.newaxis, :] + dc[mask] /= (self._root_aG * self._root_bH).flat + dc[mask] = np.nan_to_num(dc[mask]) + else: + dc /= self._root_aG * self._root_bH + dc = np.nan_to_num(dc) def undo_treatments(self): """Undo Poisson noise normalization and other pre-treatments. @@ -1672,10 +1689,9 @@ def undo_treatments(self): "set `copy=True` when calling s.decomposition()." ) - - def _mask_for_clustering(self,mask): + def _mask_for_clustering(self, mask): # Deal with masks - if hasattr(mask, 'ravel'): + if hasattr(mask, "ravel"): mask = mask.ravel() # Transform the None masks in slices to get the right behaviour @@ -1686,10 +1702,9 @@ def _mask_for_clustering(self,mask): return mask - def _scale_data_for_clustering(self, - cluster_signal, - preprocessing="norm", - preprocessing_kwargs={},): + def _scale_data_for_clustering( + self, cluster_signal, preprocessing="norm", preprocessing_kwargs=None + ): """Scale data for cluster analysis Results are stored in `learning_results`. @@ -1710,22 +1725,15 @@ def _scale_data_for_clustering(self, You can also pass a cikit-learn preprocessing object See scaling methods in scikit-learn preprocessing for further details. - preprocessing_kwargs : + preprocessing_kwargs : None or dict Additional parameters passed to the cluster preprocessing algorithm. See sklearn.preprocessing preprocessing methods for further details - See Also -------- - * :py:meth:`~.learn.mva.MVA.clusters_analysis`, - * :py:meth:`~.learn.mva.MVA.estimate_number_of_clusters`, - * :py:meth:`~.learn.mva.MVA.get_cluster_labels`, - * :py:meth:`~.learn.mva.MVA.get_cluster_signals`, - * :py:meth:`~.learn.mva.MVA.plot_cluster_metric`, - * :py:meth:`~.signal.MVATools.plot_cluster_results` - * :py:meth:`~.signal.MVATools.plot_cluster_signals` - * :py:meth:`~.signal.MVATools.plot_cluster_labels` - + clusters_analysis, estimate_number_of_clusters, get_cluster_labels, + get_cluster_signals, plot_cluster_metric, plot_cluster_results, + plot_cluster_signals, plot_cluster_labels Returns ------- @@ -1733,33 +1741,33 @@ def _scale_data_for_clustering(self, no_of_features) scaled according to the selected algorithm """ + if preprocessing_kwargs is None: + preprocessing_kwargs = {} - - preprocessing_algorithm = self._get_cluster_preprocessing_algorithm(preprocessing,**preprocessing_kwargs) - - + preprocessing_algorithm = self._get_cluster_preprocessing_algorithm( + preprocessing, **preprocessing_kwargs + ) if preprocessing_algorithm is None: return cluster_signal else: return preprocessing_algorithm.fit_transform(cluster_signal) - def _get_number_of_components_for_clustering(self): """ Returns the number of components """ if self.learning_results.number_significant_components is None: - raise ValueError("A cluster source has been set to `decomposition` " - "or `bss` but no decomposition results are available. " - "Please run a decomposition method first.") + raise ValueError( + "A cluster source has been set to `decomposition` " + "or `bss` but no decomposition results are available. " + "Please run a decomposition method first." + ) else: number_of_components = self.learning_results.number_significant_components return number_of_components - def _cluster_analysis(self, - scaled_data, - algorithm): + def _cluster_analysis(self, scaled_data, algorithm): """Cluster analysis of a scaled data - internal @@ -1767,19 +1775,19 @@ def _cluster_analysis(self, ---------- n_clusters : int Number of clusters to find. - scaled_data : numpy array - (number_of_samples,number_of_features) - algorithm: scikit learn clustering object - **kwargs + scaled_data : numpy.ndarray + Array with shape (number_of_samples, number_of_features) + algorithm : object from :mod:`sklearn.cluster` + The algorithm used for clustering. + **kwargs : dict Additional parameters passed to the clustering algorithm. This may include `n_init`, the number of times the algorithm is restarted to optimize results. - Returns ------- - alg + object from :mod:`sklearn.cluster` return the sklearn.cluster object - """ algorithm.fit(scaled_data) @@ -1790,23 +1798,17 @@ def _cluster_analysis(self, f"Fited cluster estimator {str(algorithm)} has no attribute 'labels_'" ) - return algorithm def plot_cluster_metric(self): - """Plot the cluster metrics calculated - using evaluate_number_of_clusters method + """Plot the cluster metrics calculated using the + :meth:`~hyperspy.api.signals.BaseSignal.estimate_number_of_clusters` method See Also -------- - * :py:meth:`~.learn.mva.MVA.estimate_number_of_clusters`, - * :py:meth:`~.learn.mva.MVA.cluster_analysis`, - * :py:meth:`~.learn.mva.MVA.get_cluster_labels`, - * :py:meth:`~.learn.mva.MVA.get_cluster_signals`, - * :py:meth:`~.signal.MVATools.plot_cluster_results` - * :py:meth:`~.signal.MVATools.plot_cluster_signals` - * :py:meth:`~.signal.MVATools.plot_cluster_labels` - + estimate_number_of_clusters, cluster_analysis, get_cluster_labels, + get_cluster_signals, plot_cluster_results, plot_cluster_signals, + plot_cluster_labels """ target = self.learning_results @@ -1814,50 +1816,43 @@ def plot_cluster_metric(self): if target.cluster_metric_data is not None: ydata = target.cluster_metric_data else: - raise ValueError("Cluster metrics not evaluated " - "please run evaluate_number_of_clusters first.") + raise ValueError( + "Cluster metrics not estimated, please run " + "`estimate_number_of_clusters` first." + ) if target.cluster_metric_index is not None: xdata = target.cluster_metric_index fig = plt.figure() ax = fig.add_subplot(111) ax.scatter(xdata, ydata) - ax.set_xlabel('number of clusters') - label = str(target.cluster_metric) +"_metric" + ax.set_xlabel("number of clusters") + label = str(target.cluster_metric) + "_metric" ax.set_ylabel(label) if target.number_of_clusters is not None: nclusters = target.number_of_clusters if isinstance(nclusters, list): for nc in nclusters: - ax.axvline(nc, - linewidth=2, - color='gray', - linestyle='dashed') + ax.axvline(nc, linewidth=2, color="gray", linestyle="dashed") else: - ax.axvline(nclusters, - linewidth=2, - color='gray', - linestyle='dashed') + ax.axvline(nclusters, linewidth=2, color="gray", linestyle="dashed") if target.estimated_number_of_clusters is not None: nclusters = target.estimated_number_of_clusters if isinstance(nclusters, list): for nc in nclusters: - ax.axvline(nc, - linewidth=2, - color='green', - linestyle='dashed') + ax.axvline(nc, linewidth=2, color="green", linestyle="dashed") else: - ax.axvline(nclusters, - linewidth=2, - color='green', - linestyle='dashed') + ax.axvline(nclusters, linewidth=2, color="green", linestyle="dashed") plt.draw() return ax - - def _get_cluster_signal(self,cluster_source,number_of_components=None, - navigation_mask=None, - signal_mask=None,): + def _get_cluster_signal( + self, + cluster_source, + number_of_components=None, + navigation_mask=None, + signal_mask=None, + ): """A cluster source can be an external signal, the signal data or the decomposition or bss results Return a flatten version of the data, nav and signal mask @@ -1866,20 +1861,17 @@ def _get_cluster_signal(self,cluster_source,number_of_components=None, ---------- cluster_source : str or BaseSignal "decomposition", "bss", "signal" or a Signal - number_of_components : int, optional + number_of_components : int, default None Number of components to use with decomposition sources. - The default is None. - navigation_mask : ndarray, optional + navigation_mask : ndarray, default None mask used to select regions of the cluster_source to use. - The default is None. - signal_mask : ndarray, optional + signal_mask : ndarray, default None mask used to select regions of the cluster_source signal. For decomposition or bss this is not used. - The default is None. - reproject : bool, optional + reproject : bool, default False If False the and the cluster_source is decomposition or bss the loadings are returned. If True the factor @ loadings result - is used. The default is False. + is used. Returns @@ -1890,98 +1882,120 @@ def _get_cluster_signal(self,cluster_source,number_of_components=None, """ - toreturn=None + toreturn = None # Is it a signal - i.e. BaseSignal - if type(cluster_source) is str and cluster_source=="signal": + if isinstance(cluster_source, str) and cluster_source == "signal": cluster_source = self if is_hyperspy_signal(cluster_source): - if cluster_source.axes_manager.navigation_size != self.axes_manager.navigation_size: - raise ValueError("cluster_source does not have the same " - "navigation size as the this signal") + if ( + cluster_source.axes_manager.navigation_size + != self.axes_manager.navigation_size + ): + raise ValueError( + "cluster_source does not have the same " + "navigation size as the this signal" + ) else: if cluster_source not in ("bss", "decomposition", "signal"): if not is_hyperspy_signal(cluster_source): - raise ValueError("cluster source needs to be set " - "to `decomposition` , `signal` , `bss` " - "or a suitable Signal") + raise ValueError( + "cluster source needs to be set " + "to `decomposition` , `signal` , `bss` " + "or a suitable Signal" + ) if cluster_source == "decomposition": if self.learning_results.factors is None: - raise ValueError("A cluster source has been set to decomposition " - "but no decomposition results found. " - "Please run decomposition method first") + raise ValueError( + "A cluster source has been set to decomposition " + "but no decomposition results found. " + "Please run decomposition method first" + ) if cluster_source == "bss": if self.learning_results.bss_factors is None: - raise ValueError("A cluster source has been set to bss " - " but no blind source separation results found. " - " Please run blind source separation method first") - + raise ValueError( + "A cluster source has been set to bss " + " but no blind source separation results found. " + " Please run blind source separation method first" + ) - if cluster_source in ("decomposition","bss") and number_of_components is None: + if ( + cluster_source in ("decomposition", "bss") + and number_of_components is None + ): number_of_components = self._get_number_of_components_for_clustering() - navigation_mask = self._mask_for_clustering(navigation_mask) - if not isinstance(navigation_mask,slice) and navigation_mask.size != 1: + if not isinstance(navigation_mask, slice) and navigation_mask.size != 1: if navigation_mask.size != self.axes_manager.navigation_size: - raise ValueError("Navigation mask size does not match signal navigation size") + raise ValueError( + "Navigation mask size does not match signal navigation size" + ) if is_hyperspy_signal(cluster_source): signal_mask = self._mask_for_clustering(signal_mask) - if not isinstance(signal_mask,slice): + if not isinstance(signal_mask, slice): if signal_mask.size != cluster_source.axes_manager.signal_size: - raise ValueError("signal mask size does not match your " - "cluster source signal size") + raise ValueError( + "signal mask size does not match your " + "cluster source signal size" + ) # if it's not already unfolded - unfold but remember the unfolding so # can restore later if not cluster_source.metadata._HyperSpy.Folding.unfolded: - cluster_source.unfolded4clustering=cluster_source.unfold() - data = cluster_source.data \ - if cluster_source.axes_manager[0].index_in_array == 0 else cluster_source.data.T - toreturn = data[:,signal_mask][navigation_mask,:] - elif type(cluster_source) is str: + cluster_source.unfolded4clustering = cluster_source.unfold() + data = ( + cluster_source.data + if cluster_source.axes_manager[0].index_in_array == 0 + else cluster_source.data.T + ) + toreturn = data[:, signal_mask][navigation_mask, :] + elif isinstance(cluster_source, str): if cluster_source == "bss": loadings = self.learning_results.bss_loadings else: loadings = self.learning_results.loadings - toreturn = loadings[navigation_mask,:number_of_components] + toreturn = loadings[navigation_mask, :number_of_components] return toreturn - - def cluster_analysis(self, - cluster_source, - source_for_centers=None, - preprocessing=None, - preprocessing_kwargs={}, - number_of_components=None, - navigation_mask=None, - signal_mask=None, - algorithm=None, - return_info=False, - **kwargs): - """Cluster analysis of a signal or decomposition results of a signal + def cluster_analysis( + self, + cluster_source, + source_for_centers=None, + preprocessing=None, + preprocessing_kwargs=None, + number_of_components=None, + navigation_mask=None, + signal_mask=None, + algorithm=None, + return_info=False, + **kwargs, + ): + """ + Cluster analysis of a signal or decomposition results of a signal Results are stored in `learning_results`. - Parameters ---------- - cluster_source : {"bss", "decomposition", "signal", BaseSignal} + cluster_source : str {``"bss"`` | ``"decomposition"`` | ``"signal"``}\ + or :class:`~hyperspy.api.signals.BaseSignal` If "bss" the blind source separation results are used If "decomposition" the decomposition results are used if "signal" the signal data is used Note that using the signal or BaseSignal can be memory intensive and is only recommended if the Signal dimension is small BaseSignal must have the same navigation dimensions as the signal. - source_for_centers : {None,"decomposition","bss","signal",BaseSignal}, + source_for_centers : None, str {``"decomposition"`` | ``"bss"`` | ``"signal"``}\ + or :class:`~hyperspy.api.signals.BaseSignal` default : None If None the cluster_source is used If "bss" the blind source separation results are used If "decomposition" the decomposition results are used if "signal" the signal data is used BaseSignal must have the same navigation dimensions as the signal. - preprocessing : {"standard","norm","minmax",None or scikit learn preprocessing method} + preprocessing : str {``"standard"`` | ``"norm"`` | ``"minmax"``}, None or object default: 'norm' Preprocessing the data before cluster analysis requires preprocessing the data to be clustered to similar scales. Standard preprocessing @@ -1992,8 +2006,8 @@ def cluster_analysis(self, scale_method = import sklearn.processing.StandadScaler() preprocessing = scale_method See preprocessing methods in scikit-learn preprocessing for further - details. - preprocessing_kwargs : dict + details. If ``object``, must be :mod:`sklearn.preprocessing`-like. + preprocessing_kwargs : dict or None, default None Additional parameters passed to the supported sklearn preprocessing methods. See sklearn.preprocessing scaling methods for further details number_of_components : int, default None @@ -2004,80 +2018,73 @@ def cluster_analysis(self, using the elbow method and stored in the ``learning_results.number_significant_components`` attribute. This applies to both bss and decomposition results. - navigation_mask : boolean numpy array + navigation_mask : numpy.ndarray of bool The navigation locations marked as True are not used. - signal_mask : boolean numpy array + signal_mask : numpy.ndarray of bool The signal locations marked as True are not used in the clustering for "signal" or Signals supplied as cluster source. This is not applied to decomposition results or source_for_centers (as it may be a different shape to the cluster source) - algorithm : { "kmeans" | "agglomerative" | "minibatchkmeans" | "spectralclustering"} + algorithm : {``"kmeans"`` | ``"agglomerative"`` | ``"minibatchkmeans"`` | ``"spectralclustering"``} See scikit-learn documentation. Default "kmeans" return_info : bool, default False The result of the cluster analysis is stored internally. However, the cluster class used contain a number of attributes. If True (the default is False) return the cluster object so the attributes can be accessed. - **kwargs : dict optional, default - empty + **kwargs : dict Additional parameters passed to the clustering class for initialization. - For example, in case of the "kmeans" algorithm, `n_init` can be + For example, in case of the "kmeans" algorithm, ``n_init`` can be used to define the number of times the algorithm is restarted to optimize results. Other Parameters ---------------- - n_clusters : int + int Number of clusters to find using the one of the pre-defined methods - "kmeans","agglomerative","minibatchkmeans","spectralclustering" + "kmeans", "agglomerative", "minibatchkmeans", "spectralclustering" See sklearn.cluster for details See Also -------- - * :py:meth:`~.learn.mva.MVA.estimate_number_of_clusters`, - * :py:meth:`~.learn.mva.MVA.get_cluster_labels`, - * :py:meth:`~.learn.mva.MVA.get_cluster_signals`, - * :py:meth:`~.learn.mva.MVA.get_cluster_distances`, - * :py:meth:`~.learn.mva.MVA.plot_cluster_metric`, - * :py:meth:`~.signal.MVATools.plot_cluster_results` - * :py:meth:`~.signal.MVATools.plot_cluster_signals` - * :py:meth:`~.signal.MVATools.plot_cluster_labels` - + estimate_number_of_clusters, get_cluster_labels, get_cluster_signals, + get_cluster_distances, plot_cluster_metric, plot_cluster_results, + plot_cluster_signals, plot_cluster_labels Returns ------- - If 'return_info' is True returns the Scikit-learn cluster object - used for clustering. Useful if you wish to - examine inertia or other outputs. + None or object + If ``'return_info'`` is True returns the Scikit-learn cluster object + used for clustering. Useful if you wish to examine inertia or other outputs. """ if import_sklearn.sklearn_installed is False: - raise ImportError( - 'sklearn is not installed. Nothing done') + raise ImportError("sklearn is not installed. Nothing done") + + if is_cupy_array(self.data): # pragma: no cover + raise TypeError("cupy arrays are not supported.") if source_for_centers is None: source_for_centers = cluster_source - cluster_algorithm = \ - self._get_cluster_algorithm(algorithm,**kwargs) - + cluster_algorithm = self._get_cluster_algorithm(algorithm, **kwargs) target = LearningResults() try: # scale the data before clustering - cluster_signal = \ - self._get_cluster_signal( - cluster_source, - number_of_components, - navigation_mask, - signal_mask,) - scaled_data = \ - self._scale_data_for_clustering( + cluster_signal = self._get_cluster_signal( + cluster_source, + number_of_components, + navigation_mask, + signal_mask, + ) + scaled_data = self._scale_data_for_clustering( cluster_signal=cluster_signal, preprocessing=preprocessing, - preprocessing_kwargs=preprocessing_kwargs) + preprocessing_kwargs=preprocessing_kwargs, + ) - alg = self._cluster_analysis(scaled_data, - cluster_algorithm) + alg = self._cluster_analysis(scaled_data, cluster_algorithm) if return_info: to_return = alg else: @@ -2085,22 +2092,34 @@ def cluster_analysis(self, n_clusters = int(np.amax(alg.labels_)) + 1 # Sort the labels based on clustersize from high to low - clustersizes = np.asarray([(alg.labels_ == i).sum() for i in range(n_clusters)]) + clustersizes = np.asarray( + [(alg.labels_ == i).sum() for i in range(n_clusters)] + ) idxs = np.argsort(clustersizes)[::-1] cluster_labels = np.zeros( - (n_clusters, self.axes_manager.navigation_size), dtype="bool") + (n_clusters, self.axes_manager.navigation_size), dtype="bool" + ) nav_mask = self._mask_for_clustering(navigation_mask) - for i, j in enumerate(idxs): + for i, j in enumerate(idxs): cluster_labels[j, nav_mask] = alg.labels_ == i # Calculate cluster centers cluster_sum_signals = [] cluster_centroid_signals = [] centroids = [] distances = np.full( - (n_clusters, self.axes_manager.navigation_size,), np.nan, dtype="float") - if isinstance(source_for_centers, str) and source_for_centers in ("decomposition", "bss"): + ( + n_clusters, + self.axes_manager.navigation_size, + ), + np.nan, + dtype="float", + ) + if isinstance(source_for_centers, str) and source_for_centers in ( + "decomposition", + "bss", + ): loadings = self.learning_results.loadings[:, :number_of_components] - factors = self.learning_results.factors[:, :number_of_components] + factors = self.learning_results.factors[:, :number_of_components] for i in range(n_clusters): sloadings = loadings[cluster_labels[i, :], :].sum(0, keepdims=True) cluster_sum_signals.append((sloadings @ factors.T).squeeze()) @@ -2109,19 +2128,23 @@ def cluster_analysis(self, centroids.append(centroid) # Calculate the distances to the whole dataset, except for # the masked areas - cdist = np.linalg.norm(scaled_data - centroid[np.newaxis, :], axis=1) + cdist = np.linalg.norm( + scaled_data - centroid[np.newaxis, :], axis=1 + ) mloadings = loadings[nav_mask, ...][np.argmin(cdist), ...] cluster_centroid_signals.append((mloadings @ factors.T).squeeze()) distances[i, nav_mask] = cdist else: - cluster_data = \ - self._get_cluster_signal( - source_for_centers, - number_of_components, - navigation_mask=None, - signal_mask=None) + cluster_data = self._get_cluster_signal( + source_for_centers, + number_of_components, + navigation_mask=None, + signal_mask=None, + ) for i in range(n_clusters): - cluster_sum_signal = np.sum(cluster_data[cluster_labels[i, :], ...], axis=0) + cluster_sum_signal = np.sum( + cluster_data[cluster_labels[i, :], ...], axis=0 + ) cluster_sum_signals.append(cluster_sum_signal) # Calculate centroid cdata = scaled_data[cluster_labels[i, :][nav_mask], :] @@ -2129,9 +2152,13 @@ def cluster_analysis(self, centroids.append(centroid) # Calculate the distances to the whole dataset, except for # the masked areas - cdist = np.linalg.norm(scaled_data - centroid[np.newaxis, :], axis=1) + cdist = np.linalg.norm( + scaled_data - centroid[np.newaxis, :], axis=1 + ) # Find the signal closest to the centroid - cluster_centroid_signal = cluster_data[nav_mask, ...][np.argmin(cdist), ...] + cluster_centroid_signal = cluster_data[nav_mask, ...][ + np.argmin(cdist), ... + ] distances[i, nav_mask] = cdist cluster_centroid_signals.append(cluster_centroid_signal) target.cluster_labels = cluster_labels @@ -2146,26 +2173,24 @@ def cluster_analysis(self, self.learning_results.__dict__.update(target.__dict__) # if the cluster_source or source_for_centers is a signal # fold it back, if required, when finished - if (type(cluster_source) is str and \ - cluster_source == "signal") or \ - (type(source_for_centers) is str and \ - source_for_centers == "signal"): - if hasattr(self,"unfolded4clustering"): + if (isinstance(cluster_source, str) and cluster_source == "signal") or ( + isinstance(source_for_centers, str) and source_for_centers == "signal" + ): + if hasattr(self, "unfolded4clustering"): if self.unfolded4clustering: self.fold() if is_hyperspy_signal(cluster_source): - if hasattr(cluster_source,"unfolded4clustering"): + if hasattr(cluster_source, "unfolded4clustering"): if cluster_source.unfolded4clustering: cluster_source.fold() if is_hyperspy_signal(source_for_centers): - if hasattr(source_for_centers,"unfolded4clustering"): + if hasattr(source_for_centers, "unfolded4clustering"): if source_for_centers.unfolded4clustering: source_for_centers.fold() return to_return - def _get_cluster_algorithm(self,algorithm,**kwargs): - + def _get_cluster_algorithm(self, algorithm, **kwargs): """Convenience method to lookup cluster algorithm if algorithm is a string and instantiates it with n_clusters or if it's an object check that the object has a fit method @@ -2181,9 +2206,11 @@ def _get_cluster_algorithm(self,algorithm,**kwargs): elif hasattr(algorithm, "fit"): cluster_algorithm = algorithm else: - raise ValueError("The clustering method should be either %s " - "or a class which has a fit() method and labels_" - " attribute for results"%algorithms_sklearn) + raise ValueError( + "The clustering method should be either %s " + "or a class which has a fit() method and labels_" + " attribute for results" % algorithms_sklearn + ) return cluster_algorithm def _get_cluster_preprocessing_algorithm(self, algorithm, **kwargs): @@ -2195,18 +2222,23 @@ def _get_cluster_preprocessing_algorithm(self, algorithm, **kwargs): if algorithm is not None: if not import_sklearn.sklearn_installed: raise ImportError(f"algorithm='{algorithm}' requires scikit-learn") - process_algorithm = cluster_preprocessing_algorithms[algorithm](**kwargs) + process_algorithm = cluster_preprocessing_algorithms[algorithm]( + **kwargs + ) else: process_algorithm = None elif hasattr(algorithm, "fit_transform"): process_algorithm = algorithm else: - raise ValueError("The cluster preprocessing method should be either %s " - "or an object with a fit_transform method"%preprocessing_methods) + raise ValueError( + "The cluster preprocessing method should be either %s " + "or an object with a fit_transform method" % preprocessing_methods + ) return process_algorithm - def _distances_within_cluster(self, cluster_data, memberships, - squared=True, summed=False): + def _distances_within_cluster( + self, cluster_data, memberships, squared=True, summed=False + ): """Return inter cluster distances. Parameters @@ -2230,29 +2262,33 @@ def _distances_within_cluster(self, cluster_data, memberships, list of distances for within the cluster """ - distances = \ - [import_sklearn.sklearn.metrics.pairwise.\ - euclidean_distances(cluster_data[memberships == c, :], - squared=squared) - for c in range(np.max(memberships) + 1)] - result = [np.mean(x,axis=0) / 2.0 for x in distances] + distances = [ + import_sklearn.sklearn.metrics.pairwise.euclidean_distances( + cluster_data[memberships == c, :], squared=squared + ) + for c in range(np.max(memberships) + 1) + ] + result = [np.mean(x, axis=0) / 2.0 for x in distances] if summed: result = [np.sum(x) for x in result] return result - def estimate_number_of_clusters(self, - cluster_source, - max_clusters=10, - preprocessing=None, - preprocessing_kwargs={}, - number_of_components=None, - navigation_mask=None, - signal_mask=None, - algorithm=None, - metric="gap", - n_ref=4, - **kwargs): + def estimate_number_of_clusters( + self, + cluster_source, + max_clusters=10, + preprocessing=None, + preprocessing_kwargs=None, + number_of_components=None, + navigation_mask=None, + signal_mask=None, + algorithm=None, + metric="gap", + n_ref=4, + show_progressbar=None, + **kwargs, + ): """Performs cluster analysis of a signal for cluster sizes ranging from n_clusters =2 to max_clusters ( default 12) Note that this can be a slow process for large datasets so please @@ -2264,7 +2300,8 @@ def estimate_number_of_clusters(self, Parameters ---------- - cluster_source : {"bss", "decomposition", "signal" or Signal} + cluster_source : str {"bss", "decomposition", "signal"} \ + or :class:`~hyperspy.api.signals.BaseSignal` If "bss" the blind source separation results are used If "decomposition" the decomposition results are used if "signal" the signal data is used @@ -2275,7 +2312,7 @@ def estimate_number_of_clusters(self, max_clusters : int, default 10 Max number of clusters to use. The method will scan from 2 to max_clusters. - preprocessing : {"standard","norm","minmax" or sklearn-like preprocessing object} + preprocessing : str {"standard", "norm", "minmax"} or object default: 'norm' Preprocessing the data before cluster analysis requires preprocessing the data to be clustered to similar scales. Standard preprocessing @@ -2284,8 +2321,8 @@ def estimate_number_of_clusters(self, each measurement is scaled to length 1. You can also pass an instance of a sklearn preprocessing module. See preprocessing methods in scikit-learn preprocessing for further - details. - preprocessing_kwargs : dict, default empty + details. If ``object``, must be :mod:`sklearn.preprocessing`-like. + preprocessing_kwargs : dict or None, default None Additional parameters passed to the cluster preprocessing algorithm. See sklearn.preprocessing preprocessing methods for further details number_of_components : int, default None @@ -2298,10 +2335,10 @@ def estimate_number_of_clusters(self, navigation_mask : boolean numpy array, default : None The navigation locations marked as True are not used in the clustering. - signal_mask : boolean numpy array, default : None + signal_mask : numpy.ndarray of bool, default None The signal locations marked as True are not used in the clustering. Applies to "signal" or Signal cluster sources only. - metric : {'elbow','silhouette','gap'} default 'gap' + metric : {``'elbow'`` | ``'silhouette'`` | ``'gap'``}, default ``'gap'`` Use distance,silhouette analysis or gap statistics to estimate the optimal number of clusters. Gap is believed to be, overall, the best metric but it's also @@ -2312,16 +2349,16 @@ def estimate_number_of_clusters(self, For gap the optimal k is the first k gap(k)>= gap(k+1)-std_error For silhouette the optimal k will be one of the "maxima" found with this method - n_ref : int, default 4 + n_ref : int, default 4 Number of references to use in gap statistics method Gap statistics compares the results from clustering the data to clustering uniformly distributed data. As clustering has a random variation it is typically averaged n_ref times - to get an statistical average - **kwargs : dict {} default empty + to get an statistical average. + %s + **kwargs : dict Parameters passed to the clustering algorithm. - Other Parameters ---------------- n_clusters : int @@ -2329,38 +2366,43 @@ def estimate_number_of_clusters(self, "kmeans","agglomerative","minibatchkmeans","spectralclustering" See sklearn.cluster for details - Returns ------- - best_k : int - Estimate of the best cluster size + int + Estimate of the best cluster size. See Also -------- - * :py:meth:`~.learn.mva.MVA.cluster_analysis`, - * :py:meth:`~.learn.mva.MVA.get_cluster_labels`, - * :py:meth:`~.learn.mva.MVA.get_cluster_signals`, - * :py:meth:`~.learn.mva.MVA.get_cluster_distances`, - * :py:meth:`~.learn.mva.MVA.plot_cluster_metric`, - * :py:meth:`~.signal.MVATools.plot_cluster_results` - * :py:meth:`~.signal.MVATools.plot_cluster_signals` - * :py:meth:`~.signal.MVATools.plot_cluster_labels` + cluster_analysis, get_cluster_labels, get_cluster_signals, + get_cluster_distances, plot_cluster_metric, plot_cluster_results, + plot_cluster_signals, plot_cluster_labels """ + if show_progressbar is None: + show_progressbar = preferences.General.show_progressbar + + if preprocessing_kwargs is None: + preprocessing_kwargs = {} if max_clusters < 2: - raise ValueError("The max number of clusters, max_clusters, " - "must be specified and be >= 2.") + raise ValueError( + "The max number of clusters, max_clusters, " + "must be specified and be >= 2." + ) if algorithm not in cluster_algorithms: - raise ValueError("Estimate number of clusters only works with " - "supported clustering algorithms") + raise ValueError( + "Estimate number of clusters only works with " + "supported clustering algorithms" + ) if preprocessing not in cluster_preprocessing_algorithms: - raise ValueError("Estimate number of clusters only works with " - "supported preprocessing algorithms") + raise ValueError( + "Estimate number of clusters only works with " + "supported preprocessing algorithms" + ) to_return = None - best_k = None - k_range = list(range(1, max_clusters+1)) + best_k = None + k_range = list(range(1, max_clusters + 1)) # # for silhouette k starts at 2 # for kmeans or gap k starts at 1 @@ -2368,14 +2410,14 @@ def estimate_number_of_clusters(self, # initiate the random number generator to ensure # consistent/repeatable results # - if(algorithm == "agglomerative"): - k_range = list(range(2, max_clusters+1)) - if(algorithm == "kmeans" or algorithm == "minibatchkmeans"): + if algorithm == "agglomerative": + k_range = list(range(2, max_clusters + 1)) + if algorithm == "kmeans" or algorithm == "minibatchkmeans": if metric == "gap": # set number of averages to 1 - kwargs['n_init']=1 - if metric =="silhouette": - k_range = list(range(2, max_clusters+1)) + kwargs["n_init"] = 1 + if metric == "silhouette": + k_range = list(range(2, max_clusters + 1)) min_k = np.min(k_range) @@ -2384,62 +2426,71 @@ def estimate_number_of_clusters(self, try: # scale the data # scale the data before clustering - cluster_signal = \ - self._get_cluster_signal(cluster_source, - number_of_components, - navigation_mask, - signal_mask,) - scaled_data = \ - self._scale_data_for_clustering( + cluster_signal = self._get_cluster_signal( + cluster_source, number_of_components, navigation_mask, signal_mask + ) + scaled_data = self._scale_data_for_clustering( cluster_signal=cluster_signal, preprocessing=preprocessing, - preprocessing_kwargs=preprocessing_kwargs) + preprocessing_kwargs=preprocessing_kwargs, + ) # from 2 to max_clusters # cluster and calculate silhouette_score if metric == "elbow": - pbar = progressbar(total=len(k_range)) inertia = np.zeros(len(k_range)) + with progressbar( + total=len(k_range), disable=not show_progressbar, leave=True + ) as pbar: + for i, k in enumerate(k_range): + cluster_algorithm = self._get_cluster_algorithm( + algorithm, n_clusters=k, **kwargs + ) + alg = self._cluster_analysis(scaled_data, cluster_algorithm) - for i,k in enumerate(k_range): - cluster_algorithm = self._get_cluster_algorithm(algorithm,n_clusters=k,**kwargs) - alg = self._cluster_analysis(scaled_data,cluster_algorithm) - - D = self._distances_within_cluster(scaled_data,alg.labels_,summed=True) - W = np.sum(D) - inertia[i]= np.log(W) - pbar.update(1) - _logger.info( - f"For n_clusters ={k}. " - f"The distance metric is : {inertia[-1]}") + D = self._distances_within_cluster( + scaled_data, alg.labels_, summed=True + ) + W = np.sum(D) + inertia[i] = np.log(W) + pbar.update(1) + _logger.info( + f"For n_clusters ={k}. " + f"The distance metric is : {inertia[-1]}" + ) to_return = inertia - best_k =self.estimate_elbow_position( - to_return, log=False) + min_k + best_k = self.estimate_elbow_position(to_return, log=False) + min_k elif metric == "silhouette": - k_range = list(range(2, max_clusters+1)) - pbar = progressbar(total=len(k_range)) + k_range = list(range(2, max_clusters + 1)) silhouette_avg = [] - for k in k_range: - cluster_algorithm = \ - self._get_cluster_algorithm(algorithm,n_clusters=k,**kwargs) - alg = self._cluster_analysis(scaled_data,cluster_algorithm) - cluster_labels = alg.labels_ - silhouette_avg.append( - import_sklearn.sklearn.metrics.silhouette_score( - scaled_data, - cluster_labels)) - pbar.update(1) - _logger.info( - f"For n_clusters={k} the average " - f"silhouette_score is : {silhouette_avg[-1]}") + with progressbar( + total=len(k_range), disable=not show_progressbar, leave=True + ) as pbar: + for k in k_range: + cluster_algorithm = self._get_cluster_algorithm( + algorithm, n_clusters=k, **kwargs + ) + alg = self._cluster_analysis(scaled_data, cluster_algorithm) + cluster_labels = alg.labels_ + silhouette_avg.append( + import_sklearn.sklearn.metrics.silhouette_score( + scaled_data, cluster_labels + ) + ) + pbar.update(1) + _logger.info( + f"For n_clusters={k} the average " + f"silhouette_score is : {silhouette_avg[-1]}" + ) to_return = silhouette_avg best_k = [] max_value = -1.0 # find peaks - for u in range(len(silhouette_avg)-1): - if ((silhouette_avg[u] > silhouette_avg[u-1]) & - (silhouette_avg[u] > silhouette_avg[u+1])): - best_k.append(u+min_k) + for u in range(len(silhouette_avg) - 1): + if (silhouette_avg[u] > silhouette_avg[u - 1]) & ( + silhouette_avg[u] > silhouette_avg[u + 1] + ): + best_k.append(u + min_k) max_value = max(silhouette_avg[u], max_value) if silhouette_avg[0] > max_value: best_k.insert(0, min_k) @@ -2447,85 +2498,93 @@ def estimate_number_of_clusters(self, # cluster and calculate gap statistic # various empty arrays... reference_inertia = np.zeros(len(k_range)) - reference_std = np.zeros(len(k_range)) - data_inertia=np.zeros(len(k_range)) - reference=np.zeros(scaled_data.shape) + reference_std = np.zeros(len(k_range)) + data_inertia = np.zeros(len(k_range)) + reference = np.zeros(scaled_data.shape) local_inertia = np.zeros(n_ref) - pbar = progressbar(total=n_ref*len(k_range)) # only perform 1 pass of clustering # otherwise std_dev isn't correct for f_indx in range(scaled_data.shape[1]): - xmin = np.min(scaled_data[:,f_indx]) - xmax = np.max(scaled_data[:,f_indx]) - reference[:,f_indx]= np.linspace(xmin, xmax, endpoint=True, num=reference[:,0].size) - - for o_indx,k in enumerate(k_range): - # calculate the data metric - if(algorithm=="kmeans"): - kwargs['n_init']=1 - cluster_algorithm = \ - self._get_cluster_algorithm(algorithm,n_clusters=k,**kwargs) - alg = self._cluster_analysis(scaled_data, - cluster_algorithm) - - D = self._distances_within_cluster(scaled_data,alg.labels_, - squared=True, summed=True) - W = np.sum(D) - data_inertia[o_indx]=np.log(W) - # now do n_ref clusters for a uniform random distribution - # to determine "gap" between data and random distribution - - for i_indx in range(n_ref): - # initiate with a known seed to make the overall results - # repeatable but still sampling different configurations - cluster_algorithm = \ - self._get_cluster_algorithm(algorithm,n_clusters=k,**kwargs) - alg = self._cluster_analysis(reference, - cluster_algorithm) - D = self._distances_within_cluster(reference,alg.labels_, - squared=True,summed=True) + xmin = np.min(scaled_data[:, f_indx]) + xmax = np.max(scaled_data[:, f_indx]) + reference[:, f_indx] = np.linspace( + xmin, xmax, endpoint=True, num=reference[:, 0].size + ) + + with progressbar( + total=n_ref * len(k_range), disable=not show_progressbar, leave=True + ) as pbar: + for o_indx, k in enumerate(k_range): + # calculate the data metric + if algorithm == "kmeans": + kwargs["n_init"] = 1 + cluster_algorithm = self._get_cluster_algorithm( + algorithm, n_clusters=k, **kwargs + ) + alg = self._cluster_analysis(scaled_data, cluster_algorithm) + + D = self._distances_within_cluster( + scaled_data, alg.labels_, squared=True, summed=True + ) W = np.sum(D) - local_inertia[i_indx]=np.log(W) - pbar.update(1) - reference_inertia[o_indx]=np.mean(local_inertia) - reference_std[o_indx] = np.std(local_inertia) - std_error = np.sqrt(1.0 + 1.0/n_ref)*reference_std - std_error = np.abs(std_error) - gap = reference_inertia-data_inertia + data_inertia[o_indx] = np.log(W) + # now do n_ref clusters for a uniform random distribution + # to determine "gap" between data and random distribution + + for i_indx in range(n_ref): + # initiate with a known seed to make the overall results + # repeatable but still sampling different configurations + cluster_algorithm = self._get_cluster_algorithm( + algorithm, n_clusters=k, **kwargs + ) + alg = self._cluster_analysis(reference, cluster_algorithm) + D = self._distances_within_cluster( + reference, alg.labels_, squared=True, summed=True + ) + W = np.sum(D) + local_inertia[i_indx] = np.log(W) + pbar.update(1) + reference_inertia[o_indx] = np.mean(local_inertia) + reference_std[o_indx] = np.std(local_inertia) + std_error = np.sqrt(1.0 + 1.0 / n_ref) * reference_std + std_error = abs(std_error) + gap = reference_inertia - data_inertia to_return = gap # finding the first max..check if first point is max # otherwise use elbow method to find first knee - best_k = min_k+1 - for i in range(1,len(k_range)-1): - if i < len(k_range)-1: - if gap[i] >= (gap[i+1]- std_error[i+1]): - best_k=i+min_k + best_k = min_k + 1 + for i in range(1, len(k_range) - 1): + if i < len(k_range) - 1: + if gap[i] >= (gap[i + 1] - std_error[i + 1]): + best_k = i + min_k break else: - if gap[i] > gap[i-1]+std_error[i-1]: + if gap[i] > gap[i - 1] + std_error[i - 1]: best_k = len(k_range) - target.cluster_metric_index=k_range - target.cluster_metric_data=to_return - target.cluster_metric=metric - target.estimated_number_of_clusters=best_k + target.cluster_metric_index = k_range + target.cluster_metric_data = to_return + target.cluster_metric = metric + target.estimated_number_of_clusters = best_k return best_k finally: # fold - if (type(cluster_source) is str and \ - cluster_source == "signal"): - if hasattr(self,"unfolded4clustering"): + if isinstance(cluster_source, str) is str and cluster_source == "signal": + if hasattr(self, "unfolded4clustering"): if self.unfolded4clustering: self.fold() if is_hyperspy_signal(cluster_source): - if hasattr(cluster_source,"unfolded4clustering"): + if hasattr(cluster_source, "unfolded4clustering"): if cluster_source.unfolded4clustering: cluster_source.fold() self.learning_results.__dict__.update(target.__dict__) - def estimate_elbow_position(self, explained_variance_ratio=None,log=True, - max_points=20): + estimate_number_of_clusters.__doc__ %= SHOW_PROGRESSBAR_ARG + + def estimate_elbow_position( + self, explained_variance_ratio=None, log=True, max_points=20 + ): """Estimate the elbow position of a scree plot curve. Used to estimate the number of significant components in @@ -2535,7 +2594,7 @@ def estimate_elbow_position(self, explained_variance_ratio=None,log=True, With a classic elbow scree plot, this line more or less defines a triangle. The elbow should be the point which is the furthest distance from this line. For more details, - see [Satopää2011]_. + see [1]. Parameters ---------- @@ -2549,22 +2608,21 @@ def estimate_elbow_position(self, explained_variance_ratio=None,log=True, Returns ------- - elbow position : int - Index of the elbow position in the input array. Due to + int + The index of the elbow position in the input array. Due to zero-based indexing, the number of significant components is `elbow_position + 1`. References ---------- - .. [Satopää2011] V. Satopää, J. Albrecht, D. Irwin, and B. Raghavan. + .. [1] V. Satopää, J. Albrecht, D. Irwin, and B. Raghavan. "Finding a “Kneedle” in a Haystack: Detecting Knee Points in System Behavior,. 31st International Conference on Distributed Computing Systems Workshops, pp. 166-171, June 2011. See Also -------- - * :py:meth:`~.learn.mva.MVA.get_explained_variance_ratio`, - * :py:meth:`~.learn.mva.MVA.plot_explained_variance_ratio`, + get_explained_variance_ratio, plot_explained_variance_ratio """ if explained_variance_ratio is None: @@ -2593,19 +2651,20 @@ def estimate_elbow_position(self, explained_variance_ratio=None,log=True, y1 = curve_values_adj[0] y2 = curve_values_adj[max_points] - xs = np.arange(max_points) + xs = np.arange(max_points, like=self.data) if log: ys = np.log(curve_values_adj[:max_points]) else: ys = curve_values_adj[:max_points] - numer = np.abs((x2 - x1) * (y1 - ys) - (x1 - xs) * (y2 - y1)) + numer = abs((x2 - x1) * (y1 - ys) - (x1 - xs) * (y2 - y1)) denom = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) distance = np.nan_to_num(numer / denom) elbow_position = np.argmax(distance) return elbow_position + class LearningResults(object): """Stores the parameters and results from a decomposition.""" @@ -2628,7 +2687,7 @@ class LearningResults(object): cluster_algorithm = None number_of_clusters = None estimated_number_of_clusters = None - cluster_metric_data = None + cluster_metric_data = None cluster_metric_index = None cluster_metric = None # Unmixing @@ -2798,4 +2857,3 @@ def _transpose_results(self): self.bss_loadings, self.bss_factors, ) - diff --git a/hyperspy/learn/ornmf.py b/hyperspy/learn/ornmf.py index 9c62ecb214..98f8daef0d 100644 --- a/hyperspy/learn/ornmf.py +++ b/hyperspy/learn/ornmf.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging from itertools import chain @@ -30,7 +30,7 @@ def _thresh(X, lambda1, vmax): """Soft-thresholding with clipping.""" - res = np.abs(X) - lambda1 + res = abs(X) - lambda1 np.maximum(res, 0.0, out=res) res *= np.sign(X) np.clip(res, -vmax, vmax, out=res) @@ -55,7 +55,7 @@ def _mrdivide(B, A): def _project(W): newW = W.copy() np.maximum(newW, 0, out=newW) - sumsq = np.sqrt(np.sum(W ** 2, axis=0)) + sumsq = np.sqrt(np.sum(W**2, axis=0)) np.maximum(sumsq, 1, out=sumsq) return _mrdivide(newW, np.diag(sumsq)) @@ -148,18 +148,19 @@ def __init__( Nuclear norm regularization parameter. kappa : float Step-size for projection solver. - method : {'PGD', 'RobustPGD', 'MomentumSGD'}, default 'PGD' - * 'PGD' - Proximal gradient descent - * 'RobustPGD' - Robust proximal gradient descent - * 'MomentumSGD' - Stochastic gradient descent with momentum + method : {``'PGD'``, ``'RobustPGD'``, ``'MomentumSGD'``}, default ``'PGD'`` + * ``'PGD'`` - Proximal gradient descent + * ``'RobustPGD'`` - Robust proximal gradient descent + * ``'MomentumSGD'`` - Stochastic gradient descent with momentum subspace_learning_rate : float - Learning rate for the 'MomentumSGD' method. Should be a + Learning rate for the ``'MomentumSGD'`` method. Should be a float > 0.0 subspace_momentum : float - Momentum parameter for 'MomentumSGD' method, should be + Momentum parameter for ``'MomentumSGD'`` method, should be a float between 0 and 1. - random_state : None or int or RandomState instance, default None + random_state : None or int or RandomState, default None Used to initialize the subspace on the first iteration. + See :func:`numpy.random.default_rng` for more information. """ self.n_features = None @@ -211,7 +212,7 @@ def _setup(self, X): self.W = halfnorm.rvs( size=(self.n_features, self.rank), random_state=self.random_state ) - self.W = np.abs(avg * self.W / np.sqrt(self.rank)) + self.W = abs(avg * self.W / np.sqrt(self.rank)) self.H = [] if self.subspace_tracking: @@ -227,7 +228,7 @@ def fit(self, X, batch_size=None): Parameters ---------- - X : {numpy.ndarray, iterator} + X : array-like [n_samples x n_features] matrix of observations or an iterator that yields samples, each with n_features elements. batch_size : {None, int} @@ -281,7 +282,7 @@ def _solve_W(self, A, B): n = 0 lasttwo = np.zeros(2) while n <= 2 or ( - np.abs((lasttwo[1] - lasttwo[0]) / lasttwo[0]) > 1e-5 and n < 1e9 + abs((lasttwo[1] - lasttwo[0]) / lasttwo[0]) > 1e-5 and n < 1e9 ): self.W -= eta * (self.W @ self.A - self.B) self.W = _project(self.W) @@ -312,10 +313,10 @@ def project(self, X, return_error=False): Parameters ---------- - X : {numpy.ndarray, iterator} - [n_samples x n_features] matrix of observations + X : array-like + The matrix of observations with shape (n_samples, n_features) or an iterator that yields n_samples, each with n_features elements. - return_error : bool + return_error : bool, default False If True, returns the sparse error matrix as well. Otherwise only the weights (loadings) @@ -371,7 +372,7 @@ def ornmf( Parameters ---------- - X : numpy array + X : numpy.ndarray The [n_samples, n_features] input data. rank : int The rank of the representation (number of components/factors) @@ -379,38 +380,38 @@ def ornmf( If True, stores the sparse error matrix. project : bool, default False If True, project the data X onto the learnt model. - batch_size : {None, int}, default None + batch_size : None or int, default None If not None, learn the data in batches, each of batch_size samples or less. - lambda1 : float + lambda1 : float, default 1.0 Nuclear norm regularization parameter. - kappa : float + kappa : float, default 1.0 Step-size for projection solver. method : {'PGD', 'RobustPGD', 'MomentumSGD'}, default 'PGD' - * 'PGD' - Proximal gradient descent - * 'RobustPGD' - Robust proximal gradient descent - * 'MomentumSGD' - Stochastic gradient descent with momentum - subspace_learning_rate : float + * ``'PGD'`` - Proximal gradient descent + * ``'RobustPGD'`` - Robust proximal gradient descent + * ``'MomentumSGD'`` - Stochastic gradient descent with momentum + subspace_learning_rate : float, default 1.0 Learning rate for the 'MomentumSGD' method. Should be a float > 0.0 - subspace_momentum : float + subspace_momentum : float, default 0.5 Momentum parameter for 'MomentumSGD' method, should be a float between 0 and 1. - random_state : None or int or RandomState instance, default None + random_state : None or int or RandomState, default None Used to initialize the subspace on the first iteration. Returns ------- - Xhat : numpy array - is the [n_features x n_samples] non-negative matrix + Xhat : numpy.ndarray + The non-negative matrix with shape (n_features x n_samples). Only returned if store_error is True. - Ehat : numpy array - is the [n_features x n_samples] sparse error matrix + Ehat : numpy.ndarray + The sparse error matrix with shape (n_features x n_samples). Only returned if store_error is True. - W : numpy array, shape [n_features, rank] - is the non-negative factors matrix - H : numpy array, shape [rank, n_samples] - is the non-negative loadings matrix + W : numpy.ndarray + The non-negative factors matrix with shape (n_features, rank). + H : numpy.ndarray + The non-negative loadings matrix with shape (rank, n_samples). """ X = X.T diff --git a/hyperspy/learn/orthomax.py b/hyperspy/learn/orthomax.py index b59abf8185..b4a81fdebf 100644 --- a/hyperspy/learn/orthomax.py +++ b/hyperspy/learn/orthomax.py @@ -1,24 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np - -from scipy.linalg import svd +from numpy.linalg import svd def orthomax(A, gamma=1.0, tol=1.4901e-07, max_iter=256): @@ -68,7 +67,7 @@ def orthomax(A, gamma=1.0, tol=1.4901e-07, max_iter=256): S = 0.0 for _ in range(max_iter): # pragma: no branch Sold = S - Bsq = B ** 2 + Bsq = B**2 U, S, V = svd( A.T @ (d * B * Bsq - gamma * B * np.sum(Bsq, axis=0)), full_matrices=False, @@ -77,7 +76,7 @@ def orthomax(A, gamma=1.0, tol=1.4901e-07, max_iter=256): S = np.sum(S) B = A @ W - if np.abs(S - Sold) < tol * S: + if abs(S - Sold) < tol * S: converged = True break @@ -100,10 +99,10 @@ def orthomax(A, gamma=1.0, tol=1.4901e-07, max_iter=256): vsum = v.sum() numer = 2.0 * u.T @ v - 2.0 * gamma * usum * vsum * oo_d - denom = u.T @ u - v.T @ v - gamma * (usum ** 2 - vsum ** 2) * oo_d + denom = u.T @ u - v.T @ v - gamma * (usum**2 - vsum**2) * oo_d theta = 0.25 * np.arctan2(numer, denom) - maxTheta = max(maxTheta, np.abs(theta)) + maxTheta = max(maxTheta, abs(theta)) R = np.array( [ diff --git a/hyperspy/learn/rpca.py b/hyperspy/learn/rpca.py index 55893cb44a..c8c8f1377b 100644 --- a/hyperspy/learn/rpca.py +++ b/hyperspy/learn/rpca.py @@ -1,29 +1,27 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging -import warnings from itertools import chain import numpy as np import scipy.linalg -from hyperspy.exceptions import VisibleDeprecationWarning from hyperspy.external.progressbar import progressbar from hyperspy.learn.svd_pca import svd_solve from hyperspy.misc.math_tools import check_random_state @@ -33,7 +31,7 @@ def _soft_thresh(X, lambda1): """Soft-thresholding of array X.""" - res = np.abs(X) - lambda1 + res = abs(X) - lambda1 np.maximum(res, 0.0, out=res) res *= np.sign(X) return res @@ -46,15 +44,14 @@ def rpca_godec( Decomposes a matrix Y = X + E, where X is low-rank and E is a sparse error matrix. This algorithm is based on the - Matlab code from [Zhou2011]_. See code here: - https://sites.google.com/site/godecomposition/matrix/artifact-1 + Matlab code from [Zhou2011]_. Read more in the :ref:`User Guide `. Parameters ---------- - X : numpy array, shape (n_features, n_samples) - The matrix of observations. + X : numpy.ndarray + The matrix of observations with shape (n_features, n_samples) rank : int The model dimensionality. lambda1 : None or float @@ -66,16 +63,16 @@ def rpca_godec( Convergence tolerance maxiter : int, default 1000 Maximum number of iterations - random_state : None or int or RandomState instance, default None + random_state : None, int or RandomState, default None Used to initialize the subspace on the first iteration. Returns ------- - Xhat : numpy array, shape (n_features, n_samples) - The low-rank matrix - Ehat : numpy array, shape (n_features, n_samples) - The sparse error matrix - U, S, V : numpy arrays + Xhat : numpy.ndarray + The low-rank matrix with shape (n_features, n_samples) + Ehat : numpy.ndarray + The sparse error matrix with shape (n_features, n_samples) + U, S, V : numpy.ndarray The results of an SVD on Xhat References @@ -224,7 +221,7 @@ class ORPCA: via Stochastic Optimization", Advances in Neural Information Processing Systems 26, (2013), pp. 404-412. .. [Ruder2016] Sebastian Ruder, "An overview of gradient descent optimization - algorithms", arXiv:1609.04747, (2016), http://arxiv.org/abs/1609.04747. + algorithms", arXiv:1609.04747, (2016), https://arxiv.org/abs/1609.04747. """ @@ -249,29 +246,29 @@ def __init__( The rank of the representation (number of components/factors) store_error : bool, default False If True, stores the sparse error matrix. - lambda1 : float + lambda1 : float, default 0.1 Nuclear norm regularization parameter. - lambda2 : float + lambda2 : float, default 1.0 Sparse error regularization parameter. method : {'CF', 'BCD', 'SGD', 'MomentumSGD'}, default 'BCD' - * 'CF' - Closed-form solver - * 'BCD' - Block-coordinate descent - * 'SGD' - Stochastic gradient descent - * 'MomentumSGD' - Stochastic gradient descent with momentum - init : {'qr', 'rand', np.ndarray}, default 'qr' - * 'qr' - QR-based initialization - * 'rand' - Random initialization - * np.ndarray if the shape [n_features x rank] - training_samples : int + * ``'CF'`` - Closed-form solver + * ``'BCD'`` - Block-coordinate descent + * ``'SGD'`` - Stochastic gradient descent + * ``'MomentumSGD'`` - Stochastic gradient descent with momentum + init : numpy.ndarray, {'qr', 'rand'}, default 'qr' + * ``'qr'`` - QR-based initialization + * ``'rand'`` - Random initialization + * numpy.ndarray if the shape (n_features x rank) + training_samples : int, default 10 Specifies the number of training samples to use in the 'qr' initialization. - subspace_learning_rate : float + subspace_learning_rate : float, default 1.0 Learning rate for the 'SGD' and 'MomentumSGD' methods. Should be a float > 0.0 - subspace_momentum : float + subspace_momentum : float, default 0.5 Momentum parameter for 'MomentumSGD' method, should be a float between 0 and 1. - random_state : None or int or RandomState instance, default None + random_state : None, int or RandomState, default None Used to initialize the subspace on the first iteration. """ @@ -363,10 +360,10 @@ def fit(self, X, batch_size=None): Parameters ---------- - X : {numpy.ndarray, iterator} - [n_samples x n_features] matrix of observations + X : array-like + The matrix of observations with shape (n_samples, n_features) or an iterator that yields samples, each with n_features elements. - batch_size : {None, int} + batch_size : None or int If not None, learn the data in batches, each of batch_size samples or less. @@ -437,10 +434,10 @@ def project(self, X, return_error=False): Parameters ---------- - X : {numpy.ndarray, iterator} - [n_samples x n_features] matrix of observations + X : array-like + The matrix of observations with shape (n_samples, n_features) or an iterator that yields n_samples, each with n_features elements. - return_error : bool + return_error : bool, default False If True, returns the sparse error matrix as well. Otherwise only the weights (loadings) @@ -499,8 +496,8 @@ def orpca( Parameters ---------- - X : {numpy array, iterator} - [n_features x n_samples] matrix of observations + X : array-like + The matrix of observations with shape (n_features x n_samples) or an iterator that yields samples, each with n_features elements. rank : int The rank of the representation (number of components/factors) @@ -508,57 +505,43 @@ def orpca( If True, stores the sparse error matrix. project : bool, default False If True, project the data X onto the learnt model. - batch_size : {None, int}, default None + batch_size : None, int, default None If not None, learn the data in batches, each of batch_size samples or less. - lambda1 : float + lambda1 : float, default 0.1 Nuclear norm regularization parameter. - lambda2 : float + lambda2 : float, default 1.0 Sparse error regularization parameter. method : {'CF', 'BCD', 'SGD', 'MomentumSGD'}, default 'BCD' - * 'CF' - Closed-form solver - * 'BCD' - Block-coordinate descent - * 'SGD' - Stochastic gradient descent - * 'MomentumSGD' - Stochastic gradient descent with momentum - init : {'qr', 'rand', np.ndarray}, default 'qr' - * 'qr' - QR-based initialization - * 'rand' - Random initialization - * np.ndarray if the shape [n_features x rank] - training_samples : int + * ``'CF'`` - Closed-form solver + * ``'BCD'`` - Block-coordinate descent + * ``'SGD'`` - Stochastic gradient descent + * ``'MomentumSGD'`` - Stochastic gradient descent with momentum + init : numpy.ndarray, {'qr', 'rand'}, default 'qr' + * ``'qr'`` - QR-based initialization + * ``'rand'`` - Random initialization + * numpyp.ndarray if the shape [n_features x rank] + training_samples : int, default 10 Specifies the number of training samples to use in the 'qr' initialization. - subspace_learning_rate : float + subspace_learning_rate : float, default 1.0 Learning rate for the 'SGD' and 'MomentumSGD' methods. Should be a float > 0.0 - subspace_momentum : float + subspace_momentum : float, default 0.5 Momentum parameter for 'MomentumSGD' method, should be a float between 0 and 1. - random_state : None or int or RandomState instance, default None + random_state : None or int or RandomState, default None Used to initialize the subspace on the first iteration. Returns ------- - numpy arrays + numpy.ndarray * If project is True, returns the low-rank factors and loadings only * Otherwise, returns the low-rank and sparse error matrices, as well as the results of a singular value decomposition (SVD) applied to the low-rank matrix. """ - if kwargs.get("learning_rate", False): - warnings.warn( - "The argument `learning_rate` has been deprecated and may " - "be removed in future. Please use `subspace_learning_rate` instead.", - VisibleDeprecationWarning, - ) - subspace_learning_rate = kwargs["learning_rate"] - if kwargs.get("momentum", False): - warnings.warn( - "The argument `momentum` has been deprecated and may " - "be removed in future. Please use `subspace_momentum` instead.", - VisibleDeprecationWarning, - ) - subspace_momentum = kwargs["momentum"] X = X.T diff --git a/hyperspy/learn/svd_pca.py b/hyperspy/learn/svd_pca.py index efa9129563..0a202054a1 100644 --- a/hyperspy/learn/svd_pca.py +++ b/hyperspy/learn/svd_pca.py @@ -1,35 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging -import warnings -from distutils.version import LooseVersion import numpy as np -import scipy -from scipy.linalg import svd -from scipy.sparse.linalg import svds +from numpy.linalg import svd -from hyperspy.exceptions import VisibleDeprecationWarning from hyperspy.misc.machine_learning.import_sklearn import ( randomized_svd, sklearn_installed, ) +from hyperspy.misc.utils import is_cupy_array _logger = logging.getLogger(__name__) @@ -42,7 +38,7 @@ def svd_flip_signs(u, v, u_based_decision=True): Parameters ---------- - u, v : numpy array + u, v : numpy.ndarray u and v are the outputs of a singular value decomposition. u_based_decision : bool, default True If True, use the columns of u as the basis for sign flipping. @@ -51,7 +47,7 @@ def svd_flip_signs(u, v, u_based_decision=True): Returns ------- - u, v : numpy array + u, v : numpy.ndarray Adjusted outputs with same dimensions as inputs. """ @@ -60,11 +56,11 @@ def svd_flip_signs(u, v, u_based_decision=True): # All rights reserved. if u_based_decision: - max_abs_cols = np.argmax(np.abs(u), axis=0) - signs = np.sign(u[max_abs_cols, range(u.shape[1])]) + max_abs_cols = np.argmax(abs(u), axis=0) + signs = np.sign(u[max_abs_cols, list(range(u.shape[1]))]) else: - max_abs_rows = np.argmax(np.abs(v), axis=1) - signs = np.sign(v[range(v.shape[0]), max_abs_rows]) + max_abs_rows = np.argmax(abs(v), axis=1) + signs = np.sign(v[list(range(v.shape[0])), max_abs_rows]) u *= signs v *= signs[:, np.newaxis] @@ -84,32 +80,32 @@ def svd_solve( Parameters ---------- - data : numpy array, shape (m, n) - Input data array + data : numpy.ndarray + Input data array with shape (m, n) output_dimension : None or int Number of components to keep/calculate svd_solver : {"auto", "full", "arpack", "randomized"}, default "auto" - If auto: - The solver is selected by a default policy based on `data.shape` and - `output_dimension`: if the input data is larger than 500x500 and the - number of components to extract is lower than 80% of the smallest - dimension of the data, then the more efficient "randomized" - method is enabled. Otherwise the exact full SVD is computed and - optionally truncated afterwards. - If full: - run exact SVD, calling the standard LAPACK solver via - :py:func:`scipy.linalg.svd`, and select the components by postprocessing - If arpack: - use truncated SVD, calling ARPACK solver via - :py:func:`scipy.sparse.linalg.svds`. It requires strictly - `0 < output_dimension < min(data.shape)` - If randomized: - use truncated SVD, calling :py:func:`sklearn.utils.extmath.randomized_svd` - to estimate a limited number of components + - If ``"auto"``: + The solver is selected by a default policy based on `data.shape` and + `output_dimension`: if the input data is larger than 500x500 and the + number of components to extract is lower than 80% of the smallest + dimension of the data, then the more efficient "randomized" + method is enabled. Otherwise the exact full SVD is computed and + optionally truncated afterwards. + - If ``"full"``: + Run exact SVD, calling the standard LAPACK solver via + :func:`scipy.linalg.svd`, and select the components by postprocessing + - If ``"arpack"``: + Use truncated SVD, calling ARPACK solver via + :func:`scipy.sparse.linalg.svds`. It requires strictly + `0 < output_dimension < min(data.shape)` + - If ``"randomized"``: + Use truncated SVD, calling :func:`sklearn.utils.extmath.randomized_svd` + to estimate a limited number of components svd_flip : bool, default True If True, adjusts the signs of the loadings and factors such that the loadings that are largest in absolute value are always positive. - See :py:func:`~.learn.svd_pca.svd_flip` for more details. + See :func:`~hyperspy.learn.svd_pca.svd_flip_signs` for more details. u_based_decision : bool, default True If True, and svd_flip is True, use the columns of u as the basis for sign-flipping. Otherwise, use the rows of v. The choice of which variable to base the @@ -117,7 +113,7 @@ def svd_solve( Returns ------- - U, S, V : numpy array + U, S, V : numpy.ndarray Output of SVD such that X = U*S*V.T """ @@ -151,14 +147,15 @@ def svd_solve( ) U, S, V = randomized_svd(data, n_components=output_dimension, **kwargs) elif svd_solver == "arpack": - if LooseVersion(scipy.__version__) < LooseVersion("1.4.0"): # pragma: no cover - raise ValueError('`svd_solver="arpack"` requires scipy >= 1.4.0') - if output_dimension >= min(m, n): raise ValueError( "svd_solver='arpack' requires output_dimension " "to be strictly less than min(data.shape)." ) + if is_cupy_array(data): # pragma: no cover + from cupyx.scipy.sparse.linalg import svds + else: + from scipy.sparse.linalg import svds U, S, V = svds(data, k=output_dimension, **kwargs) # svds doesn't follow scipy.linalg.svd conventions, # so reverse its outputs @@ -210,57 +207,39 @@ def svd_pca( optionally truncated afterwards. If full: run exact SVD, calling the standard LAPACK solver via - :py:func:`scipy.linalg.svd`, and select the components by postprocessing + :func:`scipy.linalg.svd`, and select the components by postprocessing If arpack: use truncated SVD, calling ARPACK solver via - :py:func:`scipy.sparse.linalg.svds`. It requires strictly + :func:`scipy.sparse.linalg.svds`. It requires strictly `0 < output_dimension < min(data.shape)` If randomized: - use truncated SVD, calling :py:func:`sklearn.utils.extmath.randomized_svd` + use truncated SVD, calling :func:`sklearn.utils.extmath.randomized_svd` to estimate a limited number of components centre : {None, "navigation", "signal"}, default None * If None, the data is not centered prior to decomposition. - * If "navigation", the data is centered along the navigation axis. - * If "signal", the data is centered along the signal axis. + * If ``"navigation"``, the data is centered along the navigation axis. + * If ``"signal"``, the data is centered along the signal axis. auto_transpose : bool, default True If True, automatically transposes the data to boost performance. svd_flip : bool, default True If True, adjusts the signs of the loadings and factors such that the loadings that are largest in absolute value are always positive. - See :py:func:`~.learn.svd_pca.svd_flip` for more details. + See :func:`~hyperspy.learn.svd_pca.svd_flip_signs` for more details. Returns ------- - factors : numpy array - loadings : numpy array - explained_variance : numpy array - mean : numpy array or None (if centre is None) + factors : numpy.ndarray + loadings : numpy.ndarray + explained_variance : numpy.ndarray + mean : numpy.ndarray or None + None if centre is None """ N, M = data.shape if centre is None: mean = None - else: - # To avoid confusion between terminology in different - # machine learning fields, we map the argument here. - # See #1159 for some discussion. - if centre in ["variables", "trials"]: - centre_map = { - "trials": "navigation", - "variables": "signal", - } - centre_new = centre_map.get(centre, None) - - warnings.warn( - f"centre='{centre}' has been deprecated and will be " - f"removed in HyperSpy 2.0. Please use '{centre_new}' instead.", - VisibleDeprecationWarning, - ) - - centre = centre_new - if centre == "signal": mean = data.mean(axis=1)[:, np.newaxis] elif centre == "navigation": @@ -285,7 +264,7 @@ def svd_pca( **kwargs, ) - explained_variance = S ** 2 / N + explained_variance = S**2 / N if auto_transpose is False: factors = V.T diff --git a/hyperspy/learn/whitening.py b/hyperspy/learn/whitening.py index f88942d60e..48e2336798 100644 --- a/hyperspy/learn/whitening.py +++ b/hyperspy/learn/whitening.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -38,22 +38,22 @@ def whiten_data(X, centre=True, method="PCA", epsilon=1e-10): Parameters ---------- - X : numpy array, shape (m, n) - The input data. + X : numpy,ndarray + The input data with shape (m, n). centre : bool, default True If True, centre the data along the features axis. If False, do not centre the data. method : {"PCA", "ZCA"} How to whiten the data. The default is PCA whitening. - epsilon : float + epsilon : float, default 1e-10 Small floating-point value to avoid divide-by-zero errors. Returns ------- - Y : numpy array, shape (m, n) - The centred and whitened data. - W : numpy array, shape (n, n) - The whitening matrix. + Y : numpy.ndarray + The centred and whitened data with shape (m, n). + W : numpy.ndarray + The whitening matrix with shape (n, n). References ---------- diff --git a/hyperspy/logger.py b/hyperspy/logger.py index dacc145018..ac1b0620f5 100644 --- a/hyperspy/logger.py +++ b/hyperspy/logger.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . + +import logging +import sys def set_log_level(level): @@ -36,32 +39,84 @@ def set_log_level(level): - 'DEBUG' - 'NOTSET' - Example - ------- + Examples + -------- For normal logging of hyperspy functions, you can set the log level like this: >>> import hyperspy.api as hs >>> hs.set_log_level('INFO') - >>> hs.load(r'my_file.dm3') - INFO:hyperspy.io_plugins.digital_micrograph:DM version: 3 - INFO:hyperspy.io_plugins.digital_micrograph:size 4796607 B - INFO:hyperspy.io_plugins.digital_micrograph:Is file Little endian? True - INFO:hyperspy.io_plugins.digital_micrograph:Total tags in root group: 15 + >>> hs.load('my_file.dm3') # doctest: +SKIP + INFO:rsciio.digital_micrograph:DM version: 3 + INFO:rsciio.digital_micrograph:size 4796607 B + INFO:rsciio.digital_micrograph:Is file Little endian? True + INFO:rsciio.digital_micrograph:Total tags in root group: 15 If you need the log output during the initial import of hyperspy, you should set the log level like this: >>> from hyperspy.logger import set_log_level - >>> set_log_level('DEBUG') - >>> import hyperspy.api as hs + >>> hs.set_log_level('DEBUG') + >>> import hyperspy.api as hs # doctest: +SKIP DEBUG:hyperspy.gui:Loading hyperspy.gui DEBUG:hyperspy.gui:Current MPL backend: TkAgg DEBUG:hyperspy.gui:Current ETS toolkit: qt4 DEBUG:hyperspy.gui:Current ETS toolkit set to: null """ - import logging - logging.basicConfig() # Does nothing if already configured - logging.getLogger('hyperspy').setLevel(level) + logger = initialize_logger("hyperspy") + logger.setLevel(level) + + +class ColoredFormatter(logging.Formatter): + """Colored log formatter. + + This class is used to format the log output. The colors can be changed + by changing the ANSI escape codes in the class variables. + + This is a modified version of both this + https://github.com/herzog0/best_python_logger + and this https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output/56944256#56944256 + """ + + grey = "\x1b[38;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + green = "\x1b[1;32m" + format = "%(levelname)s | Hyperspy | %(message)s (%(name)s:%(lineno)d)" + + FORMATS = { + logging.DEBUG: grey + format + reset, + logging.INFO: green + format + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: bold_red + format + reset, + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +def initialize_logger(*args): + """Creates a pretty logging instance where the colors can be changed + via the ColoredFormatter class. Any arguments passed to initialize_logger + will be passed to `logging.getLogger` + + The logging output will also be redirected from the standard error file to + the standard output file. + """ + formatter = ColoredFormatter() + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + + _logger = logging.getLogger(*args) + # Remove existing handler + while len(_logger.handlers): + _logger.removeHandler(_logger.handlers[0]) + _logger.addHandler(handler) + return _logger diff --git a/hyperspy/misc/__init__.py b/hyperspy/misc/__init__.py index c41a8ae211..3d813a7dda 100644 --- a/hyperspy/misc/__init__.py +++ b/hyperspy/misc/__init__.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . diff --git a/hyperspy/misc/array_tools.py b/hyperspy/misc/array_tools.py index 578ba26a5b..e615edcf46 100644 --- a/hyperspy/misc/array_tools.py +++ b/hyperspy/misc/array_tools.py @@ -1,35 +1,30 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see +# along with HyperSpy. If not, see -from collections import OrderedDict -from distutils.version import LooseVersion -import math as math import logging +import math -import dask import dask.array as da import numpy as np -from numba import njit - -from hyperspy.misc.math_tools import anyfloatin +from hyperspy.decorators import jit_ifnumba from hyperspy.docstrings.utils import REBIN_ARGS - +from hyperspy.misc.math_tools import anyfloatin _logger = logging.getLogger(__name__) @@ -46,7 +41,7 @@ def get_array_memory_size_in_GiB(shape, dtype): """ if not isinstance(dtype, np.dtype): dtype = np.dtype(dtype) - return np.array(shape).cumprod()[-1] * dtype.itemsize / 2.0 ** 30 + return np.array(shape).cumprod()[-1] * dtype.itemsize / 2.0**30 def are_aligned(shape1, shape2): @@ -87,7 +82,9 @@ def homogenize_ndim(*args): def _requires_linear_rebin(arr, scale): - """Returns True if linear_rebin is required. + """ + Returns True if linear_rebin is required. + Parameters ---------- arr: array @@ -100,31 +97,34 @@ def _requires_linear_rebin(arr, scale): def rebin(a, new_shape=None, scale=None, crop=True, dtype=None): - """Rebin data into a smaller or larger array based on a linear + """ + Rebin data into a smaller or larger array based on a linear interpolation. Specify either a new_shape or a scale. Scale of 1 means no binning. Scale less than one results in up-sampling. Parameters ---------- - a : numpy array + a : numpy.ndarray The array to rebin. %s Returns ------- - numpy array + numpy.ndarray Examples -------- - >>> a=rand(6,4); b=rebin(a,scale=(3,2)) - >>> a=rand(6); b=rebin(a,scale=(2,)) + >>> a = np.random.random((6, 4)) + >>> b = rebin(a, scale=(3, 2)) + >>> b.shape + (2, 2) Notes ----- Fast ``re_bin`` function Adapted from scipy cookbook If rebin function fails with error stating that the function is 'not binned and therefore cannot be rebinned', add binned to axes parameters with: - >>> s.axes_manager[axis].is_binned = True + >>> s.axes_manager[axis].is_binned = True # doctest: +SKIP """ # Series of if statements to check that only one out of new_shape or scale @@ -138,22 +138,18 @@ def rebin(a, new_shape=None, scale=None, crop=True, dtype=None): scale = [] for i, _ in enumerate(a.shape): scale.append(a.shape[i] / new_shape[i]) - else: - new_shape = new_shape - scale = scale - if isinstance(dtype, str) and dtype != 'same': + if isinstance(dtype, str) and dtype != "same": raise ValueError( - '`dtype` argument needs to be None, a numpy dtype or ' - 'the string "same".' - ) + "`dtype` argument needs to be None, a numpy dtype or " 'the string "same".' + ) # check whether or not interpolation is needed. if _requires_linear_rebin(arr=a, scale=scale): _logger.debug("Using linear_bin") return _linear_bin(a, scale, crop, dtype=dtype) else: - if dtype == 'same': - dtype = a.dtype + if dtype == "same": + dtype = a.dtype.name _logger.debug("Using standard rebin with lazy support") # if interpolation is not needed run fast re_bin function. # Adapted from scipy cookbook. @@ -178,21 +174,14 @@ def rebin(a, new_shape=None, scale=None, crop=True, dtype=None): rshape = () for athing in zip(new_shape, scale): rshape += athing - return a.reshape(rshape).sum(axis=tuple( - 2 * i + 1 for i in range(lenShape)), dtype=dtype) + return a.reshape(rshape).sum( + axis=tuple(2 * i + 1 for i in range(lenShape)), dtype=dtype + ) else: try: - kwargs = {} - if LooseVersion(dask.__version__) >= LooseVersion('2.11.0'): - kwargs['dtype'] = dtype - elif dtype is not None: - raise ValueError( - 'Using the dtype argument for lazy signal requires ' - 'dask >= 2.11.0.' - ) - return da.coarsen(np.sum, a, - {i: int(f) for i, f in enumerate(scale)}, - **kwargs) + return da.coarsen( + np.sum, a, {i: int(f) for i, f in enumerate(scale)}, dtype=dtype + ) # we provide slightly better error message in hyperspy context except ValueError: raise ValueError( @@ -200,11 +189,12 @@ def rebin(a, new_shape=None, scale=None, crop=True, dtype=None): "Rebin fewer dimensions at a time to avoid this error" ) + # Replacing space is necessary to get the correct indentation rebin.__doc__ %= REBIN_ARGS.replace(" ", " ") -@njit(cache=True) +@jit_ifnumba(cache=True) def _linear_bin_loop(result, data, scale): # pragma: no cover for j in range(result.shape[0]): # Begin by determining the upper and lower limits of a given new pixel. @@ -284,16 +274,17 @@ def _linear_bin(dat, scale, crop=True, dtype=None): ) # Unsuported dtype value argument - dtype_str_same_integer = (isinstance(dtype, str) and dtype == 'same' and - np.issubdtype(dat.dtype, np.integer)) - dtype_interger = (not isinstance(dtype, str) and - np.issubdtype(dtype, np.integer)) + dtype_str_same_integer = ( + isinstance(dtype, str) + and dtype == "same" + and np.issubdtype(dat.dtype, np.integer) + ) + dtype_interger = not isinstance(dtype, str) and np.issubdtype(dtype, np.integer) if dtype_str_same_integer or dtype_interger: raise ValueError( - "Linear interpolation requires float dtype, change the " - "dtype argument." - ) + "Linear interpolation requires float dtype, change the " "dtype argument." + ) if np.issubdtype(dat.dtype, np.integer): # The _linear_bin function below requires a float dtype @@ -343,58 +334,6 @@ def _linear_bin(dat, scale, crop=True, dtype=None): return result -def sarray2dict(sarray, dictionary=None): - """Converts a struct array to an ordered dictionary - - Parameters - ---------- - sarray: struct array - dictionary: None or dict - If dictionary is not None the content of sarray will be appended to the - given dictonary - - Returns - ------- - Ordered dictionary - - """ - if dictionary is None: - dictionary = OrderedDict() - for name in sarray.dtype.names: - dictionary[name] = sarray[name][0] if len(sarray[name]) == 1 else sarray[name] - return dictionary - - -def dict2sarray(dictionary, sarray=None, dtype=None): - """Populates a struct array from a dictionary - - Parameters - ---------- - dictionary: dict - sarray: struct array or None - Either sarray or dtype must be given. If sarray is given, it is - populated from the dictionary. - dtype: None, numpy dtype or dtype list - If sarray is None, dtype must be given. If so, a new struct array - is created according to the dtype, which is then populated. - - Returns - ------- - Structure array - - """ - if sarray is None: - if dtype is None: - raise ValueError("Either sarray or dtype need to be specified.") - sarray = np.zeros((1,), dtype=dtype) - for name in set(sarray.dtype.names).intersection(set(dictionary.keys())): - if len(sarray[name]) == 1: - sarray[name][0] = dictionary[name] - else: - sarray[name] = dictionary[name] - return sarray - - def numba_histogram(data, bins, ranges): """ Parameters @@ -417,7 +356,7 @@ def numba_histogram(data, bins, ranges): return _numba_histogram(data, bins, ranges) -@njit(cache=True) +@jit_ifnumba(cache=True) def _numba_histogram(data, bins, ranges): """ Numba histogram computation requiring native endian datatype. @@ -470,7 +409,7 @@ def get_signal_chunk_slice(index, chunks): raise ValueError("Index out of signal range.") -@njit(cache=True) +@jit_ifnumba(cache=True) def numba_closest_index_round(axis_array, value_array): """For each value in value_array, find the closest value in axis_array and return the result as a numpy array of the same shape as value_array. @@ -492,11 +431,13 @@ def numba_closest_index_round(axis_array, value_array): rtol = 1e-12 machineepsilon = np.min(np.abs(np.diff(axis_array))) * rtol for i, v in enumerate(value_array.flat): - index_array.flat[i] = np.abs(axis_array - v + np.sign(v) * machineepsilon).argmin() + index_array.flat[i] = np.abs( + axis_array - v + np.sign(v) * machineepsilon + ).argmin() return index_array -@njit(cache=True) +@jit_ifnumba(cache=True) def numba_closest_index_floor(axis_array, value_array): # pragma: no cover """For each value in value_array, find the closest smaller value in axis_array and return the result as a numpy array of the same shape @@ -522,7 +463,7 @@ def numba_closest_index_floor(axis_array, value_array): # pragma: no cover return index_array -@njit(cache=True) +@jit_ifnumba(cache=True) def numba_closest_index_ceil(axis_array, value_array): # pragma: no cover """For each value in value_array, find the closest larger value in axis_array and return the result as a numpy array of the same shape @@ -547,7 +488,7 @@ def numba_closest_index_ceil(axis_array, value_array): # pragma: no cover return index_array -@njit(cache=True) +@jit_ifnumba(cache=True) def round_half_towards_zero(array, decimals=0): # pragma: no cover """ Round input array using "half towards zero" strategy. @@ -565,15 +506,67 @@ def round_half_towards_zero(array, decimals=0): # pragma: no cover rounded_array : ndarray An array of the same type as a, containing the rounded values. """ - multiplier = 10 ** decimals + multiplier = 10**decimals + + return np.where( + array >= 0, + np.ceil(array * multiplier - 0.5) / multiplier, + np.floor(array * multiplier + 0.5) / multiplier, + ) + + +def get_value_at_index( + array, + indexes, + real_index, + factor=1.0, + norm=None, + minimum_intensity=None, + start=None, + stop=1.0, +): + """Get the value at the given index. - return np.where(array >= 0, - np.ceil(array * multiplier - 0.5) / multiplier, - np.floor(array * multiplier + 0.5) / multiplier - ) + Parameters + ---------- + array : 1D numpy array + Input array. + indexes : list of float + Indexes of the array to find the value at. + factor: float or 1D numpy array + Factor to multiply the value at the index with. + norm : str, optional + Normalization to apply to the intensities. Can be 'log' or None. + minimum_intensity : float, optional + Minimum intensity to use when norm is 'log'. + start : float, optional + Start value for scaling the vertical line + stop : float, optional + Stop value for scaling the vertical line or height of the point. + real_index : 1D numpy array + The real values for the indexes in calibrated units. + """ + if norm == "log" and minimum_intensity is None: + raise ValueError("minimum_intensity must be provided when norm is log") + factor = np.asarray(factor) + intensities = array[indexes] * factor + stop_intensities = intensities * stop + # set minimum_intensity so that zeros are not plotted causing errors + if norm == "log": + stop_intensities[stop_intensities < minimum_intensity] = minimum_intensity + stop_ = np.stack((real_index, stop_intensities), axis=1) + if start is not None: # make lines from start to stop + start_intensities = intensities * start + # set minimum_intensity so that zeros are not plotted causing errors + if norm == "log": + start_intensities[start_intensities < minimum_intensity] = minimum_intensity + start_ = np.stack((real_index, start_intensities), axis=1) + return np.stack((start_, stop_), axis=1) + else: + return stop_ -@njit(cache=True) +@jit_ifnumba(cache=True) def round_half_away_from_zero(array, decimals=0): # pragma: no cover """ Round input array using "half away from zero" strategy. @@ -591,9 +584,71 @@ def round_half_away_from_zero(array, decimals=0): # pragma: no cover rounded_array : ndarray An array of the same type as a, containing the rounded values. """ - multiplier = 10 ** decimals + multiplier = 10**decimals + + return np.where( + array >= 0, + np.floor(array * multiplier + 0.5) / multiplier, + np.ceil(array * multiplier - 0.5) / multiplier, + ) + + +def _get_navigation_dimension_chunk_slice(navigation_indices, chunks): + """Get the slice necessary to get the dask data chunk containing the + navigation indices. + + Parameters + ---------- + navigation_indices : iterable + chunks : iterable + + Returns + ------- + chunk_slice : list of slices + + Examples + -------- + Making all the variables + - return np.where(array >= 0, - np.floor(array * multiplier + 0.5) / multiplier, - np.ceil(array * multiplier - 0.5) / multiplier - ) + >>> from hyperspy._signals.lazy import _get_navigation_dimension_chunk_slice + >>> data = da.random.random((128, 128, 256, 256), chunks=(32, 32, 32, 32)) + >>> s = hs.signals.Signal2D(data).as_lazy() + + >>> sig_dim = s.axes_manager.signal_dimension + >>> nav_chunks = s.data.chunks[:-sig_dim] + >>> navigation_indices = s.axes_manager._getitem_tuple[:-sig_dim] + + The navigation index here is (0, 0), giving us the slice which contains + this index. + + >>> chunk_slice = _get_navigation_dimension_chunk_slice(navigation_indices, nav_chunks) + >>> print(chunk_slice) + (slice(0, 32, None), slice(0, 32, None)) + >>> data_chunk = data[chunk_slice] + + Moving the navigator to a new position, by directly setting the indices. + Normally, this is done by moving the navigator while plotting the data. + Note the "inversion" of the axes here: the indices is given in (x, y), + while the chunk_slice is given in (y, x). + + >>> s.axes_manager.indices = (127, 70) + >>> navigation_indices = s.axes_manager._getitem_tuple[:-sig_dim] + >>> chunk_slice = _get_navigation_dimension_chunk_slice(navigation_indices, nav_chunks) + >>> print(chunk_slice) + (slice(64, 96, None), slice(96, 128, None)) + >>> data_chunk = data[chunk_slice] + + """ + chunk_slice_list = da.core.slices_from_chunks(chunks) + for chunk_slice in chunk_slice_list: + is_slice = True + for index_nav in range(len(navigation_indices)): + temp_slice = chunk_slice[index_nav] + nav = navigation_indices[index_nav] + if not (temp_slice.start <= nav < temp_slice.stop): + is_slice = False + break + if is_slice: + return chunk_slice + return False diff --git a/hyperspy/misc/axis_tools.py b/hyperspy/misc/axis_tools.py index 2fefa02640..5d945d5759 100644 --- a/hyperspy/misc/axis_tools.py +++ b/hyperspy/misc/axis_tools.py @@ -1,23 +1,24 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see +# along with HyperSpy. If not, see import numpy as np -from pint.unit import Unit + +from hyperspy.api import _ureg def check_axes_calibration(ax1, ax2, rtol=1e-7): @@ -43,14 +44,13 @@ def check_axes_calibration(ax1, ax2, rtol=1e-7): """ if ax1.size == ax2.size: try: - unit1 = Unit(ax1.units) - except: + unit1 = _ureg.Unit(ax1.units) + except Exception: unit1 = ax1.units - pass try: unit2 = ax2.units - unit2 = Unit(ax2.units) - except: + unit2 = _ureg.Unit(ax2.units) + except Exception: pass if np.allclose(ax1.axis, ax2.axis, atol=0, rtol=rtol) and unit1 == unit2: return True diff --git a/hyperspy/misc/config_dir.py b/hyperspy/misc/config_dir.py deleted file mode 100644 index 510b259557..0000000000 --- a/hyperspy/misc/config_dir.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import shutil -import logging -from pathlib import Path - -_logger = logging.getLogger(__name__) - -config_files = list() -config_path = Path("~/.hyperspy").expanduser() -config_path.mkdir(parents=True, exist_ok=True) - -data_path = Path(__file__).resolve().parents[1].joinpath("data") - -for file in config_files: - templates_file = data_path.joinpath(file) - config_file = config_path.joinpath(file) - if not config_file.is_file(): - _logger.info(f"Setting configuration file: {file}") - shutil.copy(templates_file, config_file) diff --git a/hyperspy/misc/dask_widgets/lazy_signal.html.j2 b/hyperspy/misc/dask_widgets/lazy_signal.html.j2 new file mode 100644 index 0000000000..78200685a9 --- /dev/null +++ b/hyperspy/misc/dask_widgets/lazy_signal.html.j2 @@ -0,0 +1,65 @@ + + + + + +
+ + + + + + + + + + + + + + + + + + + + {% if nbytes %} + + + + + + {% endif %} + + + + + + + + + + + + + + + + +
Title: {{title}}
SignalType: {{signal_type}}
Array Chunk
Bytes {{ nbytes }} {{ cbytes }}
Shape {{ dim }} {{ chunks }}
Count {{ array.__dask_graph__() | length }} Tasks {{ array.npartitions }} Chunks
Type {{ array.dtype }} {{ array._meta | type | typename }}
+
+ + + + + + + + + + + + + +

Navigation Axes

Signal Axes

{{ nav_grid }} {{ sig_grid }}
+
diff --git a/hyperspy/misc/date_time_tools.py b/hyperspy/misc/date_time_tools.py deleted file mode 100644 index 56b5daa601..0000000000 --- a/hyperspy/misc/date_time_tools.py +++ /dev/null @@ -1,177 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import datetime -from dateutil import tz, parser -import logging - -_logger = logging.getLogger(__name__) - - -def get_date_time_from_metadata(metadata, formatting='ISO'): - """ Get the date and time from a metadata tree. - - Parameters - ---------- - metadata : metadata object - formatting : string, ('ISO', 'datetime', 'datetime64') - Default: 'ISO'. This parameter set the formatting of the date, - and the time, it can be ISO 8601 string, datetime.datetime - or a numpy.datetime64 object. In the later case, the time zone - is not supported. - - Return - ---------- - string, datetime.datetime or numpy.datetime64 object - - Example - ------- - >>> s = hs.load("example1.msa") - >>> s.metadata - ├── General - │ ├── date = 1991-10-01 - │ ├── original_filename = example1.msa - │ ├── time = 12:00:00 - │ └── title = NIO EELS OK SHELL - - >>> s = get_date_time_from_metadata(s.metadata) - '1991-10-01T12:00:00' - >>> s = get_date_time_from_metadata(s.metadata, formatting='ISO') - '1991-10-01T12:00:00' - >>> s = get_date_time_from_metadata(s.metadata, formatting='datetime') - - >>> s = get_date_time_from_metadata(s.metadata, formatting='datetime64') - - """ - date = metadata.get_item('General.date') - time = metadata.get_item('General.time') - if date and time: - dt = parser.parse('%sT%s' % (date, time)) - time_zone = metadata.get_item('General.time_zone') - if time_zone: - dt = dt.replace(tzinfo=tz.gettz(time_zone)) - if dt.tzinfo is None: - # time_zone metadata must be offset string - dt = parser.parse('%sT%s%s' % (date, time, time_zone)) - - elif not date and time: - dt = parser.parse('%s' % time).time() - elif date and not time: - dt = parser.parse('%s' % date).date() - else: - return - - if formatting == 'ISO': - res = dt.isoformat() - if formatting == 'datetime': - res = dt - # numpy.datetime64 doesn't support time zone - if formatting == 'datetime64': - res = np.datetime64('%sT%s' % (date, time)) - - return res - - -def update_date_time_in_metadata(dt, metadata): - """ Update the date and time in a metadata tree. - - Parameters - ---------- - dt : date and time information: it can be a ISO 8601 string, - a datetime.datetime or a numpy.datetime64 object - metadata : metadata object to update - - Return - ---------- - metadata object - - Example - ------- - >>> s = hs.load("example1.msa") - >>> dt = '2016-12-12T12:12:12-05:00' - >>> s.metadata = update_date_time_in_metadata(dt, s.metadata) - >>> s.metadata - ├── General - │ ├── date = 2016-12-12 - │ ├── original_filename = example1.msa - │ ├── time = 12:12:12 - │ ├── time_zone = 'EST' - │ └── title = NIO EELS OK SHELL - """ - time_zone = None - if isinstance(dt, str): - dt = parser.parse(dt) - if isinstance(dt, np.datetime64): - dt_split = np.datetime_as_string(dt).split('T') - date = dt_split[0] - time = dt_split[1] - if isinstance(dt, datetime.datetime): - date = dt.date().isoformat() - time = dt.time().isoformat() - if dt.tzname(): - time_zone = dt.tzname() - elif dt.tzinfo: - time_zone = dt.isoformat()[-6:] - - metadata.set_item('General.date', date) - metadata.set_item('General.time', time) - if time_zone: - metadata.set_item('General.time_zone', time_zone) - elif metadata.has_item('General.time_zone'): - del metadata.General.time_zone - return metadata - - -def serial_date_to_ISO_format(serial): - """ - Convert serial_date to a tuple of string (date, time, time_zone) in ISO - format. By default, the serial date is converted in local time zone. - """ - dt_utc = serial_date_to_datetime(serial) - dt_local = dt_utc.astimezone(tz.tzlocal()) - return dt_local.date().isoformat(), dt_local.time().isoformat(), dt_local.tzname() - - -def ISO_format_to_serial_date(date, time, timezone='UTC'): - """ Convert ISO format to a serial date. """ - if timezone is None or timezone == 'Coordinated Universal Time': - timezone = 'UTC' - dt = parser.parse( - '%sT%s' % - (date, time)).replace( - tzinfo=tz.gettz(timezone)) - return datetime_to_serial_date(dt) - - -def datetime_to_serial_date(dt): - """ Convert datetime.datetime object to a serial date. """ - if dt.tzname() is None: - dt = dt.replace(tzinfo=tz.tzutc()) - origin = datetime.datetime(1899, 12, 30, tzinfo=tz.tzutc()) - delta = dt - origin - return float(delta.days) + (float(delta.seconds) / 86400.0) - - -def serial_date_to_datetime(serial): - """ Convert serial date to a datetime.datetime object. """ - # Excel date&time format - origin = datetime.datetime(1899, 12, 30, tzinfo=tz.tzutc()) - secs = (serial % 1.0) * 86400 - delta = datetime.timedelta(int(serial), secs) - return origin + delta diff --git a/hyperspy/misc/eds/__init__.py b/hyperspy/misc/eds/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hyperspy/misc/eds/example_signals/1D_EDS_SEM_Spectrum.hspy b/hyperspy/misc/eds/example_signals/1D_EDS_SEM_Spectrum.hspy deleted file mode 100644 index 724222e433..0000000000 Binary files a/hyperspy/misc/eds/example_signals/1D_EDS_SEM_Spectrum.hspy and /dev/null differ diff --git a/hyperspy/misc/eds/example_signals/1D_EDS_TEM_Spectrum.hspy b/hyperspy/misc/eds/example_signals/1D_EDS_TEM_Spectrum.hspy deleted file mode 100644 index 883029458f..0000000000 Binary files a/hyperspy/misc/eds/example_signals/1D_EDS_TEM_Spectrum.hspy and /dev/null differ diff --git a/hyperspy/misc/eds/ffast_mac.py b/hyperspy/misc/eds/ffast_mac.py deleted file mode 100644 index dea6c642d8..0000000000 --- a/hyperspy/misc/eds/ffast_mac.py +++ /dev/null @@ -1,191 +0,0 @@ -# Mass absoption coefficient Chantler2005 -# See http://physics.nist.gov/ffast -# Chantler, C.T., Olsen, K., Dragoset, R.A., Kishore, A.R., Kotochigova, -# S.A., and Zucker, D.S. (2005), X-Ray Form Factor, Attenuation and -# Scattering Tables (version 2.1). -# units cm^2/g - -from hyperspy.misc import utils - -ffast_mac = {'Ru': {'mass_absorption_coefficient (cm2/g)': [199360.0, 198330.0, 196750.0, 194530.0, 191680.0, 188160.0, 184010.0, 179240.0, 173900.0, 168030.0, 161720.0, 155050.0, 148080.0, 140920.0, 133640.0, 126320.0, 119050.0, 111890.0, 104900.0, 97628.0, 88273.0, 83018.0, 81057.0, 80545.0, 137480.0, 136560.0, 130800.0, 109930.0, 90227.0, 75783.0, 64664.0, 55753.0, 48403.0, 42219.0, 37562.0, 36946.0, 36449.0, 36161.0, 36630.0, 35616.0, 33344.0, 29474.0, 26109.0, 23171.0, 20599.0, 18343.0, 16360.0, 14614.0, 13075.0, 11715.0, 10512.0, 9444.8, 8496.1, 7649.7, 6892.5, 6213.3, 5602.6, 5052.5, 4555.9, 4278.7, 4180.5, 4178.8, 4152.7, 7745.7, 7761.5, 7815.4, 7874.5, 10454.0, 10911.0, 12318.0, 15520.0, 18498.0, 20594.0, 21565.0, 21450.0, 20470.0, 20339.0, 20012.0, 19922.0, 22293.0, 21909.0, 21719.0, 21356.0, 21298.0, 21185.0, 22047.0, 21606.0, 20374.0, 18244.0, 16804.0, 16331.0, 16208.0, 16850.0, 16729.0, 16268.0, 14825.0, 12973.0, 11310.0, 9838.6, 8535.4, 7382.2, 6377.9, 5506.7, 4716.8, 4026.6, 3408.9, 2890.5, 2454.9, 2088.2, 1779.2, 1518.4, 1298.1, 1105.5, 938.86, 798.41, 679.61, 578.54, 493.06, 466.46, 449.9, 445.63, 1595.9, 1538.7, 1525.6, 1521.9, 1466.0, 1451.6, 1995.6, 1946.9, 1782.6, 1678.5, 1592.8, 1571.1, 1780.3, 1700.5, 1653.6, 1391.4, 1212.5, 1030.3, 866.03, 727.75, 611.86, 514.7, 433.08, 362.71, 303.45, 253.57, 212.08, 177.55, 148.78, 124.42, 103.21, 85.697, 71.222, 59.217, 49.218, 40.811, 33.786, 27.923, 23.098, 19.124, 15.848, 13.145, 10.912, 10.668, 10.227, 10.114, 67.868, 65.103, 61.789, 51.765, 43.567, 36.492, 30.434, 25.355, 21.127, 17.605, 14.672, 12.229, 10.18, 8.4239, 6.9713, 5.7697, 4.7756, 3.9528, 3.2718, 2.7068, 2.2387, 1.8492, 1.523, 1.2543, 1.0321, 0.84874, 0.69797, 0.57402, 0.4721, 0.38829, 0.31938, 0.2627, 0.2161, 0.17777, 0.14624, 0.12031, 0.098979, 0.081433, 0.067001, 0.055128, 0.04536, 0.037324, 0.030713, 0.025274, 0.020798, 0.017116, 0.014085, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.042238, 0.0428845, 0.0430569, 0.0433155, 0.04340198, 0.043962, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.073402, 0.07401695, 0.0745255, 0.0748251, 0.0752745, 0.076398, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.273812, 0.277928, 0.278003, 0.2791206, 0.280797, 0.2811158, 0.282182, 0.2833164, 0.285018, 0.289272, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.451388, 0.458297, 0.4601394, 0.462903, 0.469812, 0.473144, 0.4794098, 0.480386, 0.4823172, 0.485214, 0.492456, 0.5124891, 0.5478508, 0.5733, 0.582075, 0.584415, 0.5856525, 0.587925, 0.5967, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.781142, 2.82371, 2.835062, 2.852089, 2.894658, 2.904724, 2.907562, 2.952066, 2.963933, 2.981735, 3.026238, 3.10515, 3.15952, 3.20788, 3.220776, 3.24012, 3.28848, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 21.67486, 22.00661, 22.09508, 22.22779, 22.55954, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Re': {'mass_absorption_coefficient (cm2/g)': [12574.0, 12380.0, 11100.0, 11011.0, 10992.0, 50183.0, 49391.0, 72464.0, 79935.0, 87210.0, 93764.0, 99067.0, 102640.0, 104140.0, 103380.0, 100380.0, 95333.0, 88613.0, 80669.0, 71997.0, 63066.0, 54267.0, 45970.0, 38422.0, 31754.0, 29930.0, 28604.0, 28261.0, 56187.0, 54035.0, 53089.0, 44443.0, 39056.0, 37365.0, 36924.0, 41471.0, 40847.0, 39026.0, 33631.0, 30574.0, 29084.0, 28704.0, 38917.0, 37638.0, 37387.0, 31553.0, 26528.0, 22170.0, 18547.0, 15680.0, 13540.0, 12064.0, 11178.0, 10981.0, 10896.0, 10878.0, 11042.0, 10999.0, 10996.0, 11076.0, 11536.0, 12319.0, 13355.0, 14586.0, 15934.0, 17315.0, 18651.0, 19871.0, 20906.0, 21641.0, 22028.0, 22071.0, 21799.0, 21253.0, 20484.0, 19981.0, 19742.0, 19677.0, 20266.0, 20174.0, 20020.0, 19840.0, 19575.0, 19504.0, 19900.0, 19629.0, 19500.0, 18216.0, 16910.0, 15622.0, 14377.0, 13192.0, 12079.0, 11487.0, 11254.0, 11193.0, 11860.0, 11797.0, 11633.0, 10808.0, 10223.0, 10156.0, 10089.0, 10022.0, 9955.6, 9889.6, 9824.0, 9773.6, 9758.7, 9894.0, 9850.8, 9786.3, 9722.1, 9658.3, 9594.8, 9531.7, 9469.0, 9406.6, 9344.5, 9282.8, 9221.4, 9160.4, 9099.7, 9039.4, 8979.3, 8919.6, 8860.3, 8801.2, 8742.5, 8684.1, 8626.1, 8568.3, 8510.8, 8453.7, 8396.9, 8340.3, 8284.1, 8228.2, 8172.5, 8117.2, 8062.2, 8007.4, 7952.9, 7898.8, 7844.9, 7791.3, 7738.0, 7715.0, 7877.4, 7874.9, 7824.1, 7771.0, 7718.2, 7665.6, 7613.4, 7561.4, 7509.7, 7458.2, 7407.0, 7356.0, 7305.4, 7254.9, 7204.8, 7154.9, 7105.3, 7055.9, 7006.8, 6958.0, 6909.4, 6861.1, 6813.0, 6765.2, 6717.6, 6670.2, 6623.1, 6576.3, 6529.7, 6483.3, 6437.2, 6391.3, 6345.7, 6300.3, 6255.1, 6210.2, 6165.5, 6121.1, 6076.7, 6032.4, 5988.3, 5941.6, 5891.2, 5841.1, 5791.4, 5742.0, 5692.9, 5644.1, 5595.7, 5547.7, 5499.9, 5452.5, 5405.5, 5358.7, 5312.3, 5266.3, 5220.5, 5175.1, 5130.1, 5085.3, 5040.9, 4996.8, 4953.1, 4909.7, 4866.6, 4823.8, 4781.3, 4739.2, 4697.4, 4655.8, 4614.5, 4573.6, 4532.9, 4492.6, 4452.6, 4412.9, 4373.5, 4334.4, 4295.7, 4257.2, 4219.0, 4181.2, 4143.6, 4106.4, 4069.5, 4032.8, 3996.5, 3960.4, 3924.7, 3889.3, 3854.1, 3819.2, 3784.7, 3750.4, 3716.4, 3682.6, 3644.1, 3606.0, 3568.3, 3531.0, 3494.1, 3457.7, 3421.7, 3386.0, 3348.4, 3311.3, 3274.6, 3238.2, 3202.3, 3166.9, 3131.8, 3097.3, 3063.1, 3029.3, 2994.7, 2960.4, 2926.5, 2893.0, 2860.0, 2827.3, 2795.1, 2763.3, 2731.9, 2700.9, 2670.2, 2640.0, 2610.1, 2580.6, 2551.5, 2522.8, 2494.4, 2466.3, 2438.6, 2411.3, 2384.3, 2357.6, 2331.3, 2305.2, 2279.5, 2254.2, 2229.1, 2204.3, 2179.9, 2155.7, 2131.9, 2108.3, 2085.1, 2062.1, 2039.4, 2017.0, 1994.8, 1972.9, 1951.3, 1929.9, 1908.9, 1888.0, 1867.4, 1847.1, 1827.0, 1807.2, 1787.5, 1768.2, 1749.0, 1730.1, 1711.4, 1693.0, 1674.8, 1656.7, 1638.9, 1621.3, 1604.0, 1586.8, 1569.8, 1553.0, 1536.5, 1520.1, 1503.9, 1487.9, 1472.1, 1456.5, 1441.1, 1425.9, 1410.8, 1395.9, 1381.2, 1366.6, 1352.2, 1338.0, 1324.0, 1310.1, 1296.4, 1282.8, 1269.4, 1256.2, 1243.1, 1230.1, 1217.3, 1204.7, 1192.2, 1179.8, 1167.6, 1155.5, 1143.6, 1131.8, 1120.2, 1108.6, 1097.2, 1086.0, 1074.8, 1063.8, 1052.9, 1042.0, 1031.1, 1020.4, 1009.7, 999.2, 988.8, 978.52, 968.36, 958.32, 948.39, 938.57, 930.67, 3066.5, 3062.7, 3026.1, 2989.9, 2954.1, 2918.8, 2884.0, 2849.5, 2826.2, 4180.9, 4172.8, 4122.3, 4072.5, 4023.3, 3974.7, 3926.7, 3879.3, 3832.5, 3786.2, 3740.6, 3695.4, 3650.9, 3606.9, 3563.3, 3520.2, 3477.7, 3435.7, 3394.3, 3353.3, 3312.8, 3272.9, 3233.4, 3194.4, 3155.9, 3117.9, 3080.3, 3043.2, 3006.6, 2970.4, 2934.6, 2899.3, 2864.4, 2829.9, 2795.9, 2762.3, 2729.1, 2696.3, 2663.9, 2631.9, 2614.2, 3051.7, 3050.0, 3012.4, 2975.4, 2938.8, 2902.7, 2867.0, 2831.8, 2796.6, 2761.5, 2726.9, 2693.6, 2661.3, 2629.6, 2598.3, 2567.1, 2536.3, 2505.8, 2475.8, 2446.2, 2417.1, 2388.3, 2359.9, 2331.8, 2304.2, 2276.9, 2263.7, 2405.5, 2405.0, 2376.3, 2347.6, 2319.2, 2291.2, 2263.5, 2236.2, 2209.2, 2182.6, 2156.2, 2130.9, 2106.3, 2082.1, 2058.3, 2034.8, 2011.6, 1988.8, 1966.2, 1961.1, 2030.6, 2029.9, 2007.8, 1985.2, 1962.9, 1940.8, 1917.9, 1894.9, 1872.2, 1849.8, 1827.6, 1805.6, 1783.8, 1762.2, 1740.9, 1719.9, 1699.1, 1678.5, 1658.2, 1638.2, 1618.4, 1598.8, 1579.5, 1560.3, 1541.5, 1522.8, 1504.4, 1486.1, 1468.1, 1450.3, 1432.8, 1415.4, 1398.2, 1381.3, 1364.5, 1348.0, 1331.6, 1315.4, 1299.5, 1283.7, 1268.1, 1252.7, 1237.4, 1222.4, 1207.6, 1192.9, 1178.5, 1164.2, 1150.1, 1136.1, 1122.4, 1108.7, 1094.8, 1081.1, 1067.6, 1054.3, 1041.1, 1028.1, 1015.2, 1002.3, 989.48, 976.89, 964.46, 952.2, 940.11, 928.18, 916.42, 904.82, 893.37, 882.08, 870.94, 859.95, 849.11, 838.42, 827.87, 817.46, 807.2, 797.07, 787.08, 777.22, 767.5, 757.91, 748.44, 739.1, 729.89, 720.8, 711.84, 702.99, 694.26, 685.65, 677.15, 668.76, 660.49, 652.33, 644.27, 636.33, 628.48, 620.69, 612.84, 605.1, 597.46, 589.92, 582.49, 575.15, 567.87, 560.68, 553.59, 546.59, 539.69, 532.88, 526.17, 519.54, 513.0, 506.55, 500.17, 493.86, 487.64, 481.5, 475.44, 469.47, 463.58, 457.76, 452.0, 446.24, 440.57, 434.97, 429.44, 423.99, 418.61, 413.31, 408.07, 402.91, 397.81, 392.78, 387.82, 382.93, 378.1, 373.34, 368.63, 364.0, 359.41, 354.86, 350.37, 345.95, 341.58, 337.27, 333.02, 328.83, 324.69, 320.6, 316.57, 312.59, 308.67, 304.79, 300.97, 297.2, 293.48, 289.8, 286.18, 282.6, 279.07, 275.59, 272.15, 268.76, 265.41, 262.11, 258.85, 255.63, 252.45, 249.32, 246.22, 243.17, 240.16, 237.18, 234.25, 231.35, 228.49, 225.67, 222.88, 220.14, 217.42, 214.74, 212.1, 209.49, 206.92, 204.37, 201.86, 199.39, 196.94, 194.53, 192.15, 189.8, 187.48, 185.18, 182.92, 180.69, 178.49, 176.31, 174.16, 172.04, 169.95, 167.88, 165.84, 163.83, 161.84, 159.88, 157.94, 155.99, 154.07, 152.18, 150.31, 148.46, 146.63, 144.82, 143.01, 141.23, 122.4, 102.1, 85.275, 85.171, 81.694, 80.801, 215.03, 206.47, 192.12, 162.68, 159.87, 155.98, 154.26, 211.96, 203.57, 200.05, 191.94, 189.86, 216.47, 215.7, 208.44, 181.84, 152.93, 128.55, 107.92, 90.543, 75.994, 63.816, 53.585, 44.958, 37.732, 31.639, 26.324, 21.86, 18.169, 15.114, 12.583, 10.484, 8.7415, 7.2943, 6.0809, 5.0699, 4.2218, 3.5035, 2.9089, 2.4151, 2.1041, 2.0173, 2.0065, 1.9949, 10.048, 9.6829, 8.6629, 7.2598, 6.089, 5.1151, 4.2756, 3.5744, 2.9885, 2.4991, 2.0901, 1.7482, 1.4597, 1.2173, 1.0153, 0.84684, 0.70641, 0.58933, 0.49169, 0.41027, 0.34236, 0.28571, 0.23846, 0.19903, 0.16744, 0.1409, 0.11858, 0.0998, 0.084002, 0.0], - 'energies (keV)': [0.005235171, 0.005313308, 0.005941418, 0.006032358, 0.006056608, 0.006092984, 0.006183924, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.033908, 0.034427, 0.0345654, 0.034773, 0.035292, 0.03552846, 0.03797993, 0.039788, 0.040397, 0.0405594, 0.04060054, 0.040803, 0.041412, 0.04340198, 0.044688, 0.045372, 0.0455544, 0.045828, 0.04639671, 0.046512, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.081144, 0.082386, 0.0827172, 0.083214, 0.084456, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.254996, 0.258899, 0.2599398, 0.261501, 0.2629708, 0.265404, 0.268226, 0.2723315, 0.2734263, 0.2750685, 0.279174, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.435512, 0.442178, 0.4439556, 0.446622, 0.4484657, 0.453288, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.51717493, 0.5177647, 0.51862505, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62404375, 0.62581041, 0.62595625, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.8825065, 1.8832935, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9481516, 1.9496484, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3649801, 2.36962, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6780604, 2.6849291, 2.6851398, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9258366, 2.9371216, 2.9375634, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 10.32459, 10.48262, 10.52476, 10.58798, 10.74601, 11.03212, 11.71953, 11.79334, 11.89891, 11.94674, 12.01849, 12.19787, 12.27617, 12.46407, 12.51417, 12.58933, 12.60708, 12.77723, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 70.24287, 71.31802, 71.45536, 71.60472, 72.03478, 73.10993, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ra': {'mass_absorption_coefficient (cm2/g)': [210240.0, 195520.0, 180640.0, 130300.0, 95222.0, 70955.0, 53769.0, 41353.0, 32220.0, 25389.0, 20197.0, 16194.0, 13066.0, 10593.0, 9108.4, 8693.0, 8619.8, 8586.7, 9572.7, 9110.2, 7857.9, 6328.5, 5137.7, 4197.5, 3447.3, 2844.8, 2713.9, 2600.2, 2571.1, 249970.0, 239310.0, 232330.0, 191230.0, 147860.0, 103470.0, 67758.0, 42713.0, 26528.0, 16540.0, 10506.0, 6873.6, 4665.9, 3299.3, 2767.2, 2586.1, 2541.2, 3858.8, 3801.6, 3659.9, 3033.4, 2487.5, 2101.6, 1916.8, 1857.8, 1842.9, 2526.4, 2525.4, 2430.9, 2144.1, 1856.7, 1643.4, 1607.4, 1568.9, 1559.1, 1682.6, 1648.0, 1618.5, 1490.9, 1425.9, 1403.9, 1398.3, 1571.9, 1598.6, 1822.9, 2616.4, 4076.0, 6070.3, 8227.2, 10108.0, 11399.0, 11852.0, 11887.0, 11919.0, 11947.0, 11971.0, 11991.0, 12008.0, 12022.0, 12032.0, 12038.0, 12041.0, 12041.0, 12038.0, 12032.0, 12022.0, 12009.0, 11994.0, 11975.0, 11954.0, 11930.0, 11904.0, 11875.0, 11843.0, 11809.0, 11773.0, 11734.0, 11693.0, 11650.0, 11605.0, 11558.0, 11510.0, 11459.0, 11406.0, 11352.0, 11297.0, 11239.0, 11181.0, 11120.0, 11100.0, 11945.0, 11920.0, 11860.0, 11799.0, 11736.0, 11672.0, 11608.0, 11542.0, 11475.0, 11408.0, 11340.0, 11271.0, 11265.0, 11737.0, 11691.0, 11623.0, 11554.0, 11485.0, 11415.0, 11344.0, 11273.0, 11202.0, 11130.0, 11058.0, 10986.0, 10913.0, 10840.0, 10767.0, 10693.0, 10619.0, 10545.0, 10471.0, 10397.0, 10322.0, 10248.0, 10173.0, 10098.0, 10024.0, 9948.9, 9874.1, 9799.4, 9724.6, 9649.9, 9575.3, 9500.7, 9426.3, 9352.0, 9277.9, 9204.0, 9130.2, 9056.7, 8983.5, 8910.4, 8837.7, 8765.2, 8693.1, 8621.2, 8549.7, 8478.5, 8407.6, 8337.1, 8267.0, 8197.3, 8127.9, 8059.0, 7990.4, 7922.3, 7854.6, 7787.3, 7720.4, 7654.0, 7588.1, 7522.6, 7457.5, 7393.0, 7328.9, 7265.2, 7202.1, 7144.4, 7139.4, 7567.2, 7526.2, 7462.3, 7398.6, 7335.3, 7272.5, 7210.2, 7148.4, 7087.1, 7026.3, 6965.9, 6906.0, 6846.6, 6787.7, 6729.3, 6671.4, 6613.9, 6557.0, 6500.5, 6444.5, 6389.0, 6334.0, 6279.5, 6225.4, 6171.8, 6118.6, 6065.9, 6010.7, 5955.9, 5901.5, 5846.8, 5791.3, 5736.2, 5681.5, 5627.4, 5573.6, 5520.3, 5469.3, 5467.5, 5522.3, 5492.9, 5440.5, 5388.5, 5336.2, 5283.9, 5231.9, 5180.5, 5129.5, 5078.9, 5028.9, 4979.5, 4930.5, 4882.0, 4834.0, 4786.4, 4739.3, 4692.7, 4646.4, 4600.7, 4555.4, 4510.5, 4466.0, 4422.0, 4378.5, 4334.1, 4285.4, 4251.9, 4323.1, 4315.2, 4275.1, 4227.6, 4180.8, 4134.5, 4088.8, 4043.7, 3999.1, 3955.1, 3911.4, 3868.0, 3825.3, 3783.1, 3741.4, 3700.2, 3659.5, 3619.4, 3579.7, 3540.6, 3501.9, 3463.7, 3426.0, 3388.7, 3351.9, 3315.6, 3279.7, 3244.2, 3209.0, 3174.2, 3139.9, 3106.0, 3072.5, 3039.4, 3006.7, 2974.4, 2942.5, 2911.0, 2879.9, 2849.1, 2818.7, 2788.6, 2759.0, 2729.4, 2700.1, 2671.2, 2642.6, 2614.3, 2586.4, 2558.8, 2531.6, 2504.6, 2478.0, 2451.7, 2425.6, 2399.8, 2374.3, 2349.1, 2324.3, 2299.7, 2275.3, 2251.3, 2227.5, 2204.1, 2180.9, 2157.9, 2135.3, 2112.8, 2090.7, 2068.8, 2047.2, 2025.8, 2004.6, 1983.7, 1963.1, 1942.6, 1922.4, 1902.5, 1882.7, 1863.2, 1844.0, 1824.9, 1806.1, 1787.4, 1769.0, 1750.8, 1732.8, 1715.0, 1697.4, 1680.1, 1662.9, 1645.9, 1629.1, 1612.4, 1596.0, 1579.8, 1563.7, 1547.9, 1532.2, 1516.6, 1501.3, 1486.1, 1471.1, 1456.3, 1441.7, 1427.2, 1412.8, 1398.6, 1384.6, 1370.8, 1357.1, 1343.5, 1330.1, 1316.9, 1303.7, 1290.7, 1277.8, 1265.0, 1252.4, 1239.9, 1227.6, 1215.4, 1203.3, 1191.4, 1179.6, 1167.9, 1156.3, 1144.9, 1133.6, 1122.5, 1111.4, 1100.5, 1089.7, 1079.0, 1068.5, 1058.0, 1047.7, 1037.4, 1027.3, 1017.3, 1007.1, 996.65, 986.36, 976.18, 966.11, 956.16, 946.32, 936.59, 926.97, 917.45, 908.05, 898.44, 888.88, 879.43, 870.09, 860.86, 851.73, 842.71, 833.79, 824.97, 816.25, 807.63, 799.11, 790.69, 782.36, 774.13, 765.99, 757.94, 749.98, 742.12, 734.34, 726.65, 719.05, 711.54, 704.11, 696.75, 689.46, 682.26, 675.14, 668.09, 661.13, 654.25, 647.44, 640.71, 632.95, 624.93, 617.01, 609.21, 601.51, 593.92, 586.43, 585.83, 1532.0, 1515.6, 1495.4, 1475.5, 1455.8, 1436.4, 1417.3, 1398.5, 1379.9, 1361.6, 1359.1, 1959.4, 1940.0, 1914.2, 1888.7, 1863.5, 1838.7, 1814.2, 1790.1, 1766.3, 1742.8, 1719.6, 1696.8, 1674.2, 1652.0, 1630.1, 1608.4, 1587.1, 1565.9, 1545.1, 1524.5, 1504.2, 1484.2, 1464.4, 1444.9, 1425.7, 1406.8, 1388.1, 1369.6, 1351.4, 1333.5, 1315.8, 1299.0, 1298.3, 1510.3, 1500.0, 1481.2, 1462.6, 1444.3, 1426.2, 1408.4, 1390.7, 1373.4, 1356.2, 1339.2, 1322.5, 1305.9, 1289.6, 1273.2, 1257.1, 1241.2, 1225.5, 1210.0, 1194.7, 1179.5, 1164.6, 1149.9, 1135.2, 1120.7, 1106.4, 1092.3, 1078.3, 1064.6, 1051.0, 1037.6, 1024.3, 1011.2, 998.34, 988.4, 985.61, 1044.7, 1036.5, 1023.6, 1010.9, 998.4, 986.01, 973.76, 961.67, 949.73, 937.95, 926.32, 914.85, 903.54, 892.36, 881.66, 881.33, 910.8, 908.9, 897.94, 887.11, 876.42, 865.84, 855.31, 844.92, 834.65, 824.52, 814.51, 804.6, 794.82, 785.17, 775.63, 766.21, 756.91, 747.72, 738.64, 729.67, 720.82, 712.07, 703.44, 694.91, 686.49, 678.17, 669.96, 661.85, 653.84, 645.94, 638.13, 630.41, 622.77, 615.23, 607.78, 600.43, 593.17, 586.0, 578.92, 571.91, 564.95, 558.07, 551.28, 544.58, 537.96, 531.42, 524.97, 518.6, 512.3, 506.09, 499.96, 493.9, 487.83, 481.82, 475.88, 470.01, 464.22, 458.5, 452.86, 447.29, 441.79, 436.31, 430.89, 425.54, 420.26, 415.05, 409.9, 404.83, 399.81, 394.86, 389.98, 385.16, 380.4, 375.7, 371.06, 366.49, 361.97, 357.51, 353.1, 348.76, 344.47, 340.23, 336.05, 331.93, 327.86, 323.84, 319.87, 315.95, 312.08, 308.27, 304.5, 300.78, 297.07, 293.4, 289.78, 286.21, 282.68, 279.2, 275.76, 272.37, 269.02, 265.72, 262.46, 259.24, 256.06, 252.92, 249.83, 246.77, 243.76, 240.78, 237.84, 234.94, 232.08, 229.25, 226.46, 223.71, 194.51, 162.71, 136.21, 114.22, 95.879, 80.554, 67.779, 56.935, 50.072, 48.141, 47.869, 47.644, 117.99, 113.5, 101.05, 84.282, 77.895, 74.726, 73.91, 102.87, 99.499, 98.973, 98.945, 95.101, 94.104, 107.2, 103.3, 97.085, 81.871, 68.987, 58.142, 49.014, 41.071, 34.373, 28.745, 24.032, 20.113, 16.798, 14.002, 11.68, 9.7504, 8.146, 6.8107, 5.6985, 4.7716, 3.9983, 3.3462, 2.8028, 2.3426, 1.942, 1.6096, 1.3351, 1.2598, 1.2075, 1.194, 5.3627, 5.1646, 5.0868, 4.2898, 3.6138, 3.0412, 2.5585, 2.1518, 1.8075, 1.5183, 1.2755, 1.0715, 0.9003, 0.7567, 0.63628, 0.53509, 0.45004, 0.37855, 0.31845, 0.26792, 0.22543, 0.1897, 0.15964, 0.13436, 0.0], - 'energies (keV)': [0.018894, 0.019176, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04263, 0.0432825, 0.04340198, 0.0434565, 0.0437175, 0.04437, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.065856, 0.066864, 0.0671328, 0.067536, 0.068544, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.149744, 0.152036, 0.1526472, 0.153564, 0.1542005, 0.155856, 0.1648404, 0.1762144, 0.1883732, 0.196392, 0.199398, 0.2001996, 0.2013709, 0.201402, 0.204408, 0.2152655, 0.2301188, 0.245997, 0.249312, 0.253128, 0.2541456, 0.255672, 0.259488, 0.2629708, 0.2811158, 0.292922, 0.2974055, 0.2986011, 0.3005128, 0.304878, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60233476, 0.60306523, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.63551339, 0.63628665, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87813301, 0.87848628, 0.88006703, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0563415, 1.0565238, 1.0588586, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2069862, 1.2088236, 1.2098138, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1040119, 3.1057879, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2473832, 3.2494167, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7870982, 3.7878352, 3.7965019, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4829005, 4.4878381, 4.4960996, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8116809, 4.8124036, 4.8323191, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.13551, 15.36718, 15.40095, 15.42896, 15.52162, 15.75329, 16.46362, 17.59961, 18.11461, 18.39188, 18.46582, 18.57672, 18.81398, 18.85197, 18.85399, 19.14052, 19.21746, 19.33288, 19.62143, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 101.8435, 103.4023, 103.818, 104.4415, 106.0003, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Rb': {'mass_absorption_coefficient (cm2/g)': [1155.1, 1197.9, 1241.7, 1293.7, 2177.5, 2206.8, 2533.4, 3616.9, 7005.6, 11952.0, 18002.0, 24420.0, 30356.0, 35096.0, 38217.0, 39620.0, 39459.0, 38040.0, 37567.0, 37062.0, 36922.0, 39341.0, 38941.0, 38820.0, 38408.0, 38383.0, 38230.0, 39094.0, 38523.0, 36832.0, 33878.0, 30792.0, 28524.0, 27833.0, 27726.0, 27651.0, 28648.0, 27968.0, 25985.0, 23154.0, 20536.0, 18144.0, 15977.0, 14023.0, 12269.0, 10705.0, 9318.2, 8093.3, 7015.6, 6070.6, 5244.5, 4524.3, 3897.8, 3354.1, 2883.2, 2425.2, 2042.0, 1721.9, 1453.7, 1227.6, 1038.1, 879.02, 745.38, 679.83, 655.01, 648.62, 3109.1, 3068.7, 3037.3, 2964.2, 2892.3, 2855.2, 3979.8, 3797.3, 3543.2, 3184.6, 3059.8, 3027.9, 3435.0, 3413.0, 3310.1, 2897.9, 2470.0, 2095.0, 1774.1, 1500.6, 1264.5, 1062.0, 892.76, 747.35, 623.99, 520.11, 433.24, 360.87, 300.85, 251.04, 209.67, 175.18, 146.5, 122.62, 102.67, 85.691, 71.32, 58.603, 48.201, 39.683, 32.703, 26.976, 22.274, 18.409, 16.742, 16.035, 15.853, 120.04, 117.14, 114.83, 96.694, 81.066, 68.293, 57.49, 48.106, 40.198, 33.564, 28.02, 23.316, 19.381, 16.029, 13.243, 10.942, 9.0405, 7.4694, 6.1706, 5.0953, 4.2076, 3.4749, 2.8699, 2.3704, 1.9558, 1.6074, 1.3212, 1.0848, 0.88866, 0.72801, 0.59643, 0.48865, 0.40036, 0.32804, 0.26879, 0.22026, 0.18049, 0.14791, 0.12121, 0.099338, 0.081414, 0.066726, 0.05469, 0.044826, 0.036743, 0.030118, 0.024688, 0.020238, 0.01659, 0.0136, 0.01115, 0.009141, 0.0074943, 0.0], - 'energies (keV)': [0.1104581, 0.1108515, 0.111241, 0.1116882, 0.112359, 0.112506, 0.114036, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.23373, 0.2373075, 0.2382615, 0.2396925, 0.242452, 0.24327, 0.245997, 0.246163, 0.2471526, 0.248637, 0.252348, 0.2629708, 0.2811158, 0.3005128, 0.315658, 0.3204895, 0.3212482, 0.3217779, 0.3237105, 0.328542, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.768312, 1.795378, 1.802596, 1.813422, 1.820795, 1.826622, 1.840488, 1.85458, 1.862036, 1.873219, 1.901178, 1.94643, 2.023798, 2.054774, 2.063035, 2.075425, 2.080733, 2.106402, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 14.89571, 15.1237, 15.1845, 15.2757, 15.40095, 15.50369, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Rn': {'mass_absorption_coefficient (cm2/g)': [215510.0, 210090.0, 112670.0, 109430.0, 108580.0, 188900.0, 187050.0, 184380.0, 167450.0, 149260.0, 132160.0, 116160.0, 101300.0, 87647.0, 75226.0, 64058.0, 54131.0, 45409.0, 41689.0, 39986.0, 39546.0, 44411.0, 43067.0, 42431.0, 35016.0, 28410.0, 23027.0, 18642.0, 15078.0, 12191.0, 9856.0, 7972.9, 6457.3, 5239.7, 5009.7, 4947.7, 8098.8, 8661.2, 10260.0, 12934.0, 14989.0, 15620.0, 18905.0, 21844.0, 22158.0, 41339.0, 65421.0, 84201.0, 90662.0, 84419.0, 70172.0, 53600.0, 38601.0, 26797.0, 18271.0, 12424.0, 8528.8, 6416.6, 5964.6, 5927.3, 5805.7, 7910.0, 7395.5, 6182.6, 4698.3, 3684.5, 3330.0, 3174.5, 3135.5, 4362.5, 4208.5, 4154.9, 3418.2, 2834.7, 2402.1, 2179.2, 2111.3, 2094.1, 2228.2, 2218.6, 2158.9, 2095.2, 2041.4, 2027.8, 2150.6, 2140.3, 2132.3, 2126.1, 2123.9, 2223.1, 2246.3, 2329.9, 2845.6, 3840.7, 5292.0, 7062.2, 8923.8, 10631.0, 11986.0, 12879.0, 13283.0, 13242.0, 12836.0, 12436.0, 12382.0, 12327.0, 12272.0, 12215.0, 12157.0, 12098.0, 12039.0, 11978.0, 11916.0, 11854.0, 11791.0, 11727.0, 11662.0, 11596.0, 11585.0, 12342.0, 12309.0, 12246.0, 12183.0, 12119.0, 12054.0, 11989.0, 11924.0, 11858.0, 11792.0, 11725.0, 11663.0, 11658.0, 12090.0, 12039.0, 11974.0, 11909.0, 11843.0, 11777.0, 11711.0, 11645.0, 11579.0, 11513.0, 11446.0, 11379.0, 11312.0, 11245.0, 11177.0, 11110.0, 11042.0, 10974.0, 10906.0, 10838.0, 10770.0, 10701.0, 10633.0, 10564.0, 10496.0, 10427.0, 10358.0, 10288.0, 10219.0, 10150.0, 10081.0, 10012.0, 9942.3, 9873.1, 9803.9, 9734.7, 9665.6, 9596.6, 9527.7, 9458.9, 9390.2, 9321.6, 9253.1, 9184.8, 9116.7, 9048.7, 8980.9, 8913.4, 8845.8, 8778.5, 8711.4, 8644.5, 8577.8, 8511.4, 8445.3, 8379.4, 8313.8, 8248.5, 8183.4, 8118.7, 8054.3, 8007.8, 7990.1, 8467.4, 8426.4, 8360.8, 8295.6, 8230.7, 8166.2, 8102.0, 8038.2, 7974.8, 7911.7, 7849.0, 7786.7, 7724.7, 7663.2, 7602.0, 7541.3, 7480.9, 7420.8, 7361.1, 7301.8, 7242.9, 7184.5, 7126.3, 7068.6, 7011.3, 6954.3, 6897.8, 6841.7, 6785.9, 6730.6, 6675.6, 6621.1, 6567.0, 6513.3, 6459.0, 6404.1, 6349.5, 6295.3, 6248.6, 6241.4, 6296.2, 6272.3, 6219.2, 6166.6, 6114.4, 6062.6, 6011.3, 5959.6, 5907.9, 5856.7, 5805.9, 5755.6, 5705.6, 5656.2, 5607.1, 5558.2, 5496.1, 5434.8, 5374.3, 5314.4, 5255.3, 5196.9, 5139.2, 5082.2, 5025.9, 4970.2, 4915.3, 4861.0, 4807.3, 4754.4, 4702.0, 4650.3, 4599.2, 4548.7, 4534.1, 4604.4, 4593.6, 4543.8, 4494.7, 4446.2, 4398.2, 4350.8, 4304.0, 4257.7, 4212.0, 4166.8, 4122.2, 4078.1, 4034.5, 3991.5, 3948.9, 3906.9, 3865.4, 3824.3, 3783.8, 3743.7, 3704.1, 3665.0, 3626.3, 3588.1, 3550.4, 3513.0, 3476.2, 3439.7, 3403.7, 3368.1, 3332.9, 3298.2, 3263.8, 3229.8, 3196.0, 3162.5, 3129.4, 3096.6, 3064.3, 3032.3, 3000.6, 2969.4, 2938.5, 2908.0, 2877.8, 2847.9, 2818.4, 2789.3, 2760.5, 2732.0, 2703.8, 2676.0, 2648.5, 2621.2, 2594.2, 2567.5, 2541.2, 2515.1, 2489.3, 2463.9, 2438.7, 2413.8, 2389.2, 2364.8, 2340.7, 2317.0, 2293.4, 2270.2, 2247.2, 2224.4, 2201.9, 2179.7, 2157.7, 2135.8, 2114.1, 2092.7, 2071.6, 2050.7, 2030.0, 2009.5, 1989.3, 1969.3, 1949.5, 1929.9, 1910.6, 1891.4, 1872.5, 1853.8, 1835.3, 1817.0, 1798.9, 1780.9, 1763.2, 1745.7, 1728.4, 1711.3, 1694.3, 1677.6, 1661.0, 1644.6, 1628.4, 1612.3, 1596.5, 1580.8, 1565.2, 1549.9, 1534.7, 1519.7, 1504.8, 1490.1, 1475.6, 1461.2, 1447.0, 1432.9, 1419.0, 1405.2, 1391.6, 1378.1, 1364.8, 1351.6, 1338.5, 1325.6, 1312.9, 1300.2, 1287.7, 1275.4, 1263.1, 1251.0, 1239.1, 1227.2, 1215.5, 1203.9, 1192.4, 1181.1, 1169.9, 1158.2, 1146.5, 1134.9, 1123.4, 1112.0, 1100.7, 1089.6, 1078.6, 1067.7, 1056.8, 1046.2, 1035.1, 1024.2, 1013.4, 1002.7, 992.11, 981.66, 971.33, 961.11, 951.01, 941.03, 931.15, 921.39, 911.74, 902.19, 892.76, 883.42, 874.2, 865.07, 856.05, 847.13, 838.31, 829.59, 820.96, 812.44, 804.0, 795.66, 787.41, 779.26, 771.19, 763.22, 755.33, 747.53, 739.82, 732.19, 724.65, 717.19, 709.81, 702.52, 695.3, 688.17, 681.11, 674.13, 667.23, 660.4, 653.65, 646.97, 640.36, 633.82, 628.2, 1743.8, 1716.6, 1690.0, 1663.7, 1637.9, 1612.6, 1587.6, 1563.1, 1538.0, 1522.5, 2198.2, 2189.3, 2156.5, 2125.5, 2095.1, 2065.2, 2036.0, 2007.3, 1979.2, 1951.6, 1924.6, 1898.1, 1872.0, 1846.5, 1821.4, 1796.8, 1772.6, 1748.8, 1725.4, 1702.5, 1679.9, 1657.6, 1635.8, 1614.3, 1593.1, 1572.3, 1551.8, 1531.6, 1511.7, 1492.1, 1472.8, 1453.8, 1435.0, 1433.5, 1681.9, 1672.0, 1649.9, 1628.0, 1606.5, 1585.2, 1564.3, 1543.6, 1523.3, 1503.2, 1483.4, 1464.7, 1446.2, 1427.8, 1409.4, 1391.3, 1373.4, 1355.8, 1338.5, 1321.4, 1304.6, 1288.0, 1271.6, 1255.3, 1239.3, 1223.5, 1207.9, 1192.5, 1177.3, 1162.4, 1147.6, 1133.1, 1118.7, 1112.1, 1184.2, 1183.9, 1168.7, 1153.5, 1138.4, 1123.6, 1108.9, 1094.5, 1080.3, 1066.2, 1052.4, 1039.0, 1026.0, 1013.2, 1000.5, 988.09, 983.98, 1018.9, 1017.1, 1006.5, 994.4, 982.43, 970.63, 958.97, 947.25, 935.69, 924.28, 913.03, 901.92, 890.94, 880.1, 869.4, 858.83, 848.41, 838.12, 827.97, 817.95, 808.05, 798.29, 788.65, 779.14, 769.74, 760.47, 751.32, 742.28, 733.36, 724.55, 715.85, 707.27, 698.79, 690.42, 682.15, 673.98, 665.92, 657.96, 650.1, 642.25, 634.44, 626.74, 619.12, 611.6, 604.18, 596.84, 589.6, 582.44, 575.37, 568.39, 561.5, 554.69, 547.95, 541.17, 534.46, 527.83, 521.29, 514.83, 508.45, 502.16, 495.94, 489.78, 483.64, 477.59, 471.61, 465.72, 459.89, 454.15, 448.47, 442.88, 437.35, 431.89, 426.51, 421.2, 415.95, 410.77, 405.66, 400.62, 395.64, 390.73, 385.88, 381.09, 376.37, 371.71, 367.11, 362.57, 358.09, 353.67, 349.3, 344.99, 340.74, 336.55, 332.41, 328.29, 324.2, 320.18, 316.2, 312.28, 308.4, 304.58, 300.81, 297.09, 293.42, 289.79, 286.21, 282.68, 279.2, 275.76, 272.37, 269.02, 265.71, 262.45, 259.23, 256.05, 252.92, 249.82, 246.77, 243.75, 240.78, 237.84, 234.94, 232.08, 229.26, 226.48, 223.73, 221.01, 218.31, 215.64, 213.01, 210.42, 207.86, 180.67, 151.14, 126.6, 106.13, 89.098, 74.904, 62.865, 53.584, 52.817, 51.508, 50.973, 127.53, 122.65, 112.64, 93.939, 86.191, 82.682, 81.779, 113.61, 110.61, 109.14, 104.82, 103.7, 118.13, 113.79, 107.71, 90.775, 76.447, 64.39, 54.221, 45.678, 38.26, 31.974, 26.702, 22.323, 18.631, 15.517, 12.933, 10.788, 9.005, 7.5226, 6.289, 5.2614, 4.4049, 3.6839, 3.0823, 2.5802, 2.1491, 1.7791, 1.4738, 1.3435, 1.2874, 1.2729, 5.8258, 5.6994, 5.6102, 4.8053, 4.047, 3.4038, 2.8618, 2.4058, 2.0191, 1.6945, 1.4221, 1.1936, 1.0019, 0.84108, 0.70627, 0.59314, 0.49819, 0.41849, 0.35157, 0.29539, 0.24821, 0.20858, 0.1753, 0.14734, 0.12385, 0.0], - 'energies (keV)': [0.007161505, 0.007268393, 0.01036116, 0.01051975, 0.01056204, 0.01062547, 0.01069, 0.01078406, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02150088, 0.02182997, 0.02191773, 0.02204937, 0.02227063, 0.02237846, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04403377, 0.04421079, 0.04447632, 0.04514014, 0.04639671, 0.04771733, 0.04844769, 0.04864246, 0.04893461, 0.04959809, 0.04966497, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1164403, 0.1180797, 0.1182225, 0.1186978, 0.1194107, 0.1211929, 0.1262272, 0.1349368, 0.1442475, 0.1487358, 0.1510123, 0.1516194, 0.1525301, 0.1542005, 0.1548066, 0.1648404, 0.1762144, 0.1883732, 0.1968143, 0.1998268, 0.2006301, 0.2013709, 0.2018351, 0.2048475, 0.2083358, 0.2115247, 0.212375, 0.2136505, 0.2152655, 0.2168393, 0.2185326, 0.2194112, 0.2207289, 0.2240234, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53662464, 0.53737536, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56620227, 0.56639779, 0.56699778, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76674817, 0.76780567, 0.76925185, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92741143, 0.92802924, 0.93058861, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0956507, 1.0983493, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8915873, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.0205816, 3.0224186, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5337899, 3.5422103, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1532189, 4.1643407, 4.1647809, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4729462, 4.4878381, 4.4910535, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.32701, 14.40688, 14.5463, 14.60478, 14.6925, 14.91179, 15.40095, 16.46362, 16.99036, 17.25041, 17.31976, 17.42378, 17.59961, 17.68802, 17.95875, 18.03095, 18.13924, 18.40998, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 96.43592, 97.91198, 98.3056, 98.89602, 99.75239, 100.3721, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Rh': {'mass_absorption_coefficient (cm2/g)': [183820.0, 182600.0, 180820.0, 178480.0, 175550.0, 172040.0, 167970.0, 163370.0, 158280.0, 152760.0, 146870.0, 140670.0, 134240.0, 127650.0, 120970.0, 114270.0, 107620.0, 101060.0, 94666.0, 88465.0, 82495.0, 76786.0, 71357.0, 70436.0, 68835.0, 68416.0, 114820.0, 109170.0, 103840.0, 84209.0, 70124.0, 59751.0, 51656.0, 45072.0, 39564.0, 34870.0, 34660.0, 33693.0, 33443.0, 33941.0, 33058.0, 31715.0, 28242.0, 25205.0, 22532.0, 20173.0, 18085.0, 16234.0, 14589.0, 13126.0, 11821.0, 10656.0, 9612.8, 8677.9, 7837.6, 7079.5, 6394.4, 5774.2, 5212.0, 4701.8, 4238.5, 4230.9, 4131.7, 4105.8, 7982.5, 8053.2, 8109.8, 10891.0, 11327.0, 11657.0, 14325.0, 17121.0, 19132.0, 20099.0, 20039.0, 19151.0, 18880.0, 18566.0, 18479.0, 20753.0, 20382.0, 20155.0, 20057.0, 19749.0, 19640.0, 20464.0, 20039.0, 19143.0, 17126.0, 15682.0, 15236.0, 15139.0, 15120.0, 15606.0, 15171.0, 13888.0, 12138.0, 10570.0, 9183.4, 7961.7, 6884.3, 5947.1, 5099.7, 4365.9, 3733.7, 3166.1, 2683.5, 2278.2, 1937.1, 1649.9, 1407.6, 1202.9, 1025.3, 870.81, 740.49, 630.32, 536.61, 457.36, 443.04, 427.3, 423.2, 1466.2, 1415.2, 1394.2, 1370.7, 1344.6, 1331.8, 1809.8, 1744.7, 1602.4, 1572.4, 1511.1, 1495.2, 1694.5, 1632.0, 1553.1, 1308.5, 1101.0, 925.65, 777.95, 653.82, 549.76, 462.46, 388.9, 325.69, 272.57, 227.95, 190.81, 159.88, 133.76, 111.01, 92.148, 76.564, 63.677, 52.979, 44.041, 36.537, 30.246, 25.013, 20.705, 17.154, 14.225, 11.807, 10.082, 9.8071, 9.666, 9.5591, 63.274, 60.708, 54.861, 46.131, 38.694, 32.375, 26.979, 22.478, 18.731, 15.61, 13.01, 10.845, 9.011, 7.4593, 6.1754, 5.1129, 4.2335, 3.5054, 2.9024, 2.4022, 1.9853, 1.636, 1.3482, 1.1111, 0.91456, 0.75268, 0.61949, 0.50989, 0.41971, 0.34549, 0.28441, 0.23414, 0.19276, 0.15871, 0.13067, 0.10759, 0.088596, 0.072955, 0.060078, 0.049476, 0.040746, 0.033557, 0.027638, 0.022764, 0.01875, 0.015444, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.046942, 0.0476605, 0.0478521, 0.0481395, 0.048858, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.07938, 0.080595, 0.080919, 0.081405, 0.08262, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.30086, 0.305466, 0.306693, 0.308535, 0.3101415, 0.3113883, 0.3132585, 0.317934, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.486276, 0.493719, 0.4957038, 0.498681, 0.506124, 0.51058, 0.5124891, 0.518395, 0.520479, 0.523605, 0.53142, 0.5478508, 0.5856525, 0.614558, 0.6239645, 0.6260625, 0.6264729, 0.6302355, 0.639642, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 2.943724, 2.988781, 3.000796, 3.018819, 3.063876, 3.083178, 3.10515, 3.13037, 3.142954, 3.161831, 3.209022, 3.319406, 3.343662, 3.394841, 3.408488, 3.42896, 3.480138, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.7555, 22.98338, 23.1038, 23.19668, 23.336, 23.6843, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Be': {'mass_absorption_coefficient (cm2/g)': [53699.0, 55921.0, 92803.0, 100900.0, 106530.0, 109620.0, 110340.0, 108950.0, 105810.0, 101320.0, 95856.0, 89757.0, 83316.0, 76767.0, 70294.0, 64030.0, 58066.0, 52462.0, 47249.0, 42439.0, 38024.0, 33983.0, 30305.0, 26970.0, 23959.0, 21247.0, 18811.0, 16628.0, 14676.0, 12933.0, 11381.0, 9999.1, 8772.3, 7684.7, 6722.0, 5871.5, 5121.4, 4605.1, 4461.0, 4425.0, 170490.0, 165250.0, 151250.0, 131350.0, 113670.0, 98012.0, 84219.0, 72122.0, 61572.0, 52422.0, 44519.0, 37718.0, 31887.0, 26902.0, 22654.0, 19042.0, 15980.0, 13389.0, 11202.0, 9358.5, 7808.2, 6506.5, 5415.2, 4500.8, 3735.9, 3097.1, 2564.7, 2121.6, 1753.5, 1448.1, 1194.9, 985.37, 812.1, 668.96, 550.81, 450.31, 368.1, 300.9, 245.96, 201.06, 164.35, 134.35, 109.82, 89.769, 73.38, 59.983, 48.71, 39.548, 32.11, 26.07, 21.167, 17.124, 13.806, 11.132, 8.9752, 7.2353, 5.8325, 4.7018, 3.7902, 3.0554, 2.463, 1.9855, 1.6006, 1.2903, 1.0401, 0.83845, 0.6759, 0.54417, 0.43616, 0.34958, 0.28019, 0.22457, 0.18, 0.14427, 0.11563, 0.092679, 0.074282, 0.059538, 0.04772, 0.038248, 0.030656, 0.024571, 0.019694, 0.015785, 0.012629, 0.010099, 0.0080753, 0.0064573, 0.0051635, 0.0041289, 0.0033017, 0.0026401, 0.0021112, 0.0016887, 0.0013522, 0.0010827, 0.00086693, 0.00069417, 0.00055584, 0.00044507, 0.00035638, 0.00028536, 0.00022849, 0.00018296, 0.0001465, 0.00011731, 9.3931e-05, 7.5214e-05, 6.0226e-05, 4.8225e-05, 3.8615e-05, 3.092e-05, 2.4759e-05, 1.9825e-05, 1.5875e-05, 1.2712e-05, 1.0179e-05, 8.1504e-06, 6.5264e-06, 5.2259e-06, 4.1846e-06, 3.3508e-06, 2.6832e-06, 2.1485e-06, 1.7204e-06, 1.3776e-06, 0.0], - 'energies (keV)': [0.0084621, 0.0085884, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.10878, 0.1104581, 0.110889, 0.111555, 0.11322, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ba': {'mass_absorption_coefficient (cm2/g)': [183230.0, 174420.0, 173200.0, 138730.0, 129950.0, 123370.0, 121670.0, 201640.0, 191750.0, 187110.0, 148210.0, 116210.0, 90144.0, 69458.0, 53349.0, 40970.0, 31539.0, 24390.0, 18982.0, 14889.0, 11783.0, 9416.2, 9146.9, 8707.0, 8595.1, 8572.5, 8192.7, 7764.7, 6418.3, 5392.1, 4602.9, 3990.4, 3509.0, 3125.1, 2814.5, 2557.9, 2341.9, 2155.8, 1992.4, 1901.4, 1868.9, 1860.5, 199610.0, 190310.0, 153370.0, 143110.0, 132750.0, 240840.0, 182760.0, 116660.0, 35704.0, 14504.0, 7826.1, 5216.3, 4024.6, 3427.4, 3123.7, 2988.4, 2962.9, 2968.5, 2970.7, 4439.0, 4446.5, 4466.6, 4468.7, 4482.8, 4487.6, 4986.3, 5005.0, 5047.8, 5170.1, 5311.5, 5454.5, 5470.6, 5501.0, 5508.8, 6116.2, 6139.4, 6166.8, 6240.4, 6263.9, 6226.6, 6124.3, 5957.2, 5724.7, 5434.8, 5102.8, 4744.2, 4373.2, 4001.6, 3639.1, 3292.4, 2966.2, 2663.2, 2384.1, 2382.7, 2322.5, 2305.6, 10082.0, 9846.7, 9749.4, 14829.0, 14652.0, 14109.0, 13866.0, 11703.0, 9942.0, 8486.5, 7679.4, 7403.4, 7332.2, 8401.6, 8099.5, 7557.3, 7292.2, 7224.4, 7613.0, 7596.6, 7335.8, 6530.3, 6017.4, 5825.4, 5776.0, 5974.5, 5926.5, 5792.7, 5117.3, 4393.3, 3762.6, 3219.5, 2747.6, 2344.9, 2002.8, 1709.2, 1457.4, 1243.5, 1061.8, 907.57, 770.18, 645.56, 541.05, 454.08, 381.79, 321.61, 271.27, 227.96, 206.61, 198.53, 196.45, 615.13, 606.95, 586.83, 534.85, 511.99, 506.18, 693.98, 690.66, 664.27, 621.32, 595.35, 588.82, 670.59, 661.33, 645.67, 559.24, 473.75, 399.81, 336.51, 283.03, 237.74, 198.55, 165.97, 138.44, 115.28, 95.763, 79.553, 66.143, 55.031, 45.823, 38.19, 31.858, 26.599, 22.229, 18.582, 15.501, 12.917, 10.647, 8.7545, 7.2043, 5.9336, 4.8911, 4.881, 4.6715, 4.6177, 27.944, 26.889, 25.133, 21.084, 17.67, 14.801, 12.367, 10.323, 8.6151, 7.1886, 5.9975, 5.0034, 4.1703, 3.4635, 2.8667, 2.373, 1.9645, 1.6264, 1.3466, 1.1151, 0.92341, 0.76474, 0.63338, 0.52462, 0.43491, 0.36068, 0.29914, 0.24811, 0.2058, 0.17071, 0.14162, 0.11749, 0.097474, 0.080874, 0.067104, 0.055682, 0.046206, 0.038344, 0.031822, 0.0], - 'energies (keV)': [0.014673, 0.014892, 0.01492335, 0.01595306, 0.016268, 0.016517, 0.0165834, 0.016683, 0.016932, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.038318, 0.0389045, 0.0390609, 0.0392955, 0.039882, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.088102, 0.0894505, 0.0898101, 0.09041995, 0.09065, 0.091698, 0.0920375, 0.0924075, 0.0929625, 0.09435, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1788015, 0.1795203, 0.1805985, 0.183294, 0.187964, 0.1883732, 0.190841, 0.1916082, 0.192759, 0.195636, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.24794, 0.251735, 0.252747, 0.254265, 0.25806, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.765086, 0.7767965, 0.780178, 0.7846035, 0.7921195, 0.7953039, 0.796314, 0.8000805, 0.812022, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.040956, 1.056889, 1.061138, 1.067676, 1.083444, 1.113966, 1.131017, 1.135563, 1.141345, 1.142384, 1.159434, 1.220098, 1.266944, 1.286336, 1.291507, 1.299264, 1.304285, 1.318656, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.14206, 5.220765, 5.241753, 5.273235, 5.295467, 5.35194, 5.511128, 5.595482, 5.617976, 5.651718, 5.660855, 5.736072, 5.869024, 5.958856, 5.982811, 6.018744, 6.051453, 6.108576, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 36.69179, 37.2534, 37.40316, 37.6278, 38.18941, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Bi': {'mass_absorption_coefficient (cm2/g)': [110470.0, 107560.0, 42995.0, 38053.0, 33528.0, 29394.0, 25635.0, 24872.0, 24662.0, 37244.0, 35711.0, 33031.0, 27337.0, 22546.0, 18544.0, 15234.0, 12509.0, 10271.0, 8432.6, 8323.9, 7958.4, 7864.5, 14012.0, 14591.0, 15744.0, 17130.0, 18420.0, 18801.0, 22530.0, 24712.0, 25760.0, 39804.0, 59723.0, 83345.0, 107230.0, 127320.0, 140210.0, 144190.0, 139480.0, 127900.0, 112000.0, 94344.0, 76968.0, 61207.0, 47726.0, 36688.0, 27942.0, 21176.0, 16033.0, 15652.0, 14695.0, 14453.0, 18277.0, 17201.0, 15792.0, 12072.0, 9342.4, 8202.5, 7771.2, 7662.3, 9629.4, 9436.1, 9153.9, 7559.3, 6129.2, 5020.3, 4160.9, 4157.2, 4024.7, 3990.4, 3948.0, 4220.8, 4197.1, 4173.7, 4286.3, 4266.0, 4242.1, 4214.6, 4420.3, 4413.7, 4357.3, 4350.4, 4250.3, 4445.6, 4926.1, 5675.8, 6658.6, 7813.8, 9058.6, 10297.0, 11430.0, 12372.0, 13056.0, 13444.0, 13526.0, 13318.0, 13156.0, 13048.0, 13018.0, 13531.0, 13434.0, 13429.0, 13337.0, 13215.0, 13182.0, 13477.0, 13358.0, 13241.0, 12848.0, 12798.0, 12747.0, 12696.0, 12644.0, 12591.0, 12537.0, 12483.0, 12428.0, 12373.0, 12316.0, 12259.0, 12202.0, 12144.0, 12085.0, 12026.0, 11966.0, 11906.0, 11845.0, 11783.0, 11722.0, 11659.0, 11597.0, 11534.0, 11470.0, 11406.0, 11342.0, 11278.0, 11213.0, 11148.0, 11082.0, 11017.0, 10951.0, 10885.0, 10818.0, 10752.0, 10685.0, 10618.0, 10551.0, 10484.0, 10417.0, 10350.0, 10283.0, 10215.0, 10148.0, 10081.0, 10013.0, 9945.8, 9878.3, 9810.9, 9743.4, 9676.0, 9608.5, 9541.2, 9473.8, 9406.5, 9339.3, 9272.1, 9205.1, 9138.2, 9071.4, 9004.7, 8998.9, 9528.8, 9499.0, 9430.6, 9362.4, 9294.5, 9226.8, 9159.3, 9092.1, 9025.2, 8958.6, 8892.3, 8826.3, 8760.6, 8695.3, 8630.3, 8565.7, 8501.5, 8437.6, 8374.1, 8309.7, 8243.8, 8178.2, 8113.0, 8048.1, 7983.5, 7919.4, 7855.5, 7792.1, 7729.1, 7666.4, 7602.4, 7538.7, 7475.3, 7412.3, 7349.7, 7332.2, 7398.2, 7388.2, 7326.3, 7264.8, 7203.7, 7143.0, 7082.7, 7022.8, 6963.4, 6904.3, 6845.7, 6787.5, 6729.7, 6672.3, 6615.4, 6558.9, 6502.8, 6447.2, 6392.0, 6337.2, 6282.9, 6229.0, 6175.5, 6122.5, 6069.9, 6017.8, 5966.1, 5914.8, 5864.0, 5813.6, 5763.7, 5719.0, 5714.2, 5806.9, 5780.7, 5731.7, 5683.1, 5635.0, 5587.4, 5540.1, 5493.3, 5447.0, 5401.0, 5355.6, 5310.5, 5265.9, 5221.3, 5164.0, 5107.5, 5051.6, 4996.4, 4941.8, 4887.9, 4834.7, 4782.0, 4730.0, 4678.6, 4627.9, 4577.7, 4528.1, 4479.2, 4430.8, 4383.1, 4335.8, 4289.2, 4243.1, 4197.6, 4152.6, 4108.2, 4064.3, 4020.9, 3978.1, 3935.7, 3893.9, 3852.4, 3811.1, 3770.4, 3730.1, 3690.4, 3651.1, 3612.2, 3573.8, 3535.9, 3498.4, 3461.4, 3424.8, 3388.6, 3352.9, 3317.6, 3282.7, 3248.2, 3214.1, 3180.4, 3147.1, 3114.2, 3081.7, 3049.5, 3017.8, 2986.4, 2955.4, 2924.7, 2894.4, 2864.4, 2834.8, 2805.6, 2776.6, 2748.1, 2719.8, 2691.8, 2664.1, 2636.6, 2609.4, 2582.6, 2556.1, 2529.9, 2503.9, 2478.3, 2453.0, 2427.9, 2403.2, 2378.7, 2354.5, 2330.6, 2306.9, 2283.6, 2260.5, 2237.6, 2215.0, 2192.7, 2170.6, 2148.8, 2127.2, 2105.8, 2084.7, 2063.9, 2043.2, 2022.8, 2002.7, 1982.7, 1963.0, 1943.4, 1924.0, 1904.9, 1886.0, 1867.3, 1848.8, 1830.5, 1812.4, 1794.5, 1776.8, 1759.3, 1742.0, 1724.9, 1708.0, 1691.3, 1674.7, 1658.3, 1642.2, 1626.1, 1610.2, 1593.8, 1577.2, 1560.8, 1544.6, 1528.6, 1512.7, 1497.1, 1481.6, 1466.3, 1451.2, 1436.1, 1420.6, 1405.3, 1390.1, 1375.2, 1360.4, 1345.8, 1331.4, 1317.1, 1303.0, 1289.1, 1275.3, 1261.7, 1248.2, 1234.9, 1221.7, 1208.7, 1195.9, 1183.2, 1170.6, 1158.2, 1145.9, 1133.8, 1121.8, 1109.9, 1098.2, 1086.6, 1075.1, 1063.8, 1052.6, 1041.5, 1030.6, 1019.8, 1009.1, 998.49, 988.01, 977.66, 967.42, 957.3, 947.29, 937.39, 927.61, 917.94, 908.37, 898.91, 889.56, 880.31, 871.17, 862.13, 853.19, 844.35, 835.61, 826.97, 818.42, 809.97, 801.61, 793.35, 785.18, 777.09, 769.1, 761.2, 753.38, 745.66, 738.01, 730.45, 722.98, 716.17, 2035.0, 2034.2, 2009.8, 1984.9, 1960.3, 1936.0, 1912.1, 1888.4, 1865.1, 1842.0, 1838.8, 2672.9, 2647.8, 2614.3, 2581.2, 2548.5, 2516.3, 2484.4, 2452.9, 2421.9, 2391.3, 2361.0, 2331.2, 2301.7, 2272.6, 2243.9, 2215.6, 2187.6, 2160.0, 2132.8, 2105.9, 2079.2, 2052.9, 2026.9, 2000.3, 1973.7, 1947.5, 1921.6, 1896.0, 1870.8, 1845.9, 1821.3, 1797.1, 1773.2, 1749.6, 1737.5, 2031.8, 2030.4, 2002.9, 1975.7, 1948.9, 1922.4, 1896.3, 1870.5, 1845.1, 1820.0, 1795.3, 1771.6, 1748.4, 1725.5, 1702.9, 1680.7, 1658.8, 1637.3, 1616.1, 1595.2, 1574.6, 1554.2, 1534.0, 1514.2, 1494.6, 1475.3, 1456.4, 1437.7, 1419.2, 1401.1, 1383.2, 1369.0, 1365.5, 1456.6, 1444.8, 1425.9, 1407.2, 1388.7, 1370.6, 1352.7, 1335.0, 1317.6, 1300.5, 1283.7, 1267.6, 1251.8, 1236.2, 1220.8, 1205.7, 1199.0, 1243.5, 1238.7, 1228.5, 1213.8, 1199.2, 1184.9, 1170.8, 1156.8, 1142.9, 1129.1, 1115.4, 1102.0, 1088.6, 1075.5, 1062.6, 1049.8, 1037.2, 1024.7, 1012.4, 1000.3, 988.33, 976.51, 964.85, 953.33, 941.96, 930.74, 919.66, 908.72, 897.91, 887.24, 876.71, 866.3, 856.03, 845.88, 835.86, 825.96, 816.18, 806.52, 796.93, 787.34, 777.87, 768.52, 759.27, 750.14, 741.12, 732.2, 723.4, 714.7, 706.1, 697.61, 689.23, 680.94, 672.74, 664.41, 656.17, 648.02, 639.98, 632.04, 624.2, 616.47, 608.83, 601.16, 593.58, 586.09, 578.7, 571.4, 564.2, 557.1, 550.09, 543.17, 536.34, 529.6, 522.95, 516.39, 509.91, 503.52, 497.21, 490.98, 484.84, 478.77, 472.79, 466.89, 461.06, 455.31, 449.64, 444.04, 438.51, 433.06, 427.68, 422.37, 417.13, 411.96, 406.86, 401.83, 396.84, 391.87, 386.96, 382.12, 377.34, 372.63, 367.97, 363.38, 358.85, 354.38, 349.97, 345.62, 341.32, 337.09, 332.9, 328.78, 324.7, 320.68, 316.72, 312.8, 308.94, 305.13, 301.37, 297.66, 294.0, 290.38, 286.81, 283.29, 279.82, 276.39, 273.01, 269.65, 266.32, 263.04, 259.81, 256.61, 253.46, 250.35, 247.28, 244.25, 241.25, 238.3, 235.39, 232.51, 229.67, 226.87, 224.1, 221.36, 218.65, 215.98, 213.34, 210.74, 208.17, 205.62, 203.11, 200.62, 198.17, 195.74, 193.35, 190.99, 165.91, 138.79, 116.3, 97.614, 81.833, 68.593, 61.391, 58.995, 58.379, 148.98, 148.72, 142.89, 124.25, 103.51, 99.364, 98.278, 136.14, 130.85, 130.09, 124.88, 123.54, 140.95, 140.82, 135.63, 118.95, 100.1, 84.236, 70.854, 59.586, 50.137, 42.212, 35.283, 29.436, 24.568, 20.453, 17.025, 14.182, 11.823, 9.8638, 8.2354, 6.8811, 5.7538, 4.8133, 4.0207, 3.3618, 2.8085, 2.3398, 1.9453, 1.6086, 1.5363, 1.4714, 1.4548, 6.8261, 6.573, 6.3986, 5.3905, 4.5349, 3.8102, 3.2001, 2.6861, 2.2512, 1.8866, 1.5811, 1.3251, 1.1106, 0.93081, 0.78, 0.65369, 0.54789, 0.45927, 0.38502, 0.3228, 0.27066, 0.22697, 0.19034, 0.15964, 0.1339, 0.11232, 0.0], - 'energies (keV)': [0.006200758, 0.006293306, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01416222, 0.01421916, 0.01430456, 0.01451806, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.023912, 0.024278, 0.0243756, 0.024522, 0.024888, 0.02545001, 0.02597, 0.0263675, 0.0264735, 0.0266325, 0.02703, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.090944, 0.092336, 0.0927072, 0.093264, 0.094656, 0.09665893, 0.1033284, 0.1104581, 0.114464, 0.116216, 0.1166832, 0.117384, 0.1180797, 0.119136, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.154252, 0.156114, 0.156613, 0.1572426, 0.158187, 0.158662, 0.1591407, 0.1600965, 0.160548, 0.1610905, 0.1617381, 0.162486, 0.1627095, 0.1648404, 0.165138, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4312, 0.4378, 0.43956, 0.4422, 0.4484657, 0.4488, 0.454328, 0.461282, 0.4631364, 0.465918, 0.472872, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.67809211, 0.67970789, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80418063, 0.80641936, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93687713, 0.93733274, 0.93952286, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5788933, 2.5799086, 2.5803069, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6868259, 2.6883739, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1735006, 3.1802992, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6909034, 3.6945434, 3.7016966, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 3.9904219, 4.0014533, 4.007778, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.15023, 13.35151, 13.40518, 13.47697, 13.48569, 13.68697, 14.40688, 15.40095, 15.63254, 15.69539, 15.78966, 16.02532, 16.05975, 16.30556, 16.37111, 16.46362, 16.46944, 16.71525, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 88.71538, 90.07327, 90.43538, 90.97853, 92.33642, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Br': {'mass_absorption_coefficient (cm2/g)': [2138.5, 2162.8, 2258.3, 2326.6, 3809.4, 3837.9, 4286.1, 5546.1, 9006.6, 13592.0, 19048.0, 25012.0, 31029.0, 36629.0, 41409.0, 45083.0, 47502.0, 48649.0, 48610.0, 47544.0, 45645.0, 45324.0, 44778.0, 44629.0, 46760.0, 46235.0, 46160.0, 45592.0, 45443.0, 46188.0, 45630.0, 43969.0, 41123.0, 38101.0, 35007.0, 34005.0, 33302.0, 33117.0, 34255.0, 33567.0, 33331.0, 30295.0, 27361.0, 24559.0, 21925.0, 19479.0, 17233.0, 15188.0, 13342.0, 11686.0, 10208.0, 8896.8, 7737.6, 6717.0, 5821.2, 5037.3, 4352.5, 3755.2, 3236.0, 3030.3, 2996.6, 2963.3, 2930.3, 2897.7, 2865.4, 2833.5, 2802.0, 2770.8, 2739.9, 2709.4, 2679.2, 2649.3, 2619.8, 2590.6, 2561.7, 2533.2, 2504.9, 2477.0, 2449.4, 2422.1, 2395.1, 2365.2, 2335.3, 2305.8, 2276.7, 2248.0, 2219.4, 2191.1, 2163.2, 2135.7, 2108.6, 2081.8, 2055.3, 2029.3, 2003.5, 1978.1, 1953.0, 1928.3, 1903.9, 1879.8, 1856.0, 1832.5, 1809.4, 1786.5, 1764.0, 1741.7, 1719.7, 1698.1, 1676.7, 1655.6, 1634.7, 1614.2, 1593.9, 1573.8, 1554.1, 1534.6, 1515.3, 1496.3, 1477.5, 1459.0, 1440.8, 1422.7, 1404.9, 1387.4, 1370.0, 1352.9, 1336.0, 1319.3, 1302.9, 1286.7, 1270.6, 1254.8, 1239.2, 1223.4, 1207.8, 1192.4, 1177.0, 1161.7, 1146.7, 1131.9, 1117.3, 1102.8, 1088.6, 1074.6, 1060.7, 1047.1, 1033.6, 1020.4, 1007.3, 994.34, 981.6, 969.03, 956.63, 944.4, 932.33, 920.43, 908.69, 897.11, 885.69, 874.42, 863.3, 852.33, 841.51, 830.84, 820.31, 809.92, 799.67, 789.56, 779.98, 3891.6, 3891.4, 3841.3, 3791.7, 3742.7, 3694.4, 3646.7, 3607.3, 5097.4, 5089.6, 5024.0, 4959.4, 4895.5, 4832.5, 4770.3, 4709.0, 4648.4, 4588.6, 4529.6, 4471.3, 4413.8, 4357.0, 4301.0, 4245.7, 4191.2, 4137.3, 4084.1, 4031.6, 3979.8, 3928.7, 3878.2, 3838.8, 4397.6, 4390.4, 4342.8, 4288.7, 4235.3, 4182.5, 4130.5, 4079.0, 4028.3, 3978.1, 3928.7, 3880.0, 3832.4, 3785.4, 3739.0, 3693.3, 3648.1, 3603.6, 3559.6, 3516.2, 3473.4, 3431.1, 3389.4, 3348.2, 3307.6, 3267.5, 3227.7, 3188.1, 3149.0, 3110.5, 3072.4, 3034.8, 2997.5, 2960.6, 2924.1, 2888.1, 2852.6, 2817.4, 2782.8, 2748.5, 2714.7, 2681.3, 2648.3, 2615.7, 2583.6, 2551.8, 2520.4, 2489.4, 2458.8, 2428.6, 2398.7, 2369.2, 2340.0, 2311.3, 2282.7, 2254.4, 2226.4, 2198.7, 2171.4, 2144.4, 2117.7, 2091.4, 2065.4, 2039.7, 2014.3, 1989.3, 1964.5, 1940.1, 1916.0, 1892.1, 1868.6, 1845.3, 1822.3, 1799.6, 1777.2, 1755.1, 1733.3, 1711.7, 1690.4, 1669.3, 1648.5, 1628.0, 1607.7, 1587.7, 1568.0, 1548.5, 1529.2, 1510.1, 1491.4, 1472.8, 1454.5, 1436.4, 1418.5, 1400.9, 1383.4, 1366.2, 1349.3, 1332.5, 1315.9, 1299.6, 1283.4, 1267.5, 1251.8, 1236.2, 1220.9, 1205.8, 1190.4, 1175.0, 1159.8, 1144.8, 1130.0, 1115.3, 1100.9, 1086.3, 1071.8, 1057.6, 1043.6, 1029.7, 1016.0, 1002.4, 988.9, 975.61, 962.51, 949.59, 936.85, 924.28, 911.89, 899.67, 887.62, 875.74, 864.02, 852.47, 841.02, 829.73, 818.59, 807.61, 796.79, 786.11, 775.58, 765.2, 754.93, 744.63, 734.46, 724.44, 714.56, 704.82, 695.22, 685.74, 676.4, 667.19, 658.11, 649.16, 640.33, 631.62, 623.04, 614.57, 606.23, 597.99, 589.88, 581.87, 573.98, 566.2, 558.52, 550.96, 543.49, 536.14, 528.88, 521.72, 514.67, 507.71, 500.85, 494.08, 487.41, 480.82, 474.34, 467.94, 461.62, 455.4, 449.26, 443.21, 437.24, 431.36, 425.55, 419.83, 414.18, 408.61, 403.12, 397.71, 392.36, 387.1, 381.9, 376.78, 371.73, 366.75, 361.83, 356.99, 352.21, 347.49, 342.84, 338.26, 333.74, 329.27, 324.86, 320.51, 316.22, 311.98, 307.81, 303.7, 299.64, 295.63, 291.68, 287.79, 283.95, 280.16, 276.42, 272.74, 269.1, 265.52, 261.98, 258.49, 255.05, 251.66, 248.31, 245.01, 241.76, 238.55, 235.38, 232.25, 229.17, 226.13, 223.13, 220.18, 217.26, 214.38, 211.54, 208.74, 205.98, 203.26, 200.57, 197.92, 195.31, 192.73, 190.18, 187.67, 185.2, 182.76, 180.35, 177.97, 175.63, 173.31, 171.03, 168.78, 166.56, 164.38, 162.22, 160.08, 157.98, 155.91, 153.86, 151.85, 149.86, 147.89, 145.95, 144.04, 126.32, 105.34, 87.827, 73.078, 60.665, 49.821, 40.956, 33.7, 27.757, 22.884, 20.027, 19.172, 18.952, 148.58, 146.45, 139.98, 121.84, 101.91, 85.828, 72.416, 60.601, 50.63, 42.244, 35.223, 29.365, 24.489, 20.294, 16.765, 13.851, 11.444, 9.4547, 7.8092, 6.4479, 5.3244, 4.3969, 3.6312, 2.999, 2.4766, 2.0331, 1.669, 1.3702, 1.125, 0.92273, 0.7551, 0.61795, 0.50573, 0.41391, 0.33877, 0.27728, 0.22696, 0.18578, 0.15207, 0.12449, 0.10191, 0.083427, 0.0683, 0.055917, 0.045781, 0.037483, 0.030691, 0.025129, 0.020576, 0.016849, 0.013797, 0.011298, 0.0092522, 0.007577, 0.0062052, 0.0], - 'energies (keV)': [0.06923942, 0.069345, 0.0697495, 0.0700299, 0.07038, 0.0704505, 0.071502, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.17787, 0.1805925, 0.1813185, 0.1824075, 0.18513, 0.185514, 0.1883732, 0.1891107, 0.1902465, 0.193086, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.25137, 0.2552175, 0.2562435, 0.2577825, 0.26163, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9, 0.9045, 0.9090225, 0.91356761, 0.91813545, 0.92272613, 0.92733976, 0.93197646, 0.93663634, 0.94131952, 0.94602612, 0.95075625, 0.95551003, 0.96028758, 0.96508902, 0.96991446, 0.97476404, 0.97963786, 0.98453605, 0.98945873, 0.99440602, 0.99937805, 1.0043749, 1.0093968, 1.0144438, 1.019516, 1.0246136, 1.0297367, 1.0348853, 1.0400598, 1.0452601, 1.0504864, 1.0557388, 1.0610175, 1.0663226, 1.0716542, 1.0770125, 1.0823975, 1.0878095, 1.0932486, 1.0987148, 1.1042084, 1.1097294, 1.1152781, 1.1208545, 1.1264587, 1.132091, 1.1377515, 1.1434402, 1.1491574, 1.1549032, 1.1606777, 1.1664811, 1.1723135, 1.1781751, 1.184066, 1.1899863, 1.1959362, 1.2019159, 1.2079255, 1.2139651, 1.220035, 1.2261351, 1.2322658, 1.2384271, 1.2446193, 1.2508424, 1.2570966, 1.2633821, 1.269699, 1.2760475, 1.2824277, 1.2888399, 1.295284, 1.3017605, 1.3082693, 1.3148106, 1.3213847, 1.3279916, 1.3346316, 1.3413047, 1.3480112, 1.3547513, 1.361525, 1.3683327, 1.3751743, 1.3820502, 1.3889605, 1.3959053, 1.4028848, 1.4098992, 1.4169487, 1.4240335, 1.4311536, 1.4383094, 1.4455009, 1.4527284, 1.4599921, 1.467292, 1.4746285, 1.4820016, 1.4894117, 1.4968587, 1.504343, 1.5118647, 1.519424, 1.5270212, 1.5346563, 1.5423295, 1.5497311, 1.5500412, 1.550069, 1.5577914, 1.5655804, 1.5734083, 1.5812753, 1.5891817, 1.5958196, 1.5961803, 1.5971276, 1.6051132, 1.6131388, 1.6212045, 1.6293105, 1.6374571, 1.6456443, 1.6538726, 1.6621419, 1.6704526, 1.6788049, 1.6871989, 1.6956349, 1.7041131, 1.7126337, 1.7211968, 1.7298028, 1.7384518, 1.7471441, 1.7558798, 1.7646592, 1.7734825, 1.7804852, 1.7823499, 1.7835146, 1.7912617, 1.800218, 1.8092191, 1.8182652, 1.8273565, 1.8364933, 1.8456757, 1.8549041, 1.8641786, 1.8734995, 1.882867, 1.8922814, 1.9017428, 1.9112515, 1.9208077, 1.9304118, 1.9400638, 1.9497642, 1.959513, 1.9693105, 1.9791571, 1.9890529, 1.9989981, 2.0089931, 2.0190381, 2.0291333, 2.039279, 2.0494754, 2.0597227, 2.0700213, 2.0803714, 2.0907733, 2.1012272, 2.1117333, 2.122292, 2.1329034, 2.143568, 2.1542858, 2.1650572, 2.1758825, 2.1867619, 2.1976957, 2.2086842, 2.2197276, 2.2308263, 2.2419804, 2.2531903, 2.2644562, 2.2757785, 2.2871574, 2.2985932, 2.3100862, 2.3216366, 2.3332448, 2.344911, 2.3566356, 2.3684187, 2.3802608, 2.3921621, 2.404123, 2.4161436, 2.4282243, 2.4403654, 2.4525672, 2.4648301, 2.4771542, 2.48954, 2.5019877, 2.5144976, 2.5270701, 2.5397055, 2.552404, 2.565166, 2.5779919, 2.5908818, 2.6038362, 2.6168554, 2.6299397, 2.6430894, 2.6563048, 2.6695863, 2.6829343, 2.6963489, 2.7098307, 2.7233798, 2.7369967, 2.7506817, 2.7644351, 2.7782573, 2.7921486, 2.8061093, 2.8201399, 2.8342406, 2.8484118, 2.8626539, 2.8769671, 2.891352, 2.9058087, 2.9203378, 2.9349394, 2.9496141, 2.9643622, 2.979184, 2.9940799, 3.0090503, 3.0240956, 3.0392161, 3.0544122, 3.0696842, 3.0850326, 3.1004578, 3.1159601, 3.1315399, 3.1471976, 3.1629336, 3.1787482, 3.194642, 3.2106152, 3.2266683, 3.2428016, 3.2590156, 3.2753107, 3.2916873, 3.3081457, 3.3246864, 3.3413099, 3.3580164, 3.3748065, 3.3916805, 3.4086389, 3.4256821, 3.4428105, 3.4600246, 3.4773247, 3.4947113, 3.5121849, 3.5297458, 3.5473945, 3.5651315, 3.5829572, 3.6008719, 3.6188763, 3.6369707, 3.6551555, 3.6734313, 3.6917985, 3.7102575, 3.7288088, 3.7474528, 3.7661901, 3.785021, 3.8039461, 3.8229659, 3.8420807, 3.8612911, 3.8805975, 3.9000005, 3.9195005, 3.939098, 3.9587935, 3.9785875, 3.9984804, 4.0184728, 4.0385652, 4.058758, 4.0790518, 4.0994471, 4.1199443, 4.140544, 4.1612467, 4.182053, 4.2029632, 4.2239781, 4.245098, 4.2663234, 4.2876551, 4.3090933, 4.3306388, 4.352292, 4.3740535, 4.3959237, 4.4179033, 4.4399929, 4.4621928, 4.4845038, 4.5069263, 4.5294609, 4.5521082, 4.5748688, 4.5977431, 4.6207318, 4.6438355, 4.6670547, 4.69039, 4.7138419, 4.7374111, 4.7610982, 4.7849037, 4.8088282, 4.8328723, 4.8570367, 4.8813219, 4.9057285, 4.9302571, 4.9549084, 4.9796829, 5.0045814, 5.0296043, 5.0547523, 5.080026, 5.1054262, 5.1309533, 5.1566081, 5.1823911, 5.2083031, 5.2343446, 5.2605163, 5.2868189, 5.313253, 5.3398192, 5.3665183, 5.3933509, 5.4203177, 5.4474193, 5.4746564, 5.5020297, 5.5295398, 5.5571875, 5.5849734, 5.6128983, 5.6409628, 5.6691676, 5.6975135, 5.726001, 5.754631, 5.7834042, 5.8123212, 5.8413828, 5.8705897, 5.8999427, 5.9294424, 5.9590896, 5.988885, 6.0188295, 6.0489236, 6.0791682, 6.1095641, 6.1401119, 6.1708125, 6.2016665, 6.2326749, 6.2638382, 6.2951574, 6.3266332, 6.3582664, 6.3900577, 6.422008, 6.454118, 6.4863886, 6.5188206, 6.5514147, 6.5841717, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.20423, 13.40633, 13.46023, 13.47697, 13.54107, 13.74317, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'P': {'mass_absorption_coefficient (cm2/g)': [201910.0, 203980.0, 205360.0, 618080.0, 637190.0, 657870.0, 390540.0, 266220.0, 178300.0, 118750.0, 79430.0, 72633.0, 66423.0, 64882.0, 62678.0, 57530.0, 53820.0, 37199.0, 26450.0, 19559.0, 15262.0, 12752.0, 11484.0, 11050.0, 11132.0, 11479.0, 11904.0, 12270.0, 12503.0, 12575.0, 12496.0, 12288.0, 11980.0, 11603.0, 11189.0, 10764.0, 10342.0, 9930.7, 9532.5, 9148.6, 8777.3, 8415.3, 8058.9, 7704.4, 7349.3, 6991.7, 6631.2, 6268.2, 5781.0, 5582.5, 5467.8, 5437.6, 107530.0, 106230.0, 100820.0, 95065.0, 88257.0, 80809.0, 74867.0, 73090.0, 72637.0, 79834.0, 78054.0, 73050.0, 65334.0, 57988.0, 51119.0, 44799.0, 39056.0, 33895.0, 29302.0, 25245.0, 21686.0, 18580.0, 15883.0, 13549.0, 11538.0, 9809.1, 8326.4, 7056.2, 5971.2, 5046.9, 4261.4, 3595.2, 3030.9, 2553.6, 2150.9, 1811.7, 1502.3, 1246.7, 1035.6, 861.06, 716.69, 597.14, 498.03, 415.79, 347.48, 290.68, 243.41, 236.76, 227.42, 225.02, 2569.8, 2490.6, 2406.5, 2048.9, 1722.0, 1443.5, 1213.1, 1017.8, 851.56, 712.57, 595.97, 498.49, 416.34, 344.81, 285.58, 236.54, 195.93, 162.3, 134.45, 111.38, 92.268, 76.44, 63.33, 52.391, 42.962, 35.135, 28.735, 23.502, 19.222, 15.722, 12.86, 10.519, 8.6043, 7.0383, 5.7575, 4.7098, 3.8528, 3.1519, 2.5785, 2.1094, 1.7155, 1.3925, 1.1303, 0.91751, 0.74477, 0.60456, 0.49075, 0.39837, 0.32254, 0.26084, 0.21094, 0.17059, 0.13796, 0.11157, 0.090229, 0.072971, 0.059019, 0.047742, 0.038621, 0.031242, 0.025274, 0.020446, 0.01654, 0.013381, 0.010825, 0.0087573, 0.0070847, 0.0057316, 0.004637, 0.0037514, 0.003035, 0.0024555, 0.0019866, 0.0016073, 0.0013004, 0.0010521, 0.00085122, 0.0006887, 0.00055722, 0.00045084, 0.00036478, 0.00029514, 0.0], - 'energies (keV)': [0.006353005, 0.006368374, 0.006378545, 0.006416855, 0.006463425, 0.006512629, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01417226, 0.01438918, 0.01444703, 0.0145338, 0.01475072, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.129556, 0.131539, 0.1320678, 0.132861, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.185514, 0.1883732, 0.1891107, 0.1902465, 0.193086, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.10259, 2.134772, 2.143354, 2.156227, 2.18841, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Os': {'mass_absorption_coefficient (cm2/g)': [20049.0, 19575.0, 16452.0, 16262.0, 16222.0, 48882.0, 47807.0, 59114.0, 67564.0, 77132.0, 87344.0, 97565.0, 107030.0, 114910.0, 120440.0, 123030.0, 122350.0, 118430.0, 111600.0, 102460.0, 91742.0, 80230.0, 68657.0, 57586.0, 47467.0, 38564.0, 30967.0, 24644.0, 19486.0, 17837.0, 16892.0, 16625.0, 36179.0, 35022.0, 34551.0, 38679.0, 38427.0, 38048.0, 36171.0, 30593.0, 24331.0, 19455.0, 19275.0, 18345.0, 18109.0, 25321.0, 24325.0, 22831.0, 19311.0, 16406.0, 14038.0, 12246.0, 11519.0, 11261.0, 11197.0, 11311.0, 11231.0, 11105.0, 10550.0, 10354.0, 10585.0, 11182.0, 12085.0, 13222.0, 14516.0, 15893.0, 17257.0, 18528.0, 19631.0, 20500.0, 21034.0, 21201.0, 21023.0, 20543.0, 19806.0, 19580.0, 19359.0, 19299.0, 19743.0, 19522.0, 19365.0, 19226.0, 18981.0, 18915.0, 19202.0, 18956.0, 18651.0, 17459.0, 16224.0, 14988.0, 13782.0, 12625.0, 11532.0, 11174.0, 10941.0, 10879.0, 11519.0, 11290.0, 11232.0, 10606.0, 10534.0, 10462.0, 10391.0, 10320.0, 10249.0, 10179.0, 10109.0, 10039.0, 9970.4, 9901.7, 9833.3, 9765.4, 9697.8, 9630.6, 9563.8, 9497.3, 9431.2, 9395.5, 9513.5, 9505.9, 9448.2, 9383.2, 9318.5, 9254.3, 9190.4, 9126.8, 9063.6, 9000.8, 8938.3, 8876.1, 8814.4, 8752.9, 8691.8, 8631.1, 8570.7, 8510.6, 8450.9, 8391.5, 8332.4, 8273.7, 8215.3, 8157.2, 8099.5, 8042.0, 7984.9, 7928.2, 7871.7, 7815.6, 7759.7, 7704.2, 7649.0, 7594.1, 7539.5, 7485.2, 7431.3, 7398.4, 7558.5, 7545.5, 7504.5, 7450.9, 7397.5, 7344.5, 7291.7, 7239.3, 7187.1, 7135.3, 7083.7, 7032.5, 6981.5, 6930.8, 6880.4, 6830.3, 6780.5, 6731.0, 6681.7, 6632.8, 6584.1, 6535.7, 6487.6, 6439.7, 6392.2, 6344.9, 6297.8, 6251.1, 6204.6, 6158.3, 6112.4, 6066.7, 6021.3, 5976.1, 5931.2, 5886.6, 5842.2, 5798.1, 5754.2, 5710.7, 5667.0, 5623.7, 5580.6, 5537.7, 5495.1, 5452.8, 5410.7, 5368.8, 5327.3, 5285.9, 5244.8, 5204.0, 5163.4, 5123.1, 5083.0, 5043.1, 5003.5, 4964.1, 4924.6, 4881.0, 4837.6, 4794.6, 4751.9, 4709.6, 4667.5, 4625.8, 4584.4, 4543.3, 4502.6, 4462.1, 4422.0, 4382.1, 4342.6, 4303.3, 4264.4, 4225.8, 4187.5, 4149.5, 4111.8, 4074.5, 4037.4, 4000.7, 3964.3, 3928.2, 3892.4, 3856.9, 3821.6, 3781.5, 3741.8, 3702.6, 3663.8, 3625.4, 3587.5, 3549.9, 3512.8, 3476.1, 3439.8, 3403.9, 3368.4, 3333.4, 3298.7, 3264.4, 3230.5, 3196.9, 3162.7, 3127.8, 3093.3, 3059.2, 3025.4, 2992.1, 2959.2, 2926.7, 2894.5, 2862.8, 2831.4, 2800.5, 2769.0, 2737.6, 2706.5, 2675.8, 2645.5, 2615.5, 2586.0, 2556.8, 2528.0, 2499.5, 2471.4, 2443.7, 2416.3, 2389.2, 2362.4, 2336.0, 2309.9, 2284.2, 2258.7, 2233.6, 2208.8, 2184.3, 2160.1, 2136.1, 2112.5, 2089.2, 2066.1, 2043.4, 2020.9, 1998.6, 1976.7, 1955.0, 1933.6, 1912.4, 1891.5, 1870.9, 1850.5, 1830.3, 1810.4, 1790.7, 1771.3, 1752.1, 1733.1, 1714.4, 1695.9, 1677.6, 1659.5, 1641.6, 1624.0, 1606.5, 1589.3, 1572.3, 1555.4, 1538.8, 1522.4, 1506.1, 1490.1, 1474.2, 1458.6, 1443.1, 1427.8, 1412.6, 1397.7, 1382.9, 1368.3, 1353.9, 1339.6, 1325.5, 1311.6, 1297.8, 1284.2, 1270.7, 1257.4, 1244.3, 1231.3, 1218.4, 1205.8, 1193.2, 1180.8, 1168.5, 1156.4, 1144.4, 1132.6, 1120.9, 1109.3, 1097.9, 1086.6, 1075.4, 1064.3, 1053.4, 1042.6, 1031.9, 1021.4, 1010.9, 1000.6, 990.39, 980.25, 970.05, 959.96, 949.99, 940.13, 930.38, 920.74, 911.21, 901.79, 893.72, 2879.3, 2877.8, 2843.5, 2809.5, 2776.0, 2742.9, 2710.2, 2677.9, 2648.1, 2646.0, 3909.3, 3866.7, 3820.0, 3773.9, 3728.3, 3683.3, 3638.8, 3594.9, 3551.6, 3508.7, 3466.4, 3424.6, 3383.4, 3342.6, 3302.3, 3262.5, 3223.2, 3184.3, 3145.9, 3107.9, 3070.4, 3033.4, 2996.8, 2960.7, 2925.1, 2889.8, 2855.0, 2820.6, 2786.7, 2753.2, 2720.0, 2687.3, 2655.0, 2623.1, 2591.6, 2560.4, 2529.6, 2499.3, 2469.3, 2468.0, 2879.0, 2858.5, 2823.7, 2789.3, 2755.3, 2721.8, 2688.7, 2656.0, 2623.3, 2590.7, 2558.6, 2528.1, 2498.1, 2468.5, 2439.3, 2410.3, 2381.5, 2353.1, 2325.0, 2297.4, 2270.1, 2243.2, 2216.6, 2190.3, 2164.4, 2138.9, 2124.3, 2258.7, 2255.0, 2231.4, 2204.6, 2178.0, 2151.8, 2125.9, 2100.3, 2075.1, 2050.1, 2025.5, 2001.7, 1978.8, 1956.3, 1934.1, 1912.1, 1889.2, 1866.0, 1843.2, 1842.0, 1904.4, 1901.4, 1878.6, 1856.1, 1833.8, 1811.8, 1790.0, 1768.6, 1747.3, 1726.4, 1705.6, 1684.9, 1664.5, 1644.3, 1624.4, 1604.7, 1585.2, 1566.0, 1547.0, 1528.2, 1509.6, 1491.3, 1473.2, 1455.3, 1437.6, 1420.1, 1402.8, 1385.8, 1368.9, 1352.3, 1335.9, 1319.6, 1303.6, 1287.8, 1272.1, 1256.7, 1241.4, 1226.4, 1211.5, 1196.8, 1182.3, 1168.0, 1153.9, 1139.9, 1126.2, 1112.6, 1099.1, 1085.9, 1072.8, 1059.9, 1047.1, 1034.5, 1021.6, 1008.8, 996.24, 983.81, 971.54, 959.43, 947.48, 935.4, 923.49, 911.74, 900.15, 888.71, 877.44, 866.31, 855.33, 844.51, 833.83, 823.29, 812.9, 802.65, 792.53, 782.56, 772.71, 763.0, 753.42, 743.97, 734.65, 725.45, 716.37, 707.42, 698.58, 689.87, 681.27, 672.78, 664.41, 656.15, 648.0, 639.96, 632.03, 624.2, 616.48, 608.85, 601.33, 593.91, 586.59, 579.21, 571.89, 564.67, 557.54, 550.51, 543.57, 536.73, 529.98, 523.3, 516.68, 510.15, 503.71, 497.36, 491.09, 484.91, 478.81, 472.79, 466.85, 460.99, 455.21, 449.49, 443.83, 438.26, 432.75, 427.33, 421.95, 416.59, 411.29, 406.06, 400.91, 395.82, 390.81, 385.85, 380.97, 376.15, 371.4, 366.7, 362.08, 357.51, 353.0, 348.56, 344.17, 339.84, 335.56, 331.32, 327.14, 323.01, 318.94, 314.93, 310.96, 307.05, 303.19, 299.39, 295.63, 291.92, 288.26, 284.65, 281.09, 277.57, 274.1, 270.68, 267.3, 263.97, 260.67, 257.43, 254.22, 251.06, 247.94, 244.86, 241.82, 238.82, 235.86, 232.94, 230.05, 227.2, 224.4, 221.62, 218.89, 216.19, 213.52, 210.89, 208.29, 205.73, 203.2, 200.7, 198.24, 195.8, 193.4, 191.03, 188.69, 186.38, 184.1, 181.85, 179.63, 177.44, 175.27, 173.14, 171.03, 168.95, 166.89, 164.86, 162.86, 160.88, 158.93, 157.0, 155.1, 153.22, 151.37, 149.54, 147.73, 145.91, 126.56, 105.54, 88.196, 80.999, 77.703, 76.857, 203.6, 198.26, 195.53, 165.18, 152.63, 146.35, 144.73, 199.05, 192.23, 191.17, 188.07, 180.44, 178.48, 203.52, 195.96, 186.82, 157.3, 132.25, 111.15, 93.289, 78.304, 65.755, 55.245, 46.364, 38.927, 32.67, 27.183, 22.575, 18.764, 15.609, 12.995, 10.828, 9.0297, 7.5352, 6.2892, 5.2441, 4.3734, 3.6364, 3.0197, 2.5084, 2.0841, 2.0102, 1.9274, 1.9061, 9.5082, 9.1617, 8.8532, 7.439, 6.235, 5.2351, 4.3871, 3.6684, 3.0679, 2.5661, 2.1466, 1.7959, 1.5022, 1.2533, 1.0458, 0.87269, 0.72832, 0.6079, 0.50743, 0.42361, 0.35366, 0.29529, 0.24657, 0.2059, 0.17271, 0.14544, 0.1225, 0.10318, 0.08691, 0.0], - 'energies (keV)': [0.006058082, 0.006148501, 0.006911593, 0.007017383, 0.007045593, 0.007087909, 0.007193699, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.044492, 0.045173, 0.045374, 0.045627, 0.0460685, 0.0462537, 0.046308, 0.04639671, 0.0465315, 0.047226, 0.04959809, 0.05302035, 0.05667876, 0.05684, 0.05771, 0.057942, 0.05829, 0.05916, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.082026, 0.0832815, 0.0836163, 0.0841185, 0.08458368, 0.085374, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.267344, 0.271436, 0.2725272, 0.274164, 0.278256, 0.2811158, 0.283612, 0.287953, 0.2891106, 0.290847, 0.295188, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.458836, 0.465859, 0.4677318, 0.470541, 0.477564, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54571853, 0.54696447, 0.54728152, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65327272, 0.65454173, 0.65532723, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.959659, 1.9605411, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.0299512, 2.030642, 2.031649, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.454905, 2.4594951, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7883189, 2.7942246, 2.7960812, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0422811, 3.054719, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 10.65348, 10.81655, 10.86003, 10.92525, 11.03212, 11.08832, 11.79334, 12.1373, 12.32308, 12.37262, 12.44693, 12.60708, 12.6327, 12.70864, 12.90316, 12.95503, 13.03284, 13.22736, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 72.39338, 73.50144, 73.79693, 74.24015, 75.34821, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ge': {'mass_absorption_coefficient (cm2/g)': [17290.0, 17459.0, 17594.0, 18885.0, 20524.0, 22602.0, 25110.0, 28023.0, 31304.0, 34899.0, 38731.0, 42704.0, 46702.0, 50596.0, 54248.0, 57524.0, 60297.0, 62462.0, 63938.0, 64674.0, 64653.0, 63890.0, 62430.0, 60330.0, 60236.0, 59660.0, 59502.0, 61149.0, 60595.0, 59922.0, 59635.0, 59297.0, 59128.0, 59686.0, 59065.0, 57580.0, 54388.0, 50940.0, 47345.0, 43699.0, 43641.0, 42813.0, 42594.0, 44041.0, 43234.0, 41845.0, 38305.0, 34893.0, 31642.0, 28577.0, 25714.0, 23060.0, 20616.0, 18380.0, 16345.0, 14500.0, 12835.0, 11338.0, 9996.5, 8797.5, 7728.0, 6755.7, 5842.7, 5044.4, 4349.0, 3745.4, 3222.8, 2771.5, 2382.7, 2229.5, 2204.5, 2179.7, 2155.2, 2131.0, 2107.1, 2083.4, 2060.0, 2036.9, 2014.0, 1991.4, 1969.1, 1947.0, 1925.1, 1903.5, 1882.2, 1861.1, 1840.2, 1819.6, 1799.2, 1779.0, 1759.1, 1736.7, 1714.1, 1691.9, 1669.9, 1648.3, 1627.0, 1605.9, 1585.2, 1564.7, 1544.5, 1524.6, 1504.9, 1485.5, 1466.4, 1447.6, 1429.0, 1410.6, 1392.5, 1374.7, 1357.0, 1339.7, 1322.5, 1305.6, 1289.0, 1272.5, 1256.3, 1240.3, 1224.5, 1208.9, 1193.5, 1178.3, 1163.4, 1148.6, 1134.0, 1119.7, 1105.5, 1091.5, 1077.7, 1064.1, 1058.2, 5672.7, 5633.3, 5560.1, 5487.8, 5416.4, 5346.0, 5311.5, 7529.5, 7483.3, 7386.2, 7290.4, 7195.9, 7102.5, 7010.4, 6919.5, 6829.8, 6741.2, 6653.9, 6567.6, 6482.5, 6398.5, 6315.5, 6233.7, 6152.9, 6073.2, 5994.6, 5916.9, 5840.3, 5764.6, 5690.0, 5616.3, 5543.6, 5471.8, 5440.4, 6177.4, 6162.8, 6087.5, 6013.0, 5939.5, 5867.0, 5795.3, 5724.6, 5654.7, 5585.7, 5517.6, 5450.2, 5383.6, 5317.9, 5253.0, 5188.8, 5125.5, 5063.0, 5001.2, 4940.2, 4880.0, 4820.5, 4761.8, 4703.8, 4646.2, 4589.3, 4533.1, 4477.7, 4422.9, 4368.6, 4315.0, 4262.0, 4209.7, 4158.0, 4107.0, 4056.6, 4006.8, 3957.7, 3909.1, 3861.2, 3813.8, 3767.0, 3720.8, 3675.2, 3630.1, 3585.6, 3541.6, 3498.2, 3455.3, 3413.0, 3371.1, 3329.8, 3289.0, 3248.7, 3208.6, 3169.0, 3129.8, 3091.1, 3052.9, 3015.1, 2977.9, 2941.0, 2904.7, 2868.8, 2833.3, 2798.2, 2763.6, 2729.5, 2695.7, 2662.4, 2629.4, 2596.9, 2564.8, 2533.1, 2501.7, 2470.8, 2440.2, 2410.0, 2380.2, 2350.8, 2321.7, 2293.0, 2264.6, 2236.6, 2209.0, 2181.6, 2154.7, 2128.0, 2101.7, 2075.7, 2050.0, 2024.7, 1999.6, 1974.9, 1950.5, 1926.4, 1902.5, 1878.8, 1855.4, 1832.4, 1809.6, 1787.1, 1764.8, 1742.9, 1721.2, 1699.8, 1678.7, 1657.8, 1637.2, 1616.8, 1596.4, 1576.1, 1556.1, 1536.3, 1516.8, 1497.5, 1478.3, 1459.3, 1440.6, 1422.1, 1403.9, 1385.9, 1368.2, 1350.6, 1333.3, 1316.3, 1299.4, 1282.8, 1266.3, 1250.1, 1234.1, 1218.3, 1202.8, 1187.4, 1172.2, 1157.2, 1142.4, 1127.8, 1113.4, 1099.2, 1085.1, 1071.2, 1057.4, 1043.8, 1030.3, 1017.1, 1004.0, 991.11, 978.37, 965.8, 953.4, 941.16, 928.6, 915.9, 903.37, 891.02, 878.84, 866.83, 854.99, 843.32, 831.81, 820.46, 809.28, 798.24, 787.36, 776.64, 766.06, 755.63, 745.35, 735.21, 725.21, 715.36, 705.64, 696.05, 686.6, 677.28, 668.08, 658.99, 650.04, 641.21, 632.5, 623.91, 615.45, 607.1, 598.86, 590.75, 582.74, 574.85, 567.06, 559.39, 551.82, 544.35, 536.99, 529.73, 522.58, 515.52, 508.56, 501.69, 494.93, 488.25, 481.67, 475.18, 468.78, 462.46, 456.24, 450.1, 444.04, 438.07, 432.18, 426.38, 420.65, 415.0, 409.43, 403.94, 398.52, 393.18, 387.91, 382.71, 377.59, 372.53, 367.55, 362.63, 357.78, 353.0, 348.28, 343.63, 339.05, 334.52, 330.06, 325.66, 321.32, 317.03, 312.81, 308.65, 304.54, 300.49, 296.49, 292.55, 288.66, 284.83, 281.05, 277.32, 273.64, 270.01, 266.43, 262.9, 259.42, 255.98, 252.59, 249.25, 245.95, 242.7, 239.49, 236.33, 233.21, 230.13, 227.1, 224.1, 221.15, 218.23, 215.36, 212.52, 209.72, 206.97, 204.24, 201.52, 198.8, 196.12, 193.48, 190.88, 188.29, 185.73, 183.2, 180.7, 178.24, 175.82, 173.43, 171.07, 168.75, 166.46, 164.2, 161.98, 159.78, 157.62, 155.49, 153.39, 151.31, 149.27, 147.25, 145.27, 143.31, 141.38, 139.47, 137.59, 135.74, 133.9, 132.04, 130.22, 128.42, 126.64, 124.89, 123.16, 121.46, 119.79, 118.13, 116.5, 114.9, 113.31, 111.75, 110.21, 96.18, 79.991, 66.59, 55.488, 46.108, 37.839, 31.083, 26.61, 25.557, 25.452, 25.155, 198.84, 189.9, 168.04, 140.32, 118.17, 99.832, 83.619, 69.892, 58.321, 48.611, 40.497, 33.739, 28.064, 23.305, 19.353, 15.993, 13.195, 10.882, 8.9736, 7.4007, 6.1039, 5.0346, 4.1529, 3.4258, 2.8102, 2.3043, 1.8896, 1.5495, 1.2707, 1.0421, 0.85469, 0.70022, 0.57226, 0.4677, 0.38226, 0.31244, 0.25538, 0.20875, 0.17064, 0.13949, 0.11403, 0.093225, 0.076216, 0.062312, 0.050946, 0.041655, 0.03406, 0.02785, 0.022773, 0.018622, 0.015228, 0.012454, 0.010185, 0.0083292, 0.0068121, 0.0055715, 0.0045569, 0.0], - 'energies (keV)': [0.0288435, 0.02908327, 0.029274, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.118384, 0.120196, 0.1206792, 0.121404, 0.123216, 0.125342, 0.1262272, 0.1272605, 0.1277721, 0.1285395, 0.130458, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1764, 0.1791, 0.17982, 0.1809, 0.1836, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9, 0.9045, 0.9090225, 0.91356761, 0.91813545, 0.92272613, 0.92733976, 0.93197646, 0.93663634, 0.94131952, 0.94602612, 0.95075625, 0.95551003, 0.96028758, 0.96508902, 0.96991446, 0.97476404, 0.97963786, 0.98453605, 0.98945873, 0.99440602, 0.99937805, 1.0043749, 1.0093968, 1.0144438, 1.019516, 1.0246136, 1.0297367, 1.0348853, 1.0400598, 1.0452601, 1.0504864, 1.0557388, 1.0610175, 1.0663226, 1.0716542, 1.0770125, 1.0823975, 1.0878095, 1.0932486, 1.0987148, 1.1042084, 1.1097294, 1.1152781, 1.1208545, 1.1264587, 1.132091, 1.1377515, 1.1434402, 1.1491574, 1.1549032, 1.1606777, 1.1664811, 1.1723135, 1.1781751, 1.184066, 1.1899863, 1.1959362, 1.2019159, 1.2079255, 1.2139651, 1.2165985, 1.2168014, 1.220035, 1.2261351, 1.2322658, 1.2384271, 1.2446193, 1.2476951, 1.2479049, 1.2508424, 1.2570966, 1.2633821, 1.269699, 1.2760475, 1.2824277, 1.2888399, 1.295284, 1.3017605, 1.3082693, 1.3148106, 1.3213847, 1.3279916, 1.3346316, 1.3413047, 1.3480112, 1.3547513, 1.361525, 1.3683327, 1.3751743, 1.3820502, 1.3889605, 1.3959053, 1.4028848, 1.4098992, 1.4130073, 1.4155926, 1.4169487, 1.4240335, 1.4311536, 1.4383094, 1.4455009, 1.4527284, 1.4599921, 1.467292, 1.4746285, 1.4820016, 1.4894117, 1.4968587, 1.504343, 1.5118647, 1.519424, 1.5270212, 1.5346563, 1.5423295, 1.5500412, 1.5577914, 1.5655804, 1.5734083, 1.5812753, 1.5891817, 1.5971276, 1.6051132, 1.6131388, 1.6212045, 1.6293105, 1.6374571, 1.6456443, 1.6538726, 1.6621419, 1.6704526, 1.6788049, 1.6871989, 1.6956349, 1.7041131, 1.7126337, 1.7211968, 1.7298028, 1.7384518, 1.7471441, 1.7558798, 1.7646592, 1.7734825, 1.7823499, 1.7912617, 1.800218, 1.8092191, 1.8182652, 1.8273565, 1.8364933, 1.8456757, 1.8549041, 1.8641786, 1.8734995, 1.882867, 1.8922814, 1.9017428, 1.9112515, 1.9208077, 1.9304118, 1.9400638, 1.9497642, 1.959513, 1.9693105, 1.9791571, 1.9890529, 1.9989981, 2.0089931, 2.0190381, 2.0291333, 2.039279, 2.0494754, 2.0597227, 2.0700213, 2.0803714, 2.0907733, 2.1012272, 2.1117333, 2.122292, 2.1329034, 2.143568, 2.1542858, 2.1650572, 2.1758825, 2.1867619, 2.1976957, 2.2086842, 2.2197276, 2.2308263, 2.2419804, 2.2531903, 2.2644562, 2.2757785, 2.2871574, 2.2985932, 2.3100862, 2.3216366, 2.3332448, 2.344911, 2.3566356, 2.3684187, 2.3802608, 2.3921621, 2.404123, 2.4161436, 2.4282243, 2.4403654, 2.4525672, 2.4648301, 2.4771542, 2.48954, 2.5019877, 2.5144976, 2.5270701, 2.5397055, 2.552404, 2.565166, 2.5779919, 2.5908818, 2.6038362, 2.6168554, 2.6299397, 2.6430894, 2.6563048, 2.6695863, 2.6829343, 2.6963489, 2.7098307, 2.7233798, 2.7369967, 2.7506817, 2.7644351, 2.7782573, 2.7921486, 2.8061093, 2.8201399, 2.8342406, 2.8484118, 2.8626539, 2.8769671, 2.891352, 2.9058087, 2.9203378, 2.9349394, 2.9496141, 2.9643622, 2.979184, 2.9940799, 3.0090503, 3.0240956, 3.0392161, 3.0544122, 3.0696842, 3.0850326, 3.1004578, 3.1159601, 3.1315399, 3.1471976, 3.1629336, 3.1787482, 3.194642, 3.2106152, 3.2266683, 3.2428016, 3.2590156, 3.2753107, 3.2916873, 3.3081457, 3.3246864, 3.3413099, 3.3580164, 3.3748065, 3.3916805, 3.4086389, 3.4256821, 3.4428105, 3.4600246, 3.4773247, 3.4947113, 3.5121849, 3.5297458, 3.5473945, 3.5651315, 3.5829572, 3.6008719, 3.6188763, 3.6369707, 3.6551555, 3.6734313, 3.6917985, 3.7102575, 3.7288088, 3.7474528, 3.7661901, 3.785021, 3.8039461, 3.8229659, 3.8420807, 3.8612911, 3.8805975, 3.9000005, 3.9195005, 3.939098, 3.9587935, 3.9785875, 3.9984804, 4.0184728, 4.0385652, 4.058758, 4.0790518, 4.0994471, 4.1199443, 4.140544, 4.1612467, 4.182053, 4.2029632, 4.2239781, 4.245098, 4.2663234, 4.2876551, 4.3090933, 4.3306388, 4.352292, 4.3740535, 4.3959237, 4.4179033, 4.4399929, 4.4621928, 4.4845038, 4.5069263, 4.5294609, 4.5521082, 4.5748688, 4.5977431, 4.6207318, 4.6438355, 4.6670547, 4.69039, 4.7138419, 4.7374111, 4.7610982, 4.7849037, 4.8088282, 4.8328723, 4.8570367, 4.8813219, 4.9057285, 4.9302571, 4.9549084, 4.9796829, 5.0045814, 5.0296043, 5.0547523, 5.080026, 5.1054262, 5.1309533, 5.1566081, 5.1823911, 5.2083031, 5.2343446, 5.2605163, 5.2868189, 5.313253, 5.3398192, 5.3665183, 5.3933509, 5.4203177, 5.4474193, 5.4746564, 5.5020297, 5.5295398, 5.5571875, 5.5849734, 5.6128983, 5.6409628, 5.6691676, 5.6975135, 5.726001, 5.754631, 5.7834042, 5.8123212, 5.8413828, 5.8705897, 5.8999427, 5.9294424, 5.9590896, 5.988885, 6.0188295, 6.0489236, 6.0791682, 6.1095641, 6.1401119, 6.1708125, 6.2016665, 6.2326749, 6.2638382, 6.2951574, 6.3266332, 6.3582664, 6.3900577, 6.422008, 6.454118, 6.4863886, 6.5188206, 6.5514147, 6.5841717, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 10.88104, 11.03212, 11.04758, 11.092, 11.15862, 11.32516, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Gd': {'mass_absorption_coefficient (cm2/g)': [614.34, 594.76, 539.18, 521.58, 517.02, 3841.9, 3719.3, 2914.2, 2690.6, 2590.0, 2582.7, 2652.5, 2791.7, 2997.2, 3269.4, 3610.7, 4024.7, 4167.8, 4278.1, 4307.9, 143590.0, 138420.0, 136340.0, 115820.0, 97536.0, 80717.0, 66274.0, 54491.0, 45259.0, 38268.0, 33419.0, 33141.0, 32451.0, 32208.0, 32095.0, 31273.0, 29756.0, 27272.0, 25621.0, 24533.0, 23842.0, 23407.0, 23121.0, 22909.0, 22714.0, 22501.0, 22245.0, 21935.0, 21566.0, 21141.0, 20662.0, 20400.0, 20360.0, 20320.0, 20281.0, 20240.0, 20200.0, 20160.0, 20119.0, 20078.0, 20037.0, 19995.0, 19954.0, 19912.0, 19870.0, 19828.0, 19785.0, 19743.0, 19700.0, 19657.0, 19614.0, 19571.0, 19527.0, 19484.0, 19440.0, 19396.0, 19352.0, 19308.0, 19264.0, 19219.0, 19175.0, 19130.0, 19085.0, 19040.0, 18995.0, 18950.0, 18905.0, 18860.0, 18814.0, 18769.0, 18723.0, 18677.0, 18631.0, 18586.0, 18540.0, 18493.0, 18447.0, 18401.0, 18355.0, 18309.0, 18262.0, 18216.0, 18169.0, 18123.0, 18076.0, 18029.0, 17983.0, 17936.0, 17889.0, 17843.0, 17796.0, 17749.0, 17702.0, 17655.0, 17609.0, 17562.0, 17515.0, 17468.0, 17421.0, 17374.0, 17368.0, 35770.0, 35003.0, 34051.0, 33146.0, 32284.0, 31464.0, 30684.0, 29942.0, 29235.0, 28562.0, 27921.0, 27310.0, 26729.0, 26174.0, 25646.0, 25143.0, 24663.0, 24205.0, 23769.0, 23352.0, 22956.0, 22585.0, 22238.0, 21912.0, 21607.0, 21320.0, 21050.0, 20789.0, 20538.0, 20300.0, 20075.0, 19861.0, 19657.0, 19463.0, 19278.0, 19101.0, 18931.0, 18769.0, 18613.0, 18464.0, 18320.0, 18182.0, 18049.0, 17921.0, 17758.0, 17579.0, 17404.0, 17233.0, 17066.0, 16902.0, 16742.0, 16585.0, 16431.0, 16280.0, 16131.0, 15985.0, 15842.0, 15701.0, 15562.0, 15426.0, 15291.0, 15159.0, 15028.0, 14900.0, 14773.0, 14648.0, 14525.0, 14404.0, 14284.0, 14165.0, 14048.0, 13933.0, 13819.0, 13706.0, 13595.0, 13485.0, 13377.0, 13270.0, 13164.0, 13059.0, 12955.0, 12853.0, 12751.0, 12651.0, 12552.0, 12454.0, 12357.0, 12262.0, 12167.0, 12073.0, 11981.0, 11889.0, 11798.0, 11709.0, 11620.0, 11532.0, 11445.0, 11360.0, 11275.0, 11191.0, 11107.0, 11025.0, 10944.0, 10864.0, 10784.0, 10705.0, 10628.0, 10551.0, 10474.0, 10399.0, 10325.0, 10251.0, 10178.0, 10106.0, 10035.0, 9964.6, 9895.0, 9826.1, 9758.1, 9690.8, 9624.2, 9558.4, 9493.4, 9429.1, 9365.5, 9302.7, 9240.5, 9179.1, 9118.4, 9058.4, 8999.1, 8940.5, 8902.5, 10018.0, 10016.0, 9956.8, 9898.4, 9840.6, 9783.5, 9727.0, 9671.1, 9615.9, 9561.3, 9507.4, 9454.0, 9401.3, 9349.1, 9336.1, 9638.1, 9618.3, 9567.2, 9516.7, 9466.7, 9417.3, 9368.4, 9320.1, 9272.3, 9225.0, 9178.2, 9131.9, 9086.1, 9040.7, 8995.8, 8951.4, 8907.4, 8863.8, 8820.7, 8778.0, 8735.8, 8693.9, 8652.4, 8611.3, 8570.6, 8530.3, 8490.3, 8450.7, 8411.5, 8372.5, 8334.0, 8295.7, 8257.7, 8220.1, 8182.7, 8145.7, 8108.9, 8072.4, 8036.2, 8000.2, 7964.5, 7929.1, 7893.8, 7858.8, 7824.0, 7789.5, 7755.1, 7721.0, 7687.0, 7653.2, 7619.6, 7586.2, 7552.9, 7519.8, 7513.3, 7876.5, 7865.6, 7831.7, 7797.9, 7764.2, 7730.6, 7697.2, 7663.9, 7630.7, 7597.6, 7564.6, 7531.8, 7499.0, 7466.3, 7433.6, 7401.1, 7368.6, 7336.2, 7303.9, 7271.6, 7239.4, 7207.2, 7175.1, 7143.0, 7110.9, 7078.9, 7046.8, 7014.8, 6982.8, 6950.9, 6918.9, 6886.9, 6855.0, 6823.0, 6791.1, 6759.1, 6727.2, 6695.2, 6663.2, 6631.3, 6599.3, 6567.3, 6535.3, 6503.3, 6471.3, 6439.3, 6407.3, 6375.2, 6343.2, 6311.1, 6279.0, 6246.9, 6214.8, 6182.7, 6150.5, 6118.4, 6086.2, 6054.0, 6021.9, 5989.7, 5957.5, 5925.2, 5893.0, 5860.8, 5828.6, 5796.3, 5764.1, 5731.8, 5699.6, 5667.3, 5635.1, 5602.9, 5570.6, 5538.4, 5506.2, 5474.0, 5441.8, 5409.6, 5377.5, 5345.3, 5313.2, 5281.1, 5249.0, 5216.9, 5184.8, 5152.6, 5120.5, 5088.4, 5056.2, 5024.1, 4992.0, 4959.9, 4927.8, 4895.7, 4863.7, 4831.7, 4799.8, 4767.8, 4735.9, 4704.1, 4672.3, 4640.6, 4608.9, 4577.3, 4545.7, 4514.2, 4482.8, 4451.4, 4420.2, 4389.0, 4357.9, 4326.8, 4295.9, 4265.1, 4234.3, 4203.6, 4173.1, 4142.6, 4112.3, 4082.0, 4051.9, 4021.8, 3991.9, 3962.1, 3932.4, 3902.8, 3873.4, 3844.1, 3814.9, 3785.8, 3756.9, 3728.1, 3699.4, 3670.9, 3642.5, 3614.3, 3586.2, 3558.2, 3530.4, 3502.7, 3475.2, 3447.8, 3420.6, 3393.5, 3366.5, 3339.8, 3313.1, 3286.7, 3260.3, 3234.2, 3208.2, 3182.3, 3156.6, 3131.1, 3105.7, 3080.5, 3055.4, 3030.5, 3005.8, 2981.2, 2956.8, 2932.5, 2908.4, 2884.5, 2860.7, 2837.1, 2813.6, 2790.3, 2767.2, 2744.2, 2721.4, 2698.8, 2676.3, 2654.0, 2631.8, 2609.8, 2587.9, 2566.3, 2544.7, 2523.4, 2502.1, 2481.1, 2460.2, 2439.5, 2418.9, 2398.5, 2378.2, 2358.1, 2338.1, 2318.3, 2298.6, 2279.1, 2259.8, 2240.6, 2221.6, 2202.7, 2183.9, 2163.9, 2141.2, 2118.7, 2096.5, 2074.5, 2052.7, 2031.2, 2010.0, 1989.0, 1968.2, 1947.6, 1927.3, 1907.1, 1887.3, 1867.6, 1848.1, 1828.8, 1809.3, 1790.0, 1771.0, 1752.1, 1733.5, 1715.1, 1696.9, 1678.9, 1661.1, 1643.5, 1626.1, 1608.9, 1591.7, 1574.7, 1557.9, 1541.2, 1524.8, 1513.2, 5847.2, 5829.7, 5757.7, 5686.6, 5616.4, 5547.1, 5478.6, 5476.1, 8244.3, 8153.4, 8051.9, 7951.6, 7852.6, 7754.9, 7658.4, 7563.1, 7469.0, 7376.0, 7284.3, 7193.7, 7104.2, 7015.9, 6928.7, 6842.6, 6757.5, 6673.6, 6590.7, 6508.8, 6428.0, 6348.1, 6269.3, 6191.5, 6114.6, 6038.8, 5963.8, 5889.8, 5816.8, 5744.6, 5673.4, 5603.0, 5533.6, 5465.0, 5397.3, 5330.4, 5264.4, 5199.1, 5134.8, 5071.2, 5008.4, 4946.4, 4885.2, 4824.7, 4764.9, 4705.9, 4647.7, 4590.1, 4556.3, 5323.8, 5318.7, 5249.4, 5180.9, 5113.4, 5046.7, 4980.8, 4915.5, 4851.1, 4787.5, 4724.8, 4665.0, 4607.4, 4550.7, 4495.0, 4440.1, 4386.0, 4332.8, 4280.4, 4254.7, 4534.3, 4525.9, 4470.0, 4415.0, 4360.8, 4307.3, 4254.7, 4202.8, 4151.7, 4101.3, 4051.6, 4003.7, 3957.0, 3911.1, 3865.8, 3821.2, 3777.4, 3734.1, 3691.5, 3649.5, 3608.1, 3567.3, 3527.1, 3526.8, 3668.7, 3653.8, 3613.3, 3573.3, 3533.8, 3494.9, 3456.4, 3418.4, 3381.0, 3344.0, 3307.4, 3271.5, 3236.0, 3201.0, 3165.7, 3130.5, 3095.7, 3061.3, 3027.3, 2993.7, 2960.5, 2927.7, 2895.3, 2863.2, 2831.5, 2800.2, 2769.2, 2738.5, 2708.2, 2678.3, 2648.7, 2619.4, 2590.1, 2561.0, 2532.2, 2503.8, 2475.6, 2447.8, 2420.3, 2393.0, 2366.1, 2339.5, 2313.2, 2287.1, 2261.4, 2235.9, 2210.7, 2185.7, 2160.5, 2135.6, 2111.0, 2086.7, 2062.7, 2038.6, 2014.7, 1991.0, 1967.7, 1944.6, 1921.8, 1899.3, 1877.0, 1855.0, 1833.2, 1811.7, 1790.5, 1769.5, 1748.8, 1728.3, 1708.1, 1687.9, 1667.9, 1648.1, 1628.5, 1609.2, 1590.2, 1571.4, 1552.8, 1534.4, 1516.3, 1498.4, 1480.7, 1463.2, 1446.0, 1429.0, 1412.1, 1395.5, 1379.1, 1362.9, 1347.0, 1331.2, 1315.6, 1300.2, 1285.0, 1270.0, 1255.1, 1239.2, 1223.4, 1207.9, 1192.6, 1177.5, 1162.6, 1147.6, 1132.8, 1118.3, 1103.9, 1089.7, 1075.7, 1061.9, 1048.3, 1034.9, 1021.6, 1008.6, 995.68, 982.97, 970.42, 958.05, 945.84, 933.8, 921.86, 909.99, 898.28, 886.72, 875.32, 864.07, 852.98, 842.03, 831.24, 820.59, 810.08, 799.71, 789.48, 779.39, 769.44, 759.61, 749.92, 740.36, 730.93, 721.63, 712.45, 703.39, 694.41, 685.53, 676.78, 668.14, 659.62, 651.21, 642.91, 634.72, 626.65, 618.68, 610.81, 603.05, 577.74, 487.44, 411.81, 348.39, 294.41, 247.92, 209.14, 176.75, 149.25, 139.63, 134.32, 132.95, 384.35, 366.06, 315.09, 302.24, 301.0, 298.97, 407.1, 391.17, 376.14, 361.2, 357.4, 407.12, 403.51, 392.49, 341.48, 286.42, 239.92, 200.74, 167.95, 140.64, 117.87, 98.875, 82.687, 69.104, 57.618, 48.045, 40.097, 33.494, 28.004, 23.434, 19.626, 16.322, 13.552, 11.252, 9.332, 7.7302, 6.3835, 5.273, 4.358, 3.6045, 3.3307, 3.1904, 3.1543, 17.679, 17.071, 17.021, 14.364, 12.052, 10.103, 8.4624, 7.0807, 5.9244, 4.9501, 4.1234, 3.4352, 2.8622, 2.3763, 1.9723, 1.6371, 1.3589, 1.1282, 0.93668, 0.77775, 0.64585, 0.53635, 0.44545, 0.36999, 0.30772, 0.2567, 0.21415, 0.17866, 0.14907, 0.12438, 0.10379, 0.086616, 0.072286, 0.060329, 0.050353, 0.0], - 'energies (keV)': [0.008566815, 0.008694678, 0.009093814, 0.009233005, 0.009270123, 0.009325799, 0.00946499, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.019894, 0.0201985, 0.0202797, 0.0204015, 0.020706, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.035378, 0.03552846, 0.0359195, 0.0360639, 0.0362805, 0.036822, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14046403, 0.14053596, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27068519, 0.27111483, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28823661, 0.28876341, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37534905, 0.37625097, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1850578, 1.1853422, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.216959, 1.217441, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.5427108, 1.5452893, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6868076, 1.6897925, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8778472, 1.8837529, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.097944, 7.206586, 7.235557, 7.279014, 7.392525, 7.771694, 7.890649, 7.902609, 7.92237, 7.969952, 8.088906, 8.208088, 8.333722, 8.367224, 8.417478, 8.44789, 8.543112, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 49.23432, 49.98791, 50.18886, 50.4903, 51.18542, 51.24388, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ga': {'mass_absorption_coefficient (cm2/g)': [33658.0, 33313.0, 32698.0, 31256.0, 30645.0, 30944.0, 31981.0, 33647.0, 35869.0, 38585.0, 41738.0, 45264.0, 49089.0, 53124.0, 57267.0, 61401.0, 65403.0, 69146.0, 72505.0, 75364.0, 77623.0, 79203.0, 80050.0, 80061.0, 79152.0, 77393.0, 74879.0, 71726.0, 69450.0, 68590.0, 68359.0, 70857.0, 70810.0, 70117.0, 69953.0, 69224.0, 68985.0, 69934.0, 69041.0, 68192.0, 63982.0, 59676.0, 55375.0, 51152.0, 47066.0, 46780.0, 45876.0, 45639.0, 47231.0, 46364.0, 45098.0, 41391.0, 37895.0, 34612.0, 31545.0, 28690.0, 26043.0, 23593.0, 21331.0, 19246.0, 17330.0, 15346.0, 13490.0, 11834.0, 10359.0, 9047.7, 7883.9, 6855.3, 5949.7, 5155.2, 4460.7, 3855.1, 3328.5, 2871.7, 2476.4, 2135.0, 2000.2, 1978.1, 1956.3, 1934.7, 1913.4, 1892.3, 1871.4, 1850.8, 1830.4, 1810.2, 1790.2, 1770.5, 1751.0, 1731.7, 1712.6, 1693.7, 1675.1, 1656.6, 1638.4, 1620.3, 1602.5, 1584.9, 1564.9, 1544.9, 1525.2, 1505.7, 1486.5, 1467.5, 1448.8, 1430.4, 1412.1, 1394.2, 1376.5, 1359.0, 1341.7, 1324.7, 1308.0, 1291.4, 1275.1, 1258.9, 1243.0, 1227.3, 1211.9, 1196.6, 1196.5, 6577.7, 6494.4, 6409.1, 6324.9, 6241.8, 6177.4, 8765.7, 8744.3, 8629.6, 8516.5, 8404.8, 8294.6, 8185.9, 8078.6, 7972.7, 7868.2, 7765.1, 7663.3, 7562.9, 7463.8, 7366.0, 7269.6, 7174.3, 7080.4, 6987.7, 6896.2, 6805.9, 6716.8, 6628.8, 6542.1, 6456.4, 6372.0, 6288.6, 6273.2, 7087.7, 7049.8, 6964.0, 6879.4, 6795.8, 6713.3, 6631.8, 6551.4, 6471.9, 6393.6, 6316.2, 6238.9, 6162.5, 6087.0, 6012.4, 5938.8, 5866.0, 5794.1, 5723.0, 5652.9, 5583.5, 5515.0, 5447.4, 5380.6, 5314.7, 5249.6, 5185.2, 5121.7, 5058.9, 4996.9, 4935.7, 4875.2, 4815.4, 4756.4, 4698.1, 4640.5, 4583.6, 4527.5, 4472.0, 4417.2, 4363.0, 4309.6, 4256.7, 4204.6, 4153.1, 4102.2, 4052.0, 4002.3, 3953.3, 3904.9, 3857.1, 3809.9, 3763.3, 3717.3, 3671.5, 3626.2, 3581.5, 3537.4, 3493.8, 3450.8, 3408.3, 3366.3, 3324.9, 3283.9, 3243.5, 3203.5, 3164.1, 3125.2, 3086.7, 3048.7, 3011.2, 2974.1, 2937.5, 2901.3, 2865.6, 2830.4, 2795.6, 2761.2, 2727.2, 2693.7, 2660.5, 2627.6, 2595.1, 2563.0, 2531.3, 2500.0, 2469.0, 2438.5, 2408.3, 2378.4, 2348.9, 2319.8, 2291.0, 2262.6, 2234.5, 2206.8, 2179.4, 2152.4, 2125.7, 2099.3, 2073.3, 2047.6, 2022.2, 1997.2, 1972.4, 1948.0, 1923.8, 1900.0, 1876.5, 1853.2, 1829.7, 1806.6, 1783.7, 1761.1, 1738.7, 1716.4, 1694.4, 1672.7, 1651.3, 1630.2, 1609.3, 1588.7, 1568.3, 1548.2, 1528.4, 1508.8, 1489.5, 1470.5, 1451.6, 1433.1, 1414.7, 1396.6, 1378.8, 1361.1, 1343.7, 1326.6, 1309.6, 1292.9, 1276.3, 1260.0, 1243.8, 1227.8, 1211.9, 1196.3, 1180.9, 1165.6, 1150.6, 1135.8, 1121.1, 1106.7, 1092.4, 1078.3, 1064.5, 1050.7, 1037.2, 1023.9, 1010.7, 997.71, 984.88, 972.22, 959.73, 947.4, 935.24, 923.23, 911.39, 899.7, 888.16, 876.75, 865.49, 853.93, 842.25, 830.73, 819.38, 808.18, 797.14, 786.26, 775.52, 764.94, 754.51, 744.22, 734.07, 724.07, 714.21, 704.48, 694.89, 685.43, 676.11, 666.92, 657.85, 648.91, 640.1, 631.41, 622.84, 614.39, 606.05, 597.84, 589.73, 581.74, 573.86, 566.1, 558.43, 550.88, 543.43, 536.08, 528.84, 521.69, 514.65, 507.7, 500.85, 494.09, 487.43, 480.86, 474.38, 467.99, 461.69, 455.48, 449.35, 443.31, 437.35, 431.47, 425.68, 419.96, 414.32, 408.76, 403.28, 397.87, 392.54, 387.28, 382.09, 376.98, 371.93, 366.96, 362.05, 357.21, 352.44, 347.73, 343.09, 338.51, 333.99, 329.54, 325.15, 320.82, 316.54, 312.33, 308.17, 304.07, 300.03, 296.04, 292.1, 288.22, 284.4, 280.62, 276.9, 273.23, 269.6, 266.03, 262.51, 259.03, 255.6, 252.22, 248.89, 245.59, 242.35, 239.15, 235.99, 232.8, 229.64, 226.54, 223.47, 220.44, 217.42, 214.45, 211.52, 208.63, 205.77, 202.97, 200.2, 197.46, 194.77, 192.12, 189.5, 186.92, 184.38, 181.88, 179.4, 176.97, 174.57, 172.2, 169.86, 167.56, 165.29, 163.06, 160.85, 158.68, 156.53, 154.38, 152.24, 150.12, 148.04, 145.98, 143.96, 141.96, 139.99, 138.05, 136.14, 134.26, 132.4, 130.57, 128.76, 126.98, 125.23, 123.5, 121.79, 120.11, 118.45, 116.82, 115.21, 113.62, 112.05, 110.51, 108.99, 107.49, 106.01, 104.55, 103.12, 101.7, 100.3, 87.567, 72.866, 60.692, 50.6, 42.063, 34.52, 29.693, 28.356, 28.062, 222.63, 212.71, 187.43, 156.62, 131.89, 111.4, 93.277, 77.963, 65.056, 54.223, 45.17, 37.627, 31.282, 25.962, 21.546, 17.88, 14.767, 12.171, 10.032, 8.2698, 6.8174, 5.6205, 4.634, 3.8208, 3.1326, 2.5677, 2.1048, 1.7254, 1.4144, 1.1596, 0.95066, 0.77942, 0.63828, 0.52132, 0.42581, 0.34782, 0.28412, 0.23209, 0.1896, 0.15489, 0.12654, 0.10338, 0.084467, 0.069014, 0.05639, 0.046077, 0.037651, 0.030767, 0.025142, 0.020546, 0.016791, 0.013723, 0.011215, 0.0091665, 0.0074921, 0.0061237, 0.0050054, 0.0040914, 0.0], - 'energies (keV)': [0.017487, 0.017748, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.100842, 0.1023855, 0.1027971, 0.1033284, 0.1034145, 0.104664, 0.104958, 0.106266, 0.1066932, 0.107334, 0.108936, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.154938, 0.1573095, 0.1579419, 0.1588905, 0.161262, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9, 0.9045, 0.9090225, 0.91356761, 0.91813545, 0.92272613, 0.92733976, 0.93197646, 0.93663634, 0.94131952, 0.94602612, 0.95075625, 0.95551003, 0.96028758, 0.96508902, 0.96991446, 0.97476404, 0.97963786, 0.98453605, 0.98945873, 0.99440602, 0.99937805, 1.0043749, 1.0093968, 1.0144438, 1.019516, 1.0246136, 1.0297367, 1.0348853, 1.0400598, 1.0452601, 1.0504864, 1.0557388, 1.0610175, 1.0663226, 1.0716542, 1.0770125, 1.0823975, 1.0878095, 1.0932486, 1.0987148, 1.1042084, 1.1097294, 1.1152781, 1.115315, 1.115485, 1.1208545, 1.1264587, 1.132091, 1.1377515, 1.142213, 1.1423871, 1.1434402, 1.1491574, 1.1549032, 1.1606777, 1.1664811, 1.1723135, 1.1781751, 1.184066, 1.1899863, 1.1959362, 1.2019159, 1.2079255, 1.2139651, 1.220035, 1.2261351, 1.2322658, 1.2384271, 1.2446193, 1.2508424, 1.2570966, 1.2633821, 1.269699, 1.2760475, 1.2824277, 1.2888399, 1.295284, 1.2964841, 1.298916, 1.3017605, 1.3082693, 1.3148106, 1.3213847, 1.3279916, 1.3346316, 1.3413047, 1.3480112, 1.3547513, 1.361525, 1.3683327, 1.3751743, 1.3820502, 1.3889605, 1.3959053, 1.4028848, 1.4098992, 1.4169487, 1.4240335, 1.4311536, 1.4383094, 1.4455009, 1.4527284, 1.4599921, 1.467292, 1.4746285, 1.4820016, 1.4894117, 1.4968587, 1.504343, 1.5118647, 1.519424, 1.5270212, 1.5346563, 1.5423295, 1.5500412, 1.5577914, 1.5655804, 1.5734083, 1.5812753, 1.5891817, 1.5971276, 1.6051132, 1.6131388, 1.6212045, 1.6293105, 1.6374571, 1.6456443, 1.6538726, 1.6621419, 1.6704526, 1.6788049, 1.6871989, 1.6956349, 1.7041131, 1.7126337, 1.7211968, 1.7298028, 1.7384518, 1.7471441, 1.7558798, 1.7646592, 1.7734825, 1.7823499, 1.7912617, 1.800218, 1.8092191, 1.8182652, 1.8273565, 1.8364933, 1.8456757, 1.8549041, 1.8641786, 1.8734995, 1.882867, 1.8922814, 1.9017428, 1.9112515, 1.9208077, 1.9304118, 1.9400638, 1.9497642, 1.959513, 1.9693105, 1.9791571, 1.9890529, 1.9989981, 2.0089931, 2.0190381, 2.0291333, 2.039279, 2.0494754, 2.0597227, 2.0700213, 2.0803714, 2.0907733, 2.1012272, 2.1117333, 2.122292, 2.1329034, 2.143568, 2.1542858, 2.1650572, 2.1758825, 2.1867619, 2.1976957, 2.2086842, 2.2197276, 2.2308263, 2.2419804, 2.2531903, 2.2644562, 2.2757785, 2.2871574, 2.2985932, 2.3100862, 2.3216366, 2.3332448, 2.344911, 2.3566356, 2.3684187, 2.3802608, 2.3921621, 2.404123, 2.4161436, 2.4282243, 2.4403654, 2.4525672, 2.4648301, 2.4771542, 2.48954, 2.5019877, 2.5144976, 2.5270701, 2.5397055, 2.552404, 2.565166, 2.5779919, 2.5908818, 2.6038362, 2.6168554, 2.6299397, 2.6430894, 2.6563048, 2.6695863, 2.6829343, 2.6963489, 2.7098307, 2.7233798, 2.7369967, 2.7506817, 2.7644351, 2.7782573, 2.7921486, 2.8061093, 2.8201399, 2.8342406, 2.8484118, 2.8626539, 2.8769671, 2.891352, 2.9058087, 2.9203378, 2.9349394, 2.9496141, 2.9643622, 2.979184, 2.9940799, 3.0090503, 3.0240956, 3.0392161, 3.0544122, 3.0696842, 3.0850326, 3.1004578, 3.1159601, 3.1315399, 3.1471976, 3.1629336, 3.1787482, 3.194642, 3.2106152, 3.2266683, 3.2428016, 3.2590156, 3.2753107, 3.2916873, 3.3081457, 3.3246864, 3.3413099, 3.3580164, 3.3748065, 3.3916805, 3.4086389, 3.4256821, 3.4428105, 3.4600246, 3.4773247, 3.4947113, 3.5121849, 3.5297458, 3.5473945, 3.5651315, 3.5829572, 3.6008719, 3.6188763, 3.6369707, 3.6551555, 3.6734313, 3.6917985, 3.7102575, 3.7288088, 3.7474528, 3.7661901, 3.785021, 3.8039461, 3.8229659, 3.8420807, 3.8612911, 3.8805975, 3.9000005, 3.9195005, 3.939098, 3.9587935, 3.9785875, 3.9984804, 4.0184728, 4.0385652, 4.058758, 4.0790518, 4.0994471, 4.1199443, 4.140544, 4.1612467, 4.182053, 4.2029632, 4.2239781, 4.245098, 4.2663234, 4.2876551, 4.3090933, 4.3306388, 4.352292, 4.3740535, 4.3959237, 4.4179033, 4.4399929, 4.4621928, 4.4845038, 4.5069263, 4.5294609, 4.5521082, 4.5748688, 4.5977431, 4.6207318, 4.6438355, 4.6670547, 4.69039, 4.7138419, 4.7374111, 4.7610982, 4.7849037, 4.8088282, 4.8328723, 4.8570367, 4.8813219, 4.9057285, 4.9302571, 4.9549084, 4.9796829, 5.0045814, 5.0296043, 5.0547523, 5.080026, 5.1054262, 5.1309533, 5.1566081, 5.1823911, 5.2083031, 5.2343446, 5.2605163, 5.2868189, 5.313253, 5.3398192, 5.3665183, 5.3933509, 5.4203177, 5.4474193, 5.4746564, 5.5020297, 5.5295398, 5.5571875, 5.5849734, 5.6128983, 5.6409628, 5.6691676, 5.6975135, 5.726001, 5.754631, 5.7834042, 5.8123212, 5.8413828, 5.8705897, 5.8999427, 5.9294424, 5.9590896, 5.988885, 6.0188295, 6.0489236, 6.0791682, 6.1095641, 6.1401119, 6.1708125, 6.2016665, 6.2326749, 6.2638382, 6.2951574, 6.3266332, 6.3582664, 6.3900577, 6.422008, 6.454118, 6.4863886, 6.5188206, 6.5514147, 6.5841717, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.15976, 10.32004, 10.35673, 10.41894, 10.57444, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Pr': {'mass_absorption_coefficient (cm2/g)': [6628.8, 6856.4, 7108.0, 7383.7, 7683.6, 8005.8, 8347.0, 8705.0, 9077.4, 9461.2, 9852.8, 10136.0, 10226.0, 10248.0, 10250.0, 177410.0, 168510.0, 144050.0, 114780.0, 90748.0, 71673.0, 57019.0, 46020.0, 37889.0, 34878.0, 33561.0, 33229.0, 32914.0, 32111.0, 31781.0, 27808.0, 24683.0, 22399.0, 20694.0, 19408.0, 18421.0, 17645.0, 17015.0, 15944.0, 14793.0, 13700.0, 12656.0, 11658.0, 10704.0, 9796.4, 8934.9, 8880.8, 8692.0, 8642.5, 92570.0, 74804.0, 54680.0, 23614.0, 13571.0, 9768.5, 7891.4, 6765.4, 6003.0, 5455.1, 5056.0, 4806.1, 4772.2, 4752.3, 4738.9, 6054.5, 6005.8, 5904.3, 5889.0, 5854.8, 5846.4, 6243.8, 6215.9, 6182.1, 6103.4, 6057.9, 6029.1, 6025.8, 6021.8, 6019.8, 6521.6, 6509.4, 6478.7, 6398.4, 6278.5, 6110.7, 5892.5, 5625.8, 5315.2, 4967.4, 4597.5, 4219.4, 3845.0, 3483.3, 3140.5, 2820.6, 2525.8, 2256.6, 2096.5, 2042.3, 2028.2, 8590.4, 8533.6, 8499.1, 8234.1, 8142.5, 12224.0, 11705.0, 10751.0, 8926.0, 7511.2, 6392.5, 6357.1, 6157.6, 6097.2, 7034.1, 6771.9, 6290.3, 6217.5, 5997.9, 5941.7, 6263.1, 6045.8, 5742.9, 5026.2, 4956.1, 4865.1, 4823.6, 4991.8, 4839.1, 4510.2, 3876.6, 3321.3, 2841.2, 2427.7, 2071.4, 1767.5, 1509.0, 1287.7, 1099.3, 931.5, 783.77, 660.38, 556.52, 467.07, 392.44, 330.23, 278.37, 235.08, 198.05, 182.31, 175.2, 173.38, 524.03, 508.54, 500.13, 446.5, 427.69, 422.91, 576.75, 575.86, 552.63, 523.81, 502.61, 497.22, 566.42, 556.89, 545.7, 471.36, 398.54, 335.95, 282.36, 235.8, 197.09, 164.88, 138.06, 115.27, 96.105, 79.904, 66.439, 55.292, 46.054, 38.386, 32.023, 26.74, 22.349, 18.697, 15.648, 12.975, 10.718, 8.831, 7.2766, 6.0004, 4.952, 4.3067, 4.1235, 4.09, 4.0765, 24.014, 23.113, 20.58, 17.266, 14.47, 12.121, 10.126, 8.4571, 7.0626, 5.8973, 4.9238, 4.1057, 3.4067, 2.8227, 2.339, 1.9383, 1.6065, 1.3315, 1.1037, 0.91499, 0.75857, 0.62894, 0.5215, 0.43245, 0.35938, 0.29869, 0.24826, 0.20635, 0.17154, 0.1426, 0.11855, 0.098564, 0.081951, 0.068142, 0.056662, 0.047119, 0.039184, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.021854, 0.0221885, 0.02227063, 0.0222777, 0.0224115, 0.022746, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.036652, 0.037213, 0.0373626, 0.037587, 0.03797993, 0.038148, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.110936, 0.112634, 0.1130868, 0.113766, 0.115464, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.213248, 0.2152655, 0.216512, 0.2173824, 0.218688, 0.221952, 0.2301188, 0.231574, 0.2351185, 0.2360637, 0.2374815, 0.241026, 0.245997, 0.2629708, 0.2811158, 0.29841, 0.3005128, 0.3029775, 0.3041955, 0.3060225, 0.31059, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.91238, 0.926345, 0.930069, 0.932078, 0.9342948, 0.935655, 0.9463445, 0.9501489, 0.9558555, 0.970122, 0.9987612, 1.067676, 1.141345, 1.217356, 1.220098, 1.235989, 1.240958, 1.248411, 1.267044, 1.304285, 1.310652, 1.330713, 1.336063, 1.344087, 1.364148, 1.394281, 1.48078, 1.490486, 1.503445, 1.509489, 1.518555, 1.54122, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 5.845014, 5.934479, 5.958336, 5.994122, 6.051453, 6.083586, 6.311592, 6.408198, 6.43396, 6.469004, 6.472602, 6.569208, 6.698104, 6.800626, 6.827965, 6.868974, 6.915365, 6.971496, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.15079, 41.78065, 41.89992, 41.94861, 42.20055, 42.83041, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Pt': {'mass_absorption_coefficient (cm2/g)': [40474.0, 39638.0, 33428.0, 33345.0, 33339.0, 60636.0, 59765.0, 74256.0, 83433.0, 93785.0, 104840.0, 115990.0, 126540.0, 135690.0, 142680.0, 146850.0, 147750.0, 145200.0, 139350.0, 130620.0, 119650.0, 107170.0, 94012.0, 80908.0, 68426.0, 57012.0, 46920.0, 38238.0, 30932.0, 24894.0, 19973.0, 18611.0, 17696.0, 17462.0, 34762.0, 33023.0, 32410.0, 25731.0, 20414.0, 16878.0, 16187.0, 16013.0, 15792.0, 21854.0, 20937.0, 18742.0, 18411.0, 17642.0, 17446.0, 19026.0, 18234.0, 18023.0, 17241.0, 17181.0, 17039.0, 17927.0, 17148.0, 15026.0, 12274.0, 10177.0, 8631.1, 8083.3, 7846.5, 7787.6, 7888.1, 7742.2, 7692.4, 7079.1, 6792.6, 6853.7, 7223.6, 7889.4, 8822.7, 9975.7, 11292.0, 12699.0, 14107.0, 15424.0, 16559.0, 17435.0, 17997.0, 18218.0, 18084.0, 17962.0, 17854.0, 17822.0, 18161.0, 18045.0, 18000.0, 17919.0, 17771.0, 17729.0, 17944.0, 17788.0, 17588.0, 16707.0, 15674.0, 14554.0, 13398.0, 12248.0, 11539.0, 11457.0, 11374.0, 11292.0, 11210.0, 11129.0, 11047.0, 10966.0, 10948.0, 11596.0, 11574.0, 11491.0, 11409.0, 11327.0, 11245.0, 11164.0, 11083.0, 11003.0, 10922.0, 10843.0, 10763.0, 10684.0, 10606.0, 10527.0, 10449.0, 10372.0, 10295.0, 10218.0, 10142.0, 10066.0, 9990.5, 9915.4, 9840.7, 9766.4, 9692.4, 9618.9, 9545.8, 9473.1, 9400.8, 9328.9, 9257.4, 9186.3, 9163.7, 9256.9, 9249.3, 9179.0, 9109.1, 9039.6, 8970.4, 8901.7, 8833.4, 8765.6, 8698.1, 8631.0, 8564.3, 8498.0, 8432.2, 8366.7, 8301.7, 8237.0, 8172.8, 8108.9, 8045.5, 7982.4, 7919.8, 7857.5, 7795.7, 7734.2, 7673.2, 7612.5, 7552.2, 7492.3, 7432.8, 7373.7, 7315.0, 7256.6, 7198.7, 7141.1, 7121.1, 7243.6, 7186.3, 7129.3, 7072.8, 7016.6, 6960.7, 6905.3, 6850.2, 6795.5, 6741.2, 6687.2, 6633.6, 6580.4, 6527.5, 6475.0, 6422.9, 6371.1, 6319.7, 6268.6, 6217.9, 6167.6, 6117.6, 6067.9, 6018.6, 5969.6, 5920.9, 5872.6, 5824.7, 5777.0, 5729.7, 5682.8, 5636.1, 5589.8, 5543.8, 5498.2, 5452.9, 5407.9, 5363.2, 5318.8, 5274.7, 5230.8, 5187.2, 5143.9, 5100.9, 5058.2, 5015.8, 4973.8, 4932.0, 4890.6, 4849.4, 4808.6, 4768.0, 4727.8, 4687.8, 4648.2, 4608.9, 4569.8, 4531.1, 4492.6, 4454.4, 4416.5, 4378.9, 4341.6, 4304.5, 4267.7, 4231.1, 4189.8, 4148.9, 4108.3, 4068.2, 4028.4, 3989.0, 3949.7, 3910.9, 3872.4, 3834.4, 3796.6, 3759.3, 3722.3, 3685.6, 3649.4, 3613.4, 3577.8, 3542.6, 3507.7, 3473.2, 3439.0, 3405.1, 3371.6, 3338.4, 3305.5, 3273.0, 3240.8, 3208.9, 3177.3, 3146.1, 3115.1, 3084.5, 3054.2, 3024.2, 2994.5, 2965.1, 2936.0, 2907.2, 2878.6, 2850.4, 2822.4, 2794.6, 2767.1, 2739.9, 2713.0, 2685.1, 2657.2, 2629.5, 2602.2, 2575.2, 2548.4, 2522.0, 2495.8, 2470.0, 2444.4, 2419.1, 2393.2, 2367.5, 2341.0, 2314.8, 2288.9, 2263.3, 2238.1, 2213.1, 2188.5, 2164.2, 2139.9, 2115.4, 2091.2, 2067.3, 2043.7, 2020.4, 1997.4, 1974.7, 1952.3, 1930.1, 1908.3, 1886.7, 1865.4, 1844.3, 1823.6, 1803.0, 1782.8, 1762.8, 1743.0, 1723.5, 1704.2, 1685.2, 1666.4, 1647.8, 1629.5, 1611.4, 1593.5, 1575.9, 1558.4, 1541.2, 1524.2, 1507.4, 1490.8, 1474.4, 1458.2, 1442.2, 1426.4, 1410.8, 1395.4, 1380.1, 1365.1, 1350.2, 1335.5, 1321.0, 1306.7, 1292.6, 1278.6, 1264.8, 1251.1, 1237.6, 1224.3, 1211.1, 1198.1, 1185.3, 1172.6, 1160.0, 1147.6, 1135.4, 1123.3, 1111.3, 1099.5, 1087.8, 1076.3, 1064.9, 1053.6, 1042.5, 1031.5, 1020.6, 1009.8, 999.21, 988.71, 978.33, 968.07, 957.94, 947.92, 938.01, 928.23, 918.55, 908.99, 899.54, 890.09, 880.66, 871.35, 862.14, 855.44, 2614.5, 2609.3, 2577.9, 2546.9, 2516.2, 2486.0, 2456.1, 2426.6, 2397.4, 2392.5, 3510.9, 3480.9, 3438.5, 3396.7, 3355.3, 3314.5, 3274.2, 3234.4, 3195.1, 3156.3, 3118.0, 3080.1, 3042.7, 3005.8, 2969.4, 2933.3, 2897.8, 2862.7, 2828.0, 2793.7, 2759.9, 2726.4, 2693.3, 2660.7, 2628.4, 2596.5, 2565.0, 2533.9, 2503.2, 2472.9, 2443.0, 2413.4, 2384.2, 2355.3, 2326.8, 2298.7, 2270.9, 2247.9, 2243.5, 2617.7, 2592.8, 2561.4, 2530.5, 2499.9, 2469.7, 2439.9, 2410.4, 2381.3, 2352.2, 2323.5, 2296.0, 2268.8, 2242.1, 2215.7, 2189.7, 2164.0, 2138.3, 2113.0, 2088.0, 2063.3, 2038.9, 2014.8, 1991.0, 1967.5, 1944.3, 1920.0, 1902.3, 1895.6, 2014.5, 1996.4, 1970.9, 1945.8, 1921.0, 1896.4, 1872.2, 1848.3, 1824.7, 1801.4, 1778.5, 1756.2, 1734.2, 1712.5, 1691.1, 1670.0, 1649.2, 1634.8, 1628.6, 1689.8, 1680.0, 1659.5, 1639.3, 1619.3, 1599.6, 1580.2, 1561.0, 1542.1, 1523.4, 1504.8, 1486.5, 1468.4, 1450.6, 1433.0, 1415.6, 1398.5, 1381.5, 1364.9, 1348.4, 1332.1, 1316.1, 1300.3, 1284.6, 1269.2, 1254.0, 1238.9, 1224.1, 1209.4, 1195.0, 1180.7, 1166.6, 1152.7, 1139.0, 1125.4, 1112.0, 1098.7, 1085.5, 1072.4, 1059.5, 1046.8, 1034.2, 1021.8, 1009.6, 997.45, 985.49, 973.67, 962.01, 950.49, 939.11, 927.87, 916.67, 905.3, 894.06, 882.95, 871.98, 861.15, 850.46, 839.91, 829.3, 818.78, 808.39, 798.15, 788.04, 778.07, 768.23, 758.52, 748.95, 739.5, 730.17, 720.97, 711.89, 702.93, 694.1, 685.38, 676.77, 668.28, 659.9, 651.63, 643.48, 635.43, 627.49, 619.65, 611.92, 604.28, 596.75, 589.32, 581.99, 574.76, 567.62, 560.57, 553.62, 546.76, 539.99, 533.31, 526.72, 520.05, 513.47, 506.98, 500.58, 494.26, 488.03, 481.88, 475.81, 469.82, 463.91, 458.08, 452.33, 446.66, 441.06, 435.53, 430.08, 424.7, 419.39, 414.16, 408.99, 403.89, 398.83, 393.84, 388.91, 384.04, 379.24, 374.51, 369.78, 365.12, 360.52, 355.98, 351.51, 347.07, 342.69, 338.37, 334.11, 329.91, 325.76, 321.66, 317.62, 313.64, 309.7, 305.82, 301.99, 298.2, 294.45, 290.75, 287.1, 283.5, 279.95, 276.45, 272.99, 269.57, 266.2, 262.88, 259.6, 256.36, 253.17, 250.02, 246.91, 243.84, 240.81, 237.82, 234.87, 231.95, 229.08, 226.24, 223.44, 220.68, 217.95, 215.26, 212.61, 209.99, 207.4, 204.84, 202.32, 199.84, 197.38, 194.96, 192.57, 190.21, 187.87, 185.57, 183.3, 181.06, 178.85, 176.67, 174.51, 172.38, 170.28, 168.21, 166.16, 164.14, 162.15, 160.18, 158.24, 137.63, 115.04, 96.047, 80.347, 74.82, 71.853, 71.074, 186.34, 178.99, 178.92, 149.05, 136.79, 131.18, 129.73, 178.88, 173.91, 171.78, 169.57, 162.67, 160.9, 183.48, 176.65, 168.93, 142.24, 119.63, 100.6, 84.474, 70.936, 59.594, 50.091, 42.061, 35.34, 29.461, 24.467, 20.337, 16.917, 14.084, 11.735, 9.7858, 8.167, 6.8207, 5.696, 4.7532, 3.9674, 3.3016, 2.7441, 2.2816, 1.8974, 1.8675, 1.791, 1.7713, 8.6561, 8.3267, 7.9141, 6.6997, 5.6344, 4.6996, 3.9234, 3.2827, 2.747, 2.2991, 1.9244, 1.611, 1.3481, 1.126, 0.94065, 0.78587, 0.65662, 0.54868, 0.45853, 0.38323, 0.32032, 0.26776, 0.22384, 0.18715, 0.15737, 0.13277, 0.11203, 0.094541, 0.0], - 'energies (keV)': [0.006156007, 0.006247887, 0.007291108, 0.007402706, 0.007432466, 0.007477105, 0.007588704, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.050666, 0.0514415, 0.0516483, 0.0519585, 0.052734, 0.05302035, 0.05667876, 0.06058959, 0.063994, 0.06477028, 0.0649735, 0.0652347, 0.0656265, 0.066606, 0.06923942, 0.069678, 0.0707445, 0.0710289, 0.0714555, 0.072522, 0.072814, 0.0739285, 0.07401695, 0.0742257, 0.0746715, 0.075786, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.099666, 0.1011915, 0.1015983, 0.1022085, 0.1033284, 0.103734, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.307034, 0.3117335, 0.3129867, 0.3148665, 0.319566, 0.3212482, 0.324184, 0.329146, 0.3304692, 0.332454, 0.337416, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.51833567, 0.51966431, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.60832884, 0.61007116, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72085202, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1210653, 2.1221346, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2011954, 2.2026046, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6429266, 2.6450545, 2.6478735, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.0221418, 3.026344, 3.0308581, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2891773, 3.2941347, 3.3028227, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.33243, 11.50588, 11.55214, 11.62152, 11.79334, 11.79497, 12.60708, 13.00715, 13.20624, 13.25933, 13.33896, 13.47697, 13.53805, 13.6023, 13.8105, 13.86602, 13.9493, 14.1575, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 76.8269, 78.00282, 78.3164, 78.78677, 79.96269, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'C': {'mass_absorption_coefficient (cm2/g)': [1072900.0, 1051800.0, 569990.0, 525300.0, 483160.0, 443300.0, 405580.0, 369920.0, 336260.0, 304590.0, 274890.0, 254910.0, 248740.0, 247180.0, 247130.0, 272340.0, 267450.0, 253120.0, 234370.0, 217230.0, 201340.0, 186430.0, 172270.0, 158790.0, 145940.0, 133690.0, 122040.0, 111000.0, 100590.0, 90829.0, 81721.0, 73276.0, 65488.0, 58347.0, 51834.0, 45922.0, 40582.0, 35777.0, 31461.0, 27596.0, 24151.0, 21091.0, 18383.0, 15995.0, 13846.0, 11955.0, 10309.0, 8879.9, 7640.8, 6568.4, 5641.6, 4841.7, 4152.3, 3558.7, 3048.1, 2609.5, 2289.4, 2232.9, 2209.5, 2188.9, 48381.0, 46931.0, 43461.0, 37871.0, 32792.0, 28212.0, 24134.0, 20554.0, 17441.0, 14755.0, 12450.0, 10482.0, 8808.1, 7389.0, 6188.9, 5176.5, 4323.9, 3607.2, 3005.7, 2501.5, 2079.5, 1725.5, 1429.6, 1176.0, 967.44, 795.85, 654.69, 538.58, 443.06, 364.48, 299.84, 246.67, 202.92, 166.94, 137.33, 112.98, 92.946, 76.069, 61.956, 50.461, 41.099, 33.474, 27.264, 22.206, 18.086, 14.731, 11.938, 9.6701, 7.8329, 6.3448, 5.1394, 4.163, 3.3722, 2.7278, 2.1961, 1.768, 1.4234, 1.1459, 0.92254, 0.74271, 0.59793, 0.48138, 0.38755, 0.312, 0.25119, 0.20222, 0.16281, 0.13107, 0.10552, 0.084954, 0.068184, 0.054669, 0.043834, 0.035146, 0.02818, 0.022594, 0.018116, 0.014525, 0.011646, 0.0093381, 0.0074873, 0.0060033, 0.0048135, 0.0038595, 0.0030945, 0.0024812, 0.00199, 0.001597, 0.0012816, 0.0010286, 0.00082544, 0.00066244, 0.00053162, 0.00042664, 0.00034239, 0.00027478, 0.00022052, 0.00017697, 0.00014203, 0.00011398, 9.1472e-05, 7.3409e-05, 5.8913e-05, 4.728e-05, 3.7944e-05, 3.0451e-05, 2.4438e-05, 1.9612e-05, 1.574e-05, 1.2632e-05, 1.0137e-05, 8.1356e-06, 0.0], - 'energies (keV)': [0.006432, 0.006528, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.0191198, 0.01941245, 0.01948844, 0.01949049, 0.01960755, 0.0199002, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.278124, 0.2811158, 0.282381, 0.2835162, 0.285219, 0.289476, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Pb': {'mass_absorption_coefficient (cm2/g)': [117820.0, 114210.0, 25787.0, 22557.0, 22441.0, 21759.0, 21582.0, 38679.0, 37273.0, 35086.0, 29667.0, 24757.0, 20394.0, 16685.0, 13619.0, 11122.0, 10112.0, 9661.3, 9546.0, 23132.0, 23207.0, 23251.0, 24168.0, 24742.0, 25182.0, 25312.0, 31797.0, 32783.0, 32892.0, 38692.0, 46736.0, 56937.0, 68839.0, 81576.0, 93901.0, 104370.0, 111600.0, 114620.0, 113060.0, 107210.0, 97922.0, 86389.0, 73850.0, 61393.0, 49824.0, 39629.0, 31017.0, 23985.0, 18677.0, 18411.0, 17576.0, 17295.0, 22049.0, 20771.0, 18381.0, 14065.0, 11105.0, 10852.0, 10482.0, 10325.0, 12561.0, 11921.0, 10637.0, 8500.5, 6833.0, 5529.8, 5479.1, 5228.3, 5164.4, 5528.2, 5410.1, 5336.1, 5222.2, 5175.1, 5427.2, 5385.0, 5378.0, 5290.2, 5243.0, 5210.0, 5322.5, 5215.8, 5062.8, 4866.0, 4940.7, 5274.8, 5854.5, 6655.7, 7638.2, 8743.6, 9896.8, 11012.0, 12005.0, 12797.0, 13334.0, 13585.0, 13550.0, 13440.0, 13365.0, 13343.0, 13765.0, 13714.0, 13694.0, 13625.0, 13532.0, 13506.0, 13758.0, 13665.0, 13595.0, 13051.0, 12628.0, 12574.0, 12519.0, 12464.0, 12408.0, 12351.0, 12294.0, 12236.0, 12177.0, 12118.0, 12058.0, 11998.0, 11937.0, 11876.0, 11814.0, 11752.0, 11689.0, 11626.0, 11562.0, 11499.0, 11434.0, 11370.0, 11305.0, 11239.0, 11174.0, 11107.0, 11041.0, 10974.0, 10907.0, 10840.0, 10773.0, 10705.0, 10637.0, 10569.0, 10501.0, 10432.0, 10364.0, 10295.0, 10226.0, 10157.0, 10089.0, 10020.0, 9951.1, 9882.3, 9813.6, 9745.0, 9676.4, 9608.0, 9539.7, 9471.6, 9403.6, 9359.0, 9914.2, 9904.2, 9844.6, 9775.2, 9706.1, 9637.2, 9568.6, 9500.3, 9432.2, 9364.5, 9297.1, 9230.0, 9163.3, 9097.0, 9031.0, 8965.4, 8900.1, 8834.7, 8766.3, 8698.2, 8630.4, 8563.1, 8496.0, 8429.3, 8363.0, 8297.0, 8231.4, 8166.2, 8100.0, 8033.2, 7966.9, 7900.9, 7835.3, 7770.0, 7705.1, 7660.1, 7746.3, 7729.7, 7682.2, 7618.4, 7555.0, 7492.0, 7429.4, 7367.3, 7305.5, 7244.2, 7183.2, 7122.7, 7062.6, 7003.0, 6943.7, 6884.9, 6826.6, 6768.6, 6711.1, 6654.1, 6597.4, 6541.3, 6485.5, 6430.2, 6375.3, 6320.9, 6266.9, 6213.3, 6160.2, 6107.5, 6055.3, 6003.5, 5952.2, 5945.6, 6038.9, 6024.1, 5973.2, 5922.7, 5872.7, 5823.2, 5774.0, 5725.4, 5677.1, 5629.3, 5581.9, 5534.9, 5488.4, 5442.3, 5396.7, 5351.4, 5306.6, 5262.2, 5218.3, 5174.7, 5131.6, 5088.9, 5046.6, 5004.4, 4950.2, 4896.7, 4843.9, 4791.7, 4740.1, 4689.1, 4638.7, 4589.0, 4539.8, 4491.2, 4443.2, 4395.7, 4348.9, 4302.5, 4256.8, 4211.6, 4166.9, 4122.4, 4078.4, 4034.9, 3991.9, 3949.4, 3907.4, 3866.0, 3825.0, 3784.5, 3744.4, 3704.9, 3665.8, 3627.1, 3589.0, 3551.2, 3513.9, 3477.1, 3440.6, 3404.6, 3369.1, 3333.9, 3299.1, 3264.8, 3230.8, 3197.3, 3164.1, 3131.3, 3098.9, 3066.9, 3035.2, 3004.0, 2973.0, 2942.5, 2912.2, 2882.2, 2852.5, 2823.2, 2794.1, 2765.4, 2737.1, 2709.0, 2681.3, 2653.9, 2626.8, 2600.0, 2573.5, 2547.3, 2521.4, 2495.9, 2470.6, 2445.5, 2420.8, 2396.4, 2372.2, 2348.3, 2324.7, 2301.3, 2278.2, 2255.4, 2232.8, 2210.5, 2188.4, 2166.6, 2145.0, 2123.6, 2102.5, 2081.6, 2060.9, 2040.4, 2020.1, 2000.1, 1980.3, 1960.7, 1941.4, 1922.2, 1903.3, 1884.5, 1866.0, 1847.7, 1829.6, 1811.6, 1793.8, 1776.3, 1758.4, 1740.0, 1721.9, 1704.0, 1686.2, 1668.7, 1651.4, 1634.2, 1617.3, 1600.5, 1584.0, 1566.9, 1550.0, 1533.3, 1516.7, 1500.4, 1484.2, 1468.3, 1452.5, 1436.9, 1421.4, 1406.2, 1391.1, 1376.2, 1361.5, 1347.0, 1332.6, 1318.4, 1304.3, 1290.4, 1276.7, 1263.1, 1249.7, 1236.4, 1223.3, 1210.3, 1197.5, 1184.8, 1172.3, 1159.9, 1147.6, 1135.5, 1123.5, 1111.7, 1099.9, 1088.4, 1076.9, 1065.6, 1054.4, 1043.4, 1032.4, 1021.6, 1010.9, 1000.3, 989.89, 979.56, 969.34, 959.24, 949.25, 939.37, 929.6, 919.95, 910.4, 900.95, 891.62, 882.38, 873.25, 864.23, 855.3, 846.47, 837.74, 829.11, 820.57, 812.13, 803.78, 795.53, 787.36, 779.29, 771.3, 763.41, 755.6, 747.88, 745.2, 2132.8, 2118.6, 2092.6, 2067.0, 2041.7, 2016.7, 1992.0, 1967.7, 1943.6, 1934.4, 2816.8, 2799.1, 2763.8, 2729.0, 2694.7, 2660.7, 2627.3, 2594.2, 2561.6, 2529.2, 2497.3, 2465.8, 2434.7, 2404.0, 2373.8, 2343.9, 2314.3, 2285.2, 2256.4, 2228.1, 2200.0, 2172.4, 2145.1, 2118.1, 2091.5, 2065.3, 2039.4, 2013.8, 1988.5, 1963.6, 1939.0, 1913.6, 1888.2, 1863.1, 1838.4, 1827.8, 2136.9, 2132.2, 2103.2, 2074.5, 2046.3, 2018.4, 1990.9, 1963.7, 1937.0, 1910.6, 1884.5, 1859.7, 1835.2, 1811.1, 1787.4, 1764.0, 1741.0, 1718.4, 1696.1, 1674.2, 1652.4, 1631.0, 1609.8, 1589.0, 1568.5, 1548.3, 1528.4, 1508.8, 1489.4, 1470.4, 1452.7, 1451.6, 1543.7, 1534.1, 1514.2, 1494.5, 1475.0, 1455.9, 1437.1, 1418.5, 1400.1, 1382.1, 1364.3, 1347.3, 1330.5, 1313.9, 1297.7, 1281.6, 1267.9, 1265.8, 1310.2, 1305.9, 1290.2, 1274.8, 1259.6, 1244.5, 1229.7, 1215.1, 1200.5, 1186.0, 1171.7, 1157.6, 1143.6, 1129.8, 1116.3, 1102.8, 1089.6, 1076.6, 1063.7, 1050.9, 1038.4, 1026.0, 1013.7, 1001.7, 989.72, 977.93, 966.3, 954.81, 943.46, 932.26, 921.19, 910.27, 899.47, 888.81, 878.28, 867.88, 857.61, 847.43, 837.25, 827.19, 817.25, 807.43, 797.73, 788.15, 778.68, 769.33, 760.09, 750.96, 741.94, 733.03, 724.22, 715.53, 706.76, 698.0, 689.33, 680.76, 672.31, 663.96, 655.72, 647.59, 639.46, 631.37, 623.39, 615.51, 607.74, 600.06, 592.49, 585.02, 577.64, 570.36, 563.18, 556.09, 549.1, 542.19, 535.38, 528.66, 522.02, 515.48, 509.02, 502.64, 496.35, 490.14, 484.02, 477.97, 472.01, 466.12, 460.31, 454.58, 448.93, 443.34, 437.84, 432.4, 427.04, 421.75, 416.5, 411.27, 406.11, 401.01, 395.99, 391.03, 386.14, 381.31, 376.55, 371.85, 367.21, 362.63, 358.12, 353.66, 349.26, 344.93, 340.64, 336.42, 332.25, 328.13, 324.07, 320.07, 316.11, 312.21, 308.36, 304.56, 300.81, 297.11, 293.46, 289.86, 286.28, 282.74, 279.24, 275.8, 272.39, 269.03, 265.72, 262.45, 259.22, 256.03, 252.88, 249.78, 246.72, 243.69, 240.71, 237.76, 234.84, 231.95, 229.09, 226.27, 223.48, 220.72, 218.01, 215.33, 212.68, 210.07, 207.49, 204.95, 202.43, 199.94, 197.48, 195.06, 192.66, 190.3, 187.97, 185.67, 183.4, 159.31, 133.31, 111.74, 93.698, 78.453, 65.761, 63.517, 61.034, 60.396, 154.61, 148.58, 143.25, 119.42, 108.97, 104.53, 103.38, 143.07, 139.96, 137.45, 136.51, 131.0, 129.58, 147.71, 142.24, 136.05, 114.58, 96.409, 81.116, 68.193, 57.342, 48.243, 40.607, 33.92, 28.297, 23.581, 19.613, 16.325, 13.6, 11.337, 9.4586, 7.8974, 6.5988, 5.5177, 4.6109, 3.8521, 3.2195, 2.6838, 2.2356, 1.8581, 1.5898, 1.5362, 1.5225, 1.5053, 7.1232, 6.8582, 6.208, 5.2253, 4.3907, 3.6871, 3.0954, 2.5951, 2.1741, 1.8212, 1.5256, 1.2781, 1.0709, 0.8968, 0.75105, 0.62905, 0.52693, 0.44143, 0.36984, 0.30989, 0.25968, 0.21762, 0.18239, 0.15288, 0.12815, 0.10743, 0.0], - 'energies (keV)': [0.004936221, 0.005009896, 0.01069, 0.01142761, 0.01145657, 0.01163193, 0.01167869, 0.01174883, 0.01192419, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.018816, 0.019104, 0.0191808, 0.019296, 0.01948844, 0.019584, 0.02083314, 0.021364, 0.021691, 0.0217782, 0.021909, 0.022236, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08428, 0.08458368, 0.08557, 0.085914, 0.08643, 0.08772, 0.09041995, 0.09665893, 0.102704, 0.1033284, 0.104276, 0.1046952, 0.105324, 0.106896, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.135338, 0.1374095, 0.1379619, 0.1387905, 0.140042, 0.140862, 0.1421855, 0.1427571, 0.1436145, 0.1442475, 0.144354, 0.145758, 0.1465635, 0.1471527, 0.1480365, 0.150246, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.404642, 0.4108355, 0.4124871, 0.4149645, 0.4195189, 0.421158, 0.426496, 0.433024, 0.4347648, 0.437376, 0.443904, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64372017, 0.64482097, 0.64527986, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76283052, 0.76398574, 0.76496944, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89230427, 0.89489571, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.4833243, 2.4846756, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5848604, 2.5863394, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0632417, 3.0695584, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5490108, 3.5500321, 3.5593891, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8424209, 3.8449373, 3.8589789, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 12.7745, 12.97002, 13.02216, 13.10038, 13.2959, 13.47697, 14.40688, 14.896, 15.124, 15.1848, 15.276, 15.40095, 15.504, 15.54358, 15.7815, 15.84494, 15.9401, 16.17802, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 86.24441, 87.29069, 87.56448, 87.9165, 88.44452, 89.76459, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Pa': {'mass_absorption_coefficient (cm2/g)': [76785.0, 72702.0, 71344.0, 55780.0, 43536.0, 33887.0, 26354.0, 23733.0, 22416.0, 22081.0, 53980.0, 51096.0, 50867.0, 39131.0, 30222.0, 23768.0, 18962.0, 15297.0, 12445.0, 11501.0, 10989.0, 10859.0, 11754.0, 11204.0, 9130.3, 7477.3, 6156.1, 5087.8, 4217.5, 3505.3, 2921.2, 2441.7, 2048.1, 1878.7, 1807.0, 1788.6, 22261.0, 21839.0, 21499.0, 19570.0, 18885.0, 18673.0, 30263.0, 29360.0, 26358.0, 21817.0, 17066.0, 12831.0, 9450.7, 6928.1, 5117.6, 3843.3, 2951.5, 2761.9, 2614.0, 2576.9, 3568.5, 3415.2, 3328.3, 2773.6, 2361.2, 2073.8, 2053.3, 2013.7, 1998.4, 2479.3, 2397.8, 2238.0, 1960.1, 1747.6, 1582.9, 1562.3, 1531.1, 1523.1, 1635.2, 1606.3, 1574.6, 1465.6, 1429.0, 1408.4, 1403.0, 1523.6, 1529.1, 1537.8, 1539.3, 1548.0, 1554.7, 1665.4, 1727.7, 1976.5, 2977.3, 4659.7, 6726.1, 8683.2, 10102.0, 10792.0, 10795.0, 10284.0, 9862.4, 9665.9, 9612.5, 10427.0, 10357.0, 10225.0, 10105.0, 9888.7, 9830.8, 10265.0, 10051.0, 9925.4, 8948.9, 7997.5, 7108.6, 6445.7, 6304.1, 6270.2, 6222.7, 6592.7, 6402.1, 5928.6, 5222.6, 4755.3, 4616.9, 4602.8, 4581.1, 4596.4, 4465.8, 4095.4, 3767.5, 3653.6, 3624.1, 3648.3, 3543.5, 3178.3, 2744.2, 2371.7, 2051.8, 1777.0, 1541.3, 1338.6, 1165.0, 1015.6, 886.8, 773.82, 661.91, 558.71, 536.64, 516.58, 511.41, 1143.1, 1146.8, 1149.4, 1150.3, 1155.5, 1157.4, 1664.5, 1615.4, 1509.9, 1263.6, 1232.3, 1178.4, 1164.4, 1339.0, 1283.9, 1219.6, 1014.9, 874.89, 850.64, 840.74, 831.97, 872.57, 840.78, 779.2, 766.21, 750.49, 743.1, 764.81, 737.9, 680.49, 579.61, 493.96, 420.51, 356.29, 302.02, 256.41, 217.01, 181.68, 152.23, 127.57, 107.0, 89.897, 75.631, 63.672, 53.605, 45.554, 45.089, 43.802, 43.351, 105.89, 101.89, 94.072, 78.598, 67.39, 65.536, 64.656, 63.952, 89.453, 86.462, 86.054, 83.096, 82.229, 93.62, 90.52, 90.24, 76.485, 64.52, 54.45, 45.69, 38.263, 32.07, 26.873, 22.496, 18.852, 15.795, 13.186, 11.013, 9.2053, 7.7001, 6.4458, 5.4, 4.5272, 3.7983, 3.1848, 2.6644, 2.218, 1.8437, 1.5304, 1.2713, 1.1562, 1.1085, 1.0963, 4.7711, 4.6837, 4.5953, 3.9531, 3.3336, 2.8084, 2.3654, 1.9921, 1.6759, 1.4099, 1.1862, 0.99811, 0.83994, 0.70707, 0.59578, 0.50207, 0.42315, 0.35668, 0.30068, 0.2535, 0.21374, 0.18024, 0.152, 0.0], - 'energies (keV)': [0.02042217, 0.02072698, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02797422, 0.0284024, 0.02851658, 0.02868785, 0.02908327, 0.02911602, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04454936, 0.04523124, 0.04541307, 0.04568582, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.08745598, 0.08879459, 0.08915156, 0.089687, 0.09041995, 0.09102561, 0.09474531, 0.0961955, 0.09665893, 0.09716228, 0.09861247, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1793891, 0.1821348, 0.182867, 0.1839653, 0.1867111, 0.1883732, 0.2013709, 0.2152655, 0.2289513, 0.2301188, 0.2324557, 0.2333902, 0.2347919, 0.2382963, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.303408, 0.308052, 0.3092904, 0.311148, 0.315792, 0.3212482, 0.3434143, 0.35231, 0.3577025, 0.3591405, 0.3612975, 0.363776, 0.36669, 0.3671099, 0.369344, 0.3708288, 0.373056, 0.378624, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.694036, 0.704659, 0.7074918, 0.711741, 0.7154399, 0.722364, 0.728532, 0.739683, 0.7426566, 0.747117, 0.758268, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.986566, 0.9987612, 1.001667, 1.005693, 1.011734, 1.026834, 1.067676, 1.141345, 1.199814, 1.218179, 1.220098, 1.223076, 1.230422, 1.248786, 1.304285, 1.359358, 1.380164, 1.385713, 1.394281, 1.414842, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.372964, 3.424591, 3.438358, 3.459009, 3.510636, 3.538976, 3.548445, 3.593144, 3.607589, 3.629256, 3.683424, 3.793288, 4.055024, 4.090324, 4.152931, 4.169626, 4.194669, 4.257276, 4.334821, 4.633924, 4.900882, 4.953664, 4.975895, 4.995899, 5.025904, 5.100918, 5.259562, 5.295467, 5.340065, 5.361533, 5.393734, 5.474238, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.39844, 16.46362, 16.64944, 16.71637, 16.81677, 17.06776, 17.59961, 18.81398, 19.90743, 20.11215, 20.21213, 20.29339, 20.41527, 20.68251, 20.71997, 20.99908, 21.0835, 21.21012, 21.49988, 21.52669, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 110.3494, 112.0384, 112.4888, 113.1644, 113.9931, 114.8534, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Pd': {'mass_absorption_coefficient (cm2/g)': [101340.0, 96767.0, 84925.0, 80997.0, 79991.0, 141020.0, 134070.0, 218340.0, 240760.0, 258860.0, 270570.0, 274410.0, 269810.0, 257200.0, 237890.0, 213790.0, 187050.0, 159690.0, 133380.0, 109340.0, 88284.0, 70467.0, 55791.0, 43953.0, 34557.0, 27186.0, 21449.0, 17005.0, 13570.0, 10915.0, 8856.6, 8600.2, 8214.1, 8115.8, 59666.0, 54426.0, 48980.0, 32687.0, 22771.0, 16760.0, 12911.0, 10329.0, 8527.6, 7229.1, 7211.8, 6968.7, 6907.1, 7506.4, 7307.6, 6993.9, 6347.5, 5885.6, 5542.5, 5265.4, 5033.6, 4832.7, 4652.6, 4486.1, 4328.0, 4174.6, 4023.2, 3872.0, 3719.2, 3563.1, 3403.4, 3240.8, 3075.7, 2909.0, 2741.8, 2689.7, 2650.4, 2641.7, 5521.1, 5606.2, 5668.0, 7782.3, 7923.2, 8212.8, 10293.0, 13270.0, 15727.0, 17229.0, 17671.0, 17198.0, 16974.0, 16732.0, 16664.0, 18821.0, 18516.0, 18287.0, 18284.0, 17938.0, 17843.0, 18624.0, 18253.0, 17561.0, 15745.0, 14442.0, 14032.0, 13923.0, 14367.0, 13966.0, 12764.0, 11148.0, 9700.2, 8422.7, 7300.7, 6315.5, 5411.8, 4632.7, 3961.7, 3387.3, 2881.5, 2442.5, 2073.8, 1763.6, 1502.3, 1281.8, 1095.6, 934.21, 793.58, 674.67, 574.1, 488.65, 410.93, 409.22, 392.36, 388.03, 1266.9, 1244.6, 1232.4, 1210.5, 1208.1, 1204.9, 1614.1, 1572.5, 1453.8, 1438.1, 1401.4, 1387.5, 1569.4, 1512.9, 1395.2, 1163.5, 969.95, 812.54, 682.1, 573.89, 483.58, 407.73, 343.24, 287.6, 240.78, 201.54, 168.85, 141.25, 117.24, 97.364, 80.88, 67.247, 55.966, 46.594, 38.737, 32.152, 26.611, 22.021, 18.24, 15.122, 12.548, 10.421, 9.3898, 9.003, 8.9036, 58.117, 57.488, 55.802, 48.085, 40.368, 33.786, 28.215, 23.507, 19.587, 16.324, 13.605, 11.34, 9.4536, 7.8418, 6.4942, 5.3787, 4.4551, 3.6905, 3.0569, 2.5322, 2.0936, 1.7259, 1.4229, 1.1731, 0.96708, 0.79636, 0.65582, 0.54011, 0.44484, 0.3664, 0.3018, 0.2486, 0.20479, 0.16871, 0.13899, 0.11451, 0.094349, 0.077739, 0.064056, 0.052783, 0.043496, 0.035844, 0.029539, 0.024344, 0.020064, 0.016536, 0.0], - 'energies (keV)': [0.005043505, 0.005118781, 0.005337699, 0.005419399, 0.005441185, 0.005473865, 0.005555565, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.050078, 0.0508445, 0.0510489, 0.0513555, 0.052122, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.084672, 0.085968, 0.0863136, 0.086832, 0.088128, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.328006, 0.3332, 0.3343653, 0.3363735, 0.3383, 0.33966, 0.3417, 0.3434143, 0.3468, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.52087, 0.5288425, 0.5309685, 0.5341575, 0.54213, 0.5478508, 0.547918, 0.5563045, 0.5585409, 0.5618955, 0.570282, 0.5856525, 0.6260625, 0.656502, 0.6665505, 0.6692609, 0.6732495, 0.683298, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.109834, 3.157434, 3.170127, 3.189167, 3.236766, 3.263694, 3.313649, 3.319406, 3.32697, 3.346952, 3.396906, 3.532214, 3.548445, 3.586279, 3.600696, 3.622322, 3.676386, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 23.86329, 24.22855, 24.32595, 24.47205, 24.56923, 24.83731, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Cd': {'mass_absorption_coefficient (cm2/g)': [99114.0, 92377.0, 54375.0, 47747.0, 47432.0, 51600.0, 59801.0, 72145.0, 88882.0, 110090.0, 135380.0, 163670.0, 193100.0, 221140.0, 244940.0, 261880.0, 270030.0, 268640.0, 258140.0, 240050.0, 216540.0, 190010.0, 162610.0, 136230.0, 112180.0, 91154.0, 73356.0, 58663.0, 46761.0, 37256.0, 35751.0, 33958.0, 33500.0, 51853.0, 48611.0, 45642.0, 34421.0, 26579.0, 21090.0, 17104.0, 14136.0, 11884.0, 11309.0, 10907.0, 10805.0, 11472.0, 11120.0, 10974.0, 9644.5, 8594.9, 7749.3, 7056.6, 6478.6, 5986.9, 5571.4, 5273.3, 5002.6, 4751.5, 4514.3, 4286.7, 4065.9, 3849.3, 3634.7, 3422.2, 3212.3, 3005.6, 2802.9, 2778.7, 2733.4, 2728.9, 2721.5, 9860.0, 9920.8, 9957.7, 14875.0, 14907.0, 15157.0, 15191.0, 16413.0, 17189.0, 17083.0, 16356.0, 15193.0, 14561.0, 14242.0, 14157.0, 15999.0, 15756.0, 15653.0, 15325.0, 14968.0, 14874.0, 15529.0, 15175.0, 14977.0, 13350.0, 12041.0, 11732.0, 11685.0, 11592.0, 11970.0, 11627.0, 10740.0, 9382.3, 8178.0, 7120.8, 6125.7, 5249.4, 4492.2, 3840.9, 3279.9, 2800.8, 2392.3, 2032.1, 1723.9, 1464.9, 1246.7, 1062.8, 907.47, 774.22, 658.27, 559.84, 470.84, 391.53, 347.52, 333.37, 329.74, 1120.3, 1115.2, 1076.6, 1045.9, 1008.9, 998.77, 1356.7, 1314.9, 1307.8, 1196.7, 1150.2, 1138.2, 1291.9, 1278.4, 1244.6, 1078.8, 908.63, 763.98, 642.2, 539.55, 453.55, 381.42, 320.91, 269.57, 226.03, 189.41, 158.41, 131.42, 109.13, 90.706, 75.447, 62.766, 52.257, 43.551, 36.299, 30.175, 25.061, 20.747, 17.192, 14.258, 11.836, 9.8338, 8.2533, 8.1777, 7.9149, 7.8279, 49.766, 47.787, 44.016, 36.824, 30.857, 25.874, 21.618, 18.035, 15.037, 12.53, 10.438, 8.6932, 7.2273, 5.99, 4.9649, 4.1156, 3.4119, 2.8287, 2.3415, 1.9318, 1.5938, 1.3151, 1.0851, 0.89548, 0.73849, 0.60893, 0.50213, 0.41408, 0.34149, 0.28164, 0.23229, 0.19159, 0.15803, 0.13036, 0.10754, 0.088712, 0.073187, 0.06038, 0.049817, 0.041103, 0.033914, 0.027984, 0.023092, 0.019055, 0.0], - 'energies (keV)': [0.0093465, 0.009486, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.065562, 0.0665655, 0.0668331, 0.0672345, 0.068238, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.105448, 0.107062, 0.1074924, 0.108138, 0.109752, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.395626, 0.4016815, 0.40229, 0.4032963, 0.4057185, 0.4084475, 0.4100895, 0.411774, 0.4125525, 0.41871, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.60417, 0.6134175, 0.6158835, 0.6195825, 0.6260625, 0.62883, 0.637686, 0.6474465, 0.6500493, 0.6539535, 0.663714, 0.6692609, 0.7154399, 0.754796, 0.7648052, 0.766349, 0.7694298, 0.774051, 0.785604, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.46675, 3.519812, 3.533962, 3.548445, 3.555187, 3.60825, 3.65246, 3.708365, 3.723273, 3.745635, 3.793288, 3.80154, 3.93764, 3.99791, 4.013982, 4.03809, 4.055024, 4.09836, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.17698, 26.2645, 26.57764, 26.68449, 26.84476, 27.24542, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Po': {'mass_absorption_coefficient (cm2/g)': [126860.0, 122300.0, 63243.0, 61432.0, 60965.0, 160760.0, 156920.0, 91275.0, 80886.0, 71265.0, 62391.0, 54260.0, 46870.0, 40212.0, 37436.0, 36094.0, 35746.0, 44854.0, 43380.0, 42937.0, 35615.0, 29205.0, 23924.0, 19578.0, 16005.0, 13072.0, 10668.0, 8701.4, 7321.7, 7095.1, 6989.4, 6904.1, 12346.0, 13327.0, 17275.0, 33293.0, 61867.0, 98350.0, 133170.0, 156430.0, 162990.0, 153940.0, 134370.0, 110290.0, 86400.0, 65438.0, 48446.0, 35388.0, 25704.0, 18685.0, 13870.0, 13665.0, 12932.0, 12696.0, 15872.0, 14878.0, 13076.0, 9889.9, 7616.6, 6523.2, 6182.7, 6096.8, 7854.1, 7833.3, 7444.4, 6285.6, 5115.0, 4226.6, 3547.8, 3487.7, 3410.5, 3360.1, 3327.7, 3555.4, 3538.5, 3664.1, 3638.1, 3593.4, 3576.6, 3746.9, 3746.8, 3723.4, 3769.7, 4135.3, 4829.1, 5826.6, 7067.4, 8455.8, 9873.5, 11198.0, 12321.0, 13162.0, 13674.0, 13848.0, 13703.0, 13278.0, 12973.0, 12820.0, 12778.0, 13378.0, 13304.0, 13232.0, 13076.0, 12911.0, 12867.0, 12860.0, 13240.0, 13200.0, 13147.0, 13093.0, 13039.0, 12984.0, 12929.0, 12874.0, 12818.0, 12761.0, 12704.0, 12646.0, 12588.0, 12529.0, 12470.0, 12410.0, 12350.0, 12289.0, 12228.0, 12166.0, 12104.0, 12042.0, 11979.0, 11915.0, 11851.0, 11787.0, 11723.0, 11657.0, 11592.0, 11526.0, 11460.0, 11394.0, 11327.0, 11260.0, 11193.0, 11126.0, 11059.0, 10991.0, 10924.0, 10856.0, 10788.0, 10720.0, 10651.0, 10583.0, 10514.0, 10446.0, 10377.0, 10308.0, 10240.0, 10171.0, 10102.0, 10033.0, 9964.8, 9896.1, 9827.5, 9758.9, 9690.4, 9622.0, 9553.7, 9485.5, 9417.4, 9349.5, 9281.7, 9214.0, 9146.4, 9079.0, 9011.7, 8944.5, 8877.6, 8835.9, 9365.6, 9355.0, 9296.9, 9228.3, 9159.9, 9091.8, 9023.8, 8956.2, 8888.8, 8821.6, 8754.8, 8688.2, 8622.0, 8556.1, 8490.5, 8425.2, 8360.3, 8295.8, 8231.6, 8167.8, 8104.4, 8041.4, 7978.7, 7916.5, 7854.7, 7793.2, 7732.2, 7669.7, 7606.9, 7544.5, 7482.5, 7420.9, 7359.6, 7298.8, 7238.4, 7178.4, 7118.8, 7059.6, 6999.8, 6980.2, 7040.8, 7036.3, 6976.8, 6917.8, 6859.1, 6800.9, 6743.0, 6685.6, 6628.6, 6572.1, 6515.9, 6460.2, 6404.9, 6350.1, 6295.7, 6241.7, 6188.1, 6135.0, 6082.4, 6030.2, 5978.4, 5927.0, 5876.1, 5825.7, 5775.7, 5726.1, 5676.9, 5628.3, 5580.0, 5532.2, 5484.8, 5437.9, 5402.5, 5391.4, 5494.4, 5461.4, 5401.1, 5341.5, 5282.7, 5224.6, 5167.2, 5110.5, 5054.4, 4999.1, 4944.4, 4890.4, 4837.0, 4784.3, 4732.2, 4680.8, 4630.0, 4579.7, 4530.1, 4481.1, 4432.7, 4385.0, 4337.8, 4291.2, 4245.1, 4199.6, 4154.7, 4110.3, 4066.4, 4023.1, 3980.2, 3937.9, 3896.1, 3854.8, 3814.0, 3773.7, 3733.8, 3694.1, 3654.8, 3616.0, 3577.6, 3539.7, 3502.3, 3465.3, 3428.7, 3392.5, 3356.8, 3321.5, 3286.6, 3252.2, 3218.1, 3184.4, 3151.1, 3118.2, 3085.7, 3053.6, 3021.9, 2990.5, 2959.5, 2928.8, 2898.5, 2868.6, 2839.0, 2809.7, 2780.8, 2752.2, 2724.0, 2696.0, 2668.4, 2641.2, 2614.2, 2587.5, 2561.2, 2535.1, 2509.3, 2483.7, 2458.4, 2433.3, 2408.6, 2384.1, 2359.9, 2335.9, 2312.3, 2288.9, 2265.7, 2242.9, 2220.3, 2197.9, 2175.8, 2153.9, 2132.3, 2110.9, 2089.8, 2068.9, 2048.3, 2027.8, 2007.6, 1987.7, 1967.9, 1948.4, 1929.0, 1909.9, 1891.0, 1872.4, 1853.9, 1835.6, 1817.5, 1799.5, 1781.8, 1764.2, 1746.8, 1729.6, 1712.6, 1695.8, 1679.2, 1662.8, 1646.5, 1630.4, 1614.5, 1598.8, 1583.2, 1567.8, 1552.6, 1537.5, 1522.6, 1507.8, 1493.2, 1478.8, 1464.5, 1450.3, 1435.4, 1420.6, 1405.9, 1391.5, 1377.2, 1363.0, 1349.1, 1335.2, 1321.6, 1308.1, 1294.7, 1280.9, 1267.2, 1253.7, 1240.3, 1227.1, 1214.1, 1201.2, 1188.4, 1175.8, 1163.3, 1151.0, 1138.8, 1126.7, 1114.8, 1103.0, 1091.4, 1079.9, 1068.5, 1057.2, 1046.1, 1035.1, 1024.2, 1013.5, 1002.8, 992.34, 981.94, 971.67, 961.51, 951.46, 941.53, 931.71, 921.99, 912.39, 902.89, 893.48, 884.19, 874.99, 865.9, 856.91, 848.03, 839.24, 830.54, 821.95, 813.45, 805.05, 796.73, 788.51, 780.39, 772.35, 764.4, 756.54, 748.76, 741.08, 733.47, 725.96, 718.52, 711.17, 703.89, 698.13, 1953.3, 1951.1, 1926.7, 1902.6, 1878.8, 1855.3, 1832.1, 1809.2, 1786.6, 1764.3, 1759.6, 2558.1, 2535.8, 2502.8, 2470.2, 2438.0, 2406.2, 2374.9, 2344.0, 2313.5, 2283.4, 2253.7, 2224.4, 2195.5, 2167.0, 2138.9, 2110.4, 2082.1, 2054.3, 2026.8, 1999.8, 1973.1, 1946.8, 1920.9, 1895.4, 1870.2, 1845.4, 1821.0, 1796.8, 1772.9, 1749.4, 1726.2, 1703.4, 1680.8, 1658.6, 1653.1, 1937.2, 1929.1, 1902.9, 1877.0, 1851.5, 1826.3, 1801.5, 1777.0, 1752.9, 1729.1, 1705.6, 1683.3, 1661.4, 1639.6, 1618.2, 1597.2, 1576.5, 1556.1, 1536.0, 1516.2, 1496.7, 1477.3, 1458.1, 1439.3, 1420.7, 1402.4, 1384.4, 1366.6, 1349.1, 1331.9, 1314.9, 1298.1, 1295.0, 1378.6, 1374.3, 1356.3, 1338.5, 1321.0, 1303.7, 1286.7, 1269.9, 1253.4, 1237.1, 1221.0, 1205.6, 1190.5, 1175.7, 1161.0, 1148.7, 1146.6, 1187.3, 1183.0, 1168.7, 1154.6, 1140.7, 1127.0, 1113.5, 1100.2, 1086.8, 1073.6, 1060.5, 1047.6, 1034.9, 1022.4, 1010.1, 997.86, 985.83, 973.95, 962.23, 950.67, 939.25, 927.99, 916.87, 905.89, 895.06, 884.36, 873.8, 863.38, 853.08, 842.92, 832.88, 822.97, 813.19, 803.53, 793.98, 784.56, 775.25, 766.06, 756.98, 747.86, 738.85, 729.94, 721.15, 712.45, 703.87, 695.39, 687.01, 678.73, 670.56, 662.48, 654.5, 646.62, 638.8, 630.88, 623.05, 615.31, 607.68, 600.14, 592.69, 585.35, 578.09, 570.83, 563.64, 556.54, 549.53, 542.62, 535.79, 529.06, 522.41, 515.85, 509.37, 502.98, 496.68, 490.45, 484.31, 478.25, 472.27, 466.36, 460.54, 454.79, 449.11, 443.51, 437.99, 432.53, 427.15, 421.84, 416.6, 411.43, 406.33, 401.29, 396.32, 391.42, 386.58, 381.8, 377.06, 372.34, 367.69, 363.1, 358.57, 354.1, 349.69, 345.34, 341.05, 336.81, 332.63, 328.5, 324.43, 320.41, 316.45, 312.53, 308.67, 304.86, 301.1, 297.39, 293.73, 290.11, 286.55, 283.03, 279.55, 276.13, 272.74, 269.41, 266.11, 262.86, 259.65, 256.47, 253.32, 250.21, 247.14, 244.11, 241.12, 238.17, 235.26, 232.38, 229.55, 226.75, 223.98, 221.25, 218.56, 215.9, 213.27, 210.67, 208.1, 205.57, 203.07, 200.6, 174.36, 145.84, 122.15, 102.48, 86.059, 72.153, 60.558, 59.851, 57.521, 56.922, 144.11, 138.51, 130.5, 108.78, 99.363, 95.313, 94.271, 130.65, 127.75, 125.59, 124.96, 119.99, 118.71, 135.3, 130.32, 124.38, 104.8, 88.203, 74.233, 62.451, 52.559, 44.26, 37.038, 30.899, 25.804, 21.521, 17.912, 14.919, 12.436, 10.375, 8.6609, 7.2357, 6.0496, 5.0616, 4.2312, 3.5372, 2.9586, 2.4686, 2.053, 1.6981, 1.4977, 1.4347, 1.4185, 6.6541, 6.6076, 6.3622, 5.6085, 4.7218, 3.969, 3.3347, 2.8013, 2.3495, 1.9699, 1.6515, 1.3847, 1.161, 0.9736, 0.81633, 0.68455, 0.57411, 0.48153, 0.40392, 0.33886, 0.2843, 0.23855, 0.20018, 0.16799, 0.14099, 0.11834, 0.0], - 'energies (keV)': [0.005421745, 0.005502667, 0.007408549, 0.007521945, 0.007552184, 0.007597543, 0.007710939, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01644213, 0.01669379, 0.0167609, 0.01686157, 0.01705382, 0.01711323, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.030772, 0.03109002, 0.031243, 0.0313686, 0.031557, 0.032028, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09634786, 0.09665893, 0.09782257, 0.09821583, 0.09880571, 0.1002804, 0.1033284, 0.1104581, 0.1180797, 0.1231813, 0.1250667, 0.1255695, 0.1262272, 0.1263237, 0.1282091, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1659746, 0.1674882, 0.168515, 0.1691924, 0.1702086, 0.1707354, 0.1718369, 0.172749, 0.1744671, 0.1751685, 0.1762144, 0.1762205, 0.1788507, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.463932, 0.471033, 0.4729266, 0.475767, 0.4794098, 0.482868, 0.490196, 0.497699, 0.4996998, 0.5, 0.50056912, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70406938, 0.70538832, 0.70593058, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.84973202, 0.852268, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99395634, 0.99514537, 0.99664365, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6822596, 2.6837406, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.7971803, 2.7988199, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.2982348, 3.305565, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.8485501, 3.8596499, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1406035, 4.1436226, 4.158197, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 13.53752, 13.74473, 13.79999, 13.88287, 14.09008, 14.40688, 15.40095, 15.91941, 16.16308, 16.22806, 16.32552, 16.46362, 16.56919, 16.60051, 16.8546, 16.92236, 17.024, 17.27809, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 91.2429, 92.63948, 93.0119, 93.31374, 93.57053, 94.9671, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Pm': {'mass_absorption_coefficient (cm2/g)': [6580.8, 6918.1, 7277.5, 7661.6, 8072.6, 8512.4, 8982.6, 9481.4, 10006.0, 10554.0, 11060.0, 11124.0, 11192.0, 11227.0, 214750.0, 204070.0, 181600.0, 145040.0, 114960.0, 90865.0, 72251.0, 58252.0, 47927.0, 40418.0, 37455.0, 36277.0, 35981.0, 35715.0, 35180.0, 34709.0, 31348.0, 28632.0, 26709.0, 25343.0, 24365.0, 23648.0, 23105.0, 22671.0, 22301.0, 21962.0, 20889.0, 19608.0, 18337.0, 17078.0, 16444.0, 16351.0, 16258.0, 16166.0, 16074.0, 15982.0, 15890.0, 15798.0, 15707.0, 15615.0, 15524.0, 15433.0, 15342.0, 15251.0, 15160.0, 15070.0, 14979.0, 14889.0, 14799.0, 14709.0, 14620.0, 14530.0, 14441.0, 14352.0, 14263.0, 14175.0, 14086.0, 13998.0, 13910.0, 13822.0, 13735.0, 13647.0, 13560.0, 13473.0, 13387.0, 13300.0, 13214.0, 13128.0, 13111.0, 73387.0, 70092.0, 65987.0, 62165.0, 58607.0, 55294.0, 52209.0, 49335.0, 46658.0, 44164.0, 41841.0, 39675.0, 37656.0, 35774.0, 34019.0, 32382.0, 30855.0, 29430.0, 28100.0, 26859.0, 25702.0, 24641.0, 23669.0, 22775.0, 21953.0, 21196.0, 20495.0, 19847.0, 19247.0, 18689.0, 18169.0, 17685.0, 17232.0, 16809.0, 16412.0, 16039.0, 15689.0, 15358.0, 15047.0, 14752.0, 14473.0, 14208.0, 13957.0, 13718.0, 13490.0, 13273.0, 13066.0, 12867.0, 12677.0, 12495.0, 12320.0, 12151.0, 11989.0, 11833.0, 11683.0, 11537.0, 11397.0, 11261.0, 11129.0, 11001.0, 10878.0, 10757.0, 10641.0, 10527.0, 10416.0, 10309.0, 10204.0, 10101.0, 10002.0, 9904.3, 9809.1, 9716.1, 9625.2, 9536.3, 9449.2, 9364.0, 9280.6, 9198.8, 9118.7, 9040.1, 8963.0, 8887.5, 8813.3, 8740.5, 8669.0, 8598.8, 8529.9, 8462.2, 8395.6, 8330.2, 8266.0, 8202.8, 8140.7, 8079.6, 8019.6, 7960.5, 7902.4, 7845.3, 7789.1, 7733.9, 7679.5, 7626.0, 7573.4, 7521.6, 7470.7, 7420.6, 7371.3, 7322.8, 7275.1, 7228.2, 7182.0, 7136.6, 7092.0, 7048.0, 7004.8, 6962.3, 6920.5, 6879.4, 6839.0, 6799.3, 6760.2, 6721.8, 6684.1, 6647.0, 6610.5, 6574.7, 6539.5, 6504.9, 6470.9, 6437.6, 6404.8, 6372.6, 6341.0, 6309.9, 6279.5, 6249.6, 6248.3, 7506.9, 7484.9, 7454.7, 7425.1, 7396.0, 7367.4, 7339.3, 7311.8, 7284.7, 7258.2, 7232.1, 7206.6, 7181.5, 7156.9, 7132.7, 7109.0, 7107.1, 7477.3, 7462.1, 7439.1, 7416.6, 7394.6, 7372.9, 7351.6, 7330.7, 7310.2, 7290.0, 7270.1, 7250.6, 7231.4, 7212.6, 7194.0, 7175.8, 7157.8, 7140.2, 7122.8, 7105.7, 7088.9, 7072.3, 7056.0, 7039.9, 7024.0, 7008.3, 6992.9, 6977.7, 6962.6, 6947.8, 6933.1, 6918.6, 6904.3, 6890.1, 6876.0, 6862.1, 6848.3, 6834.7, 6821.1, 6807.7, 6794.3, 6781.1, 6767.9, 6754.8, 6741.7, 6728.7, 6715.8, 6702.9, 6690.0, 6677.2, 6664.3, 6651.5, 6638.7, 6633.3, 7086.4, 7084.0, 7069.9, 7055.7, 7041.5, 7027.3, 7013.0, 6998.7, 6984.4, 6970.0, 6955.5, 6940.9, 6926.3, 6911.6, 6896.8, 6881.9, 6867.0, 6851.9, 6836.7, 6821.4, 6806.0, 6790.4, 6774.7, 6758.8, 6742.8, 6726.7, 6710.3, 6693.8, 6677.2, 6660.4, 6643.4, 6626.2, 6608.8, 6591.3, 6573.5, 6555.6, 6537.5, 6519.2, 6500.7, 6482.0, 6463.0, 6443.9, 6424.6, 6405.0, 6385.3, 6365.4, 6345.3, 6324.9, 6304.4, 6283.7, 6262.8, 6241.7, 6220.4, 6198.8, 6177.1, 6155.2, 6133.1, 6110.9, 6088.4, 6065.7, 6042.9, 6019.8, 5996.6, 5973.2, 5949.6, 5925.8, 5901.8, 5877.7, 5853.4, 5829.0, 5804.3, 5779.5, 5754.6, 5729.4, 5704.2, 5678.7, 5653.1, 5627.4, 5601.5, 5575.3, 5549.0, 5522.5, 5495.8, 5469.0, 5441.9, 5414.8, 5387.4, 5360.0, 5332.3, 5304.6, 5276.7, 5248.7, 5220.5, 5192.3, 5163.9, 5135.5, 5106.9, 5078.3, 5049.5, 5020.7, 4991.8, 4962.8, 4933.8, 4904.7, 4875.6, 4846.4, 4817.2, 4787.9, 4758.6, 4729.3, 4699.9, 4670.5, 4641.1, 4611.8, 4582.4, 4553.0, 4523.6, 4494.2, 4464.8, 4435.5, 4406.1, 4376.8, 4347.6, 4318.3, 4289.2, 4260.0, 4230.9, 4201.9, 4172.9, 4143.9, 4115.1, 4086.2, 4057.5, 4028.8, 4000.2, 3971.7, 3943.3, 3914.9, 3886.7, 3858.5, 3830.4, 3802.4, 3774.5, 3746.8, 3719.1, 3691.5, 3664.0, 3636.7, 3609.4, 3582.3, 3555.2, 3528.3, 3501.6, 3474.9, 3448.3, 3421.9, 3395.6, 3369.4, 3343.3, 3317.4, 3291.6, 3266.0, 3240.4, 3215.0, 3189.8, 3164.7, 3139.7, 3114.9, 3090.2, 3065.6, 3041.2, 3016.9, 2992.8, 2968.8, 2945.0, 2921.3, 2897.7, 2874.3, 2851.1, 2827.9, 2805.0, 2782.2, 2759.5, 2737.0, 2714.6, 2692.4, 2670.3, 2648.3, 2626.6, 2604.9, 2583.4, 2562.1, 2540.9, 2519.8, 2498.9, 2478.2, 2457.6, 2437.1, 2416.8, 2396.6, 2376.6, 2356.7, 2337.0, 2317.4, 2298.0, 2278.7, 2259.5, 2240.5, 2221.6, 2202.9, 2184.3, 2165.8, 2147.5, 2129.4, 2111.3, 2093.4, 2075.7, 2058.1, 2040.6, 2023.2, 2006.0, 1989.0, 1972.0, 1954.0, 1933.6, 1913.4, 1893.4, 1873.5, 1854.3, 7497.3, 7496.3, 7402.5, 7309.0, 7216.7, 7125.5, 7061.9, 10677.0, 10645.0, 10509.0, 10375.0, 10242.0, 10111.0, 9982.3, 9854.9, 9729.1, 9604.9, 9482.3, 9361.3, 9241.9, 9124.0, 9007.6, 8892.8, 8779.4, 8667.4, 8556.9, 8447.9, 8340.2, 8234.0, 8129.0, 8025.5, 7923.3, 7822.4, 7722.8, 7624.5, 7527.4, 7431.6, 7337.0, 7243.7, 7151.5, 7060.5, 6970.7, 6882.1, 6794.6, 6708.2, 6623.0, 6538.8, 6455.7, 6373.7, 6292.7, 6212.8, 6133.9, 6056.0, 5979.2, 5903.2, 5828.2, 5754.4, 5681.9, 5610.3, 5559.5, 6485.4, 6485.1, 6400.4, 6316.6, 6234.1, 6152.6, 6072.3, 5993.0, 5914.8, 5837.7, 5761.5, 5688.5, 5618.6, 5549.8, 5482.2, 5415.6, 5350.0, 5288.2, 5285.5, 5635.2, 5582.9, 5514.4, 5446.9, 5380.5, 5315.0, 5250.6, 5187.0, 5124.4, 5062.8, 5002.4, 4944.4, 4887.2, 4831.0, 4775.6, 4721.1, 4667.3, 4614.4, 4562.3, 4510.9, 4460.3, 4410.4, 4361.2, 4342.9, 4523.5, 4519.4, 4469.8, 4420.8, 4372.5, 4324.9, 4277.9, 4231.4, 4185.6, 4140.4, 4095.7, 4051.7, 4008.3, 3965.5, 3923.2, 3880.7, 3838.1, 3796.0, 3754.4, 3713.3, 3672.7, 3632.6, 3592.8, 3553.6, 3514.8, 3476.4, 3438.4, 3400.9, 3363.8, 3327.1, 3290.8, 3254.7, 3218.6, 3182.9, 3147.5, 3112.6, 3078.0, 3043.8, 3009.9, 2976.5, 2943.4, 2910.6, 2878.2, 2846.2, 2814.5, 2783.1, 2751.2, 2719.6, 2688.3, 2657.5, 2626.8, 2596.0, 2565.6, 2535.6, 2505.8, 2476.3, 2447.1, 2418.3, 2389.9, 2361.7, 2333.9, 2306.5, 2279.3, 2252.5, 2225.9, 2199.4, 2173.2, 2147.3, 2121.6, 2096.3, 2071.3, 2046.6, 2022.2, 1998.1, 1974.3, 1950.8, 1927.5, 1904.6, 1881.9, 1859.5, 1837.4, 1815.6, 1794.0, 1772.7, 1751.7, 1730.9, 1710.4, 1690.1, 1670.1, 1650.3, 1630.8, 1611.5, 1592.5, 1573.7, 1555.2, 1536.8, 1518.7, 1500.9, 1483.2, 1465.8, 1448.6, 1431.6, 1414.6, 1397.8, 1381.2, 1364.9, 1348.7, 1332.7, 1317.0, 1301.4, 1286.1, 1270.9, 1255.9, 1241.1, 1226.5, 1212.1, 1197.8, 1183.8, 1169.8, 1156.0, 1142.3, 1128.8, 1114.1, 1099.7, 1085.4, 1071.4, 1057.5, 1043.8, 1030.3, 1017.0, 1003.9, 990.93, 978.15, 965.55, 953.12, 940.86, 928.76, 916.82, 905.05, 893.43, 881.95, 870.59, 859.38, 848.32, 837.41, 826.65, 816.04, 805.56, 795.23, 785.03, 774.97, 765.05, 755.26, 745.6, 736.07, 726.67, 717.39, 708.24, 699.21, 690.3, 681.51, 672.84, 664.28, 655.84, 647.51, 639.29, 631.18, 623.18, 615.28, 607.49, 599.8, 592.21, 584.73, 577.34, 570.05, 562.86, 555.77, 548.76, 541.85, 519.31, 438.81, 369.95, 311.2, 262.23, 221.39, 186.82, 166.39, 159.98, 158.31, 472.57, 467.56, 446.88, 394.16, 387.37, 377.83, 373.66, 512.03, 490.46, 466.28, 446.33, 441.76, 503.14, 484.56, 435.94, 368.82, 310.67, 259.76, 217.08, 181.56, 152.0, 127.36, 106.5, 88.903, 74.037, 61.614, 51.32, 42.784, 35.699, 29.814, 24.919, 20.845, 17.454, 14.516, 12.019, 9.9491, 8.2058, 6.7684, 5.5857, 4.6133, 3.9399, 3.8132, 3.773, 3.7301, 21.537, 20.734, 18.793, 15.773, 13.221, 11.076, 9.2594, 7.7388, 6.4675, 5.4046, 4.5102, 3.7529, 3.1174, 2.5845, 2.1429, 1.7769, 1.4736, 1.2221, 1.0136, 0.8408, 0.69749, 0.57865, 0.48009, 0.39835, 0.33143, 0.27586, 0.22962, 0.19114, 0.15912, 0.13247, 0.11029, 0.091832, 0.076466, 0.063674, 0.053024, 0.044158, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.020678, 0.02083314, 0.0209945, 0.0210789, 0.0212055, 0.021522, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03675, 0.0373125, 0.0374625, 0.0376875, 0.03797993, 0.03825, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12038579, 0.1204142, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23585864, 0.23614137, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25423287, 0.25456716, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33005637, 0.33074361, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0267933, 1.0269536, 1.0270069, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0513391, 1.0516608, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3559325, 1.3578453, 1.3578674, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4703391, 1.4706431, 1.4724609, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6442772, 1.6487228, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.330114, 6.427004, 6.452841, 6.469004, 6.491597, 6.588486, 6.872544, 6.915365, 6.977736, 7.005787, 7.047864, 7.153056, 7.279342, 7.392525, 7.420472, 7.465039, 7.576458, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.28032, 44.79101, 44.95808, 45.13881, 45.40992, 46.08768, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ho': {'mass_absorption_coefficient (cm2/g)': [7039.2, 7283.1, 7559.6, 7870.1, 8216.2, 8599.5, 9021.5, 9483.1, 9985.1, 10528.0, 10703.0, 10835.0, 10871.0, 321370.0, 302910.0, 295610.0, 227140.0, 174640.0, 134230.0, 103870.0, 81485.0, 65200.0, 53485.0, 45140.0, 39258.0, 35165.0, 32368.0, 30509.0, 29328.0, 29177.0, 29000.0, 28957.0, 29040.0, 28905.0, 28787.0, 28462.0, 28387.0, 28485.0, 28698.0, 27892.0, 27010.0, 26095.0, 25140.0, 24141.0, 23615.0, 23536.0, 23458.0, 23379.0, 23301.0, 23221.0, 23142.0, 23062.0, 22983.0, 22903.0, 22822.0, 22742.0, 22661.0, 22580.0, 22499.0, 22418.0, 22336.0, 22254.0, 22172.0, 22090.0, 22008.0, 21925.0, 21842.0, 21759.0, 21676.0, 21593.0, 21510.0, 21426.0, 21342.0, 21258.0, 21174.0, 21090.0, 21005.0, 20920.0, 20836.0, 20751.0, 20666.0, 20580.0, 20495.0, 20410.0, 20324.0, 20238.0, 20152.0, 20066.0, 19980.0, 19894.0, 19808.0, 19722.0, 19635.0, 19549.0, 19462.0, 19375.0, 19288.0, 19201.0, 19114.0, 19027.0, 18940.0, 18853.0, 18766.0, 18679.0, 18591.0, 18504.0, 18417.0, 18329.0, 18242.0, 18154.0, 18067.0, 17979.0, 17892.0, 17804.0, 17717.0, 17629.0, 17542.0, 17454.0, 17367.0, 17279.0, 17192.0, 17105.0, 17017.0, 16930.0, 16842.0, 16755.0, 16668.0, 16581.0, 16494.0, 16407.0, 16320.0, 16233.0, 16146.0, 16059.0, 15972.0, 15886.0, 15799.0, 15713.0, 15626.0, 15540.0, 15506.0, 38113.0, 37489.0, 36090.0, 34770.0, 33524.0, 32347.0, 31235.0, 30184.0, 29191.0, 28252.0, 27364.0, 26524.0, 25729.0, 24976.0, 24264.0, 23589.0, 22949.0, 22343.0, 21768.0, 21222.0, 20704.0, 20220.0, 19768.0, 19346.0, 18952.0, 18582.0, 18234.0, 17907.0, 17598.0, 17307.0, 17030.0, 16768.0, 16520.0, 16283.0, 16057.0, 15841.0, 15635.0, 15437.0, 15247.0, 15065.0, 14890.0, 14721.0, 14558.0, 14400.0, 14248.0, 14100.0, 13957.0, 13818.0, 13683.0, 13552.0, 13424.0, 13299.0, 13178.0, 13059.0, 12943.0, 12830.0, 12719.0, 12611.0, 12505.0, 12400.0, 12298.0, 12198.0, 12100.0, 12003.0, 11908.0, 11815.0, 11723.0, 11632.0, 11543.0, 11456.0, 11369.0, 11284.0, 11201.0, 11118.0, 11036.0, 10956.0, 10877.0, 10799.0, 10721.0, 10645.0, 10570.0, 10495.0, 10422.0, 10350.0, 10278.0, 10207.0, 10137.0, 10068.0, 9999.8, 9932.3, 9865.6, 9799.6, 9734.4, 9669.9, 9606.2, 9543.1, 9480.8, 9419.2, 9358.3, 9298.0, 9238.4, 9179.5, 9121.2, 9063.6, 9006.5, 8950.2, 8894.4, 8839.3, 8784.8, 8730.8, 8677.5, 8624.8, 8572.6, 8521.0, 8470.0, 8419.6, 8369.7, 8320.4, 8271.6, 8223.3, 8175.6, 8128.5, 8081.8, 8035.7, 7990.1, 7945.0, 7900.4, 7856.3, 7812.7, 7769.6, 7750.9, 8702.5, 8695.1, 8651.6, 8608.6, 8566.1, 8524.0, 8482.4, 8441.3, 8400.5, 8360.3, 8320.4, 8281.0, 8242.1, 8203.5, 8165.4, 8127.6, 8090.3, 8053.4, 8016.8, 7980.7, 7944.9, 7909.5, 7874.4, 7839.6, 7832.9, 8064.0, 8051.8, 8017.7, 7983.9, 7950.5, 7917.3, 7884.5, 7851.9, 7819.7, 7787.7, 7756.0, 7724.5, 7693.3, 7662.4, 7631.7, 7601.3, 7571.0, 7541.0, 7511.3, 7481.7, 7452.4, 7423.2, 7394.3, 7365.5, 7336.9, 7308.5, 7280.3, 7252.2, 7224.3, 7196.5, 7168.9, 7141.4, 7114.0, 7086.8, 7059.7, 7032.6, 7005.7, 6978.9, 6952.2, 6925.6, 6899.0, 6872.5, 6846.1, 6819.8, 6793.5, 6767.3, 6741.2, 6715.0, 6694.3, 6689.0, 6983.6, 6967.1, 6940.3, 6913.5, 6886.7, 6859.9, 6833.1, 6806.4, 6779.7, 6752.9, 6726.2, 6699.5, 6672.7, 6646.0, 6619.2, 6592.5, 6565.7, 6538.9, 6512.1, 6485.2, 6458.3, 6431.4, 6404.4, 6377.4, 6350.3, 6323.2, 6296.0, 6268.8, 6241.5, 6214.2, 6186.8, 6159.3, 6131.8, 6104.2, 6076.6, 6048.9, 6021.2, 5993.4, 5965.5, 5937.6, 5909.6, 5881.5, 5853.4, 5825.3, 5797.1, 5768.8, 5740.5, 5712.1, 5683.7, 5655.2, 5626.7, 5598.1, 5569.5, 5540.8, 5512.1, 5483.3, 5454.5, 5425.7, 5396.8, 5367.9, 5339.0, 5310.0, 5281.0, 5251.9, 5222.9, 5193.8, 5164.7, 5135.6, 5106.4, 5077.3, 5048.1, 5018.9, 4989.7, 4960.5, 4931.3, 4902.1, 4872.9, 4843.7, 4814.5, 4785.4, 4756.2, 4726.9, 4697.7, 4668.4, 4639.1, 4609.9, 4580.6, 4551.3, 4522.1, 4492.8, 4463.6, 4434.4, 4405.2, 4376.1, 4346.9, 4317.9, 4288.8, 4259.8, 4230.9, 4202.0, 4173.2, 4144.4, 4115.7, 4087.1, 4058.5, 4030.1, 4001.6, 3973.1, 3944.7, 3916.4, 3888.2, 3860.1, 3832.1, 3804.1, 3776.3, 3748.6, 3721.0, 3693.5, 3666.1, 3638.8, 3611.6, 3584.6, 3557.6, 3530.8, 3504.1, 3477.6, 3451.1, 3424.8, 3398.6, 3372.6, 3346.7, 3320.9, 3295.2, 3269.7, 3244.4, 3219.1, 3194.0, 3169.1, 3144.3, 3119.6, 3095.1, 3070.8, 3046.5, 3022.5, 2998.5, 2974.8, 2951.1, 2927.6, 2904.3, 2881.1, 2858.1, 2835.2, 2812.5, 2789.9, 2767.5, 2745.2, 2723.1, 2701.2, 2679.4, 2657.7, 2636.2, 2614.8, 2593.6, 2572.6, 2551.7, 2530.9, 2510.3, 2487.7, 2460.8, 2434.3, 2408.1, 2382.2, 2356.6, 2331.2, 2306.2, 2281.4, 2257.0, 2232.8, 2208.8, 2185.2, 2161.8, 2138.7, 2115.8, 2093.2, 2070.9, 2048.8, 2027.0, 2005.4, 1984.0, 1962.9, 1942.1, 1921.4, 1901.0, 1880.9, 1860.9, 1841.2, 1821.7, 1802.4, 1783.4, 1764.5, 1745.9, 1727.5, 1709.3, 1691.3, 1673.5, 1655.9, 1638.5, 1621.2, 1604.1, 1586.9, 1569.9, 1553.1, 1536.4, 1520.0, 1503.8, 1487.7, 1471.8, 1456.1, 1440.6, 1425.3, 1410.1, 1395.1, 1380.3, 1365.6, 1351.1, 1336.8, 1322.6, 1308.6, 1308.4, 5003.5, 4945.1, 4882.8, 4821.4, 4760.6, 4700.6, 4649.5, 6997.1, 6993.2, 6904.1, 6816.1, 6729.3, 6643.6, 6559.0, 6475.5, 6393.0, 6311.7, 6231.3, 6152.0, 6073.8, 5996.5, 5920.3, 5845.0, 5770.7, 5697.3, 5624.9, 5553.5, 5482.9, 5413.3, 5344.5, 5276.7, 5209.7, 5143.6, 5078.3, 5013.9, 4950.3, 4887.5, 4825.5, 4764.4, 4704.0, 4644.4, 4585.5, 4527.4, 4470.1, 4413.5, 4357.6, 4302.4, 4247.8, 4193.9, 4140.7, 4088.2, 4036.3, 3985.1, 3952.0, 4622.4, 4618.4, 4560.6, 4499.6, 4439.4, 4380.0, 4321.4, 4263.6, 4206.6, 4150.3, 4094.8, 4041.6, 3990.9, 3941.0, 3891.9, 3843.6, 3796.1, 3749.4, 3703.4, 3658.1, 3613.6, 3589.7, 3824.7, 3822.7, 3774.9, 3727.8, 3681.4, 3635.8, 3590.8, 3546.5, 3502.8, 3459.7, 3417.3, 3376.4, 3336.7, 3297.6, 3259.2, 3221.3, 3184.0, 3147.3, 3111.1, 3075.4, 3040.2, 3014.5, 3005.6, 3130.7, 3112.2, 3077.2, 3042.6, 3008.6, 2974.9, 2941.7, 2909.0, 2876.6, 2844.7, 2813.2, 2782.2, 2751.6, 2721.0, 2690.4, 2660.2, 2630.4, 2600.8, 2571.7, 2542.9, 2514.4, 2486.2, 2458.4, 2430.9, 2403.7, 2376.9, 2350.3, 2324.0, 2298.0, 2272.3, 2246.9, 2221.8, 2197.0, 2172.2, 2147.6, 2123.3, 2099.2, 2075.4, 2051.9, 2028.7, 2005.7, 1982.9, 1960.4, 1938.2, 1916.2, 1894.5, 1873.0, 1851.7, 1830.7, 1809.6, 1788.7, 1768.1, 1747.7, 1727.5, 1707.5, 1687.5, 1667.8, 1648.3, 1629.1, 1610.1, 1591.3, 1572.7, 1554.4, 1536.3, 1518.4, 1500.7, 1483.3, 1466.0, 1449.0, 1432.1, 1413.9, 1395.9, 1378.0, 1360.3, 1342.9, 1325.8, 1308.8, 1292.1, 1275.6, 1259.4, 1243.4, 1227.6, 1212.0, 1196.6, 1181.4, 1166.4, 1151.6, 1137.1, 1122.7, 1108.5, 1094.5, 1080.7, 1067.1, 1053.7, 1040.4, 1027.3, 1014.5, 1001.7, 989.17, 976.74, 964.23, 951.9, 939.73, 927.72, 915.88, 904.2, 892.67, 881.31, 870.09, 859.02, 848.11, 837.34, 826.71, 816.23, 805.89, 795.69, 785.62, 775.69, 765.89, 756.18, 746.5, 736.94, 727.51, 718.2, 709.03, 699.97, 691.04, 661.9, 557.85, 470.38, 397.16, 335.78, 284.26, 240.32, 202.57, 170.99, 144.59, 122.08, 121.8, 117.19, 116.0, 328.76, 313.6, 288.83, 263.37, 253.18, 250.52, 343.74, 336.17, 329.15, 317.74, 303.86, 300.4, 341.94, 328.8, 322.38, 270.49, 227.19, 190.91, 160.07, 134.15, 112.43, 94.263, 78.985, 66.063, 55.244, 46.116, 38.53, 32.22, 26.967, 22.59, 18.79, 15.6, 12.962, 10.78, 8.9566, 7.4375, 6.1627, 5.0968, 4.2158, 3.4893, 2.9219, 2.8902, 2.7996, 2.7681, 15.069, 14.514, 13.43, 11.282, 9.4613, 7.9308, 6.6437, 5.5572, 4.6344, 3.8653, 3.2243, 2.6899, 2.2408, 1.8618, 1.5471, 1.2857, 1.0685, 0.88815, 0.73828, 0.61375, 0.51027, 0.42427, 0.35279, 0.29337, 0.245, 0.20485, 0.1713, 0.14325, 0.1198, 0.1002, 0.083811, 0.070106, 0.058645, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.019894, 0.0201985, 0.0202797, 0.0204015, 0.020706, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.050176, 0.050944, 0.0511488, 0.051456, 0.052224, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16093012, 0.16106987, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.3062977, 0.30690231, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34311527, 0.34388471, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43505516, 0.43550006, 0.43634484, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3512135, 1.3515865, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3911646, 1.3918353, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7394239, 1.7424222, 1.742976, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9208002, 1.9247997, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1244052, 2.1271389, 2.1321947, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 7.909678, 8.030745, 8.063029, 8.111456, 8.232522, 8.44789, 8.739444, 8.873211, 8.908882, 8.962389, 9.030794, 9.096156, 9.206316, 9.347229, 9.384806, 9.441171, 9.582084, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.50534, 54.71721, 55.33961, 55.56208, 55.89579, 56.73005, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Hf': {'mass_absorption_coefficient (cm2/g)': [47938.0, 46879.0, 42624.0, 42740.0, 42314.0, 41310.0, 39738.0, 37645.0, 35117.0, 33036.0, 32365.0, 32263.0, 32186.0, 39787.0, 38711.0, 35577.0, 31132.0, 27222.0, 23752.0, 20691.0, 18035.0, 15782.0, 13926.0, 13203.0, 12873.0, 12790.0, 68213.0, 66178.0, 65464.0, 55158.0, 45911.0, 39752.0, 38026.0, 37821.0, 37584.0, 58709.0, 56445.0, 50347.0, 42569.0, 36054.0, 30862.0, 26982.0, 24283.0, 22579.0, 21849.0, 21696.0, 21669.0, 21661.0, 21788.0, 21690.0, 21550.0, 21649.0, 21972.0, 22403.0, 22853.0, 23259.0, 23435.0, 23458.0, 23481.0, 23503.0, 23524.0, 23545.0, 23565.0, 23584.0, 23603.0, 23621.0, 23638.0, 23654.0, 23670.0, 23685.0, 23699.0, 23713.0, 23726.0, 23738.0, 23749.0, 23759.0, 23769.0, 23778.0, 23786.0, 23793.0, 23800.0, 23805.0, 23810.0, 23814.0, 23818.0, 23820.0, 23822.0, 23823.0, 23823.0, 23822.0, 23821.0, 23818.0, 23815.0, 23811.0, 23807.0, 23801.0, 23795.0, 23788.0, 23780.0, 23771.0, 23762.0, 23751.0, 23740.0, 23728.0, 23716.0, 23702.0, 23688.0, 23673.0, 23658.0, 23641.0, 23624.0, 23606.0, 23587.0, 23568.0, 23548.0, 23527.0, 23505.0, 23483.0, 23460.0, 23436.0, 23412.0, 23387.0, 23361.0, 23334.0, 23307.0, 23279.0, 23251.0, 23222.0, 23192.0, 23161.0, 23130.0, 23098.0, 23066.0, 23033.0, 22999.0, 22965.0, 22930.0, 22895.0, 22859.0, 22822.0, 22785.0, 22747.0, 22709.0, 22670.0, 22631.0, 22591.0, 22550.0, 22509.0, 22468.0, 22426.0, 22383.0, 22340.0, 22297.0, 22253.0, 22209.0, 22164.0, 22119.0, 22073.0, 22027.0, 21980.0, 21933.0, 21886.0, 21838.0, 21790.0, 21741.0, 21692.0, 21643.0, 21593.0, 21543.0, 21492.0, 21442.0, 21390.0, 21339.0, 21287.0, 21235.0, 21183.0, 21130.0, 21077.0, 21024.0, 20970.0, 20916.0, 20862.0, 20808.0, 20753.0, 20698.0, 20643.0, 20588.0, 20532.0, 20477.0, 20421.0, 20365.0, 20308.0, 20252.0, 20195.0, 20138.0, 20081.0, 20024.0, 19966.0, 19909.0, 19851.0, 19793.0, 19735.0, 19677.0, 19619.0, 19560.0, 19502.0, 19443.0, 19385.0, 19326.0, 19320.0, 21973.0, 21891.0, 21755.0, 21621.0, 21489.0, 21359.0, 21231.0, 21105.0, 20981.0, 20859.0, 20816.0, 22603.0, 22547.0, 22373.0, 22202.0, 22035.0, 21871.0, 21710.0, 21552.0, 21397.0, 21245.0, 21095.0, 20949.0, 20806.0, 20667.0, 20532.0, 20400.0, 20271.0, 20146.0, 20023.0, 19903.0, 19786.0, 19671.0, 19561.0, 19453.0, 19348.0, 19245.0, 19145.0, 19048.0, 18953.0, 18860.0, 18769.0, 18680.0, 18593.0, 18507.0, 18423.0, 18341.0, 18260.0, 18181.0, 18103.0, 18026.0, 17950.0, 17876.0, 17803.0, 17730.0, 17659.0, 17589.0, 17520.0, 17452.0, 17385.0, 17318.0, 17253.0, 17181.0, 17105.0, 17030.0, 16955.0, 16882.0, 16809.0, 16736.0, 16665.0, 16594.0, 16523.0, 16454.0, 16384.0, 16316.0, 16242.0, 16166.0, 16090.0, 16015.0, 15940.0, 15866.0, 15792.0, 15719.0, 15646.0, 15570.0, 15453.0, 15337.0, 15222.0, 15108.0, 14995.0, 14882.0, 14771.0, 14660.0, 14550.0, 14440.0, 14332.0, 14224.0, 14117.0, 14010.0, 13905.0, 13800.0, 13696.0, 13593.0, 13490.0, 13388.0, 13288.0, 13187.0, 13088.0, 12989.0, 12891.0, 12794.0, 12697.0, 12602.0, 12506.0, 12412.0, 12319.0, 12226.0, 12134.0, 12076.0, 12865.0, 12853.0, 12772.0, 12681.0, 12590.0, 12499.0, 12410.0, 12321.0, 12232.0, 12145.0, 12058.0, 11972.0, 11886.0, 11801.0, 11717.0, 11634.0, 11551.0, 11469.0, 11387.0, 11306.0, 11226.0, 11146.0, 11067.0, 10989.0, 10911.0, 10833.0, 10757.0, 10681.0, 10605.0, 10575.0, 10716.0, 10714.0, 10640.0, 10566.0, 10493.0, 10420.0, 10348.0, 10277.0, 10206.0, 10135.0, 10065.0, 9995.9, 9927.0, 9858.6, 9790.7, 9723.4, 9656.5, 9590.0, 9524.1, 9458.7, 9393.7, 9329.2, 9265.1, 9201.5, 9138.3, 9075.6, 9013.3, 8951.4, 8889.9, 8828.9, 8768.3, 8708.1, 8648.2, 8588.8, 8529.8, 8471.1, 8412.9, 8355.0, 8297.5, 8240.3, 8183.5, 8127.1, 8071.0, 8065.6, 8260.3, 8245.3, 8189.2, 8133.4, 8077.9, 8022.8, 7968.0, 7913.5, 7859.3, 7805.5, 7752.0, 7698.8, 7645.8, 7593.2, 7540.9, 7488.8, 7437.1, 7385.6, 7334.4, 7283.5, 7232.9, 7182.5, 7132.4, 7082.6, 7033.0, 6983.7, 6934.6, 6885.8, 6837.3, 6789.0, 6741.0, 6693.2, 6645.6, 6598.3, 6551.2, 6504.4, 6457.8, 6411.4, 6365.3, 6319.4, 6273.7, 6228.3, 6183.1, 6138.1, 6093.4, 6048.9, 6004.6, 5960.5, 5916.7, 5873.1, 5829.7, 5786.5, 5743.5, 5700.8, 5658.3, 5616.0, 5573.9, 5532.0, 5490.4, 5448.9, 5407.7, 5366.7, 5325.9, 5285.4, 5245.0, 5204.7, 5164.6, 5124.8, 5085.1, 5045.7, 5006.4, 4967.4, 4928.6, 4890.0, 4851.6, 4813.4, 4775.4, 4737.7, 4700.1, 4662.8, 4625.6, 4588.7, 4552.0, 4515.5, 4479.2, 4443.1, 4407.3, 4371.6, 4336.2, 4300.9, 4265.9, 4231.1, 4196.5, 4162.1, 4127.9, 4093.9, 4060.1, 4026.5, 3993.1, 3959.9, 3926.9, 3894.1, 3861.5, 3829.1, 3796.9, 3764.9, 3733.1, 3701.5, 3670.1, 3638.9, 3607.9, 3577.1, 3546.5, 3516.1, 3486.0, 3456.1, 3426.4, 3396.9, 3367.7, 3338.7, 3309.9, 3281.4, 3253.0, 3225.0, 3197.1, 3166.4, 3130.0, 3094.1, 3058.6, 3023.5, 2988.9, 2954.7, 2921.0, 2887.7, 2854.8, 2822.3, 2790.2, 2758.5, 2727.3, 2696.4, 2665.9, 2635.8, 2606.0, 2576.6, 2547.6, 2519.0, 2490.7, 2462.7, 2435.1, 2407.9, 2380.9, 2354.3, 2328.1, 2302.1, 2276.5, 2251.2, 2226.2, 2201.5, 2177.1, 2153.0, 2129.2, 2105.7, 2082.5, 2059.5, 2036.9, 2014.5, 1992.3, 1970.5, 1948.9, 1927.6, 1906.5, 1885.6, 1865.1, 1844.7, 1824.7, 1804.8, 1785.2, 1765.8, 1746.7, 1727.8, 1709.1, 1690.6, 1672.4, 1654.4, 1636.6, 1619.0, 1601.6, 1584.4, 1567.4, 1550.6, 1534.0, 1517.6, 1501.5, 1485.4, 1469.6, 1454.0, 1438.6, 1423.3, 1408.2, 1393.3, 1378.6, 1364.0, 1349.6, 1335.4, 1321.3, 1307.4, 1293.7, 1280.1, 1266.7, 1253.4, 1240.0, 1226.8, 1213.8, 1200.9, 1188.1, 1175.5, 1163.1, 1150.8, 1138.6, 1126.6, 1114.7, 1103.0, 1091.4, 1079.9, 1068.6, 1057.4, 1046.3, 1041.3, 3693.8, 3672.2, 3627.1, 3582.7, 3538.8, 3495.5, 3452.7, 3413.8, 5094.3, 5091.4, 5031.3, 4969.1, 4907.7, 4847.0, 4787.0, 4727.7, 4669.2, 4611.5, 4554.4, 4498.1, 4442.4, 4387.5, 4333.3, 4279.7, 4226.9, 4174.6, 4123.1, 4072.2, 4021.9, 3972.3, 3923.3, 3874.9, 3827.1, 3779.9, 3733.3, 3687.3, 3641.8, 3597.0, 3552.7, 3508.9, 3465.7, 3423.1, 3381.0, 3339.4, 3298.3, 3257.7, 3217.7, 3178.1, 3139.1, 3100.5, 3065.2, 3062.4, 3578.1, 3549.4, 3504.1, 3459.4, 3415.3, 3371.7, 3328.2, 3285.2, 3242.8, 3200.9, 3160.0, 3121.4, 3083.2, 3045.4, 3008.2, 2971.6, 2935.4, 2899.9, 2864.8, 2830.3, 2796.2, 2762.7, 2729.6, 2696.9, 2695.4, 2867.5, 2850.7, 2815.8, 2781.3, 2747.3, 2713.8, 2680.8, 2648.1, 2615.9, 2584.2, 2552.9, 2523.1, 2493.8, 2464.9, 2436.4, 2408.3, 2380.6, 2353.3, 2326.3, 2301.9, 2299.7, 2385.6, 2377.9, 2351.1, 2324.6, 2298.5, 2272.7, 2247.2, 2222.1, 2197.2, 2172.6, 2148.3, 2124.3, 2100.4, 2076.8, 2053.5, 2030.4, 2007.6, 1985.1, 1962.8, 1940.8, 1919.1, 1897.5, 1876.3, 1855.2, 1834.4, 1813.9, 1793.5, 1773.4, 1753.5, 1733.7, 1712.7, 1692.0, 1671.5, 1651.2, 1631.2, 1611.3, 1591.7, 1572.3, 1553.2, 1534.3, 1515.6, 1497.2, 1478.9, 1461.0, 1443.2, 1425.7, 1408.4, 1391.3, 1374.4, 1357.8, 1341.0, 1324.3, 1307.7, 1291.4, 1275.2, 1259.3, 1243.6, 1227.7, 1212.1, 1196.6, 1181.4, 1166.4, 1151.6, 1137.0, 1122.6, 1108.4, 1094.4, 1080.5, 1066.9, 1053.4, 1040.1, 1027.0, 1014.1, 1001.4, 988.79, 976.38, 964.14, 952.06, 940.15, 928.39, 916.79, 905.35, 894.06, 882.92, 871.93, 861.08, 850.38, 815.35, 688.51, 580.67, 489.56, 412.86, 348.37, 294.37, 249.08, 211.05, 178.76, 150.98, 127.69, 107.76, 97.317, 93.298, 92.265, 254.52, 250.63, 242.51, 203.45, 192.26, 184.15, 182.08, 252.22, 241.4, 236.41, 235.58, 225.32, 222.69, 253.98, 244.4, 228.96, 193.28, 162.9, 137.08, 115.01, 96.414, 80.819, 67.775, 56.791, 47.607, 39.878, 33.39, 27.981, 23.278, 19.329, 16.064, 13.362, 11.122, 9.2652, 7.7215, 6.4274, 5.349, 4.4377, 3.6785, 3.0496, 2.5298, 2.3661, 2.2679, 2.2427, 11.637, 11.21, 11.132, 9.3777, 7.8727, 6.6035, 5.5254, 4.6153, 3.8556, 3.2214, 2.6919, 2.2497, 1.8804, 1.566, 1.3041, 1.086, 0.90456, 0.75347, 0.62768, 0.52293, 0.4357, 0.36305, 0.30254, 0.25214, 0.21068, 0.17684, 0.14845, 0.12463, 0.10464, 0.087857, 0.073773, 0.0], - 'energies (keV)': [0.005025, 0.0051, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.016758, 0.0170145, 0.01705382, 0.0170829, 0.0171855, 0.017442, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.029988, 0.030447, 0.0305694, 0.030753, 0.03109002, 0.031212, 0.03323523, 0.03552846, 0.037338, 0.0379095, 0.03797993, 0.0380619, 0.0382905, 0.038862, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.063602, 0.0645755, 0.06477028, 0.0648351, 0.0652245, 0.066198, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.21354015, 0.21385984, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22361559, 0.22398441, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.3799207, 0.38063135, 0.38087931, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.4363751, 0.43762492, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53724443, 0.53895558, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.6614142, 1.6619858, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.7158508, 1.716545, 1.7169493, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.1052605, 2.106026, 2.1099394, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3625852, 2.3682149, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.5957502, 2.596799, 2.6060497, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.369486, 9.512897, 9.55114, 9.608504, 9.653919, 9.751914, 10.32004, 10.52461, 10.6857, 10.72866, 10.7931, 10.95419, 11.03212, 11.04529, 11.21435, 11.25943, 11.32705, 11.49611, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 64.04378, 65.02405, 65.28545, 65.67755, 66.65782, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Hg': {'mass_absorption_coefficient (cm2/g)': [77863.0, 75086.0, 55092.0, 54292.0, 54117.0, 75150.0, 74100.0, 81377.0, 88928.0, 98272.0, 108890.0, 120060.0, 130960.0, 140690.0, 148390.0, 153350.0, 155100.0, 153450.0, 148520.0, 140700.0, 130570.0, 118750.0, 105890.0, 92780.0, 80058.0, 68179.0, 57425.0, 47935.0, 39731.0, 32760.0, 26915.0, 22068.0, 18303.0, 18082.0, 17492.0, 17284.0, 28308.0, 27044.0, 24597.0, 20044.0, 16337.0, 13341.0, 11030.0, 10935.0, 10549.0, 10426.0, 14201.0, 13676.0, 12692.0, 10767.0, 9094.5, 9062.3, 8732.9, 8639.3, 9499.3, 9222.2, 9149.2, 8872.4, 8782.0, 9307.4, 9173.6, 8980.1, 7814.8, 6784.6, 6763.8, 6591.2, 6543.0, 6660.8, 6511.1, 6265.1, 5881.2, 5759.0, 5889.1, 6267.5, 6888.1, 7735.5, 8778.5, 9965.9, 11226.0, 12473.0, 13614.0, 14562.0, 15245.0, 15617.0, 15662.0, 15591.0, 15528.0, 15509.0, 15830.0, 15763.0, 15713.0, 15623.0, 15598.0, 15801.0, 15707.0, 15587.0, 14983.0, 14181.0, 13241.0, 12604.0, 12527.0, 12450.0, 12373.0, 12296.0, 12218.0, 12140.0, 12062.0, 11984.0, 11906.0, 11828.0, 11749.0, 11671.0, 11593.0, 11514.0, 11436.0, 11358.0, 11279.0, 11201.0, 11123.0, 11045.0, 10967.0, 10889.0, 10811.0, 10733.0, 10656.0, 10578.0, 10550.0, 11159.0, 11149.0, 11069.0, 10990.0, 10911.0, 10832.0, 10753.0, 10675.0, 10596.0, 10518.0, 10441.0, 10363.0, 10286.0, 10209.0, 10132.0, 10056.0, 9980.0, 9904.2, 9828.8, 9753.7, 9678.9, 9604.5, 9530.3, 9456.5, 9383.1, 9310.0, 9237.2, 9164.8, 9092.7, 9021.1, 8949.7, 8878.8, 8808.2, 8737.9, 8668.1, 8636.7, 8718.6, 8717.4, 8649.5, 8580.7, 8512.4, 8444.4, 8376.9, 8309.7, 8242.9, 8176.5, 8110.5, 8044.9, 7979.7, 7914.9, 7850.5, 7786.5, 7723.0, 7659.8, 7597.0, 7534.6, 7472.6, 7411.1, 7349.9, 7289.2, 7228.8, 7168.8, 7109.3, 7050.1, 6991.4, 6933.0, 6875.1, 6817.5, 6760.4, 6703.6, 6647.3, 6647.2, 6754.8, 6733.4, 6677.3, 6621.6, 6566.3, 6511.4, 6456.9, 6402.8, 6349.1, 6295.8, 6242.9, 6190.3, 6138.2, 6086.4, 6035.0, 5984.0, 5933.4, 5883.1, 5833.3, 5783.8, 5734.7, 5685.9, 5637.5, 5589.5, 5541.9, 5494.6, 5447.7, 5401.1, 5354.9, 5309.0, 5263.6, 5218.4, 5173.6, 5129.2, 5085.1, 5041.3, 4997.9, 4954.9, 4912.1, 4869.6, 4827.4, 4785.5, 4744.0, 4702.8, 4662.0, 4621.3, 4574.5, 4528.2, 4482.4, 4437.1, 4392.3, 4347.9, 4303.9, 4260.4, 4217.4, 4174.8, 4132.6, 4090.9, 4049.6, 4008.7, 3968.2, 3928.2, 3888.5, 3849.2, 3810.3, 3771.8, 3733.7, 3696.0, 3658.7, 3621.8, 3585.3, 3549.2, 3513.4, 3477.8, 3442.6, 3407.7, 3373.2, 3339.1, 3305.3, 3271.8, 3238.7, 3206.0, 3173.6, 3141.5, 3109.8, 3078.4, 3047.3, 3016.6, 2986.1, 2956.0, 2926.2, 2896.7, 2867.5, 2838.7, 2810.1, 2781.8, 2753.8, 2726.2, 2698.8, 2671.7, 2644.9, 2618.3, 2592.1, 2566.1, 2540.4, 2514.9, 2489.7, 2464.7, 2440.0, 2415.5, 2391.3, 2367.4, 2343.7, 2320.2, 2297.0, 2274.1, 2251.4, 2228.9, 2206.6, 2183.4, 2160.5, 2137.8, 2115.4, 2093.2, 2071.3, 2049.7, 2028.2, 2007.0, 1986.1, 1964.6, 1943.4, 1922.5, 1901.7, 1881.2, 1861.0, 1841.0, 1821.2, 1801.6, 1782.3, 1763.2, 1744.3, 1725.6, 1707.2, 1688.9, 1670.9, 1653.0, 1635.4, 1618.0, 1600.8, 1583.7, 1566.9, 1550.2, 1533.8, 1517.5, 1501.4, 1485.5, 1469.8, 1454.3, 1438.9, 1423.7, 1408.7, 1393.8, 1379.2, 1364.6, 1350.3, 1336.1, 1322.1, 1308.2, 1294.5, 1280.6, 1266.7, 1253.0, 1239.5, 1226.1, 1212.9, 1199.8, 1186.8, 1173.7, 1160.8, 1148.0, 1135.4, 1123.0, 1110.7, 1098.5, 1086.5, 1074.7, 1062.9, 1051.4, 1040.0, 1028.7, 1017.5, 1006.5, 995.63, 984.88, 974.26, 963.77, 953.41, 943.17, 933.06, 923.06, 913.19, 903.43, 893.79, 884.27, 874.86, 865.56, 856.38, 847.3, 838.33, 829.46, 820.7, 811.96, 807.81, 2378.7, 2366.5, 2337.3, 2308.6, 2280.2, 2252.2, 2224.6, 2197.3, 2170.3, 2165.4, 3163.1, 3135.5, 3096.5, 3058.1, 3020.1, 2982.7, 2945.7, 2909.1, 2873.1, 2837.5, 2802.3, 2767.6, 2733.3, 2699.5, 2666.1, 2633.1, 2600.5, 2568.4, 2536.7, 2505.3, 2474.4, 2443.8, 2413.7, 2383.9, 2354.5, 2325.4, 2296.8, 2268.4, 2240.4, 2212.7, 2185.4, 2158.5, 2131.8, 2105.6, 2079.6, 2054.0, 2039.7, 2362.7, 2361.3, 2333.5, 2306.0, 2278.8, 2252.0, 2225.4, 2199.2, 2173.3, 2147.7, 2122.4, 2097.3, 2071.1, 2044.8, 2018.9, 1993.4, 1968.1, 1943.2, 1918.6, 1894.1, 1869.9, 1846.0, 1822.5, 1799.3, 1776.3, 1753.6, 1731.3, 1709.2, 1687.4, 1671.2, 1665.8, 1772.4, 1757.3, 1734.6, 1712.1, 1690.0, 1668.2, 1646.6, 1625.4, 1604.4, 1583.7, 1563.4, 1543.7, 1524.3, 1505.2, 1486.4, 1467.8, 1449.5, 1445.3, 1495.1, 1493.8, 1476.9, 1458.9, 1441.2, 1423.7, 1406.4, 1389.4, 1372.6, 1355.9, 1339.3, 1323.0, 1306.9, 1291.0, 1275.4, 1259.9, 1244.6, 1229.6, 1214.8, 1200.1, 1185.7, 1171.4, 1157.3, 1143.4, 1129.7, 1116.2, 1102.8, 1089.6, 1076.6, 1063.8, 1051.1, 1038.6, 1026.2, 1014.0, 1002.0, 990.1, 978.36, 966.71, 955.1, 943.64, 932.31, 921.13, 910.09, 899.18, 888.41, 877.77, 867.26, 856.88, 846.64, 836.51, 826.52, 816.64, 806.71, 796.73, 786.86, 777.1, 767.47, 757.97, 748.58, 739.32, 730.02, 720.79, 711.68, 702.7, 693.83, 685.08, 676.45, 667.93, 659.53, 651.23, 643.05, 634.97, 627.01, 619.14, 611.38, 603.73, 596.17, 588.71, 581.36, 574.1, 566.93, 559.86, 552.88, 546.0, 539.21, 532.5, 525.89, 519.36, 512.91, 506.56, 500.28, 494.09, 487.98, 481.95, 476.0, 470.09, 464.16, 458.32, 452.55, 446.85, 441.23, 435.69, 430.22, 424.82, 419.5, 414.24, 409.06, 403.94, 398.89, 393.91, 388.99, 384.14, 379.36, 374.64, 369.98, 365.38, 360.84, 356.36, 351.94, 347.58, 343.28, 339.04, 334.85, 330.71, 326.59, 322.53, 318.52, 314.56, 310.65, 306.79, 302.96, 299.19, 295.46, 291.78, 288.14, 284.56, 281.02, 277.53, 274.09, 270.69, 267.32, 263.98, 260.68, 257.43, 254.22, 251.06, 247.93, 244.85, 241.8, 238.8, 235.84, 232.91, 230.02, 227.18, 224.36, 221.59, 218.85, 216.15, 213.48, 210.85, 208.25, 205.69, 203.15, 200.66, 198.19, 195.76, 193.36, 190.98, 188.64, 186.34, 184.06, 181.81, 179.58, 177.39, 175.23, 173.09, 170.98, 148.63, 124.47, 104.23, 87.126, 72.945, 69.09, 66.38, 65.683, 170.41, 163.57, 160.81, 133.78, 122.3, 117.3, 116.02, 160.24, 156.44, 153.93, 152.51, 146.36, 144.77, 165.07, 158.96, 152.1, 128.11, 107.77, 90.639, 76.151, 63.986, 53.791, 45.246, 38.026, 31.759, 26.439, 21.974, 18.277, 15.215, 12.675, 10.567, 8.8175, 7.3631, 6.153, 5.1401, 4.2916, 3.5844, 2.9855, 2.4844, 2.0683, 1.7309, 1.7178, 1.6572, 1.6383, 7.8704, 7.5771, 7.0257, 5.9118, 4.9665, 4.1685, 3.4978, 2.9314, 2.454, 2.0541, 1.7194, 1.4393, 1.2049, 1.008, 0.84311, 0.70527, 0.59002, 0.49366, 0.41307, 0.34567, 0.28929, 0.24213, 0.20268, 0.16966, 0.14204, 0.11892, 0.099576, 0.0], - 'energies (keV)': [0.006432, 0.006528, 0.007559334, 0.007675038, 0.007705893, 0.007752174, 0.007867878, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.056448, 0.05667876, 0.057312, 0.0575424, 0.057888, 0.058752, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07889, 0.07912411, 0.0800975, 0.0804195, 0.0809025, 0.08211, 0.08458368, 0.09041995, 0.09653, 0.09665893, 0.0980075, 0.0984015, 0.0989925, 0.100156, 0.10047, 0.101689, 0.1020978, 0.102711, 0.1033284, 0.104244, 0.1104581, 0.117894, 0.1180797, 0.1196985, 0.1201797, 0.1209015, 0.122706, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.352604, 0.358001, 0.3594402, 0.361599, 0.3671099, 0.370734, 0.3764085, 0.3779217, 0.3801915, 0.385866, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57028052, 0.57171944, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6759456, 0.6777972, 0.67785446, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.79906754, 0.80153246, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.2942849, 2.295515, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3842276, 2.3855726, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8443469, 2.8498532, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2736807, 3.2777459, 3.2833195, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.554085, 3.5677822, 3.5691149, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.03822, 12.22248, 12.27162, 12.34532, 12.52958, 12.60708, 13.47697, 13.92453, 14.13766, 14.19449, 14.27974, 14.40688, 14.49287, 14.54251, 14.7651, 14.82446, 14.9135, 15.13609, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.44026, 81.6564, 82.68679, 83.0192, 83.51781, 84.76435, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Mg': {'mass_absorption_coefficient (cm2/g)': [57335.0, 60975.0, 63888.0, 84293.0, 107110.0, 127540.0, 143650.0, 154280.0, 159140.0, 158620.0, 156480.0, 155200.0, 154830.0, 165880.0, 165270.0, 164410.0, 157200.0, 146810.0, 134920.0, 122370.0, 109790.0, 97637.0, 86210.0, 75677.0, 66114.0, 57534.0, 49905.0, 43171.0, 37254.0, 32069.0, 27544.0, 23612.0, 20205.0, 17262.0, 14725.0, 12543.0, 10670.0, 9063.3, 7688.2, 6513.5, 5511.8, 4659.1, 3934.6, 3320.0, 2799.3, 2359.0, 1987.0, 1673.1, 1408.6, 1185.8, 998.26, 838.99, 695.61, 577.37, 479.85, 421.47, 404.25, 399.32, 5226.5, 5191.5, 4966.1, 4264.5, 3536.5, 2898.1, 2414.9, 2022.0, 1699.3, 1430.5, 1204.2, 1012.6, 846.87, 706.29, 586.7, 485.54, 401.84, 332.58, 275.27, 227.85, 188.6, 156.12, 129.24, 106.97, 87.889, 72.21, 59.33, 48.749, 40.055, 32.913, 26.991, 21.977, 17.894, 14.571, 11.864, 9.6609, 7.8668, 6.406, 5.2165, 4.2479, 3.4592, 2.817, 2.294, 1.8682, 1.5214, 1.239, 1.009, 0.81823, 0.66071, 0.53352, 0.43082, 0.34789, 0.28092, 0.22682, 0.18311, 0.14783, 0.11934, 0.096344, 0.07778, 0.062793, 0.050693, 0.040926, 0.03304, 0.026674, 0.021534, 0.017385, 0.014036, 0.011331, 0.0091482, 0.0073856, 0.0059627, 0.0048139, 0.0038864, 0.0031376, 0.0025331, 0.0020451, 0.0016511, 0.001333, 0.0010762, 0.00086886, 0.00070147, 0.00056633, 0.00045722, 0.00036914, 0.00029802, 0.00024061, 0.00019426, 0.00015683, 0.00012662, 0.0], - 'energies (keV)': [0.051657, 0.052428, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.087612, 0.088953, 0.0893106, 0.089847, 0.09041995, 0.091188, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.2789, 1.298475, 1.304285, 1.311525, 1.3311, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'K': {'mass_absorption_coefficient (cm2/g)': [2555100.0, 2265800.0, 2191700.0, 1275600.0, 758240.0, 472940.0, 308750.0, 210380.0, 149200.0, 109810.0, 83640.0, 65743.0, 62575.0, 61757.0, 62832.0, 60155.0, 55782.0, 47388.0, 41425.0, 37087.0, 33856.0, 31378.0, 29412.0, 27790.0, 26399.0, 25161.0, 24026.0, 22955.0, 21916.0, 20893.0, 19878.0, 18864.0, 17850.0, 16836.0, 15826.0, 14823.0, 13831.0, 12855.0, 11900.0, 10972.0, 10078.0, 9221.2, 8406.6, 7637.3, 6915.8, 6243.2, 5620.2, 5046.3, 4857.7, 4785.0, 4737.6, 4706.3, 39275.0, 39210.0, 38965.0, 55490.0, 54870.0, 54494.0, 53882.0, 47733.0, 41700.0, 36216.0, 35702.0, 34548.0, 34248.0, 38186.0, 37028.0, 35506.0, 30817.0, 26662.0, 22996.0, 19779.0, 16968.0, 14523.0, 12403.0, 10570.0, 8990.9, 7634.7, 6472.8, 5479.8, 4632.9, 3912.1, 3293.3, 2769.4, 2317.8, 1933.3, 1613.9, 1348.5, 1124.1, 937.26, 782.21, 653.43, 546.36, 457.28, 383.09, 321.25, 269.65, 226.55, 188.58, 155.62, 129.91, 128.54, 124.4, 122.98, 1195.0, 1148.8, 1059.5, 890.25, 749.0, 630.45, 528.56, 442.46, 370.12, 309.4, 258.57, 216.07, 180.13, 149.66, 124.35, 103.17, 85.137, 70.262, 57.99, 47.864, 39.508, 32.613, 26.922, 22.226, 18.252, 14.967, 12.273, 10.065, 8.2542, 6.7694, 5.552, 4.5536, 3.7114, 3.0189, 2.4556, 1.9975, 1.6249, 1.3218, 1.0753, 0.87475, 0.71163, 0.57893, 0.47099, 0.38317, 0.31174, 0.25362, 0.20635, 0.16788, 0.13624, 0.11049, 0.089608, 0.072673, 0.05894, 0.047802, 0.038769, 0.031443, 0.025502, 0.020683, 0.016775, 0.013606, 0.011035, 0.0089503, 0.0072593, 0.0058879, 0.0047756, 0.0038734, 0.0031417, 0.0025482, 0.0020668, 0.0016764, 0.0013597, 0.0011029, 0.00089458, 0.0007256, 0.0], - 'energies (keV)': [0.017889, 0.018156, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.0337305, 0.0338661, 0.0340695, 0.034578, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.287728, 0.290374, 0.292132, 0.2933064, 0.2948185, 0.295068, 0.2960037, 0.2977815, 0.299472, 0.3005128, 0.302226, 0.3212482, 0.3434143, 0.3671099, 0.369558, 0.3752145, 0.3767229, 0.3789855, 0.384642, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.535252, 3.548445, 3.589363, 3.603793, 3.625437, 3.679548, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Mn': {'mass_absorption_coefficient (cm2/g)': [9237.2, 9298.5, 9337.4, 45441.0, 45514.0, 46220.0, 64199.0, 67239.0, 70201.0, 73081.0, 75867.0, 78543.0, 81089.0, 83479.0, 85683.0, 87669.0, 89401.0, 90842.0, 91957.0, 92709.0, 93068.0, 93005.0, 92415.0, 91232.0, 89479.0, 87191.0, 84415.0, 81205.0, 77628.0, 76138.0, 75254.0, 75018.0, 110990.0, 106940.0, 92074.0, 81167.0, 73253.0, 66892.0, 61443.0, 56611.0, 52253.0, 49929.0, 49044.0, 48814.0, 52172.0, 51996.0, 51343.0, 48412.0, 45145.0, 42140.0, 39371.0, 36811.0, 34431.0, 32191.0, 29528.0, 27009.0, 24689.0, 22540.0, 20540.0, 18670.0, 16908.0, 15250.0, 13698.0, 12253.0, 10918.0, 9691.4, 8573.2, 7560.2, 6647.8, 5830.7, 5102.7, 4457.1, 3886.9, 3385.1, 2944.8, 2559.4, 2222.9, 2212.2, 2142.1, 2133.1, 2124.0, 13730.0, 13492.0, 13360.0, 18937.0, 18828.0, 18157.0, 17838.0, 15151.0, 13345.0, 12874.0, 12860.0, 12734.0, 14286.0, 13805.0, 12542.0, 10746.0, 9196.0, 7860.1, 6686.5, 5654.3, 4769.5, 4022.7, 3384.7, 2847.7, 2395.7, 2015.0, 1695.2, 1426.6, 1201.0, 1011.5, 852.45, 718.84, 603.36, 506.29, 421.86, 347.98, 287.31, 237.46, 196.45, 162.69, 134.86, 111.91, 92.957, 77.292, 64.331, 54.996, 53.597, 52.763, 52.189, 455.4, 436.04, 392.88, 329.79, 278.25, 234.69, 196.7, 164.09, 136.9, 114.23, 95.325, 79.543, 66.107, 54.675, 45.194, 37.359, 30.884, 25.533, 21.111, 17.455, 14.433, 11.935, 9.8703, 8.1245, 6.6465, 5.4337, 4.4423, 3.632, 2.9695, 2.4278, 1.985, 1.623, 1.3271, 1.0852, 0.88734, 0.72561, 0.59337, 0.48524, 0.39683, 0.32399, 0.26353, 0.21436, 0.17436, 0.14183, 0.11537, 0.093852, 0.076346, 0.062106, 0.050522, 0.0411, 0.033435, 0.0272, 0.022128, 0.018002, 0.014646, 0.011915, 0.0096937, 0.0078865, 0.0064163, 0.0052202, 0.0042471, 0.0034554, 0.0028113, 0.0022873, 0.001861, 0.0], - 'energies (keV)': [0.007179501, 0.007225283, 0.007254329, 0.007286658, 0.007297899, 0.007406823, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.047628, 0.048357, 0.0485514, 0.048843, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.082222, 0.0834805, 0.0838161, 0.0843195, 0.08458368, 0.085578, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.627494, 0.6370985, 0.638372, 0.6396597, 0.6435015, 0.648143, 0.6507486, 0.653106, 0.654657, 0.664428, 0.6692609, 0.7154399, 0.75362, 0.7648052, 0.765155, 0.768231, 0.772845, 0.78438, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.40822, 6.469004, 6.506305, 6.532461, 6.571695, 6.66978, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'O': {'mass_absorption_coefficient (cm2/g)': [806800.0, 806010.0, 704530.0, 673130.0, 640970.0, 608780.0, 577060.0, 546130.0, 516170.0, 487270.0, 459430.0, 432620.0, 406790.0, 381870.0, 366620.0, 361190.0, 359760.0, 371670.0, 371520.0, 366830.0, 350980.0, 331410.0, 312450.0, 293700.0, 275090.0, 256670.0, 238490.0, 220640.0, 203220.0, 186350.0, 170120.0, 154620.0, 139940.0, 126140.0, 113260.0, 101320.0, 90338.0, 80288.0, 71150.0, 62884.0, 55445.0, 48768.0, 42799.0, 37485.0, 32772.0, 28527.0, 24487.0, 20989.0, 17970.0, 15369.0, 13134.0, 11216.0, 9573.5, 8167.9, 6966.6, 5940.8, 5065.6, 4319.2, 3683.1, 3141.0, 2679.3, 2286.0, 1951.1, 1664.1, 1407.8, 1190.2, 1139.7, 1096.9, 1085.8, 20202.0, 19562.0, 19160.0, 16577.0, 14271.0, 12192.0, 10362.0, 8777.3, 7420.9, 6268.8, 5279.1, 4433.1, 3699.0, 3084.5, 2570.8, 2141.7, 1783.6, 1484.9, 1235.8, 1028.3, 855.38, 711.45, 591.67, 489.48, 403.88, 333.25, 274.98, 226.9, 186.3, 152.24, 124.4, 101.66, 83.077, 67.89, 55.48, 45.339, 37.052, 30.28, 24.746, 20.223, 16.527, 13.507, 11.038, 9.0211, 7.3617, 5.976, 4.8409, 3.9125, 3.1618, 2.5551, 2.0649, 1.6687, 1.3485, 1.0898, 0.88072, 0.71175, 0.57519, 0.46484, 0.37566, 0.30359, 0.24535, 0.19746, 0.1587, 0.12755, 0.10251, 0.08239, 0.066219, 0.053221, 0.042775, 0.034379, 0.027631, 0.022208, 0.017849, 0.014346, 0.01153, 0.009267, 0.0074482, 0.0059845, 0.0048053, 0.0038584, 0.0030981, 0.0024876, 0.0019974, 0.0016038, 0.0012878, 0.001034, 0.00083029, 0.00066669, 0.00053533, 0.00042985, 0.00034515, 0.00027714, 0.00022254, 0.00017869, 0.00014348, 0.00011521, 9.2511e-05, 7.4284e-05, 5.9648e-05, 4.7896e-05, 3.8459e-05, 3.0882e-05, 2.4798e-05, 0.0], - 'energies (keV)': [0.0071355, 0.007242, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.023226, 0.0235815, 0.0236763, 0.0238073, 0.0238185, 0.024174, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.52136, 0.52934, 0.531468, 0.53466, 0.54264, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'S': {'mass_absorption_coefficient (cm2/g)': [302010.0, 308730.0, 617060.0, 633200.0, 669540.0, 1233400.0, 992700.0, 738180.0, 518810.0, 351340.0, 233000.0, 153380.0, 101320.0, 91697.0, 83627.0, 81627.0, 78762.0, 72101.0, 67804.0, 46314.0, 32566.0, 23830.0, 18410.0, 15237.0, 13592.0, 12951.0, 12906.0, 13153.0, 13469.0, 13709.0, 13800.0, 13733.0, 13521.0, 13195.0, 12794.0, 12351.0, 11894.0, 11454.0, 11038.0, 10645.0, 10274.0, 9919.8, 9577.0, 9238.4, 8897.7, 8549.7, 8191.0, 7820.0, 7437.0, 7043.6, 6483.1, 6076.3, 5944.5, 5909.8, 105830.0, 105090.0, 102800.0, 95814.0, 86606.0, 77717.0, 69225.0, 64072.0, 62285.0, 61818.0, 68112.0, 67994.0, 66233.0, 60411.0, 53330.0, 46840.0, 40946.0, 35641.0, 30900.0, 26694.0, 22986.0, 19737.0, 16904.0, 14443.0, 12315.0, 10480.0, 8904.1, 7553.1, 6398.0, 5411.3, 4570.7, 3856.7, 3251.8, 2740.2, 2308.3, 1913.2, 1586.9, 1317.6, 1095.2, 911.27, 759.06, 632.96, 528.38, 441.55, 369.39, 309.35, 259.35, 217.66, 207.29, 199.22, 197.15, 2150.3, 2078.4, 2040.3, 1731.5, 1455.7, 1223.9, 1027.0, 859.53, 719.43, 602.24, 504.15, 421.83, 352.89, 292.5, 242.47, 201.0, 166.63, 138.15, 114.54, 94.97, 78.743, 65.193, 53.687, 44.214, 36.312, 29.718, 24.322, 19.906, 16.293, 13.335, 10.915, 8.9346, 7.3136, 5.9867, 4.9007, 4.0118, 3.2842, 2.6886, 2.1875, 1.7762, 1.4423, 1.1711, 0.95097, 0.77221, 0.62706, 0.5092, 0.4135, 0.33578, 0.27218, 0.22031, 0.17832, 0.14434, 0.11684, 0.094576, 0.076544, 0.061926, 0.0501, 0.040533, 0.032792, 0.02653, 0.021464, 0.017366, 0.01405, 0.011367, 0.0091967, 0.0074407, 0.00602, 0.0048706, 0.0039407, 0.0031883, 0.0025796, 0.0020871, 0.0016886, 0.0013662, 0.0011054, 0.00089438, 0.00072364, 0.00058549, 0.00047372, 0.00038329, 0.0], - 'energies (keV)': [0.007774563, 0.007805818, 0.0078527, 0.007889576, 0.007969904, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01733447, 0.01759979, 0.01767054, 0.01777667, 0.01804199, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.161504, 0.163976, 0.1646352, 0.1648404, 0.165624, 0.168096, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.224616, 0.228054, 0.2289708, 0.2301188, 0.230346, 0.233784, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.42256, 2.45964, 2.469528, 2.48436, 2.52144, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'W': {'mass_absorption_coefficient (cm2/g)': [47191.0, 46237.0, 60149.0, 66397.0, 72486.0, 77928.0, 82236.0, 84987.0, 85881.0, 84786.0, 81755.0, 77013.0, 70918.0, 63904.0, 56419.0, 48864.0, 41553.0, 34775.0, 28709.0, 24120.0, 23434.0, 23006.0, 22719.0, 25354.0, 24172.0, 22813.0, 21707.0, 21494.0, 21422.0, 53789.0, 53753.0, 51413.0, 50802.0, 51821.0, 49573.0, 46708.0, 38241.0, 30999.0, 25971.0, 25028.0, 24738.0, 24424.0, 36197.0, 34788.0, 31454.0, 26559.0, 22375.0, 18932.0, 16286.0, 14416.0, 13255.0, 13025.0, 12892.0, 12862.0, 13002.0, 12920.0, 12894.0, 12877.0, 13288.0, 14041.0, 14528.0, 14605.0, 14683.0, 14761.0, 14841.0, 14922.0, 15004.0, 15087.0, 15171.0, 15256.0, 15341.0, 15428.0, 15515.0, 15603.0, 15692.0, 15782.0, 15872.0, 15963.0, 16055.0, 16147.0, 16240.0, 16333.0, 16426.0, 16521.0, 16615.0, 16710.0, 16806.0, 16901.0, 16998.0, 17096.0, 17193.0, 17291.0, 17389.0, 17487.0, 17585.0, 17683.0, 17781.0, 17880.0, 17978.0, 18076.0, 18173.0, 18271.0, 18369.0, 18466.0, 18563.0, 18660.0, 18756.0, 18852.0, 18948.0, 19044.0, 19138.0, 19233.0, 19327.0, 19420.0, 19513.0, 19606.0, 19698.0, 19789.0, 19879.0, 19969.0, 20058.0, 20146.0, 20233.0, 20319.0, 20403.0, 20487.0, 20569.0, 20650.0, 20730.0, 20809.0, 20886.0, 20963.0, 21037.0, 21111.0, 21183.0, 21254.0, 21323.0, 21391.0, 21458.0, 21522.0, 21585.0, 21647.0, 21706.0, 21764.0, 21820.0, 21875.0, 21927.0, 21978.0, 22028.0, 22075.0, 22121.0, 22165.0, 22207.0, 22247.0, 22286.0, 22323.0, 22358.0, 22391.0, 22423.0, 22453.0, 22481.0, 22507.0, 22531.0, 22554.0, 22575.0, 22594.0, 22611.0, 22627.0, 22641.0, 22653.0, 22663.0, 22672.0, 22679.0, 22684.0, 22687.0, 22689.0, 22689.0, 22688.0, 22685.0, 22680.0, 22673.0, 22665.0, 22655.0, 22644.0, 22631.0, 22616.0, 22600.0, 22583.0, 22564.0, 22543.0, 22521.0, 22497.0, 22472.0, 22445.0, 22417.0, 22388.0, 22357.0, 22325.0, 22291.0, 22256.0, 22220.0, 22183.0, 22144.0, 22104.0, 22062.0, 22020.0, 21976.0, 21931.0, 21885.0, 21837.0, 21789.0, 21739.0, 21688.0, 21636.0, 21583.0, 21529.0, 21474.0, 21418.0, 21361.0, 21303.0, 21245.0, 21185.0, 21124.0, 21062.0, 21000.0, 20936.0, 20872.0, 20807.0, 20741.0, 20675.0, 20608.0, 20539.0, 20471.0, 20401.0, 20331.0, 20260.0, 20189.0, 20117.0, 20044.0, 19971.0, 19908.0, 20879.0, 20866.0, 20791.0, 20700.0, 20608.0, 20515.0, 20421.0, 20327.0, 20233.0, 20138.0, 20042.0, 19946.0, 19898.0, 20574.0, 20554.0, 20450.0, 20345.0, 20240.0, 20134.0, 20028.0, 19922.0, 19816.0, 19709.0, 19602.0, 19495.0, 19388.0, 19281.0, 19175.0, 19068.0, 18961.0, 18855.0, 18749.0, 18642.0, 18536.0, 18430.0, 18325.0, 18220.0, 18115.0, 18011.0, 17906.0, 17803.0, 17700.0, 17597.0, 17494.0, 17392.0, 17290.0, 17188.0, 17087.0, 16986.0, 16886.0, 16785.0, 16686.0, 16586.0, 16487.0, 16388.0, 16290.0, 16192.0, 16094.0, 15997.0, 15900.0, 15803.0, 15707.0, 15611.0, 15516.0, 15421.0, 15326.0, 15232.0, 15138.0, 15045.0, 14952.0, 14859.0, 14767.0, 14675.0, 14584.0, 14493.0, 14403.0, 14313.0, 14223.0, 14134.0, 14045.0, 13957.0, 13869.0, 13781.0, 13694.0, 13608.0, 13521.0, 13436.0, 13351.0, 13266.0, 13181.0, 13098.0, 13014.0, 12931.0, 12849.0, 12767.0, 12685.0, 12604.0, 12523.0, 12443.0, 12363.0, 12284.0, 12205.0, 12127.0, 12049.0, 11972.0, 11895.0, 11818.0, 11742.0, 11666.0, 11591.0, 11516.0, 11442.0, 11368.0, 11295.0, 11295.0, 12032.0, 11994.0, 11920.0, 11846.0, 11772.0, 11699.0, 11626.0, 11554.0, 11482.0, 11411.0, 11340.0, 11270.0, 11200.0, 11130.0, 11061.0, 10992.0, 10924.0, 10856.0, 10788.0, 10721.0, 10655.0, 10588.0, 10522.0, 10457.0, 10392.0, 10327.0, 10262.0, 10198.0, 10135.0, 10071.0, 10070.0, 10200.0, 10173.0, 10110.0, 10048.0, 9986.0, 9924.4, 9863.1, 9802.2, 9741.6, 9681.4, 9621.5, 9561.9, 9502.6, 9443.7, 9385.1, 9326.8, 9268.8, 9211.1, 9153.7, 9096.6, 9039.8, 8983.4, 8927.2, 8871.3, 8815.6, 8760.3, 8705.3, 8650.5, 8596.0, 8541.8, 8487.8, 8434.1, 8380.7, 8327.5, 8274.6, 8222.0, 8169.6, 8117.5, 8065.6, 8051.7, 8225.3, 8218.5, 8166.4, 8114.6, 8063.0, 8011.7, 7960.6, 7909.8, 7859.2, 7808.8, 7758.7, 7708.8, 7659.1, 7605.5, 7550.0, 7494.9, 7440.0, 7385.5, 7331.3, 7277.5, 7223.9, 7170.6, 7117.7, 7065.0, 7012.7, 6960.6, 6908.8, 6857.2, 6806.0, 6754.9, 6700.4, 6646.3, 6592.5, 6539.1, 6486.0, 6433.3, 6380.9, 6328.8, 6277.1, 6225.6, 6174.2, 6123.1, 6072.3, 6021.9, 5971.9, 5922.1, 5872.7, 5823.7, 5774.9, 5726.5, 5678.4, 5630.7, 5583.2, 5536.1, 5489.4, 5442.9, 5396.8, 5351.0, 5305.5, 5260.3, 5215.4, 5170.9, 5126.7, 5082.8, 5039.2, 4995.9, 4953.0, 4910.3, 4867.9, 4825.8, 4783.9, 4742.4, 4701.1, 4660.2, 4619.6, 4579.2, 4539.2, 4499.5, 4460.0, 4420.9, 4382.1, 4343.5, 4305.2, 4267.3, 4229.6, 4192.2, 4155.1, 4118.3, 4081.8, 4045.5, 4009.6, 3973.9, 3938.5, 3903.4, 3868.6, 3834.0, 3799.7, 3765.7, 3732.0, 3698.6, 3665.4, 3632.5, 3599.8, 3567.4, 3535.3, 3501.0, 3461.9, 3423.3, 3385.1, 3347.4, 3310.2, 3273.4, 3236.4, 3198.9, 3161.9, 3125.4, 3089.3, 3053.7, 3018.6, 2983.9, 2949.7, 2915.8, 2882.5, 2849.5, 2817.0, 2784.8, 2753.1, 2721.8, 2690.9, 2660.3, 2630.2, 2600.4, 2571.0, 2542.0, 2513.3, 2485.0, 2457.1, 2429.5, 2402.2, 2375.3, 2348.7, 2322.5, 2296.6, 2271.0, 2245.7, 2220.7, 2196.1, 2171.7, 2147.7, 2123.9, 2100.4, 2077.3, 2054.4, 2031.8, 2009.4, 1987.4, 1965.6, 1944.0, 1922.8, 1901.8, 1881.0, 1860.5, 1840.3, 1820.3, 1800.5, 1781.0, 1761.7, 1742.7, 1723.8, 1705.2, 1686.9, 1668.7, 1650.8, 1633.0, 1615.5, 1598.2, 1581.1, 1564.3, 1547.6, 1531.1, 1514.8, 1498.7, 1482.7, 1467.0, 1451.5, 1436.1, 1420.9, 1405.9, 1391.1, 1376.4, 1362.0, 1347.6, 1333.5, 1319.5, 1305.7, 1292.1, 1278.6, 1265.2, 1252.0, 1239.0, 1226.1, 1213.4, 1200.8, 1188.4, 1176.1, 1164.0, 1151.9, 1140.1, 1128.3, 1116.7, 1105.3, 1093.9, 1082.5, 1071.2, 1060.0, 1048.9, 1037.9, 1027.1, 1016.4, 1005.8, 995.34, 984.99, 974.77, 964.66, 959.64, 3241.3, 3224.9, 3186.1, 3147.9, 3110.1, 3072.8, 3035.9, 2999.5, 2989.8, 4437.5, 4405.6, 4352.1, 4299.3, 4247.1, 4195.6, 4144.7, 4094.5, 4044.9, 3995.8, 3947.4, 3899.5, 3852.2, 3805.4, 3759.3, 3713.7, 3668.6, 3624.1, 3580.2, 3536.8, 3493.9, 3451.6, 3409.8, 3368.5, 3327.7, 3287.5, 3247.7, 3208.4, 3169.6, 3131.3, 3093.4, 3056.0, 3019.1, 2982.6, 2946.6, 2911.0, 2875.9, 2841.1, 2806.9, 2773.0, 2746.4, 2739.6, 3216.7, 3184.1, 3143.5, 3103.4, 3063.8, 3024.7, 2986.2, 2947.5, 2909.2, 2871.4, 2834.9, 2800.4, 2766.4, 2733.0, 2699.7, 2666.9, 2634.6, 2602.8, 2571.4, 2540.5, 2510.1, 2480.1, 2450.5, 2421.4, 2392.6, 2391.2, 2544.9, 2530.9, 2499.8, 2469.2, 2439.0, 2409.2, 2379.8, 2350.7, 2322.1, 2293.9, 2266.0, 2239.7, 2213.9, 2188.4, 2163.3, 2138.5, 2114.1, 2090.0, 2066.3, 2063.7, 2137.4, 2135.0, 2111.1, 2087.5, 2064.2, 2041.2, 2018.4, 1995.9, 1973.6, 1951.6, 1929.8, 1908.3, 1887.1, 1866.0, 1843.5, 1821.2, 1799.1, 1777.4, 1755.9, 1734.6, 1713.6, 1692.9, 1672.4, 1652.2, 1632.2, 1612.4, 1592.9, 1573.6, 1554.5, 1535.7, 1517.0, 1498.7, 1480.5, 1462.5, 1444.8, 1427.3, 1409.9, 1392.8, 1375.8, 1359.1, 1342.5, 1326.2, 1310.1, 1294.2, 1278.4, 1262.9, 1247.6, 1232.5, 1217.5, 1202.7, 1188.2, 1173.5, 1158.8, 1144.3, 1130.0, 1115.9, 1102.0, 1088.2, 1074.4, 1060.7, 1047.2, 1033.9, 1020.7, 1007.8, 994.97, 982.35, 969.9, 957.62, 945.51, 933.57, 921.78, 883.36, 746.53, 631.46, 532.55, 449.18, 378.84, 319.67, 270.13, 228.6, 193.74, 164.37, 139.04, 117.3, 97.842, 88.837, 85.196, 84.261, 230.35, 225.87, 219.52, 183.58, 170.84, 163.68, 161.85, 222.69, 213.84, 212.92, 209.78, 201.31, 199.13, 227.25, 218.91, 207.51, 175.26, 147.5, 123.99, 104.01, 87.212, 73.15, 61.393, 51.491, 43.203, 36.224, 30.36, 25.257, 20.973, 17.431, 14.499, 12.069, 10.055, 8.3832, 6.9916, 5.8244, 4.8521, 4.0302, 3.3437, 2.7749, 2.3038, 2.1844, 2.0941, 2.0709, 10.536, 10.151, 9.9591, 8.3857, 7.0346, 5.8989, 4.9397, 4.1282, 3.4505, 2.8844, 2.4115, 2.0164, 1.6861, 1.4053, 1.1714, 0.97654, 0.81416, 0.67884, 0.56606, 0.47206, 0.39371, 0.32838, 0.27392, 0.22851, 0.19127, 0.16081, 0.13522, 0.1137, 0.095617, 0.080415, 0.0], - 'energies (keV)': [0.0061305, 0.006222, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.032928, 0.03323523, 0.033432, 0.0335664, 0.033768, 0.034272, 0.034888, 0.035422, 0.03552846, 0.0355644, 0.03577, 0.035778, 0.0363175, 0.0364635, 0.0366825, 0.03723, 0.03797993, 0.04060054, 0.04340198, 0.045864, 0.04639671, 0.046566, 0.0467532, 0.047034, 0.047736, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.075558, 0.0767145, 0.0770229, 0.0774855, 0.078642, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24521055, 0.24540936, 0.24558945, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25859581, 0.25900419, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42477688, 0.42582312, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.4909216, 0.49227841, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59410753, 0.59589253, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.8088491, 1.809551, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8709394, 1.8722607, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.2786505, 2.280976, 2.2833493, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5716555, 2.5781443, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8140455, 2.8251547, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.00266, 10.15577, 10.19659, 10.25783, 10.32004, 10.41094, 11.03212, 11.31312, 11.48628, 11.53246, 11.60172, 11.77488, 11.79334, 11.8578, 12.0393, 12.0877, 12.1603, 12.3418, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 68.1345, 69.17738, 69.45548, 69.87263, 70.9155, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Zn': {'mass_absorption_coefficient (cm2/g)': [52275.0, 52416.0, 55176.0, 56102.0, 57145.0, 58313.0, 59612.0, 61041.0, 62600.0, 64283.0, 66081.0, 67982.0, 69969.0, 72024.0, 74123.0, 76237.0, 78335.0, 80383.0, 82341.0, 84158.0, 85679.0, 86845.0, 87636.0, 88040.0, 88054.0, 87682.0, 86937.0, 85841.0, 84420.0, 82586.0, 80271.0, 77641.0, 74738.0, 71607.0, 71444.0, 70702.0, 70505.0, 80620.0, 79386.0, 77473.0, 72228.0, 67553.0, 63343.0, 59437.0, 55764.0, 52958.0, 52292.0, 52186.0, 51984.0, 53860.0, 53124.0, 51189.0, 47737.0, 43222.0, 39068.0, 35262.0, 31784.0, 28611.0, 25720.0, 23087.0, 20689.0, 18505.0, 16518.0, 14711.0, 13070.0, 11580.0, 10228.0, 9006.2, 7907.7, 6925.0, 6050.1, 5274.9, 4590.5, 3988.8, 3461.4, 3000.8, 2599.5, 2250.7, 1948.0, 1828.2, 1808.5, 1789.1, 1769.8, 1750.8, 1732.0, 1713.4, 1695.0, 1676.8, 1658.8, 1640.9, 1623.3, 1605.9, 1588.6, 1571.5, 1554.7, 1538.0, 1521.4, 1505.1, 1488.9, 1472.9, 1457.1, 1439.2, 1421.1, 1403.3, 1385.7, 1385.3, 7934.7, 7832.8, 7727.0, 7622.6, 7519.6, 7467.3, 10616.0, 10550.0, 10408.0, 10267.0, 10129.0, 9992.1, 9857.2, 9724.1, 9592.9, 9463.4, 9335.7, 9209.7, 9085.5, 8962.9, 8842.0, 8722.7, 8605.1, 8489.0, 8374.6, 8261.6, 8150.3, 8040.4, 7932.0, 7825.1, 7719.6, 7615.6, 7513.0, 7411.8, 7370.2, 8317.7, 8296.4, 8190.2, 8085.4, 7982.0, 7880.0, 7779.3, 7679.9, 7581.9, 7485.1, 7389.6, 7295.6, 7203.0, 7111.6, 7021.4, 6932.5, 6844.7, 6758.1, 6672.6, 6588.3, 6505.1, 6423.0, 6342.5, 6263.8, 6186.2, 6109.6, 6034.0, 5959.8, 5886.7, 5814.6, 5743.4, 5673.1, 5603.8, 5535.4, 5467.8, 5401.2, 5335.3, 5270.4, 5206.3, 5143.0, 5080.5, 5018.8, 4957.9, 4897.8, 4838.5, 4779.9, 4722.0, 4664.9, 4608.5, 4552.9, 4497.9, 4443.6, 4390.0, 4337.0, 4284.2, 4231.8, 4180.1, 4129.0, 4078.6, 4028.8, 3979.6, 3931.0, 3883.0, 3835.4, 3788.1, 3741.5, 3695.4, 3649.9, 3604.9, 3560.5, 3516.6, 3473.3, 3430.6, 3388.3, 3346.6, 3305.4, 3264.7, 3224.5, 3184.8, 3145.6, 3106.9, 3068.6, 3030.8, 2993.5, 2956.7, 2920.3, 2884.3, 2848.8, 2813.8, 2779.1, 2744.9, 2711.1, 2677.8, 2644.8, 2612.3, 2580.1, 2548.4, 2517.0, 2486.0, 2455.4, 2425.2, 2395.4, 2365.9, 2336.8, 2308.0, 2279.6, 2251.6, 2223.9, 2196.5, 2168.8, 2141.4, 2114.3, 2087.6, 2061.0, 2034.6, 2008.5, 1982.8, 1957.4, 1932.3, 1907.5, 1883.1, 1859.0, 1835.2, 1811.7, 1788.5, 1765.5, 1742.9, 1720.6, 1698.6, 1676.9, 1655.4, 1634.2, 1613.3, 1592.7, 1572.3, 1552.2, 1532.3, 1512.8, 1493.4, 1474.3, 1455.4, 1436.6, 1418.0, 1399.7, 1381.6, 1363.7, 1346.1, 1328.7, 1311.6, 1294.6, 1277.9, 1261.4, 1245.1, 1229.1, 1213.2, 1197.5, 1182.0, 1166.7, 1151.7, 1136.8, 1122.1, 1107.6, 1093.3, 1079.2, 1065.3, 1051.6, 1038.0, 1024.6, 1011.4, 998.38, 985.52, 972.83, 960.3, 947.95, 935.75, 923.72, 911.84, 900.12, 888.55, 877.14, 865.88, 854.76, 843.79, 832.97, 822.29, 811.75, 800.94, 790.01, 779.24, 768.61, 758.14, 747.81, 737.62, 727.58, 717.68, 707.91, 698.28, 688.79, 679.43, 670.2, 661.1, 652.12, 643.27, 634.54, 625.94, 617.45, 609.08, 600.83, 592.69, 584.67, 576.76, 568.96, 561.26, 553.68, 546.19, 538.82, 531.54, 524.37, 517.29, 510.31, 503.43, 496.65, 489.96, 483.36, 476.85, 470.43, 464.1, 457.86, 451.7, 445.63, 439.64, 433.73, 427.91, 422.16, 416.5, 410.91, 405.4, 399.97, 394.61, 389.32, 384.11, 378.97, 373.9, 368.9, 363.97, 359.11, 354.31, 349.58, 344.91, 340.31, 335.77, 331.3, 326.88, 322.53, 318.23, 314.0, 309.82, 305.7, 301.64, 297.63, 293.67, 289.78, 285.93, 282.14, 278.3, 274.51, 270.78, 267.1, 263.45, 259.82, 256.25, 252.73, 249.26, 245.84, 242.46, 239.14, 235.86, 232.63, 229.44, 226.3, 223.21, 220.16, 217.15, 214.19, 211.26, 208.38, 205.54, 202.74, 199.98, 197.26, 194.58, 191.93, 189.32, 186.75, 184.22, 181.69, 179.16, 176.66, 174.2, 171.77, 169.38, 167.02, 164.7, 162.4, 160.15, 157.92, 155.72, 153.56, 151.43, 149.33, 147.26, 145.21, 143.2, 141.21, 139.26, 137.33, 135.43, 133.55, 131.71, 129.89, 128.09, 126.32, 124.58, 122.86, 121.16, 119.49, 117.84, 116.22, 114.62, 113.04, 111.48, 109.95, 108.43, 106.94, 105.47, 104.02, 102.59, 101.19, 99.797, 98.427, 97.077, 95.746, 94.434, 93.141, 81.343, 67.719, 56.432, 47.072, 39.148, 34.062, 32.568, 32.135, 255.48, 244.34, 213.41, 178.58, 150.39, 126.87, 106.2, 88.753, 74.058, 61.73, 51.427, 42.84, 35.595, 29.524, 24.487, 20.309, 16.84, 13.894, 11.447, 9.4315, 7.7713, 6.4038, 5.2772, 4.3486, 3.5629, 2.9194, 2.3922, 1.9603, 1.6064, 1.3164, 1.0787, 0.88398, 0.72441, 0.59292, 0.48392, 0.39498, 0.32239, 0.26315, 0.21481, 0.17535, 0.14314, 0.11686, 0.0954, 0.077886, 0.063588, 0.051917, 0.042389, 0.034611, 0.028261, 0.023076, 0.018843, 0.015387, 0.012566, 0.010262, 0.0083802, 0.0068439, 0.0055894, 0.004565, 0.0037285, 0.0], - 'energies (keV)': [0.0081405, 0.008262, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.084868, 0.086167, 0.0865134, 0.087033, 0.088332, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.133182, 0.1349368, 0.1352205, 0.1357641, 0.1365795, 0.138618, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9, 0.9045, 0.9090225, 0.91356761, 0.91813545, 0.92272613, 0.92733976, 0.93197646, 0.93663634, 0.94131952, 0.94602612, 0.95075625, 0.95551003, 0.96028758, 0.96508902, 0.96991446, 0.97476404, 0.97963786, 0.98453605, 0.98945873, 0.99440602, 0.99937805, 1.0043749, 1.0093968, 1.0144438, 1.019516, 1.019629, 1.0197711, 1.0246136, 1.0297367, 1.0348853, 1.0400598, 1.0427279, 1.042872, 1.0452601, 1.0504864, 1.0557388, 1.0610175, 1.0663226, 1.0716542, 1.0770125, 1.0823975, 1.0878095, 1.0932486, 1.0987148, 1.1042084, 1.1097294, 1.1152781, 1.1208545, 1.1264587, 1.132091, 1.1377515, 1.1434402, 1.1491574, 1.1549032, 1.1606777, 1.1664811, 1.1723135, 1.1781751, 1.184066, 1.1899863, 1.1924542, 1.1947459, 1.1959362, 1.2019159, 1.2079255, 1.2139651, 1.220035, 1.2261351, 1.2322658, 1.2384271, 1.2446193, 1.2508424, 1.2570966, 1.2633821, 1.269699, 1.2760475, 1.2824277, 1.2888399, 1.295284, 1.3017605, 1.3082693, 1.3148106, 1.3213847, 1.3279916, 1.3346316, 1.3413047, 1.3480112, 1.3547513, 1.361525, 1.3683327, 1.3751743, 1.3820502, 1.3889605, 1.3959053, 1.4028848, 1.4098992, 1.4169487, 1.4240335, 1.4311536, 1.4383094, 1.4455009, 1.4527284, 1.4599921, 1.467292, 1.4746285, 1.4820016, 1.4894117, 1.4968587, 1.504343, 1.5118647, 1.519424, 1.5270212, 1.5346563, 1.5423295, 1.5500412, 1.5577914, 1.5655804, 1.5734083, 1.5812753, 1.5891817, 1.5971276, 1.6051132, 1.6131388, 1.6212045, 1.6293105, 1.6374571, 1.6456443, 1.6538726, 1.6621419, 1.6704526, 1.6788049, 1.6871989, 1.6956349, 1.7041131, 1.7126337, 1.7211968, 1.7298028, 1.7384518, 1.7471441, 1.7558798, 1.7646592, 1.7734825, 1.7823499, 1.7912617, 1.800218, 1.8092191, 1.8182652, 1.8273565, 1.8364933, 1.8456757, 1.8549041, 1.8641786, 1.8734995, 1.882867, 1.8922814, 1.9017428, 1.9112515, 1.9208077, 1.9304118, 1.9400638, 1.9497642, 1.959513, 1.9693105, 1.9791571, 1.9890529, 1.9989981, 2.0089931, 2.0190381, 2.0291333, 2.039279, 2.0494754, 2.0597227, 2.0700213, 2.0803714, 2.0907733, 2.1012272, 2.1117333, 2.122292, 2.1329034, 2.143568, 2.1542858, 2.1650572, 2.1758825, 2.1867619, 2.1976957, 2.2086842, 2.2197276, 2.2308263, 2.2419804, 2.2531903, 2.2644562, 2.2757785, 2.2871574, 2.2985932, 2.3100862, 2.3216366, 2.3332448, 2.344911, 2.3566356, 2.3684187, 2.3802608, 2.3921621, 2.404123, 2.4161436, 2.4282243, 2.4403654, 2.4525672, 2.4648301, 2.4771542, 2.48954, 2.5019877, 2.5144976, 2.5270701, 2.5397055, 2.552404, 2.565166, 2.5779919, 2.5908818, 2.6038362, 2.6168554, 2.6299397, 2.6430894, 2.6563048, 2.6695863, 2.6829343, 2.6963489, 2.7098307, 2.7233798, 2.7369967, 2.7506817, 2.7644351, 2.7782573, 2.7921486, 2.8061093, 2.8201399, 2.8342406, 2.8484118, 2.8626539, 2.8769671, 2.891352, 2.9058087, 2.9203378, 2.9349394, 2.9496141, 2.9643622, 2.979184, 2.9940799, 3.0090503, 3.0240956, 3.0392161, 3.0544122, 3.0696842, 3.0850326, 3.1004578, 3.1159601, 3.1315399, 3.1471976, 3.1629336, 3.1787482, 3.194642, 3.2106152, 3.2266683, 3.2428016, 3.2590156, 3.2753107, 3.2916873, 3.3081457, 3.3246864, 3.3413099, 3.3580164, 3.3748065, 3.3916805, 3.4086389, 3.4256821, 3.4428105, 3.4600246, 3.4773247, 3.4947113, 3.5121849, 3.5297458, 3.5473945, 3.5651315, 3.5829572, 3.6008719, 3.6188763, 3.6369707, 3.6551555, 3.6734313, 3.6917985, 3.7102575, 3.7288088, 3.7474528, 3.7661901, 3.785021, 3.8039461, 3.8229659, 3.8420807, 3.8612911, 3.8805975, 3.9000005, 3.9195005, 3.939098, 3.9587935, 3.9785875, 3.9984804, 4.0184728, 4.0385652, 4.058758, 4.0790518, 4.0994471, 4.1199443, 4.140544, 4.1612467, 4.182053, 4.2029632, 4.2239781, 4.245098, 4.2663234, 4.2876551, 4.3090933, 4.3306388, 4.352292, 4.3740535, 4.3959237, 4.4179033, 4.4399929, 4.4621928, 4.4845038, 4.5069263, 4.5294609, 4.5521082, 4.5748688, 4.5977431, 4.6207318, 4.6438355, 4.6670547, 4.69039, 4.7138419, 4.7374111, 4.7610982, 4.7849037, 4.8088282, 4.8328723, 4.8570367, 4.8813219, 4.9057285, 4.9302571, 4.9549084, 4.9796829, 5.0045814, 5.0296043, 5.0547523, 5.080026, 5.1054262, 5.1309533, 5.1566081, 5.1823911, 5.2083031, 5.2343446, 5.2605163, 5.2868189, 5.313253, 5.3398192, 5.3665183, 5.3933509, 5.4203177, 5.4474193, 5.4746564, 5.5020297, 5.5295398, 5.5571875, 5.5849734, 5.6128983, 5.6409628, 5.6691676, 5.6975135, 5.726001, 5.754631, 5.7834042, 5.8123212, 5.8413828, 5.8705897, 5.8999427, 5.9294424, 5.9590896, 5.988885, 6.0188295, 6.0489236, 6.0791682, 6.1095641, 6.1401119, 6.1708125, 6.2016665, 6.2326749, 6.2638382, 6.2951574, 6.3266332, 6.3582664, 6.3900577, 6.422008, 6.454118, 6.4863886, 6.5188206, 6.5514147, 6.5841717, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.465428, 9.610307, 9.653919, 9.706893, 9.851772, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Eu': {'mass_absorption_coefficient (cm2/g)': [6492.8, 6808.1, 7151.3, 7523.2, 7924.7, 8356.1, 8817.7, 9309.1, 9830.0, 10379.0, 10955.0, 11261.0, 11398.0, 11435.0, 226460.0, 220600.0, 214640.0, 173780.0, 137060.0, 107400.0, 84375.0, 67035.0, 66508.0, 63276.0, 62460.0, 61474.0, 58676.0, 54490.0, 45285.0, 38724.0, 34105.0, 30890.0, 28681.0, 27188.0, 26199.0, 25052.0, 23682.0, 22443.0, 21287.0, 20181.0, 19105.0, 18048.0, 17003.0, 15970.0, 15448.0, 15372.0, 15296.0, 15220.0, 15144.0, 15068.0, 14992.0, 14917.0, 14841.0, 14765.0, 14690.0, 14615.0, 14539.0, 14464.0, 14389.0, 14314.0, 14239.0, 14164.0, 14090.0, 14015.0, 13940.0, 13866.0, 13792.0, 13718.0, 13644.0, 13570.0, 13496.0, 13422.0, 13349.0, 13275.0, 13202.0, 13129.0, 13056.0, 12983.0, 12910.0, 12837.0, 12765.0, 12692.0, 12620.0, 12548.0, 12476.0, 12404.0, 12333.0, 12261.0, 12190.0, 12119.0, 12048.0, 11977.0, 11906.0, 11835.0, 11765.0, 11695.0, 11625.0, 11555.0, 11485.0, 11416.0, 11346.0, 11277.0, 11247.0, 52145.0, 50796.0, 48115.0, 45606.0, 43258.0, 41061.0, 39004.0, 37080.0, 35277.0, 33590.0, 32009.0, 30529.0, 29142.0, 27843.0, 26625.0, 25483.0, 24412.0, 23408.0, 22466.0, 21582.0, 20753.0, 19987.0, 19281.0, 18630.0, 18029.0, 17473.0, 16956.0, 16477.0, 16031.0, 15615.0, 15226.0, 14863.0, 14522.0, 14202.0, 13901.0, 13617.0, 13350.0, 13097.0, 12858.0, 12631.0, 12416.0, 12211.0, 12016.0, 11830.0, 11652.0, 11482.0, 11320.0, 11164.0, 11014.0, 10871.0, 10732.0, 10599.0, 10471.0, 10347.0, 10227.0, 10111.0, 9999.3, 9890.7, 9785.4, 9683.2, 9584.0, 9487.5, 9393.6, 9302.3, 9213.3, 9126.5, 9042.0, 8959.4, 8878.8, 8800.1, 8723.1, 8647.9, 8574.2, 8502.2, 8431.6, 8362.5, 8294.8, 8228.4, 8163.4, 8099.6, 8036.9, 7975.5, 7915.2, 7856.0, 7797.8, 7740.7, 7684.6, 7629.5, 7575.3, 7522.0, 7469.7, 7418.2, 7367.6, 7317.8, 7268.9, 7220.8, 7173.4, 7126.9, 7081.1, 7036.0, 6991.7, 6948.1, 6905.2, 6863.0, 6821.4, 6780.6, 6740.4, 6700.9, 6662.0, 6623.7, 6586.0, 6549.0, 6512.6, 6476.8, 6441.5, 6406.9, 6372.8, 6339.3, 6306.3, 6273.9, 6242.0, 6210.7, 6179.9, 6149.6, 6119.9, 6090.7, 6061.9, 6033.7, 6006.0, 5978.7, 5951.9, 5925.6, 5905.0, 7063.3, 7061.1, 7036.6, 7010.3, 6984.5, 6959.1, 6934.2, 6909.7, 6885.6, 6862.0, 6838.7, 6815.9, 6793.4, 6771.4, 6749.8, 6728.5, 6707.6, 6687.1, 6666.9, 6647.1, 6627.6, 6608.5, 6607.6, 6925.6, 6913.9, 6895.4, 6877.1, 6859.1, 6841.4, 6823.9, 6806.8, 6789.8, 6773.1, 6756.7, 6740.5, 6724.5, 6708.7, 6693.2, 6677.8, 6662.7, 6647.7, 6633.0, 6618.4, 6603.9, 6589.7, 6575.5, 6561.6, 6547.7, 6534.0, 6520.4, 6506.9, 6493.5, 6480.3, 6467.1, 6454.0, 6441.0, 6428.0, 6415.1, 6402.3, 6389.5, 6376.8, 6364.1, 6351.4, 6338.7, 6326.1, 6313.4, 6300.8, 6288.2, 6275.5, 6262.8, 6250.1, 6241.1, 6640.4, 6638.1, 6626.5, 6612.5, 6598.5, 6584.5, 6570.4, 6556.2, 6542.0, 6527.7, 6513.3, 6498.8, 6484.3, 6469.6, 6454.9, 6440.0, 6425.1, 6410.0, 6394.8, 6379.5, 6364.1, 6348.5, 6332.8, 6317.0, 6301.0, 6284.8, 6268.5, 6252.0, 6235.4, 6218.5, 6201.5, 6184.3, 6167.0, 6149.4, 6131.7, 6113.8, 6095.8, 6077.5, 6059.1, 6040.5, 6021.8, 6002.8, 5983.7, 5964.4, 5944.9, 5925.2, 5905.4, 5885.3, 5865.1, 5844.7, 5824.1, 5803.4, 5782.5, 5761.4, 5740.1, 5718.6, 5697.0, 5675.2, 5653.3, 5631.1, 5608.8, 5586.4, 5563.8, 5541.0, 5518.0, 5495.0, 5471.7, 5448.3, 5424.8, 5401.1, 5377.3, 5353.3, 5329.2, 5304.9, 5280.6, 5256.1, 5231.5, 5206.7, 5181.8, 5156.9, 5131.8, 5106.5, 5081.1, 5055.6, 5029.9, 5004.0, 4978.1, 4952.0, 4925.8, 4899.5, 4873.0, 4846.5, 4819.9, 4793.1, 4766.3, 4739.4, 4712.5, 4685.4, 4658.3, 4631.2, 4603.9, 4576.7, 4549.3, 4522.0, 4494.5, 4467.1, 4439.6, 4412.1, 4384.6, 4357.0, 4329.5, 4302.0, 4274.4, 4246.9, 4219.4, 4191.9, 4164.4, 4136.9, 4109.5, 4082.1, 4054.7, 4027.4, 4000.1, 3972.8, 3945.6, 3918.5, 3891.4, 3864.4, 3837.4, 3810.5, 3783.7, 3756.9, 3730.2, 3703.6, 3677.1, 3650.7, 3624.3, 3598.0, 3571.9, 3545.8, 3519.8, 3493.9, 3468.1, 3442.4, 3416.8, 3391.4, 3366.0, 3340.7, 3315.6, 3290.5, 3265.6, 3240.8, 3216.1, 3191.6, 3167.1, 3142.8, 3118.6, 3094.5, 3070.6, 3046.8, 3023.1, 2999.5, 2976.1, 2952.8, 2929.6, 2906.6, 2883.7, 2860.9, 2838.2, 2815.7, 2793.4, 2771.1, 2749.0, 2727.1, 2705.3, 2683.6, 2662.0, 2640.6, 2619.3, 2598.2, 2577.2, 2556.4, 2535.6, 2515.1, 2494.6, 2474.3, 2454.2, 2434.1, 2414.3, 2394.5, 2374.9, 2355.4, 2336.1, 2316.9, 2297.8, 2278.9, 2260.1, 2241.5, 2223.0, 2204.6, 2186.4, 2168.3, 2150.3, 2132.4, 2114.7, 2097.2, 2078.4, 2057.2, 2036.3, 2015.5, 1995.0, 1974.6, 1954.0, 1933.6, 1913.4, 1893.4, 1873.7, 1854.1, 1834.8, 1815.7, 1796.8, 1778.1, 1759.6, 1741.3, 1723.2, 1705.3, 1687.5, 1670.0, 1652.7, 1635.5, 1618.6, 1613.4, 6465.4, 6412.8, 6331.9, 6252.1, 6173.3, 6095.5, 6057.6, 9159.0, 9108.1, 8991.9, 8877.2, 8764.0, 8652.2, 8541.9, 8433.0, 8325.5, 8219.4, 8114.7, 8011.3, 7909.2, 7808.5, 7709.0, 7610.9, 7514.0, 7418.3, 7323.9, 7230.7, 7138.7, 7047.8, 6958.2, 6869.7, 6782.3, 6696.1, 6611.0, 6526.9, 6443.8, 6361.9, 6280.9, 6201.0, 6122.1, 6044.3, 5967.4, 5891.6, 5816.7, 5742.8, 5669.8, 5597.8, 5526.7, 5456.5, 5387.2, 5318.8, 5251.3, 5184.6, 5118.8, 5053.8, 4989.7, 4926.6, 4914.3, 5733.2, 5696.3, 5621.7, 5548.2, 5475.6, 5404.0, 5333.4, 5263.8, 5195.0, 5127.2, 5060.3, 4997.8, 4936.2, 4875.6, 4816.0, 4757.3, 4699.5, 4642.6, 4616.2, 4918.2, 4906.2, 4845.8, 4786.2, 4727.5, 4669.6, 4612.7, 4556.5, 4501.2, 4446.7, 4392.9, 4341.1, 4290.5, 4240.8, 4191.8, 4143.5, 4096.0, 4049.1, 4003.0, 3957.5, 3912.7, 3868.5, 3825.0, 3815.6, 3971.1, 3963.1, 3919.1, 3875.7, 3832.9, 3790.7, 3749.0, 3707.9, 3667.3, 3627.2, 3587.6, 3548.6, 3510.2, 3472.3, 3434.8, 3396.7, 3359.0, 3321.8, 3285.1, 3248.7, 3212.8, 3177.3, 3142.3, 3107.6, 3073.3, 3039.4, 3005.9, 2972.8, 2940.0, 2907.7, 2875.6, 2844.0, 2812.3, 2780.8, 2749.7, 2718.9, 2688.4, 2658.3, 2628.5, 2599.1, 2569.9, 2541.1, 2512.6, 2484.4, 2456.6, 2429.0, 2401.7, 2374.3, 2347.0, 2319.9, 2293.2, 2266.8, 2240.5, 2214.3, 2188.3, 2162.6, 2137.1, 2112.0, 2087.2, 2062.7, 2038.4, 2014.5, 1990.8, 1967.4, 1944.3, 1921.4, 1898.9, 1876.4, 1854.0, 1832.0, 1810.2, 1788.6, 1767.3, 1746.3, 1725.6, 1705.1, 1684.8, 1664.8, 1645.1, 1625.6, 1606.3, 1587.3, 1568.5, 1549.9, 1531.6, 1513.5, 1495.6, 1478.0, 1460.6, 1443.4, 1426.4, 1409.6, 1393.0, 1376.7, 1360.5, 1344.6, 1328.8, 1313.3, 1297.9, 1282.7, 1267.8, 1252.9, 1238.1, 1223.5, 1207.7, 1192.1, 1176.7, 1161.6, 1146.6, 1131.9, 1117.3, 1103.0, 1088.8, 1074.8, 1061.1, 1047.5, 1034.1, 1020.8, 1007.7, 994.7, 981.85, 969.18, 956.67, 944.34, 932.17, 920.17, 908.33, 896.65, 885.12, 873.76, 862.54, 851.48, 840.57, 829.8, 819.18, 808.7, 798.36, 788.16, 778.1, 768.17, 758.35, 748.63, 739.04, 729.57, 720.24, 711.03, 701.94, 692.98, 684.13, 675.41, 666.8, 658.3, 649.92, 641.65, 633.49, 625.44, 617.5, 609.66, 601.92, 594.29, 586.76, 562.2, 474.55, 401.13, 339.19, 285.6, 240.7, 203.23, 171.8, 149.03, 144.77, 143.35, 141.89, 414.1, 394.93, 350.88, 341.38, 327.3, 323.72, 443.99, 425.49, 407.05, 405.35, 390.01, 385.82, 439.52, 423.54, 395.05, 333.79, 279.54, 234.04, 195.76, 163.83, 137.22, 115.03, 96.31, 80.495, 67.136, 55.926, 46.628, 38.911, 32.5, 27.169, 22.733, 19.035, 15.831, 13.145, 10.898, 9.0314, 7.4611, 6.1603, 5.0873, 4.2045, 3.5472, 3.4776, 3.3975, 3.359, 19.008, 18.303, 16.792, 14.099, 11.819, 9.9026, 8.2843, 6.9285, 5.7946, 4.8396, 4.03, 3.3562, 2.7907, 2.3152, 1.9209, 1.5939, 1.3227, 1.0977, 0.91107, 0.75623, 0.62776, 0.52115, 0.43268, 0.35926, 0.29925, 0.24945, 0.20795, 0.17336, 0.14454, 0.12051, 0.10049, 0.083796, 0.069879, 0.058277, 0.048604, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02156, 0.02189, 0.021978, 0.02211, 0.02227063, 0.02244, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.031164, 0.031641, 0.0317682, 0.031959, 0.032436, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13317243, 0.13322758, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25641678, 0.25667624, 0.2567832, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28366578, 0.28413421, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.35978936, 0.36031129, 0.36061062, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1307711, 1.1310289, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1603841, 1.1608158, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4794437, 1.4817563, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6125491, 1.6152508, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.7973, 1.8027, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.837362, 6.915365, 6.942016, 6.969923, 7.011785, 7.116438, 7.392525, 7.464758, 7.579014, 7.609483, 7.655185, 7.769442, 7.89096, 7.902609, 8.01174, 8.043948, 8.09226, 8.21304, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.54862, 47.88159, 48.27641, 48.47048, 48.7616, 49.48938, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Zr': {'mass_absorption_coefficient (cm2/g)': [162020.0, 156300.0, 145190.0, 129980.0, 112320.0, 93901.0, 76157.0, 60092.0, 46295.0, 34973.0, 26017.0, 19137.0, 13972.0, 10161.0, 7384.1, 6302.3, 5864.0, 5753.6, 126350.0, 121920.0, 118530.0, 91486.0, 68632.0, 51432.0, 38671.0, 29287.0, 22413.0, 17374.0, 13666.0, 13041.0, 12384.0, 12217.0, 12989.0, 12433.0, 11970.0, 10007.0, 8545.8, 7437.9, 6584.9, 5916.4, 5388.7, 4960.7, 4603.7, 4299.5, 4034.8, 3800.3, 3588.9, 3395.5, 3216.0, 3047.3, 2887.2, 2734.0, 2586.6, 2584.4, 2555.7, 2551.5, 2542.9, 5906.4, 5974.4, 6060.8, 8461.6, 8521.7, 9056.0, 9604.8, 13506.0, 18339.0, 23003.0, 26796.0, 29274.0, 30306.0, 30022.0, 28702.0, 28485.0, 28056.0, 27937.0, 30635.0, 30142.0, 29631.0, 29537.0, 29493.0, 30485.0, 29955.0, 28278.0, 25637.0, 22973.0, 22769.0, 22174.0, 22018.0, 22762.0, 22179.0, 21346.0, 18887.0, 16629.0, 14584.0, 12747.0, 11114.0, 9669.7, 8398.1, 7277.7, 6294.0, 5435.1, 4687.9, 4040.0, 3427.8, 2910.1, 2473.0, 2104.1, 1783.5, 1508.5, 1277.9, 1084.1, 919.86, 781.28, 664.43, 595.2, 573.86, 568.36, 2350.8, 2322.6, 2241.3, 2223.4, 2143.2, 2118.1, 2919.1, 2798.1, 2717.4, 2424.8, 2331.0, 2307.0, 2621.8, 2615.6, 2521.4, 2226.2, 1894.1, 1599.3, 1344.4, 1129.3, 949.21, 798.51, 672.3, 564.18, 471.36, 393.31, 328.1, 273.94, 228.81, 191.13, 159.8, 133.74, 112.0, 93.641, 77.7, 64.262, 53.142, 43.798, 36.131, 29.833, 24.654, 20.393, 16.884, 13.991, 13.907, 13.326, 13.177, 93.892, 89.92, 83.703, 69.936, 58.803, 49.506, 41.511, 34.728, 28.934, 24.079, 20.041, 16.682, 13.859, 11.452, 9.4637, 7.8213, 6.4643, 5.3425, 4.415, 3.6467, 3.0124, 2.4885, 2.0559, 1.6986, 1.3983, 1.1475, 0.94179, 0.77296, 0.63443, 0.52075, 0.42747, 0.3509, 0.28807, 0.23649, 0.19416, 0.15941, 0.13089, 0.10747, 0.088246, 0.072464, 0.059506, 0.048867, 0.040131, 0.032959, 0.027069, 0.022232, 0.01826, 0.014998, 0.012319, 0.010119, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.028126, 0.0285565, 0.0286713, 0.0288435, 0.02908327, 0.029274, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.050274, 0.0510435, 0.0512487, 0.0515565, 0.052326, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1764, 0.178752, 0.1791, 0.17982, 0.1809, 0.181488, 0.1822176, 0.183312, 0.1836, 0.186048, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.32389, 0.3288475, 0.3301695, 0.3321525, 0.337316, 0.342479, 0.3434143, 0.3438558, 0.345921, 0.351084, 0.3671099, 0.3924405, 0.4195189, 0.421694, 0.4281485, 0.4298697, 0.4324515, 0.438906, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.177854, 2.211189, 2.220078, 2.224304, 2.233412, 2.260566, 2.266746, 2.295166, 2.304393, 2.318233, 2.352834, 2.377781, 2.480968, 2.518942, 2.529068, 2.541848, 2.544258, 2.582232, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 17.63765, 17.90761, 17.9796, 18.08759, 18.35755, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Er': {'mass_absorption_coefficient (cm2/g)': [7238.3, 7163.2, 6646.7, 6854.8, 7094.9, 7368.3, 7676.3, 8020.3, 8402.0, 8822.8, 9284.0, 9785.8, 10328.0, 10912.0, 11536.0, 12200.0, 12902.0, 13535.0, 13641.0, 13707.0, 13752.0, 208900.0, 196000.0, 168170.0, 127700.0, 97355.0, 75159.0, 59391.0, 48441.0, 40984.0, 36002.0, 32748.0, 30688.0, 29983.0, 29720.0, 29656.0, 29692.0, 29576.0, 29488.0, 28907.0, 28614.0, 28567.0, 28678.0, 28065.0, 27099.0, 26105.0, 25585.0, 25507.0, 25430.0, 25352.0, 25274.0, 25196.0, 25117.0, 25038.0, 24959.0, 24880.0, 24801.0, 24721.0, 24641.0, 24561.0, 24480.0, 24399.0, 24318.0, 24237.0, 24156.0, 24074.0, 23992.0, 23910.0, 23828.0, 23745.0, 23662.0, 23579.0, 23496.0, 23412.0, 23328.0, 23244.0, 23160.0, 23076.0, 22991.0, 22906.0, 22821.0, 22736.0, 22650.0, 22565.0, 22479.0, 22393.0, 22307.0, 22220.0, 22134.0, 22047.0, 21960.0, 21873.0, 21786.0, 21698.0, 21611.0, 21523.0, 21435.0, 21347.0, 21258.0, 21170.0, 21082.0, 20993.0, 20904.0, 20815.0, 20726.0, 20637.0, 20548.0, 20458.0, 20369.0, 20279.0, 20190.0, 20100.0, 20010.0, 19920.0, 19830.0, 19740.0, 19650.0, 19559.0, 19469.0, 19378.0, 19288.0, 19197.0, 19107.0, 19016.0, 18926.0, 18835.0, 18744.0, 18653.0, 18563.0, 18472.0, 18381.0, 18290.0, 18199.0, 18109.0, 18018.0, 17927.0, 17836.0, 17745.0, 17655.0, 17564.0, 17473.0, 17383.0, 17292.0, 17201.0, 17111.0, 17020.0, 16930.0, 16840.0, 16749.0, 16659.0, 16620.0, 28823.0, 28531.0, 27740.0, 26991.0, 26281.0, 25607.0, 24967.0, 24359.0, 23782.0, 23234.0, 22712.0, 22216.0, 22203.0, 29547.0, 28877.0, 28021.0, 27209.0, 26438.0, 25708.0, 25014.0, 24356.0, 23730.0, 23136.0, 22575.0, 22047.0, 21548.0, 21077.0, 20632.0, 20211.0, 19811.0, 19432.0, 19071.0, 18729.0, 18403.0, 18095.0, 17805.0, 17530.0, 17269.0, 17021.0, 16784.0, 16559.0, 16344.0, 16138.0, 15940.0, 15751.0, 15568.0, 15393.0, 15223.0, 15060.0, 14901.0, 14748.0, 14600.0, 14456.0, 14316.0, 14179.0, 14047.0, 13917.0, 13791.0, 13668.0, 13548.0, 13430.0, 13315.0, 13203.0, 13092.0, 12984.0, 12878.0, 12773.0, 12671.0, 12570.0, 12471.0, 12374.0, 12278.0, 12184.0, 12091.0, 11999.0, 11909.0, 11820.0, 11732.0, 11646.0, 11561.0, 11476.0, 11393.0, 11311.0, 11230.0, 11150.0, 11071.0, 10993.0, 10916.0, 10840.0, 10764.0, 10690.0, 10616.0, 10543.0, 10471.0, 10400.0, 10329.0, 10260.0, 10191.0, 10123.0, 10055.0, 9988.4, 9922.3, 9857.0, 9792.4, 9728.5, 9665.2, 9602.6, 9540.7, 9479.4, 9418.7, 9358.7, 9299.3, 9240.6, 9182.4, 9124.9, 9067.9, 9011.6, 8955.8, 8900.7, 8846.1, 8792.0, 8738.6, 8685.7, 8633.4, 8581.6, 8530.3, 8479.6, 8429.4, 8379.8, 8330.7, 8282.0, 8234.0, 8186.4, 9096.1, 9068.3, 9020.4, 8972.9, 8926.0, 8879.5, 8833.5, 8788.0, 8743.0, 8698.4, 8654.3, 8610.6, 8567.3, 8524.5, 8482.2, 8440.2, 8398.7, 8357.6, 8316.9, 8276.6, 8236.7, 8197.2, 8158.0, 8119.2, 8080.7, 8042.6, 8004.9, 7967.4, 7967.1, 8176.2, 8157.4, 8120.6, 8084.2, 8048.1, 8012.2, 7976.7, 7941.5, 7906.5, 7871.9, 7837.5, 7803.3, 7769.4, 7735.8, 7702.4, 7669.2, 7636.3, 7603.6, 7571.1, 7538.9, 7506.8, 7475.0, 7443.3, 7411.8, 7380.5, 7349.4, 7318.4, 7287.6, 7257.0, 7226.5, 7196.1, 7165.9, 7135.8, 7105.9, 7076.0, 7046.3, 7016.7, 6987.2, 6957.8, 6928.5, 6899.2, 6874.0, 6870.1, 7146.9, 7130.9, 7101.1, 7071.4, 7041.7, 7012.2, 6982.6, 6953.1, 6923.7, 6894.3, 6865.0, 6835.7, 6806.4, 6777.2, 6747.9, 6718.8, 6689.6, 6660.4, 6631.3, 6602.2, 6573.0, 6543.9, 6514.8, 6485.6, 6456.5, 6427.3, 6398.1, 6368.8, 6339.6, 6310.3, 6281.0, 6251.7, 6222.4, 6193.0, 6163.6, 6134.2, 6104.7, 6075.3, 6045.8, 6016.2, 5986.6, 5957.0, 5927.4, 5897.7, 5868.0, 5838.2, 5808.4, 5778.6, 5748.8, 5718.9, 5689.0, 5659.1, 5629.1, 5599.2, 5569.2, 5539.2, 5509.2, 5479.1, 5449.0, 5419.0, 5388.9, 5358.8, 5328.7, 5298.6, 5268.4, 5238.3, 5208.2, 5178.1, 5147.9, 5117.8, 5087.7, 5057.6, 5027.4, 4997.4, 4967.3, 4937.2, 4907.1, 4877.1, 4847.1, 4817.1, 4787.2, 4757.2, 4727.3, 4697.4, 4667.5, 4637.6, 4607.8, 4577.9, 4548.1, 4518.4, 4488.6, 4458.9, 4429.3, 4399.7, 4370.1, 4340.5, 4311.0, 4281.6, 4252.2, 4222.8, 4193.6, 4164.4, 4135.2, 4106.1, 4077.1, 4048.2, 4019.3, 3990.4, 3961.6, 3932.9, 3904.3, 3875.8, 3847.4, 3819.1, 3790.9, 3762.8, 3734.8, 3706.9, 3679.1, 3651.5, 3624.0, 3596.6, 3569.3, 3542.1, 3515.1, 3488.2, 3461.4, 3434.8, 3408.3, 3382.0, 3355.8, 3329.7, 3303.8, 3278.0, 3252.3, 3226.8, 3201.5, 3176.3, 3151.2, 3126.3, 3101.6, 3077.0, 3052.5, 3028.2, 3004.1, 2980.1, 2956.3, 2932.6, 2909.1, 2885.7, 2862.5, 2839.4, 2816.6, 2793.8, 2771.2, 2748.8, 2726.6, 2704.5, 2682.5, 2660.8, 2639.1, 2615.2, 2586.7, 2558.5, 2530.6, 2503.0, 2475.8, 2448.8, 2422.2, 2395.9, 2369.9, 2344.2, 2318.8, 2293.7, 2268.9, 2244.4, 2220.2, 2196.2, 2172.5, 2149.1, 2125.9, 2103.1, 2080.5, 2058.1, 2036.0, 2014.2, 1992.5, 1971.2, 1950.1, 1929.2, 1908.6, 1888.2, 1868.0, 1848.1, 1828.4, 1808.9, 1789.7, 1770.6, 1751.8, 1733.2, 1714.8, 1696.6, 1678.7, 1660.9, 1643.3, 1625.9, 1608.8, 1591.8, 1575.0, 1558.4, 1542.0, 1525.5, 1509.1, 1492.9, 1476.9, 1461.1, 1445.4, 1430.0, 1414.7, 1399.6, 1384.7, 1369.9, 1355.3, 1340.9, 1326.6, 1312.5, 1298.5, 1284.8, 1271.1, 1257.7, 1251.9, 4743.6, 4712.8, 4653.3, 4594.7, 4536.7, 4479.6, 4423.1, 4391.3, 6600.7, 6573.2, 6489.5, 6406.8, 6325.1, 6244.5, 6164.9, 6086.3, 6008.8, 5932.3, 5856.7, 5782.2, 5708.6, 5635.9, 5564.2, 5493.4, 5423.5, 5354.5, 5286.5, 5219.3, 5152.9, 5087.4, 5022.8, 4959.0, 4896.0, 4833.8, 4772.5, 4711.9, 4652.1, 4593.1, 4534.8, 4477.3, 4420.5, 4364.5, 4309.2, 4254.6, 4200.7, 4147.5, 4094.9, 4043.1, 3991.9, 3941.4, 3891.5, 3842.3, 3793.6, 3764.0, 4405.3, 4402.8, 4346.0, 4287.6, 4229.8, 4172.9, 4116.7, 4061.4, 4006.8, 3952.9, 3899.7, 3848.9, 3800.5, 3752.9, 3706.1, 3660.1, 3614.9, 3570.2, 3526.3, 3483.0, 3440.5, 3398.6, 3398.3, 3621.1, 3595.6, 3550.4, 3506.0, 3462.3, 3419.2, 3376.8, 3335.0, 3293.8, 3253.2, 3213.4, 3175.5, 3138.2, 3101.4, 3065.3, 3029.7, 2994.7, 2960.1, 2926.1, 2893.9, 2892.6, 3002.9, 2993.6, 2959.8, 2926.4, 2893.5, 2861.0, 2829.0, 2797.3, 2766.1, 2735.3, 2704.8, 2675.0, 2645.5, 2616.4, 2587.2, 2558.1, 2529.3, 2500.9, 2472.8, 2445.0, 2417.6, 2390.4, 2363.6, 2337.1, 2310.9, 2285.0, 2259.4, 2234.1, 2209.1, 2184.3, 2159.8, 2135.6, 2111.7, 2088.0, 2064.6, 2041.1, 2018.0, 1995.1, 1972.4, 1950.0, 1927.9, 1906.0, 1884.3, 1862.9, 1841.7, 1820.8, 1800.1, 1779.6, 1759.4, 1739.4, 1719.5, 1699.6, 1680.0, 1660.7, 1641.5, 1622.5, 1603.7, 1585.0, 1566.5, 1548.3, 1530.3, 1512.5, 1494.8, 1475.8, 1457.0, 1438.4, 1420.1, 1402.1, 1384.3, 1366.7, 1349.4, 1332.3, 1315.4, 1298.7, 1282.1, 1265.9, 1249.8, 1233.9, 1218.3, 1202.9, 1187.7, 1172.7, 1157.8, 1143.2, 1128.7, 1114.4, 1100.3, 1086.5, 1072.7, 1059.2, 1045.9, 1032.7, 1019.7, 1006.9, 994.29, 981.83, 969.53, 957.39, 945.42, 933.61, 921.9, 910.12, 898.49, 887.03, 875.72, 864.56, 853.56, 842.7, 831.99, 821.42, 810.99, 800.71, 790.56, 780.55, 770.67, 760.93, 751.31, 741.83, 732.47, 723.24, 692.8, 583.7, 492.02, 415.17, 350.78, 296.77, 251.38, 212.14, 178.98, 151.2, 127.9, 116.74, 112.33, 111.19, 312.82, 307.2, 298.5, 252.75, 249.17, 239.23, 236.67, 324.85, 311.27, 301.31, 292.51, 288.23, 284.93, 324.18, 311.69, 282.6, 236.62, 198.87, 166.96, 139.95, 117.3, 98.349, 82.495, 69.051, 57.787, 48.302, 40.36, 33.752, 28.251, 23.668, 19.687, 16.347, 13.583, 11.296, 9.3978, 7.8085, 6.4856, 5.3673, 4.4414, 3.6759, 3.0447, 2.8047, 2.6875, 2.6573, 14.33, 13.885, 13.8, 11.693, 9.8139, 8.2298, 6.9001, 5.7745, 4.8172, 4.0192, 3.3538, 2.7989, 2.3362, 1.9428, 1.615, 1.3426, 1.1163, 0.92817, 0.77183, 0.64189, 0.53386, 0.44405, 0.36938, 0.30729, 0.25607, 0.21427, 0.1793, 0.15006, 0.12559, 0.10512, 0.087988, 0.073654, 0.061659, 0.0], - 'energies (keV)': [0.0043215, 0.004386, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.028812, 0.02908327, 0.029253, 0.0293706, 0.029547, 0.029988, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.058604, 0.059501, 0.0597402, 0.060099, 0.06058959, 0.060996, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16751369, 0.16768632, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17660087, 0.17679912, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32033919, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36576056, 0.36663944, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44843083, 0.44872947, 0.44976915, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.409097, 1.4095029, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4529192, 1.4536808, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.8098795, 1.813351, 1.8137205, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0036337, 2.0079663, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2022856, 2.2027149, 2.2107145, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.190742, 8.31611, 8.349542, 8.399689, 8.44789, 8.525058, 9.030794, 9.079014, 9.217979, 9.255036, 9.310622, 9.449586, 9.556274, 9.653919, 9.702543, 9.741549, 9.800056, 9.946326, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 56.33579, 57.19807, 57.42801, 57.77293, 58.4927, 58.63521, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ni': {'mass_absorption_coefficient (cm2/g)': [96199.0, 96924.0, 97839.0, 98922.0, 100150.0, 101490.0, 102850.0, 104180.0, 105440.0, 106600.0, 107620.0, 108490.0, 109170.0, 109650.0, 109900.0, 109910.0, 109670.0, 109200.0, 108470.0, 107520.0, 106340.0, 104950.0, 103380.0, 101640.0, 99415.0, 96684.0, 93751.0, 90650.0, 89212.0, 88337.0, 87907.0, 104810.0, 102720.0, 102150.0, 91562.0, 81936.0, 73820.0, 66710.0, 60352.0, 54620.0, 50042.0, 49438.0, 48920.0, 48628.0, 51047.0, 49994.0, 47611.0, 43391.0, 39571.0, 36110.0, 32972.0, 30121.0, 27523.0, 25145.0, 22958.0, 20938.0, 19063.0, 17319.0, 15694.0, 14179.0, 12761.0, 11439.0, 10215.0, 9086.9, 8055.6, 7118.4, 6272.0, 5511.7, 4832.2, 4227.8, 3692.4, 3220.1, 2804.7, 2440.4, 2121.7, 1843.5, 1751.6, 1696.2, 1679.3, 9947.9, 9698.7, 9599.8, 9578.2, 13543.0, 13453.0, 12953.0, 11417.0, 9905.5, 9636.5, 9531.5, 9434.0, 10568.0, 10191.0, 9295.0, 7914.5, 6749.1, 5758.5, 4898.8, 4152.5, 3498.9, 2947.2, 2482.4, 2091.4, 1760.7, 1480.2, 1244.9, 1047.6, 882.12, 743.34, 623.01, 519.58, 432.25, 358.57, 297.83, 247.71, 205.31, 170.23, 141.27, 117.34, 97.561, 81.191, 67.631, 56.388, 47.057, 43.068, 41.34, 40.895, 332.05, 323.6, 317.83, 270.07, 228.08, 191.01, 159.13, 132.76, 110.78, 92.441, 77.148, 64.391, 53.744, 44.579, 36.929, 30.595, 25.349, 20.989, 17.381, 14.393, 11.865, 9.7658, 8.0386, 6.6173, 5.4404, 4.4537, 3.646, 2.985, 2.4439, 2.0009, 1.6382, 1.3413, 1.0983, 0.89927, 0.73636, 0.60298, 0.49308, 0.40193, 0.32765, 0.2671, 0.21775, 0.17752, 0.14473, 0.118, 0.096207, 0.078442, 0.063959, 0.052152, 0.042525, 0.034677, 0.028277, 0.023059, 0.018805, 0.015336, 0.012507, 0.0102, 0.008319, 0.0067849, 0.0055339, 0.0045136, 0.0036816, 0.003003, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.066738, 0.0677595, 0.0680319, 0.0684405, 0.06923942, 0.069462, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.109564, 0.1104581, 0.111241, 0.1116882, 0.112359, 0.114036, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.837606, 0.8504265, 0.854462, 0.8589735, 0.8675405, 0.8710281, 0.871794, 0.8739896, 0.8762595, 0.889338, 0.9342948, 0.987938, 0.9987612, 1.00306, 1.007092, 1.013141, 1.028262, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.166144, 8.291136, 8.324467, 8.374464, 8.44789, 8.499456, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Na': {'mass_absorption_coefficient (cm2/g)': [86393.0, 89898.0, 101880.0, 121220.0, 140000.0, 156860.0, 170960.0, 181720.0, 188790.0, 192120.0, 191860.0, 188360.0, 186440.0, 185020.0, 184630.0, 191510.0, 190190.0, 189900.0, 182730.0, 173920.0, 163720.0, 152550.0, 140810.0, 128850.0, 116980.0, 105430.0, 94384.0, 83986.0, 74278.0, 65318.0, 57149.0, 49779.0, 43189.0, 37342.0, 32190.0, 27675.0, 23739.0, 20322.0, 17366.0, 14816.0, 12621.0, 10738.0, 9125.5, 7747.4, 6571.8, 5570.4, 4718.6, 3994.8, 3380.4, 2859.4, 2417.2, 2030.7, 1705.4, 1431.9, 1202.2, 1009.5, 847.9, 712.4, 598.84, 520.58, 498.01, 493.73, 5685.5, 6033.1, 6770.9, 6135.4, 4951.7, 3829.1, 3125.5, 2592.6, 2181.0, 1851.0, 1575.8, 1339.6, 1124.8, 936.91, 780.44, 650.13, 541.6, 449.39, 371.49, 307.11, 253.9, 209.91, 173.55, 143.49, 117.81, 96.698, 79.372, 65.153, 53.482, 43.902, 36.039, 29.585, 24.287, 19.898, 16.185, 13.165, 10.709, 8.7112, 7.0861, 5.7642, 4.689, 3.8143, 3.1029, 2.5241, 2.0533, 1.6704, 1.3589, 1.105, 0.89501, 0.72491, 0.58543, 0.47222, 0.38086, 0.30717, 0.24775, 0.19982, 0.16116, 0.12998, 0.10484, 0.084554, 0.068197, 0.055004, 0.044364, 0.035782, 0.02886, 0.023278, 0.018775, 0.015143, 0.012214, 0.0098513, 0.0079457, 0.0064088, 0.0051691, 0.0041693, 0.0033628, 0.0027124, 0.0021877, 0.0017646, 0.0014233, 0.001148, 0.00092595, 0.00074686, 0.0006024, 0.00048589, 0.00039191, 0.00031611, 0.00025497, 0.00020566, 0.00016588, 0.0001338, 0.00010792, 8.7048e-05, 0.0], - 'energies (keV)': [0.0312555, 0.031722, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.062034, 0.0629835, 0.0632367, 0.0636165, 0.064566, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.050658, 1.067676, 1.071028, 1.077461, 1.093542, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Nb': {'mass_absorption_coefficient (cm2/g)': [187540.0, 170140.0, 151630.0, 132900.0, 114680.0, 97604.0, 82109.0, 68422.0, 56593.0, 46553.0, 38153.0, 31207.0, 25514.0, 20877.0, 17119.0, 14080.0, 11627.0, 9644.9, 9259.8, 9158.5, 125020.0, 115480.0, 99903.0, 70207.0, 50270.0, 36867.0, 27684.0, 21273.0, 16713.0, 13409.0, 13219.0, 12611.0, 12458.0, 13137.0, 12625.0, 11929.0, 10239.0, 8967.2, 7987.4, 7216.2, 6595.4, 6084.0, 5653.0, 5281.8, 4955.4, 4663.0, 4396.9, 4151.3, 3921.6, 3704.3, 3496.9, 3297.8, 3105.7, 2931.6, 2919.8, 2894.4, 2890.0, 2879.1, 5825.7, 5892.1, 5968.5, 8091.1, 8133.2, 8628.3, 9341.8, 13060.0, 17572.0, 21814.0, 25104.0, 27052.0, 27601.0, 26943.0, 26213.0, 25834.0, 25729.0, 28398.0, 28217.0, 27963.0, 27916.0, 27441.0, 27311.0, 28295.0, 27795.0, 27213.0, 24745.0, 22189.0, 21305.0, 20734.0, 20585.0, 21260.0, 20702.0, 20574.0, 18172.0, 15966.0, 13971.0, 12189.0, 10605.0, 9208.4, 7982.8, 6908.8, 5968.0, 5147.9, 4436.1, 3764.0, 3195.1, 2714.5, 2308.3, 1965.1, 1671.3, 1415.6, 1199.9, 1018.6, 865.13, 735.23, 625.61, 563.53, 543.38, 538.19, 2147.6, 2135.4, 2050.1, 2043.8, 1960.0, 1936.9, 2665.0, 2555.1, 2478.4, 2231.4, 2145.6, 2123.6, 2407.4, 2394.3, 2321.3, 2035.5, 1724.7, 1450.0, 1218.2, 1023.5, 860.31, 723.62, 609.16, 511.09, 427.14, 356.52, 297.56, 248.57, 207.81, 173.71, 145.32, 121.68, 101.73, 84.401, 70.071, 57.992, 47.978, 39.569, 32.663, 26.988, 22.319, 18.473, 15.304, 13.092, 12.69, 12.547, 12.407, 86.967, 83.372, 74.955, 62.803, 52.799, 44.392, 37.185, 30.997, 25.805, 21.485, 17.891, 14.9, 12.364, 10.22, 8.4478, 6.9837, 5.7736, 4.773, 3.9453, 3.2596, 2.6933, 2.2255, 1.8391, 1.5179, 1.2467, 1.0237, 0.84063, 0.69034, 0.56695, 0.46564, 0.38245, 0.31413, 0.25803, 0.21196, 0.17412, 0.14304, 0.11751, 0.096546, 0.079322, 0.065174, 0.053551, 0.044002, 0.036157, 0.029712, 0.024416, 0.020065, 0.01649, 0.013552, 0.011138, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.0337305, 0.0338661, 0.0340695, 0.034578, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.056938, 0.0578095, 0.0580419, 0.0583905, 0.059262, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.200508, 0.2013709, 0.203252, 0.203577, 0.2043954, 0.205623, 0.206363, 0.2071926, 0.208437, 0.208692, 0.211548, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.35574, 0.361185, 0.362637, 0.364815, 0.3671099, 0.37026, 0.370832, 0.376508, 0.3780216, 0.380292, 0.385968, 0.3924405, 0.4195189, 0.4484657, 0.459032, 0.466058, 0.4679316, 0.470742, 0.477768, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.32309, 2.358648, 2.36813, 2.377781, 2.382353, 2.415406, 2.41791, 2.452376, 2.462235, 2.477023, 2.513994, 2.541848, 2.643746, 2.684212, 2.695002, 2.711189, 2.717235, 2.751654, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.60589, 18.81398, 18.89067, 18.96661, 19.08053, 19.36531, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Nd': {'mass_absorption_coefficient (cm2/g)': [7394.4, 7692.6, 8017.8, 8369.8, 8748.0, 9152.0, 9580.6, 10032.0, 10505.0, 10996.0, 11445.0, 11502.0, 11562.0, 11593.0, 200770.0, 190960.0, 170280.0, 136570.0, 108690.0, 86231.0, 68802.0, 55632.0, 45868.0, 38726.0, 35892.0, 34762.0, 34478.0, 34227.0, 33714.0, 33260.0, 30012.0, 27361.0, 25457.0, 24083.0, 23078.0, 22250.0, 20810.0, 19480.0, 18227.0, 17031.0, 15878.0, 14763.0, 13684.0, 12643.0, 12127.0, 12052.0, 11978.0, 11903.0, 11829.0, 11755.0, 11682.0, 11608.0, 11535.0, 11462.0, 11389.0, 11317.0, 11244.0, 11172.0, 11100.0, 11029.0, 10957.0, 10886.0, 10815.0, 10745.0, 10674.0, 10604.0, 10534.0, 10465.0, 10395.0, 10326.0, 10257.0, 10189.0, 10120.0, 10052.0, 9984.2, 9916.6, 9849.2, 9827.9, 81489.0, 81489.0, 78014.0, 72970.0, 68290.0, 63946.0, 59915.0, 56173.0, 52700.0, 49475.0, 46481.0, 43701.0, 41120.0, 38722.0, 36495.0, 34425.0, 32503.0, 30716.0, 29055.0, 27511.0, 26076.0, 24742.0, 23524.0, 22412.0, 21395.0, 20465.0, 19611.0, 18826.0, 18104.0, 17438.0, 16822.0, 16252.0, 15724.0, 15233.0, 14777.0, 14351.0, 13954.0, 13583.0, 13235.0, 12909.0, 12602.0, 12313.0, 12042.0, 11785.0, 11543.0, 11313.0, 11095.0, 10889.0, 10693.0, 10506.0, 10328.0, 10158.0, 9995.4, 9840.2, 9691.6, 9549.1, 9412.4, 9281.0, 9154.7, 9033.1, 8915.9, 8802.9, 8693.8, 8588.5, 8486.6, 8388.0, 8292.5, 8200.0, 8110.2, 8023.1, 7938.6, 7856.4, 7776.5, 7698.8, 7623.2, 7549.5, 7477.7, 7407.8, 7339.6, 7273.1, 7208.1, 7144.7, 7082.8, 7022.4, 6963.3, 6905.5, 6849.0, 6793.8, 6739.8, 6686.9, 6635.2, 6584.6, 6535.0, 6486.5, 6439.0, 6392.5, 6346.9, 6302.3, 6258.5, 6215.7, 6173.8, 6132.7, 6092.4, 6052.9, 6014.3, 5976.4, 5939.3, 5903.0, 5867.4, 5832.5, 5798.4, 5764.9, 5732.2, 5700.1, 5668.7, 5637.9, 5607.8, 5578.4, 5549.6, 5521.3, 5493.8, 5466.8, 5440.4, 5414.6, 5389.3, 5364.7, 5340.6, 5317.0, 5294.0, 5271.6, 5249.6, 5228.3, 5225.6, 6523.5, 6508.9, 6487.2, 6465.9, 6445.2, 6424.9, 6405.1, 6385.8, 6367.0, 6348.6, 6330.7, 6313.2, 6296.2, 6279.6, 6263.5, 6247.8, 6232.5, 6230.2, 6619.6, 6610.5, 6595.8, 6581.5, 6567.7, 6554.2, 6541.0, 6528.1, 6515.6, 6503.4, 6491.5, 6479.9, 6468.6, 6457.5, 6446.8, 6436.3, 6426.1, 6416.2, 6406.5, 6397.0, 6387.8, 6378.8, 6370.0, 6361.4, 6353.0, 6344.9, 6336.9, 6329.0, 6321.4, 6313.9, 6306.5, 6299.3, 6292.2, 6285.3, 6278.5, 6271.7, 6265.1, 6258.6, 6252.1, 6245.8, 6239.5, 6233.2, 6227.1, 6220.9, 6214.8, 6208.7, 6202.7, 6196.6, 6190.6, 6184.5, 6178.5, 6172.4, 6166.3, 6644.9, 6640.2, 6632.7, 6625.1, 6617.4, 6609.7, 6601.9, 6594.1, 6586.2, 6578.2, 6570.1, 6561.8, 6553.5, 6545.1, 6536.6, 6527.9, 6519.1, 6510.1, 6501.1, 6491.8, 6482.4, 6472.9, 6463.1, 6453.2, 6443.1, 6432.7, 6422.2, 6411.5, 6400.6, 6389.4, 6378.1, 6366.5, 6354.7, 6342.7, 6330.4, 6317.9, 6305.2, 6292.3, 6279.1, 6265.7, 6252.0, 6238.1, 6224.0, 6209.6, 6195.0, 6180.1, 6165.0, 6149.5, 6133.8, 6117.8, 6101.6, 6085.2, 6068.5, 6051.6, 6034.4, 6016.9, 5999.2, 5981.3, 5963.1, 5944.7, 5926.1, 5907.2, 5888.0, 5868.7, 5849.0, 5829.2, 5809.1, 5788.8, 5768.3, 5747.6, 5726.6, 5705.4, 5684.0, 5662.4, 5640.5, 5618.5, 5596.3, 5573.8, 5551.2, 5528.3, 5505.3, 5482.1, 5458.6, 5434.9, 5411.0, 5386.9, 5362.5, 5337.9, 5313.2, 5288.2, 5263.0, 5237.7, 5212.1, 5186.4, 5160.6, 5134.5, 5108.3, 5082.0, 5055.5, 5028.9, 5002.1, 4975.3, 4948.3, 4921.2, 4893.9, 4866.6, 4839.2, 4811.7, 4784.1, 4756.5, 4728.7, 4700.9, 4673.0, 4645.1, 4617.2, 4589.1, 4561.1, 4533.0, 4504.9, 4476.7, 4448.5, 4420.4, 4392.2, 4364.0, 4335.8, 4307.6, 4279.4, 4251.2, 4223.1, 4195.0, 4166.9, 4138.8, 4110.7, 4082.7, 4054.8, 4026.9, 3999.0, 3971.2, 3943.5, 3915.8, 3888.1, 3860.6, 3833.1, 3805.7, 3778.3, 3751.1, 3723.9, 3696.8, 3669.8, 3642.9, 3616.1, 3589.4, 3562.7, 3536.2, 3509.8, 3483.5, 3457.2, 3431.1, 3405.2, 3379.3, 3353.5, 3327.9, 3302.3, 3276.9, 3251.6, 3226.4, 3201.3, 3176.4, 3151.6, 3126.9, 3102.4, 3077.9, 3053.7, 3029.5, 3005.5, 2981.6, 2957.8, 2934.2, 2910.7, 2887.4, 2864.2, 2841.1, 2818.2, 2795.4, 2772.8, 2750.3, 2727.9, 2705.7, 2683.6, 2661.7, 2639.9, 2618.2, 2596.7, 2575.3, 2554.1, 2533.0, 2512.1, 2491.3, 2470.6, 2450.1, 2429.7, 2409.5, 2389.4, 2369.4, 2349.6, 2330.0, 2310.5, 2291.1, 2271.8, 2252.7, 2233.8, 2214.9, 2196.2, 2177.7, 2159.3, 2141.0, 2122.9, 2104.9, 2087.0, 2069.3, 2051.7, 2034.2, 2016.9, 1999.7, 1982.6, 1965.7, 1948.9, 1932.3, 1930.2, 7714.0, 7640.3, 7552.9, 7466.5, 7381.2, 7335.6, 11059.0, 11070.0, 10933.0, 10798.0, 10664.0, 10532.0, 10402.0, 10273.0, 10146.0, 10020.0, 9896.5, 9774.1, 9653.3, 9533.9, 9416.0, 9299.6, 9184.7, 9071.1, 8959.0, 8848.3, 8739.0, 8631.0, 8524.4, 8419.1, 8315.1, 8212.4, 8111.0, 8010.9, 7912.0, 7814.3, 7717.9, 7622.6, 7528.6, 7435.7, 7343.9, 7253.3, 7163.9, 7075.5, 6988.3, 6902.1, 6817.0, 6733.0, 6650.0, 6568.0, 6487.1, 6407.2, 6328.2, 6250.3, 6173.2, 6097.1, 6022.0, 5947.8, 5874.5, 5821.3, 6798.5, 6797.9, 6709.6, 6622.0, 6535.5, 6450.1, 6365.9, 6282.8, 6200.9, 6120.0, 6040.2, 5964.0, 5891.4, 5819.9, 5749.7, 5680.6, 5612.5, 5585.9, 5950.1, 5925.7, 5853.8, 5782.9, 5713.2, 5644.5, 5576.9, 5510.2, 5444.6, 5380.0, 5316.3, 5255.1, 5195.2, 5136.2, 5078.1, 5021.0, 4964.7, 4909.3, 4854.7, 4800.9, 4747.9, 4695.7, 4644.2, 4593.5, 4567.2, 4760.4, 4758.3, 4709.2, 4658.7, 4608.9, 4559.7, 4511.2, 4463.3, 4416.0, 4369.3, 4323.2, 4276.8, 4230.5, 4184.8, 4139.7, 4094.1, 4047.6, 4001.7, 3956.3, 3911.4, 3867.1, 3823.3, 3780.0, 3737.2, 3694.8, 3653.0, 3611.6, 3570.8, 3530.3, 3490.4, 3450.8, 3411.2, 3371.9, 3333.1, 3294.7, 3256.7, 3219.1, 3182.0, 3145.3, 3108.9, 3073.0, 3037.5, 3002.4, 2967.6, 2933.3, 2899.4, 2865.8, 2832.6, 2799.8, 2767.3, 2735.2, 2703.5, 2672.1, 2641.1, 2610.4, 2580.0, 2549.9, 2520.1, 2490.7, 2461.6, 2432.9, 2404.5, 2376.4, 2348.7, 2321.2, 2294.1, 2267.3, 2240.8, 2214.7, 2188.6, 2162.7, 2137.1, 2111.8, 2086.8, 2062.1, 2037.7, 2013.6, 1989.8, 1966.3, 1943.0, 1920.1, 1897.4, 1875.0, 1852.9, 1831.0, 1809.5, 1788.1, 1767.1, 1746.3, 1725.7, 1705.5, 1685.4, 1665.6, 1646.1, 1626.8, 1607.7, 1588.9, 1570.3, 1551.9, 1533.8, 1515.8, 1498.1, 1480.4, 1462.9, 1445.6, 1428.5, 1411.7, 1395.0, 1378.6, 1362.3, 1346.3, 1330.4, 1314.8, 1299.4, 1284.1, 1269.0, 1254.1, 1239.4, 1224.8, 1210.3, 1196.0, 1181.9, 1168.0, 1154.3, 1140.7, 1127.3, 1114.0, 1101.0, 1088.0, 1075.3, 1062.6, 1049.0, 1035.5, 1022.2, 1009.1, 996.2, 983.43, 970.84, 958.41, 946.15, 934.03, 922.03, 910.19, 898.51, 886.98, 875.6, 864.38, 853.3, 842.37, 831.58, 820.94, 810.43, 800.07, 789.84, 779.75, 769.79, 759.96, 750.27, 740.7, 731.26, 721.94, 712.75, 703.67, 694.72, 685.89, 677.17, 668.57, 660.08, 651.7, 643.43, 635.28, 627.23, 619.28, 611.45, 603.71, 596.08, 588.55, 581.11, 573.78, 566.54, 559.4, 552.35, 545.39, 538.53, 531.61, 524.76, 518.01, 511.35, 489.38, 411.02, 345.67, 291.16, 245.7, 207.61, 174.83, 172.46, 165.75, 164.03, 489.89, 467.96, 438.05, 415.41, 398.1, 393.69, 539.41, 516.72, 503.99, 489.98, 469.42, 464.31, 528.8, 509.29, 488.1, 413.14, 348.97, 293.61, 245.31, 205.03, 171.51, 143.61, 120.22, 100.38, 83.627, 69.532, 57.863, 48.194, 40.174, 33.513, 27.982, 23.386, 19.563, 16.381, 13.601, 11.254, 9.29, 7.6591, 6.3161, 5.2127, 4.3056, 4.0797, 3.9065, 3.862, 22.521, 21.685, 21.255, 17.874, 14.987, 12.554, 10.501, 8.774, 7.3302, 6.1236, 5.1153, 4.2671, 3.5491, 2.9413, 2.4379, 2.0208, 1.6752, 1.3889, 1.1515, 0.95486, 0.79183, 0.65668, 0.54464, 0.45175, 0.37514, 0.312, 0.2595, 0.21585, 0.17955, 0.14937, 0.12426, 0.10339, 0.086019, 0.071574, 0.059557, 0.049561, 0.041244, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.020678, 0.02083314, 0.0209945, 0.0210789, 0.0212055, 0.021522, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03675, 0.0373125, 0.0374625, 0.0376875, 0.03797993, 0.03825, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11748954, 0.11751046, 0.11751048, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22447692, 0.22472308, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24315743, 0.24344258, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3155152, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.97760223, 0.97779777, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 0.99936105, 0.99963891, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2965165, 1.2982423, 1.2982835, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4018615, 1.4037384, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5732678, 1.5770017, 1.5773321, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.083742, 6.176861, 6.201692, 6.23894, 6.332058, 6.469004, 6.58707, 6.687892, 6.714778, 6.755107, 6.85593, 6.915365, 6.98348, 7.09037, 7.118874, 7.16163, 7.26852, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 42.69752, 43.35106, 43.52533, 43.78675, 44.44028, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ne': {'mass_absorption_coefficient (cm2/g)': [250390.0, 251630.0, 255270.0, 260940.0, 266170.0, 270600.0, 273930.0, 275900.0, 276320.0, 275050.0, 272000.0, 267150.0, 260550.0, 252310.0, 242560.0, 240030.0, 237550.0, 236890.0, 241260.0, 239000.0, 237340.0, 226590.0, 215160.0, 203110.0, 190620.0, 177840.0, 164950.0, 152090.0, 139320.0, 126760.0, 114600.0, 103000.0, 92050.0, 81840.0, 72414.0, 63791.0, 55968.0, 48925.0, 42627.0, 37029.0, 32081.0, 27728.0, 23910.0, 20577.0, 17677.0, 15162.0, 12988.0, 11112.0, 9498.4, 8112.2, 6923.5, 5844.0, 4928.6, 4154.1, 3499.8, 2947.6, 2482.1, 2089.9, 1759.8, 1481.9, 1248.2, 1051.6, 886.32, 747.31, 677.32, 650.76, 643.92, 10215.0, 10143.0, 9881.3, 8733.9, 7500.6, 6357.9, 5366.6, 4516.3, 3793.1, 3182.1, 2665.1, 2223.4, 1854.5, 1546.4, 1289.2, 1074.5, 895.25, 745.72, 621.01, 517.03, 430.39, 356.85, 294.8, 242.97, 199.32, 163.53, 134.16, 110.07, 90.306, 74.093, 60.791, 49.879, 40.925, 33.58, 27.553, 22.608, 18.551, 15.192, 12.353, 10.045, 8.1677, 6.6416, 5.4007, 4.3917, 3.5712, 2.904, 2.3598, 1.9106, 1.5469, 1.2525, 1.0139, 0.82082, 0.66448, 0.53792, 0.43387, 0.34953, 0.28158, 0.22684, 0.18275, 0.14722, 0.1186, 0.095549, 0.076976, 0.062013, 0.049959, 0.040248, 0.032425, 0.026122, 0.021045, 0.016954, 0.013667, 0.011031, 0.0089035, 0.0071863, 0.0058003, 0.0046816, 0.0037787, 0.0030499, 0.0024617, 0.001987, 0.0016037, 0.0012945, 0.0010448, 0.00084331, 0.00068067, 0.0005494, 0.00044344, 0.00035792, 0.0002889, 0.00023318, 0.00018821, 0.00015191, 0.00012262, 9.897e-05, 7.9883e-05, 6.4478e-05, 0.0], - 'energies (keV)': [0.0183915, 0.018666, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.0441, 0.044775, 0.044955, 0.045225, 0.0459, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.849562, 0.8625655, 0.8660331, 0.8712345, 0.8739896, 0.884238, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Fr': {'mass_absorption_coefficient (cm2/g)': [258930.0, 257090.0, 247360.0, 209240.0, 170230.0, 138270.0, 112060.0, 91808.0, 90598.0, 87442.0, 86322.0, 207310.0, 195480.0, 172970.0, 132970.0, 103760.0, 82163.0, 65763.0, 53048.0, 43030.0, 35038.0, 28601.0, 28270.0, 27000.0, 26674.0, 28550.0, 27240.0, 25338.0, 20536.0, 16682.0, 13576.0, 11060.0, 9017.1, 7356.3, 6008.9, 4916.9, 4032.3, 3934.9, 3762.8, 3718.8, 32358.0, 34772.0, 38107.0, 44199.0, 47848.0, 48870.0, 81488.0, 86336.0, 87206.0, 107820.0, 115640.0, 104420.0, 82662.0, 59546.0, 40300.0, 26327.0, 16975.0, 10998.0, 7258.7, 4929.6, 4150.1, 3833.2, 3754.8, 5525.4, 5296.0, 5183.3, 4032.0, 3163.0, 2568.1, 2533.2, 2427.7, 2401.2, 3345.8, 3198.0, 2996.9, 2494.5, 2123.3, 1848.2, 1842.1, 1790.0, 1776.7, 1913.3, 1866.5, 1793.7, 1747.8, 1709.5, 1699.7, 1791.5, 1787.1, 1783.1, 1781.8, 1781.8, 1781.9, 1860.5, 1889.5, 2089.6, 2826.8, 4155.4, 6003.2, 8114.0, 10141.0, 11777.0, 12843.0, 13300.0, 13214.0, 12935.0, 12893.0, 12848.0, 12802.0, 12754.0, 12705.0, 12653.0, 12601.0, 12547.0, 12491.0, 12434.0, 12376.0, 12316.0, 12255.0, 12193.0, 12130.0, 12066.0, 12001.0, 11934.0, 11867.0, 11799.0, 11730.0, 11660.0, 11590.0, 11519.0, 11447.0, 11374.0, 11301.0, 11227.0, 11183.0, 11973.0, 11963.0, 11891.0, 11819.0, 11747.0, 11675.0, 11602.0, 11528.0, 11455.0, 11381.0, 11342.0, 11792.0, 11776.0, 11705.0, 11633.0, 11560.0, 11488.0, 11416.0, 11343.0, 11271.0, 11198.0, 11126.0, 11054.0, 10981.0, 10909.0, 10836.0, 10764.0, 10691.0, 10618.0, 10546.0, 10473.0, 10400.0, 10328.0, 10255.0, 10183.0, 10110.0, 10038.0, 9965.6, 9893.4, 9821.2, 9749.1, 9677.1, 9605.3, 9533.6, 9462.1, 9390.8, 9319.6, 9248.5, 9177.6, 9106.9, 9036.5, 8966.3, 8896.3, 8826.6, 8757.2, 8688.0, 8619.1, 8550.5, 8482.2, 8414.2, 8346.6, 8279.2, 8212.2, 8145.5, 8079.2, 8013.2, 7947.6, 7882.4, 7817.5, 7753.0, 7688.8, 7659.6, 8109.3, 8043.8, 7978.8, 7914.2, 7850.0, 7786.2, 7722.8, 7659.9, 7597.3, 7535.2, 7473.5, 7412.3, 7351.5, 7291.1, 7231.2, 7171.7, 7112.7, 7054.1, 6995.9, 6938.2, 6881.0, 6824.1, 6767.7, 6711.8, 6656.3, 6601.2, 6546.5, 6492.4, 6438.6, 6385.3, 6332.4, 6280.0, 6228.0, 6176.5, 6125.3, 6074.7, 6024.4, 5974.5, 5941.7, 6003.1, 5992.2, 5952.1, 5901.6, 5851.6, 5801.7, 5738.8, 5676.7, 5615.4, 5554.6, 5493.3, 5432.5, 5372.4, 5313.0, 5254.4, 5196.4, 5139.2, 5082.7, 5026.8, 4971.6, 4917.1, 4863.2, 4809.9, 4757.3, 4705.3, 4653.9, 4603.1, 4552.9, 4503.2, 4454.0, 4405.4, 4357.4, 4309.9, 4263.0, 4250.0, 4318.8, 4307.6, 4261.2, 4215.4, 4170.1, 4125.4, 4081.1, 4037.4, 3994.2, 3951.5, 3909.4, 3867.8, 3826.7, 3786.0, 3745.9, 3706.3, 3667.1, 3628.3, 3590.1, 3552.2, 3514.9, 3477.9, 3441.4, 3405.4, 3369.7, 3334.5, 3299.7, 3265.3, 3231.3, 3197.6, 3164.4, 3131.6, 3099.1, 3067.1, 3035.3, 3004.0, 2972.7, 2941.7, 2911.1, 2880.9, 2851.0, 2821.5, 2792.2, 2763.4, 2734.8, 2706.6, 2678.7, 2651.1, 2623.9, 2596.9, 2570.3, 2543.9, 2517.9, 2492.1, 2466.6, 2441.3, 2416.3, 2391.6, 2367.2, 2343.1, 2319.2, 2295.6, 2272.3, 2249.2, 2226.4, 2203.8, 2181.5, 2159.5, 2137.6, 2116.1, 2094.7, 2073.7, 2052.8, 2032.2, 2011.7, 1991.4, 1971.3, 1951.4, 1931.8, 1912.4, 1893.2, 1874.2, 1855.4, 1836.8, 1818.4, 1800.2, 1782.3, 1764.5, 1746.9, 1729.5, 1712.3, 1695.3, 1678.4, 1661.8, 1645.3, 1629.0, 1612.9, 1597.0, 1581.2, 1565.6, 1550.2, 1534.9, 1519.9, 1504.9, 1490.2, 1475.5, 1461.1, 1446.8, 1432.7, 1418.7, 1404.8, 1391.1, 1377.6, 1364.2, 1351.0, 1337.8, 1324.9, 1312.0, 1299.3, 1286.8, 1274.4, 1262.1, 1249.9, 1237.9, 1226.0, 1214.2, 1202.5, 1191.0, 1179.6, 1168.3, 1157.2, 1146.1, 1135.2, 1124.4, 1113.7, 1103.1, 1092.6, 1082.2, 1072.0, 1061.8, 1051.5, 1040.9, 1030.4, 1020.0, 1009.7, 999.48, 989.41, 979.46, 969.6, 959.69, 949.62, 939.67, 929.83, 920.09, 910.47, 900.95, 891.53, 882.2, 872.98, 863.86, 854.84, 845.92, 837.1, 828.38, 819.76, 811.23, 802.8, 794.46, 786.21, 778.06, 770.0, 762.02, 754.14, 746.34, 738.63, 731.0, 723.46, 716.0, 708.62, 701.33, 694.12, 686.98, 679.92, 672.95, 666.04, 659.22, 652.47, 645.79, 639.19, 632.66, 626.2, 619.81, 613.49, 612.43, 1622.8, 1607.3, 1585.9, 1564.7, 1543.9, 1523.3, 1503.0, 1483.0, 1463.3, 1443.9, 1442.2, 2079.5, 2057.6, 2030.2, 2003.2, 1976.5, 1950.2, 1924.2, 1898.6, 1873.2, 1848.2, 1823.6, 1799.2, 1775.2, 1751.5, 1728.2, 1705.1, 1682.4, 1660.0, 1637.9, 1616.1, 1594.5, 1573.3, 1552.4, 1531.7, 1511.3, 1491.2, 1471.4, 1451.8, 1432.6, 1413.5, 1394.7, 1376.2, 1375.5, 1614.9, 1604.3, 1582.4, 1560.8, 1539.4, 1518.4, 1497.6, 1477.2, 1457.0, 1437.1, 1417.6, 1399.2, 1381.0, 1363.1, 1345.4, 1327.8, 1310.6, 1293.6, 1276.8, 1260.4, 1244.1, 1228.2, 1212.4, 1196.7, 1181.3, 1166.1, 1151.2, 1136.4, 1121.9, 1107.6, 1093.4, 1079.5, 1065.8, 1052.3, 1047.0, 1106.6, 1106.2, 1092.6, 1079.2, 1066.0, 1053.0, 1040.1, 1027.4, 1014.9, 1002.5, 990.32, 978.26, 966.36, 954.62, 943.02, 934.01, 931.56, 965.53, 961.15, 949.73, 938.45, 927.31, 916.28, 905.18, 894.22, 883.39, 872.69, 862.13, 851.68, 841.36, 831.17, 821.1, 811.15, 801.32, 791.6, 782.01, 772.53, 763.17, 753.92, 744.78, 735.76, 726.85, 718.05, 709.35, 700.76, 692.28, 683.9, 675.63, 667.45, 659.38, 651.41, 643.53, 635.75, 628.07, 620.49, 612.99, 605.55, 598.18, 590.9, 583.72, 576.62, 569.61, 562.68, 555.85, 549.09, 542.42, 535.84, 529.33, 522.91, 516.48, 510.1, 503.79, 497.56, 491.41, 485.33, 479.34, 473.42, 467.58, 461.75, 455.99, 450.3, 444.69, 439.15, 433.68, 428.29, 422.96, 417.7, 412.51, 407.38, 402.33, 397.34, 392.41, 387.55, 382.75, 378.01, 373.33, 368.72, 364.16, 359.67, 355.23, 350.85, 346.53, 342.26, 338.05, 333.89, 329.79, 325.74, 321.75, 317.8, 313.88, 309.99, 306.15, 302.37, 298.63, 294.94, 291.3, 287.71, 284.16, 280.66, 277.21, 273.8, 270.43, 267.11, 263.83, 260.6, 257.4, 254.25, 251.14, 248.07, 245.03, 242.04, 239.09, 236.17, 233.29, 230.45, 227.65, 224.88, 222.15, 219.45, 216.79, 188.41, 157.6, 132.0, 110.71, 92.914, 78.095, 65.646, 55.128, 52.034, 50.026, 49.51, 123.16, 118.48, 117.09, 97.977, 82.32, 81.666, 78.968, 78.105, 108.61, 104.41, 100.3, 99.236, 113.05, 111.82, 108.91, 94.398, 79.518, 66.988, 56.436, 47.555, 39.84, 33.335, 27.842, 23.276, 19.463, 16.223, 13.522, 11.278, 9.4146, 7.8647, 6.5749, 5.5007, 4.6053, 3.8553, 3.2258, 2.7015, 2.2546, 1.8662, 1.5459, 1.3048, 1.2815, 1.2504, 1.2364, 5.6165, 5.4082, 4.9688, 4.1864, 3.5229, 2.9629, 2.4915, 2.0932, 1.7574, 1.4754, 1.2388, 1.0402, 0.8735, 0.73385, 0.61664, 0.51821, 0.43554, 0.3661, 0.30777, 0.25875, 0.21756, 0.18295, 0.15385, 0.1294, 0.0], - 'energies (keV)': [0.01066532, 0.01069, 0.0108245, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01486167, 0.01492335, 0.01508915, 0.01514981, 0.0152408, 0.01546827, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02731051, 0.02772853, 0.02784, 0.02800721, 0.02842523, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05346183, 0.05428013, 0.05449834, 0.05482565, 0.05564395, 0.05667876, 0.05834708, 0.05924014, 0.05947829, 0.05983552, 0.06058959, 0.06072859, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1302983, 0.1322926, 0.1328244, 0.1336222, 0.1349368, 0.1356165, 0.1442475, 0.1542005, 0.1648404, 0.1656291, 0.1681643, 0.1688403, 0.1698543, 0.1723895, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2156348, 0.2189353, 0.2198155, 0.2211357, 0.2244362, 0.2301188, 0.2340857, 0.2376687, 0.2386241, 0.2400573, 0.2415586, 0.2436403, 0.245256, 0.245997, 0.2462419, 0.2477208, 0.2514182, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57662439, 0.57737565, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60290602, 0.60369393, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.8089146, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.97859862, 0.98036623, 0.98140142, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1516164, 1.1543836, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 2.9988482, 3.000552, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.135234, 3.1371659, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6585679, 3.6674323, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3208125, 4.3331878, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6423238, 4.6472882, 4.6616761, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 14.73058, 14.95604, 15.01617, 15.10636, 15.33182, 15.40095, 16.46362, 17.54837, 17.59961, 17.81697, 17.88859, 17.99603, 18.26622, 18.5458, 18.62036, 18.73219, 18.81398, 19.01178, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.11426, 99.75239, 100.6313, 101.0359, 101.6427, 103.1597, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Fe': {'mass_absorption_coefficient (cm2/g)': [100370.0, 101300.0, 102340.0, 103460.0, 104640.0, 105830.0, 106910.0, 107850.0, 108610.0, 109170.0, 109490.0, 109560.0, 109370.0, 108910.0, 108180.0, 107180.0, 105920.0, 104270.0, 102160.0, 99741.0, 97039.0, 94080.0, 90896.0, 87520.0, 84092.0, 83990.0, 83272.0, 83054.0, 111210.0, 108240.0, 102960.0, 92554.0, 84927.0, 77903.0, 70070.0, 63163.0, 57003.0, 51487.0, 50954.0, 49795.0, 49494.0, 52465.0, 51383.0, 49973.0, 45571.0, 41626.0, 38081.0, 34893.0, 32018.0, 29413.0, 27037.0, 24853.0, 22831.0, 20946.0, 19178.0, 17513.0, 15938.0, 14442.0, 13030.0, 11704.0, 10468.0, 9325.7, 8277.0, 7321.3, 6455.9, 5676.8, 4979.4, 4358.3, 3807.5, 3321.1, 2893.0, 2517.2, 2188.2, 2027.4, 1963.4, 1951.0, 1946.8, 12343.0, 12180.0, 12093.0, 11973.0, 16996.0, 16853.0, 16240.0, 14728.0, 12470.0, 12043.0, 11599.0, 11485.0, 12901.0, 12461.0, 12098.0, 10351.0, 8856.6, 7541.9, 6418.8, 5453.7, 4600.1, 3880.1, 3267.6, 2748.7, 2311.9, 1944.2, 1635.1, 1375.5, 1157.6, 974.76, 821.25, 692.39, 583.2, 486.11, 402.95, 333.27, 275.51, 227.98, 188.83, 156.56, 129.93, 107.93, 89.748, 74.699, 62.234, 51.899, 50.807, 48.757, 48.23, 411.05, 393.42, 372.05, 311.31, 262.89, 221.49, 185.01, 154.39, 128.85, 107.54, 89.77, 74.939, 62.448, 51.676, 42.751, 35.358, 29.245, 24.191, 20.012, 16.556, 13.697, 11.333, 9.333, 7.6746, 6.2905, 5.145, 4.2083, 3.4423, 2.8158, 2.3032, 1.884, 1.5411, 1.2607, 1.0313, 0.84372, 0.69026, 0.56472, 0.46203, 0.37736, 0.30699, 0.24975, 0.20319, 0.16531, 0.1345, 0.10943, 0.089031, 0.072438, 0.058938, 0.047955, 0.039019, 0.031749, 0.025833, 0.02102, 0.017104, 0.013918, 0.011325, 0.0092156, 0.0074991, 0.0061023, 0.0049658, 0.0040409, 0.0032883, 0.002676, 0.0021776, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05292, 0.05302035, 0.05373, 0.053946, 0.05427, 0.05508, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.091042, 0.0924355, 0.0928071, 0.0933645, 0.094758, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.693938, 0.7045595, 0.706678, 0.7073919, 0.7116405, 0.7154399, 0.7174945, 0.7203789, 0.722262, 0.7247055, 0.735522, 0.7648052, 0.8175768, 0.829178, 0.8418695, 0.8452539, 0.8503305, 0.863022, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 6.96976, 7.07644, 7.104888, 7.14756, 7.25424, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'B': {'mass_absorption_coefficient (cm2/g)': [965910.0, 937690.0, 232720.0, 206500.0, 182670.0, 161080.0, 157790.0, 153250.0, 152070.0, 189340.0, 186480.0, 183580.0, 173290.0, 164970.0, 157460.0, 150370.0, 143350.0, 136200.0, 128820.0, 121210.0, 113420.0, 105530.0, 97639.0, 89854.0, 82270.0, 74970.0, 68019.0, 61464.0, 55338.0, 49655.0, 44421.0, 39627.0, 35255.0, 31276.0, 27673.0, 24428.0, 21515.0, 18912.0, 16592.0, 14520.0, 12670.0, 11040.0, 9606.0, 8347.5, 7244.8, 6280.4, 5438.3, 4704.1, 4064.9, 3509.3, 3179.7, 3074.3, 3047.0, 81883.0, 81378.0, 78936.0, 71382.0, 62155.0, 53819.0, 46354.0, 39731.0, 33914.0, 28848.0, 24464.0, 20692.0, 17460.0, 14702.0, 12356.0, 10366.0, 8683.2, 7262.8, 6066.3, 5060.4, 4216.0, 3508.3, 2916.1, 2421.1, 2007.6, 1663.0, 1376.3, 1138.4, 933.38, 765.2, 627.32, 514.28, 421.62, 345.65, 283.37, 232.31, 190.45, 156.14, 128.01, 104.94, 86.034, 70.532, 57.824, 47.406, 38.702, 31.473, 25.583, 20.693, 16.737, 13.538, 10.95, 8.8568, 7.1638, 5.7945, 4.6868, 3.7902, 3.0649, 2.4785, 2.0042, 1.6207, 1.3087, 1.0512, 0.84441, 0.67829, 0.54485, 0.43766, 0.35156, 0.2824, 0.22684, 0.18221, 0.14637, 0.11757, 0.094443, 0.075864, 0.06094, 0.048951, 0.039321, 0.031514, 0.025239, 0.020213, 0.016188, 0.012965, 0.010383, 0.0083155, 0.0066597, 0.0053336, 0.0042715, 0.003421, 0.0027398, 0.0021942, 0.0017573, 0.0014074, 0.0011272, 0.0009045, 0.00072902, 0.00058758, 0.00047359, 0.00038171, 0.00030765, 0.00024797, 0.00019986, 0.00016108, 0.00012983, 0.00010464, 8.4342e-05, 6.7979e-05, 5.479e-05, 4.4161e-05, 3.5593e-05, 2.8688e-05, 2.3122e-05, 1.8636e-05, 1.5021e-05, 1.2107e-05, 9.7578e-06, 7.8647e-06, 6.3389e-06, 5.1091e-06, 4.1179e-06, 0.0], - 'energies (keV)': [0.0047235, 0.004794, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0132006, 0.01340265, 0.01345653, 0.01353735, 0.0137394, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.18424, 0.18706, 0.187812, 0.1883732, 0.18894, 0.19176, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'F': {'mass_absorption_coefficient (cm2/g)': [545850.0, 546560.0, 548490.0, 542100.0, 532700.0, 520870.0, 507110.0, 491860.0, 475460.0, 458220.0, 440350.0, 422050.0, 403450.0, 384680.0, 365820.0, 346960.0, 328160.0, 309480.0, 297360.0, 293160.0, 292060.0, 299340.0, 298810.0, 295100.0, 282800.0, 266810.0, 250940.0, 235190.0, 219630.0, 204310.0, 189300.0, 174680.0, 160530.0, 146900.0, 133880.0, 121510.0, 109850.0, 98922.0, 88755.0, 79355.0, 70717.0, 62825.0, 55655.0, 49173.0, 43341.0, 38109.0, 33434.0, 29274.0, 25448.0, 21812.0, 18660.0, 15938.0, 13594.0, 11580.0, 9854.1, 8378.3, 7118.5, 6044.9, 5131.1, 4354.1, 3694.2, 3134.2, 2659.1, 2256.3, 1914.9, 1625.7, 1380.6, 1172.1, 988.52, 833.22, 825.51, 793.95, 785.82, 13724.0, 13244.0, 12529.0, 10673.0, 9080.5, 7712.4, 6541.2, 5542.4, 4675.7, 3940.7, 3299.0, 2754.7, 2298.5, 1916.7, 1597.4, 1330.6, 1108.0, 922.31, 767.55, 638.62, 531.27, 441.92, 367.58, 303.96, 250.05, 204.87, 167.85, 137.53, 112.68, 92.321, 75.642, 61.977, 50.782, 41.609, 34.093, 27.936, 22.89, 18.757, 15.369, 12.594, 10.301, 8.3693, 6.8001, 5.5252, 4.4893, 3.6477, 2.955, 2.3905, 1.9337, 1.5641, 1.2652, 1.0234, 0.82787, 0.66967, 0.5417, 0.43819, 0.35446, 0.28559, 0.22981, 0.18492, 0.1488, 0.11974, 0.096351, 0.077532, 0.062389, 0.050203, 0.040398, 0.032508, 0.026159, 0.02105, 0.016939, 0.01363, 0.010968, 0.008827, 0.007105, 0.005719, 0.0046033, 0.0037053, 0.0029825, 0.0024007, 0.0019324, 0.0015554, 0.001252, 0.0010078, 0.00081119, 0.00065295, 0.00052558, 0.00042306, 0.00034053, 0.00027411, 0.00022064, 0.0001776, 0.00014296, 0.00011507, 9.2625e-05, 7.4558e-05, 6.0014e-05, 4.8308e-05, 3.8885e-05, 0.0], - 'energies (keV)': [0.008643, 0.008772, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03038, 0.030845, 0.030969, 0.03109002, 0.031155, 0.03162, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.671692, 0.681973, 0.6847146, 0.688827, 0.699108, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Sr': {'mass_absorption_coefficient (cm2/g)': [449530.0, 425200.0, 385630.0, 299840.0, 226370.0, 165710.0, 118950.0, 84538.0, 59956.0, 42709.0, 30716.0, 25473.0, 23721.0, 23281.0, 23186.0, 22940.0, 21720.0, 17271.0, 13356.0, 10626.0, 8701.4, 7325.5, 6324.6, 5580.6, 5013.5, 4569.0, 4210.4, 3912.8, 3660.2, 3441.6, 3248.1, 3073.6, 2913.7, 2765.5, 2626.5, 2561.1, 2533.3, 2531.3, 2523.5, 4477.7, 4542.9, 4616.9, 6030.7, 6049.4, 6492.4, 8425.9, 12730.0, 18150.0, 23927.0, 29232.0, 33377.0, 35969.0, 36940.0, 36464.0, 34858.0, 34770.0, 34276.0, 34140.0, 36746.0, 36261.0, 36224.0, 35699.0, 35546.0, 36503.0, 36492.0, 35915.0, 33776.0, 30827.0, 27819.0, 26928.0, 26257.0, 26081.0, 26955.0, 26298.0, 26003.0, 23163.0, 20528.0, 18117.0, 15935.0, 13976.0, 12226.0, 10667.0, 9283.8, 8063.1, 6989.8, 6049.1, 5227.4, 4511.5, 3889.4, 3349.8, 2847.4, 2406.2, 2029.8, 1714.7, 1450.7, 1228.2, 1040.7, 883.1, 750.36, 676.05, 651.69, 645.41, 2789.0, 2777.8, 2711.3, 2667.7, 2601.2, 2572.8, 3552.3, 3413.2, 3268.2, 2921.3, 2808.7, 2779.7, 3159.9, 3149.0, 3034.6, 2678.1, 2275.8, 1929.1, 1634.5, 1377.6, 1156.9, 972.35, 817.94, 685.57, 572.5, 477.39, 397.92, 331.75, 276.64, 230.9, 192.9, 161.27, 134.91, 112.96, 94.649, 78.87, 65.108, 53.571, 44.12, 36.37, 30.011, 24.787, 20.491, 16.956, 15.822, 15.157, 14.987, 110.22, 105.47, 104.77, 86.799, 72.853, 61.375, 51.601, 43.175, 36.083, 30.142, 25.091, 20.864, 17.351, 14.34, 11.848, 9.7903, 8.0901, 6.6849, 5.5233, 4.5612, 3.767, 3.1112, 2.5698, 2.1227, 1.7506, 1.4393, 1.1824, 0.96916, 0.79445, 0.65126, 0.5339, 0.43771, 0.35886, 0.29423, 0.24125, 0.19782, 0.16221, 0.13302, 0.10908, 0.089454, 0.073362, 0.060167, 0.049347, 0.040474, 0.033197, 0.027229, 0.022335, 0.018321, 0.015029, 0.012329, 0.010114, 0.0082974, 0.0], - 'energies (keV)': [0.0199995, 0.020298, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.036946, 0.0375115, 0.0376623, 0.0378885, 0.03797993, 0.038454, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.130438, 0.1323, 0.1324345, 0.1329669, 0.1337655, 0.134325, 0.1349368, 0.135675, 0.135762, 0.1377, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.263718, 0.2677545, 0.2688309, 0.2704455, 0.274204, 0.274482, 0.278401, 0.2795202, 0.2811158, 0.281199, 0.285396, 0.3005128, 0.3212482, 0.3434143, 0.35035, 0.3557125, 0.3571425, 0.3592875, 0.36465, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.900808, 1.929902, 1.93766, 1.94643, 1.949298, 1.966664, 1.978392, 1.996766, 2.004793, 2.016834, 2.046936, 2.080733, 2.171974, 2.205219, 2.214084, 2.224304, 2.227382, 2.260626, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 15.78251, 16.02408, 16.0885, 16.18512, 16.42669, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'N': {'mass_absorption_coefficient (cm2/g)': [741310.0, 732860.0, 662550.0, 628910.0, 596430.0, 564860.0, 534020.0, 503770.0, 474030.0, 444790.0, 416030.0, 387820.0, 360190.0, 333250.0, 307080.0, 281780.0, 276950.0, 271370.0, 269900.0, 285170.0, 280460.0, 276180.0, 256290.0, 237700.0, 220020.0, 203170.0, 187060.0, 171530.0, 156630.0, 142410.0, 128920.0, 116220.0, 104340.0, 93299.0, 83116.0, 73783.0, 65282.0, 57584.0, 50650.0, 44437.0, 38894.0, 33969.0, 29607.0, 25749.0, 22349.0, 19363.0, 16748.0, 14466.0, 12477.0, 10729.0, 9175.6, 7839.8, 6693.3, 5710.5, 4869.1, 4149.5, 3534.7, 3009.8, 2562.1, 2180.4, 1855.1, 1578.2, 1567.3, 1510.6, 1496.0, 30008.0, 29062.0, 27602.0, 23891.0, 20592.0, 17651.0, 15058.0, 12796.0, 10837.0, 9152.1, 7709.9, 6480.8, 5437.2, 4553.8, 3808.0, 3179.8, 2649.6, 2204.7, 1832.3, 1521.2, 1261.9, 1046.1, 866.68, 714.38, 588.46, 484.73, 399.29, 328.92, 270.95, 223.2, 183.87, 151.47, 124.13, 101.23, 82.553, 67.324, 54.905, 44.776, 36.516, 29.78, 24.287, 19.807, 16.153, 13.174, 10.744, 8.762, 7.1183, 5.7735, 4.6775, 3.7741, 3.0452, 2.4571, 1.9825, 1.5995, 1.2905, 1.0411, 0.83998, 0.67769, 0.54676, 0.44112, 0.35589, 0.28713, 0.23166, 0.1869, 0.15079, 0.1212, 0.097291, 0.0781, 0.062695, 0.050328, 0.040401, 0.032432, 0.026035, 0.0209, 0.016777, 0.013468, 0.010812, 0.008679, 0.0069672, 0.005593, 0.0044898, 0.0036038, 0.0028918, 0.0023205, 0.0018621, 0.0014942, 0.001199, 0.00096212, 0.00077205, 0.00061953, 0.00049713, 0.00039892, 0.00032011, 0.00025688, 0.00020613, 0.00016541, 0.00013273, 0.00010651, 8.547e-05, 6.8586e-05, 5.5037e-05, 4.4165e-05, 3.5441e-05, 2.844e-05, 2.2822e-05, 1.8313e-05, 1.4696e-05, 0.0], - 'energies (keV)': [0.009246, 0.009384, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.0257838, 0.02617845, 0.02628369, 0.02644155, 0.0268362, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.393568, 0.399592, 0.4011984, 0.403608, 0.409632, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Kr': {'mass_absorption_coefficient (cm2/g)': [2312.4, 2579.6, 2647.6, 4746.7, 8387.9, 13269.0, 19054.0, 25187.0, 31027.0, 36001.0, 39709.0, 41968.0, 42790.0, 42333.0, 40838.0, 39566.0, 39019.0, 38870.0, 41125.0, 41059.0, 40556.0, 39978.0, 39822.0, 40609.0, 40034.0, 39518.0, 36724.0, 33772.0, 30777.0, 30548.0, 29872.0, 29694.0, 30706.0, 30045.0, 29091.0, 26229.0, 23521.0, 20988.0, 18633.0, 16469.0, 14499.0, 12721.0, 11127.0, 9706.0, 8446.1, 7333.9, 6355.9, 5498.7, 4749.8, 4097.2, 3529.7, 3304.5, 3267.6, 3231.1, 3194.9, 3159.1, 3123.8, 3088.8, 3054.1, 3019.9, 2986.0, 2952.5, 2919.4, 2886.6, 2854.2, 2822.1, 2790.4, 2759.0, 2728.0, 2697.4, 2667.0, 2637.1, 2607.4, 2574.5, 2541.6, 2509.1, 2477.0, 2445.4, 2414.2, 2383.4, 2353.0, 2323.0, 2293.4, 2264.2, 2235.4, 2207.0, 2179.0, 2151.3, 2124.0, 2097.0, 2070.5, 2044.2, 2018.3, 1992.8, 1967.6, 1942.7, 1918.2, 1893.9, 1870.0, 1846.5, 1823.2, 1800.2, 1777.3, 1754.7, 1732.5, 1710.5, 1688.8, 1667.4, 1646.3, 1625.5, 1604.9, 1584.6, 1564.6, 1544.8, 1525.3, 1506.1, 1487.1, 1468.4, 1449.9, 1431.7, 1413.6, 1395.9, 1378.3, 1361.0, 1344.0, 1327.1, 1310.5, 1294.1, 1277.9, 1261.9, 1246.1, 1230.5, 1215.1, 1200.0, 1185.0, 1170.2, 1155.6, 1141.2, 1127.0, 1113.0, 1099.2, 1085.5, 1072.0, 1058.7, 1045.6, 1032.6, 1019.8, 1007.2, 994.72, 982.42, 970.27, 958.28, 946.44, 934.76, 923.23, 911.84, 900.6, 889.51, 878.56, 867.75, 857.08, 846.55, 836.15, 825.88, 815.75, 805.75, 795.87, 786.12, 776.5, 767.0, 757.62, 748.36, 739.22, 730.19, 721.28, 712.44, 707.83, 3414.7, 3395.0, 3351.0, 3307.5, 3264.7, 3222.4, 3180.6, 3152.8, 4447.1, 4431.2, 4373.9, 4317.4, 4261.7, 4206.7, 4152.4, 4098.8, 4045.9, 3993.7, 3942.1, 3891.3, 3841.1, 3791.5, 3742.6, 3694.4, 3646.7, 3599.7, 3553.3, 3507.5, 3462.3, 3417.7, 3379.8, 3373.7, 3870.2, 3830.3, 3782.4, 3735.0, 3688.2, 3642.1, 3596.5, 3551.5, 3507.0, 3463.2, 3420.1, 3377.9, 3336.3, 3295.3, 3254.8, 3214.9, 3175.5, 3136.7, 3098.3, 3060.5, 3023.1, 2986.3, 2949.9, 2914.0, 2878.6, 2843.6, 2808.7, 2774.2, 2740.2, 2706.6, 2673.4, 2640.7, 2608.2, 2576.0, 2544.3, 2512.9, 2482.0, 2451.4, 2421.2, 2391.4, 2362.0, 2332.9, 2304.2, 2275.8, 2247.8, 2220.1, 2192.8, 2165.8, 2139.1, 2112.8, 2086.8, 2061.1, 2035.7, 2010.6, 1985.6, 1960.9, 1936.6, 1912.5, 1888.7, 1865.3, 1842.1, 1819.2, 1796.6, 1774.2, 1752.2, 1730.4, 1708.8, 1687.6, 1666.6, 1645.9, 1625.4, 1605.2, 1585.2, 1565.5, 1546.0, 1526.8, 1507.8, 1489.1, 1470.6, 1452.3, 1434.2, 1416.4, 1398.8, 1381.5, 1364.3, 1347.4, 1330.6, 1314.1, 1297.8, 1281.7, 1265.4, 1249.0, 1232.8, 1216.8, 1201.0, 1185.5, 1170.1, 1154.9, 1140.0, 1125.2, 1110.6, 1096.3, 1082.1, 1068.1, 1054.2, 1040.6, 1027.1, 1013.9, 1000.7, 987.81, 975.04, 962.44, 949.84, 937.21, 924.76, 912.49, 900.38, 888.43, 876.59, 864.79, 853.15, 841.68, 830.37, 819.21, 808.21, 797.36, 786.66, 776.11, 765.71, 755.45, 745.34, 735.36, 725.52, 715.82, 706.26, 696.82, 687.52, 678.35, 669.3, 660.37, 651.4, 642.55, 633.82, 625.21, 616.73, 608.36, 600.11, 591.97, 583.94, 575.99, 568.15, 560.42, 552.8, 545.28, 537.87, 530.56, 523.36, 516.23, 509.21, 502.28, 495.45, 488.72, 482.08, 475.53, 469.07, 462.71, 456.43, 450.24, 444.13, 438.11, 432.18, 426.32, 420.55, 414.86, 409.25, 403.71, 398.25, 392.87, 387.56, 382.33, 377.17, 372.08, 367.06, 362.11, 357.23, 352.41, 347.67, 342.98, 338.37, 333.81, 329.32, 324.9, 320.53, 316.22, 311.98, 307.79, 303.66, 299.58, 295.56, 291.6, 287.69, 283.84, 280.04, 276.29, 272.59, 268.95, 265.35, 261.8, 258.3, 254.84, 251.43, 248.07, 244.75, 241.48, 238.25, 235.06, 231.92, 228.83, 225.77, 222.76, 219.79, 216.86, 213.96, 211.11, 208.3, 205.53, 202.79, 200.09, 197.43, 194.81, 192.22, 189.67, 187.15, 184.66, 182.21, 179.79, 177.41, 175.06, 172.74, 170.45, 168.19, 165.97, 163.77, 161.61, 159.47, 157.36, 155.28, 153.23, 134.48, 112.58, 93.918, 78.347, 65.009, 53.402, 43.91, 36.14, 29.774, 24.553, 20.267, 18.029, 17.263, 17.067, 130.34, 124.89, 107.23, 89.793, 75.63, 63.738, 53.337, 44.563, 37.194, 31.03, 25.89, 21.533, 17.818, 14.72, 12.162, 10.048, 8.3016, 6.8575, 5.6625, 4.6761, 3.8617, 3.1894, 2.6343, 2.1746, 1.7862, 1.4673, 1.2053, 0.98918, 0.80991, 0.66316, 0.54302, 0.44466, 0.36414, 0.29821, 0.24422, 0.20002, 0.16382, 0.13418, 0.10991, 0.090025, 0.073744, 0.060409, 0.049487, 0.040541, 0.033214, 0.027211, 0.022294, 0.018266, 0.014966, 0.012263, 0.010048, 0.0082338, 0.0067471, 0.0], - 'energies (keV)': [0.0893445, 0.09041995, 0.090678, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.209524, 0.212731, 0.2135862, 0.214869, 0.2152655, 0.218246, 0.2215865, 0.2224773, 0.2238135, 0.227154, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.2825634, 0.2868883, 0.2880417, 0.2897716, 0.2940966, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9, 0.9045, 0.9090225, 0.91356761, 0.91813545, 0.92272613, 0.92733976, 0.93197646, 0.93663634, 0.94131952, 0.94602612, 0.95075625, 0.95551003, 0.96028758, 0.96508902, 0.96991446, 0.97476404, 0.97963786, 0.98453605, 0.98945873, 0.99440602, 0.99937805, 1.0043749, 1.0093968, 1.0144438, 1.019516, 1.0246136, 1.0297367, 1.0348853, 1.0400598, 1.0452601, 1.0504864, 1.0557388, 1.0610175, 1.0663226, 1.0716542, 1.0770125, 1.0823975, 1.0878095, 1.0932486, 1.0987148, 1.1042084, 1.1097294, 1.1152781, 1.1208545, 1.1264587, 1.132091, 1.1377515, 1.1434402, 1.1491574, 1.1549032, 1.1606777, 1.1664811, 1.1723135, 1.1781751, 1.184066, 1.1899863, 1.1959362, 1.2019159, 1.2079255, 1.2139651, 1.220035, 1.2261351, 1.2322658, 1.2384271, 1.2446193, 1.2508424, 1.2570966, 1.2633821, 1.269699, 1.2760475, 1.2824277, 1.2888399, 1.295284, 1.3017605, 1.3082693, 1.3148106, 1.3213847, 1.3279916, 1.3346316, 1.3413047, 1.3480112, 1.3547513, 1.361525, 1.3683327, 1.3751743, 1.3820502, 1.3889605, 1.3959053, 1.4028848, 1.4098992, 1.4169487, 1.4240335, 1.4311536, 1.4383094, 1.4455009, 1.4527284, 1.4599921, 1.467292, 1.4746285, 1.4820016, 1.4894117, 1.4968587, 1.504343, 1.5118647, 1.519424, 1.5270212, 1.5346563, 1.5423295, 1.5500412, 1.5577914, 1.5655804, 1.5734083, 1.5812753, 1.5891817, 1.5971276, 1.6051132, 1.6131388, 1.6212045, 1.6293105, 1.6374571, 1.6456443, 1.6538726, 1.6621419, 1.6704526, 1.6747007, 1.6750994, 1.6788049, 1.6871989, 1.6956349, 1.7041131, 1.7126337, 1.7211968, 1.7269841, 1.7274159, 1.7298028, 1.7384518, 1.7471441, 1.7558798, 1.7646592, 1.7734825, 1.7823499, 1.7912617, 1.800218, 1.8092191, 1.8182652, 1.8273565, 1.8364933, 1.8456757, 1.8549041, 1.8641786, 1.8734995, 1.882867, 1.8922814, 1.9017428, 1.9112515, 1.9194747, 1.9208077, 1.9225253, 1.9304118, 1.9400638, 1.9497642, 1.959513, 1.9693105, 1.9791571, 1.9890529, 1.9989981, 2.0089931, 2.0190381, 2.0291333, 2.039279, 2.0494754, 2.0597227, 2.0700213, 2.0803714, 2.0907733, 2.1012272, 2.1117333, 2.122292, 2.1329034, 2.143568, 2.1542858, 2.1650572, 2.1758825, 2.1867619, 2.1976957, 2.2086842, 2.2197276, 2.2308263, 2.2419804, 2.2531903, 2.2644562, 2.2757785, 2.2871574, 2.2985932, 2.3100862, 2.3216366, 2.3332448, 2.344911, 2.3566356, 2.3684187, 2.3802608, 2.3921621, 2.404123, 2.4161436, 2.4282243, 2.4403654, 2.4525672, 2.4648301, 2.4771542, 2.48954, 2.5019877, 2.5144976, 2.5270701, 2.5397055, 2.552404, 2.565166, 2.5779919, 2.5908818, 2.6038362, 2.6168554, 2.6299397, 2.6430894, 2.6563048, 2.6695863, 2.6829343, 2.6963489, 2.7098307, 2.7233798, 2.7369967, 2.7506817, 2.7644351, 2.7782573, 2.7921486, 2.8061093, 2.8201399, 2.8342406, 2.8484118, 2.8626539, 2.8769671, 2.891352, 2.9058087, 2.9203378, 2.9349394, 2.9496141, 2.9643622, 2.979184, 2.9940799, 3.0090503, 3.0240956, 3.0392161, 3.0544122, 3.0696842, 3.0850326, 3.1004578, 3.1159601, 3.1315399, 3.1471976, 3.1629336, 3.1787482, 3.194642, 3.2106152, 3.2266683, 3.2428016, 3.2590156, 3.2753107, 3.2916873, 3.3081457, 3.3246864, 3.3413099, 3.3580164, 3.3748065, 3.3916805, 3.4086389, 3.4256821, 3.4428105, 3.4600246, 3.4773247, 3.4947113, 3.5121849, 3.5297458, 3.5473945, 3.5651315, 3.5829572, 3.6008719, 3.6188763, 3.6369707, 3.6551555, 3.6734313, 3.6917985, 3.7102575, 3.7288088, 3.7474528, 3.7661901, 3.785021, 3.8039461, 3.8229659, 3.8420807, 3.8612911, 3.8805975, 3.9000005, 3.9195005, 3.939098, 3.9587935, 3.9785875, 3.9984804, 4.0184728, 4.0385652, 4.058758, 4.0790518, 4.0994471, 4.1199443, 4.140544, 4.1612467, 4.182053, 4.2029632, 4.2239781, 4.245098, 4.2663234, 4.2876551, 4.3090933, 4.3306388, 4.352292, 4.3740535, 4.3959237, 4.4179033, 4.4399929, 4.4621928, 4.4845038, 4.5069263, 4.5294609, 4.5521082, 4.5748688, 4.5977431, 4.6207318, 4.6438355, 4.6670547, 4.69039, 4.7138419, 4.7374111, 4.7610982, 4.7849037, 4.8088282, 4.8328723, 4.8570367, 4.8813219, 4.9057285, 4.9302571, 4.9549084, 4.9796829, 5.0045814, 5.0296043, 5.0547523, 5.080026, 5.1054262, 5.1309533, 5.1566081, 5.1823911, 5.2083031, 5.2343446, 5.2605163, 5.2868189, 5.313253, 5.3398192, 5.3665183, 5.3933509, 5.4203177, 5.4474193, 5.4746564, 5.5020297, 5.5295398, 5.5571875, 5.5849734, 5.6128983, 5.6409628, 5.6691676, 5.6975135, 5.726001, 5.754631, 5.7834042, 5.8123212, 5.8413828, 5.8705897, 5.8999427, 5.9294424, 5.9590896, 5.988885, 6.0188295, 6.0489236, 6.0791682, 6.1095641, 6.1401119, 6.1708125, 6.2016665, 6.2326749, 6.2638382, 6.2951574, 6.3266332, 6.3582664, 6.3900577, 6.422008, 6.454118, 6.4863886, 6.5188206, 6.5514147, 6.5841717, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.03909, 14.25397, 14.31127, 14.40688, 14.61211, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Si': {'mass_absorption_coefficient (cm2/g)': [540640.0, 562830.0, 69340.0, 55167.0, 50670.0, 49550.0, 49471.0, 49156.0, 45733.0, 34829.0, 25351.0, 19091.0, 14960.0, 12246.0, 10487.0, 9381.1, 8729.0, 8395.1, 8291.3, 8344.5, 8497.9, 8707.5, 8938.9, 9164.4, 9362.4, 9516.5, 9615.3, 9651.8, 9623.2, 9529.1, 9365.6, 9138.5, 8857.2, 8531.7, 8171.7, 7786.0, 7382.9, 6969.4, 6551.8, 6135.3, 5704.9, 5661.1, 5545.8, 5515.5, 108300.0, 108850.0, 109630.0, 112150.0, 112570.0, 110040.0, 105130.0, 98468.0, 97336.0, 95604.0, 95138.0, 104350.0, 102520.0, 100450.0, 91788.0, 82924.0, 74171.0, 65785.0, 57933.0, 50706.0, 44156.0, 38289.0, 33081.0, 28494.0, 24478.0, 20980.0, 17945.0, 15322.0, 13060.0, 11111.0, 9438.5, 8006.3, 6783.0, 5740.5, 4853.3, 4099.3, 3459.5, 2917.8, 2459.7, 2073.0, 1746.9, 1472.2, 1222.8, 1016.3, 845.49, 704.03, 586.78, 489.51, 408.74, 341.61, 293.74, 285.77, 282.07, 279.07, 3309.3, 3219.5, 2994.1, 2540.7, 2129.5, 1778.5, 1492.8, 1255.1, 1055.3, 884.13, 738.59, 617.06, 513.45, 425.24, 352.2, 291.72, 241.64, 200.16, 165.8, 137.35, 113.79, 94.267, 78.099, 64.707, 53.312, 43.815, 35.789, 29.233, 23.879, 19.507, 15.935, 13.017, 10.634, 8.6877, 7.0976, 5.7986, 4.7374, 3.8705, 3.1623, 2.5837, 2.111, 1.7248, 1.4022, 1.138, 0.92365, 0.74966, 0.60846, 0.49385, 0.39906, 0.32246, 0.26057, 0.21056, 0.17015, 0.13749, 0.1111, 0.089782, 0.072561, 0.058646, 0.047399, 0.038309, 0.030963, 0.025026, 0.020227, 0.016349, 0.013214, 0.01068, 0.0086327, 0.0069776, 0.0056398, 0.0045586, 0.0036847, 0.0029783, 0.0024074, 0.0019459, 0.0015729, 0.0012714, 0.0010277, 0.0008307, 0.00067147, 0.00054277, 0.00043874, 0.00035465, 0.00028668, 0.00023174, 0.0], - 'energies (keV)': [0.005108469, 0.005184715, 0.01069, 0.01113003, 0.01130038, 0.01134581, 0.01141396, 0.01142761, 0.01158431, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.097216, 0.098704, 0.0991008, 0.099696, 0.101184, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.145726, 0.1479565, 0.1485513, 0.1494435, 0.151674, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.802122, 1.820795, 1.829705, 1.837061, 1.848094, 1.875678, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Sn': {'mass_absorption_coefficient (cm2/g)': [10525.0, 12539.0, 20856.0, 44523.0, 78007.0, 113010.0, 140150.0, 153340.0, 151940.0, 139420.0, 120770.0, 100350.0, 81072.0, 64398.0, 50753.0, 39976.0, 31650.0, 25296.0, 20474.0, 16819.0, 14040.0, 13139.0, 12658.0, 12536.0, 19059.0, 18236.0, 15178.0, 12907.0, 11294.0, 10115.0, 9232.1, 8634.8, 8556.2, 8499.0, 8464.4, 9182.0, 9063.5, 8804.2, 8390.3, 8041.3, 7733.3, 7450.1, 7179.5, 6912.9, 6643.9, 6368.6, 6084.8, 5791.9, 5490.6, 5182.4, 4869.8, 4555.3, 4241.1, 3930.5, 3627.0, 3340.0, 3296.1, 3266.2, 3255.7, 3246.9, 18042.0, 17765.0, 17616.0, 27303.0, 27152.0, 26296.0, 25273.0, 21880.0, 18991.0, 16507.0, 14341.0, 13030.0, 12613.0, 12505.0, 14133.0, 14033.0, 13620.0, 13157.0, 12759.0, 12656.0, 13185.0, 13029.0, 12807.0, 11423.0, 10154.0, 9966.2, 9840.1, 9758.7, 10075.0, 9775.8, 9087.4, 7927.9, 6811.9, 5849.7, 5021.6, 4297.0, 3672.2, 3139.0, 2682.1, 2291.8, 1957.9, 1673.2, 1419.6, 1205.3, 1025.0, 872.99, 744.61, 635.4, 534.37, 444.53, 370.41, 308.73, 296.49, 284.53, 281.46, 971.58, 925.9, 891.03, 878.34, 836.93, 827.17, 1138.1, 1088.7, 1019.4, 992.48, 951.59, 941.18, 1071.5, 1031.5, 987.05, 836.98, 709.51, 598.43, 503.31, 422.74, 355.01, 298.26, 250.75, 210.53, 176.31, 146.22, 121.37, 100.84, 83.852, 69.794, 58.144, 48.446, 40.39, 33.708, 28.096, 23.375, 19.401, 16.083, 13.344, 11.082, 9.2116, 7.583, 7.1678, 6.853, 6.7722, 42.681, 41.0, 40.153, 33.571, 28.124, 23.577, 19.727, 16.463, 13.731, 11.446, 9.5388, 7.9476, 6.6212, 5.4924, 4.5563, 3.7801, 3.1364, 2.5985, 2.1453, 1.7713, 1.4626, 1.2078, 0.99745, 0.82378, 0.68027, 0.56166, 0.46375, 0.38294, 0.31622, 0.26114, 0.21566, 0.17812, 0.14711, 0.12151, 0.10037, 0.082909, 0.068489, 0.05658, 0.046743, 0.038618, 0.031906, 0.026362, 0.021782, 0.0], - 'energies (keV)': [0.0240195, 0.024378, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.086828, 0.088157, 0.0885114, 0.089043, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.13377, 0.1349368, 0.1358175, 0.1363635, 0.1371825, 0.13923, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.475104, 0.4794098, 0.482376, 0.483434, 0.4843152, 0.487224, 0.4908335, 0.4928067, 0.494496, 0.4957665, 0.503166, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.700112, 0.710828, 0.7136856, 0.7154399, 0.717972, 0.728688, 0.741272, 0.752618, 0.7556436, 0.760182, 0.7648052, 0.771528, 0.8175768, 0.866124, 0.8739896, 0.879381, 0.8829162, 0.888219, 0.901476, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 3.850224, 3.909156, 3.924871, 3.948444, 4.007376, 4.055024, 4.072978, 4.135319, 4.151944, 4.17688, 4.239222, 4.334821, 4.375406, 4.442377, 4.460236, 4.487024, 4.553994, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 28.6161, 29.0541, 29.1709, 29.3461, 29.7841, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Sm': {'mass_absorption_coefficient (cm2/g)': [1692.5, 1749.4, 4991.2, 5381.7, 5789.3, 6216.8, 6666.6, 7141.3, 7643.0, 8173.5, 8734.2, 9326.0, 9948.9, 9967.7, 10114.0, 10153.0, 219940.0, 208740.0, 191390.0, 151980.0, 119940.0, 94305.0, 74545.0, 59727.0, 48839.0, 40958.0, 38087.0, 36842.0, 36529.0, 36238.0, 35484.0, 35176.0, 31510.0, 28716.0, 26760.0, 25388.0, 24410.0, 23649.0, 23068.0, 22601.0, 22203.0, 21842.0, 21498.0, 21158.0, 20813.0, 20462.0, 20279.0, 20252.0, 20225.0, 20198.0, 20170.0, 20143.0, 20116.0, 20048.0, 19945.0, 19841.0, 19737.0, 19634.0, 19530.0, 19426.0, 19323.0, 19219.0, 19115.0, 19012.0, 18908.0, 18804.0, 18701.0, 18597.0, 18494.0, 18390.0, 18287.0, 18184.0, 18080.0, 17977.0, 17874.0, 17771.0, 17668.0, 17566.0, 17463.0, 17360.0, 17258.0, 17155.0, 17053.0, 16951.0, 16849.0, 16747.0, 16645.0, 16544.0, 16443.0, 16341.0, 16240.0, 16139.0, 16039.0, 15938.0, 15838.0, 15738.0, 15638.0, 15538.0, 15535.0, 63193.0, 60137.0, 57000.0, 54071.0, 51333.0, 48776.0, 46385.0, 44151.0, 42062.0, 40108.0, 38281.0, 36571.0, 34972.0, 33475.0, 32073.0, 30760.0, 29530.0, 28377.0, 27297.0, 26283.0, 25337.0, 24466.0, 23663.0, 22922.0, 22236.0, 21600.0, 21009.0, 20459.0, 19946.0, 19467.0, 19018.0, 18597.0, 18202.0, 17829.0, 17478.0, 17146.0, 16832.0, 16533.0, 16250.0, 15981.0, 15724.0, 15479.0, 15245.0, 15021.0, 14806.0, 14600.0, 14402.0, 14211.0, 14027.0, 13850.0, 13679.0, 13513.0, 13353.0, 13197.0, 13046.0, 12900.0, 12758.0, 12620.0, 12485.0, 12354.0, 12226.0, 12101.0, 11979.0, 11860.0, 11744.0, 11630.0, 11518.0, 11409.0, 11302.0, 11198.0, 11095.0, 10994.0, 10895.0, 10798.0, 10703.0, 10609.0, 10517.0, 10427.0, 10338.0, 10250.0, 10164.0, 10079.0, 9995.8, 9913.6, 9832.8, 9753.2, 9674.8, 9597.5, 9521.4, 9446.4, 9372.5, 9299.7, 9227.9, 9157.1, 9087.4, 9018.6, 8950.8, 8884.0, 8818.1, 8753.1, 8689.0, 8625.8, 8563.5, 8502.1, 8441.5, 8381.8, 8322.8, 8264.8, 8207.5, 8151.0, 8095.3, 8040.4, 7986.2, 7932.9, 7880.2, 7828.4, 7777.2, 7726.8, 7677.1, 7628.1, 7579.8, 7532.2, 7485.3, 7439.1, 7393.6, 7348.7, 7304.5, 7261.0, 7218.1, 7175.8, 7134.1, 7114.0, 8295.5, 8285.0, 8243.3, 8202.2, 8161.7, 8121.9, 8082.6, 8043.9, 8005.7, 7968.2, 7931.2, 7894.8, 7858.9, 7823.5, 7788.7, 7764.5, 8103.9, 8070.1, 8036.8, 8003.9, 7971.6, 7939.8, 7908.4, 7877.5, 7847.0, 7817.0, 7787.3, 7758.1, 7729.3, 7700.9, 7672.8, 7645.2, 7617.9, 7591.0, 7564.4, 7538.2, 7512.4, 7486.8, 7461.6, 7436.7, 7412.1, 7387.9, 7363.9, 7340.1, 7316.7, 7293.5, 7270.6, 7247.9, 7225.5, 7203.2, 7181.3, 7159.5, 7137.9, 7116.6, 7095.4, 7074.4, 7053.5, 7032.9, 7012.4, 6992.0, 6971.8, 6951.7, 6931.7, 6911.9, 6892.2, 6872.5, 6853.0, 6833.5, 6814.1, 6804.8, 7219.9, 7218.2, 7197.8, 7177.4, 7157.0, 7136.7, 7116.5, 7096.3, 7076.1, 7055.9, 7035.7, 7015.6, 6995.4, 6975.2, 6955.0, 6934.8, 6914.6, 6894.4, 6874.1, 6853.7, 6833.4, 6812.9, 6792.4, 6771.8, 6751.2, 6730.4, 6709.6, 6688.6, 6667.6, 6646.5, 6625.2, 6603.8, 6582.3, 6560.7, 6539.0, 6517.1, 6495.2, 6473.1, 6450.9, 6428.5, 6406.0, 6383.4, 6360.7, 6337.9, 6314.9, 6291.7, 6268.5, 6245.1, 6221.6, 6197.9, 6174.1, 6150.1, 6126.1, 6101.9, 6077.5, 6053.0, 6028.4, 6003.6, 5978.7, 5953.7, 5928.6, 5903.3, 5877.9, 5852.3, 5826.6, 5800.8, 5774.9, 5748.9, 5722.7, 5696.4, 5670.0, 5643.5, 5616.9, 5590.2, 5563.3, 5536.4, 5509.3, 5482.2, 5454.9, 5427.6, 5400.2, 5372.6, 5345.0, 5317.2, 5289.4, 5261.3, 5233.2, 5205.0, 5176.6, 5148.2, 5119.6, 5091.0, 5062.2, 5033.4, 5004.5, 4975.6, 4946.6, 4917.5, 4888.3, 4859.1, 4829.9, 4800.6, 4771.3, 4742.0, 4712.6, 4683.2, 4653.8, 4624.4, 4594.9, 4565.5, 4536.1, 4506.7, 4477.2, 4447.8, 4418.5, 4389.1, 4359.8, 4330.5, 4301.2, 4272.0, 4242.8, 4213.7, 4184.6, 4155.6, 4126.6, 4097.7, 4068.9, 4040.1, 4011.4, 3982.8, 3954.3, 3925.8, 3897.4, 3869.2, 3841.0, 3812.9, 3784.8, 3756.9, 3729.1, 3701.4, 3673.8, 3646.3, 3618.9, 3591.7, 3564.5, 3537.4, 3510.5, 3483.6, 3456.9, 3430.4, 3403.9, 3377.6, 3351.4, 3325.3, 3299.4, 3273.6, 3247.9, 3222.4, 3197.0, 3171.8, 3146.6, 3121.7, 3096.8, 3072.1, 3047.6, 3023.2, 2998.9, 2974.8, 2950.8, 2927.0, 2903.3, 2879.8, 2856.4, 2833.1, 2810.0, 2787.1, 2764.3, 2741.6, 2719.1, 2696.8, 2674.6, 2652.5, 2630.6, 2608.9, 2587.3, 2565.8, 2544.5, 2523.4, 2502.4, 2481.5, 2460.8, 2440.3, 2419.8, 2399.6, 2379.5, 2359.5, 2339.7, 2320.0, 2300.5, 2281.1, 2261.9, 2242.8, 2223.8, 2205.0, 2186.3, 2167.8, 2149.5, 2131.2, 2113.1, 2095.2, 2077.4, 2059.7, 2042.2, 2024.8, 2006.1, 1984.9, 1963.9, 1943.2, 1922.6, 1902.3, 1882.3, 1862.4, 1842.8, 1823.3, 1804.1, 1785.2, 1766.3, 1747.5, 1728.9, 1710.5, 1708.4, 6859.7, 6786.4, 6700.6, 6615.8, 6532.1, 6463.2, 9764.2, 9751.9, 9627.2, 9504.1, 9382.6, 9262.7, 9144.3, 9027.5, 8912.1, 8798.3, 8685.9, 8575.0, 8465.5, 8357.4, 8250.8, 8145.5, 8041.5, 7938.9, 7837.7, 7737.7, 7639.0, 7541.6, 7445.5, 7350.6, 7256.9, 7164.4, 7073.1, 6983.0, 6894.1, 6806.3, 6719.6, 6634.1, 6549.6, 6466.3, 6384.0, 6302.8, 6222.6, 6143.5, 6065.3, 5988.2, 5912.1, 5837.0, 5762.8, 5689.6, 5617.3, 5545.9, 5475.5, 5405.9, 5337.2, 5269.5, 5202.9, 5150.5, 6011.8, 6004.3, 5933.6, 5856.3, 5780.2, 5705.1, 5631.0, 5557.9, 5485.8, 5414.6, 5344.3, 5276.5, 5211.6, 5147.7, 5084.9, 5023.0, 4962.1, 4902.1, 4891.8, 5213.7, 5180.5, 5116.5, 5053.4, 4991.4, 4930.2, 4869.9, 4810.5, 4752.0, 4694.3, 4637.6, 4583.3, 4530.0, 4477.5, 4425.8, 4374.9, 4324.7, 4275.3, 4226.6, 4178.7, 4131.4, 4084.9, 4038.9, 4018.7, 4184.9, 4184.3, 4138.6, 4092.9, 4047.8, 4003.3, 3959.4, 3916.0, 3873.2, 3831.0, 3789.3, 3748.2, 3707.8, 3667.8, 3628.4, 3588.9, 3549.3, 3510.1, 3471.5, 3433.2, 3395.4, 3358.1, 3321.2, 3284.7, 3248.6, 3213.0, 3177.7, 3142.9, 3108.4, 3074.3, 3040.7, 3007.3, 2973.8, 2940.6, 2907.8, 2875.3, 2843.2, 2811.5, 2780.1, 2749.0, 2718.3, 2687.9, 2657.9, 2628.2, 2598.8, 2569.7, 2540.9, 2511.8, 2482.9, 2454.4, 2426.1, 2398.1, 2370.1, 2342.3, 2314.8, 2287.5, 2260.6, 2234.0, 2207.7, 2181.8, 2156.1, 2130.7, 2105.6, 2080.8, 2056.3, 2032.1, 2008.2, 1984.6, 1961.3, 1938.2, 1915.1, 1892.3, 1869.7, 1847.4, 1825.4, 1803.7, 1782.2, 1761.0, 1740.1, 1719.4, 1699.0, 1678.8, 1658.8, 1639.2, 1619.7, 1600.5, 1581.6, 1562.8, 1544.4, 1526.1, 1508.1, 1490.3, 1472.7, 1455.3, 1438.2, 1421.3, 1404.5, 1388.0, 1371.7, 1355.7, 1339.8, 1324.1, 1308.4, 1292.9, 1277.6, 1262.5, 1247.6, 1232.9, 1218.3, 1204.0, 1189.8, 1175.8, 1161.9, 1147.0, 1132.2, 1117.6, 1103.3, 1089.1, 1075.0, 1061.1, 1047.3, 1033.8, 1020.4, 1007.2, 994.16, 981.32, 968.65, 956.15, 943.82, 931.66, 919.67, 907.83, 896.16, 884.64, 873.28, 862.07, 851.01, 840.11, 829.35, 818.73, 808.24, 797.85, 787.6, 777.49, 767.51, 757.67, 747.96, 738.38, 728.93, 719.6, 710.4, 701.33, 692.37, 683.54, 674.82, 666.22, 657.73, 649.36, 641.1, 632.95, 624.91, 616.97, 609.14, 601.42, 593.8, 586.27, 578.85, 571.53, 564.3, 557.17, 533.91, 450.89, 381.31, 321.17, 270.39, 228.07, 192.73, 162.49, 155.44, 149.5, 147.95, 434.78, 414.88, 402.71, 361.83, 346.8, 342.98, 470.29, 462.16, 450.49, 429.57, 411.47, 407.01, 463.72, 446.7, 445.13, 377.0, 318.09, 266.21, 222.66, 186.21, 155.86, 130.57, 109.46, 91.45, 76.33, 63.541, 52.927, 44.125, 36.82, 30.751, 25.706, 21.505, 18.006, 14.976, 12.421, 10.289, 8.5091, 7.022, 5.7947, 4.7856, 3.9553, 3.6898, 3.5338, 3.4937, 19.963, 19.226, 19.113, 16.088, 13.493, 11.303, 9.4604, 7.9093, 6.6124, 5.5281, 4.6153, 3.8418, 3.1983, 2.653, 2.2003, 1.825, 1.5139, 1.2559, 1.042, 0.86458, 0.71742, 0.59536, 0.49411, 0.4101, 0.34081, 0.28387, 0.23646, 0.19698, 0.1641, 0.13672, 0.11391, 0.094913, 0.079088, 0.065906, 0.054923, 0.045773, 0.0], - 'energies (keV)': [0.0055275, 0.00561, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.020874, 0.0211935, 0.0212787, 0.0214065, 0.021726, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.036652, 0.037213, 0.0373626, 0.037587, 0.03797993, 0.038148, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12897987, 0.12902012, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.2472382, 0.2475618, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26540451, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34532318, 0.34607681, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0800822, 1.0803177, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.105813, 1.1061869, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4187409, 1.4201848, 1.4208592, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.5395028, 1.5418971, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7203536, 1.7251278, 1.7252464, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.581876, 6.682619, 6.709484, 6.749781, 6.850524, 6.915365, 7.165564, 7.275241, 7.304488, 7.348359, 7.392525, 7.458036, 7.582064, 7.698116, 7.729063, 7.775484, 7.891536, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 45.89752, 46.60003, 46.78737, 47.06837, 47.77089, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'V': {'mass_absorption_coefficient (cm2/g)': [107610.0, 107130.0, 106530.0, 105760.0, 104820.0, 103670.0, 102300.0, 100710.0, 98873.0, 96802.0, 94499.0, 91974.0, 89242.0, 86323.0, 83240.0, 80018.0, 76686.0, 73272.0, 69808.0, 67625.0, 66831.0, 66621.0, 140470.0, 140320.0, 131650.0, 107460.0, 85002.0, 70730.0, 60848.0, 53352.0, 47341.0, 42369.0, 38195.0, 37844.0, 37002.0, 36784.0, 40957.0, 40195.0, 39185.0, 36250.0, 33751.0, 31604.0, 29746.0, 28119.0, 26668.0, 25340.0, 24091.0, 22882.0, 21685.0, 20480.0, 19256.0, 17995.0, 16704.0, 15400.0, 14105.0, 12836.0, 11613.0, 10450.0, 9357.4, 8342.3, 7408.1, 6556.1, 5785.1, 5092.0, 4472.5, 3921.4, 3433.3, 3002.3, 2728.2, 2648.0, 2645.4, 2622.9, 18070.0, 17874.0, 17708.0, 25016.0, 25010.0, 24167.0, 22462.0, 19223.0, 17091.0, 16487.0, 16424.0, 16331.0, 18304.0, 17704.0, 16045.0, 13784.0, 11812.0, 10110.0, 8643.6, 7363.5, 6269.5, 5287.7, 4449.8, 3744.0, 3149.3, 2648.9, 2228.3, 1875.1, 1578.4, 1329.3, 1120.1, 943.43, 790.09, 662.45, 555.67, 464.85, 389.23, 323.23, 266.38, 219.75, 181.47, 150.01, 124.12, 102.81, 85.246, 70.752, 68.556, 65.72, 64.991, 592.13, 567.17, 542.35, 452.57, 381.09, 321.7, 270.56, 226.73, 189.84, 158.75, 132.26, 110.18, 91.698, 75.812, 62.642, 51.764, 42.778, 35.354, 29.22, 24.152, 19.964, 16.503, 13.643, 11.279, 9.2949, 7.6406, 6.248, 5.1006, 4.1637, 3.399, 2.7748, 2.2653, 1.8494, 1.5099, 1.2328, 1.0065, 0.82184, 0.67105, 0.54793, 0.44742, 0.36535, 0.29834, 0.24321, 0.19753, 0.16043, 0.1303, 0.10583, 0.08596, 0.06982, 0.05671, 0.046063, 0.037415, 0.030391, 0.024686, 0.020052, 0.016288, 0.013231, 0.010747, 0.0087303, 0.0070918, 0.0057609, 0.0046797, 0.0038015, 0.0030881, 0.0025087, 0.0020379, 0.0016555, 0.0013449, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.037044, 0.037611, 0.0377622, 0.03797993, 0.037989, 0.038556, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06517, 0.0661675, 0.0664335, 0.0668325, 0.06783, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.502642, 0.51009, 0.5103355, 0.5124891, 0.5154645, 0.5178975, 0.5199795, 0.5231025, 0.523158, 0.53091, 0.5478508, 0.5856525, 0.615636, 0.625059, 0.6260625, 0.6275718, 0.631341, 0.640764, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.355798, 5.437774, 5.459635, 5.492425, 5.574402, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Sc': {'mass_absorption_coefficient (cm2/g)': [186430.0, 160500.0, 146920.0, 74830.0, 41275.0, 26322.0, 18922.0, 14996.0, 12854.0, 11788.0, 11720.0, 11615.0, 11574.0, 16562.0, 16477.0, 16372.0, 16439.0, 16735.0, 17161.0, 17655.0, 18167.0, 18651.0, 19063.0, 19365.0, 19521.0, 19508.0, 19309.0, 18921.0, 18341.0, 17569.0, 16635.0, 15582.0, 14450.0, 13278.0, 12102.0, 10950.0, 9843.8, 8799.8, 7828.6, 6936.1, 6124.3, 5392.2, 4736.5, 4152.6, 3635.3, 3603.6, 3524.1, 3495.4, 3467.3, 25522.0, 25459.0, 25234.0, 35733.0, 35442.0, 34579.0, 33728.0, 29089.0, 25038.0, 23779.0, 22966.0, 22755.0, 25433.0, 24623.0, 24405.0, 21064.0, 18141.0, 15578.0, 13339.0, 11398.0, 9721.9, 8279.3, 7037.7, 5970.5, 5059.3, 4265.1, 3590.5, 3019.6, 2537.8, 2132.1, 1791.1, 1504.8, 1258.8, 1053.0, 881.86, 737.36, 615.88, 514.89, 430.88, 360.91, 302.6, 251.42, 207.14, 170.83, 141.02, 116.52, 96.373, 92.203, 88.319, 87.321, 824.43, 790.36, 765.7, 638.98, 537.52, 453.4, 381.25, 319.24, 267.02, 223.19, 186.45, 155.73, 129.92, 107.39, 88.735, 73.33, 60.603, 50.088, 41.401, 34.222, 28.289, 23.386, 19.334, 15.985, 13.171, 10.815, 8.88, 7.2916, 5.9873, 4.8878, 3.9828, 3.2454, 2.6446, 2.1551, 1.7562, 1.4312, 1.1664, 0.95055, 0.7747, 0.63138, 0.51459, 0.41941, 0.34184, 0.27863, 0.2271, 0.18491, 0.1502, 0.122, 0.099096, 0.080495, 0.065385, 0.053113, 0.043144, 0.035047, 0.02847, 0.023127, 0.018787, 0.015261, 0.012398, 0.010071, 0.0081817, 0.0066466, 0.0053995, 0.0043865, 0.0035635, 0.002895, 0.0023519, 0.0019107, 0.0015522, 0.0012611, 0.0010245, 0.0], - 'energies (keV)': [0.0324615, 0.032946, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.052724, 0.05302035, 0.053531, 0.0537462, 0.054069, 0.054876, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.394156, 0.398566, 0.400189, 0.4017978, 0.404211, 0.4046665, 0.4062933, 0.4087335, 0.410244, 0.414834, 0.4195189, 0.4484657, 0.4794098, 0.490392, 0.497898, 0.4998996, 0.502902, 0.510408, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.402944, 4.470336, 4.488307, 4.515264, 4.582656, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Sb': {'mass_absorption_coefficient (cm2/g)': [2821.5, 3996.2, 9531.6, 43573.0, 129290.0, 248730.0, 339460.0, 354960.0, 303590.0, 224380.0, 150030.0, 94243.0, 57344.0, 34632.0, 21158.0, 13266.0, 8624.2, 5854.8, 4214.7, 4168.0, 3926.3, 3855.1, 8412.2, 7989.1, 7245.9, 5895.2, 4986.6, 4386.0, 3989.1, 3732.7, 3645.8, 3611.4, 3603.0, 4327.0, 4310.2, 4301.1, 4225.1, 4184.3, 4166.9, 4163.3, 4170.6, 4181.5, 4184.3, 4170.3, 4139.2, 4089.1, 4018.3, 3926.2, 3813.7, 3682.6, 3535.0, 3374.0, 3203.9, 3022.4, 2998.1, 2955.3, 2948.4, 2944.0, 18669.0, 18285.0, 18087.0, 28226.0, 28001.0, 26829.0, 22253.0, 18624.0, 15812.0, 13528.0, 12129.0, 11715.0, 11609.0, 11608.0, 13021.0, 12615.0, 12120.0, 11733.0, 11633.0, 12118.0, 12068.0, 11751.0, 10507.0, 9338.8, 9142.5, 9047.5, 8971.9, 9258.3, 8982.1, 8329.4, 7160.5, 6149.4, 5280.3, 4533.3, 3879.0, 3314.3, 2832.5, 2420.0, 2068.4, 1767.5, 1510.9, 1284.9, 1091.1, 928.07, 790.61, 674.54, 569.68, 474.77, 395.57, 330.11, 276.55, 275.54, 265.46, 262.61, 897.66, 854.87, 804.82, 779.44, 766.95, 757.97, 1043.7, 997.97, 912.95, 896.72, 875.13, 865.52, 985.82, 948.96, 872.93, 741.19, 627.71, 529.0, 444.68, 373.49, 313.71, 263.66, 221.68, 185.81, 154.34, 128.09, 106.39, 88.44, 73.589, 61.288, 51.089, 42.591, 35.528, 29.665, 24.718, 20.568, 17.062, 14.154, 11.752, 9.7664, 8.0405, 6.6897, 6.603, 6.397, 6.3219, 39.64, 38.091, 35.031, 29.334, 24.581, 20.604, 17.212, 14.361, 11.977, 9.984, 8.321, 6.9344, 5.7698, 4.7884, 3.9743, 3.2989, 2.734, 2.2578, 1.8647, 1.5401, 1.2722, 1.0509, 0.86813, 0.71721, 0.59248, 0.48945, 0.40436, 0.33407, 0.27602, 0.22807, 0.18846, 0.15573, 0.12869, 0.10635, 0.087897, 0.072646, 0.060043, 0.049629, 0.041022, 0.033909, 0.02803, 0.023172, 0.0], - 'energies (keV)': [0.031557, 0.032028, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.096432, 0.09665893, 0.097908, 0.0983016, 0.098892, 0.100368, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.14896, 0.15124, 0.151848, 0.15276, 0.1542005, 0.15504, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.51695, 0.5248625, 0.526162, 0.5269725, 0.5301375, 0.5342155, 0.5363631, 0.53805, 0.5395845, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.750288, 0.761772, 0.7648052, 0.7648344, 0.769428, 0.780912, 0.795662, 0.8078405, 0.8110881, 0.8159595, 0.8175768, 0.828138, 0.8739896, 0.924826, 0.9342948, 0.9389815, 0.9427563, 0.9484185, 0.962574, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.049556, 4.055024, 4.111539, 4.128068, 4.152861, 4.214844, 4.292792, 4.334821, 4.358498, 4.37602, 4.402302, 4.468008, 4.604334, 4.633924, 4.674808, 4.693602, 4.721791, 4.792266, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 29.88138, 30.01405, 30.33874, 30.46071, 30.64366, 31.10102, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Se': {'mass_absorption_coefficient (cm2/g)': [5074.1, 5549.7, 7353.9, 10905.0, 15328.0, 20417.0, 25916.0, 31507.0, 36854.0, 41645.0, 45631.0, 48640.0, 50587.0, 51468.0, 51345.0, 50331.0, 48565.0, 47619.0, 47075.0, 46927.0, 48708.0, 48261.0, 48198.0, 47721.0, 47575.0, 48195.0, 47666.0, 46675.0, 44032.0, 41176.0, 38192.0, 35806.0, 35158.0, 35113.0, 34930.0, 36139.0, 35456.0, 33572.0, 30547.0, 27614.0, 24815.0, 22184.0, 19738.0, 17488.0, 15437.0, 13581.0, 11913.0, 10423.0, 9097.3, 7923.9, 6888.9, 5979.0, 5181.1, 4482.1, 3871.5, 3340.1, 2879.1, 2696.6, 2666.7, 2637.1, 2607.9, 2578.9, 2550.3, 2522.0, 2494.1, 2466.4, 2439.0, 2411.9, 2385.2, 2358.7, 2332.5, 2306.6, 2281.0, 2255.6, 2230.6, 2205.8, 2181.3, 2157.1, 2133.2, 2106.5, 2079.8, 2053.4, 2027.3, 2001.6, 1976.3, 1951.3, 1926.6, 1902.2, 1878.1, 1854.4, 1831.0, 1807.5, 1783.7, 1760.2, 1737.0, 1714.1, 1691.6, 1669.3, 1647.4, 1625.8, 1604.4, 1583.4, 1562.7, 1542.2, 1522.0, 1502.1, 1482.5, 1463.2, 1444.1, 1425.3, 1406.7, 1388.4, 1370.4, 1352.6, 1335.0, 1317.7, 1300.6, 1283.7, 1267.1, 1250.7, 1234.6, 1218.6, 1202.9, 1187.4, 1172.1, 1157.0, 1142.1, 1127.4, 1112.9, 1098.6, 1084.5, 1070.6, 1056.9, 1043.4, 1030.0, 1016.9, 1003.9, 991.05, 978.41, 965.94, 953.63, 941.49, 929.52, 917.7, 906.05, 894.55, 883.2, 872.0, 860.96, 850.06, 839.31, 832.61, 4283.7, 4265.3, 4210.3, 4155.9, 4102.2, 4049.3, 3997.0, 3987.0, 5643.7, 5587.9, 5516.0, 5445.0, 5374.9, 5305.7, 5237.5, 5170.1, 5103.6, 5037.9, 4973.1, 4909.1, 4846.0, 4783.7, 4722.2, 4661.5, 4601.5, 4542.4, 4484.0, 4426.4, 4369.5, 4313.3, 4257.9, 4212.6, 4203.2, 4809.9, 4760.8, 4701.7, 4643.4, 4585.8, 4529.0, 4472.8, 4417.4, 4362.7, 4308.6, 4255.5, 4203.3, 4151.8, 4101.0, 4050.9, 4001.4, 3952.6, 3904.4, 3856.9, 3809.9, 3763.6, 3717.8, 3672.7, 3628.1, 3584.1, 3540.3, 3496.9, 3454.0, 3411.7, 3369.9, 3328.6, 3287.6, 3247.1, 3207.1, 3167.6, 3128.6, 3090.1, 3052.1, 3014.5, 2977.4, 2940.8, 2904.6, 2868.9, 2833.6, 2798.8, 2764.3, 2730.3, 2696.8, 2663.6, 2630.8, 2598.5, 2566.5, 2535.0, 2503.7, 2472.6, 2441.8, 2411.5, 2381.5, 2351.9, 2322.7, 2293.8, 2265.2, 2237.1, 2209.2, 2181.7, 2154.6, 2127.8, 2101.3, 2075.1, 2049.3, 2023.7, 1998.5, 1973.6, 1949.1, 1924.8, 1900.8, 1877.1, 1853.7, 1830.6, 1807.8, 1785.3, 1763.0, 1741.0, 1719.3, 1697.9, 1676.8, 1655.9, 1635.2, 1614.8, 1594.7, 1574.9, 1555.2, 1535.9, 1516.7, 1497.8, 1479.2, 1460.7, 1442.5, 1424.6, 1406.8, 1389.3, 1372.0, 1355.0, 1338.1, 1321.5, 1305.0, 1288.8, 1272.8, 1256.9, 1241.3, 1225.9, 1210.6, 1195.4, 1180.3, 1165.5, 1150.8, 1136.3, 1122.0, 1107.8, 1093.8, 1079.4, 1064.9, 1050.6, 1036.4, 1022.4, 1008.6, 994.98, 981.56, 968.34, 955.29, 942.42, 929.71, 917.18, 904.81, 892.63, 880.61, 868.76, 857.07, 845.55, 834.18, 822.79, 811.56, 800.48, 789.56, 778.79, 768.17, 757.71, 747.38, 737.2, 727.17, 717.27, 707.51, 697.89, 688.4, 679.05, 669.82, 660.72, 651.75, 642.91, 634.19, 625.58, 617.1, 608.74, 600.49, 592.36, 584.34, 576.43, 568.63, 560.94, 553.36, 545.88, 538.51, 531.24, 524.06, 516.99, 510.02, 503.14, 496.36, 489.67, 483.07, 476.57, 470.16, 463.83, 457.59, 451.44, 445.37, 439.39, 433.49, 427.67, 421.93, 416.27, 410.68, 405.18, 399.75, 394.39, 389.09, 383.87, 378.72, 373.64, 368.63, 363.69, 358.82, 354.02, 349.28, 344.6, 339.99, 335.45, 330.96, 326.54, 322.18, 317.88, 313.63, 309.45, 305.32, 301.25, 297.24, 293.28, 289.37, 285.52, 281.72, 277.97, 274.27, 270.63, 267.03, 263.48, 259.98, 256.53, 253.13, 249.77, 246.46, 243.19, 239.97, 236.79, 233.66, 230.57, 227.52, 224.51, 221.54, 218.61, 215.72, 212.88, 210.07, 207.29, 204.56, 201.86, 199.2, 196.58, 193.99, 191.44, 188.92, 186.43, 183.98, 181.56, 179.18, 176.83, 174.5, 172.21, 169.96, 167.73, 165.53, 163.36, 161.22, 159.11, 157.03, 154.97, 152.95, 150.93, 148.91, 146.91, 144.94, 143.0, 141.09, 139.18, 137.3, 135.44, 133.61, 131.8, 130.02, 113.78, 94.78, 78.825, 65.619, 54.49, 44.737, 36.766, 30.244, 24.903, 21.508, 20.524, 20.346, 158.64, 151.57, 133.35, 111.43, 93.833, 79.237, 66.327, 55.422, 46.243, 38.55, 32.128, 26.782, 22.295, 18.442, 15.23, 12.577, 10.386, 8.5742, 7.0768, 5.8412, 4.8217, 3.9804, 3.2861, 2.7131, 2.2269, 1.8275, 1.4998, 1.231, 1.0103, 0.82837, 0.67749, 0.55411, 0.45322, 0.37071, 0.30323, 0.24804, 0.20291, 0.16599, 0.1358, 0.1111, 0.090892, 0.074365, 0.060845, 0.049784, 0.040735, 0.033332, 0.027275, 0.02232, 0.018265, 0.014947, 0.012232, 0.010011, 0.0081932, 0.0067056, 0.0054883, 0.0], - 'energies (keV)': [0.0569835, 0.057834, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.158662, 0.1610905, 0.1617381, 0.1627095, 0.1648404, 0.165138, 0.167359, 0.1680318, 0.169041, 0.171564, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.22687, 0.2301188, 0.2303425, 0.2312685, 0.2326575, 0.23613, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9, 0.9045, 0.9090225, 0.91356761, 0.91813545, 0.92272613, 0.92733976, 0.93197646, 0.93663634, 0.94131952, 0.94602612, 0.95075625, 0.95551003, 0.96028758, 0.96508902, 0.96991446, 0.97476404, 0.97963786, 0.98453605, 0.98945873, 0.99440602, 0.99937805, 1.0043749, 1.0093968, 1.0144438, 1.019516, 1.0246136, 1.0297367, 1.0348853, 1.0400598, 1.0452601, 1.0504864, 1.0557388, 1.0610175, 1.0663226, 1.0716542, 1.0770125, 1.0823975, 1.0878095, 1.0932486, 1.0987148, 1.1042084, 1.1097294, 1.1152781, 1.1208545, 1.1264587, 1.132091, 1.1377515, 1.1434402, 1.1491574, 1.1549032, 1.1606777, 1.1664811, 1.1723135, 1.1781751, 1.184066, 1.1899863, 1.1959362, 1.2019159, 1.2079255, 1.2139651, 1.220035, 1.2261351, 1.2322658, 1.2384271, 1.2446193, 1.2508424, 1.2570966, 1.2633821, 1.269699, 1.2760475, 1.2824277, 1.2888399, 1.295284, 1.3017605, 1.3082693, 1.3148106, 1.3213847, 1.3279916, 1.3346316, 1.3413047, 1.3480112, 1.3547513, 1.361525, 1.3683327, 1.3751743, 1.3820502, 1.3889605, 1.3959053, 1.4028848, 1.4098992, 1.4169487, 1.4240335, 1.4311536, 1.4356568, 1.4359431, 1.4383094, 1.4455009, 1.4527284, 1.4599921, 1.467292, 1.4746285, 1.4760494, 1.4763506, 1.4820016, 1.4894117, 1.4968587, 1.504343, 1.5118647, 1.519424, 1.5270212, 1.5346563, 1.5423295, 1.5500412, 1.5577914, 1.5655804, 1.5734083, 1.5812753, 1.5891817, 1.5971276, 1.6051132, 1.6131388, 1.6212045, 1.6293105, 1.6374571, 1.6456443, 1.6524595, 1.6538726, 1.6553406, 1.6621419, 1.6704526, 1.6788049, 1.6871989, 1.6956349, 1.7041131, 1.7126337, 1.7211968, 1.7298028, 1.7384518, 1.7471441, 1.7558798, 1.7646592, 1.7734825, 1.7823499, 1.7912617, 1.800218, 1.8092191, 1.8182652, 1.8273565, 1.8364933, 1.8456757, 1.8549041, 1.8641786, 1.8734995, 1.882867, 1.8922814, 1.9017428, 1.9112515, 1.9208077, 1.9304118, 1.9400638, 1.9497642, 1.959513, 1.9693105, 1.9791571, 1.9890529, 1.9989981, 2.0089931, 2.0190381, 2.0291333, 2.039279, 2.0494754, 2.0597227, 2.0700213, 2.0803714, 2.0907733, 2.1012272, 2.1117333, 2.122292, 2.1329034, 2.143568, 2.1542858, 2.1650572, 2.1758825, 2.1867619, 2.1976957, 2.2086842, 2.2197276, 2.2308263, 2.2419804, 2.2531903, 2.2644562, 2.2757785, 2.2871574, 2.2985932, 2.3100862, 2.3216366, 2.3332448, 2.344911, 2.3566356, 2.3684187, 2.3802608, 2.3921621, 2.404123, 2.4161436, 2.4282243, 2.4403654, 2.4525672, 2.4648301, 2.4771542, 2.48954, 2.5019877, 2.5144976, 2.5270701, 2.5397055, 2.552404, 2.565166, 2.5779919, 2.5908818, 2.6038362, 2.6168554, 2.6299397, 2.6430894, 2.6563048, 2.6695863, 2.6829343, 2.6963489, 2.7098307, 2.7233798, 2.7369967, 2.7506817, 2.7644351, 2.7782573, 2.7921486, 2.8061093, 2.8201399, 2.8342406, 2.8484118, 2.8626539, 2.8769671, 2.891352, 2.9058087, 2.9203378, 2.9349394, 2.9496141, 2.9643622, 2.979184, 2.9940799, 3.0090503, 3.0240956, 3.0392161, 3.0544122, 3.0696842, 3.0850326, 3.1004578, 3.1159601, 3.1315399, 3.1471976, 3.1629336, 3.1787482, 3.194642, 3.2106152, 3.2266683, 3.2428016, 3.2590156, 3.2753107, 3.2916873, 3.3081457, 3.3246864, 3.3413099, 3.3580164, 3.3748065, 3.3916805, 3.4086389, 3.4256821, 3.4428105, 3.4600246, 3.4773247, 3.4947113, 3.5121849, 3.5297458, 3.5473945, 3.5651315, 3.5829572, 3.6008719, 3.6188763, 3.6369707, 3.6551555, 3.6734313, 3.6917985, 3.7102575, 3.7288088, 3.7474528, 3.7661901, 3.785021, 3.8039461, 3.8229659, 3.8420807, 3.8612911, 3.8805975, 3.9000005, 3.9195005, 3.939098, 3.9587935, 3.9785875, 3.9984804, 4.0184728, 4.0385652, 4.058758, 4.0790518, 4.0994471, 4.1199443, 4.140544, 4.1612467, 4.182053, 4.2029632, 4.2239781, 4.245098, 4.2663234, 4.2876551, 4.3090933, 4.3306388, 4.352292, 4.3740535, 4.3959237, 4.4179033, 4.4399929, 4.4621928, 4.4845038, 4.5069263, 4.5294609, 4.5521082, 4.5748688, 4.5977431, 4.6207318, 4.6438355, 4.6670547, 4.69039, 4.7138419, 4.7374111, 4.7610982, 4.7849037, 4.8088282, 4.8328723, 4.8570367, 4.8813219, 4.9057285, 4.9302571, 4.9549084, 4.9796829, 5.0045814, 5.0296043, 5.0547523, 5.080026, 5.1054262, 5.1309533, 5.1566081, 5.1823911, 5.2083031, 5.2343446, 5.2605163, 5.2868189, 5.313253, 5.3398192, 5.3665183, 5.3933509, 5.4203177, 5.4474193, 5.4746564, 5.5020297, 5.5295398, 5.5571875, 5.5849734, 5.6128983, 5.6409628, 5.6691676, 5.6975135, 5.726001, 5.754631, 5.7834042, 5.8123212, 5.8413828, 5.8705897, 5.8999427, 5.9294424, 5.9590896, 5.988885, 6.0188295, 6.0489236, 6.0791682, 6.1095641, 6.1401119, 6.1708125, 6.2016665, 6.2326749, 6.2638382, 6.2951574, 6.3266332, 6.3582664, 6.3900577, 6.422008, 6.454118, 6.4863886, 6.5188206, 6.5514147, 6.5841717, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.40464, 12.60708, 12.64514, 12.72109, 12.91096, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Co': {'mass_absorption_coefficient (cm2/g)': [103950.0, 104850.0, 105880.0, 106960.0, 108040.0, 109100.0, 110090.0, 110990.0, 111770.0, 112390.0, 112840.0, 113090.0, 113140.0, 112970.0, 112580.0, 111960.0, 111120.0, 110080.0, 108830.0, 107400.0, 105810.0, 103720.0, 101160.0, 98394.0, 95446.0, 89904.0, 86874.0, 85265.0, 84842.0, 106560.0, 103790.0, 103440.0, 91080.0, 80619.0, 72160.0, 64948.0, 58619.0, 52987.0, 47948.0, 46490.0, 45454.0, 45185.0, 47867.0, 46898.0, 46514.0, 42475.0, 38853.0, 35591.0, 32649.0, 29987.0, 27568.0, 25356.0, 23318.0, 21428.0, 19664.0, 18007.0, 16448.0, 14977.0, 13581.0, 12261.0, 11022.0, 9866.0, 8796.6, 7814.1, 6917.9, 6105.8, 5373.9, 4718.1, 4133.4, 3614.5, 3155.8, 2751.5, 2396.4, 2085.2, 1821.9, 1813.0, 1764.6, 1750.3, 1749.8, 10776.0, 10529.0, 10422.0, 14818.0, 14657.0, 14110.0, 13753.0, 11589.0, 10549.0, 10154.0, 10053.0, 11238.0, 11125.0, 10858.0, 9536.7, 8147.3, 6949.9, 5919.9, 5040.8, 4258.3, 3587.8, 3021.1, 2540.5, 2136.5, 1796.7, 1511.1, 1271.3, 1070.1, 901.21, 759.47, 640.46, 537.2, 445.57, 369.65, 306.51, 253.61, 210.04, 174.12, 144.48, 120.0, 99.765, 83.02, 69.152, 57.656, 48.116, 45.376, 43.554, 43.086, 358.64, 343.19, 338.11, 282.0, 238.37, 199.9, 167.08, 139.42, 116.35, 97.104, 81.052, 67.66, 56.477, 46.767, 38.72, 32.041, 26.517, 21.946, 18.165, 15.036, 12.447, 10.255, 8.4373, 6.942, 5.6994, 4.6638, 3.8165, 3.1232, 2.556, 2.0917, 1.7118, 1.4009, 1.1465, 0.93833, 0.76799, 0.62858, 0.5145, 0.42042, 0.34226, 0.27864, 0.22685, 0.18468, 0.15036, 0.12242, 0.09967, 0.081151, 0.066073, 0.053798, 0.043803, 0.035666, 0.029041, 0.023647, 0.019255, 0.015679, 0.012767, 0.010396, 0.0084657, 0.0068937, 0.0056137, 0.0045714, 0.0037226, 0.0030315, 0.0024687, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.05831, 0.0592025, 0.0594405, 0.0597975, 0.06058959, 0.06069, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.098686, 0.1001965, 0.1005993, 0.1012035, 0.102714, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.763028, 0.7648052, 0.774707, 0.777728, 0.7778214, 0.782493, 0.789632, 0.7928064, 0.794172, 0.797568, 0.809472, 0.8175768, 0.8739896, 0.907088, 0.920972, 0.9246744, 0.930228, 0.9342948, 0.944112, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.554722, 7.670355, 7.701191, 7.747444, 7.863078, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Cl': {'mass_absorption_coefficient (cm2/g)': [732110.0, 795320.0, 1180500.0, 890660.0, 646820.0, 458590.0, 321200.0, 224450.0, 157740.0, 112200.0, 109100.0, 101220.0, 99249.0, 96518.0, 89881.0, 81361.0, 60339.0, 46049.0, 36355.0, 29830.0, 25504.0, 22694.0, 20939.0, 19888.0, 19268.0, 18890.0, 18633.0, 18418.0, 18205.0, 17971.0, 17711.0, 17423.0, 17110.0, 16774.0, 16416.0, 16036.0, 15630.0, 15196.0, 14731.0, 14235.0, 13707.0, 13149.0, 12562.0, 11951.0, 11321.0, 10455.0, 9557.2, 8687.2, 7853.0, 7061.5, 6318.2, 5900.5, 5818.9, 5745.7, 5705.2, 63321.0, 63065.0, 62834.0, 62817.0, 90257.0, 89034.0, 87632.0, 79989.0, 69995.0, 61145.0, 53290.0, 52528.0, 50887.0, 50461.0, 55507.0, 53911.0, 51842.0, 45380.0, 39620.0, 34481.0, 29914.0, 25875.0, 22316.0, 19195.0, 16467.0, 14092.0, 12033.0, 10254.0, 8720.9, 7404.3, 6276.6, 5313.1, 4491.5, 3791.9, 3198.0, 2695.6, 2246.7, 1866.5, 1550.7, 1289.6, 1073.6, 894.66, 746.32, 623.21, 520.96, 435.93, 365.16, 306.2, 257.02, 215.97, 181.65, 173.5, 166.83, 165.12, 1700.0, 1640.3, 1605.3, 1353.5, 1136.9, 957.38, 804.13, 672.76, 562.91, 471.05, 394.22, 329.81, 275.78, 228.78, 189.8, 157.47, 130.66, 108.41, 89.957, 74.532, 61.423, 50.622, 41.722, 34.389, 28.262, 23.145, 18.956, 15.525, 12.716, 10.415, 8.5307, 6.9876, 5.7238, 4.6887, 3.8408, 3.1464, 2.5618, 2.0816, 1.6915, 1.3745, 1.1169, 0.90764, 0.73757, 0.59939, 0.48709, 0.39584, 0.32169, 0.26143, 0.21201, 0.17166, 0.13899, 0.11254, 0.091112, 0.073749, 0.059695, 0.04832, 0.039113, 0.03166, 0.025627, 0.020744, 0.016792, 0.013592, 0.011003, 0.0089064, 0.0072096, 0.005836, 0.0047242, 0.0038242, 0.0030956, 0.0025059, 0.0020285, 0.0016421, 0.0013293, 0.0010761, 0.00087107, 0.00070514, 0.00057082, 0.00046209, 0.0], - 'energies (keV)': [0.006834, 0.006936, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01715, 0.0174125, 0.0174825, 0.0175875, 0.01785, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.196, 0.197568, 0.199, 0.1998, 0.200592, 0.201, 0.2013709, 0.2013984, 0.202608, 0.204, 0.205632, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.264796, 0.268849, 0.2699298, 0.271551, 0.275604, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.765952, 2.808288, 2.819578, 2.836512, 2.878848, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ca': {'mass_absorption_coefficient (cm2/g)': [685160.0, 659290.0, 545890.0, 292820.0, 129380.0, 67275.0, 40688.0, 27905.0, 21224.0, 17558.0, 15847.0, 15535.0, 15495.0, 15411.0, 20554.0, 20365.0, 20045.0, 19991.0, 20284.0, 20776.0, 21379.0, 22024.0, 22654.0, 23216.0, 23663.0, 23954.0, 24055.0, 23942.0, 23603.0, 23008.0, 22168.0, 21121.0, 19910.0, 18582.0, 17186.0, 15763.0, 14351.0, 12979.0, 11670.0, 10441.0, 9301.4, 8256.3, 7306.7, 6450.4, 5683.1, 4999.1, 4491.8, 4402.3, 4391.9, 4360.9, 4326.9, 33163.0, 32876.0, 46641.0, 46191.0, 45167.0, 42515.0, 36782.0, 31740.0, 30183.0, 29167.0, 28904.0, 32219.0, 31211.0, 30926.0, 26761.0, 23078.0, 19849.0, 17031.0, 14581.0, 12460.0, 10631.0, 9050.9, 7688.7, 6521.5, 5525.0, 4676.4, 3937.9, 3312.2, 2784.1, 2339.4, 1964.9, 1640.7, 1371.3, 1147.6, 958.76, 800.11, 668.34, 558.8, 467.65, 391.75, 328.48, 275.69, 229.25, 189.0, 155.96, 128.81, 114.15, 109.32, 108.08, 1039.0, 1036.6, 994.05, 863.21, 724.83, 610.83, 514.93, 431.46, 361.1, 301.95, 252.28, 210.72, 176.02, 146.61, 121.73, 100.52, 83.02, 68.568, 56.635, 46.781, 38.644, 31.924, 26.374, 21.79, 17.993, 14.765, 12.116, 9.9419, 8.1585, 6.6953, 5.4947, 4.482, 3.6488, 2.9705, 2.4184, 1.9689, 1.6031, 1.3052, 1.0627, 0.86527, 0.70454, 0.57367, 0.46712, 0.38037, 0.30973, 0.25221, 0.20538, 0.16708, 0.13561, 0.11006, 0.089334, 0.072509, 0.058853, 0.04777, 0.038774, 0.031473, 0.025547, 0.020736, 0.016832, 0.013663, 0.01109, 0.0090025, 0.0073076, 0.0059319, 0.0048152, 0.0039087, 0.0031729, 0.0025756, 0.0020908, 0.0016973, 0.0013778, 0.0011184, 0.00090793, 0.0], - 'energies (keV)': [0.02545001, 0.025527, 0.025908, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.042826, 0.04340198, 0.0434815, 0.0436563, 0.0439185, 0.044574, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.339472, 0.343, 0.3434143, 0.344668, 0.3460536, 0.34825, 0.34965, 0.35175, 0.353328, 0.357, 0.3671099, 0.3924405, 0.4195189, 0.429044, 0.435611, 0.4373622, 0.439989, 0.446556, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 3.957338, 4.017909, 4.034062, 4.055024, 4.05829, 4.118862, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ce': {'mass_absorption_coefficient (cm2/g)': [172420.0, 164130.0, 148030.0, 118470.0, 93174.0, 72048.0, 55102.0, 41894.0, 31801.0, 24187.0, 18487.0, 15680.0, 14781.0, 14553.0, 14409.0, 14396.0, 13619.0, 11278.0, 8954.3, 7221.5, 5921.5, 4938.6, 4188.1, 3608.0, 3153.2, 2791.0, 2497.4, 2255.2, 2066.1, 2052.8, 2024.1, 2013.3, 2345.2, 2353.5, 2397.5, 2639.3, 3050.2, 3368.7, 3489.5, 3521.8, 111360.0, 109880.0, 86781.0, 39626.0, 17039.0, 10662.0, 8465.7, 7499.1, 6931.1, 6501.1, 6129.9, 5800.9, 5762.9, 5695.5, 5678.1, 7018.3, 6952.9, 6875.2, 6809.0, 6750.8, 6735.8, 7144.4, 7091.6, 7056.4, 6854.5, 6693.5, 6562.2, 6545.0, 6518.1, 6511.1, 7029.0, 6999.0, 6964.4, 6826.1, 6667.2, 6473.9, 6238.4, 5960.4, 5643.1, 5287.7, 4905.3, 4510.4, 4115.6, 3731.3, 3365.0, 3021.9, 2704.5, 2414.3, 2187.2, 2150.9, 2130.0, 2111.5, 8836.7, 8606.0, 8516.7, 8502.7, 12763.0, 12263.0, 11742.0, 9822.7, 8281.8, 7015.1, 6717.3, 6472.1, 6409.0, 7380.8, 7108.5, 6947.4, 6573.2, 6341.4, 6282.1, 6622.2, 6391.3, 6321.2, 5433.7, 5336.6, 5164.8, 5120.7, 5297.3, 5135.0, 4941.5, 4255.8, 3649.6, 3122.8, 2669.2, 2279.9, 1944.7, 1660.6, 1416.4, 1208.1, 1031.7, 874.3, 735.99, 620.24, 520.29, 436.79, 367.2, 309.21, 260.86, 219.92, 189.41, 184.88, 182.01, 180.11, 549.96, 524.96, 472.31, 469.96, 452.35, 447.27, 608.4, 584.0, 552.12, 540.34, 529.9, 524.22, 596.87, 575.13, 527.6, 446.51, 376.52, 317.01, 266.36, 222.42, 185.9, 155.51, 129.96, 108.36, 90.138, 74.894, 62.282, 51.838, 43.175, 35.991, 30.03, 25.079, 20.964, 17.54, 14.654, 12.138, 9.9985, 8.2327, 6.7828, 5.5928, 4.6154, 4.4702, 4.2795, 4.2305, 25.154, 24.214, 23.263, 19.54, 16.379, 13.719, 11.469, 9.5765, 7.995, 6.6739, 5.5705, 4.6491, 3.8732, 3.2082, 2.6577, 2.2018, 1.8243, 1.5117, 1.2527, 1.0382, 0.86047, 0.71323, 0.59123, 0.49013, 0.40673, 0.3378, 0.28057, 0.23305, 0.19359, 0.16082, 0.13361, 0.11101, 0.092231, 0.076636, 0.063681, 0.052919, 0.043978, 0.036549, 0.0], - 'energies (keV)': [0.019899, 0.020196, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.037044, 0.037611, 0.0377622, 0.03797993, 0.037989, 0.038556, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.084182, 0.08458368, 0.0854705, 0.0858141, 0.0863295, 0.087618, 0.09041995, 0.09665893, 0.1033284, 0.1078, 0.10945, 0.10989, 0.1104581, 0.11055, 0.1122, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.203056, 0.206164, 0.2069928, 0.208236, 0.211344, 0.2152655, 0.218834, 0.2221835, 0.2230767, 0.2244165, 0.227766, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.283808, 0.288152, 0.2893104, 0.291048, 0.295392, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.865634, 0.8739896, 0.8788835, 0.883274, 0.8877165, 0.8967935, 0.9003987, 0.900966, 0.9058065, 0.919326, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.161692, 1.179473, 1.184215, 1.191327, 1.209108, 1.220098, 1.247344, 1.266436, 1.271527, 1.279164, 1.298256, 1.304285, 1.394281, 1.405908, 1.427427, 1.433165, 1.441773, 1.463292, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.608932, 5.660855, 5.694783, 5.717677, 5.752017, 5.837868, 6.040916, 6.051453, 6.133379, 6.158036, 6.195021, 6.287484, 6.417824, 6.469004, 6.516056, 6.542251, 6.581544, 6.679776, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 39.63414, 40.24079, 40.40256, 40.64522, 41.25186, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Xe': {'mass_absorption_coefficient (cm2/g)': [13641.0, 14194.0, 14842.0, 20764.0, 28929.0, 34579.0, 36531.0, 35170.0, 31653.0, 27195.0, 22687.0, 18628.0, 15214.0, 12459.0, 10386.0, 10290.0, 9958.7, 9849.8, 11975.0, 11574.0, 10826.0, 9435.3, 8359.2, 7525.0, 6872.6, 6765.3, 6642.7, 6611.3, 7196.4, 7085.3, 6984.9, 6564.2, 6213.0, 5908.2, 5633.6, 5377.4, 5130.9, 4888.2, 4645.1, 4399.1, 4149.2, 3895.5, 3639.3, 3382.5, 3127.2, 2876.1, 2631.4, 2449.8, 2395.3, 2382.8, 13073.0, 12746.0, 12607.0, 19577.0, 19330.0, 18538.0, 17368.0, 14416.0, 12141.0, 10331.0, 9191.9, 8869.1, 8824.9, 8785.8, 9904.7, 9589.6, 9099.3, 8802.7, 8711.5, 9180.3, 8883.5, 8006.9, 7178.6, 6942.7, 6908.3, 6879.2, 7103.3, 6866.3, 6196.1, 5302.1, 4541.4, 3893.6, 3338.2, 2854.0, 2434.4, 2077.0, 1772.9, 1514.3, 1294.0, 1106.4, 946.31, 805.08, 678.97, 568.52, 477.03, 400.68, 335.37, 280.44, 234.87, 227.96, 219.0, 216.69, 707.71, 674.88, 642.36, 622.82, 595.37, 588.55, 809.03, 774.23, 736.39, 717.21, 687.49, 679.97, 775.07, 746.24, 713.69, 604.87, 512.09, 431.73, 363.12, 305.21, 256.64, 215.6, 180.02, 149.92, 124.68, 103.51, 86.011, 71.513, 59.51, 49.566, 41.322, 34.48, 28.774, 24.024, 20.045, 16.705, 13.893, 11.538, 9.5022, 7.8085, 6.4221, 5.4835, 5.2863, 5.2462, 5.1853, 31.958, 30.73, 27.698, 23.214, 19.454, 16.302, 13.612, 11.36, 9.4768, 7.904, 6.5913, 5.4963, 4.5753, 3.8022, 3.1551, 2.6091, 2.1577, 1.7846, 1.4761, 1.221, 1.0101, 0.83571, 0.69146, 0.57216, 0.47372, 0.39226, 0.32483, 0.26901, 0.2228, 0.18453, 0.15285, 0.12661, 0.10488, 0.08689, 0.071986, 0.059642, 0.049417, 0.040947, 0.03393, 0.028117, 0.0], - 'energies (keV)': [0.06432, 0.06477028, 0.06528, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.143766, 0.1442475, 0.1459665, 0.1465533, 0.1474335, 0.149634, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.203938, 0.2070595, 0.2078919, 0.2091405, 0.212262, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.658854, 0.6692609, 0.671692, 0.6756615, 0.681973, 0.6847146, 0.685746, 0.688827, 0.699108, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.91826, 0.932315, 0.9342948, 0.936063, 0.941685, 0.95574, 0.97902, 0.994005, 0.9987612, 1.003995, 1.01898, 1.067676, 1.121708, 1.138877, 1.141345, 1.143455, 1.150323, 1.167492, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.686556, 4.758289, 4.777418, 4.806111, 4.877844, 4.953664, 5.001626, 5.078182, 5.098596, 5.129219, 5.205774, 5.295467, 5.343744, 5.425536, 5.447347, 5.480064, 5.561856, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 33.87017, 34.29889, 34.38859, 34.52684, 34.73421, 35.25263, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Tm': {'mass_absorption_coefficient (cm2/g)': [6683.5, 6609.5, 6036.3, 6210.0, 6416.7, 6657.0, 6931.9, 7242.5, 7590.2, 7976.3, 8402.6, 8870.4, 9381.1, 9935.2, 10531.0, 11167.0, 11843.0, 12557.0, 13306.0, 13514.0, 13692.0, 13739.0, 196250.0, 183540.0, 176480.0, 131800.0, 99372.0, 75863.0, 59323.0, 47956.0, 40296.0, 36314.0, 35334.0, 35234.0, 35091.0, 34903.0, 34093.0, 32134.0, 30093.0, 28878.0, 28215.0, 27917.0, 27852.0, 27931.0, 28090.0, 28286.0, 28304.0, 28230.0, 28155.0, 28080.0, 28005.0, 27929.0, 27853.0, 27776.0, 27699.0, 27622.0, 27545.0, 27467.0, 27389.0, 27311.0, 27232.0, 27153.0, 27073.0, 26994.0, 26913.0, 26833.0, 26752.0, 26671.0, 26589.0, 26508.0, 26425.0, 26343.0, 26260.0, 26177.0, 26093.0, 26009.0, 25925.0, 25841.0, 25756.0, 25671.0, 25585.0, 25499.0, 25413.0, 25327.0, 25240.0, 25153.0, 25066.0, 24978.0, 24890.0, 24802.0, 24713.0, 24624.0, 24535.0, 24446.0, 24356.0, 24266.0, 24176.0, 24085.0, 23994.0, 23903.0, 23812.0, 23720.0, 23629.0, 23537.0, 23444.0, 23352.0, 23259.0, 23166.0, 23073.0, 22980.0, 22886.0, 22792.0, 22698.0, 22604.0, 22510.0, 22415.0, 22320.0, 22225.0, 22130.0, 22035.0, 21940.0, 21844.0, 21748.0, 21652.0, 21556.0, 21460.0, 21364.0, 21268.0, 21171.0, 21075.0, 20978.0, 20881.0, 20784.0, 20687.0, 20590.0, 20493.0, 20395.0, 20298.0, 20201.0, 20103.0, 20006.0, 19908.0, 19810.0, 19713.0, 19615.0, 19517.0, 19420.0, 19322.0, 19224.0, 19126.0, 19028.0, 18931.0, 18833.0, 18735.0, 18637.0, 18540.0, 18442.0, 18344.0, 18247.0, 18149.0, 18051.0, 17954.0, 17857.0, 17759.0, 17734.0, 17732.0, 35276.0, 35251.0, 34752.0, 33686.0, 32675.0, 31716.0, 30805.0, 29941.0, 29121.0, 28341.0, 27601.0, 26897.0, 26228.0, 25591.0, 24985.0, 24408.0, 23859.0, 23335.0, 22836.0, 22360.0, 21905.0, 21472.0, 21064.0, 20681.0, 20321.0, 19982.0, 19662.0, 19359.0, 19071.0, 18798.0, 18538.0, 18291.0, 18054.0, 17827.0, 17610.0, 17402.0, 17202.0, 17009.0, 16823.0, 16643.0, 16469.0, 16301.0, 16137.0, 15979.0, 15825.0, 15675.0, 15529.0, 15387.0, 15248.0, 15113.0, 14980.0, 14850.0, 14723.0, 14599.0, 14477.0, 14357.0, 14240.0, 14125.0, 14011.0, 13900.0, 13790.0, 13682.0, 13576.0, 13471.0, 13368.0, 13266.0, 13166.0, 13067.0, 12969.0, 12873.0, 12778.0, 12684.0, 12591.0, 12499.0, 12408.0, 12319.0, 12230.0, 12143.0, 12056.0, 11970.0, 11886.0, 11802.0, 11719.0, 11637.0, 11556.0, 11475.0, 11396.0, 11317.0, 11239.0, 11162.0, 11086.0, 11010.0, 10935.0, 10861.0, 10788.0, 10715.0, 10643.0, 10572.0, 10502.0, 10432.0, 10363.0, 10294.0, 10226.0, 10159.0, 10093.0, 10027.0, 9961.5, 9896.9, 9833.0, 9769.6, 9706.9, 9644.7, 9583.2, 9522.3, 9462.0, 9402.2, 9343.1, 9284.5, 9226.5, 9169.0, 9112.2, 9055.9, 9000.1, 8944.9, 8890.2, 8836.0, 8782.4, 8729.4, 8722.8, 9587.3, 9564.6, 9511.3, 9458.4, 9406.1, 9354.3, 9303.0, 9252.2, 9201.9, 9152.0, 9102.7, 9053.8, 9005.3, 8957.4, 8909.8, 8862.8, 8816.1, 8769.9, 8724.2, 8678.8, 8633.9, 8589.4, 8545.2, 8501.5, 8458.1, 8415.1, 8372.4, 8330.1, 8309.1, 8500.3, 8499.8, 8458.8, 8417.6, 8376.7, 8336.1, 8295.9, 8255.9, 8216.3, 8177.0, 8138.0, 8099.2, 8060.7, 8022.5, 7984.6, 7946.9, 7909.5, 7872.4, 7835.4, 7798.7, 7762.3, 7726.0, 7690.0, 7654.2, 7618.6, 7583.2, 7548.0, 7512.9, 7478.1, 7443.4, 7408.9, 7374.5, 7340.4, 7306.3, 7272.4, 7238.7, 7205.0, 7171.5, 7138.2, 7104.9, 7071.8, 7048.4, 7038.8, 7300.9, 7277.7, 7244.2, 7210.7, 7177.3, 7144.0, 7110.9, 7077.7, 7044.7, 7011.8, 6978.9, 6946.0, 6913.3, 6880.6, 6847.9, 6815.4, 6782.8, 6750.3, 6717.9, 6685.5, 6653.1, 6620.8, 6588.4, 6556.1, 6523.8, 6491.6, 6459.3, 6427.1, 6394.8, 6362.6, 6330.4, 6298.2, 6266.0, 6233.8, 6201.6, 6169.4, 6137.2, 6105.1, 6072.9, 6040.7, 6008.6, 5976.4, 5944.3, 5912.1, 5880.0, 5847.9, 5815.7, 5783.6, 5751.5, 5719.4, 5687.3, 5655.2, 5623.1, 5591.0, 5558.9, 5526.8, 5494.7, 5462.7, 5430.7, 5398.7, 5366.7, 5334.7, 5302.8, 5270.8, 5239.0, 5207.1, 5175.3, 5143.5, 5111.7, 5080.0, 5048.3, 5016.6, 4985.0, 4953.5, 4921.9, 4890.5, 4859.0, 4827.7, 4796.3, 4765.1, 4733.9, 4702.7, 4671.6, 4640.6, 4609.6, 4578.7, 4547.8, 4517.0, 4486.2, 4455.5, 4424.8, 4394.2, 4363.6, 4333.1, 4302.6, 4272.3, 4242.0, 4211.8, 4181.6, 4151.6, 4121.6, 4091.8, 4062.0, 4032.3, 4002.8, 3973.3, 3943.9, 3914.5, 3885.3, 3856.2, 3827.2, 3798.3, 3769.5, 3740.9, 3712.4, 3684.0, 3655.8, 3627.7, 3599.7, 3571.9, 3544.2, 3516.7, 3489.3, 3462.1, 3435.0, 3408.0, 3381.2, 3354.6, 3328.1, 3301.8, 3275.6, 3249.6, 3223.7, 3198.0, 3172.5, 3147.1, 3121.9, 3096.8, 3072.0, 3047.2, 3022.7, 2998.3, 2974.1, 2950.0, 2926.1, 2902.4, 2878.8, 2855.5, 2832.2, 2809.2, 2786.3, 2760.8, 2730.2, 2699.9, 2669.9, 2640.4, 2611.1, 2582.3, 2553.7, 2525.5, 2497.7, 2470.2, 2443.0, 2416.1, 2389.5, 2363.3, 2337.4, 2311.8, 2286.5, 2261.4, 2236.7, 2212.3, 2188.1, 2164.3, 2140.7, 2117.4, 2094.4, 2071.6, 2049.2, 2026.9, 2005.0, 1983.3, 1961.8, 1940.6, 1919.7, 1898.9, 1878.5, 1858.2, 1838.3, 1818.5, 1799.0, 1779.7, 1760.6, 1741.7, 1723.1, 1704.6, 1686.4, 1668.4, 1650.6, 1633.0, 1615.7, 1598.5, 1581.5, 1564.7, 1548.1, 1531.7, 1515.5, 1499.4, 1483.6, 1467.9, 1452.4, 1436.9, 1421.4, 1406.2, 1391.1, 1376.1, 1361.4, 1346.8, 1332.4, 1318.2, 1304.1, 1290.2, 1276.4, 1262.8, 1249.4, 1236.1, 1222.9, 1210.0, 1202.6, 4515.2, 4494.0, 4437.3, 4381.3, 4326.1, 4271.6, 4217.7, 4172.7, 6263.3, 6260.3, 6180.7, 6102.0, 6024.4, 5947.8, 5872.1, 5797.3, 5723.5, 5650.7, 5578.8, 5507.8, 5437.8, 5368.6, 5300.4, 5233.0, 5166.5, 5100.9, 5036.1, 4972.1, 4909.0, 4846.7, 4785.1, 4724.4, 4664.5, 4605.3, 4546.9, 4489.3, 4432.4, 4376.2, 4320.8, 4266.0, 4212.0, 4158.7, 4106.0, 4054.0, 4002.7, 3952.1, 3902.1, 3852.8, 3804.1, 3756.0, 3708.5, 3661.7, 3615.4, 3592.8, 4194.2, 4190.8, 4135.5, 4081.1, 4027.2, 3974.0, 3921.5, 3869.8, 3818.7, 3768.3, 3718.5, 3671.1, 3625.3, 3580.2, 3535.9, 3492.2, 3449.3, 3407.0, 3365.4, 3324.4, 3284.0, 3244.2, 3235.4, 3443.7, 3429.8, 3387.2, 3345.3, 3304.0, 3263.2, 3223.1, 3183.5, 3144.5, 3106.1, 3068.1, 3031.9, 2996.4, 2961.4, 2926.9, 2893.0, 2859.5, 2826.5, 2794.0, 2762.0, 2734.8, 2730.4, 2837.0, 2825.9, 2793.9, 2762.4, 2731.3, 2700.6, 2670.2, 2640.3, 2610.7, 2581.5, 2552.6, 2524.2, 2496.2, 2468.2, 2440.3, 2412.7, 2385.5, 2358.6, 2332.0, 2305.7, 2279.7, 2254.1, 2228.7, 2203.6, 2178.8, 2154.3, 2130.0, 2106.1, 2082.4, 2058.9, 2035.8, 2012.9, 1990.2, 1967.8, 1945.4, 1923.3, 1901.4, 1879.8, 1858.5, 1837.3, 1816.4, 1795.8, 1775.3, 1755.2, 1735.2, 1715.5, 1696.0, 1676.7, 1657.6, 1638.7, 1619.8, 1601.2, 1582.7, 1564.4, 1544.8, 1525.4, 1505.9, 1486.7, 1467.7, 1449.0, 1430.6, 1412.4, 1394.4, 1376.7, 1359.2, 1342.0, 1325.0, 1308.2, 1291.7, 1275.4, 1259.3, 1243.4, 1227.7, 1212.3, 1197.0, 1182.0, 1167.1, 1152.4, 1137.8, 1123.4, 1109.1, 1095.1, 1081.3, 1067.6, 1054.2, 1040.9, 1027.8, 1014.9, 1002.1, 989.53, 977.12, 964.88, 952.8, 940.88, 929.12, 917.52, 906.07, 894.78, 883.63, 872.55, 861.43, 850.45, 839.62, 828.94, 818.4, 808.0, 797.75, 787.63, 777.64, 767.8, 758.08, 726.39, 612.42, 516.38, 435.47, 367.73, 310.93, 263.25, 222.89, 187.99, 158.74, 134.23, 113.45, 112.53, 108.28, 107.19, 298.6, 282.34, 263.4, 237.96, 229.53, 227.32, 312.35, 311.38, 298.99, 290.27, 277.86, 274.66, 311.89, 299.49, 248.28, 207.62, 174.59, 146.46, 122.8, 102.96, 86.362, 72.41, 60.607, 50.732, 42.402, 35.463, 29.686, 24.871, 20.687, 17.177, 14.273, 11.869, 9.8791, 8.2156, 6.8299, 5.6671, 4.6906, 3.8834, 3.2165, 2.7038, 2.6662, 2.5909, 2.5619, 13.682, 13.179, 12.162, 10.217, 8.5696, 7.1865, 6.0174, 5.0217, 4.1914, 3.4988, 2.9211, 2.439, 2.0334, 1.6909, 1.4063, 1.1696, 0.97292, 0.80936, 0.67336, 0.56025, 0.46619, 0.38795, 0.32286, 0.26871, 0.22474, 0.18821, 0.15763, 0.13203, 0.11059, 0.092641, 0.077609, 0.06502, 0.0], - 'energies (keV)': [0.0053265, 0.005406, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.031654, 0.0321385, 0.0322677, 0.0324615, 0.032946, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.052136, 0.052934, 0.05302035, 0.0531468, 0.053466, 0.054264, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.17946997, 0.17949044, 0.17970956, 0.17973003, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33621965, 0.33698036, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38540604, 0.38636941, 0.38639394, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47099246, 0.47167755, 0.47240756, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4674798, 1.4679202, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5141684, 1.5150317, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8824082, 1.8865918, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0874384, 2.0921614, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3022094, 2.3038428, 2.3113904, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 8.47504, 8.60476, 8.639352, 8.69124, 8.82096, 9.030794, 9.424561, 9.568814, 9.607282, 9.653919, 9.664983, 9.809237, 9.913386, 10.06512, 10.10558, 10.16628, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.20181, 58.4927, 59.09265, 59.33021, 59.68655, 60.57739, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Cs': {'mass_absorption_coefficient (cm2/g)': [460090.0, 456310.0, 435160.0, 371510.0, 316430.0, 300680.0, 298760.0, 296600.0, 512590.0, 485960.0, 413820.0, 321410.0, 247200.0, 188770.0, 143520.0, 108910.0, 82651.0, 63135.0, 62851.0, 59345.0, 58384.0, 57125.0, 53809.0, 48124.0, 36972.0, 28601.0, 22306.0, 17559.0, 13965.0, 11231.0, 9140.6, 7532.2, 6286.1, 5313.4, 4550.3, 3946.2, 3463.2, 3072.2, 2750.7, 2482.1, 2254.2, 2214.4, 2168.5, 2156.6, 953770.0, 880410.0, 729930.0, 669280.0, 622540.0, 1459000.0, 1434900.0, 1085600.0, 416170.0, 130950.0, 52538.0, 26098.0, 15436.0, 10480.0, 7910.8, 6467.4, 5612.5, 5092.4, 4946.5, 4875.6, 4858.2, 6439.1, 6372.2, 6277.2, 6224.8, 6211.9, 6770.6, 6721.7, 6713.4, 6544.7, 6434.4, 6359.9, 6316.1, 6303.1, 6301.3, 6299.7, 6945.4, 6927.4, 6871.7, 6774.6, 6650.2, 6492.2, 6296.3, 6057.1, 5778.7, 5468.4, 5134.8, 4786.2, 4431.2, 4077.4, 3731.3, 3397.8, 3080.6, 2782.3, 2529.3, 2504.2, 2468.5, 2452.9, 2452.6, 11637.0, 11365.0, 11247.0, 17211.0, 17014.0, 16353.0, 15758.0, 13195.0, 11162.0, 9515.1, 8549.2, 8248.4, 8170.6, 9432.3, 9346.9, 9024.2, 8498.4, 8199.0, 8121.8, 8581.7, 8530.3, 8229.2, 7331.6, 6646.3, 6428.4, 6372.3, 6624.3, 6588.2, 6372.9, 5689.0, 4888.7, 4193.7, 3594.8, 3078.3, 2625.6, 2239.8, 1911.7, 1631.2, 1392.2, 1188.9, 1016.2, 869.52, 734.1, 614.67, 515.56, 433.29, 364.37, 305.68, 256.77, 220.23, 215.36, 211.62, 209.4, 667.75, 636.88, 584.26, 570.31, 558.93, 552.55, 758.38, 725.65, 676.02, 655.65, 647.75, 640.63, 729.76, 702.56, 639.23, 542.04, 458.18, 385.94, 324.56, 272.95, 229.29, 191.52, 159.85, 133.13, 110.66, 91.903, 76.389, 63.536, 52.89, 44.067, 36.749, 30.673, 25.606, 21.396, 17.851, 14.88, 12.369, 10.189, 8.3754, 6.8907, 5.6739, 5.2343, 5.0088, 4.9509, 30.177, 29.117, 29.038, 24.452, 20.494, 17.166, 14.359, 11.985, 10.001, 8.3441, 6.9608, 5.8063, 4.843, 4.0277, 3.3437, 2.7662, 2.2888, 1.8939, 1.5672, 1.297, 1.0735, 0.88858, 0.73556, 0.60894, 0.50427, 0.41788, 0.3463, 0.28701, 0.23788, 0.19717, 0.16344, 0.13549, 0.11232, 0.093121, 0.077207, 0.064016, 0.053081, 0.044016, 0.036501, 0.03027, 0.0], - 'energies (keV)': [0.01142761, 0.011457, 0.011628, 0.01221612, 0.012838, 0.0130345, 0.01305903, 0.0130869, 0.0131655, 0.013362, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.022246, 0.02227063, 0.0225865, 0.0226773, 0.0228135, 0.023154, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07497, 0.0761175, 0.0764235, 0.0768825, 0.077224, 0.07803, 0.078406, 0.0787212, 0.07912411, 0.079194, 0.080376, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.158368, 0.160792, 0.1614384, 0.162408, 0.1648404, 0.168854, 0.1714385, 0.1721277, 0.1731615, 0.175746, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.226184, 0.229646, 0.2301188, 0.2305692, 0.231954, 0.235416, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.71099, 0.7154399, 0.7218725, 0.72471, 0.7247745, 0.7291275, 0.7358025, 0.7387605, 0.74001, 0.7431975, 0.75429, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.977648, 0.992612, 0.9966024, 0.9987612, 1.002588, 1.017552, 1.0437, 1.059675, 1.063935, 1.067676, 1.070325, 1.0863, 1.141345, 1.192758, 1.211015, 1.215883, 1.220098, 1.223186, 1.241442, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.911662, 4.953664, 4.98684, 5.006888, 5.036959, 5.112138, 5.252212, 5.295467, 5.332603, 5.35404, 5.386197, 5.466588, 5.600014, 5.660855, 5.685729, 5.708586, 5.742872, 5.828586, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 35.26491, 35.80468, 35.94862, 36.16452, 36.66551, 36.70429, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Cr': {'mass_absorption_coefficient (cm2/g)': [154920.0, 152420.0, 149980.0, 147550.0, 145100.0, 142570.0, 139940.0, 137180.0, 134270.0, 131180.0, 127910.0, 124460.0, 120820.0, 117010.0, 113030.0, 108910.0, 104660.0, 100320.0, 95907.0, 91450.0, 86981.0, 85274.0, 84260.0, 83992.0, 191640.0, 176080.0, 174860.0, 123310.0, 94676.0, 78301.0, 67227.0, 58853.0, 52095.0, 46448.0, 42946.0, 41910.0, 41651.0, 41643.0, 44808.0, 43882.0, 41220.0, 37830.0, 34906.0, 32374.0, 30175.0, 28248.0, 26536.0, 24985.0, 23549.0, 22185.0, 20863.0, 19560.0, 18261.0, 16947.0, 15626.0, 14313.0, 13027.0, 11784.0, 10599.0, 9483.9, 8446.9, 7492.2, 6621.2, 5833.3, 5125.7, 4494.0, 3933.0, 3437.0, 3000.0, 2616.0, 2472.9, 2393.2, 2376.8, 16383.0, 16146.0, 15986.0, 22671.0, 22638.0, 22578.0, 21761.0, 19203.0, 16279.0, 15614.0, 15043.0, 14895.0, 16661.0, 16099.0, 15739.0, 13490.0, 11554.0, 9880.0, 8441.4, 7204.6, 6081.1, 5130.3, 4322.1, 3637.0, 3060.2, 2574.0, 2165.3, 1821.9, 1533.5, 1291.3, 1087.9, 917.14, 771.17, 646.31, 542.3, 454.35, 377.25, 311.01, 256.67, 212.03, 175.33, 145.14, 120.26, 99.752, 82.824, 68.838, 62.295, 59.743, 59.086, 526.58, 518.54, 504.62, 430.52, 362.03, 305.41, 257.33, 215.78, 180.61, 150.57, 125.54, 104.67, 87.268, 72.289, 59.747, 49.367, 40.794, 33.712, 27.861, 23.027, 19.032, 15.732, 13.004, 10.75, 8.879, 7.2662, 5.9369, 4.851, 3.9638, 3.2386, 2.6462, 2.1623, 1.7669, 1.4438, 1.1798, 0.96417, 0.78795, 0.64394, 0.52627, 0.43011, 0.35153, 0.28682, 0.23312, 0.18948, 0.15401, 0.12518, 0.10175, 0.082707, 0.067228, 0.054647, 0.044421, 0.036109, 0.029352, 0.02386, 0.019396, 0.015767, 0.012818, 0.01042, 0.0084706, 0.0068861, 0.005598, 0.0045509, 0.0036997, 0.0030077, 0.0024452, 0.0019879, 0.0016161, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04165, 0.0422875, 0.0424575, 0.0427125, 0.04335, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.072618, 0.0737295, 0.07401695, 0.0740259, 0.0744705, 0.075582, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.56301, 0.572026, 0.5739255, 0.5773725, 0.5807815, 0.5831163, 0.5856525, 0.58599, 0.5866185, 0.595374, 0.6260625, 0.6692609, 0.680708, 0.691127, 0.6939054, 0.698073, 0.708492, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 5.869416, 5.959254, 5.983211, 6.019146, 6.051453, 6.108984, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Cu': {'mass_absorption_coefficient (cm2/g)': [134630.0, 132640.0, 130780.0, 129050.0, 127430.0, 125910.0, 124470.0, 123100.0, 121770.0, 120490.0, 119220.0, 117950.0, 116670.0, 115360.0, 114000.0, 112590.0, 109540.0, 104990.0, 100450.0, 95904.0, 91369.0, 86848.0, 82351.0, 77886.0, 73465.0, 69102.0, 64809.0, 60600.0, 56490.0, 54027.0, 53123.0, 52886.0, 77537.0, 74465.0, 64727.0, 55532.0, 49158.0, 44255.0, 40201.0, 36710.0, 33890.0, 33640.0, 33234.0, 33064.0, 34962.0, 34361.0, 33098.0, 30710.0, 28549.0, 26579.0, 24773.0, 23106.0, 21553.0, 20093.0, 18707.0, 17381.0, 16103.0, 14869.0, 13676.0, 12525.0, 11412.0, 10342.0, 9325.6, 8368.7, 7477.1, 6654.0, 5900.6, 5215.9, 4598.0, 4043.6, 3548.9, 3109.4, 2720.6, 2348.3, 2019.9, 1738.7, 1578.9, 1526.2, 1512.6, 9321.5, 9244.7, 9196.7, 8861.4, 8743.8, 12332.0, 11718.0, 10598.0, 8597.6, 8449.0, 8092.4, 8001.6, 8807.3, 8519.1, 8146.3, 6980.6, 5956.2, 5076.1, 4329.5, 3693.4, 3117.4, 2629.6, 2218.0, 1870.7, 1576.6, 1325.1, 1114.2, 937.54, 789.54, 661.4, 551.34, 460.01, 383.63, 318.38, 264.55, 220.1, 182.75, 151.58, 125.84, 104.57, 86.981, 72.416, 60.347, 50.336, 42.025, 37.656, 36.113, 35.687, 282.91, 271.43, 233.59, 195.73, 164.84, 138.89, 116.23, 97.122, 81.041, 67.557, 56.288, 46.894, 38.925, 32.262, 26.743, 22.169, 18.37, 15.219, 12.551, 10.336, 8.512, 7.0103, 5.7738, 4.7527, 3.8924, 3.1879, 2.6111, 2.1387, 1.7519, 1.4349, 1.1754, 0.96279, 0.78869, 0.64609, 0.52858, 0.43116, 0.3517, 0.28689, 0.23403, 0.19092, 0.15576, 0.12707, 0.10367, 0.084585, 0.069013, 0.05631, 0.045946, 0.037491, 0.030592, 0.024964, 0.020372, 0.016625, 0.013567, 0.011072, 0.0090362, 0.0073748, 0.0060191, 0.0049127, 0.0040098, 0.0032729, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.072128, 0.073232, 0.0735264, 0.07401695, 0.075072, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.117404, 0.1180797, 0.119201, 0.1196802, 0.120399, 0.122196, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.912478, 0.9264445, 0.9301689, 0.93198, 0.9342948, 0.9357555, 0.946245, 0.950049, 0.955755, 0.97002, 0.9987612, 1.067676, 1.074178, 1.090619, 1.095004, 1.10158, 1.118022, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 8.799322, 8.934005, 8.969921, 9.030794, 9.158478, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'La': {'mass_absorption_coefficient (cm2/g)': [177890.0, 171900.0, 165690.0, 141980.0, 119920.0, 99244.0, 80886.0, 65208.0, 52195.0, 41616.0, 33142.0, 26424.0, 21132.0, 16978.0, 16021.0, 15260.0, 15066.0, 15071.0, 14404.0, 14027.0, 11523.0, 9567.9, 8034.4, 6825.4, 5866.0, 5098.5, 4478.7, 3972.6, 3555.6, 3209.5, 2917.7, 2668.0, 2451.1, 2260.4, 2090.4, 1937.5, 1931.6, 1899.0, 1890.5, 73550.0, 59572.0, 42453.0, 17115.0, 8618.1, 5499.3, 4131.8, 3458.2, 3111.8, 2946.5, 2897.1, 2926.3, 2930.5, 2942.4, 2947.2, 4449.5, 4465.0, 4506.0, 4508.4, 4532.5, 4539.3, 5028.8, 5054.9, 5103.2, 5246.3, 5398.1, 5540.9, 5556.0, 5584.6, 5592.0, 6176.9, 6197.2, 6220.6, 6273.7, 6270.5, 6202.4, 6067.6, 5869.3, 5611.2, 5300.3, 4953.0, 4584.9, 4209.9, 3839.1, 3481.1, 3141.5, 2824.3, 2531.5, 2275.1, 2263.2, 2217.2, 2199.1, 9135.3, 8918.6, 8833.1, 8816.0, 13211.0, 12737.0, 12433.0, 10550.0, 8992.3, 7652.7, 7107.6, 6852.5, 6786.7, 7801.5, 7593.0, 7518.5, 6985.1, 6740.7, 6678.0, 7032.3, 6900.8, 6790.0, 5925.2, 5641.1, 5459.9, 5413.3, 5598.3, 5427.6, 5381.1, 4643.2, 3984.9, 3411.8, 2918.6, 2491.6, 2126.6, 1816.9, 1550.6, 1322.8, 1129.5, 965.16, 818.7, 689.86, 578.63, 485.42, 407.81, 343.22, 289.39, 244.11, 205.23, 197.54, 189.83, 187.85, 570.51, 547.39, 529.11, 501.09, 480.77, 475.55, 647.32, 621.57, 609.55, 585.59, 561.97, 555.93, 632.82, 609.59, 591.4, 499.82, 421.76, 355.25, 299.02, 251.27, 209.85, 175.41, 146.7, 122.34, 101.83, 84.567, 70.288, 58.469, 48.671, 40.549, 33.812, 28.221, 23.575, 19.712, 16.472, 13.745, 11.356, 9.3433, 7.6906, 6.3355, 5.2235, 4.6602, 4.4609, 4.4097, 26.467, 26.334, 25.467, 22.124, 18.553, 15.548, 13.019, 10.871, 9.0752, 7.5744, 6.3207, 5.2741, 4.4005, 3.659, 3.0298, 2.509, 2.0779, 1.721, 1.4256, 1.1809, 0.97836, 0.81059, 0.67164, 0.55655, 0.46129, 0.38282, 0.31772, 0.26371, 0.21889, 0.1817, 0.15084, 0.12523, 0.10397, 0.086323, 0.071676, 0.059518, 0.049424, 0.041044, 0.034086, 0.0], - 'energies (keV)': [0.014472, 0.014688, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.031654, 0.0321385, 0.0322677, 0.0324615, 0.032946, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.096922, 0.0984055, 0.0988011, 0.0993945, 0.100878, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.187572, 0.1883732, 0.190443, 0.1912086, 0.192357, 0.195228, 0.2013709, 0.201684, 0.204771, 0.2055942, 0.206829, 0.209916, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.264992, 0.269048, 0.2701296, 0.271752, 0.275808, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.815066, 0.8175768, 0.8275415, 0.83153, 0.8358585, 0.8442575, 0.8476515, 0.848334, 0.8527425, 0.86547, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.100932, 1.117783, 1.122277, 1.129017, 1.141345, 1.145868, 1.180312, 1.198378, 1.203196, 1.210422, 1.220098, 1.228488, 1.304285, 1.334074, 1.354493, 1.359939, 1.368106, 1.388526, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.373046, 5.455286, 5.477217, 5.510113, 5.592354, 5.660855, 5.772788, 5.861147, 5.88471, 5.920053, 6.008412, 6.051453, 6.140974, 6.234969, 6.260034, 6.297632, 6.391626, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 38.14611, 38.72998, 38.88567, 39.11922, 39.19543, 39.70309, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Li': {'mass_absorption_coefficient (cm2/g)': [121650.0, 122300.0, 100640.0, 94014.0, 87249.0, 80477.0, 73813.0, 67348.0, 61155.0, 55284.0, 49772.0, 44641.0, 39900.0, 35543.0, 31553.0, 27922.0, 24637.0, 21683.0, 19037.0, 16678.0, 14583.0, 12729.0, 11092.0, 9650.9, 8385.6, 7277.0, 6307.6, 6148.2, 5950.1, 5898.8, 285130.0, 282100.0, 279090.0, 265970.0, 250690.0, 232170.0, 211710.0, 190470.0, 169350.0, 149040.0, 130010.0, 112540.0, 96781.0, 82763.0, 70439.0, 59708.0, 50439.0, 42485.0, 35697.0, 29930.0, 25050.0, 20934.0, 17470.0, 14561.0, 12120.0, 10077.0, 8368.7, 6943.0, 5754.6, 4765.3, 3942.4, 3258.8, 2691.5, 2221.0, 1831.3, 1508.8, 1242.1, 1021.7, 839.84, 689.86, 566.28, 464.55, 380.86, 312.08, 255.6, 209.24, 169.97, 137.66, 111.49, 90.296, 73.13, 59.228, 47.968, 38.85, 31.464, 25.483, 20.638, 16.715, 13.537, 10.963, 8.8782, 7.1897, 5.7955, 4.6515, 3.7333, 2.9963, 2.4048, 1.9301, 1.5491, 1.2433, 0.99788, 0.8009, 0.6428, 0.51591, 0.41407, 0.33233, 0.26673, 0.21408, 0.17168, 0.1373, 0.1098, 0.087811, 0.070225, 0.056161, 0.044913, 0.035918, 0.028725, 0.022972, 0.018371, 0.014692, 0.01175, 0.0093966, 0.0075147, 0.0060097, 0.0048144, 0.0038515, 0.0030794, 0.0024621, 0.0019686, 0.0015739, 0.0012584, 0.0010062, 0.00080446, 0.00064319, 0.00051426, 0.00041117, 0.00032875, 0.00026285, 0.00021016, 0.00016803, 0.00013435, 0.00010742, 8.5885e-05, 6.8669e-05, 5.4904e-05, 4.3899e-05, 3.5099e-05, 2.8064e-05, 2.2438e-05, 1.7941e-05, 1.4344e-05, 1.1469e-05, 9.1702e-06, 7.3321e-06, 5.8624e-06, 4.6873e-06, 3.7478e-06, 2.9966e-06, 2.3959e-06, 1.9157e-06, 1.5317e-06, 1.2247e-06, 9.7922e-07, 7.8295e-07, 6.2602e-07, 5.0054e-07, 4.0022e-07, 0.0], - 'energies (keV)': [0.0053667, 0.0054468, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.053655, 0.05447625, 0.05469525, 0.05502375, 0.055845, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Tl': {'mass_absorption_coefficient (cm2/g)': [20294.0, 19652.0, 16481.0, 13758.0, 10897.0, 8925.4, 8366.6, 8299.1, 8222.9, 39884.0, 37665.0, 31765.0, 24948.0, 24632.0, 23756.0, 23558.0, 39304.0, 38150.0, 36757.0, 34556.0, 36079.0, 41184.0, 49647.0, 61437.0, 76334.0, 93643.0, 112050.0, 129630.0, 144190.0, 153630.0, 156560.0, 152560.0, 142300.0, 127300.0, 109530.0, 90947.0, 73154.0, 57258.0, 43821.0, 32956.0, 24485.0, 18198.0, 18058.0, 16970.0, 16660.0, 23166.0, 21696.0, 19133.0, 14261.0, 10718.0, 8154.1, 7843.1, 7387.4, 7272.5, 9997.1, 9494.7, 8957.4, 7175.8, 6096.0, 5802.1, 5774.7, 5727.0, 6264.6, 6089.5, 6018.4, 5847.2, 5785.6, 6141.7, 5942.1, 5843.2, 5223.1, 5132.2, 5089.4, 5056.6, 5174.2, 5068.1, 4864.2, 4703.4, 4796.9, 5134.4, 5706.1, 6495.2, 7471.5, 8586.8, 9774.6, 10964.0, 12058.0, 12979.0, 13656.0, 14045.0, 14127.0, 14065.0, 14011.0, 13994.0, 14352.0, 14313.0, 14299.0, 14249.0, 14172.0, 14150.0, 14372.0, 14292.0, 14224.0, 13725.0, 13062.0, 12570.0, 12509.0, 12446.0, 12383.0, 12319.0, 12255.0, 12190.0, 12125.0, 12059.0, 11993.0, 11926.0, 11859.0, 11791.0, 11723.0, 11654.0, 11586.0, 11517.0, 11447.0, 11377.0, 11307.0, 11237.0, 11167.0, 11096.0, 11025.0, 10954.0, 10883.0, 10811.0, 10740.0, 10668.0, 10597.0, 10525.0, 10453.0, 10382.0, 10310.0, 10238.0, 10166.0, 10094.0, 10023.0, 9950.9, 9879.3, 9858.4, 10429.0, 10414.0, 10340.0, 10267.0, 10193.0, 10120.0, 10047.0, 9974.2, 9901.5, 9829.0, 9756.7, 9684.6, 9612.6, 9540.9, 9469.5, 9398.2, 9327.2, 9256.5, 9186.0, 9115.8, 9045.8, 8976.1, 8906.7, 8837.6, 8768.7, 8700.2, 8631.9, 8563.9, 8496.2, 8428.9, 8361.8, 8295.1, 8228.7, 8162.7, 8096.9, 8084.4, 8159.4, 8143.3, 8078.2, 8013.5, 7949.1, 7885.1, 7821.4, 7758.1, 7695.1, 7632.5, 7570.3, 7508.5, 7447.0, 7385.9, 7325.1, 7264.8, 7204.8, 7145.2, 7086.0, 7027.1, 6968.6, 6910.6, 6852.9, 6795.5, 6738.6, 6682.1, 6625.9, 6570.1, 6514.7, 6459.7, 6405.0, 6350.8, 6296.9, 6295.5, 6396.2, 6376.1, 6322.4, 6269.2, 6216.3, 6163.9, 6111.8, 6060.1, 6008.8, 5957.8, 5907.2, 5857.1, 5807.2, 5757.8, 5708.7, 5660.1, 5611.7, 5563.8, 5516.2, 5469.0, 5422.2, 5375.7, 5329.6, 5283.8, 5238.4, 5193.3, 5148.7, 5104.3, 5060.3, 5016.7, 4973.4, 4930.5, 4887.9, 4845.6, 4803.5, 4754.2, 4705.4, 4657.2, 4609.4, 4562.2, 4515.3, 4468.7, 4422.6, 4376.9, 4331.8, 4287.1, 4242.9, 4199.2, 4155.9, 4113.1, 4070.8, 4028.9, 3987.4, 3946.4, 3905.9, 3865.7, 3826.0, 3786.8, 3747.9, 3709.4, 3671.4, 3633.8, 3596.5, 3559.7, 3523.3, 3487.2, 3451.5, 3416.2, 3381.3, 3346.8, 3312.6, 3278.8, 3245.3, 3212.3, 3179.5, 3146.9, 3114.7, 3082.8, 3051.2, 3020.0, 2989.1, 2958.6, 2928.3, 2898.4, 2868.8, 2839.6, 2810.6, 2782.0, 2753.6, 2725.6, 2697.8, 2670.4, 2643.2, 2616.4, 2589.8, 2563.5, 2537.5, 2511.7, 2486.3, 2461.1, 2436.1, 2411.5, 2387.1, 2363.0, 2339.1, 2315.4, 2292.0, 2268.7, 2245.7, 2223.0, 2200.4, 2178.2, 2156.1, 2134.3, 2112.8, 2091.4, 2070.3, 2049.5, 2028.8, 2008.4, 1988.2, 1968.2, 1947.5, 1927.0, 1906.7, 1886.6, 1866.7, 1847.1, 1827.6, 1808.4, 1789.5, 1770.7, 1751.8, 1732.9, 1714.1, 1695.6, 1677.3, 1659.2, 1641.4, 1623.7, 1606.2, 1589.0, 1571.9, 1555.0, 1538.3, 1521.9, 1505.6, 1489.5, 1473.6, 1457.8, 1442.3, 1426.9, 1411.7, 1396.7, 1381.8, 1367.1, 1352.6, 1338.3, 1324.1, 1310.1, 1296.2, 1282.5, 1268.9, 1255.5, 1242.2, 1229.1, 1216.2, 1203.4, 1190.7, 1178.2, 1165.8, 1153.5, 1141.4, 1129.5, 1117.6, 1105.9, 1094.4, 1082.9, 1071.6, 1060.4, 1049.4, 1038.5, 1027.6, 1017.0, 1006.4, 995.94, 985.61, 975.39, 965.28, 955.29, 945.42, 935.65, 925.99, 916.43, 906.99, 897.46, 887.95, 878.55, 869.27, 860.09, 851.01, 842.05, 833.08, 824.13, 815.29, 806.55, 797.92, 789.39, 780.97, 776.32, 2249.3, 2240.0, 2212.3, 2185.1, 2158.1, 2131.4, 2105.0, 2079.0, 2053.3, 2042.1, 2978.3, 2961.5, 2924.3, 2887.6, 2851.4, 2815.7, 2780.4, 2745.6, 2711.2, 2677.3, 2643.8, 2610.7, 2578.0, 2545.8, 2514.0, 2482.6, 2451.6, 2421.0, 2390.7, 2360.9, 2331.5, 2302.4, 2273.7, 2245.4, 2217.4, 2189.8, 2162.5, 2135.6, 2109.0, 2082.8, 2056.9, 2031.3, 2006.0, 1981.1, 1956.4, 1932.1, 1929.1, 2252.6, 2238.5, 2209.0, 2180.0, 2150.7, 2121.7, 2093.4, 2065.5, 2038.1, 2011.1, 1984.5, 1959.0, 1933.9, 1909.1, 1884.7, 1860.7, 1837.0, 1813.6, 1790.6, 1767.6, 1745.0, 1722.7, 1700.7, 1679.0, 1657.6, 1636.5, 1615.7, 1595.1, 1574.8, 1555.2, 1554.7, 1652.4, 1642.9, 1621.4, 1600.2, 1579.4, 1558.8, 1538.4, 1518.4, 1498.6, 1479.0, 1459.8, 1441.3, 1423.2, 1405.3, 1387.6, 1370.2, 1353.1, 1351.5, 1396.7, 1395.7, 1378.7, 1361.8, 1345.3, 1328.9, 1312.8, 1296.9, 1281.2, 1265.5, 1250.0, 1234.8, 1219.7, 1204.9, 1190.2, 1175.8, 1161.6, 1147.5, 1133.7, 1120.0, 1106.5, 1093.2, 1080.1, 1067.1, 1054.3, 1041.7, 1029.3, 1017.0, 1004.8, 992.87, 981.05, 969.38, 957.86, 946.49, 935.26, 924.18, 913.23, 902.41, 891.56, 880.86, 870.28, 859.84, 849.53, 839.34, 829.28, 819.35, 809.54, 799.84, 790.27, 780.82, 771.48, 762.25, 753.01, 743.71, 734.5, 725.4, 716.42, 707.55, 698.8, 690.17, 681.54, 672.94, 664.46, 656.09, 647.83, 639.67, 631.63, 623.69, 615.86, 608.13, 600.5, 592.97, 585.54, 578.21, 570.98, 563.84, 556.79, 549.84, 542.98, 536.21, 529.52, 522.93, 516.42, 510.0, 503.66, 497.41, 491.24, 485.15, 479.14, 473.21, 467.35, 461.58, 455.88, 450.25, 444.7, 439.14, 433.61, 428.15, 422.77, 417.46, 412.22, 407.05, 401.94, 396.91, 391.94, 387.04, 382.2, 377.43, 372.72, 368.07, 363.49, 358.96, 354.5, 350.09, 345.74, 341.45, 337.22, 333.04, 328.92, 324.85, 320.84, 316.88, 312.97, 309.11, 305.28, 301.49, 297.75, 294.06, 290.42, 286.82, 283.27, 279.77, 276.31, 272.9, 269.53, 266.21, 262.91, 259.65, 256.44, 253.26, 250.13, 247.02, 243.96, 240.94, 237.96, 235.01, 232.1, 229.22, 226.38, 223.57, 220.81, 218.07, 215.38, 212.72, 210.09, 207.5, 204.94, 202.42, 199.93, 197.47, 195.04, 192.65, 190.28, 187.95, 185.64, 183.37, 181.13, 178.91, 176.73, 153.56, 128.54, 107.77, 90.191, 75.498, 66.055, 63.468, 63.299, 62.803, 161.78, 155.39, 138.2, 115.08, 110.45, 109.24, 150.98, 145.07, 143.82, 138.05, 136.57, 156.27, 155.69, 149.94, 131.85, 110.92, 93.302, 78.435, 65.913, 55.417, 46.619, 39.212, 32.753, 27.305, 22.715, 18.893, 15.726, 13.101, 10.922, 9.1128, 7.609, 6.3582, 5.3166, 4.4386, 3.7088, 3.096, 2.5767, 2.1456, 1.7827, 1.6543, 1.5841, 1.5661, 7.4619, 7.1742, 6.0431, 5.083, 4.2687, 3.5833, 3.0067, 2.5181, 2.1086, 1.7656, 1.4785, 1.2382, 1.0368, 0.86765, 0.72621, 0.60788, 0.50889, 0.42606, 0.35674, 0.29873, 0.25018, 0.20953, 0.1755, 0.14701, 0.12316, 0.10318, 0.0], - 'energies (keV)': [0.009713156, 0.009858128, 0.01069, 0.01142761, 0.01221612, 0.012838, 0.0130345, 0.01305903, 0.0130869, 0.0131655, 0.013362, 0.0139601, 0.01492335, 0.014994, 0.0152235, 0.0152847, 0.0153765, 0.015606, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.073892, 0.07401695, 0.075023, 0.0753246, 0.075777, 0.076908, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.097608, 0.099102, 0.0995004, 0.100098, 0.101592, 0.1033284, 0.1104581, 0.11613, 0.1179075, 0.1180797, 0.1183815, 0.1190925, 0.120344, 0.12087, 0.122186, 0.1226772, 0.123414, 0.125256, 0.1262272, 0.133574, 0.1349368, 0.1356185, 0.1361637, 0.1369815, 0.139026, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.378476, 0.384269, 0.3858138, 0.388131, 0.3924405, 0.393924, 0.398468, 0.404567, 0.4061934, 0.408633, 0.414732, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.60824487, 0.60975519, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72029019, 0.72230983, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84423174, 0.84676824, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.388655, 2.3899452, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.4843943, 2.4858058, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9536434, 2.9595566, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.410713, 3.4111733, 3.4206869, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.6962102, 3.7119896, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.40435, 12.59421, 12.60708, 12.64484, 12.72079, 12.91065, 13.47697, 14.40688, 14.62441, 14.6832, 14.77139, 14.99186, 15.03977, 15.26997, 15.33135, 15.40095, 15.42343, 15.65363, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 83.8198, 85.10275, 85.44487, 85.95806, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Lu': {'mass_absorption_coefficient (cm2/g)': [14520.0, 13954.0, 6259.5, 5932.8, 5717.2, 5593.3, 5547.8, 5571.3, 5657.7, 5803.0, 6005.0, 6262.8, 6576.1, 6945.6, 7372.4, 7857.6, 8402.5, 8476.8, 8611.1, 8647.0, 135460.0, 129620.0, 122860.0, 101270.0, 82827.0, 67202.0, 54709.0, 45144.0, 38072.0, 33006.0, 29490.0, 27143.0, 25990.0, 25708.0, 25659.0, 25640.0, 25725.0, 25503.0, 24991.0, 24589.0, 24502.0, 24631.0, 24898.0, 25249.0, 25638.0, 26034.0, 26230.0, 26258.0, 26286.0, 26314.0, 26342.0, 26369.0, 26396.0, 26424.0, 26450.0, 26477.0, 26504.0, 26530.0, 26556.0, 26582.0, 26607.0, 26633.0, 26658.0, 26682.0, 26707.0, 26731.0, 26755.0, 26779.0, 26802.0, 26825.0, 26848.0, 26871.0, 26893.0, 26915.0, 26937.0, 26958.0, 26979.0, 27000.0, 27021.0, 27041.0, 27061.0, 27080.0, 27099.0, 27118.0, 27137.0, 27155.0, 27173.0, 27190.0, 27207.0, 27224.0, 27241.0, 27257.0, 27273.0, 27288.0, 27303.0, 27318.0, 27332.0, 27346.0, 27355.0, 27276.0, 27196.0, 27116.0, 27035.0, 26953.0, 26871.0, 26789.0, 26706.0, 26622.0, 26538.0, 26453.0, 26368.0, 26282.0, 26196.0, 26109.0, 26022.0, 25934.0, 25846.0, 25757.0, 25668.0, 25578.0, 25488.0, 25397.0, 25306.0, 25215.0, 25123.0, 25030.0, 24937.0, 24844.0, 24751.0, 24657.0, 24562.0, 24467.0, 24372.0, 24277.0, 24181.0, 24084.0, 23988.0, 23891.0, 23793.0, 23696.0, 23598.0, 23500.0, 23401.0, 23302.0, 23203.0, 23104.0, 23004.0, 22904.0, 22804.0, 22703.0, 22603.0, 22502.0, 22401.0, 22299.0, 22198.0, 22096.0, 21994.0, 21892.0, 21789.0, 21687.0, 21584.0, 21481.0, 21378.0, 21275.0, 21172.0, 21068.0, 20965.0, 20861.0, 20757.0, 20654.0, 20550.0, 20446.0, 20342.0, 20237.0, 20133.0, 20029.0, 19925.0, 19820.0, 19716.0, 19612.0, 19533.0, 24710.0, 24695.0, 24380.0, 24060.0, 23749.0, 23447.0, 23153.0, 22868.0, 22591.0, 22321.0, 22058.0, 21916.0, 25183.0, 25145.0, 24752.0, 24371.0, 24002.0, 23644.0, 23298.0, 22962.0, 22637.0, 22321.0, 22014.0, 21717.0, 21432.0, 21156.0, 20890.0, 20633.0, 20385.0, 20145.0, 19912.0, 19686.0, 19466.0, 19254.0, 19049.0, 18852.0, 18660.0, 18475.0, 18295.0, 18120.0, 17950.0, 17784.0, 17623.0, 17466.0, 17312.0, 17162.0, 17015.0, 16871.0, 16730.0, 16591.0, 16456.0, 16322.0, 16191.0, 16063.0, 15936.0, 15811.0, 15688.0, 15568.0, 15448.0, 15331.0, 15215.0, 15100.0, 14987.0, 14876.0, 14766.0, 14657.0, 14549.0, 14443.0, 14338.0, 14234.0, 14131.0, 14029.0, 13928.0, 13829.0, 13730.0, 13632.0, 13536.0, 13440.0, 13345.0, 13251.0, 13158.0, 13066.0, 12975.0, 12884.0, 12795.0, 12706.0, 12618.0, 12530.0, 12444.0, 12358.0, 12273.0, 12189.0, 12106.0, 12023.0, 11941.0, 11860.0, 11779.0, 11699.0, 11620.0, 11542.0, 11464.0, 11387.0, 11310.0, 11234.0, 11159.0, 11085.0, 11011.0, 10938.0, 10865.0, 10793.0, 10722.0, 10651.0, 10581.0, 10511.0, 10442.0, 10374.0, 10306.0, 10239.0, 10173.0, 10107.0, 10041.0, 9976.3, 9912.1, 9848.3, 9785.2, 9722.6, 9710.9, 10533.0, 10513.0, 10450.0, 10387.0, 10325.0, 10264.0, 10203.0, 10142.0, 10082.0, 10023.0, 9963.5, 9905.0, 9847.0, 9789.4, 9732.4, 9675.8, 9619.7, 9564.0, 9508.8, 9454.1, 9399.8, 9345.9, 9292.5, 9239.5, 9186.9, 9134.7, 9082.9, 9048.4, 9227.9, 9216.0, 9176.8, 9126.2, 9076.0, 9026.1, 8976.6, 8927.4, 8878.6, 8830.1, 8782.0, 8734.3, 8686.8, 8639.7, 8592.9, 8546.4, 8500.2, 8454.4, 8408.8, 8363.5, 8318.5, 8273.8, 8229.3, 8185.1, 8141.2, 8097.6, 8054.1, 8011.0, 7968.0, 7925.3, 7882.9, 7840.6, 7798.6, 7756.8, 7715.2, 7673.8, 7632.6, 7591.6, 7550.8, 7510.1, 7469.7, 7429.4, 7389.3, 7355.3, 7349.4, 7578.0, 7556.7, 7516.3, 7476.1, 7436.0, 7396.1, 7356.4, 7316.8, 7277.3, 7237.9, 7198.7, 7159.6, 7120.7, 7081.8, 7043.1, 7004.5, 6966.0, 6927.7, 6889.4, 6851.3, 6813.2, 6775.3, 6737.4, 6699.7, 6662.0, 6624.5, 6587.0, 6549.6, 6512.3, 6475.0, 6437.9, 6400.8, 6363.8, 6326.9, 6290.1, 6253.4, 6216.7, 6180.1, 6143.6, 6107.2, 6070.9, 6034.6, 5998.4, 5962.3, 5926.2, 5890.3, 5854.4, 5818.6, 5782.9, 5747.2, 5711.7, 5676.2, 5640.8, 5605.5, 5570.2, 5535.1, 5500.0, 5465.0, 5430.1, 5395.3, 5360.6, 5325.9, 5291.4, 5256.9, 5222.4, 5188.0, 5153.6, 5119.3, 5085.2, 5051.1, 5017.1, 4983.2, 4949.4, 4915.7, 4882.1, 4848.6, 4815.2, 4782.0, 4748.8, 4715.7, 4682.8, 4649.9, 4617.2, 4584.6, 4552.0, 4519.7, 4487.4, 4455.2, 4423.2, 4391.2, 4359.4, 4327.6, 4296.0, 4264.5, 4233.1, 4201.9, 4170.7, 4139.7, 4108.8, 4078.0, 4047.3, 4016.8, 3986.4, 3956.1, 3926.0, 3896.0, 3866.1, 3836.3, 3806.6, 3777.1, 3747.7, 3718.5, 3689.4, 3660.5, 3631.8, 3603.2, 3574.8, 3546.5, 3518.4, 3490.5, 3462.8, 3435.2, 3407.8, 3380.6, 3353.5, 3326.6, 3299.9, 3273.4, 3247.0, 3220.9, 3194.9, 3169.1, 3143.5, 3118.0, 3092.8, 3067.7, 3042.8, 3014.8, 2980.3, 2946.3, 2912.8, 2879.6, 2846.9, 2814.6, 2782.6, 2751.1, 2720.0, 2689.2, 2658.9, 2628.9, 2599.3, 2570.0, 2541.1, 2512.6, 2484.4, 2456.5, 2429.0, 2401.9, 2375.1, 2348.6, 2322.4, 2296.5, 2271.0, 2245.8, 2220.9, 2196.3, 2172.0, 2147.9, 2124.2, 2100.8, 2077.6, 2054.8, 2032.2, 2009.9, 1987.8, 1966.0, 1944.5, 1923.3, 1902.3, 1881.5, 1861.0, 1840.7, 1820.7, 1801.0, 1781.4, 1762.1, 1743.0, 1724.2, 1705.6, 1687.2, 1669.0, 1651.0, 1633.3, 1615.7, 1598.4, 1581.3, 1564.3, 1547.6, 1531.1, 1514.7, 1498.6, 1482.6, 1466.9, 1451.3, 1435.9, 1420.7, 1405.6, 1390.8, 1376.1, 1361.6, 1347.0, 1332.6, 1318.3, 1304.2, 1290.2, 1276.4, 1262.8, 1249.3, 1236.0, 1222.8, 1209.8, 1197.0, 1184.3, 1171.7, 1159.3, 1147.1, 1134.9, 1123.0, 1111.1, 1099.4, 1094.5, 3986.0, 3960.7, 3911.5, 3862.9, 3814.9, 3767.6, 3720.8, 3687.7, 5515.7, 5504.8, 5435.7, 5367.4, 5300.0, 5233.5, 5167.8, 5103.0, 5038.9, 4975.7, 4913.3, 4851.8, 4790.9, 4730.9, 4671.6, 4613.1, 4555.4, 4498.3, 4442.0, 4386.4, 4331.5, 4277.4, 4223.9, 4171.1, 4118.9, 4067.4, 4016.6, 3966.5, 3916.9, 3868.0, 3819.7, 3772.1, 3725.0, 3678.5, 3632.6, 3587.3, 3542.6, 3498.5, 3454.9, 3411.8, 3369.4, 3327.4, 3286.0, 3254.5, 3819.8, 3808.5, 3769.1, 3719.2, 3669.9, 3621.3, 3573.0, 3525.4, 3478.4, 3432.1, 3386.3, 3342.3, 3300.6, 3259.5, 3219.1, 3179.4, 3140.3, 3101.8, 3063.9, 3026.6, 2989.9, 2953.8, 2918.2, 2883.1, 2875.5, 3062.3, 3050.7, 3012.8, 2975.5, 2938.7, 2902.5, 2866.8, 2831.6, 2796.9, 2762.7, 2728.9, 2696.9, 2665.4, 2634.5, 2604.0, 2573.9, 2544.3, 2515.2, 2486.4, 2458.1, 2450.5, 2542.7, 2540.3, 2514.1, 2485.8, 2457.9, 2430.4, 2403.2, 2376.4, 2349.9, 2323.7, 2297.8, 2272.3, 2247.0, 2221.7, 2196.8, 2172.1, 2147.8, 2123.7, 2099.9, 2076.3, 2053.1, 2030.1, 2007.3, 1984.8, 1962.6, 1940.6, 1918.9, 1897.3, 1876.1, 1855.0, 1834.2, 1813.6, 1793.2, 1773.1, 1753.2, 1733.3, 1713.8, 1694.4, 1675.2, 1654.8, 1634.7, 1614.8, 1595.2, 1575.9, 1556.7, 1537.8, 1519.2, 1500.7, 1482.5, 1464.5, 1446.4, 1428.4, 1410.5, 1392.9, 1375.5, 1358.3, 1341.3, 1324.2, 1307.3, 1290.7, 1274.3, 1258.1, 1242.1, 1226.4, 1210.9, 1195.5, 1180.4, 1165.5, 1150.8, 1136.3, 1122.0, 1107.9, 1093.9, 1080.2, 1066.6, 1053.2, 1040.0, 1027.0, 1014.2, 1001.4, 988.8, 976.38, 964.13, 952.04, 940.12, 928.35, 916.75, 905.3, 893.98, 882.79, 871.74, 860.84, 850.09, 839.49, 829.02, 818.7, 784.92, 661.67, 558.18, 470.67, 397.01, 335.3, 283.56, 240.14, 203.51, 171.83, 145.18, 122.84, 103.53, 102.62, 98.368, 97.275, 269.44, 256.62, 237.49, 205.15, 196.46, 195.22, 194.25, 268.78, 257.25, 250.39, 239.5, 236.72, 270.03, 263.25, 259.84, 222.09, 187.32, 157.87, 132.55, 111.12, 93.125, 78.055, 65.429, 54.805, 45.919, 38.426, 32.172, 26.96, 22.427, 18.622, 15.476, 12.872, 10.714, 8.9249, 7.4285, 6.1818, 5.1354, 4.2538, 3.525, 2.922, 2.4769, 2.424, 2.3739, 2.3475, 12.298, 11.848, 10.86, 9.123, 7.6531, 6.4189, 5.3623, 4.4784, 3.7407, 3.1249, 2.6108, 2.1816, 1.82, 1.5148, 1.261, 1.0497, 0.87398, 0.72772, 0.60599, 0.50466, 0.42032, 0.35009, 0.29163, 0.24294, 0.20354, 0.17073, 0.14322, 0.12015, 0.1008, 0.084576, 0.070966, 0.0], - 'energies (keV)': [0.0069345, 0.007038, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02744, 0.02786, 0.027972, 0.02814, 0.02856, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.055664, 0.056516, 0.05667876, 0.0567432, 0.057084, 0.057936, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19485647, 0.19509776, 0.19514351, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20462018, 0.20497981, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.35885446, 0.35974552, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.40952587, 0.41019983, 0.41067415, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.50541541, 0.5057898, 0.50698463, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5882395, 1.5887605, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6388836, 1.6399164, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0213134, 2.0236492, 2.0258868, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2608064, 2.2661935, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4861428, 2.4952257, 2.4962571, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.059218, 9.197879, 9.234856, 9.29032, 9.428982, 9.653919, 10.14163, 10.29686, 10.32004, 10.33825, 10.40034, 10.55557, 10.65299, 10.81605, 10.85953, 10.92475, 11.03212, 11.08781, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.04752, 62.5287, 62.99723, 63.25049, 63.63037, 64.58008, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Th': {'mass_absorption_coefficient (cm2/g)': [26113.0, 25526.0, 24152.0, 17961.0, 14989.0, 13848.0, 13563.0, 26767.0, 25896.0, 24995.0, 19089.0, 14190.0, 12551.0, 11762.0, 11563.0, 11958.0, 11310.0, 11231.0, 8573.5, 6564.9, 5082.9, 3976.4, 3142.1, 2951.0, 2802.6, 2764.8, 21513.0, 20767.0, 20353.0, 19327.0, 18645.0, 18469.0, 29390.0, 28528.0, 28249.0, 24230.0, 19799.0, 15330.0, 11481.0, 8466.5, 6237.1, 4640.9, 3515.4, 2724.1, 2618.2, 2481.6, 2447.2, 3417.0, 3273.5, 3131.4, 2625.3, 2247.6, 2054.5, 1994.7, 1979.5, 2468.2, 2457.6, 2375.9, 2132.7, 1874.7, 1678.3, 1649.1, 1612.6, 1603.3, 1718.8, 1685.5, 1653.3, 1528.5, 1492.2, 1468.8, 1462.8, 1613.2, 1614.8, 1630.0, 1633.4, 1636.9, 1640.1, 1774.3, 1840.2, 2181.2, 3282.8, 5024.0, 7096.9, 9038.9, 10459.0, 11181.0, 11235.0, 10775.0, 10113.0, 9986.4, 9910.7, 9855.8, 10664.0, 10458.0, 10255.0, 10035.0, 9975.8, 10438.0, 10393.0, 10178.0, 9465.3, 8504.4, 7589.3, 6745.8, 6573.3, 6397.3, 6351.5, 6697.3, 6527.0, 6390.1, 5650.9, 4988.0, 4958.9, 4813.8, 4776.2, 4789.0, 4652.7, 4439.8, 3890.8, 3882.6, 3773.7, 3743.4, 3771.0, 3656.2, 3443.8, 2975.0, 2575.0, 2230.0, 1931.8, 1675.9, 1456.6, 1268.5, 1106.7, 966.76, 845.64, 735.37, 628.75, 553.55, 532.81, 531.17, 527.47, 1257.6, 1236.3, 1227.1, 1206.3, 1200.9, 1744.5, 1691.7, 1675.8, 1406.4, 1243.3, 1191.4, 1178.0, 1369.1, 1358.9, 1306.2, 1146.7, 965.79, 913.83, 878.64, 869.59, 912.18, 879.14, 867.43, 815.45, 785.33, 777.57, 799.99, 771.87, 768.5, 653.52, 555.88, 472.81, 401.12, 339.66, 288.06, 244.47, 206.98, 173.37, 145.17, 121.63, 102.07, 85.796, 72.173, 60.776, 51.104, 46.479, 44.691, 44.231, 108.36, 106.97, 104.28, 89.813, 74.92, 69.883, 67.039, 66.307, 92.578, 89.33, 89.045, 88.754, 85.838, 84.939, 96.723, 93.221, 86.632, 73.089, 61.636, 52.003, 43.603, 36.507, 30.593, 25.602, 21.431, 17.959, 15.01, 12.525, 10.459, 8.7406, 7.3101, 6.1184, 5.1248, 4.2957, 3.6023, 3.0185, 2.5247, 2.0997, 1.742, 1.4453, 1.2, 1.1747, 1.1261, 1.1136, 4.899, 4.7186, 4.4961, 3.7931, 3.1965, 2.6916, 2.266, 1.9068, 1.6032, 1.3481, 1.1336, 0.95331, 0.80183, 0.67486, 0.56826, 0.47856, 0.40306, 0.33951, 0.28602, 0.24097, 0.20304, 0.1711, 0.1442, 0.0], - 'energies (keV)': [0.043215, 0.04340198, 0.04386, 0.04639671, 0.04802, 0.048755, 0.048951, 0.049245, 0.04959809, 0.04998, 0.05302035, 0.05667876, 0.05831, 0.0592025, 0.0594405, 0.0597975, 0.06058959, 0.06069, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.086142, 0.0874605, 0.0878121, 0.0883395, 0.089658, 0.09041995, 0.092414, 0.0938285, 0.0942057, 0.0947715, 0.096186, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.178164, 0.180891, 0.1816182, 0.182709, 0.185436, 0.1883732, 0.2013709, 0.2152655, 0.224812, 0.228253, 0.2291706, 0.2301188, 0.230547, 0.233988, 0.245997, 0.2629708, 0.2811158, 0.284396, 0.288749, 0.2899098, 0.291651, 0.296004, 0.3005128, 0.3212482, 0.328496, 0.333524, 0.3348648, 0.336876, 0.337512, 0.341904, 0.342678, 0.3434143, 0.3440556, 0.346122, 0.351288, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.662872, 0.6692609, 0.673018, 0.6757236, 0.679782, 0.689928, 0.699818, 0.7105295, 0.7133859, 0.7154399, 0.7176705, 0.728382, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.947954, 0.9624635, 0.9663327, 0.9721365, 0.986646, 0.9987612, 1.067676, 1.141345, 1.144836, 1.162359, 1.167032, 1.174041, 1.191564, 1.220098, 1.30291, 1.304285, 1.322852, 1.32817, 1.336147, 1.35609, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.26536, 3.31534, 3.319406, 3.328668, 3.34866, 3.39864, 3.420984, 3.473346, 3.487309, 3.508254, 3.548445, 3.560616, 3.793288, 3.965178, 4.02587, 4.042054, 4.055024, 4.066331, 4.127022, 4.334821, 4.633924, 4.733792, 4.806248, 4.82557, 4.854552, 4.927008, 4.953664, 5.078654, 5.156389, 5.177118, 5.208212, 5.285946, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 15.97429, 16.2188, 16.284, 16.3818, 16.46362, 16.62631, 17.59961, 18.81398, 19.29934, 19.59473, 19.67351, 19.79167, 20.06266, 20.08706, 20.11215, 20.36974, 20.45163, 20.57446, 20.88154, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 107.4579, 109.1026, 109.5413, 110.1992, 111.8439, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ti': {'mass_absorption_coefficient (cm2/g)': [111400.0, 98018.0, 92523.0, 51992.0, 31199.0, 21267.0, 16102.0, 13276.0, 11718.0, 10910.0, 10661.0, 10607.0, 10596.0, 15404.0, 15403.0, 15399.0, 15493.0, 15797.0, 16205.0, 16658.0, 17114.0, 17531.0, 17872.0, 18104.0, 18200.0, 18139.0, 17912.0, 17516.0, 16955.0, 16226.0, 15360.0, 14391.0, 13356.0, 12289.0, 11219.0, 10172.0, 9165.7, 8214.5, 7327.3, 6509.6, 5763.4, 5088.1, 4481.1, 3938.6, 3456.2, 3056.8, 3028.8, 2978.3, 2965.9, 2942.3, 21324.0, 21175.0, 20984.0, 29708.0, 29591.0, 28727.0, 27563.0, 23693.0, 20317.0, 19928.0, 19233.0, 19053.0, 21351.0, 20658.0, 19825.0, 17060.0, 14652.0, 12559.0, 10756.0, 9172.5, 7808.9, 6644.9, 5650.9, 4755.7, 4000.6, 3364.1, 2828.9, 2379.3, 2001.7, 1684.7, 1418.6, 1195.2, 1000.5, 837.97, 702.72, 587.72, 491.51, 411.44, 344.73, 286.33, 235.89, 194.54, 160.6, 132.71, 109.78, 90.896, 79.162, 75.855, 75.337, 75.006, 695.81, 666.98, 589.19, 494.51, 416.81, 351.68, 294.79, 246.81, 206.48, 172.69, 144.26, 120.08, 99.571, 82.293, 68.002, 56.196, 46.444, 38.386, 31.728, 26.226, 21.679, 17.922, 14.816, 12.25, 10.064, 8.2686, 6.7934, 5.5507, 4.5269, 3.6921, 3.0113, 2.4561, 2.0034, 1.6341, 1.3329, 1.0873, 0.88697, 0.72355, 0.59026, 0.48153, 0.39284, 0.32048, 0.26146, 0.21297, 0.17282, 0.14025, 0.11382, 0.092368, 0.074961, 0.060836, 0.049372, 0.040069, 0.03252, 0.026393, 0.02142, 0.017385, 0.01411, 0.011452, 0.0092946, 0.0075438, 0.0061229, 0.0049696, 0.0040336, 0.0032739, 0.0026573, 0.0021568, 0.0017506, 0.0014209, 0.0011534, 0.0], - 'energies (keV)': [0.034773, 0.035292, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.059094, 0.0599985, 0.0602397, 0.06058959, 0.0606015, 0.061506, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.44639, 0.4484657, 0.45227, 0.4532225, 0.4550445, 0.4577775, 0.4591925, 0.4610385, 0.4638075, 0.46461, 0.47073, 0.4794098, 0.5124891, 0.5478508, 0.552426, 0.5608815, 0.5631363, 0.5665185, 0.574974, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.867072, 4.941568, 4.953664, 4.961434, 4.991232, 5.065728, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Te': {'mass_absorption_coefficient (cm2/g)': [2221.0, 3181.0, 15848.0, 64072.0, 157560.0, 259590.0, 312900.0, 297280.0, 237040.0, 167130.0, 108780.0, 67671.0, 41375.0, 25407.0, 15929.0, 10319.0, 7913.9, 7256.2, 7094.6, 10363.0, 10245.0, 9623.1, 7934.7, 6338.4, 5298.0, 4609.3, 4146.9, 3835.1, 3833.0, 3778.1, 3764.5, 4435.9, 4390.6, 4320.4, 4187.2, 4097.1, 4033.4, 3986.3, 3948.5, 3913.6, 3876.4, 3831.8, 3775.7, 3704.3, 3615.0, 3506.3, 3378.1, 3231.2, 3067.4, 2890.0, 2703.1, 2636.9, 2593.2, 2585.1, 2581.7, 16804.0, 16416.0, 16226.0, 25348.0, 25074.0, 24011.0, 20522.0, 16961.0, 14304.0, 12193.0, 10893.0, 10513.0, 10424.0, 10415.0, 11700.0, 11328.0, 10836.0, 10484.0, 10393.0, 10832.0, 10831.0, 10498.0, 9411.3, 8400.3, 8175.3, 8135.8, 8060.4, 8317.4, 8040.1, 7341.5, 6303.8, 5413.5, 4650.5, 3992.4, 3416.1, 2918.6, 2494.1, 2132.3, 1824.0, 1560.6, 1334.3, 1137.2, 966.18, 822.17, 700.75, 592.07, 495.82, 413.8, 345.19, 288.44, 253.7, 243.66, 241.41, 241.08, 811.0, 772.63, 722.68, 689.38, 681.37, 938.61, 937.92, 896.96, 824.31, 790.17, 781.49, 895.14, 890.36, 857.04, 756.22, 642.24, 542.78, 457.04, 384.05, 322.6, 271.07, 227.97, 191.31, 159.13, 132.16, 109.74, 91.188, 75.838, 63.13, 52.599, 43.864, 36.583, 30.528, 25.499, 21.246, 17.689, 14.67, 12.177, 10.117, 8.3299, 6.842, 6.1184, 5.8517, 5.7833, 36.046, 35.714, 34.662, 29.937, 25.082, 21.01, 17.586, 14.675, 12.242, 10.209, 8.5123, 7.0963, 5.9157, 4.9167, 4.0824, 3.3899, 2.8108, 2.3227, 1.9195, 1.5864, 1.3112, 1.0838, 0.89596, 0.7407, 0.6124, 0.50639, 0.41875, 0.3463, 0.28641, 0.23689, 0.19594, 0.16208, 0.13407, 0.11092, 0.091763, 0.075921, 0.062817, 0.051977, 0.04301, 0.035591, 0.029454, 0.024376, 0.0], - 'energies (keV)': [0.039999, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.107996, 0.109649, 0.1100898, 0.1104581, 0.110751, 0.112404, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.164934, 0.1674585, 0.1681317, 0.1691415, 0.171666, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.560658, 0.5692395, 0.57085, 0.5715279, 0.5749605, 0.5795875, 0.5819175, 0.583542, 0.5856525, 0.59415, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.802326, 0.8146065, 0.8175768, 0.8178813, 0.8227935, 0.835074, 0.852306, 0.8653515, 0.8688303, 0.8739896, 0.8740485, 0.887094, 0.9342948, 0.98588, 0.9987612, 1.00097, 1.004994, 1.01103, 1.02612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.254572, 4.319693, 4.334821, 4.337059, 4.363107, 4.428228, 4.51976, 4.58894, 4.607388, 4.633924, 4.63506, 4.70424, 4.840416, 4.914504, 4.934261, 4.953664, 4.963896, 5.037984, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 31.17752, 31.65473, 31.78199, 31.97287, 32.08502, 32.45008, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Tb': {'mass_absorption_coefficient (cm2/g)': [1353.5, 1305.3, 1157.7, 1115.4, 1104.5, 4760.4, 4590.3, 3591.1, 3244.6, 3058.9, 2989.9, 3013.2, 3115.2, 3289.4, 3533.6, 3848.2, 4235.6, 4699.1, 5242.1, 5868.1, 6333.4, 6500.8, 6545.8, 123840.0, 122780.0, 117750.0, 102810.0, 85574.0, 70367.0, 57703.0, 47723.0, 40199.0, 39601.0, 38233.0, 37888.0, 37608.0, 36435.0, 34958.0, 31126.0, 28525.0, 26789.0, 25638.0, 24895.0, 24407.0, 24067.0, 23796.0, 23544.0, 23275.0, 22968.0, 22615.0, 22210.0, 21985.0, 21951.0, 21917.0, 21882.0, 21847.0, 21812.0, 21777.0, 21741.0, 21705.0, 21669.0, 21633.0, 21597.0, 21560.0, 21523.0, 21486.0, 21448.0, 21411.0, 21373.0, 21335.0, 21296.0, 21258.0, 21219.0, 21180.0, 21141.0, 21102.0, 21062.0, 21023.0, 20983.0, 20943.0, 20903.0, 20862.0, 20822.0, 20781.0, 20740.0, 20699.0, 20658.0, 20616.0, 20575.0, 20533.0, 20491.0, 20449.0, 20407.0, 20365.0, 20323.0, 20280.0, 20238.0, 20195.0, 20152.0, 20109.0, 20066.0, 20023.0, 19979.0, 19936.0, 19892.0, 19849.0, 19805.0, 19761.0, 19717.0, 19673.0, 19629.0, 19585.0, 19541.0, 19497.0, 19453.0, 19408.0, 19364.0, 19319.0, 19275.0, 19230.0, 19185.0, 19141.0, 19096.0, 19051.0, 19006.0, 18962.0, 18917.0, 18872.0, 18827.0, 18819.0, 48209.0, 46899.0, 45104.0, 43418.0, 41833.0, 40345.0, 38946.0, 37632.0, 36397.0, 35236.0, 34144.0, 33118.0, 32152.0, 31244.0, 30390.0, 29586.0, 28829.0, 28117.0, 27446.0, 26815.0, 26203.0, 25634.0, 25107.0, 24618.0, 24164.0, 23741.0, 23347.0, 22978.0, 22632.0, 22308.0, 22003.0, 21717.0, 21446.0, 21190.0, 20948.0, 20719.0, 20501.0, 20290.0, 20036.0, 19791.0, 19555.0, 19326.0, 19105.0, 18891.0, 18683.0, 18481.0, 18285.0, 18094.0, 17908.0, 17727.0, 17550.0, 17377.0, 17208.0, 17042.0, 16880.0, 16721.0, 16566.0, 16413.0, 16263.0, 16115.0, 15971.0, 15828.0, 15688.0, 15550.0, 15414.0, 15281.0, 15149.0, 15019.0, 14891.0, 14765.0, 14640.0, 14517.0, 14396.0, 14276.0, 14157.0, 14040.0, 13925.0, 13811.0, 13698.0, 13587.0, 13477.0, 13368.0, 13260.0, 13154.0, 13048.0, 12944.0, 12841.0, 12740.0, 12639.0, 12539.0, 12441.0, 12343.0, 12247.0, 12151.0, 12057.0, 11963.0, 11871.0, 11780.0, 11689.0, 11599.0, 11511.0, 11423.0, 11336.0, 11251.0, 11166.0, 11082.0, 10998.0, 10916.0, 10835.0, 10754.0, 10675.0, 10596.0, 10518.0, 10441.0, 10364.0, 10289.0, 10214.0, 10140.0, 10067.0, 9995.0, 9923.6, 9852.9, 9783.0, 9713.9, 9645.5, 9578.0, 9511.1, 9445.0, 9379.7, 9315.1, 9251.2, 9188.1, 9125.6, 9075.6, 10107.0, 10097.0, 10045.0, 9983.6, 9922.7, 9862.5, 9802.9, 9744.1, 9685.9, 9628.3, 9571.4, 9515.2, 9459.6, 9404.6, 9350.3, 9296.5, 9243.4, 9190.9, 9150.7, 9423.4, 9414.2, 9372.0, 9321.3, 9271.1, 9221.4, 9172.2, 9123.6, 9075.5, 9027.9, 8980.8, 8934.2, 8888.0, 8842.4, 8797.1, 8752.4, 8708.1, 8664.2, 8620.8, 8577.8, 8535.2, 8493.0, 8451.2, 8409.8, 8368.8, 8328.2, 8287.9, 8248.0, 8208.4, 8169.2, 8130.3, 8091.7, 8053.4, 8015.5, 7977.8, 7940.4, 7903.3, 7866.5, 7830.0, 7793.7, 7757.7, 7721.9, 7686.3, 7651.0, 7615.9, 7581.0, 7546.3, 7511.8, 7477.5, 7443.4, 7409.5, 7387.8, 7724.2, 7719.1, 7689.6, 7655.2, 7621.0, 7586.8, 7552.9, 7519.0, 7485.3, 7451.8, 7418.3, 7384.9, 7351.7, 7318.6, 7285.5, 7252.6, 7219.7, 7186.9, 7154.2, 7121.6, 7089.1, 7056.6, 7024.1, 6991.7, 6959.3, 6927.0, 6894.7, 6862.4, 6830.2, 6797.9, 6765.7, 6733.6, 6701.4, 6669.2, 6637.1, 6605.0, 6572.8, 6540.7, 6508.6, 6476.4, 6444.2, 6412.0, 6379.9, 6347.7, 6315.5, 6283.3, 6251.1, 6218.9, 6186.7, 6154.5, 6122.3, 6090.1, 6057.8, 6025.6, 5993.4, 5961.1, 5928.9, 5896.6, 5864.3, 5832.1, 5799.8, 5767.5, 5735.3, 5703.0, 5670.7, 5638.5, 5606.2, 5574.0, 5541.7, 5509.5, 5477.2, 5445.0, 5412.8, 5380.6, 5348.4, 5316.3, 5284.1, 5252.0, 5219.9, 5187.8, 5155.7, 5123.7, 5091.6, 5059.5, 5027.4, 4995.3, 4963.2, 4931.1, 4899.1, 4867.0, 4835.0, 4803.0, 4771.1, 4739.1, 4707.2, 4675.4, 4643.6, 4611.8, 4580.1, 4548.5, 4516.9, 4485.4, 4454.0, 4422.6, 4391.3, 4360.1, 4328.9, 4297.9, 4266.9, 4236.0, 4205.2, 4174.5, 4143.9, 4113.5, 4083.1, 4052.8, 4022.6, 3992.6, 3962.7, 3932.9, 3903.2, 3873.6, 3844.1, 3814.8, 3785.6, 3756.5, 3727.6, 3698.8, 3670.1, 3641.6, 3613.2, 3585.0, 3556.9, 3528.9, 3501.1, 3473.4, 3445.9, 3418.5, 3391.3, 3364.3, 3337.4, 3310.6, 3284.0, 3257.6, 3231.3, 3205.2, 3179.2, 3153.4, 3127.8, 3102.3, 3077.0, 3051.8, 3026.9, 3002.0, 2977.4, 2952.9, 2928.5, 2904.3, 2880.3, 2856.5, 2832.8, 2809.3, 2786.0, 2762.8, 2739.7, 2716.9, 2694.2, 2671.7, 2649.3, 2627.1, 2605.1, 2583.2, 2561.5, 2539.9, 2518.6, 2497.3, 2476.3, 2455.4, 2434.6, 2414.0, 2393.6, 2373.4, 2353.2, 2333.3, 2313.5, 2293.9, 2272.8, 2248.6, 2224.8, 2201.2, 2177.8, 2154.7, 2131.9, 2109.3, 2087.0, 2065.0, 2043.2, 2021.6, 2000.3, 1979.2, 1958.3, 1937.7, 1917.3, 1897.1, 1877.2, 1857.5, 1838.0, 1818.7, 1799.6, 1780.8, 1762.2, 1743.7, 1725.5, 1707.0, 1688.8, 1670.7, 1652.9, 1635.3, 1617.8, 1600.6, 1583.6, 1566.7, 1550.1, 1533.6, 1517.3, 1501.2, 1485.3, 1469.6, 1454.0, 1439.2, 5641.9, 5640.7, 5570.5, 5500.0, 5430.4, 5361.7, 5293.8, 5271.3, 7947.4, 7888.4, 7787.6, 7688.1, 7590.0, 7493.0, 7397.4, 7303.0, 7209.8, 7117.8, 7027.0, 6937.4, 6848.9, 6761.6, 6675.4, 6590.3, 6506.3, 6423.4, 6341.6, 6260.8, 6181.1, 6102.4, 6024.7, 5948.0, 5872.3, 5797.6, 5723.8, 5651.0, 5579.2, 5508.2, 5438.2, 5369.1, 5300.9, 5233.5, 5167.0, 5101.4, 5036.6, 4972.7, 4909.6, 4847.3, 4785.8, 4725.0, 4665.1, 4605.9, 4547.5, 4489.9, 4432.9, 4376.7, 4369.2, 5097.5, 5063.5, 4997.2, 4931.7, 4867.1, 4803.4, 4740.6, 4678.5, 4617.2, 4556.7, 4497.2, 4441.4, 4386.4, 4332.3, 4279.1, 4226.7, 4175.1, 4124.3, 4074.3, 4039.6, 4310.3, 4306.2, 4256.6, 4203.7, 4151.6, 4100.3, 4049.7, 3999.8, 3950.7, 3902.3, 3854.6, 3808.3, 3763.6, 3719.6, 3676.3, 3633.7, 3591.6, 3550.3, 3509.5, 3469.3, 3429.8, 3390.7, 3352.3, 3351.2, 3485.0, 3472.8, 3433.9, 3395.6, 3357.8, 3320.4, 3283.6, 3247.3, 3211.4, 3175.9, 3140.9, 3106.6, 3072.6, 3039.1, 3005.1, 2971.5, 2938.4, 2905.6, 2873.2, 2841.2, 2809.6, 2778.4, 2747.5, 2717.0, 2686.8, 2657.0, 2627.5, 2598.3, 2569.5, 2541.0, 2512.8, 2484.9, 2457.1, 2429.5, 2402.1, 2375.0, 2348.3, 2321.8, 2295.6, 2269.7, 2244.1, 2218.8, 2193.8, 2169.0, 2144.6, 2120.4, 2096.4, 2072.7, 2048.8, 2025.2, 2001.9, 1978.8, 1956.0, 1933.2, 1910.6, 1888.2, 1866.0, 1844.2, 1822.6, 1801.2, 1780.1, 1759.3, 1738.6, 1718.3, 1698.2, 1678.3, 1658.7, 1639.3, 1620.1, 1601.0, 1582.0, 1563.3, 1544.8, 1526.5, 1508.5, 1490.7, 1473.1, 1455.7, 1438.5, 1421.6, 1404.9, 1388.4, 1372.0, 1355.9, 1340.0, 1324.3, 1307.4, 1290.7, 1274.3, 1258.1, 1242.1, 1226.3, 1210.7, 1195.4, 1180.2, 1165.3, 1150.6, 1136.0, 1121.7, 1107.5, 1093.4, 1079.3, 1065.4, 1051.8, 1038.3, 1024.9, 1011.8, 998.86, 986.08, 973.48, 961.04, 948.77, 936.67, 924.73, 912.95, 901.33, 889.87, 878.56, 867.38, 856.22, 845.21, 834.35, 823.63, 813.06, 802.63, 792.34, 782.19, 772.18, 762.3, 752.55, 742.94, 733.45, 724.09, 714.85, 705.74, 696.76, 687.89, 679.14, 670.51, 661.99, 653.55, 645.2, 636.96, 610.09, 514.29, 434.11, 366.94, 310.57, 262.02, 220.8, 186.37, 157.6, 134.25, 132.92, 129.15, 127.84, 366.89, 350.07, 317.87, 298.17, 286.18, 283.12, 388.47, 372.51, 368.63, 358.27, 343.33, 339.76, 386.94, 372.96, 357.75, 301.87, 253.03, 211.22, 176.52, 147.76, 123.79, 103.81, 86.989, 72.778, 60.787, 50.689, 42.305, 35.34, 29.547, 24.726, 20.71, 17.224, 14.3, 11.883, 9.8632, 8.1814, 6.7694, 5.5943, 4.6233, 3.8238, 3.2055, 3.165, 3.0707, 3.0361, 16.845, 16.223, 14.984, 12.584, 10.551, 8.8423, 7.4025, 6.196, 5.179, 4.3157, 3.5967, 2.9979, 2.4951, 2.0715, 1.72, 1.4282, 1.1861, 0.98506, 0.81819, 0.67965, 0.56461, 0.46907, 0.38974, 0.32384, 0.27008, 0.22548, 0.18825, 0.15719, 0.13126, 0.10961, 0.091536, 0.076449, 0.063851, 0.053333, 0.0], - 'energies (keV)': [0.008643, 0.008772, 0.009212, 0.009353, 0.0093906, 0.009447, 0.009588, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.024892, 0.025273, 0.0253746, 0.02545001, 0.025527, 0.025908, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.03822, 0.038805, 0.038961, 0.039195, 0.03978, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14695355, 0.14704645, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.2847492, 0.28501845, 0.2852508, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.3098867, 0.31023873, 0.31051331, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39739863, 0.39810635, 0.39840134, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2410448, 1.2412555, 1.2413551, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2747297, 1.2752703, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6098611, 1.6127389, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7660507, 1.7686894, 1.7693492, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9642536, 1.9707463, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.36372, 7.392525, 7.47643, 7.506486, 7.55157, 7.66428, 7.902609, 8.086568, 8.210342, 8.243349, 8.292858, 8.416632, 8.44789, 8.53384, 8.66446, 8.699292, 8.75154, 8.88216, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 50.95579, 51.18542, 51.73572, 51.94371, 52.25568, 53.03561, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Tc': {'mass_absorption_coefficient (cm2/g)': [17960.0, 17147.0, 17090.0, 16297.0, 16094.0, 82638.0, 79014.0, 77220.0, 92123.0, 110490.0, 131680.0, 154530.0, 177310.0, 197920.0, 214160.0, 224150.0, 226730.0, 221630.0, 209570.0, 191970.0, 170710.0, 147730.0, 124740.0, 103070.0, 83640.0, 66912.0, 52957.0, 52256.0, 49484.0, 48775.0, 101290.0, 94900.0, 85773.0, 64011.0, 48345.0, 37096.0, 28898.0, 22848.0, 18335.0, 14934.0, 13516.0, 12948.0, 12804.0, 13698.0, 13455.0, 13206.0, 11508.0, 10005.0, 8823.5, 7881.2, 7117.9, 6489.4, 5962.5, 5513.0, 5122.3, 4797.6, 4525.2, 4275.1, 4041.8, 3820.8, 3609.5, 3406.2, 3209.5, 3018.9, 2833.8, 2813.4, 2776.1, 2772.2, 2761.3, 7572.9, 7649.8, 7734.0, 11122.0, 11162.0, 11693.0, 11917.0, 15169.0, 18694.0, 21444.0, 23088.0, 23570.0, 23038.0, 21916.0, 21748.0, 21557.0, 21458.0, 23941.0, 23531.0, 23368.0, 22924.0, 22804.0, 23713.0, 23621.0, 23253.0, 21477.0, 19257.0, 17743.0, 17254.0, 17126.0, 17853.0, 17706.0, 17228.0, 15752.0, 13825.0, 12089.0, 10546.0, 9162.0, 7937.3, 6868.0, 5938.3, 5131.2, 4357.2, 3691.7, 3132.6, 2662.3, 2265.9, 1931.7, 1649.6, 1410.7, 1199.5, 1019.0, 866.73, 737.91, 628.27, 535.51, 496.73, 479.12, 474.58, 1773.5, 1723.8, 1700.1, 1687.9, 1616.3, 1598.0, 2190.1, 2104.9, 2000.4, 1872.4, 1800.6, 1781.6, 2017.7, 1940.4, 1636.5, 1377.8, 1158.5, 974.14, 818.74, 688.48, 579.1, 487.32, 408.38, 341.52, 285.25, 238.42, 199.46, 167.03, 139.87, 116.91, 97.003, 80.563, 66.936, 55.619, 46.092, 38.155, 31.512, 26.05, 21.554, 17.85, 14.794, 12.273, 11.443, 10.969, 10.847, 73.809, 70.796, 70.472, 58.778, 49.395, 41.529, 34.707, 28.912, 24.088, 20.071, 16.726, 13.94, 11.62, 9.6163, 7.9548, 6.581, 5.4449, 4.5051, 3.7273, 3.083, 2.549, 2.1077, 1.7405, 1.433, 1.1791, 0.96919, 0.79669, 0.65493, 0.53841, 0.44265, 0.36393, 0.29923, 0.24604, 0.20231, 0.16636, 0.1368, 0.1125, 0.092523, 0.076093, 0.062583, 0.051474, 0.042338, 0.034824, 0.028645, 0.023563, 0.019383, 0.015945, 0.013117, 0.0], - 'energies (keV)': [0.006763068, 0.00686401, 0.006871351, 0.006976524, 0.007004571, 0.00704664, 0.007151814, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.038122, 0.0387055, 0.0388611, 0.0390945, 0.039678, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.067032, 0.068058, 0.0683316, 0.068742, 0.06923942, 0.069768, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.247842, 0.251272, 0.2516355, 0.2526471, 0.2541645, 0.255118, 0.2561436, 0.257682, 0.257958, 0.261528, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4165, 0.4195189, 0.422875, 0.424575, 0.427125, 0.4335, 0.436002, 0.4426755, 0.4444551, 0.4471245, 0.4484657, 0.453798, 0.4794098, 0.5124891, 0.536648, 0.544862, 0.5470524, 0.5478508, 0.550338, 0.558552, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.623362, 2.663515, 2.674223, 2.690284, 2.717235, 2.730438, 2.737336, 2.779234, 2.790407, 2.807166, 2.849064, 2.904724, 2.98165, 3.027288, 3.039458, 3.057713, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 20.62312, 20.93878, 21.02296, 21.14922, 21.46488, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ta': {'mass_absorption_coefficient (cm2/g)': [47371.0, 46396.0, 55425.0, 58905.0, 61782.0, 63767.0, 64634.0, 64246.0, 62577.0, 59707.0, 55814.0, 51143.0, 45972.0, 40582.0, 35215.0, 32961.0, 31788.0, 31481.0, 37323.0, 36071.0, 35883.0, 30020.0, 24845.0, 20628.0, 17211.0, 14483.0, 14338.0, 13811.0, 13677.0, 51572.0, 49299.0, 46036.0, 37794.0, 30936.0, 29681.0, 28358.0, 28020.0, 42452.0, 40830.0, 39472.0, 33413.0, 28442.0, 24373.0, 21288.0, 19149.0, 17850.0, 17766.0, 17588.0, 17547.0, 17668.0, 17550.0, 17436.0, 17407.0, 17804.0, 18508.0, 19411.0, 19916.0, 19992.0, 20067.0, 20142.0, 20217.0, 20291.0, 20366.0, 20439.0, 20513.0, 20585.0, 20658.0, 20730.0, 20801.0, 20872.0, 20942.0, 21013.0, 21083.0, 21152.0, 21220.0, 21288.0, 21354.0, 21420.0, 21486.0, 21550.0, 21613.0, 21676.0, 21738.0, 21798.0, 21858.0, 21917.0, 21975.0, 22031.0, 22087.0, 22142.0, 22195.0, 22248.0, 22300.0, 22350.0, 22399.0, 22447.0, 22494.0, 22540.0, 22585.0, 22628.0, 22670.0, 22711.0, 22751.0, 22790.0, 22827.0, 22863.0, 22898.0, 22932.0, 22964.0, 22995.0, 23025.0, 23054.0, 23081.0, 23107.0, 23132.0, 23155.0, 23178.0, 23199.0, 23218.0, 23237.0, 23254.0, 23269.0, 23284.0, 23297.0, 23309.0, 23320.0, 23329.0, 23337.0, 23344.0, 23349.0, 23354.0, 23357.0, 23358.0, 23359.0, 23358.0, 23356.0, 23353.0, 23348.0, 23343.0, 23336.0, 23328.0, 23318.0, 23308.0, 23296.0, 23283.0, 23269.0, 23254.0, 23238.0, 23220.0, 23201.0, 23182.0, 23161.0, 23139.0, 23116.0, 23091.0, 23066.0, 23040.0, 23013.0, 22984.0, 22955.0, 22924.0, 22893.0, 22860.0, 22827.0, 22792.0, 22757.0, 22721.0, 22683.0, 22645.0, 22606.0, 22566.0, 22525.0, 22483.0, 22441.0, 22397.0, 22353.0, 22308.0, 22262.0, 22215.0, 22168.0, 22119.0, 22070.0, 22020.0, 21970.0, 21919.0, 21867.0, 21814.0, 21761.0, 21706.0, 21652.0, 21596.0, 21540.0, 21484.0, 21427.0, 21369.0, 21311.0, 21252.0, 21192.0, 21132.0, 21072.0, 21011.0, 20949.0, 20887.0, 20824.0, 20761.0, 20698.0, 20634.0, 20570.0, 20505.0, 20440.0, 20374.0, 20308.0, 20242.0, 20175.0, 20108.0, 20041.0, 19973.0, 19905.0, 19837.0, 19768.0, 19699.0, 19630.0, 19561.0, 19544.0, 21077.0, 21032.0, 20935.0, 20838.0, 20741.0, 20644.0, 20548.0, 20452.0, 20356.0, 20260.0, 20165.0, 20122.0, 21186.0, 21159.0, 21043.0, 20927.0, 20812.0, 20698.0, 20585.0, 20472.0, 20360.0, 20248.0, 20137.0, 20018.0, 19901.0, 19785.0, 19670.0, 19556.0, 19444.0, 19332.0, 19215.0, 19098.0, 18983.0, 18868.0, 18755.0, 18643.0, 18532.0, 18422.0, 18314.0, 18206.0, 18100.0, 17994.0, 17889.0, 17786.0, 17683.0, 17581.0, 17479.0, 17379.0, 17279.0, 17180.0, 17081.0, 16984.0, 16887.0, 16790.0, 16695.0, 16600.0, 16505.0, 16411.0, 16318.0, 16225.0, 16133.0, 16042.0, 15951.0, 15861.0, 15771.0, 15681.0, 15593.0, 15504.0, 15417.0, 15329.0, 15243.0, 15157.0, 15071.0, 14986.0, 14901.0, 14817.0, 14733.0, 14650.0, 14567.0, 14485.0, 14403.0, 14322.0, 14241.0, 14161.0, 14081.0, 14002.0, 13923.0, 13845.0, 13767.0, 13689.0, 13612.0, 13536.0, 13460.0, 13384.0, 13309.0, 13234.0, 13160.0, 13086.0, 13013.0, 12940.0, 12867.0, 12795.0, 12724.0, 12653.0, 12582.0, 12512.0, 12442.0, 12372.0, 12303.0, 12235.0, 12167.0, 12099.0, 12031.0, 11965.0, 11898.0, 11832.0, 11771.0, 11766.0, 12531.0, 12494.0, 12427.0, 12361.0, 12295.0, 12229.0, 12164.0, 12100.0, 12035.0, 11971.0, 11908.0, 11845.0, 11782.0, 11719.0, 11657.0, 11595.0, 11534.0, 11473.0, 11412.0, 11352.0, 11292.0, 11232.0, 11173.0, 11114.0, 11055.0, 10997.0, 10939.0, 10881.0, 10837.0, 10823.0, 10977.0, 10940.0, 10883.0, 10805.0, 10726.0, 10648.0, 10570.0, 10493.0, 10417.0, 10340.0, 10265.0, 10190.0, 10115.0, 10041.0, 9967.5, 9894.4, 9821.8, 9749.7, 9678.1, 9607.0, 9536.4, 9466.2, 9396.5, 9327.2, 9258.4, 9190.1, 9122.2, 9054.8, 8987.8, 8921.2, 8855.1, 8789.4, 8724.2, 8659.3, 8594.9, 8530.9, 8467.4, 8404.2, 8341.4, 8279.1, 8276.2, 8453.6, 8434.9, 8372.6, 8310.6, 8249.1, 8188.0, 8127.2, 8066.9, 8006.9, 7947.3, 7888.0, 7829.2, 7770.7, 7712.5, 7654.8, 7597.3, 7540.3, 7483.6, 7427.2, 7371.2, 7315.6, 7260.2, 7205.3, 7150.6, 7096.3, 7042.3, 6988.6, 6935.3, 6882.2, 6829.5, 6777.1, 6725.1, 6673.3, 6621.8, 6570.7, 6519.9, 6469.3, 6419.1, 6369.2, 6319.6, 6270.2, 6221.1, 6172.4, 6123.9, 6075.7, 6027.8, 5980.2, 5932.9, 5885.9, 5839.2, 5792.8, 5746.6, 5700.7, 5655.2, 5609.8, 5564.8, 5520.1, 5475.6, 5431.4, 5387.5, 5343.9, 5300.5, 5257.5, 5214.7, 5172.1, 5129.9, 5087.9, 5046.0, 5004.4, 4963.1, 4922.1, 4881.3, 4840.7, 4800.4, 4760.4, 4720.7, 4681.2, 4642.0, 4603.0, 4564.3, 4525.8, 4487.6, 4449.7, 4412.0, 4374.6, 4337.4, 4300.5, 4263.8, 4227.4, 4191.2, 4155.3, 4119.6, 4084.2, 4049.0, 4014.1, 3979.4, 3944.9, 3910.7, 3876.8, 3843.1, 3809.6, 3776.4, 3743.4, 3710.6, 3678.1, 3645.8, 3613.7, 3581.8, 3550.2, 3518.8, 3487.7, 3456.8, 3426.2, 3395.8, 3365.7, 3332.8, 3294.3, 3256.2, 3218.7, 3181.6, 3145.1, 3108.9, 3073.3, 3038.0, 3003.3, 2968.9, 2935.0, 2901.6, 2868.5, 2835.9, 2803.6, 2771.8, 2740.4, 2709.3, 2678.7, 2648.4, 2618.5, 2589.0, 2559.9, 2531.1, 2502.7, 2474.6, 2446.9, 2419.5, 2392.4, 2365.7, 2339.3, 2313.3, 2287.5, 2262.1, 2237.0, 2212.2, 2187.7, 2163.4, 2139.5, 2115.9, 2092.6, 2069.5, 2046.8, 2024.3, 2002.1, 1980.1, 1958.4, 1937.0, 1915.8, 1894.9, 1874.3, 1853.8, 1833.7, 1813.8, 1794.1, 1774.6, 1755.4, 1736.4, 1717.6, 1699.1, 1680.8, 1662.7, 1644.8, 1627.1, 1609.6, 1592.3, 1575.3, 1558.4, 1541.7, 1525.3, 1509.0, 1492.9, 1477.0, 1461.3, 1445.8, 1430.5, 1415.3, 1400.3, 1385.5, 1370.9, 1356.4, 1342.1, 1328.0, 1314.0, 1300.2, 1286.6, 1273.1, 1259.8, 1246.6, 1233.6, 1220.7, 1208.0, 1195.4, 1182.9, 1170.7, 1158.5, 1146.3, 1134.2, 1122.3, 1110.5, 1098.8, 1087.3, 1075.9, 1064.6, 1053.4, 1042.4, 1031.5, 1020.8, 1010.1, 999.62, 998.37, 3453.9, 3420.0, 3378.6, 3337.8, 3297.4, 3257.5, 3218.1, 3191.1, 4749.6, 4739.3, 4681.4, 4624.2, 4567.6, 4511.8, 4456.7, 4402.3, 4348.6, 4295.3, 4242.8, 4190.9, 4139.7, 4089.1, 4039.1, 3989.7, 3941.0, 3892.9, 3845.3, 3798.4, 3752.0, 3706.2, 3661.0, 3616.4, 3572.3, 3528.7, 3485.7, 3443.2, 3401.3, 3359.9, 3318.9, 3278.5, 3238.6, 3199.2, 3160.3, 3121.8, 3083.9, 3046.4, 3009.4, 2972.8, 2936.7, 2901.0, 3389.1, 3364.3, 3321.8, 3279.8, 3238.4, 3197.6, 3157.1, 3116.6, 3076.5, 3037.0, 2998.4, 2961.9, 2926.0, 2890.4, 2855.2, 2820.5, 2786.4, 2752.7, 2719.5, 2686.8, 2654.6, 2622.8, 2591.5, 2560.6, 2541.7, 2709.0, 2705.8, 2675.7, 2642.8, 2610.3, 2578.3, 2546.8, 2515.6, 2484.9, 2454.6, 2424.6, 2395.7, 2367.8, 2340.3, 2313.2, 2286.6, 2260.3, 2234.3, 2208.8, 2183.5, 2183.1, 2261.9, 2256.7, 2231.3, 2206.3, 2181.6, 2157.2, 2133.0, 2109.2, 2085.6, 2062.3, 2039.3, 2016.6, 1994.0, 1971.7, 1949.6, 1927.8, 1906.3, 1885.0, 1863.9, 1843.1, 1822.5, 1802.0, 1780.2, 1758.6, 1737.3, 1716.2, 1695.4, 1674.9, 1654.6, 1634.5, 1614.7, 1595.1, 1575.8, 1556.7, 1537.8, 1519.1, 1500.6, 1482.3, 1464.3, 1446.4, 1428.8, 1411.5, 1394.3, 1377.4, 1360.6, 1344.1, 1327.8, 1311.7, 1295.8, 1280.1, 1264.6, 1248.8, 1233.2, 1217.8, 1202.5, 1187.5, 1172.7, 1157.9, 1143.2, 1128.6, 1114.2, 1100.1, 1086.1, 1072.3, 1058.7, 1045.3, 1032.0, 1019.0, 1006.1, 993.38, 980.85, 968.48, 956.28, 944.25, 932.38, 920.66, 909.11, 897.71, 886.46, 849.79, 719.01, 606.46, 511.7, 431.28, 363.78, 307.16, 259.73, 219.93, 186.49, 157.83, 133.46, 112.61, 93.837, 93.045, 89.217, 88.234, 242.33, 230.87, 213.71, 181.31, 175.84, 173.69, 171.75, 235.99, 226.64, 221.89, 212.97, 210.69, 240.41, 237.68, 231.62, 201.03, 169.4, 142.47, 119.55, 100.22, 84.024, 70.48, 59.135, 49.583, 41.583, 34.833, 29.192, 24.285, 20.166, 16.759, 13.94, 11.604, 9.6663, 8.0587, 6.7128, 5.5913, 4.6506, 3.8557, 3.1983, 2.6533, 2.2755, 2.2028, 2.1813, 2.1571, 11.082, 10.678, 9.6904, 8.1411, 6.8291, 5.7232, 4.7823, 3.9959, 3.3392, 2.7909, 2.3328, 1.9502, 1.6277, 1.356, 1.1298, 0.94142, 0.78453, 0.65384, 0.54497, 0.45427, 0.37869, 0.31572, 0.26323, 0.21949, 0.18424, 0.15479, 0.13005, 0.10928, 0.091825, 0.077167, 0.0], - 'energies (keV)': [0.0057285, 0.005814, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.0245, 0.024875, 0.024975, 0.025125, 0.02545001, 0.0255, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.035672, 0.036218, 0.0363636, 0.036582, 0.037128, 0.03797993, 0.04060054, 0.04340198, 0.044002, 0.0446755, 0.0448551, 0.0451245, 0.045798, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.069678, 0.0707445, 0.0710289, 0.0714555, 0.072522, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.22912551, 0.2294745, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24111348, 0.24148653, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.4039782, 0.40410785, 0.40502181, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46412139, 0.46467255, 0.46547861, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56457826, 0.56642179, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7347877, 1.7354123, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7926172, 1.7937828, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.1963695, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4657375, 2.4704593, 2.4716624, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7027735, 2.7132264, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 9.683478, 9.831694, 9.871219, 9.930505, 10.07872, 10.32004, 10.91338, 11.03212, 11.08042, 11.12496, 11.19178, 11.35882, 11.44787, 11.62309, 11.66982, 11.73991, 11.79334, 11.91513, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.06807, 66.84318, 67.07932, 67.34898, 67.75348, 68.76473, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Yb': {'mass_absorption_coefficient (cm2/g)': [6144.2, 6073.0, 5466.0, 5601.3, 5772.6, 5979.0, 6220.6, 6497.8, 6811.6, 7163.0, 7553.3, 7983.9, 8456.2, 8971.6, 9211.5, 9339.4, 9373.6, 319480.0, 302630.0, 299260.0, 226030.0, 169290.0, 127030.0, 96207.0, 74097.0, 58430.0, 47437.0, 39792.0, 34532.0, 30963.0, 28594.0, 27075.0, 26821.0, 26759.0, 26845.0, 26642.0, 26335.0, 25846.0, 25653.0, 25665.0, 25811.0, 26039.0, 26309.0, 26593.0, 26866.0, 26997.0, 27015.0, 27034.0, 27052.0, 27070.0, 27087.0, 27105.0, 27122.0, 27139.0, 27156.0, 27172.0, 27189.0, 27205.0, 27221.0, 27236.0, 27252.0, 27267.0, 27282.0, 27296.0, 27310.0, 27325.0, 27338.0, 27352.0, 27365.0, 27378.0, 27391.0, 27403.0, 27415.0, 27427.0, 27438.0, 27450.0, 27461.0, 27471.0, 27481.0, 27465.0, 27387.0, 27308.0, 27229.0, 27149.0, 27069.0, 26988.0, 26907.0, 26825.0, 26743.0, 26660.0, 26577.0, 26493.0, 26409.0, 26325.0, 26240.0, 26154.0, 26068.0, 25982.0, 25895.0, 25808.0, 25721.0, 25633.0, 25544.0, 25456.0, 25366.0, 25277.0, 25187.0, 25097.0, 25006.0, 24915.0, 24823.0, 24732.0, 24639.0, 24547.0, 24454.0, 24361.0, 24268.0, 24174.0, 24080.0, 23985.0, 23891.0, 23796.0, 23700.0, 23605.0, 23509.0, 23413.0, 23316.0, 23220.0, 23123.0, 23026.0, 22928.0, 22831.0, 22733.0, 22635.0, 22537.0, 22438.0, 22339.0, 22241.0, 22142.0, 22042.0, 21943.0, 21843.0, 21744.0, 21644.0, 21544.0, 21444.0, 21343.0, 21243.0, 21142.0, 21042.0, 20941.0, 20840.0, 20739.0, 20638.0, 20537.0, 20436.0, 20334.0, 20233.0, 20132.0, 20030.0, 19929.0, 19827.0, 19725.0, 19624.0, 19522.0, 19421.0, 19319.0, 19217.0, 19116.0, 19106.0, 28782.0, 28378.0, 27749.0, 27151.0, 26580.0, 26036.0, 25517.0, 25021.0, 24548.0, 24095.0, 23663.0, 23249.0, 22852.0, 22473.0, 22154.0, 22109.0, 27716.0, 27223.0, 26593.0, 25992.0, 25418.0, 24871.0, 24349.0, 23854.0, 23385.0, 22940.0, 22517.0, 22115.0, 21732.0, 21366.0, 21017.0, 20684.0, 20365.0, 20059.0, 19766.0, 19485.0, 19215.0, 18958.0, 18713.0, 18479.0, 18255.0, 18040.0, 17833.0, 17634.0, 17442.0, 17257.0, 17078.0, 16905.0, 16737.0, 16574.0, 16415.0, 16261.0, 16111.0, 15964.0, 15821.0, 15681.0, 15545.0, 15411.0, 15280.0, 15152.0, 15026.0, 14902.0, 14781.0, 14661.0, 14544.0, 14429.0, 14315.0, 14203.0, 14093.0, 13984.0, 13877.0, 13771.0, 13667.0, 13564.0, 13462.0, 13362.0, 13263.0, 13165.0, 13068.0, 12972.0, 12878.0, 12784.0, 12691.0, 12600.0, 12509.0, 12420.0, 12331.0, 12243.0, 12156.0, 12070.0, 11985.0, 11901.0, 11817.0, 11734.0, 11652.0, 11571.0, 11491.0, 11411.0, 11333.0, 11255.0, 11177.0, 11101.0, 11025.0, 10950.0, 10875.0, 10801.0, 10728.0, 10656.0, 10584.0, 10513.0, 10443.0, 10373.0, 10304.0, 10235.0, 10167.0, 10100.0, 10033.0, 9967.5, 9902.1, 9837.4, 9773.2, 9709.6, 9646.7, 9584.3, 9522.5, 9461.3, 9400.6, 9389.9, 10222.0, 10202.0, 10141.0, 10081.0, 10021.0, 9961.7, 9903.0, 9844.9, 9787.3, 9730.3, 9673.8, 9617.8, 9562.3, 9507.3, 9452.8, 9398.8, 9345.3, 9292.3, 9239.8, 9187.7, 9136.1, 9085.0, 9034.3, 8984.0, 8934.2, 8884.8, 8835.8, 8787.2, 8739.0, 8691.2, 8690.5, 8864.8, 8844.2, 8797.3, 8750.7, 8704.5, 8658.6, 8613.2, 8568.0, 8523.3, 8478.8, 8434.8, 8391.0, 8347.6, 8304.4, 8261.6, 8219.2, 8177.0, 8135.1, 8093.5, 8052.1, 8011.1, 7970.3, 7929.8, 7889.5, 7849.5, 7809.8, 7770.3, 7731.0, 7692.0, 7653.1, 7614.5, 7576.1, 7537.9, 7499.9, 7462.1, 7424.5, 7387.0, 7349.8, 7312.7, 7275.8, 7239.0, 7202.4, 7195.5, 7429.6, 7421.9, 7384.9, 7348.1, 7311.3, 7274.7, 7238.3, 7202.0, 7165.7, 7129.7, 7093.7, 7057.8, 7022.1, 6986.4, 6950.8, 6915.4, 6880.0, 6844.7, 6809.6, 6774.5, 6739.4, 6704.5, 6669.6, 6634.8, 6600.0, 6565.3, 6530.6, 6496.0, 6461.5, 6427.0, 6392.5, 6358.1, 6323.7, 6289.4, 6255.1, 6220.9, 6186.7, 6152.6, 6118.5, 6084.4, 6050.4, 6016.4, 5982.4, 5948.5, 5914.7, 5880.8, 5847.0, 5813.3, 5779.6, 5745.9, 5712.3, 5678.7, 5645.1, 5611.6, 5578.1, 5544.7, 5511.3, 5478.0, 5444.7, 5411.4, 5378.2, 5345.1, 5312.0, 5278.9, 5245.9, 5213.0, 5180.1, 5147.3, 5114.5, 5081.8, 5049.1, 5016.5, 4984.0, 4951.5, 4919.1, 4886.8, 4854.5, 4822.3, 4790.2, 4758.2, 4726.2, 4694.3, 4662.5, 4630.7, 4599.1, 4567.5, 4536.0, 4504.6, 4473.2, 4441.9, 4410.7, 4379.6, 4348.5, 4317.6, 4286.7, 4256.0, 4225.3, 4194.7, 4164.2, 4133.9, 4103.6, 4073.4, 4043.3, 4013.3, 3983.5, 3953.7, 3924.0, 3894.4, 3864.9, 3835.5, 3806.3, 3777.2, 3748.2, 3719.3, 3690.6, 3662.0, 3633.5, 3605.2, 3577.1, 3549.1, 3521.2, 3493.5, 3466.0, 3438.6, 3411.3, 3384.2, 3357.3, 3330.5, 3303.9, 3277.5, 3251.2, 3225.1, 3199.2, 3173.4, 3147.8, 3122.4, 3097.1, 3072.1, 3047.1, 3022.4, 2997.8, 2973.4, 2949.2, 2925.2, 2901.3, 2874.6, 2842.1, 2810.0, 2778.3, 2747.0, 2716.0, 2685.5, 2655.3, 2625.5, 2596.1, 2567.0, 2538.3, 2509.9, 2481.8, 2454.2, 2426.8, 2399.8, 2373.1, 2346.7, 2320.7, 2295.0, 2269.5, 2244.4, 2219.6, 2195.1, 2170.9, 2147.0, 2123.3, 2100.0, 2076.9, 2054.1, 2031.6, 2009.3, 1987.3, 1965.6, 1944.1, 1922.9, 1902.0, 1881.3, 1860.8, 1840.6, 1820.6, 1800.8, 1781.3, 1762.1, 1743.0, 1724.2, 1705.6, 1687.2, 1669.0, 1651.1, 1633.3, 1615.8, 1598.4, 1581.3, 1564.4, 1547.7, 1531.1, 1514.8, 1498.6, 1482.7, 1466.9, 1451.3, 1435.9, 1420.5, 1405.1, 1389.9, 1374.9, 1360.1, 1345.4, 1331.0, 1316.7, 1302.5, 1288.5, 1274.7, 1261.1, 1247.6, 1234.2, 1221.1, 1208.0, 1195.2, 1182.4, 1169.8, 1157.4, 1145.1, 1137.6, 4235.7, 4218.4, 4165.2, 4112.7, 4060.9, 4009.8, 3959.3, 3917.0, 5875.7, 5873.7, 5799.0, 5725.1, 5652.2, 5580.2, 5509.2, 5439.1, 5369.9, 5301.6, 5234.2, 5167.6, 5101.9, 5037.1, 4973.1, 4909.9, 4847.5, 4786.0, 4725.2, 4665.2, 4606.0, 4547.6, 4489.9, 4432.9, 4376.7, 4321.3, 4266.5, 4212.4, 4159.1, 4106.4, 4054.4, 4003.1, 3952.4, 3902.4, 3853.0, 3804.3, 3756.2, 3708.7, 3661.9, 3615.6, 3569.9, 3524.8, 3480.3, 3436.4, 3422.7, 4001.2, 3989.3, 3935.8, 3883.1, 3831.0, 3779.6, 3728.9, 3678.8, 3629.4, 3580.8, 3532.7, 3487.8, 3444.0, 3401.0, 3358.7, 3317.1, 3276.2, 3235.9, 3196.3, 3157.3, 3118.9, 3081.0, 3043.8, 3042.3, 3239.6, 3219.8, 3179.7, 3140.1, 3101.1, 3062.8, 3024.9, 2987.6, 2950.9, 2914.7, 2879.1, 2845.3, 2812.0, 2779.2, 2746.9, 2715.1, 2683.8, 2653.0, 2622.6, 2592.6, 2574.1, 2563.0, 2668.7, 2652.3, 2622.4, 2592.9, 2563.8, 2535.0, 2506.6, 2478.6, 2450.9, 2423.6, 2396.6, 2370.1, 2343.4, 2317.0, 2290.9, 2265.2, 2239.7, 2214.5, 2189.7, 2165.1, 2140.8, 2116.7, 2093.0, 2069.5, 2046.3, 2023.3, 2000.6, 1978.1, 1955.9, 1933.9, 1912.2, 1890.7, 1869.4, 1848.4, 1827.4, 1806.7, 1786.2, 1765.9, 1745.8, 1726.0, 1706.4, 1687.0, 1667.8, 1648.9, 1630.1, 1611.5, 1591.9, 1572.5, 1553.4, 1534.3, 1515.0, 1496.1, 1477.4, 1458.9, 1440.6, 1422.5, 1404.4, 1386.5, 1368.8, 1351.4, 1334.2, 1317.3, 1300.5, 1284.0, 1267.8, 1251.7, 1235.9, 1220.3, 1204.8, 1189.6, 1174.6, 1159.8, 1145.2, 1130.8, 1116.6, 1102.5, 1088.6, 1074.9, 1061.4, 1048.0, 1034.8, 1021.8, 1009.0, 996.3, 983.81, 971.49, 959.33, 947.34, 935.51, 923.78, 912.22, 900.8, 889.54, 878.43, 867.47, 856.66, 845.99, 835.46, 824.94, 814.42, 804.04, 793.81, 783.71, 750.78, 633.46, 533.81, 450.17, 379.91, 321.04, 271.66, 230.17, 194.55, 164.2, 138.76, 117.47, 107.0, 102.97, 101.82, 282.2, 277.91, 268.95, 225.67, 217.44, 208.21, 205.86, 284.24, 272.08, 264.46, 261.22, 252.98, 250.05, 285.11, 274.34, 253.45, 213.88, 180.15, 151.46, 127.0, 106.42, 89.17, 74.747, 62.595, 52.432, 43.875, 36.699, 30.723, 25.742, 21.413, 17.78, 14.776, 12.289, 10.228, 8.5168, 7.083, 5.889, 4.8798, 4.0421, 3.3479, 2.775, 2.5712, 2.4641, 2.4366, 12.89, 12.43, 12.414, 10.469, 8.7868, 7.3706, 6.175, 5.1554, 4.3047, 3.5949, 3.0026, 2.5081, 2.0954, 1.7439, 1.4509, 1.2072, 1.0046, 0.83605, 0.69584, 0.5792, 0.48215, 0.40139, 0.33419, 0.27826, 0.23216, 0.19457, 0.16308, 0.13669, 0.11458, 0.096057, 0.080531, 0.067518, 0.0], - 'energies (keV)': [0.0063315, 0.006426, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.022932, 0.023283, 0.0233766, 0.023517, 0.0238073, 0.023868, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.0538295, 0.0540459, 0.0543705, 0.055182, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18476613, 0.18503387, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19791478, 0.19803888, 0.19828522, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34308435, 0.34391562, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39615652, 0.39724347, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41638364, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48646432, 0.48793566, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5275616, 1.5280383, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5758114, 1.5767887, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6742677, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8406875, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9475382, 1.9520618, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0439363, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.170436, 2.1755642, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3931119, 2.3976254, 2.4030879, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 8.764728, 8.898882, 8.934656, 8.988318, 9.030794, 9.122472, 9.653919, 9.778636, 9.928309, 9.968222, 10.02809, 10.17776, 10.27667, 10.32004, 10.43397, 10.47591, 10.53883, 10.69613, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 60.10565, 61.02564, 61.27097, 61.63896, 62.5287, 62.55894, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Dy': {'mass_absorption_coefficient (cm2/g)': [5196.6, 5185.5, 6417.8, 6690.1, 6993.9, 7331.1, 7703.5, 8113.0, 8561.3, 9050.0, 9578.4, 10146.0, 10753.0, 11398.0, 12079.0, 12796.0, 12936.0, 13105.0, 13150.0, 213830.0, 201470.0, 190470.0, 146720.0, 113230.0, 87701.0, 68914.0, 55446.0, 45988.0, 39462.0, 35034.0, 32089.0, 30178.0, 28983.0, 28278.0, 28155.0, 28063.0, 28041.0, 28117.0, 28053.0, 28018.0, 27886.0, 27895.0, 27927.0, 26850.0, 25747.0, 24610.0, 24017.0, 23930.0, 23842.0, 23753.0, 23665.0, 23576.0, 23488.0, 23399.0, 23309.0, 23220.0, 23131.0, 23041.0, 22951.0, 22861.0, 22771.0, 22680.0, 22590.0, 22499.0, 22408.0, 22317.0, 22226.0, 22135.0, 22043.0, 21952.0, 21860.0, 21768.0, 21676.0, 21584.0, 21492.0, 21400.0, 21307.0, 21215.0, 21122.0, 21029.0, 20936.0, 20843.0, 20750.0, 20657.0, 20564.0, 20471.0, 20377.0, 20284.0, 20190.0, 20097.0, 20003.0, 19909.0, 19816.0, 19722.0, 19628.0, 19534.0, 19440.0, 19346.0, 19252.0, 19158.0, 19064.0, 18970.0, 18876.0, 18782.0, 18688.0, 18594.0, 18500.0, 18406.0, 18312.0, 18218.0, 18124.0, 18030.0, 17936.0, 17842.0, 17748.0, 17654.0, 17561.0, 17467.0, 17374.0, 17280.0, 17187.0, 17093.0, 17000.0, 16907.0, 16814.0, 16721.0, 16628.0, 16535.0, 16442.0, 16350.0, 16257.0, 16165.0, 16072.0, 16003.0, 41462.0, 41305.0, 39675.0, 38138.0, 36690.0, 35323.0, 34035.0, 32819.0, 31672.0, 30589.0, 29566.0, 28601.0, 27688.0, 26826.0, 26011.0, 25240.0, 24510.0, 23820.0, 23166.0, 22547.0, 21960.0, 21408.0, 20895.0, 20416.0, 19970.0, 19552.0, 19160.0, 18792.0, 18445.0, 18118.0, 17809.0, 17517.0, 17240.0, 16976.0, 16726.0, 16487.0, 16258.0, 16040.0, 15831.0, 15630.0, 15437.0, 15252.0, 15073.0, 14901.0, 14734.0, 14573.0, 14417.0, 14266.0, 14119.0, 13976.0, 13838.0, 13703.0, 13571.0, 13443.0, 13318.0, 13196.0, 13076.0, 12960.0, 12845.0, 12733.0, 12623.0, 12516.0, 12410.0, 12307.0, 12205.0, 12105.0, 12007.0, 11910.0, 11815.0, 11721.0, 11629.0, 11538.0, 11449.0, 11361.0, 11274.0, 11189.0, 11104.0, 11021.0, 10939.0, 10858.0, 10778.0, 10699.0, 10621.0, 10544.0, 10468.0, 10393.0, 10319.0, 10246.0, 10174.0, 10102.0, 10032.0, 9962.0, 9893.1, 9825.0, 9757.6, 9691.1, 9625.3, 9560.3, 9496.0, 9432.5, 9369.6, 9307.6, 9246.2, 9185.5, 9125.5, 9066.2, 9007.5, 8949.6, 8892.2, 8835.6, 8779.6, 8724.2, 8669.5, 8615.3, 8561.8, 8508.9, 8456.7, 8405.0, 8353.9, 8303.4, 8253.5, 8204.2, 8155.4, 8107.2, 8059.6, 8012.5, 7966.0, 7920.0, 7874.5, 7829.6, 7817.0, 8811.5, 8795.8, 8750.6, 8705.9, 8661.7, 8618.0, 8574.8, 8532.2, 8490.0, 8448.2, 8407.0, 8366.2, 8325.9, 8286.1, 8246.7, 8207.8, 8169.3, 8131.2, 8093.5, 8056.3, 8019.5, 7983.1, 7947.0, 7911.4, 7876.1, 7841.1, 7832.1, 8077.1, 8066.2, 8032.0, 7998.1, 7964.5, 7931.3, 7898.3, 7865.7, 7833.4, 7801.4, 7769.7, 7738.2, 7707.1, 7676.2, 7645.5, 7615.2, 7585.0, 7555.2, 7525.5, 7496.1, 7466.9, 7437.9, 7409.1, 7380.5, 7352.1, 7323.9, 7295.9, 7268.0, 7240.3, 7212.8, 7185.4, 7158.2, 7131.1, 7104.1, 7077.3, 7050.5, 7023.9, 6997.4, 6971.0, 6944.7, 6918.5, 6892.3, 6866.3, 6840.3, 6814.4, 6788.5, 6770.8, 7087.7, 7081.4, 7061.0, 7034.4, 7007.8, 6981.3, 6954.8, 6928.3, 6901.9, 6875.4, 6849.0, 6822.6, 6796.2, 6769.8, 6743.4, 6717.0, 6690.6, 6664.2, 6637.7, 6611.3, 6584.8, 6558.3, 6531.8, 6505.2, 6478.5, 6451.8, 6425.1, 6398.3, 6371.5, 6344.6, 6317.6, 6290.6, 6263.5, 6236.4, 6209.2, 6181.9, 6154.6, 6127.1, 6099.6, 6072.1, 6044.4, 6016.7, 5989.0, 5961.2, 5933.3, 5905.4, 5877.4, 5849.3, 5821.2, 5793.0, 5764.7, 5736.4, 5708.1, 5679.6, 5651.2, 5622.6, 5594.1, 5565.4, 5536.8, 5508.0, 5479.3, 5450.4, 5421.6, 5392.7, 5363.8, 5334.8, 5305.8, 5276.7, 5247.7, 5218.6, 5189.4, 5160.3, 5131.1, 5101.9, 5072.7, 5043.5, 5014.3, 4985.0, 4955.8, 4926.5, 4897.3, 4868.0, 4838.7, 4809.3, 4779.9, 4750.5, 4721.1, 4691.7, 4662.2, 4632.8, 4603.3, 4573.9, 4544.5, 4515.1, 4485.7, 4456.3, 4426.9, 4397.6, 4368.3, 4339.1, 4309.9, 4280.7, 4251.6, 4222.5, 4193.5, 4164.6, 4135.7, 4106.8, 4077.9, 4049.0, 4020.3, 3991.6, 3963.0, 3934.5, 3906.1, 3877.8, 3849.6, 3821.5, 3793.4, 3765.5, 3737.7, 3710.0, 3682.4, 3654.9, 3627.5, 3600.2, 3573.1, 3546.1, 3519.2, 3492.4, 3465.7, 3439.2, 3412.8, 3386.6, 3360.4, 3334.4, 3308.6, 3282.8, 3257.3, 3231.8, 3206.5, 3181.4, 3156.3, 3131.5, 3106.7, 3082.1, 3057.7, 3033.4, 3009.3, 2985.3, 2961.4, 2937.7, 2914.2, 2890.8, 2867.5, 2844.4, 2821.5, 2798.7, 2776.1, 2753.6, 2731.3, 2709.1, 2687.1, 2665.2, 2643.5, 2621.9, 2600.5, 2579.2, 2558.1, 2537.2, 2516.4, 2495.7, 2475.2, 2454.8, 2434.7, 2414.6, 2394.7, 2373.0, 2347.6, 2322.5, 2297.6, 2273.1, 2248.8, 2224.8, 2201.1, 2177.6, 2154.4, 2131.5, 2108.8, 2086.4, 2064.2, 2042.3, 2020.6, 1999.2, 1978.0, 1957.0, 1936.3, 1915.8, 1895.6, 1875.5, 1855.7, 1836.2, 1816.8, 1797.7, 1778.7, 1760.0, 1741.5, 1723.2, 1705.2, 1687.0, 1668.9, 1651.0, 1633.3, 1615.9, 1598.6, 1581.5, 1564.6, 1547.9, 1531.3, 1515.0, 1498.9, 1482.9, 1467.1, 1451.5, 1436.1, 1420.8, 1405.8, 1390.9, 1376.1, 1369.4, 5296.5, 5263.6, 5197.3, 5131.7, 5067.0, 5003.2, 4940.1, 4929.0, 7421.9, 7352.4, 7258.7, 7166.1, 7074.8, 6984.6, 6895.6, 6807.8, 6721.1, 6635.5, 6551.0, 6467.6, 6385.2, 6304.0, 6223.8, 6144.6, 6066.4, 5989.3, 5913.1, 5837.9, 5763.7, 5690.5, 5618.2, 5546.8, 5476.4, 5406.8, 5338.2, 5270.4, 5203.5, 5137.5, 5072.3, 5007.9, 4944.4, 4881.7, 4819.8, 4758.7, 4698.4, 4638.9, 4580.1, 4522.1, 4464.9, 4408.3, 4352.5, 4297.4, 4243.1, 4189.4, 4138.0, 4136.3, 4828.9, 4787.6, 4724.6, 4662.4, 4601.1, 4540.6, 4480.9, 4422.0, 4363.8, 4306.3, 4250.2, 4197.3, 4145.2, 4094.0, 4043.6, 3994.0, 3945.1, 3897.0, 3849.6, 3806.5, 3802.9, 4053.8, 4019.5, 3969.7, 3920.7, 3872.4, 3824.7, 3777.8, 3731.6, 3686.0, 3640.9, 3596.9, 3554.6, 3513.0, 3472.0, 3431.6, 3391.8, 3352.7, 3314.1, 3276.0, 3238.5, 3201.6, 3167.6, 3165.2, 3291.8, 3278.1, 3241.2, 3204.9, 3169.1, 3133.7, 3098.8, 3064.3, 3030.3, 2996.7, 2963.5, 2930.9, 2898.7, 2866.8, 2834.6, 2802.8, 2771.4, 2740.3, 2709.6, 2679.3, 2649.4, 2619.7, 2590.5, 2561.5, 2532.9, 2504.7, 2476.7, 2449.1, 2421.7, 2394.7, 2368.0, 2341.6, 2315.4, 2289.2, 2263.3, 2237.7, 2212.4, 2187.4, 2162.7, 2138.2, 2114.0, 2090.1, 2066.4, 2043.0, 2019.9, 1997.0, 1974.4, 1952.0, 1929.7, 1907.4, 1885.4, 1863.6, 1842.1, 1820.9, 1799.6, 1778.5, 1757.7, 1737.1, 1716.8, 1696.7, 1676.8, 1657.2, 1637.9, 1618.7, 1599.8, 1581.1, 1562.7, 1544.5, 1526.5, 1508.7, 1491.1, 1473.5, 1456.1, 1438.9, 1422.0, 1405.2, 1388.7, 1372.3, 1354.7, 1337.4, 1320.3, 1303.5, 1286.8, 1270.4, 1254.3, 1238.3, 1222.6, 1207.0, 1191.7, 1176.6, 1161.7, 1147.0, 1132.5, 1118.2, 1104.1, 1090.1, 1076.4, 1062.8, 1049.5, 1036.3, 1023.0, 1009.9, 996.95, 984.18, 971.58, 959.15, 946.89, 934.8, 922.87, 911.1, 899.49, 888.04, 876.74, 865.59, 854.59, 843.74, 833.04, 822.48, 812.06, 801.64, 791.35, 781.2, 771.18, 761.3, 751.55, 741.94, 732.45, 723.09, 713.85, 704.74, 695.75, 686.88, 678.13, 669.5, 660.98, 633.2, 533.56, 450.14, 380.27, 321.67, 272.22, 229.42, 193.51, 163.48, 138.18, 127.27, 122.44, 121.2, 345.49, 335.49, 329.78, 279.01, 275.57, 267.9, 265.06, 363.6, 348.11, 335.3, 321.9, 319.06, 318.5, 362.74, 349.69, 312.48, 262.57, 218.76, 182.95, 153.14, 128.28, 107.55, 90.236, 75.524, 63.174, 52.741, 44.022, 36.777, 30.751, 25.736, 21.558, 17.931, 14.887, 12.37, 10.282, 8.5352, 7.0825, 5.8529, 4.8391, 4.0021, 3.3124, 3.0483, 2.9204, 2.8875, 15.873, 15.39, 15.285, 12.956, 10.872, 9.115, 7.6382, 6.3955, 5.3477, 4.458, 3.7168, 3.0992, 2.5845, 2.1477, 1.7838, 1.4817, 1.2309, 1.0227, 0.84973, 0.70609, 0.58678, 0.48767, 0.40533, 0.33691, 0.28046, 0.23431, 0.19577, 0.16358, 0.1367, 0.11424, 0.095471, 0.079794, 0.066695, 0.055749, 0.0], - 'energies (keV)': [0.004221, 0.004284, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.025774, 0.0261685, 0.0262737, 0.0264315, 0.026826, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.061642, 0.0625855, 0.0628371, 0.0632145, 0.064158, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1, 0.1005, 0.1010025, 0.10150751, 0.10201505, 0.10252513, 0.10303775, 0.10355294, 0.1040707, 0.10459106, 0.10511401, 0.10563958, 0.10616778, 0.10669862, 0.10723211, 0.10776827, 0.10830712, 0.10884865, 0.10939289, 0.10993986, 0.11048956, 0.11104201, 0.11159722, 0.1121552, 0.11271598, 0.11327956, 0.11384596, 0.11441519, 0.11498726, 0.1155622, 0.11614001, 0.11672071, 0.11730431, 0.11789083, 0.11848029, 0.11907269, 0.11966805, 0.12026639, 0.12086772, 0.12147206, 0.12207942, 0.12268982, 0.12330327, 0.12391979, 0.12453939, 0.12516208, 0.12578789, 0.12641683, 0.12704892, 0.12768416, 0.12832258, 0.12896419, 0.12960902, 0.13025706, 0.13090835, 0.13156289, 0.1322207, 0.13288181, 0.13354621, 0.13421395, 0.13488502, 0.13555944, 0.13623724, 0.13691842, 0.13760302, 0.13829103, 0.13898249, 0.1396774, 0.14037579, 0.14107766, 0.14178305, 0.14249197, 0.14320443, 0.14392045, 0.14464005, 0.14536325, 0.14609007, 0.14682052, 0.14755462, 0.14829239, 0.14903386, 0.14977903, 0.15052792, 0.15128056, 0.15203696, 0.15279715, 0.15356113, 0.15414295, 0.15425706, 0.15432894, 0.15510058, 0.15587609, 0.15665547, 0.15743875, 0.15822594, 0.15901707, 0.15981215, 0.16061121, 0.16141427, 0.16222134, 0.16303245, 0.16384761, 0.16466685, 0.16549018, 0.16631763, 0.16714922, 0.16798497, 0.16882489, 0.16966902, 0.17051736, 0.17136995, 0.1722268, 0.17308793, 0.17395337, 0.17482314, 0.17569726, 0.17657574, 0.17745862, 0.17834591, 0.17923764, 0.18013383, 0.1810345, 0.18193967, 0.18284937, 0.18376362, 0.18468244, 0.18560585, 0.18653388, 0.18746655, 0.18840388, 0.1893459, 0.19029263, 0.19124409, 0.19220031, 0.19316131, 0.19412712, 0.19509776, 0.19607325, 0.19705361, 0.19803888, 0.19902907, 0.20002422, 0.20102434, 0.20202946, 0.20303961, 0.20405481, 0.20507508, 0.20610046, 0.20713096, 0.20816661, 0.20920745, 0.21025348, 0.21130475, 0.21236128, 0.21342308, 0.2144902, 0.21556265, 0.21664046, 0.21772366, 0.21881228, 0.21990634, 0.22100588, 0.2221109, 0.22322146, 0.22433757, 0.22545925, 0.22658655, 0.22771948, 0.22885808, 0.23000237, 0.23115238, 0.23230814, 0.23346969, 0.23463703, 0.23581022, 0.23698927, 0.23817422, 0.23936509, 0.24056191, 0.24176472, 0.24297355, 0.24418841, 0.24540936, 0.2466364, 0.24786959, 0.24910893, 0.25035448, 0.25160625, 0.25286428, 0.2541286, 0.25539925, 0.25667624, 0.25795962, 0.25924942, 0.26054567, 0.2618484, 0.26315764, 0.26447343, 0.26579579, 0.26712477, 0.2684604, 0.2698027, 0.27115171, 0.27250747, 0.27387001, 0.27523936, 0.27661556, 0.27799863, 0.27938863, 0.28078557, 0.2821895, 0.28360044, 0.28501845, 0.28644354, 0.28787576, 0.28931514, 0.29076171, 0.29221552, 0.29262731, 0.29317269, 0.2936766, 0.29514498, 0.29662071, 0.29810381, 0.29959433, 0.3010923, 0.30259776, 0.30411075, 0.3056313, 0.30715946, 0.30869526, 0.31023873, 0.31178993, 0.31334888, 0.31491562, 0.3164902, 0.31807265, 0.31966301, 0.32126133, 0.32286764, 0.32448197, 0.32610438, 0.32773491, 0.32937358, 0.33102045, 0.33144831, 0.33215172, 0.33267555, 0.33433893, 0.33601062, 0.33769068, 0.33937913, 0.34107602, 0.3427814, 0.34449531, 0.34621779, 0.34794888, 0.34968862, 0.35143706, 0.35319425, 0.35496022, 0.35673502, 0.3585187, 0.36031129, 0.36211285, 0.36392341, 0.36574303, 0.36757174, 0.3694096, 0.37125665, 0.37311293, 0.3749785, 0.37685339, 0.37873766, 0.38063135, 0.3825345, 0.38444718, 0.38636941, 0.38830126, 0.39024276, 0.39219398, 0.39415495, 0.39612572, 0.39810635, 0.40009688, 0.40209737, 0.40410785, 0.40612839, 0.40815904, 0.41019983, 0.41225083, 0.41431208, 0.41572967, 0.41638364, 0.41687033, 0.41846556, 0.42055789, 0.42266068, 0.42477398, 0.42689785, 0.42903234, 0.4311775, 0.43333339, 0.43550006, 0.43767756, 0.43986595, 0.44206528, 0.4442756, 0.44649698, 0.44872947, 0.45097311, 0.45322798, 0.45549412, 0.45777159, 0.46006045, 0.46236075, 0.46467255, 0.46699592, 0.4693309, 0.47167755, 0.47403594, 0.47640612, 0.47878815, 0.48118209, 0.483588, 0.48600594, 0.48843597, 0.49087815, 0.49333254, 0.4957992, 0.4982782, 0.50076959, 0.50327344, 0.5057898, 0.50831875, 0.51086035, 0.51341465, 0.51598172, 0.51856163, 0.52115444, 0.52376021, 0.52637901, 0.52901091, 0.53165596, 0.53431424, 0.53698581, 0.53967074, 0.5423691, 0.54508094, 0.54780635, 0.55054538, 0.5532981, 0.5560646, 0.55884492, 0.56163914, 0.56444734, 0.56726958, 0.57010592, 0.57295645, 0.57582123, 0.57870034, 0.58159384, 0.58450181, 0.58742432, 0.59036144, 0.59331325, 0.59627982, 0.59926122, 0.60225752, 0.60526881, 0.60829515, 0.61133663, 0.61439331, 0.61746528, 0.6205526, 0.62365537, 0.62677364, 0.62990751, 0.63305705, 0.63622234, 0.63940345, 0.64260046, 0.64581347, 0.64904253, 0.65228775, 0.65554919, 0.65882693, 0.66212107, 0.66543167, 0.66875883, 0.67210262, 0.67546314, 0.67884045, 0.68223466, 0.68564583, 0.68907406, 0.69251943, 0.69598202, 0.69946194, 0.70295924, 0.70647404, 0.71000641, 0.71355644, 0.71712423, 0.72070985, 0.7243134, 0.72793496, 0.73157464, 0.73523251, 0.73890867, 0.74260322, 0.74631623, 0.75004781, 0.75379805, 0.75756704, 0.76135488, 0.76516165, 0.76898746, 0.7728324, 0.77669656, 0.78058004, 0.78448294, 0.78840536, 0.79234738, 0.79630912, 0.80029067, 0.80429212, 0.80831358, 0.81235515, 0.81641693, 0.82049901, 0.8246015, 0.82872451, 0.83286813, 0.83703248, 0.84121764, 0.84542373, 0.84965084, 0.8538991, 0.85816859, 0.86245944, 0.86677173, 0.87110559, 0.87546112, 0.87983843, 0.88423762, 0.88865881, 0.8931021, 0.89756761, 0.90205545, 0.90656573, 0.91109856, 0.91565405, 0.92023232, 0.92483348, 0.92945765, 0.93410494, 0.93877546, 0.94346934, 0.94818668, 0.95292762, 0.95769226, 0.96248072, 0.96729312, 0.97212959, 0.97699023, 0.98187519, 0.98678456, 0.99171848, 0.99667708, 1.0016605, 1.0066688, 1.0117021, 1.0167606, 1.0218444, 1.0269536, 1.0320884, 1.0372489, 1.0424351, 1.0476473, 1.0528855, 1.0581499, 1.0634407, 1.0687579, 1.0741017, 1.0794722, 1.0848695, 1.0902939, 1.0957454, 1.1012241, 1.1067302, 1.1122639, 1.1178252, 1.1234143, 1.1290314, 1.1346765, 1.1403499, 1.1460517, 1.1517819, 1.1575408, 1.1633285, 1.1691452, 1.1749909, 1.1808659, 1.1867702, 1.192704, 1.1986676, 1.2046609, 1.2106842, 1.2167376, 1.2228213, 1.2289354, 1.2350801, 1.2412555, 1.2474618, 1.2536991, 1.2599676, 1.2662674, 1.2725988, 1.2789618, 1.2853566, 1.2917833, 1.294729, 1.2950709, 1.2982423, 1.3047335, 1.3112571, 1.3178134, 1.3244025, 1.3310245, 1.3321988, 1.3328011, 1.3376796, 1.344368, 1.3510899, 1.3578453, 1.3646345, 1.3714577, 1.378315, 1.3852066, 1.3921326, 1.3990933, 1.4060887, 1.4131192, 1.4201848, 1.4272857, 1.4344221, 1.4415942, 1.4488022, 1.4560462, 1.4633265, 1.4706431, 1.4779963, 1.4853863, 1.4928132, 1.5002773, 1.5077787, 1.5153176, 1.5228942, 1.5305086, 1.5381612, 1.545852, 1.5535812, 1.5613491, 1.5691559, 1.5770017, 1.5848867, 1.5928111, 1.6007752, 1.608779, 1.6168229, 1.624907, 1.6330316, 1.6411967, 1.6494027, 1.6576497, 1.665938, 1.6740015, 1.6742677, 1.6771986, 1.682639, 1.6910522, 1.6995075, 1.708005, 1.716545, 1.7251278, 1.7337534, 1.7424222, 1.7511343, 1.7598899, 1.7686894, 1.7775328, 1.7864205, 1.7953526, 1.8043294, 1.813351, 1.8224178, 1.8315299, 1.8399858, 1.8406875, 1.8436141, 1.8498909, 1.8591404, 1.8684361, 1.8777783, 1.8871672, 1.896603, 1.906086, 1.9156165, 1.9251945, 1.9348205, 1.9444946, 1.9542171, 1.9639882, 1.9738081, 1.9836772, 1.9935955, 2.0035635, 2.0135813, 2.0236492, 2.0337675, 2.0432385, 2.0439363, 2.0503613, 2.054156, 2.0644268, 2.0747489, 2.0851227, 2.0955483, 2.106026, 2.1165562, 2.1271389, 2.1377746, 2.1484635, 2.1592058, 2.1700018, 2.1808519, 2.1917561, 2.2027149, 2.2137285, 2.2247971, 2.2359211, 2.2471007, 2.2583362, 2.2696279, 2.280976, 2.2923809, 2.3038428, 2.315362, 2.3269388, 2.3385735, 2.3502664, 2.3620177, 2.3738278, 2.385697, 2.3976254, 2.4096136, 2.4216616, 2.4337699, 2.4459388, 2.4581685, 2.4704593, 2.4828116, 2.4952257, 2.5077018, 2.5202403, 2.5328415, 2.5455057, 2.5582333, 2.5710244, 2.5838796, 2.596799, 2.6097829, 2.6228319, 2.635946, 2.6491257, 2.6623714, 2.6756832, 2.6890617, 2.702507, 2.7160195, 2.7295996, 2.7432476, 2.7569638, 2.7707486, 2.7846024, 2.7985254, 2.812518, 2.8265806, 2.8407135, 2.8549171, 2.8691917, 2.8835376, 2.8979553, 2.9124451, 2.9270073, 2.9416424, 2.9563506, 2.9711323, 2.985988, 3.0009179, 3.0159225, 3.0310021, 3.0461571, 3.0613879, 3.0766949, 3.0920783, 3.1075387, 3.1230764, 3.1386918, 3.1543853, 3.1701572, 3.186008, 3.201938, 3.2179477, 3.2340374, 3.2502076, 3.2664587, 3.282791, 3.2992049, 3.3157009, 3.3322794, 3.3489408, 3.3656856, 3.382514, 3.3994265, 3.4164237, 3.4335058, 3.4506733, 3.4679267, 3.4852663, 3.5026927, 3.5202061, 3.5378072, 3.5554962, 3.5732737, 3.59114, 3.6090957, 3.6271412, 3.6452769, 3.6635033, 3.6818208, 3.7002299, 3.7187311, 3.7373247, 3.7560114, 3.7747914, 3.7936654, 3.8126337, 3.8316969, 3.8508554, 3.8701096, 3.8894602, 3.9089075, 3.928452, 3.9480943, 3.9678347, 3.9876739, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.634298, 7.75115, 7.78231, 7.829051, 7.902609, 7.945902, 8.408988, 8.44789, 8.537697, 8.572019, 8.623503, 8.752212, 8.864884, 9.000571, 9.030794, 9.036754, 9.091029, 9.226716, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 52.71273, 53.51956, 53.73471, 54.05744, 54.71721, 54.86427, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'I': {'mass_absorption_coefficient (cm2/g)': [4174.4, 5362.5, 11846.0, 34289.0, 70404.0, 106260.0, 126140.0, 124730.0, 107770.0, 84689.0, 62528.0, 44535.0, 31247.0, 21949.0, 15625.0, 14294.0, 13287.0, 13036.0, 15503.0, 14611.0, 14130.0, 11072.0, 8972.6, 7526.4, 6516.6, 5801.8, 5504.5, 5393.9, 5366.0, 6003.7, 5967.0, 5907.8, 5593.0, 5315.5, 5098.2, 4920.1, 4766.3, 4626.0, 4490.7, 4354.0, 4211.1, 4058.3, 3893.5, 3715.9, 3525.8, 3324.7, 3114.9, 2898.8, 2680.3, 2563.3, 2513.9, 2501.5, 2500.9, 15194.0, 14950.0, 14809.0, 14642.0, 22843.0, 22563.0, 21605.0, 19301.0, 15964.0, 13439.0, 11438.0, 10237.0, 9880.9, 9782.1, 11017.0, 10669.0, 10173.0, 9844.0, 9758.9, 10193.0, 10173.0, 9858.5, 8846.5, 7924.9, 7641.0, 7585.2, 7829.8, 7566.9, 6856.4, 5881.4, 5046.0, 4331.0, 3714.9, 3176.6, 2712.6, 2317.0, 1980.1, 1693.3, 1448.6, 1239.1, 1058.0, 899.45, 765.9, 646.62, 541.83, 454.43, 379.79, 317.22, 265.39, 245.19, 235.52, 233.03, 772.32, 743.58, 736.21, 684.04, 653.24, 645.7, 888.21, 847.42, 784.94, 752.39, 744.14, 847.83, 814.85, 689.97, 585.14, 493.85, 415.52, 349.17, 293.42, 246.73, 207.3, 172.7, 143.66, 119.28, 99.085, 82.363, 68.524, 57.062, 47.56, 39.676, 33.102, 27.632, 23.067, 19.222, 15.995, 13.274, 11.026, 9.0797, 7.4598, 6.134, 5.9045, 5.6482, 5.5824, 34.603, 33.275, 32.095, 26.9, 22.54, 18.883, 15.787, 13.175, 10.991, 9.1664, 7.6433, 6.3726, 5.3109, 4.4114, 3.6646, 3.0398, 2.5128, 2.0774, 1.7175, 1.4202, 1.1744, 0.97118, 0.80321, 0.66434, 0.54961, 0.4548, 0.37637, 0.31148, 0.2578, 0.21338, 0.17663, 0.14621, 0.12104, 0.10021, 0.082965, 0.068693, 0.056879, 0.047099, 0.039003, 0.032299, 0.02675, 0.0], - 'energies (keV)': [0.049848, 0.050592, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.120246, 0.1220865, 0.1225773, 0.1233135, 0.125154, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.182672, 0.185468, 0.1862136, 0.187332, 0.1883732, 0.190128, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.607012, 0.616303, 0.618674, 0.6187806, 0.622497, 0.6260625, 0.6281435, 0.6306687, 0.631788, 0.6344565, 0.643926, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.857108, 0.870227, 0.8739896, 0.878973, 0.892092, 0.91189, 0.9258475, 0.9295695, 0.9342948, 0.9351525, 0.94911, 0.9987612, 1.050658, 1.067676, 1.071028, 1.077461, 1.093542, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.465958, 4.534314, 4.552543, 4.579885, 4.633924, 4.648242, 4.755058, 4.827839, 4.847248, 4.87636, 4.953664, 5.084338, 5.162159, 5.182912, 5.21404, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 32.50601, 33.00355, 33.13623, 33.33525, 33.83279, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'U': {'mass_absorption_coefficient (cm2/g)': [43925.0, 41045.0, 39435.0, 29057.0, 21398.0, 15774.0, 14353.0, 13401.0, 13161.0, 32671.0, 30575.0, 29776.0, 22128.0, 16612.0, 12684.0, 9820.9, 7691.3, 6080.4, 4844.2, 4833.3, 4594.0, 4533.1, 5139.6, 4877.5, 4452.1, 3540.3, 2844.2, 2308.0, 2028.0, 1938.5, 1915.7, 19377.0, 19300.0, 18422.0, 15922.0, 15716.0, 15182.0, 14993.0, 24864.0, 23825.0, 21701.0, 17431.0, 13494.0, 10162.0, 7564.9, 5639.3, 4251.6, 3264.1, 2563.1, 2436.6, 2316.9, 2286.8, 3201.2, 3074.8, 2979.5, 2522.5, 2177.6, 1916.1, 1811.7, 1767.0, 1755.6, 2175.3, 2134.9, 2110.0, 1876.6, 1676.1, 1547.9, 1521.6, 1516.3, 1508.3, 1614.3, 1585.2, 1514.3, 1411.8, 1389.0, 1369.0, 1363.9, 1479.0, 1480.2, 1492.9, 1495.6, 1501.2, 1600.2, 1606.8, 1663.6, 2032.3, 3145.3, 4893.0, 6908.5, 8681.4, 9836.3, 10262.0, 10059.0, 9425.1, 9300.3, 9110.0, 9058.2, 9862.1, 9664.5, 9441.1, 9228.6, 9171.9, 9595.9, 9386.1, 9006.1, 8067.7, 7177.0, 6363.8, 6069.6, 5895.8, 5850.6, 6143.4, 5983.8, 5965.1, 5283.3, 4660.9, 4471.3, 4338.7, 4304.3, 4317.1, 4191.3, 4151.4, 3622.4, 3530.0, 3422.1, 3394.1, 3417.5, 3316.9, 3224.0, 2799.3, 2417.7, 2091.8, 1811.6, 1570.9, 1362.4, 1183.8, 1030.7, 898.57, 784.88, 673.78, 568.62, 504.62, 485.81, 480.87, 1034.7, 1048.9, 1057.9, 1076.3, 1081.6, 1485.8, 1473.1, 1470.3, 1321.2, 1187.8, 1134.8, 1120.9, 1281.3, 1272.8, 1226.7, 1041.5, 862.65, 806.29, 774.13, 765.9, 803.66, 773.79, 770.22, 720.65, 693.92, 687.04, 707.14, 681.68, 581.12, 496.29, 423.79, 360.45, 305.56, 259.4, 219.58, 183.77, 154.11, 129.18, 108.34, 90.992, 76.545, 64.461, 54.329, 45.715, 43.232, 41.572, 41.145, 100.09, 96.295, 95.016, 79.607, 66.409, 62.806, 60.262, 59.608, 83.498, 80.775, 80.34, 79.051, 77.647, 76.841, 87.479, 84.333, 77.249, 65.211, 55.04, 46.237, 38.734, 32.47, 27.242, 22.806, 19.111, 16.032, 13.407, 11.199, 9.3624, 7.8328, 6.5581, 5.4951, 4.6078, 3.8666, 3.2449, 2.7155, 2.261, 1.8829, 1.5639, 1.2994, 1.0988, 1.0803, 1.0537, 1.0421, 4.4898, 4.3242, 3.9778, 3.3558, 2.8286, 2.3833, 2.008, 1.6908, 1.4232, 1.198, 1.0085, 0.84913, 0.71501, 0.60273, 0.50826, 0.42865, 0.36155, 0.30499, 0.2573, 0.21709, 0.18318, 0.15459, 0.0], - 'energies (keV)': [0.0324615, 0.032946, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.041454, 0.0420885, 0.0422577, 0.0425115, 0.043146, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.069286, 0.0703465, 0.0706293, 0.0710535, 0.072114, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.094374, 0.0958185, 0.0962037, 0.09665893, 0.0967815, 0.098226, 0.1029, 0.1033284, 0.104475, 0.104895, 0.105525, 0.1071, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.191198, 0.1941245, 0.1949049, 0.1960755, 0.199002, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.254114, 0.2580035, 0.2590407, 0.2605965, 0.2629708, 0.264486, 0.2811158, 0.3005128, 0.317226, 0.3212482, 0.3220815, 0.3233763, 0.3253185, 0.330174, 0.3434143, 0.3671099, 0.373282, 0.3789955, 0.3805191, 0.3828045, 0.383474, 0.388518, 0.3893435, 0.3909087, 0.3924405, 0.3932565, 0.399126, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.722946, 0.7340115, 0.7369623, 0.7413885, 0.752454, 0.7648052, 0.776498, 0.7796196, 0.784302, 0.796008, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.024002, 1.039675, 1.043855, 1.050124, 1.065798, 1.067676, 1.141345, 1.220098, 1.247148, 1.266237, 1.271327, 1.278963, 1.298052, 1.304285, 1.394281, 1.411984, 1.433596, 1.439359, 1.448004, 1.469616, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.480666, 3.533942, 3.548445, 3.569459, 3.622734, 3.653048, 3.708962, 3.723872, 3.746238, 3.793288, 3.802152, 4.055024, 4.217332, 4.281883, 4.299097, 4.324917, 4.334821, 4.389468, 4.633924, 4.953664, 5.078556, 5.156289, 5.177018, 5.208111, 5.285844, 5.295467, 5.43704, 5.52026, 5.542452, 5.57574, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 16.82297, 17.08047, 17.14913, 17.25213, 17.50963, 17.59961, 18.81398, 20.11215, 20.52865, 20.84286, 20.92665, 21.05234, 21.32225, 21.36655, 21.49988, 21.64861, 21.73564, 21.86619, 22.19255, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.294, 113.9931, 115.0281, 115.4905, 116.1841, 117.9182, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Y': {'mass_absorption_coefficient (cm2/g)': [182340.0, 171650.0, 145230.0, 110490.0, 82879.0, 61580.0, 45632.0, 33917.0, 25402.0, 19240.0, 17412.0, 16396.0, 16140.0, 16639.0, 15782.0, 15676.0, 12540.0, 10275.0, 8616.2, 7385.9, 6458.7, 5746.6, 5188.1, 4739.8, 4371.4, 4061.5, 3795.1, 3561.6, 3353.6, 3165.3, 2992.3, 2831.3, 2679.9, 2536.1, 2535.4, 2506.4, 2503.6, 2495.3, 5307.8, 5383.7, 5464.7, 7485.8, 7519.3, 8048.2, 8577.0, 12440.0, 17530.0, 22816.0, 27501.0, 30974.0, 32933.0, 33387.0, 32557.0, 31412.0, 30952.0, 30825.0, 33670.0, 33532.0, 33051.0, 33034.0, 32514.0, 32369.0, 33361.0, 32806.0, 32494.0, 29792.0, 26955.0, 24856.0, 24221.0, 24135.0, 24054.0, 24864.0, 24242.0, 22444.0, 19876.0, 17523.0, 15386.0, 13468.0, 11760.0, 10246.0, 8906.9, 7725.7, 6688.4, 5780.9, 4989.6, 4301.4, 3704.6, 3144.4, 2670.6, 2267.6, 1915.1, 1618.0, 1368.9, 1159.7, 982.89, 834.19, 708.95, 633.97, 611.17, 605.29, 2581.2, 2547.2, 2471.9, 2441.3, 2366.6, 2339.6, 3228.6, 3097.2, 3000.5, 2667.0, 2563.8, 2537.3, 2896.1, 2876.2, 2772.1, 2456.7, 2089.7, 1772.8, 1495.4, 1255.5, 1054.9, 887.08, 746.59, 626.29, 523.06, 436.29, 363.74, 303.52, 253.22, 211.4, 176.66, 147.77, 123.65, 103.57, 86.612, 71.579, 59.158, 48.724, 40.167, 33.141, 27.37, 22.625, 18.719, 15.502, 14.898, 14.275, 14.114, 102.2, 97.831, 94.262, 78.414, 65.883, 55.496, 46.596, 38.982, 32.586, 27.138, 22.576, 18.783, 15.618, 12.904, 10.663, 8.8118, 7.2824, 6.0182, 4.973, 4.107, 3.3921, 2.8018, 2.3143, 1.9118, 1.5759, 1.295, 1.0621, 0.87111, 0.71451, 0.58608, 0.48076, 0.39438, 0.32354, 0.26543, 0.21777, 0.17867, 0.1466, 0.12028, 0.098699, 0.08099, 0.066461, 0.05454, 0.044758, 0.036732, 0.030147, 0.024742, 0.020307, 0.016668, 0.013681, 0.01123, 0.0092179, 0.0], - 'energies (keV)': [0.025728, 0.026112, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.044492, 0.045173, 0.0453546, 0.045627, 0.046308, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.154252, 0.156408, 0.156613, 0.1572426, 0.158187, 0.158802, 0.1594404, 0.160398, 0.160548, 0.162792, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.294294, 0.2987985, 0.2999997, 0.3005128, 0.3018015, 0.306152, 0.306306, 0.310838, 0.3120876, 0.313962, 0.318648, 0.3212482, 0.3434143, 0.3671099, 0.385728, 0.391632, 0.3924405, 0.3932064, 0.395568, 0.401472, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.0384, 2.0696, 2.07792, 2.080733, 2.0904, 2.11239, 2.1216, 2.144722, 2.153344, 2.166277, 2.19861, 2.224304, 2.32505, 2.360637, 2.370127, 2.377781, 2.384362, 2.41995, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 16.69763, 16.95321, 17.02136, 17.12359, 17.37917, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ac': {'mass_absorption_coefficient (cm2/g)': [86550.0, 81955.0, 71371.0, 55808.0, 43547.0, 33941.0, 29665.0, 28031.0, 27615.0, 64526.0, 63126.0, 60808.0, 48347.0, 37260.0, 29222.0, 23247.0, 18700.0, 15172.0, 13300.0, 12705.0, 12552.0, 13514.0, 13446.0, 12837.0, 10987.0, 8973.7, 7369.4, 6076.4, 5025.5, 4167.0, 3463.8, 2887.1, 2414.0, 2298.5, 2208.5, 2185.4, 36115.0, 35415.0, 35033.0, 33710.0, 33044.0, 32871.0, 51408.0, 50787.0, 50659.0, 45962.0, 38708.0, 29961.0, 21873.0, 15428.0, 10729.0, 7479.9, 5293.4, 3836.7, 3253.5, 3047.4, 2996.0, 4114.5, 4045.4, 3906.7, 3242.3, 2666.8, 2253.2, 2235.6, 2159.5, 2140.2, 2726.7, 2625.0, 2477.0, 2125.6, 1861.5, 1735.0, 1692.0, 1681.1, 1805.2, 1801.0, 1766.3, 1641.9, 1585.1, 1556.6, 1549.3, 1660.2, 1660.9, 1664.4, 1665.3, 1668.9, 1672.6, 1771.1, 1819.3, 2021.9, 2895.2, 4459.6, 6542.8, 8742.3, 10612.0, 11851.0, 12260.0, 12290.0, 12316.0, 12338.0, 12357.0, 12372.0, 12384.0, 12391.0, 12396.0, 12397.0, 12394.0, 12389.0, 12380.0, 12368.0, 12353.0, 12335.0, 12315.0, 12291.0, 12265.0, 12236.0, 12204.0, 12170.0, 12134.0, 12095.0, 12054.0, 12010.0, 11965.0, 11917.0, 11868.0, 11817.0, 11763.0, 11708.0, 11652.0, 11594.0, 11534.0, 11473.0, 11410.0, 11346.0, 11281.0, 11215.0, 11147.0, 11079.0, 11009.0, 10939.0, 10867.0, 10795.0, 10722.0, 10648.0, 10574.0, 10541.0, 11406.0, 11381.0, 11308.0, 11233.0, 11159.0, 11083.0, 11008.0, 10932.0, 10855.0, 10779.0, 10702.0, 10625.0, 10548.0, 10546.0, 11037.0, 10980.0, 10905.0, 10829.0, 10753.0, 10677.0, 10601.0, 10525.0, 10449.0, 10373.0, 10297.0, 10221.0, 10146.0, 10070.0, 9994.3, 9918.8, 9843.5, 9768.3, 9693.3, 9618.5, 9544.0, 9469.5, 9395.3, 9321.3, 9247.4, 9173.7, 9100.3, 9027.1, 8954.2, 8881.6, 8809.3, 8737.2, 8665.5, 8594.2, 8523.1, 8452.5, 8382.1, 8312.2, 8242.7, 8173.5, 8104.7, 8036.4, 7968.5, 7901.0, 7833.9, 7767.3, 7701.1, 7635.4, 7570.1, 7505.3, 7441.0, 7377.0, 7313.2, 7249.9, 7187.0, 7124.6, 7098.0, 7523.7, 7510.5, 7447.1, 7384.2, 7321.7, 7259.7, 7198.2, 7137.2, 7076.6, 7016.6, 6957.0, 6897.9, 6839.3, 6781.1, 6723.5, 6666.3, 6609.6, 6553.4, 6497.7, 6442.5, 6387.7, 6333.5, 6279.6, 6226.3, 6173.3, 6117.8, 6062.6, 6007.9, 5953.5, 5899.6, 5846.0, 5792.9, 5740.2, 5688.0, 5636.1, 5584.6, 5533.6, 5483.0, 5432.9, 5382.7, 5372.1, 5430.3, 5409.8, 5358.2, 5307.1, 5256.5, 5206.3, 5156.5, 5107.2, 5058.4, 5010.0, 4962.0, 4914.8, 4867.2, 4819.6, 4772.4, 4725.7, 4679.4, 4633.6, 4588.2, 4540.5, 4492.5, 4445.1, 4398.2, 4351.8, 4306.0, 4258.5, 4211.2, 4164.5, 4118.3, 4072.8, 4027.8, 3983.4, 3939.5, 3917.3, 3976.4, 3975.2, 3932.0, 3889.4, 3847.3, 3805.8, 3764.7, 3724.1, 3684.0, 3644.4, 3605.3, 3566.6, 3528.4, 3490.6, 3453.3, 3416.5, 3380.1, 3344.1, 3308.5, 3273.4, 3238.7, 3204.4, 3170.3, 3136.7, 3103.4, 3070.5, 3038.1, 3005.9, 2973.9, 2942.4, 2911.1, 2880.3, 2849.8, 2819.7, 2789.9, 2760.4, 2731.0, 2702.0, 2673.3, 2644.9, 2616.9, 2589.2, 2561.8, 2534.6, 2507.7, 2481.1, 2454.8, 2428.8, 2403.1, 2377.7, 2352.6, 2327.8, 2303.3, 2279.0, 2255.0, 2231.3, 2207.8, 2184.6, 2161.7, 2139.0, 2116.6, 2094.4, 2072.5, 2050.9, 2029.5, 2008.3, 1987.4, 1966.7, 1946.3, 1926.0, 1906.1, 1886.3, 1866.8, 1847.4, 1828.3, 1809.5, 1790.8, 1772.3, 1754.1, 1736.0, 1718.2, 1700.6, 1683.1, 1665.9, 1648.8, 1632.0, 1615.3, 1598.8, 1582.5, 1566.4, 1550.5, 1534.7, 1519.2, 1503.8, 1488.5, 1473.5, 1458.6, 1443.8, 1429.3, 1414.9, 1400.6, 1386.6, 1372.6, 1358.9, 1345.2, 1331.8, 1318.5, 1305.3, 1292.2, 1279.3, 1266.4, 1253.8, 1241.2, 1228.8, 1216.6, 1204.4, 1192.4, 1180.6, 1168.8, 1157.2, 1145.7, 1134.4, 1123.2, 1112.1, 1101.1, 1090.2, 1079.5, 1068.8, 1058.3, 1047.9, 1037.6, 1027.4, 1017.4, 1007.4, 997.55, 987.81, 978.17, 968.63, 959.2, 949.87, 940.63, 931.21, 921.64, 912.18, 902.82, 893.57, 884.41, 875.37, 866.42, 857.57, 848.82, 840.17, 831.61, 822.87, 814.19, 805.62, 797.14, 788.76, 780.47, 772.28, 764.18, 756.17, 748.26, 740.43, 732.69, 725.03, 717.46, 709.98, 702.58, 695.27, 688.03, 680.88, 673.81, 666.81, 658.78, 650.47, 642.28, 634.2, 626.23, 618.34, 610.57, 602.9, 595.34, 587.87, 580.51, 573.25, 566.09, 559.02, 556.81, 1449.7, 1438.8, 1419.7, 1400.9, 1382.3, 1364.0, 1346.0, 1328.2, 1310.6, 1293.3, 1284.5, 1800.7, 1793.9, 1775.1, 1756.5, 1738.2, 1720.1, 1702.2, 1684.6, 1667.2, 1650.0, 1633.0, 1614.1, 1594.6, 1575.3, 1556.0, 1536.8, 1517.6, 1498.7, 1479.8, 1461.0, 1442.4, 1423.9, 1405.5, 1387.3, 1369.3, 1351.4, 1333.6, 1316.0, 1298.6, 1281.3, 1264.2, 1263.2, 1466.2, 1458.1, 1439.7, 1421.5, 1403.5, 1385.7, 1368.1, 1350.7, 1333.5, 1316.5, 1299.7, 1283.0, 1266.5, 1250.2, 1234.1, 1218.0, 1202.0, 1186.3, 1170.8, 1155.5, 1140.3, 1125.4, 1110.7, 1096.1, 1081.9, 1067.9, 1054.0, 1040.4, 1026.9, 1013.6, 1000.5, 987.53, 974.76, 962.17, 949.74, 937.48, 936.59, 989.83, 985.82, 973.45, 961.24, 949.18, 937.29, 925.55, 913.98, 902.55, 891.28, 880.15, 869.2, 858.39, 847.72, 837.2, 834.0, 863.2, 861.23, 852.77, 842.48, 832.32, 822.2, 812.22, 802.37, 792.64, 783.04, 773.57, 764.2, 754.94, 745.79, 736.76, 727.85, 719.04, 710.35, 701.77, 693.3, 684.93, 676.66, 668.48, 660.4, 652.42, 644.54, 636.76, 629.08, 621.49, 613.99, 606.59, 599.27, 592.05, 584.93, 577.89, 570.94, 564.07, 557.29, 550.6, 543.99, 537.41, 530.91, 524.5, 518.16, 511.9, 505.72, 499.62, 493.59, 487.63, 481.76, 475.95, 470.22, 464.5, 458.81, 453.2, 447.65, 442.18, 436.77, 431.43, 426.15, 420.95, 415.77, 410.61, 405.52, 400.49, 395.53, 390.63, 385.8, 381.03, 376.33, 371.68, 367.09, 362.57, 358.1, 353.69, 349.34, 345.04, 340.8, 336.62, 332.49, 328.41, 324.39, 320.41, 316.49, 312.62, 308.8, 305.03, 301.31, 297.64, 294.01, 290.43, 286.87, 283.34, 279.86, 276.42, 273.03, 269.68, 266.37, 263.11, 259.89, 256.72, 253.58, 250.48, 247.43, 244.41, 241.43, 238.5, 235.59, 232.73, 202.38, 169.39, 141.84, 118.89, 99.818, 83.883, 70.559, 59.367, 49.901, 48.642, 46.769, 46.287, 114.01, 109.7, 105.0, 87.803, 74.396, 73.187, 71.369, 70.59, 98.374, 94.911, 94.635, 91.215, 90.264, 102.81, 100.63, 99.087, 85.014, 71.664, 60.409, 50.945, 42.701, 35.744, 29.933, 25.029, 20.95, 17.535, 14.628, 12.204, 10.189, 8.514, 7.1195, 5.9579, 4.9895, 4.1817, 3.5033, 2.9349, 2.4542, 2.0377, 1.689, 1.401, 1.2265, 1.1756, 1.163, 1.1626, 5.174, 4.9827, 4.4331, 3.7368, 3.1459, 2.6476, 2.228, 1.873, 1.5741, 1.3229, 1.1118, 0.93459, 0.78569, 0.66107, 0.55629, 0.46817, 0.39405, 0.3317, 0.27925, 0.23511, 0.19797, 0.16672, 0.14041, 0.0], - 'energies (keV)': [0.0184941, 0.01877013, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.0246814, 0.02505918, 0.02515992, 0.02531103, 0.02545001, 0.0256888, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.03965432, 0.04026127, 0.04042313, 0.04060054, 0.04066591, 0.04127286, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07540014, 0.07655423, 0.07686198, 0.07732361, 0.0784777, 0.07912411, 0.08147337, 0.08272042, 0.08305296, 0.08355178, 0.08458368, 0.08479882, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1599698, 0.1624183, 0.1630713, 0.1640507, 0.1648404, 0.1664992, 0.1762144, 0.1883732, 0.2013709, 0.202048, 0.2051405, 0.2059652, 0.2072023, 0.2102948, 0.2152655, 0.2301188, 0.245997, 0.2560302, 0.259949, 0.260994, 0.2625616, 0.2629708, 0.2664804, 0.2811158, 0.2891658, 0.2935918, 0.294772, 0.2965424, 0.2978648, 0.3005128, 0.3009684, 0.302424, 0.3036397, 0.3054634, 0.3100226, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.63664076, 0.63735929, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.674518, 0.67528199, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.88919009, 0.89080989, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0789287, 1.0810714, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.2675407, 1.2704594, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2180698, 3.2199304, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3691316, 3.3712683, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9040746, 3.9139253, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6489696, 4.6630307, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 4.9909955, 5.0083023, 5.0130043, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 15.55358, 15.79165, 15.85513, 15.95036, 16.18842, 16.46362, 17.59961, 18.70154, 18.81398, 18.98778, 19.06412, 19.17862, 19.4432, 19.46486, 19.7408, 19.82016, 19.9392, 20.11215, 20.2368, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 104.6202, 106.2215, 106.6353, 106.6485, 107.2891, 108.8904, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ag': {'mass_absorption_coefficient (cm2/g)': [167750.0, 167440.0, 166510.0, 164950.0, 162680.0, 159630.0, 155860.0, 151450.0, 146470.0, 141040.0, 135220.0, 129130.0, 122840.0, 116450.0, 110030.0, 103640.0, 97352.0, 91213.0, 85263.0, 79537.0, 74059.0, 68847.0, 63911.0, 59258.0, 54888.0, 52849.0, 51925.0, 51683.0, 73446.0, 71677.0, 70527.0, 60486.0, 58750.0, 56700.0, 56150.0, 65461.0, 62625.0, 60099.0, 50409.0, 43330.0, 37899.0, 33503.0, 29815.0, 28273.0, 27562.0, 27378.0, 27884.0, 27434.0, 27235.0, 24722.0, 22339.0, 20224.0, 18336.0, 16642.0, 15118.0, 13743.0, 12500.0, 11372.0, 10348.0, 9416.5, 8566.8, 7790.7, 7079.9, 6428.0, 5829.5, 5280.0, 4775.7, 4313.3, 4021.0, 3927.1, 3919.1, 3902.6, 8289.7, 8334.0, 8411.0, 8460.3, 11600.0, 11641.0, 12017.0, 12917.0, 15159.0, 16867.0, 17647.0, 17521.0, 16675.0, 16282.0, 15984.0, 15902.0, 17939.0, 17591.0, 17474.0, 17279.0, 16901.0, 16799.0, 17523.0, 17132.0, 16630.0, 14823.0, 13510.0, 13117.0, 13062.0, 13014.0, 13424.0, 13042.0, 11948.0, 10421.0, 9057.1, 7855.1, 6802.0, 5840.7, 5004.5, 4283.9, 3662.6, 3130.6, 2672.5, 2264.0, 1920.9, 1632.5, 1389.7, 1185.0, 1012.2, 863.1, 733.38, 623.57, 530.78, 446.12, 382.06, 370.95, 366.4, 362.37, 1204.9, 1173.4, 1152.2, 1121.6, 1113.0, 1500.8, 1494.4, 1454.2, 1337.2, 1287.1, 1281.5, 1274.0, 1443.7, 1391.2, 1243.2, 1041.5, 872.75, 732.83, 615.57, 517.77, 435.86, 367.06, 308.61, 258.63, 216.63, 181.48, 151.8, 125.97, 104.63, 86.957, 72.289, 60.144, 50.09, 41.731, 34.689, 28.812, 23.84, 19.741, 16.362, 13.574, 11.271, 9.3665, 8.9233, 8.5567, 8.4624, 54.529, 52.35, 51.042, 42.957, 36.138, 30.117, 25.038, 20.863, 17.386, 14.491, 12.079, 10.069, 8.3924, 6.9525, 5.7602, 4.7727, 3.9549, 3.2774, 2.7159, 2.2474, 1.8534, 1.5286, 1.2608, 1.0399, 0.85741, 0.70652, 0.58222, 0.47981, 0.39543, 0.32591, 0.26863, 0.22142, 0.18252, 0.15046, 0.12404, 0.10226, 0.084306, 0.069508, 0.057311, 0.047255, 0.038965, 0.032131, 0.026496, 0.02185, 0.01802, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.054782, 0.0556205, 0.0558441, 0.0561795, 0.05667876, 0.057018, 0.06058959, 0.061348, 0.062287, 0.0625374, 0.062913, 0.063852, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.093296, 0.094724, 0.0951048, 0.095676, 0.09665893, 0.097104, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.359366, 0.3648665, 0.365344, 0.3663333, 0.3671099, 0.3685335, 0.370936, 0.3724272, 0.374034, 0.374664, 0.380256, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.559972, 0.568543, 0.5708286, 0.574257, 0.582828, 0.5856525, 0.590352, 0.599388, 0.6017976, 0.605412, 0.614448, 0.6260625, 0.6692609, 0.70315, 0.7139125, 0.7154399, 0.7167825, 0.7210875, 0.73185, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.284078, 3.319406, 3.334344, 3.347749, 3.367855, 3.418122, 3.453226, 3.506081, 3.520176, 3.541318, 3.548445, 3.594174, 3.729684, 3.786771, 3.793288, 3.801994, 3.824829, 3.881916, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 25.00372, 25.38643, 25.48849, 25.64157, 26.02428, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ir': {'mass_absorption_coefficient (cm2/g)': [25921.0, 25050.0, 19380.0, 19072.0, 19006.0, 48342.0, 46942.0, 46273.0, 52931.0, 61738.0, 72476.0, 84772.0, 98015.0, 111330.0, 123640.0, 133750.0, 140580.0, 143310.0, 141570.0, 135500.0, 125720.0, 113210.0, 99084.0, 84454.0, 70284.0, 57254.0, 45806.0, 36121.0, 28168.0, 21794.0, 16925.0, 16780.0, 15941.0, 15690.0, 31770.0, 30081.0, 27041.0, 21149.0, 17894.0, 16910.0, 16660.0, 18390.0, 18155.0, 17195.0, 17164.0, 16771.0, 16235.0, 15999.0, 22217.0, 21946.0, 22760.0, 22184.0, 21764.0, 21661.0, 17803.0, 14744.0, 12272.0, 10350.0, 8948.4, 8458.3, 8257.3, 8208.1, 8329.8, 8218.1, 8171.3, 7734.3, 7646.8, 7919.3, 8516.8, 9408.6, 10541.0, 11854.0, 13280.0, 14726.0, 16103.0, 17325.0, 18317.0, 19020.0, 19397.0, 19409.0, 19050.0, 18801.0, 18642.0, 18598.0, 18961.0, 18809.0, 18798.0, 18628.0, 18437.0, 18385.0, 18617.0, 18422.0, 18267.0, 17258.0, 16135.0, 14954.0, 13760.0, 12589.0, 11463.0, 11293.0, 11047.0, 10983.0, 11595.0, 11489.0, 11408.0, 11328.0, 11247.0, 11168.0, 11088.0, 11009.0, 10930.0, 10852.0, 10774.0, 10697.0, 10620.0, 10543.0, 10467.0, 10391.0, 10316.0, 10241.0, 10166.0, 10092.0, 10018.0, 9945.1, 9872.1, 9799.5, 9727.3, 9655.5, 9584.1, 9513.1, 9442.5, 9372.2, 9339.9, 9443.6, 9440.6, 9374.2, 9305.1, 9236.5, 9168.2, 9100.3, 9032.9, 8965.8, 8899.1, 8832.8, 8766.9, 8701.4, 8636.2, 8571.5, 8507.1, 8443.1, 8379.6, 8316.3, 8253.5, 8191.1, 8129.0, 8067.3, 8006.0, 7945.1, 7884.5, 7824.3, 7764.5, 7705.0, 7645.9, 7587.2, 7528.8, 7470.8, 7413.2, 7355.9, 7298.9, 7242.4, 7226.3, 7361.1, 7357.0, 7300.5, 7244.4, 7188.7, 7133.3, 7078.2, 7023.5, 6969.2, 6915.2, 6861.5, 6808.2, 6755.3, 6702.6, 6650.4, 6598.4, 6546.8, 6495.6, 6444.6, 6394.0, 6343.8, 6293.8, 6244.2, 6194.9, 6145.9, 6097.3, 6049.0, 6000.9, 5953.2, 5905.9, 5858.8, 5812.0, 5765.6, 5719.5, 5673.6, 5628.1, 5582.9, 5538.0, 5493.4, 5449.1, 5404.8, 5360.9, 5317.2, 5273.9, 5230.8, 5188.0, 5145.6, 5103.4, 5061.5, 5019.9, 4978.5, 4937.5, 4896.8, 4856.3, 4816.1, 4776.2, 4736.6, 4697.3, 4658.2, 4619.5, 4581.0, 4542.8, 4504.8, 4467.1, 4429.8, 4392.6, 4355.7, 4319.1, 4282.8, 4246.7, 4210.9, 4175.3, 4139.9, 4104.8, 4070.0, 4035.3, 3996.4, 3957.9, 3919.7, 3881.9, 3844.4, 3807.3, 3770.5, 3734.0, 3697.9, 3662.1, 3626.7, 3591.6, 3556.8, 3522.4, 3488.3, 3454.5, 3421.0, 3387.9, 3355.1, 3322.6, 3290.4, 3258.5, 3226.9, 3195.7, 3164.7, 3133.3, 3101.3, 3069.7, 3038.4, 3007.4, 2976.8, 2946.4, 2916.4, 2885.1, 2853.8, 2821.8, 2790.3, 2759.1, 2728.3, 2697.9, 2667.9, 2638.2, 2609.0, 2580.0, 2550.4, 2521.1, 2492.2, 2463.7, 2435.5, 2407.6, 2380.2, 2353.0, 2326.3, 2299.8, 2273.7, 2247.9, 2222.4, 2197.3, 2172.5, 2148.0, 2123.7, 2099.8, 2076.2, 2052.9, 2029.9, 2007.1, 1984.7, 1962.5, 1940.6, 1919.0, 1897.6, 1876.5, 1855.7, 1835.1, 1814.7, 1794.7, 1774.8, 1755.2, 1735.9, 1716.8, 1697.9, 1679.2, 1660.8, 1642.6, 1624.6, 1606.9, 1589.3, 1572.0, 1554.9, 1538.0, 1521.2, 1504.7, 1488.4, 1472.3, 1456.4, 1440.7, 1425.1, 1409.8, 1394.6, 1379.6, 1364.8, 1350.2, 1335.7, 1321.4, 1307.3, 1293.3, 1279.6, 1265.9, 1252.5, 1239.2, 1226.0, 1213.0, 1200.2, 1187.5, 1175.0, 1162.6, 1150.3, 1138.2, 1126.3, 1114.4, 1102.7, 1091.2, 1079.8, 1068.5, 1057.3, 1046.3, 1035.4, 1024.6, 1014.0, 1003.4, 993.04, 982.76, 972.59, 962.54, 952.61, 942.79, 933.01, 923.2, 913.51, 903.92, 894.45, 885.09, 876.63, 2749.2, 2748.9, 2716.2, 2683.6, 2651.4, 2619.6, 2588.2, 2557.2, 2526.5, 2520.7, 3710.2, 3680.3, 3635.6, 3591.5, 3547.9, 3504.8, 3462.3, 3420.3, 3378.8, 3337.9, 3297.4, 3257.5, 3218.0, 3179.0, 3140.6, 3102.5, 3065.0, 3027.9, 2991.3, 2955.0, 2919.2, 2883.8, 2848.9, 2814.4, 2780.3, 2746.7, 2713.4, 2680.6, 2648.2, 2616.1, 2584.5, 2553.3, 2522.4, 2491.9, 2461.8, 2432.1, 2402.8, 2373.8, 2358.5, 2755.9, 2752.6, 2718.4, 2684.7, 2651.4, 2618.5, 2586.0, 2553.9, 2522.3, 2490.7, 2459.2, 2429.1, 2400.0, 2371.4, 2343.3, 2315.6, 2288.2, 2260.9, 2234.0, 2207.4, 2181.2, 2155.4, 2129.9, 2104.7, 2079.9, 2055.4, 2031.2, 2012.9, 2007.3, 2137.7, 2120.0, 2093.7, 2067.8, 2042.2, 2016.9, 1991.9, 1967.0, 1942.5, 1918.4, 1894.7, 1871.5, 1848.6, 1826.0, 1803.7, 1781.7, 1760.0, 1738.6, 1736.1, 1795.1, 1793.9, 1772.5, 1751.3, 1730.4, 1709.7, 1689.2, 1669.0, 1649.0, 1629.3, 1609.6, 1590.2, 1571.0, 1552.0, 1533.2, 1514.6, 1496.2, 1478.1, 1460.1, 1442.3, 1424.8, 1407.5, 1390.3, 1373.4, 1356.6, 1340.1, 1323.7, 1307.5, 1291.6, 1275.8, 1260.2, 1244.8, 1229.5, 1214.5, 1199.7, 1185.0, 1170.6, 1156.4, 1142.4, 1128.5, 1114.9, 1101.4, 1088.1, 1074.9, 1062.0, 1049.2, 1036.5, 1024.0, 1011.7, 999.51, 987.49, 975.59, 963.43, 951.43, 939.55, 927.82, 916.25, 904.83, 893.56, 882.24, 871.0, 859.91, 848.98, 838.2, 827.56, 817.06, 806.71, 796.5, 786.42, 776.49, 766.68, 757.01, 747.47, 738.06, 728.77, 719.61, 710.58, 701.66, 692.87, 684.19, 675.63, 667.18, 658.85, 650.63, 642.52, 634.51, 626.62, 618.83, 611.14, 603.55, 596.07, 588.69, 581.4, 574.21, 567.12, 560.12, 553.16, 546.17, 539.27, 532.46, 525.75, 519.12, 512.59, 506.14, 499.78, 493.5, 487.31, 481.2, 475.17, 469.22, 463.35, 457.55, 451.84, 446.17, 440.57, 435.04, 429.59, 424.21, 418.9, 413.67, 408.5, 403.4, 398.37, 393.34, 388.34, 383.42, 378.56, 373.76, 369.03, 364.36, 359.75, 355.21, 350.73, 346.3, 341.94, 337.63, 333.38, 329.19, 325.05, 320.97, 316.93, 312.94, 309.0, 305.11, 301.27, 297.48, 293.75, 290.06, 286.42, 282.83, 279.29, 275.79, 272.34, 268.94, 265.58, 262.26, 258.99, 255.77, 252.58, 249.44, 246.33, 243.27, 240.25, 237.27, 234.32, 231.42, 228.55, 225.72, 222.93, 220.18, 217.46, 214.77, 212.12, 209.51, 206.93, 204.38, 201.86, 199.38, 196.93, 194.52, 192.13, 189.77, 187.45, 185.15, 182.89, 180.65, 178.45, 176.27, 174.12, 172.0, 169.9, 167.83, 165.79, 163.77, 161.78, 159.82, 157.88, 155.97, 154.08, 152.21, 132.3, 110.37, 92.191, 77.929, 77.156, 74.796, 73.984, 194.96, 187.23, 172.29, 144.69, 143.45, 138.75, 137.22, 188.88, 181.45, 178.86, 171.64, 169.78, 193.91, 193.6, 186.43, 163.53, 137.54, 115.63, 97.114, 81.511, 68.445, 57.503, 48.304, 40.554, 34.061, 28.356, 23.55, 19.575, 16.285, 13.559, 11.298, 9.4223, 7.864, 6.5679, 5.4793, 4.5729, 3.8121, 3.1664, 2.6312, 2.1865, 1.9415, 1.8617, 1.8412, 9.1237, 9.0923, 8.7628, 7.705, 6.4619, 5.4198, 4.5469, 3.8029, 3.181, 2.6612, 2.2267, 1.8633, 1.5594, 1.3029, 1.0876, 0.90808, 0.75823, 0.63316, 0.52878, 0.44164, 0.3689, 0.30816, 0.25744, 0.21509, 0.17984, 0.15158, 0.12777, 0.10771, 0.090804, 0.0], - 'energies (keV)': [0.00688883, 0.006991648, 0.007901491, 0.008022432, 0.008054683, 0.008103059, 0.008224, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04949, 0.04959809, 0.0502475, 0.0504495, 0.0507525, 0.05151, 0.05302035, 0.05667876, 0.05929, 0.0601975, 0.0604395, 0.06058959, 0.0608025, 0.06171, 0.06174, 0.062132, 0.062685, 0.062937, 0.063083, 0.0633366, 0.063717, 0.06426, 0.064668, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.093296, 0.094724, 0.0951048, 0.095676, 0.09665893, 0.097104, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.289002, 0.2934255, 0.2946051, 0.2963745, 0.3005128, 0.300798, 0.305172, 0.309843, 0.3110886, 0.312957, 0.317628, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.484414, 0.4918285, 0.4938057, 0.4967715, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57626318, 0.57781099, 0.57793677, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.68899585, 0.69120417, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0399144, 2.0407952, 2.0408856, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1153256, 2.1168746, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5483176, 2.5530823, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9045696, 2.9079692, 2.9128303, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1671623, 3.1802379, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4282291, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 10.9909, 11.03212, 11.15912, 11.20399, 11.27128, 11.4395, 11.79334, 12.56762, 12.60708, 12.75998, 12.81128, 12.88822, 13.08058, 13.15013, 13.35141, 13.40508, 13.47697, 13.48559, 13.68687, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 74.58878, 75.73045, 76.03489, 76.38578, 76.49156, 77.63322, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Al': {'mass_absorption_coefficient (cm2/g)': [19119.0, 16513.0, 2677.3, 2054.0, 1744.8, 1609.9, 1586.4, 1645.1, 1772.8, 1963.6, 2215.0, 2524.7, 2888.8, 3300.5, 3748.8, 4218.6, 4691.0, 5144.2, 5555.8, 5904.6, 6162.5, 6305.1, 6334.2, 6259.1, 6095.1, 5853.5, 5538.7, 5180.1, 4795.5, 4400.5, 4008.1, 3812.2, 3726.1, 3703.6, 83378.0, 84589.0, 85797.0, 96319.0, 108400.0, 116930.0, 121370.0, 121850.0, 118890.0, 115460.0, 114020.0, 113620.0, 124430.0, 124250.0, 122650.0, 116650.0, 107640.0, 97963.0, 88119.0, 78493.0, 69358.0, 60883.0, 53154.0, 46198.0, 40004.0, 34534.0, 29734.0, 25545.0, 21903.0, 18741.0, 16006.0, 13647.0, 11619.0, 9878.3, 8388.4, 7115.2, 6028.2, 5101.6, 4313.1, 3643.4, 3075.3, 2594.3, 2187.5, 1843.9, 1553.9, 1309.5, 1103.7, 918.78, 765.33, 638.09, 532.48, 444.15, 369.28, 344.6, 330.52, 326.9, 4035.9, 3954.9, 3946.3, 3483.8, 2941.0, 2445.6, 2033.6, 1704.1, 1430.3, 1201.8, 1009.7, 847.65, 709.06, 587.62, 486.76, 403.24, 334.05, 276.75, 229.29, 189.98, 157.41, 130.43, 108.08, 89.558, 74.022, 60.881, 50.073, 41.186, 33.814, 27.577, 22.491, 18.343, 14.961, 12.202, 9.9528, 8.118, 6.6216, 5.401, 4.4056, 3.5936, 2.9314, 2.3912, 1.9506, 1.5912, 1.298, 1.0542, 0.85493, 0.69335, 0.56128, 0.45343, 0.3663, 0.29591, 0.23905, 0.19312, 0.15602, 0.12603, 0.1018, 0.082227, 0.066417, 0.053647, 0.043333, 0.035002, 0.028272, 0.022837, 0.018447, 0.0149, 0.012036, 0.009722, 0.007853, 0.0063434, 0.0051239, 0.0041389, 0.0033433, 0.0027006, 0.0021815, 0.0017622, 0.0014234, 0.0011498, 0.0009288, 0.00075026, 0.00060605, 0.00048956, 0.00039546, 0.00031945, 0.00025805, 0.00020845, 0.00016838, 0.0], - 'energies (keV)': [0.008417543, 0.008543178, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.071638, 0.0727345, 0.0730269, 0.0734655, 0.07401695, 0.074562, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.115346, 0.1171115, 0.1175823, 0.1180797, 0.1182885, 0.120054, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.528408, 1.551802, 1.55804, 1.567398, 1.590792, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'As': {'mass_absorption_coefficient (cm2/g)': [9196.7, 9709.9, 10929.0, 13940.0, 17489.0, 21481.0, 25836.0, 30438.0, 35139.0, 39773.0, 44166.0, 48146.0, 51564.0, 54293.0, 56248.0, 57381.0, 57686.0, 57192.0, 55963.0, 54086.0, 53405.0, 52862.0, 52715.0, 54328.0, 53823.0, 53784.0, 53595.0, 53245.0, 53100.0, 53660.0, 53134.0, 51947.0, 49280.0, 46334.0, 43165.0, 40359.0, 39878.0, 39604.0, 39405.0, 40753.0, 40010.0, 38189.0, 34892.0, 31684.0, 28609.0, 25702.0, 22984.0, 20471.0, 18165.0, 16067.0, 14170.0, 12465.0, 10940.0, 9581.0, 8375.4, 7309.1, 6368.1, 5538.5, 4809.0, 4169.7, 3611.1, 3105.4, 2665.2, 2491.8, 2463.4, 2435.4, 2407.7, 2380.3, 2353.2, 2326.4, 2300.0, 2273.8, 2247.9, 2222.4, 2197.1, 2172.1, 2147.4, 2123.0, 2098.9, 2075.1, 2051.5, 2028.2, 2005.2, 1982.4, 1959.9, 1934.6, 1909.2, 1884.1, 1859.4, 1835.1, 1811.0, 1787.3, 1763.9, 1740.9, 1718.2, 1695.7, 1673.6, 1651.8, 1630.3, 1609.1, 1588.2, 1567.6, 1547.2, 1527.1, 1507.4, 1487.8, 1468.6, 1449.6, 1430.8, 1412.4, 1394.1, 1376.2, 1358.4, 1340.9, 1323.7, 1306.7, 1289.9, 1273.3, 1257.0, 1240.9, 1225.0, 1209.3, 1193.8, 1178.5, 1163.5, 1148.6, 1134.0, 1119.5, 1105.3, 1091.2, 1077.3, 1063.6, 1050.1, 1036.8, 1023.6, 1010.6, 997.84, 985.2, 972.74, 960.45, 948.31, 945.4, 4967.0, 4920.6, 4856.9, 4794.1, 4732.1, 4671.0, 4637.6, 6569.7, 6534.6, 6450.3, 6367.1, 6285.0, 6204.0, 6124.0, 6045.1, 5967.2, 5890.3, 5814.4, 5739.4, 5665.5, 5592.5, 5520.4, 5449.3, 5379.1, 5309.9, 5241.5, 5174.0, 5107.4, 5041.6, 4976.7, 4912.7, 4865.0, 5546.6, 5539.0, 5478.3, 5410.7, 5344.1, 5278.2, 5213.2, 5149.0, 5085.6, 5023.0, 4961.2, 4900.3, 4840.4, 4781.3, 4722.9, 4665.2, 4608.3, 4552.1, 4496.6, 4441.9, 4387.8, 4334.5, 4281.8, 4229.7, 4178.4, 4127.6, 4077.0, 4027.1, 3977.8, 3929.2, 3881.1, 3833.4, 3786.3, 3739.8, 3693.8, 3648.4, 3603.6, 3559.4, 3515.6, 3472.5, 3429.8, 3387.7, 3346.1, 3305.0, 3264.5, 3224.4, 3184.8, 3145.8, 3107.2, 3069.0, 3031.4, 2994.2, 2957.5, 2921.2, 2885.2, 2849.4, 2814.1, 2779.2, 2744.7, 2710.7, 2677.1, 2643.9, 2611.1, 2578.7, 2546.7, 2515.1, 2483.9, 2453.1, 2422.6, 2392.5, 2362.8, 2333.5, 2304.5, 2275.9, 2247.6, 2219.7, 2192.2, 2164.9, 2138.0, 2111.5, 2085.2, 2059.3, 2033.7, 2008.5, 1983.5, 1958.9, 1934.5, 1910.5, 1886.8, 1863.3, 1840.2, 1817.3, 1794.7, 1772.4, 1750.4, 1728.7, 1707.2, 1686.0, 1665.1, 1644.4, 1624.0, 1603.8, 1583.9, 1564.3, 1544.8, 1525.7, 1506.7, 1488.0, 1469.6, 1451.4, 1433.4, 1415.5, 1397.7, 1380.0, 1362.5, 1345.2, 1328.2, 1311.3, 1294.5, 1278.0, 1261.6, 1245.5, 1229.5, 1213.8, 1198.2, 1182.9, 1167.8, 1152.8, 1138.1, 1123.5, 1109.2, 1095.0, 1081.0, 1067.2, 1053.6, 1040.2, 1026.9, 1013.3, 999.62, 986.12, 972.79, 959.47, 946.33, 933.38, 920.61, 908.02, 895.61, 883.37, 871.3, 859.4, 847.67, 836.1, 824.69, 813.45, 802.36, 791.42, 780.64, 770.01, 759.53, 749.19, 739.0, 728.95, 719.04, 709.27, 699.64, 690.14, 680.77, 671.53, 662.42, 653.44, 644.58, 635.85, 627.24, 618.75, 610.37, 602.12, 593.97, 585.94, 578.02, 570.22, 562.51, 554.92, 547.43, 540.05, 532.77, 525.58, 518.49, 511.49, 504.58, 497.77, 491.06, 484.44, 477.91, 471.47, 465.12, 458.86, 452.69, 446.6, 440.59, 434.67, 428.83, 423.07, 417.38, 411.78, 406.26, 400.81, 395.43, 390.13, 384.91, 379.75, 374.67, 369.65, 364.71, 359.83, 355.02, 350.28, 345.6, 340.98, 336.43, 331.94, 327.52, 323.15, 318.84, 314.6, 310.41, 306.27, 302.2, 298.18, 294.21, 290.3, 286.45, 282.64, 278.89, 275.19, 271.54, 267.94, 264.39, 260.88, 257.43, 254.02, 250.66, 247.34, 244.07, 240.84, 237.66, 234.52, 231.43, 228.37, 225.36, 222.39, 219.46, 216.56, 213.71, 210.9, 208.12, 205.38, 202.68, 200.02, 197.39, 194.8, 192.24, 189.72, 187.23, 184.77, 182.35, 179.96, 177.6, 175.23, 172.88, 170.56, 168.27, 166.02, 163.78, 161.56, 159.37, 157.21, 155.08, 152.98, 150.9, 148.86, 146.85, 144.87, 142.91, 140.98, 139.08, 137.2, 135.36, 133.53, 131.74, 129.96, 128.22, 126.49, 124.8, 123.12, 121.47, 106.01, 88.123, 73.327, 61.073, 50.733, 41.642, 34.214, 28.137, 24.125, 23.162, 23.082, 22.814, 179.33, 171.27, 151.5, 126.52, 106.54, 89.994, 75.367, 62.988, 52.558, 43.81, 36.504, 30.42, 25.315, 21.035, 17.391, 14.354, 11.849, 9.7755, 8.0645, 6.6535, 5.4897, 4.5297, 3.7379, 3.0846, 2.5315, 2.0767, 1.7038, 1.3978, 1.1469, 0.94102, 0.77126, 0.63052, 0.51548, 0.42145, 0.34459, 0.28175, 0.23039, 0.18839, 0.15405, 0.12598, 0.10303, 0.08426, 0.068913, 0.056363, 0.0461, 0.037707, 0.030844, 0.02523, 0.020639, 0.016883, 0.013812, 0.0113, 0.0092445, 0.0075634, 0.0061882, 0.0050632, 0.0], - 'energies (keV)': [0.041406, 0.042024, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.13769, 0.1397975, 0.1403595, 0.1412025, 0.14331, 0.143472, 0.1442475, 0.145668, 0.1462536, 0.147132, 0.149328, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.19943, 0.2013709, 0.2024825, 0.2032965, 0.2045175, 0.20757, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9, 0.9045, 0.9090225, 0.91356761, 0.91813545, 0.92272613, 0.92733976, 0.93197646, 0.93663634, 0.94131952, 0.94602612, 0.95075625, 0.95551003, 0.96028758, 0.96508902, 0.96991446, 0.97476404, 0.97963786, 0.98453605, 0.98945873, 0.99440602, 0.99937805, 1.0043749, 1.0093968, 1.0144438, 1.019516, 1.0246136, 1.0297367, 1.0348853, 1.0400598, 1.0452601, 1.0504864, 1.0557388, 1.0610175, 1.0663226, 1.0716542, 1.0770125, 1.0823975, 1.0878095, 1.0932486, 1.0987148, 1.1042084, 1.1097294, 1.1152781, 1.1208545, 1.1264587, 1.132091, 1.1377515, 1.1434402, 1.1491574, 1.1549032, 1.1606777, 1.1664811, 1.1723135, 1.1781751, 1.184066, 1.1899863, 1.1959362, 1.2019159, 1.2079255, 1.2139651, 1.220035, 1.2261351, 1.2322658, 1.2384271, 1.2446193, 1.2508424, 1.2570966, 1.2633821, 1.269699, 1.2760475, 1.2824277, 1.2888399, 1.295284, 1.3017605, 1.3082693, 1.3148106, 1.3213847, 1.3229793, 1.3232206, 1.3279916, 1.3346316, 1.3413047, 1.3480112, 1.3547513, 1.3584739, 1.3587261, 1.361525, 1.3683327, 1.3751743, 1.3820502, 1.3889605, 1.3959053, 1.4028848, 1.4098992, 1.4169487, 1.4240335, 1.4311536, 1.4383094, 1.4455009, 1.4527284, 1.4599921, 1.467292, 1.4746285, 1.4820016, 1.4894117, 1.4968587, 1.504343, 1.5118647, 1.519424, 1.5251383, 1.5270212, 1.5278616, 1.5346563, 1.5423295, 1.5500412, 1.5577914, 1.5655804, 1.5734083, 1.5812753, 1.5891817, 1.5971276, 1.6051132, 1.6131388, 1.6212045, 1.6293105, 1.6374571, 1.6456443, 1.6538726, 1.6621419, 1.6704526, 1.6788049, 1.6871989, 1.6956349, 1.7041131, 1.7126337, 1.7211968, 1.7298028, 1.7384518, 1.7471441, 1.7558798, 1.7646592, 1.7734825, 1.7823499, 1.7912617, 1.800218, 1.8092191, 1.8182652, 1.8273565, 1.8364933, 1.8456757, 1.8549041, 1.8641786, 1.8734995, 1.882867, 1.8922814, 1.9017428, 1.9112515, 1.9208077, 1.9304118, 1.9400638, 1.9497642, 1.959513, 1.9693105, 1.9791571, 1.9890529, 1.9989981, 2.0089931, 2.0190381, 2.0291333, 2.039279, 2.0494754, 2.0597227, 2.0700213, 2.0803714, 2.0907733, 2.1012272, 2.1117333, 2.122292, 2.1329034, 2.143568, 2.1542858, 2.1650572, 2.1758825, 2.1867619, 2.1976957, 2.2086842, 2.2197276, 2.2308263, 2.2419804, 2.2531903, 2.2644562, 2.2757785, 2.2871574, 2.2985932, 2.3100862, 2.3216366, 2.3332448, 2.344911, 2.3566356, 2.3684187, 2.3802608, 2.3921621, 2.404123, 2.4161436, 2.4282243, 2.4403654, 2.4525672, 2.4648301, 2.4771542, 2.48954, 2.5019877, 2.5144976, 2.5270701, 2.5397055, 2.552404, 2.565166, 2.5779919, 2.5908818, 2.6038362, 2.6168554, 2.6299397, 2.6430894, 2.6563048, 2.6695863, 2.6829343, 2.6963489, 2.7098307, 2.7233798, 2.7369967, 2.7506817, 2.7644351, 2.7782573, 2.7921486, 2.8061093, 2.8201399, 2.8342406, 2.8484118, 2.8626539, 2.8769671, 2.891352, 2.9058087, 2.9203378, 2.9349394, 2.9496141, 2.9643622, 2.979184, 2.9940799, 3.0090503, 3.0240956, 3.0392161, 3.0544122, 3.0696842, 3.0850326, 3.1004578, 3.1159601, 3.1315399, 3.1471976, 3.1629336, 3.1787482, 3.194642, 3.2106152, 3.2266683, 3.2428016, 3.2590156, 3.2753107, 3.2916873, 3.3081457, 3.3246864, 3.3413099, 3.3580164, 3.3748065, 3.3916805, 3.4086389, 3.4256821, 3.4428105, 3.4600246, 3.4773247, 3.4947113, 3.5121849, 3.5297458, 3.5473945, 3.5651315, 3.5829572, 3.6008719, 3.6188763, 3.6369707, 3.6551555, 3.6734313, 3.6917985, 3.7102575, 3.7288088, 3.7474528, 3.7661901, 3.785021, 3.8039461, 3.8229659, 3.8420807, 3.8612911, 3.8805975, 3.9000005, 3.9195005, 3.939098, 3.9587935, 3.9785875, 3.9984804, 4.0184728, 4.0385652, 4.058758, 4.0790518, 4.0994471, 4.1199443, 4.140544, 4.1612467, 4.182053, 4.2029632, 4.2239781, 4.245098, 4.2663234, 4.2876551, 4.3090933, 4.3306388, 4.352292, 4.3740535, 4.3959237, 4.4179033, 4.4399929, 4.4621928, 4.4845038, 4.5069263, 4.5294609, 4.5521082, 4.5748688, 4.5977431, 4.6207318, 4.6438355, 4.6670547, 4.69039, 4.7138419, 4.7374111, 4.7610982, 4.7849037, 4.8088282, 4.8328723, 4.8570367, 4.8813219, 4.9057285, 4.9302571, 4.9549084, 4.9796829, 5.0045814, 5.0296043, 5.0547523, 5.080026, 5.1054262, 5.1309533, 5.1566081, 5.1823911, 5.2083031, 5.2343446, 5.2605163, 5.2868189, 5.313253, 5.3398192, 5.3665183, 5.3933509, 5.4203177, 5.4474193, 5.4746564, 5.5020297, 5.5295398, 5.5571875, 5.5849734, 5.6128983, 5.6409628, 5.6691676, 5.6975135, 5.726001, 5.754631, 5.7834042, 5.8123212, 5.8413828, 5.8705897, 5.8999427, 5.9294424, 5.9590896, 5.988885, 6.0188295, 6.0489236, 6.0791682, 6.1095641, 6.1401119, 6.1708125, 6.2016665, 6.2326749, 6.2638382, 6.2951574, 6.3266332, 6.3582664, 6.3900577, 6.422008, 6.454118, 6.4863886, 6.5188206, 6.5514147, 6.5841717, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.62937, 11.79334, 11.80737, 11.85483, 11.92603, 12.10403, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Ar': {'mass_absorption_coefficient (cm2/g)': [382460.0, 421040.0, 518140.0, 788460.0, 1014100.0, 1089500.0, 1015000.0, 847230.0, 651470.0, 472570.0, 329920.0, 225410.0, 177940.0, 162850.0, 159080.0, 153110.0, 141280.0, 104440.0, 72397.0, 51499.0, 37927.0, 29122.0, 23402.0, 19670.0, 17210.0, 15559.0, 14413.0, 13591.0, 12972.0, 12475.0, 12051.0, 11674.0, 11328.0, 11006.0, 10700.0, 10407.0, 10123.0, 9841.2, 9555.8, 9261.8, 8955.1, 8632.4, 8291.4, 7931.2, 7552.4, 7157.1, 6748.8, 6331.6, 5910.7, 5491.2, 5152.7, 5081.9, 5026.9, 4993.9, 50586.0, 50555.0, 50384.0, 50089.0, 71651.0, 70615.0, 69233.0, 62860.0, 53864.0, 46247.0, 41978.0, 40555.0, 40188.0, 44257.0, 44154.0, 42784.0, 38405.0, 33317.0, 28854.0, 24936.0, 21499.0, 18491.0, 15861.0, 13572.0, 11587.0, 9870.7, 8391.7, 7121.1, 6032.8, 5103.1, 4310.9, 3637.2, 3065.6, 2555.2, 2130.9, 1778.5, 1483.8, 1235.2, 1029.1, 858.28, 716.47, 598.67, 500.72, 419.2, 351.3, 294.68, 247.42, 207.95, 174.94, 145.81, 141.37, 135.35, 133.81, 1323.6, 1275.1, 1225.1, 1032.6, 868.48, 729.77, 611.83, 512.14, 428.45, 358.36, 299.68, 250.55, 209.12, 173.6, 144.12, 119.66, 99.352, 82.371, 67.929, 56.022, 46.204, 38.109, 31.432, 25.927, 21.307, 17.461, 14.309, 11.726, 9.6104, 7.8765, 6.4556, 5.2912, 4.3369, 3.5549, 2.8963, 2.355, 1.915, 1.5572, 1.2663, 1.0297, 0.8374, 0.68099, 0.5538, 0.45038, 0.36627, 0.29787, 0.24225, 0.19702, 0.1597, 0.12925, 0.10459, 0.084595, 0.068425, 0.055347, 0.044768, 0.036212, 0.029291, 0.023693, 0.019165, 0.015503, 0.01254, 0.010144, 0.0082055, 0.0066376, 0.0053693, 0.0043433, 0.0035134, 0.0028421, 0.0022991, 0.0018598, 0.0015045, 0.0012171, 0.00098455, 0.00079646, 0.0006443, 0.00052121, 0.0], - 'energies (keV)': [0.012462, 0.012648, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.024794, 0.0251735, 0.0252747, 0.02545001, 0.025806, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.240296, 0.242354, 0.243974, 0.2449548, 0.245997, 0.2460635, 0.246426, 0.2470527, 0.2485365, 0.250104, 0.252246, 0.2629708, 0.2811158, 0.3005128, 0.3136, 0.3184, 0.31968, 0.3212482, 0.3216, 0.3264, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.138842, 3.186885, 3.199697, 3.218914, 3.266958, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Au': {'mass_absorption_coefficient (cm2/g)': [42705.0, 41481.0, 32469.0, 32352.0, 32341.0, 57837.0, 56718.0, 59861.0, 67392.0, 77054.0, 88504.0, 101290.0, 114780.0, 128180.0, 140530.0, 150790.0, 158010.0, 161410.0, 160580.0, 155490.0, 146550.0, 134520.0, 120340.0, 105070.0, 89725.0, 75109.0, 61791.0, 50114.0, 40190.0, 31965.0, 25284.0, 20477.0, 19938.0, 19396.0, 19120.0, 34412.0, 32653.0, 28930.0, 22850.0, 18036.0, 14260.0, 13546.0, 12851.0, 12673.0, 17817.0, 17073.0, 16498.0, 13697.0, 12769.0, 12228.0, 12088.0, 13428.0, 12854.0, 12797.0, 12757.0, 12191.0, 12044.0, 12810.0, 12253.0, 11335.0, 9286.4, 7773.8, 7375.5, 7129.4, 7068.0, 7166.9, 6959.8, 6900.0, 6215.1, 5861.8, 5834.1, 6093.0, 6623.8, 7423.2, 8456.3, 9678.9, 11029.0, 12425.0, 13774.0, 14979.0, 15954.0, 16631.0, 16972.0, 16964.0, 16902.0, 16833.0, 16812.0, 17137.0, 17061.0, 17011.0, 16983.0, 16876.0, 16845.0, 17051.0, 16934.0, 16734.0, 15983.0, 15052.0, 14006.0, 12905.0, 12203.0, 12120.0, 12037.0, 11954.0, 11871.0, 11788.0, 11706.0, 11624.0, 11542.0, 11460.0, 11378.0, 11296.0, 11215.0, 11134.0, 11053.0, 10972.0, 10892.0, 10811.0, 10798.0, 11427.0, 11401.0, 11319.0, 11237.0, 11156.0, 11074.0, 10994.0, 10913.0, 10833.0, 10753.0, 10673.0, 10594.0, 10515.0, 10437.0, 10359.0, 10281.0, 10203.0, 10126.0, 10049.0, 9973.1, 9897.2, 9821.6, 9746.3, 9671.5, 9597.0, 9522.9, 9449.3, 9376.0, 9303.0, 9230.5, 9158.4, 9086.7, 9015.4, 8944.4, 8918.6, 9005.2, 9000.6, 8930.5, 8860.7, 8791.4, 8722.5, 8653.9, 8585.8, 8518.1, 8450.8, 8383.9, 8317.4, 8251.4, 8185.7, 8120.5, 8055.6, 7991.2, 7927.2, 7863.6, 7800.4, 7737.6, 7675.3, 7613.3, 7551.7, 7490.6, 7429.8, 7369.5, 7309.5, 7250.0, 7190.8, 7132.1, 7073.8, 7015.8, 6958.3, 6940.2, 7054.5, 7051.8, 6994.5, 6937.7, 6881.2, 6825.1, 6769.5, 6714.2, 6659.3, 6604.8, 6550.6, 6496.9, 6443.5, 6390.6, 6338.0, 6285.7, 6233.9, 6182.4, 6131.3, 6080.6, 6030.3, 5980.3, 5930.6, 5881.4, 5832.5, 5783.9, 5735.7, 5687.8, 5640.3, 5593.2, 5546.4, 5499.9, 5453.8, 5408.0, 5362.6, 5317.5, 5272.7, 5228.3, 5184.2, 5140.5, 5097.0, 5053.8, 5010.8, 4968.2, 4925.9, 4883.9, 4842.3, 4801.0, 4759.9, 4719.3, 4678.9, 4638.8, 4599.1, 4559.6, 4520.5, 4481.7, 4443.0, 4399.0, 4355.3, 4312.0, 4269.1, 4226.7, 4184.6, 4143.0, 4101.8, 4061.1, 4020.7, 3980.7, 3941.1, 3901.9, 3863.1, 3824.7, 3786.7, 3749.1, 3711.6, 3674.5, 3637.8, 3601.5, 3565.5, 3529.9, 3494.7, 3459.8, 3425.2, 3391.0, 3357.2, 3323.7, 3290.5, 3257.7, 3225.2, 3193.1, 3161.2, 3129.7, 3098.5, 3067.7, 3037.1, 3006.9, 2977.0, 2947.4, 2918.1, 2889.0, 2860.3, 2831.9, 2803.8, 2776.0, 2748.5, 2721.2, 2694.2, 2667.5, 2640.9, 2614.7, 2588.8, 2563.1, 2537.7, 2512.5, 2487.7, 2461.6, 2435.9, 2410.4, 2385.2, 2360.3, 2335.6, 2311.3, 2287.2, 2263.3, 2239.8, 2216.1, 2192.2, 2168.6, 2145.3, 2122.2, 2099.4, 2076.9, 2054.6, 2032.5, 2010.8, 1989.2, 1968.0, 1946.9, 1926.1, 1905.6, 1885.2, 1865.1, 1845.3, 1825.6, 1806.2, 1786.6, 1766.9, 1747.4, 1728.1, 1709.1, 1690.3, 1671.7, 1653.4, 1635.2, 1616.8, 1598.6, 1580.6, 1562.9, 1545.4, 1528.0, 1511.0, 1494.1, 1477.4, 1460.9, 1444.7, 1428.6, 1412.8, 1397.1, 1381.7, 1366.4, 1351.3, 1336.4, 1321.7, 1307.2, 1292.8, 1278.6, 1264.6, 1250.8, 1237.1, 1223.6, 1210.3, 1197.1, 1184.1, 1171.2, 1158.5, 1146.0, 1133.6, 1121.3, 1109.3, 1097.3, 1085.5, 1073.8, 1062.3, 1050.9, 1039.7, 1028.6, 1017.6, 1006.7, 996.0, 985.39, 974.92, 964.57, 954.34, 944.23, 934.25, 924.38, 914.62, 904.98, 895.46, 886.05, 876.74, 867.55, 858.47, 849.37, 840.32, 835.59, 2503.0, 2491.8, 2461.6, 2431.7, 2402.2, 2373.1, 2344.4, 2316.0, 2287.9, 2284.0, 3345.4, 3314.7, 3274.0, 3233.8, 3194.1, 3154.9, 3116.2, 3078.0, 3040.3, 3003.1, 2966.3, 2930.0, 2894.1, 2858.7, 2823.7, 2789.2, 2755.1, 2721.4, 2688.1, 2655.3, 2622.9, 2590.8, 2559.2, 2528.0, 2497.1, 2466.6, 2436.5, 2406.7, 2377.3, 2348.3, 2319.6, 2291.3, 2263.3, 2235.7, 2208.5, 2181.5, 2155.0, 2152.2, 2490.4, 2475.3, 2446.8, 2418.7, 2390.8, 2363.3, 2336.2, 2309.3, 2282.8, 2256.6, 2230.2, 2204.1, 2178.3, 2152.7, 2127.5, 2102.6, 2078.0, 2053.6, 2029.2, 2003.8, 1978.3, 1953.1, 1928.2, 1903.6, 1879.3, 1855.3, 1831.6, 1808.3, 1794.5, 1903.2, 1898.8, 1879.0, 1855.1, 1831.5, 1808.3, 1785.3, 1762.6, 1740.2, 1718.0, 1696.2, 1674.8, 1653.8, 1633.2, 1612.8, 1592.7, 1572.9, 1553.4, 1545.9, 1601.8, 1597.4, 1582.3, 1563.2, 1544.2, 1525.6, 1507.1, 1488.9, 1471.0, 1453.3, 1435.6, 1418.1, 1400.9, 1383.9, 1367.1, 1350.6, 1334.2, 1318.1, 1302.2, 1286.4, 1270.9, 1255.6, 1240.5, 1225.6, 1210.8, 1196.3, 1181.9, 1167.8, 1153.8, 1140.0, 1126.3, 1112.9, 1099.6, 1086.4, 1073.5, 1060.7, 1048.1, 1035.5, 1023.0, 1010.7, 998.57, 986.57, 974.71, 963.01, 951.45, 940.03, 928.75, 917.61, 906.62, 895.76, 885.03, 874.43, 863.8, 853.09, 842.49, 832.03, 821.7, 811.5, 801.44, 791.5, 781.5, 771.6, 761.83, 752.18, 742.67, 733.28, 724.02, 714.89, 705.87, 696.98, 688.2, 679.54, 671.0, 662.57, 654.25, 646.04, 637.94, 629.95, 622.07, 614.29, 606.61, 599.03, 591.56, 584.18, 576.9, 569.72, 562.64, 555.64, 548.74, 541.93, 535.22, 528.58, 522.04, 515.59, 509.21, 502.93, 496.61, 490.35, 484.16, 478.06, 472.04, 466.1, 460.24, 454.45, 448.75, 443.12, 437.56, 432.08, 426.67, 421.33, 416.06, 410.87, 405.74, 400.68, 395.69, 390.76, 385.9, 381.1, 376.37, 371.7, 367.09, 362.54, 358.05, 353.59, 349.14, 344.76, 340.43, 336.16, 331.94, 327.78, 323.68, 319.63, 315.63, 311.68, 307.77, 303.92, 300.11, 296.36, 292.65, 288.99, 285.38, 281.81, 278.28, 274.79, 271.36, 267.96, 264.62, 261.31, 258.06, 254.84, 251.67, 248.53, 245.44, 242.39, 239.38, 236.41, 233.48, 230.59, 227.73, 224.91, 222.13, 219.39, 216.68, 214.0, 211.36, 208.76, 206.19, 203.65, 201.15, 198.68, 196.24, 193.83, 191.45, 189.11, 186.79, 184.51, 182.25, 180.02, 177.83, 175.66, 173.52, 171.4, 169.31, 167.25, 165.22, 143.66, 120.3, 100.52, 84.034, 72.211, 70.399, 69.374, 68.636, 179.11, 171.88, 155.38, 129.77, 129.3, 124.47, 123.1, 169.97, 163.25, 161.45, 154.91, 153.23, 175.29, 174.74, 168.27, 147.84, 124.38, 104.63, 87.922, 73.837, 62.033, 52.143, 43.824, 36.825, 30.736, 25.548, 21.235, 17.664, 14.705, 12.252, 10.216, 8.5253, 7.12, 5.9502, 4.966, 4.1473, 3.4596, 2.8765, 2.3924, 1.9904, 1.8073, 1.7326, 1.7127, 8.2842, 8.1479, 7.9759, 6.8623, 5.7711, 4.8446, 4.0647, 3.4096, 2.8537, 2.3879, 1.998, 1.6718, 1.399, 1.1705, 0.97838, 0.81787, 0.68376, 0.5717, 0.47805, 0.39978, 0.33435, 0.27966, 0.23393, 0.1957, 0.16372, 0.13699, 0.11462, 0.095918, 0.0], - 'energies (keV)': [0.006824268, 0.006926123, 0.008142217, 0.008266843, 0.008300076, 0.008349926, 0.008474552, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.052626, 0.05302035, 0.0534315, 0.0536463, 0.0539685, 0.054774, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.070266, 0.0713415, 0.0716283, 0.0720585, 0.073134, 0.07401695, 0.07912411, 0.081144, 0.082386, 0.0827172, 0.083214, 0.084456, 0.08458368, 0.084672, 0.085968, 0.0863136, 0.086832, 0.088128, 0.09041995, 0.09665893, 0.1033284, 0.105644, 0.107261, 0.1076922, 0.108339, 0.109956, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.327222, 0.3322305, 0.3335661, 0.3355695, 0.340578, 0.3434143, 0.34496, 0.35024, 0.351648, 0.35376, 0.35904, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4794098, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.5334931, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54469646, 0.54610359, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64278595, 0.64461406, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.75759354, 0.76000652, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.8872931, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0408331, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2051132, 2.2062866, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.2904585, 2.2917415, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7404353, 2.7455647, 2.7527269, 2.7664905, 2.780323, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1431727, 3.1495376, 3.1524272, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.417742, 3.4282291, 3.4320581, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3122972, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.68033, 11.79334, 11.85911, 11.90678, 11.97829, 12.15707, 12.60708, 13.45893, 13.47697, 13.66493, 13.71987, 13.80227, 14.00827, 14.06574, 14.28104, 14.33845, 14.40688, 14.42456, 14.63986, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 79.1104, 80.32127, 80.64417, 81.12852, 81.6564, 82.3394, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'At': {'mass_absorption_coefficient (cm2/g)': [181600.0, 176050.0, 92600.0, 90020.0, 89349.0, 180270.0, 176140.0, 138860.0, 123960.0, 109980.0, 96900.0, 84748.0, 73553.0, 63344.0, 54135.0, 45921.0, 41591.0, 39984.0, 39568.0, 46307.0, 45929.0, 44278.0, 37503.0, 30568.0, 24895.0, 20251.0, 16456.0, 13359.0, 10839.0, 8792.2, 7134.0, 6333.5, 6040.9, 5966.0, 8879.9, 8979.1, 9378.2, 13473.0, 13960.0, 16157.0, 16835.0, 19804.0, 23207.0, 29438.0, 54527.0, 84849.0, 110030.0, 121800.0, 118240.0, 103300.0, 83163.0, 63030.0, 45819.0, 32464.0, 22725.0, 15892.0, 11201.0, 9713.8, 9001.0, 8823.0, 11443.0, 10757.0, 10697.0, 8017.1, 6103.8, 4775.4, 4679.7, 4444.4, 4385.3, 5885.5, 5595.8, 5220.3, 4232.6, 3498.2, 2949.0, 2737.5, 2645.8, 2622.5, 2747.1, 2740.5, 2693.4, 2666.4, 2658.2, 2637.3, 2807.7, 2784.8, 2769.1, 2751.0, 2743.9, 2876.3, 2885.9, 2888.1, 3165.9, 3857.6, 4954.1, 6394.9, 8047.6, 9735.9, 11281.0, 12536.0, 13410.0, 13869.0, 13928.0, 13637.0, 13340.0, 13199.0, 13160.0, 13900.0, 13868.0, 13760.0, 13441.0, 13385.0, 13329.0, 13273.0, 13215.0, 13157.0, 13098.0, 13038.0, 12977.0, 12916.0, 12855.0, 12792.0, 12729.0, 12681.0, 13101.0, 13099.0, 13039.0, 12978.0, 12915.0, 12852.0, 12789.0, 12725.0, 12660.0, 12595.0, 12530.0, 12464.0, 12398.0, 12331.0, 12264.0, 12197.0, 12129.0, 12061.0, 11993.0, 11925.0, 11856.0, 11787.0, 11718.0, 11649.0, 11579.0, 11509.0, 11439.0, 11369.0, 11298.0, 11227.0, 11157.0, 11086.0, 11015.0, 10943.0, 10872.0, 10801.0, 10729.0, 10658.0, 10587.0, 10515.0, 10444.0, 10372.0, 10301.0, 10229.0, 10158.0, 10087.0, 10015.0, 9944.0, 9872.9, 9801.9, 9731.1, 9660.4, 9589.9, 9519.6, 9449.4, 9379.5, 9309.7, 9240.2, 9170.9, 9101.8, 9032.9, 8964.3, 8896.0, 8828.0, 8760.2, 8692.7, 8625.5, 8604.7, 9103.6, 9096.6, 9027.9, 8959.4, 8891.3, 8823.6, 8756.2, 8689.1, 8622.3, 8555.9, 8489.8, 8423.9, 8358.5, 8293.4, 8228.6, 8164.2, 8100.2, 8036.6, 7971.2, 7905.9, 7841.0, 7776.4, 7712.2, 7648.3, 7584.8, 7521.7, 7459.0, 7396.6, 7334.7, 7273.1, 7212.0, 7151.2, 7090.9, 7031.0, 6971.6, 6912.5, 6853.9, 6831.4, 6888.9, 6887.3, 6831.1, 6773.8, 6716.9, 6659.4, 6602.1, 6545.2, 6488.8, 6432.9, 6377.3, 6322.2, 6267.6, 6213.3, 6159.6, 6106.3, 6053.4, 6001.0, 5949.1, 5897.6, 5846.5, 5796.0, 5745.9, 5696.2, 5647.0, 5597.9, 5535.4, 5473.7, 5412.7, 5352.5, 5293.0, 5234.2, 5176.2, 5120.8, 5118.8, 5196.5, 5167.1, 5110.7, 5055.0, 4999.9, 4945.6, 4891.8, 4838.8, 4786.4, 4734.6, 4683.4, 4632.8, 4582.9, 4533.5, 4484.7, 4436.5, 4388.9, 4341.9, 4295.4, 4249.5, 4204.1, 4159.3, 4115.0, 4071.2, 4028.0, 3985.2, 3943.0, 3901.3, 3860.0, 3819.3, 3779.0, 3739.3, 3699.9, 3661.1, 3622.7, 3584.8, 3547.3, 3510.1, 3473.1, 3436.6, 3400.4, 3364.7, 3329.4, 3294.5, 3260.1, 3226.0, 3192.3, 3159.0, 3126.1, 3093.6, 3061.5, 3029.7, 2998.3, 2967.2, 2936.5, 2906.1, 2876.0, 2846.3, 2817.0, 2787.9, 2759.2, 2730.9, 2702.8, 2675.1, 2647.7, 2620.6, 2593.8, 2567.3, 2541.2, 2515.3, 2489.7, 2464.4, 2439.4, 2414.7, 2390.1, 2365.8, 2341.7, 2317.9, 2294.4, 2271.2, 2248.2, 2225.4, 2203.0, 2180.7, 2158.8, 2137.0, 2115.6, 2094.3, 2073.3, 2052.5, 2032.0, 2011.7, 1991.6, 1971.7, 1952.0, 1932.6, 1913.4, 1894.4, 1875.6, 1857.0, 1838.6, 1820.5, 1802.5, 1784.7, 1767.1, 1749.7, 1732.5, 1715.5, 1698.7, 1682.1, 1665.6, 1649.3, 1633.2, 1617.3, 1601.5, 1586.0, 1570.6, 1555.3, 1540.2, 1525.3, 1510.6, 1495.8, 1480.4, 1465.2, 1450.2, 1435.3, 1420.6, 1406.0, 1391.7, 1377.4, 1363.4, 1349.5, 1335.7, 1322.1, 1308.7, 1295.4, 1282.2, 1269.2, 1256.3, 1243.6, 1230.9, 1218.4, 1206.1, 1193.8, 1181.7, 1169.1, 1156.8, 1144.5, 1132.4, 1120.4, 1108.6, 1096.9, 1085.4, 1073.9, 1062.6, 1051.5, 1040.4, 1029.5, 1018.7, 1008.0, 997.46, 987.02, 976.7, 966.49, 956.4, 946.43, 936.56, 926.81, 917.16, 907.62, 898.19, 888.87, 879.65, 870.53, 861.51, 852.59, 843.78, 835.06, 826.43, 817.91, 809.48, 801.14, 792.89, 784.74, 776.67, 768.68, 760.78, 752.97, 745.24, 737.6, 730.04, 722.57, 715.18, 707.87, 700.64, 693.49, 686.42, 679.42, 676.63, 1869.1, 1857.4, 1833.5, 1809.9, 1786.6, 1763.7, 1741.0, 1718.7, 1696.6, 1674.8, 2455.5, 2424.5, 2390.0, 2356.1, 2322.7, 2289.7, 2257.2, 2224.4, 2191.8, 2159.8, 2128.7, 2098.9, 2069.7, 2041.0, 2012.8, 1985.1, 1958.0, 1931.3, 1905.0, 1879.2, 1853.9, 1828.9, 1804.4, 1780.3, 1756.6, 1733.2, 1710.3, 1687.6, 1665.4, 1643.4, 1621.8, 1600.6, 1579.6, 1566.4, 1840.1, 1837.6, 1815.5, 1791.3, 1767.4, 1743.8, 1720.6, 1697.7, 1675.1, 1652.8, 1630.8, 1609.6, 1589.1, 1568.5, 1548.3, 1528.4, 1508.8, 1489.5, 1470.5, 1451.7, 1433.3, 1415.0, 1396.7, 1378.8, 1361.0, 1343.6, 1326.4, 1309.4, 1292.7, 1276.2, 1260.0, 1244.0, 1228.2, 1227.4, 1306.7, 1300.0, 1283.0, 1266.3, 1249.8, 1233.5, 1217.5, 1201.6, 1186.0, 1170.6, 1155.5, 1141.0, 1126.7, 1112.7, 1098.9, 1087.9, 1085.3, 1124.7, 1119.7, 1106.1, 1092.8, 1079.7, 1066.7, 1053.9, 1041.2, 1028.5, 1016.0, 1003.6, 991.39, 979.35, 967.46, 955.73, 944.16, 932.74, 921.47, 910.34, 899.36, 888.53, 877.83, 867.27, 856.84, 846.55, 836.4, 826.37, 816.46, 806.69, 797.04, 787.51, 778.09, 768.8, 759.63, 750.56, 741.61, 732.78, 724.05, 715.42, 706.76, 698.2, 689.75, 681.4, 673.15, 665.01, 656.96, 649.02, 641.17, 633.41, 625.75, 618.19, 610.72, 603.23, 595.75, 588.34, 581.04, 573.82, 566.7, 559.67, 552.72, 545.87, 539.01, 532.23, 525.54, 518.94, 512.42, 505.99, 499.65, 493.38, 487.2, 481.1, 475.08, 469.14, 463.27, 457.49, 451.78, 446.14, 440.58, 435.09, 429.67, 424.32, 419.05, 413.84, 408.7, 403.63, 398.63, 393.69, 388.82, 384.01, 379.26, 374.58, 369.96, 365.4, 360.89, 356.39, 351.95, 347.57, 343.24, 338.98, 334.77, 330.61, 326.51, 322.47, 318.47, 314.53, 310.64, 306.81, 303.02, 299.28, 295.6, 291.96, 288.37, 284.82, 281.32, 277.87, 274.46, 271.1, 267.78, 264.51, 261.28, 258.09, 254.94, 251.84, 248.77, 245.75, 242.74, 239.77, 236.84, 233.95, 231.09, 228.27, 225.49, 222.74, 220.03, 217.35, 214.71, 212.11, 209.53, 182.11, 152.29, 127.53, 106.94, 89.828, 75.433, 63.285, 58.063, 55.806, 55.225, 139.04, 135.93, 133.68, 113.61, 94.68, 91.045, 90.05, 124.91, 120.09, 119.67, 114.94, 113.72, 129.59, 129.24, 124.84, 109.1, 91.848, 77.321, 65.082, 54.783, 46.142, 38.643, 32.254, 26.936, 22.502, 18.745, 15.612, 13.013, 10.855, 9.0616, 7.5702, 6.3289, 5.2952, 4.431, 3.7042, 3.0994, 2.5915, 2.1559, 1.7842, 1.4777, 1.4555, 1.3946, 1.3789, 6.3594, 6.1242, 5.7996, 4.8876, 4.1131, 3.4579, 2.9062, 2.4405, 2.0473, 1.7173, 1.4406, 1.2086, 1.014, 0.85087, 0.71405, 0.59929, 0.50303, 0.42228, 0.35453, 0.29768, 0.24997, 0.20993, 0.17632, 0.1481, 0.12441, 0.0], - 'energies (keV)': [0.006275726, 0.006369394, 0.008850419, 0.008985884, 0.009022009, 0.009076195, 0.00921166, 0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.0189522, 0.01924229, 0.01931964, 0.01943568, 0.01948844, 0.01972576, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03690857, 0.0374735, 0.03762415, 0.03785012, 0.03797993, 0.03841505, 0.04060054, 0.04076232, 0.04138623, 0.04155261, 0.04180217, 0.04242608, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1062575, 0.1078839, 0.1083176, 0.1089681, 0.1104581, 0.1105945, 0.1180797, 0.1262272, 0.1349368, 0.1357294, 0.1378069, 0.1383609, 0.1391919, 0.1412694, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1819046, 0.1846888, 0.1854313, 0.186545, 0.1867657, 0.1883732, 0.1893292, 0.1896243, 0.1903866, 0.1915301, 0.1931341, 0.1943887, 0.1960902, 0.1968785, 0.198061, 0.2010171, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.4484657, 0.4658769, 0.4730077, 0.4749092, 0.4777615, 0.4794098, 0.4848923, 0.5, 0.5025, 0.5050125, 0.50753756, 0.51007525, 0.51262563, 0.51518875, 0.5177647, 0.52035352, 0.52295529, 0.52557007, 0.52819792, 0.53083891, 0.53281612, 0.5334931, 0.53358393, 0.53616057, 0.53884137, 0.54153558, 0.54424325, 0.54696447, 0.54969929, 0.55244779, 0.55521003, 0.55798608, 0.56077601, 0.56357989, 0.56639779, 0.56922978, 0.57207593, 0.5749363, 0.57781099, 0.58070004, 0.58360354, 0.58652156, 0.58945417, 0.59240144, 0.59536345, 0.59834026, 0.60133196, 0.60433862, 0.60736032, 0.61039712, 0.6134491, 0.61651635, 0.61959893, 0.62269693, 0.62581041, 0.62893946, 0.63208416, 0.63524458, 0.6384208, 0.64161291, 0.64482097, 0.64804508, 0.6512853, 0.65454173, 0.65781444, 0.66110351, 0.66440903, 0.66773107, 0.67106973, 0.67442508, 0.6777972, 0.68118619, 0.68459212, 0.68801508, 0.69145515, 0.69491243, 0.69838699, 0.70187893, 0.70538832, 0.70891526, 0.71245984, 0.71602214, 0.71960225, 0.72320026, 0.72681626, 0.73045034, 0.7341026, 0.73777311, 0.73891221, 0.74108781, 0.74146197, 0.74516928, 0.74889513, 0.75263961, 0.7564028, 0.76018482, 0.76398574, 0.76780567, 0.7716447, 0.77550292, 0.77938044, 0.78327734, 0.78719373, 0.79112969, 0.79508534, 0.79906077, 0.80305607, 0.80707135, 0.81110671, 0.81516224, 0.81923806, 0.82333425, 0.82745092, 0.83158817, 0.83574611, 0.83992484, 0.84412447, 0.84834509, 0.85258682, 0.85684975, 0.861134, 0.86543967, 0.86976687, 0.8741157, 0.87848628, 0.88287871, 0.88458238, 0.8872931, 0.88741758, 0.89172957, 0.89618822, 0.90066916, 0.9051725, 0.90969837, 0.91424686, 0.91881809, 0.92341218, 0.92802924, 0.93266939, 0.93733274, 0.9420194, 0.9467295, 0.95146315, 0.95622046, 0.96100156, 0.96580657, 0.9706356, 0.97548878, 0.98036623, 0.98526806, 0.9901944, 0.99514537, 1.0001211, 1.0051217, 1.0101473, 1.015198, 1.020274, 1.0253754, 1.0305023, 1.0356548, 1.0406559, 1.0408331, 1.0433442, 1.0460372, 1.0512674, 1.0565238, 1.0618064, 1.0671154, 1.072451, 1.0778132, 1.0832023, 1.0886183, 1.0940614, 1.0995317, 1.1050294, 1.1105545, 1.1161073, 1.1216878, 1.1272963, 1.1329328, 1.1385974, 1.1442904, 1.1500119, 1.1557619, 1.1615407, 1.1673484, 1.1731852, 1.1790511, 1.1849464, 1.1908711, 1.1968254, 1.2028096, 1.2088236, 1.2148677, 1.2209421, 1.2270468, 1.233182, 1.2393479, 1.2455447, 1.2517724, 1.2580312, 1.2643214, 1.270643, 1.2769962, 1.2833812, 1.2897981, 1.2962471, 1.3027283, 1.309242, 1.3157882, 1.3223671, 1.328979, 1.3356239, 1.342302, 1.3490135, 1.3557586, 1.3625374, 1.36935, 1.3761968, 1.3830778, 1.3899932, 1.3969431, 1.4039278, 1.4109475, 1.4180022, 1.4250922, 1.4322177, 1.4393788, 1.4465757, 1.4538086, 1.4610776, 1.468383, 1.4757249, 1.4831035, 1.490519, 1.4979716, 1.5054615, 1.5129888, 1.5205537, 1.5281565, 1.5357973, 1.5434763, 1.5511937, 1.5589496, 1.5667444, 1.5745781, 1.582451, 1.5903633, 1.5983151, 1.6063066, 1.6143382, 1.6224099, 1.6305219, 1.6386745, 1.6468679, 1.6551022, 1.6633777, 1.6716946, 1.6800531, 1.6884534, 1.6968956, 1.7053801, 1.713907, 1.7224766, 1.7310889, 1.7397444, 1.7484431, 1.7571853, 1.7659712, 1.7748011, 1.7836751, 1.7925935, 1.8015565, 1.8105642, 1.8196171, 1.8287151, 1.8378587, 1.847048, 1.8562833, 1.8655647, 1.8748925, 1.884267, 1.8936883, 1.9031567, 1.9126725, 1.9222359, 1.9318471, 1.9415063, 1.9512138, 1.9609699, 1.9707747, 1.9806286, 1.9905318, 2.0004844, 2.0104868, 2.0205393, 2.030642, 2.0407952, 2.0509992, 2.0612542, 2.0715604, 2.0819182, 2.0923278, 2.1027895, 2.1133034, 2.1238699, 2.1344893, 2.1451617, 2.1558875, 2.166667, 2.1775003, 2.1883878, 2.1993297, 2.2103264, 2.221378, 2.2324849, 2.2436473, 2.2548656, 2.2661399, 2.2774706, 2.2888579, 2.3003022, 2.3118037, 2.3233628, 2.3349796, 2.3466545, 2.3583878, 2.3701797, 2.3820306, 2.3939407, 2.4059104, 2.41794, 2.4300297, 2.4421798, 2.4543907, 2.4666627, 2.478996, 2.491391, 2.5038479, 2.5163672, 2.528949, 2.5415938, 2.5543017, 2.5670732, 2.5799086, 2.5928082, 2.6057722, 2.6188011, 2.6318951, 2.6450545, 2.6582798, 2.6715712, 2.6849291, 2.6983537, 2.7118455, 2.7254047, 2.7390317, 2.7527269, 2.7664905, 2.780323, 2.7859225, 2.7874775, 2.7942246, 2.8081957, 2.8222367, 2.8363479, 2.8505296, 2.8647823, 2.8791062, 2.8935017, 2.9079692, 2.9095697, 2.9225091, 2.9371216, 2.9518072, 2.9665662, 2.9813991, 2.9963061, 3.0112876, 3.026344, 3.0414758, 3.0566831, 3.0719666, 3.0873264, 3.102763, 3.1182768, 3.1338682, 3.1495376, 3.1652853, 3.1811117, 3.1970172, 3.2130023, 3.2290673, 3.2452127, 3.2614387, 3.2777459, 3.2941347, 3.3106053, 3.3271584, 3.3437941, 3.3605131, 3.3773157, 3.3942023, 3.4111733, 3.4220602, 3.4282291, 3.42994, 3.4453703, 3.4625971, 3.4799101, 3.4973097, 3.5147962, 3.5323702, 3.5500321, 3.5677822, 3.5856211, 3.6035492, 3.621567, 3.6396748, 3.6578732, 3.6761626, 3.6945434, 3.7130161, 3.7315812, 3.7502391, 3.7689903, 3.7878352, 3.8067744, 3.8258083, 3.8449373, 3.864162, 3.8834828, 3.9029002, 3.9224147, 3.9420268, 3.9617369, 3.9815456, 4.0014533, 4.0023486, 4.0136512, 4.0214606, 4.0415679, 4.0617757, 4.0820846, 4.102495, 4.1230075, 4.1436226, 4.1643407, 4.1851624, 4.2060882, 4.2271186, 4.2482542, 4.2694955, 4.290843, 4.3080637, 4.3122972, 4.3259361, 4.3338587, 4.355528, 4.3773056, 4.3991921, 4.4211881, 4.443294, 4.4655105, 4.4878381, 4.5102772, 4.5328286, 4.5554928, 4.5782702, 4.6011616, 4.6241674, 4.6472882, 4.6705247, 4.6938773, 4.7173467, 4.7409334, 4.7646381, 4.7884613, 4.8124036, 4.8364656, 4.8606479, 4.8849512, 4.9093759, 4.9339228, 4.9585924, 4.9833854, 5.0083023, 5.0333438, 5.0585105, 5.0838031, 5.1092221, 5.1347682, 5.1604421, 5.1862443, 5.2121755, 5.2382364, 5.2644276, 5.2907497, 5.3172034, 5.3437895, 5.3705084, 5.3973609, 5.4243477, 5.4514695, 5.4787268, 5.5061205, 5.5336511, 5.5613193, 5.5891259, 5.6170716, 5.6451569, 5.6733827, 5.7017496, 5.7302584, 5.7589096, 5.7877042, 5.8166427, 5.8457259, 5.8749546, 5.9043293, 5.933851, 5.9635202, 5.9933378, 6.0233045, 6.053421, 6.0836882, 6.1141066, 6.1446771, 6.1754005, 6.2062775, 6.2373089, 6.2684954, 6.2998379, 6.3313371, 6.3629938, 6.3948088, 6.4267828, 6.4589167, 6.4912113, 6.5236674, 6.5562857, 6.5890671, 6.6220125, 6.6551225, 6.6883981, 6.7218401, 6.7554493, 6.7892266, 6.8231727, 6.8572886, 6.891575, 6.9260329, 6.9606631, 6.9954664, 7.0304437, 7.0655959, 7.1009239, 7.1364285, 7.1721107, 7.2079712, 7.2440111, 7.2802311, 7.3166323, 7.3532155, 7.3899815, 7.4269314, 7.4640661, 7.5013864, 7.5388934, 7.5765878, 7.6144708, 7.6525431, 7.6908058, 7.7292599, 7.7679062, 7.8067457, 7.8457794, 7.8850083, 7.9244334, 7.9640555, 8.0038758, 8.0438952, 8.0841147, 8.1245352, 8.1651579, 8.2059837, 8.2470136, 8.2882487, 8.3296899, 8.3713384, 8.4131951, 8.455261, 8.4975373, 8.540025, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 13.92923, 14.14243, 14.19929, 14.28457, 14.40688, 14.49777, 15.40095, 16.46362, 16.70078, 16.76792, 16.86862, 17.12039, 17.14314, 17.40554, 17.47551, 17.58047, 17.59961, 17.84286, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 93.8153, 95.25125, 95.63417, 96.20855, 97.64449, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'In': {'mass_absorption_coefficient (cm2/g)': [43466.0, 43615.0, 43934.0, 44767.0, 47567.0, 52566.0, 59521.0, 68209.0, 78321.0, 89394.0, 100800.0, 111760.0, 121440.0, 129040.0, 133940.0, 135730.0, 134330.0, 129940.0, 123010.0, 114140.0, 104000.0, 93237.0, 82413.0, 71968.0, 62231.0, 58880.0, 56867.0, 56344.0, 66368.0, 63716.0, 63326.0, 52700.0, 44030.0, 37066.0, 31393.0, 26740.0, 22910.0, 22315.0, 21567.0, 21375.0, 21902.0, 21226.0, 20568.0, 17971.0, 15813.0, 14007.0, 12486.0, 11195.0, 10090.0, 9134.7, 8301.0, 7565.4, 6909.6, 6319.1, 5782.7, 5291.7, 4858.9, 4460.1, 4087.4, 3738.9, 3412.9, 3252.9, 3184.2, 3175.1, 3166.2, 14531.0, 14448.0, 14446.0, 14400.0, 22034.0, 21992.0, 21736.0, 21032.0, 19833.0, 18282.0, 16548.0, 14788.0, 13790.0, 13412.0, 13314.0, 14983.0, 14919.0, 14601.0, 14210.0, 13834.0, 13736.0, 14330.0, 13996.0, 13969.0, 12428.0, 11104.0, 10871.0, 10766.0, 10679.0, 11028.0, 10705.0, 9931.9, 8672.7, 7570.4, 6504.6, 5582.9, 4777.9, 4084.9, 3493.3, 2985.0, 2550.0, 2179.6, 1858.7, 1576.2, 1338.7, 1138.9, 970.33, 828.02, 706.54, 600.98, 505.04, 420.25, 349.81, 322.52, 309.44, 306.08, 1056.8, 1019.4, 1010.1, 967.28, 923.79, 913.3, 1252.8, 1200.4, 1168.3, 1092.3, 1047.9, 1036.6, 1178.8, 1135.1, 1126.7, 953.36, 807.57, 681.12, 573.05, 481.42, 404.37, 339.74, 285.63, 239.8, 201.2, 168.26, 139.56, 115.87, 96.285, 80.086, 66.667, 55.505, 46.244, 38.567, 32.147, 26.734, 22.197, 18.389, 15.248, 12.654, 10.511, 8.7392, 7.7493, 7.4078, 7.3202, 46.316, 46.303, 44.496, 38.732, 32.433, 27.171, 22.753, 18.985, 15.834, 13.2, 11.001, 9.166, 7.6367, 6.3415, 5.2583, 4.3605, 3.6163, 2.9993, 2.4839, 2.05, 1.692, 1.3966, 1.1528, 0.95167, 0.78562, 0.6482, 0.53485, 0.44135, 0.36421, 0.30057, 0.24806, 0.20473, 0.16898, 0.13948, 0.11514, 0.095043, 0.07846, 0.064773, 0.053476, 0.044151, 0.036453, 0.030099, 0.024853, 0.020522, 0.0], - 'energies (keV)': [0.016281, 0.016524, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.06477028, 0.06923942, 0.07401695, 0.075852, 0.077013, 0.0773226, 0.077787, 0.078948, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.119462, 0.1212905, 0.1217781, 0.1225095, 0.124338, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.2301188, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.3924405, 0.4195189, 0.434238, 0.4408845, 0.441784, 0.4426569, 0.4453155, 0.4484657, 0.448546, 0.4503492, 0.451962, 0.453054, 0.459816, 0.4794098, 0.5124891, 0.5478508, 0.5856525, 0.6260625, 0.651014, 0.6609785, 0.6636357, 0.6676215, 0.6692609, 0.677586, 0.688156, 0.698689, 0.7014978, 0.705711, 0.7154399, 0.716244, 0.7648052, 0.809088, 0.8175768, 0.821472, 0.8247744, 0.829728, 0.842112, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.541848, 2.717235, 2.904724, 3.10515, 3.319406, 3.548445, 3.655498, 3.711449, 3.72637, 3.74875, 3.793288, 3.804702, 3.85924, 3.91831, 3.934062, 3.95769, 4.01676, 4.055024, 4.15275, 4.216313, 4.233263, 4.258688, 4.32225, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 20.11215, 21.49988, 22.98338, 24.56923, 26.2645, 27.3811, 27.8002, 27.91196, 28.07676, 28.0796, 28.4987, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}, - 'Mo': {'mass_absorption_coefficient (cm2/g)': [173430.0, 167340.0, 160950.0, 154290.0, 147410.0, 140360.0, 133210.0, 126000.0, 118800.0, 111660.0, 104640.0, 97787.0, 91140.0, 84736.0, 78604.0, 72768.0, 67243.0, 62040.0, 59921.0, 58489.0, 58115.0, 149980.0, 141270.0, 110500.0, 87909.0, 71394.0, 58993.0, 49441.0, 41912.0, 35857.0, 30904.0, 29929.0, 29671.0, 30231.0, 29332.0, 27778.0, 24404.0, 21550.0, 19109.0, 17008.0, 15187.0, 13601.0, 12212.0, 10992.0, 9915.4, 8962.5, 8116.2, 7362.1, 6687.2, 6081.0, 5534.7, 5040.7, 4592.6, 4184.9, 3997.5, 3917.9, 3913.7, 3891.9, 6998.8, 7068.6, 7137.8, 9370.5, 9382.6, 9865.0, 11817.0, 15689.0, 19607.0, 22799.0, 24811.0, 25514.0, 25047.0, 24184.0, 23826.0, 23726.0, 26442.0, 26317.0, 25900.0, 25801.0, 25344.0, 25219.0, 26172.0, 25690.0, 25561.0, 23236.0, 20819.0, 19705.0, 19166.0, 19025.0, 19643.0, 19268.0, 19117.0, 16990.0, 14902.0, 13020.0, 11344.0, 9855.4, 8542.3, 7393.9, 6389.6, 5514.3, 4754.7, 4031.2, 3420.2, 2904.7, 2469.7, 2102.3, 1791.8, 1527.6, 1296.6, 1100.4, 935.2, 795.29, 676.58, 576.26, 526.24, 507.52, 502.69, 1928.7, 1909.3, 1845.6, 1768.2, 1748.3, 2399.9, 2304.9, 2215.8, 2033.5, 1956.5, 1936.7, 2194.8, 2149.1, 2116.9, 1821.7, 1537.7, 1292.1, 1084.6, 911.21, 766.02, 644.24, 542.28, 454.78, 380.28, 317.51, 265.19, 221.7, 185.5, 155.19, 129.92, 108.62, 90.127, 74.827, 62.152, 51.472, 42.598, 35.156, 29.042, 24.013, 19.871, 16.459, 13.644, 12.168, 11.663, 11.533, 79.499, 76.366, 66.041, 55.42, 46.603, 39.141, 32.644, 27.187, 22.646, 18.866, 15.718, 13.097, 10.855, 8.9758, 7.4226, 6.1386, 5.077, 4.1989, 3.4719, 2.8696, 2.3719, 1.9607, 1.6186, 1.3318, 1.0941, 0.89895, 0.73863, 0.60693, 0.49874, 0.40985, 0.33682, 0.27681, 0.22751, 0.18699, 0.1537, 0.12634, 0.10385, 0.08537, 0.07018, 0.057695, 0.047433, 0.038997, 0.032063, 0.026362, 0.021676, 0.017823, 0.014656, 0.012051, 0.0], - 'energies (keV)': [0.01069, 0.01142761, 0.01221612, 0.01305903, 0.0139601, 0.01492335, 0.01595306, 0.01705382, 0.01823053, 0.01948844, 0.02083314, 0.02227063, 0.0238073, 0.02545001, 0.02720606, 0.02908327, 0.03109002, 0.03323523, 0.034104, 0.034626, 0.0347652, 0.034974, 0.03552846, 0.03797993, 0.04060054, 0.04340198, 0.04639671, 0.04959809, 0.05302035, 0.05667876, 0.06058959, 0.061491, 0.0617382, 0.062109, 0.063036, 0.06477028, 0.06923942, 0.07401695, 0.07912411, 0.08458368, 0.09041995, 0.09665893, 0.1033284, 0.1104581, 0.1180797, 0.1262272, 0.1349368, 0.1442475, 0.1542005, 0.1648404, 0.1762144, 0.1883732, 0.2013709, 0.2152655, 0.22246, 0.225694, 0.225865, 0.226773, 0.228135, 0.2291485, 0.2301188, 0.2314515, 0.23154, 0.234906, 0.245997, 0.2629708, 0.2811158, 0.3005128, 0.3212482, 0.3434143, 0.3671099, 0.384454, 0.3903385, 0.3919077, 0.3924405, 0.3942615, 0.400146, 0.401506, 0.4076515, 0.4092903, 0.4117485, 0.417894, 0.4195189, 0.4484657, 0.4794098, 0.494508, 0.502077, 0.5040954, 0.507123, 0.5124891, 0.514692, 0.5478508, 0.5856525, 0.6260625, 0.6692609, 0.7154399, 0.7648052, 0.8175768, 0.8739896, 0.9342948, 0.9987612, 1.067676, 1.141345, 1.220098, 1.304285, 1.394281, 1.490486, 1.593329, 1.703269, 1.820795, 1.94643, 2.080733, 2.224304, 2.377781, 2.469796, 2.507599, 2.51768, 2.532801, 2.541848, 2.572598, 2.611974, 2.622475, 2.638225, 2.677602, 2.717235, 2.80819, 2.851172, 2.862634, 2.879827, 2.904724, 2.92281, 3.10515, 3.319406, 3.548445, 3.793288, 4.055024, 4.334821, 4.633924, 4.953664, 5.295467, 5.660855, 6.051453, 6.469004, 6.915365, 7.392525, 7.902609, 8.44789, 9.030794, 9.653919, 10.32004, 11.03212, 11.79334, 12.60708, 13.47697, 14.40688, 15.40095, 16.46362, 17.59961, 18.81398, 19.59951, 19.8995, 19.9795, 20.11215, 20.39949, 21.49988, 22.98338, 24.56923, 26.2645, 28.07676, 30.01405, 32.08502, 34.29889, 36.66551, 39.19543, 41.89992, 44.79101, 47.88159, 51.18542, 54.71721, 58.4927, 62.5287, 66.84318, 71.45536, 76.38578, 81.6564, 87.29069, 93.31374, 99.75239, 106.6353, 113.9931, 121.8587, 130.2669, 139.2553, 148.864, 159.1356, 170.1159, 181.8539, 194.4018, 207.8156, 222.1548, 237.4835, 253.8699, 271.3869, 290.1126, 310.1304, 331.5294, 354.4049, 378.8588, 405.0001, 432.9451, 0.0]}} - -ffast_mac_db = utils.DictionaryTreeBrowser(ffast_mac) diff --git a/hyperspy/misc/eds/utils.py b/hyperspy/misc/eds/utils.py deleted file mode 100644 index 9d5654481d..0000000000 --- a/hyperspy/misc/eds/utils.py +++ /dev/null @@ -1,731 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import math -from scipy import constants -from hyperspy.misc.utils import stack -from hyperspy.misc.elements import elements as elements_db -from functools import reduce - -eV2keV = 1000. -sigma2fwhm = 2 * math.sqrt(2 * math.log(2)) - - -def _get_element_and_line(xray_line): - """ - Returns the element name and line character for a particular X-ray line as - a tuple. - - By example, if xray_line = 'Mn_Ka' this function returns ('Mn', 'Ka') - """ - lim = xray_line.find('_') - if lim == -1: - raise ValueError(f"Invalid xray-line: {xray_line}") - return xray_line[:lim], xray_line[lim + 1:] - - -def _get_energy_xray_line(xray_line): - """ - Returns the energy (in keV) associated with a given X-ray line. - - By example, if xray_line = 'Mn_Ka' this function returns 5.8987 - """ - element, line = _get_element_and_line(xray_line) - return elements_db[element]['Atomic_properties']['Xray_lines'][ - line]['energy (keV)'] - - -def _get_xray_lines_family(xray_line): - """ - Returns the family to which a particular X-ray line belongs. - - By example, if xray_line = 'Mn_Ka' this function returns 'Mn_K' - """ - return xray_line[:xray_line.find('_') + 2] - - -def _parse_only_lines(only_lines): - if isinstance(only_lines, str): - pass - elif hasattr(only_lines, '__iter__'): - if any(isinstance(line, str) is False for line in only_lines): - return only_lines - else: - return only_lines - only_lines = list(only_lines) - for only_line in only_lines: - if only_line == 'a': - only_lines.extend(['Ka', 'La', 'Ma']) - elif only_line == 'b': - only_lines.extend(['Kb', 'Lb1', 'Mb']) - return only_lines - - -def get_xray_lines_near_energy(energy, width=0.2, only_lines=None): - """Find xray lines near a specific energy, more specifically all xray lines - that satisfy only_lines and are within the given energy window width around - the passed energy. - - Parameters - ---------- - energy : float - Energy to search near in keV - width : float - Window width in keV around energy in which to find nearby energies, - i.e. a value of 0.2 keV (the default) means to search +/- 0.1 keV. - only_lines : - If not None, only the given lines will be added (eg. ('a','Kb')). - - Returns - ------- - List of xray-lines sorted by energy difference to given energy. - """ - only_lines = _parse_only_lines(only_lines) - valid_lines = [] - E_min, E_max = energy - width / 2., energy + width / 2. - for element, el_props in elements_db.items(): - # Not all elements in the DB have the keys, so catch KeyErrors - try: - lines = el_props['Atomic_properties']['Xray_lines'] - except KeyError: - continue - for line, l_props in lines.items(): - if only_lines and line not in only_lines: - continue - line_energy = l_props['energy (keV)'] - if E_min <= line_energy <= E_max: - # Store line in Element_Line format, and energy difference - valid_lines.append((element + "_" + line, - np.abs(line_energy - energy))) - # Sort by energy difference, but return only the line names - return [line for line, _ in sorted(valid_lines, key=lambda x: x[1])] - - -def get_FWHM_at_Energy(energy_resolution_MnKa, E): - """Calculates an approximate FWHM, accounting for peak broadening due to the - detector, for a peak at energy E given a known width at a reference energy. - - The factor 2.5 is a constant derived by Fiori & Newbury as references - below. - - Parameters - ---------- - energy_resolution_MnKa : float - Energy resolution of Mn Ka in eV - E : float - Energy of the peak in keV - - Returns - ------- - float : FWHM of the peak in keV - - Notes - ----- - This method implements the equation derived by Fiori and Newbury as is - documented in the following: - - Fiori, C. E., and Newbury, D. E. (1978). In SEM/1978/I, SEM, Inc., - AMF O'Hare, Illinois, p. 401. - - Goldstein et al. (2003). "Scanning Electron Microscopy & X-ray - Microanalysis", Plenum, third edition, p 315. - - """ - FWHM_ref = energy_resolution_MnKa - E_ref = _get_energy_xray_line('Mn_Ka') - - FWHM_e = 2.5 * (E - E_ref) * eV2keV + FWHM_ref * FWHM_ref - - return math.sqrt(FWHM_e) / 1000. # In mrad - - -def xray_range(xray_line, beam_energy, density='auto'): - """Return the maximum range of X-ray generation according to the - Anderson-Hasler parameterization. - - Parameters - ---------- - xray_line: str - The X-ray line, e.g. 'Al_Ka' - beam_energy: float - The energy of the beam in kV. - density: {float, 'auto'} - The density of the material in g/cm3. If 'auto', the density - of the pure element is used. - - Returns - ------- - X-ray range in micrometer. - - Examples - -------- - >>> # X-ray range of Cu Ka in pure Copper at 30 kV in micron - >>> hs.eds.xray_range('Cu_Ka', 30.) - 1.9361716759499248 - - >>> # X-ray range of Cu Ka in pure Carbon at 30kV in micron - >>> hs.eds.xray_range('Cu_Ka', 30., hs.material.elements.C. - >>> Physical_properties.density_gcm3) - 7.6418811280855454 - - Notes - ----- - From Anderson, C.A. and M.F. Hasler (1966). In proceedings of the - 4th international conference on X-ray optics and microanalysis. - - See also the textbook of Goldstein et al., Plenum publisher, - third edition p 286 - - """ - - element, line = _get_element_and_line(xray_line) - if density == 'auto': - density = elements_db[ - element][ - 'Physical_properties'][ - 'density (g/cm^3)'] - Xray_energy = _get_energy_xray_line(xray_line) - # Note: magic numbers here are from Andersen-Hasler parameterization. See - # docstring for associated references. - return 0.064 / density * (np.power(beam_energy, 1.68) - - np.power(Xray_energy, 1.68)) - - -def electron_range(element, beam_energy, density='auto', tilt=0): - """Returns the maximum electron range for a pure bulk material according to - the Kanaya-Okayama parameterziation. - - Parameters - ---------- - element: str - The element symbol, e.g. 'Al'. - beam_energy: float - The energy of the beam in keV. - density: {float, 'auto'} - The density of the material in g/cm3. If 'auto', the density of - the pure element is used. - tilt: float. - The tilt of the sample in degrees. - - Returns - ------- - Electron range in micrometers. - - Examples - -------- - >>> # Electron range in pure Copper at 30 kV in micron - >>> hs.eds.electron_range('Cu', 30.) - 2.8766744984001607 - - Notes - ----- - From Kanaya, K. and S. Okayama (1972). J. Phys. D. Appl. Phys. 5, p43 - - See also the textbook of Goldstein et al., Plenum publisher, - third edition p 72. - - """ - - if density == 'auto': - density = elements_db[ - element]['Physical_properties']['density (g/cm^3)'] - Z = elements_db[element]['General_properties']['Z'] - A = elements_db[element]['General_properties']['atomic_weight'] - # Note: magic numbers here are from Kanaya-Okayama parameterization. See - # docstring for associated references. - return (0.0276 * A / np.power(Z, 0.89) / density * - np.power(beam_energy, 1.67) * math.cos(math.radians(tilt))) - - -def take_off_angle(tilt_stage, azimuth_angle, elevation_angle, beta_tilt=0.0): - """Calculate the take-off-angle (TOA). - - TOA is the angle with which the X-rays leave the surface towards - the detector. - - Parameters - ---------- - alpha_tilt: float - The alpha-tilt of the stage in degrees. The sample is facing the detector - when positively tilted. - azimuth_angle: float - The azimuth of the detector in degrees. 0 is perpendicular to the alpha - tilt axis. - elevation_angle: float - The elevation of the detector in degrees. - beta_tilt: float - The beta-tilt of the stage in degrees. The sample is facing positive 90 - in the azimuthal direction when positively tilted. - - Returns - ------- - take_off_angle: float. - In degrees. - - Examples - -------- - >>> hs.eds.take_off_angle(alpha_tilt=10., beta_tilt=0. - >>> azimuth_angle=45., elevation_angle=22.) - 28.865971201155283 - """ - - if tilt_stage is None: - raise ValueError( - "Unable to calculate take-off angle. The metadata property " - "`Stage.tilt_alpha` is not set." - ) - - if azimuth_angle is None: - raise ValueError( - "Unable to calculate take-off angle. The metadata property " - "`Detector.EDS.azimuth_angle` is not set." - ) - - if elevation_angle is None: - raise ValueError( - "Unable to calculate take-off angle. The metadata property " - "`Detector.EDS.elevation_angle` is not set." - ) - - alpha = math.radians(tilt_stage) - beta = -math.radians(beta_tilt) - phi = math.radians(azimuth_angle) - theta = -math.radians(elevation_angle) - - return 90 - math.degrees( - np.arccos( - math.sin(alpha) * math.cos(beta) * math.cos(phi) * math.cos(theta) - - math.sin(beta) * math.sin(phi) * math.cos(theta) - - math.cos(alpha) * math.cos(beta) * math.sin(theta) - ) - ) - -def xray_lines_model(elements, - beam_energy=200, - weight_percents=None, - energy_resolution_MnKa=130, - energy_axis=None): - """ - Generate a model of X-ray lines using a Gaussian distribution for each - peak. - - The area under a main peak (alpha) is equal to 1 and weighted by the - composition. - - Parameters - ---------- - elements : list of strings - A list of chemical element symbols. - beam_energy: float - The energy of the beam in keV. - weight_percents: list of float - The composition in weight percent. - energy_resolution_MnKa: float - The energy resolution of the detector in eV - energy_axis: dic - The dictionary for the energy axis. It must contains 'size' and the - units must be 'eV' of 'keV'. - - Example - ------- - >>> s = xray_lines_model(['Cu', 'Fe'], beam_energy=30) - >>> s.plot() - """ - from hyperspy._signals.eds_tem import EDSTEMSpectrum - from hyperspy import components1d - if energy_axis is None: - energy_axis = {'name': 'E', 'scale': 0.01, 'units': 'keV', - 'offset': -0.1, 'size': 1024} - s = EDSTEMSpectrum(np.zeros(energy_axis['size']), axes=[energy_axis]) - s.set_microscope_parameters( - beam_energy=beam_energy, - energy_resolution_MnKa=energy_resolution_MnKa) - s.add_elements(elements) - counts_rate = 1. - live_time = 1. - if weight_percents is None: - weight_percents = [100. / len(elements)] * len(elements) - m = s.create_model() - if len(elements) == len(weight_percents): - for (element, weight_percent) in zip(elements, weight_percents): - for line, properties in elements_db[ - element]['Atomic_properties']['Xray_lines'].items(): - line_energy = properties['energy (keV)'] - ratio_line = properties['weight'] - if s._get_xray_lines_in_spectral_range( - [element + '_' + line])[1] == []: - g = components1d.Gaussian() - g.centre.value = line_energy - g.sigma.value = get_FWHM_at_Energy( - energy_resolution_MnKa, line_energy) / sigma2fwhm - g.A.value = live_time * counts_rate * \ - weight_percent / 100 * ratio_line - m.append(g) - else: - raise ValueError("The number of elements specified is not the same " - "as the number of weight_percents") - - s.data = m.as_signal().data - return s - - -def quantification_cliff_lorimer(intensities, - kfactors, - absorption_correction=None, - mask=None): - """ - Quantification using Cliff-Lorimer - - Parameters - ---------- - intensities: numpy.array - the intensities for each X-ray lines. The first axis should be the - elements axis. - kfactors: list of float - The list of kfactor in same order as intensities eg. kfactors = - [1, 1.47, 1.72] for ['Al_Ka','Cr_Ka', 'Ni_Ka'] - mask: array of bool of signal of bool - The mask with the dimension of intensities[0]. If a pixel is True, - the composition is set to zero. - - Return - ------ - numpy.array containing the weight fraction with the same - shape as intensities. - """ - # Value used as an threshold to prevent using zeros as denominator - min_intensity = 0.1 - dim = intensities.shape - if len(dim) > 1: - dim2 = reduce(lambda x, y: x * y, dim[1:]) - intens = intensities.reshape(dim[0], dim2) - intens = intens.astype('float') - if absorption_correction is not None: - absorption_correction = absorption_correction.reshape(dim[0], dim2) - else: - # default to ones - absorption_correction = np.ones_like(intens, dtype='float') - for i in range(dim2): - index = np.where(intens[:, i] > min_intensity)[0] - if len(index) > 1: - ref_index, ref_index2 = index[:2] - intens[:, i] = _quantification_cliff_lorimer( - intens[:, i], kfactors, absorption_correction[:, i], - ref_index, ref_index2) - else: - intens[:, i] = np.zeros_like(intens[:, i]) - if len(index) == 1: - intens[index[0], i] = 1. - intens = intens.reshape(dim) - if mask is not None: - from hyperspy.signals import BaseSignal - if isinstance(mask, BaseSignal): - mask = mask.data - for i in range(dim[0]): - intens[i][(mask==True)] = 0 - return intens - else: - index = np.where(intensities > min_intensity)[0] - if absorption_correction is not None: - absorption_correction = absorption_correction - else: - # default to ones - absorption_correction = np.ones_like(intens, dtype='float') - if len(index) > 1: - ref_index, ref_index2 = index[:2] - intens = _quantification_cliff_lorimer( - intensities, - kfactors, - absorption_correction, - ref_index, - ref_index2) - else: - intens = np.zeros_like(intensities) - if len(index) == 1: - intens[index[0]] = 1. - return intens - - -def _quantification_cliff_lorimer(intensities, - kfactors, - absorption_correction, - ref_index=0, - ref_index2=1 - ): - """ - Quantification using Cliff-Lorimer - - Parameters - ---------- - intensities: numpy.array - the intensities for each X-ray lines. The first axis should be the - elements axis. - absorption_correction: numpy.array - value between 0 and 1 in order to correct the intensities based on - estimated absorption. - kfactors: list of float - The list of kfactor in same order as intensities eg. kfactors = - [1, 1.47, 1.72] for ['Al_Ka','Cr_Ka', 'Ni_Ka'] - ref_index, ref_index2: int - index of the elements that will be in the denominator. Should be non - zeros if possible. - - Return - ------ - numpy.array containing the weight fraction with the same - shape as intensities. - """ - if len(intensities) != len(kfactors): - raise ValueError('The number of kfactors must match the size of the ' - 'first axis of intensities.') - - ab = np.zeros_like(intensities, dtype='float') - composition = np.ones_like(intensities, dtype='float') - # ab = Ia/Ib / kab - other_index = list(range(len(kfactors))) - other_index.pop(ref_index) - for i in other_index: - ab[i] = (intensities[ref_index] * absorption_correction[ref_index]) \ - / (intensities[i] * absorption_correction[i]) \ - *( kfactors[ref_index] / kfactors[i]) - # Ca = ab /(1 + ab + ab/ac + ab/ad + ...) - ab = ab - for i in other_index: - if i == ref_index2: - composition[ref_index] += ab[ref_index2] - else: - composition[ref_index] += (ab[ref_index2] / ab[i]) - composition[ref_index] = ab[ref_index2] / composition[ref_index] - # Cb = Ca / ab - for i in other_index: - composition[i] = composition[ref_index] / ab[i] - return composition - - -def quantification_zeta_factor(intensities, - zfactors, - dose, - absorption_correction=None): - """ - Quantification using the zeta-factor method - - Parameters - ---------- - intensities: numpy.array - The intensities for each X-ray line. The first axis should be the - elements axis. - zfactors: list of float - The list of zeta-factors in the same order as intensities - e.g. zfactors = [628.10, 539.89] for ['As_Ka', 'Ga_Ka']. - dose: float - The total electron dose given by i*t*N, i the current, - t the acquisition time and - N the number of electrons per unit electric charge (1/e). - - Returns - ------ - A numpy.array containing the weight fraction with the same - shape as intensities and mass thickness in kg/m^2. - """ - if absorption_correction is not None: - absorption_correction = absorption_correction - else: - # default to ones - absorption_correction = np.ones_like(intensities, dtype='float') - - sumzi = np.zeros_like(intensities[0], dtype='float') - composition = np.zeros_like(intensities, dtype='float') - for intensity, zfactor, acf in zip(intensities, zfactors, absorption_correction): - sumzi = sumzi + (intensity * zfactor * acf) - for i, (intensity, zfactor, acf) in enumerate(zip(intensities, zfactors, absorption_correction)): - composition[i] = intensity * zfactor * acf / sumzi - mass_thickness = sumzi / dose - return composition, mass_thickness - - -def get_abs_corr_zeta(weight_percent, mass_thickness, take_off_angle): # take_off_angle, temporary value for testing - """ - Calculate absorption correction terms. - - Parameters - ---------- - weight_percent: list of signal - Composition in weight percent. - mass_thickness: signal - Density-thickness map in kg/m^2 - take_off_angle: float - X-ray take-off angle in degrees. - """ - from hyperspy.misc import material - - toa_rad = np.radians(take_off_angle) - csc_toa = 1.0/np.sin(toa_rad) - # convert from cm^2/g to m^2/kg - mac = stack( - material.mass_absorption_mixture(weight_percent=weight_percent), - show_progressbar=False - ) * 0.1 - expo = mac.data * mass_thickness.data * csc_toa - acf = expo/(1.0 - np.exp(-(expo))) - return acf - -def quantification_cross_section(intensities, - cross_sections, - dose, - absorption_correction=None): - """ - Quantification using EDX cross sections - Calculate the atomic compostion and the number of atoms per pixel - from the raw X-ray intensity - - Parameters - ---------- - intensity : numpy.ndarray - The integrated intensity for each X-ray line, where the first axis - is the element axis. - cross_sections : list of floats - List of X-ray scattering cross-sections in the same order as the - intensities. - dose: float - the dose per unit area given by i*t*N/A, i the current, - t the acquisition time, and - N the number of electron by unit electric charge. - - Returns - ------- - numpy.array containing the atomic fraction of each element, with - the same shape as the intensity input. - numpy.array of the number of atoms counts for each element, with the same - shape as the intensity input. - """ - - - if absorption_correction is not None: - absorption_correction = absorption_correction - else: - # default to ones - absorption_correction = np.ones_like(intensities, dtype='float') - - shp = len(intensities.shape) - 1 - slices = (slice(None),) + (None,) * shp - x_sections = np.array(cross_sections, dtype='float')[slices] - number_of_atoms = intensities / (x_sections * dose * 1e-10) * absorption_correction - total_atoms = np.cumsum(number_of_atoms, axis=0)[-1] - composition = number_of_atoms / total_atoms - - return composition, number_of_atoms - - -def get_abs_corr_cross_section(composition, number_of_atoms, take_off_angle, - probe_area): - """ - Calculate absorption correction terms. - - Parameters - ---------- - number_of_atoms: list of signal - Stack of maps with number of atoms per pixel. - take_off_angle: float - X-ray take-off angle in degrees. - """ - from hyperspy.misc import material - - toa_rad = np.radians(take_off_angle) - Av = constants.Avogadro - elements = [intensity.metadata.Sample.elements[0] for intensity in number_of_atoms] - atomic_weights = np.array( - [elements_db[element]['General_properties']['atomic_weight'] - for element in elements]) - - number_of_atoms = stack(number_of_atoms, show_progressbar=False).data - - #calculate the total_mass in kg/m^2, or mass thickness. - total_mass = np.zeros_like(number_of_atoms[0], dtype = 'float') - for i, (weight) in enumerate(atomic_weights): - total_mass += (number_of_atoms[i] * weight / Av / 1E3 / probe_area / 1E-18) - # determine mass absorption coefficients and convert from cm^2/g to m^2/kg. - to_stack = material.mass_absorption_mixture( - weight_percent=material.atomic_to_weight(composition) - ) - mac = stack(to_stack, show_progressbar=False) * 0.1 - acf = np.zeros_like(number_of_atoms) - csc_toa = 1/math.sin(toa_rad) - #determine an absorption coeficient per element per pixel. - for i, (weight) in enumerate(atomic_weights): - expo = (mac.data[i] * total_mass * csc_toa) - acf[i] = expo/(1 - np.exp(-expo)) - return acf - - -def edx_cross_section_to_zeta(cross_sections, elements): - """Convert a list of cross_sections in barns (b) to zeta-factors (kg/m^2). - - Parameters - ---------- - cross_section: list of float - A list of cross sections in barns. - elements: list of str - A list of element chemical symbols in the same order as the - cross sections e.g. ['Al','Zn'] - - Returns - ------- - zeta_factors : list of float - zeta_factors with units kg/m^2. - - """ - if len(elements) != len(cross_sections): - raise ValueError( - 'The number of elements must match the number of cross sections.') - zeta_factors = [] - for i, element in enumerate(elements): - atomic_weight = elements_db[element]['General_properties'][ - 'atomic_weight'] - zeta = atomic_weight / (cross_sections[i] * constants.Avogadro * 1E-25) - zeta_factors.append(zeta) - return zeta_factors - - -def zeta_to_edx_cross_section(zfactors, elements): - """Convert a list of zeta-factors (kg/m^2) to cross_sections in barns (b). - - Parameters - ---------- - zfactors: list of float - A list of zeta-factors. - elements: list of str - A list of element chemical symbols in the same order as the - cross sections e.g. ['Al','Zn'] - - Returns - ------- - cross_sections : list of float - cross_sections with units in barns. - - """ - if len(elements) != len(zfactors): - raise ValueError( - 'The number of elements must match the number of cross sections.') - cross_sections = [] - for i, element in enumerate(elements): - atomic_weight = elements_db[element]['General_properties'][ - 'atomic_weight'] - xsec = atomic_weight / (zfactors[i] * constants.Avogadro * 1E-25) - cross_sections.append(xsec) - return cross_sections diff --git a/hyperspy/misc/eels/__init__.py b/hyperspy/misc/eels/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hyperspy/misc/eels/base_gos.py b/hyperspy/misc/eels/base_gos.py deleted file mode 100644 index e566360e06..0000000000 --- a/hyperspy/misc/eels/base_gos.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - -from hyperspy.misc.math_tools import get_linear_interpolation -from hyperspy.misc.elements import elements - - -class GOSBase(object): - - def read_elements(self): - element = self.element - subshell = self.subshell - # Convert to the "GATAN" nomenclature - if (element in elements) is not True: - raise ValueError("The given element " + element + - " is not in the database.") - elif subshell not in elements[element]['Atomic_properties']['Binding_energies']: - raise ValueError( - "The given subshell " + subshell + - " is not in the database.\n" + - "The available subshells are:\n" + - str(list(elements[element]['Atomic_properties']['subshells'].keys()))) - - self.onset_energy = \ - elements[ - element][ - 'Atomic_properties'][ - 'Binding_energies'][ - subshell][ - 'onset_energy (eV)'] - self.subshell_factor = \ - elements[ - element][ - 'Atomic_properties'][ - 'Binding_energies'][ - subshell][ - 'factor'] - self.Z = elements[element]['General_properties']['Z'] - self.element_dict = elements[element] - - def get_parametrized_qaxis(self, k1, k2, n): - return k1 * (np.exp(np.arange(n) * k2) - 1) * 1e10 - - def get_parametrized_energy_axis(self, k1, k2, n): - return k1 * (np.exp(np.arange(n) * k2 / k1) - 1) - - def get_qaxis_and_gos(self, ienergy, qmin, qmax): - qgosi = self.gos_array[ienergy, :] - if qmax > self.qaxis[-1]: - # Linear extrapolation - g1, g2 = qgosi[-2:] - q1, q2 = self.qaxis[-2:] - gosqmax = get_linear_interpolation((q1, g1), (q2, g2), qmax) - qaxis = np.hstack((self.qaxis, qmax)) - qgosi = np.hstack((qgosi, gosqmax)) - else: - index = self.qaxis.searchsorted(qmax) - g1, g2 = qgosi[index - 1:index + 1] - q1, q2 = self.qaxis[index - 1: index + 1] - gosqmax = get_linear_interpolation((q1, g1), (q2, g2), qmax) - qaxis = np.hstack((self.qaxis[:index], qmax)) - qgosi = np.hstack((qgosi[:index, ], gosqmax)) - - if qmin > 0: - index = self.qaxis.searchsorted(qmin) - g1, g2 = qgosi[index - 1:index + 1] - q1, q2 = qaxis[index - 1:index + 1] - gosqmin = get_linear_interpolation((q1, g1), (q2, g2), qmin) - qaxis = np.hstack((qmin, qaxis[index:])) - qgosi = np.hstack((gosqmin, qgosi[index:],)) - return qaxis, qgosi.clip(0) diff --git a/hyperspy/misc/eels/eelsdb.py b/hyperspy/misc/eels/eelsdb.py deleted file mode 100644 index c043100a3c..0000000000 --- a/hyperspy/misc/eels/eelsdb.py +++ /dev/null @@ -1,295 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -"""This module provides tools to interact with The EELS Database.""" -import requests -import logging - -from hyperspy.io_plugins.msa import parse_msa_string -from hyperspy.io import dict2signal - -_logger = logging.getLogger(__name__) - - -def eelsdb(spectrum_type=None, title=None, author=None, element=None, formula=None, - edge=None, min_energy=None, max_energy=None, resolution=None, - min_energy_compare="gt", max_energy_compare="lt", - resolution_compare="lt", max_n=-1, monochromated=None, order=None, - order_direction="ASC", verify_certificate=True): - r"""Download spectra from the EELS Data Base. - - Parameters - ---------- - spectrum_type: {'coreloss', 'lowloss', 'zeroloss', 'xrayabs'}, optional - title: string - Search spectra titles for a text string. - author: string, optional - Search authors for a text string. - element: string or list of strings, optional - Filter for the presence of one or more element. Each string must - correspond with a valid element symbol. - formula: string - Chemical formula of the sample. - edge: {'K', 'L1', 'L2,3', 'M2,3', 'M4,5', 'N2,3', 'N4,5' 'O2,3', 'O4,5'}, optional - Filter for spectra with a specific class of edge. - min_energy, max_energy: float, optional - Minimum and maximum energy in eV. - resolution: float, optional - Energy resolution in eV. - resolution_compare: {"lt", "eq", "gt"}, optional, default "lt" - "lt" to search for all spectra with resolution less than `resolution`. - "eq" for equal, "gt" for greater than. - min_energy_compare, max_energy_compare: {"lt", "eq", "gt"}, optional - "lt" to search for all spectra with min/max energy less than - `min_energy`\`max_energy`. "eq" for equal, "gt" for greater than. - Deafault values are "gt"/"lt" for `min_energy`\`max_energy` - respectively. - monochromated: bool or None (default) - max_n: int, default -1 - Maximum number of spectra to return. -1 to return all. - order: string - Key to sort results by. Valid keys are: - * "spectrumType", - * "spectrumMin", - * "spectrumMax", - * "stepSize", - * "spectrumFormula", - * "spectrumElement", - * "spectrumUpload", - * "source_purity", - * "spectrumEdges", - * "microscope", - * "guntype", - * "beamenergy", - * "resolution", - * "monochromated", - * "acquisition_mode", - * "convergence", - * "collection", - * "probesize", - * "beamcurrent", - * "integratetime", - * "readouts", - * "detector", - * "darkcurrent", - * "gainvariation", - * "calibration", - * "zeroloss_deconv", - * "thickness", - * "deconv_fourier_log", - * "deconv_fourier_ratio", - * "deconv_stephens_deconvolution", - * "deconv_richardson_lucy", - * "deconv_maximum_entropy", - * "deconv_other", - * "assoc_spectra", - * "ref_freetext", - * "ref_doi", - * "ref_url", - * "ref_authors", - * "ref_journal", - * "ref_volume", - * "ref_issue", - * "ref_page", - * "ref_year", - * "ref_title", - * "otherURLs" - order_direction : {"ASC", "DESC"} - Sorting `order` direction. - verify_certificate: bool - If True, verify the eelsdb website certificate and raise an error - if it is invalid. If False, continue querying the database if the certificate - is invalid. (This is a potential security risk.) - - - - Returns - ------- - spectra: list - A list containing all the spectra matching the given criteria if - any. - - """ - # Verify arguments - if spectrum_type is not None and spectrum_type not in { - 'coreloss', 'lowloss', 'zeroloss', 'xrayabs'}: - raise ValueError("spectrum_type must be one of \'coreloss\', \'lowloss\', " - "\'zeroloss\', \'xrayabs\'.") - valid_edges = [ - 'K', 'L1', 'L2,3', 'M2,3', 'M4,5', 'N2,3', 'N4,5', 'O2,3', 'O4,5'] - valid_order_keys = [ - "spectrumType", - "spectrumMin", - "spectrumMax", - "stepSize", - "spectrumFormula", - "spectrumElement", - "spectrumUpload", - "source_purity", - "spectrumEdges", - "microscope", - "guntype", - "beamenergy", - "resolution", - "monochromated", - "acquisition_mode", - "convergence", - "collection", - "probesize", - "beamcurrent", - "integratetime", - "readouts", - "detector", - "darkcurrent", - "gainvariation", - "calibration", - "zeroloss_deconv", - "thickness", - "deconv_fourier_log", - "deconv_fourier_ratio", - "deconv_stephens_deconvolution", - "deconv_richardson_lucy", - "deconv_maximum_entropy", - "deconv_other", - "assoc_spectra", - "ref_freetext", - "ref_doi", - "ref_url", - "ref_authors", - "ref_journal", - "ref_volume", - "ref_issue", - "ref_page", - "ref_year", - "ref_title", - "otherURLs"] - if edge is not None and edge not in valid_edges: - raise ValueError("`edge` must be one of %s." % ", ".join(valid_edges)) - - if order is not None and order not in valid_order_keys: - raise ValueError("`order` must be one of %s." % ", ".join( - valid_order_keys)) - if order_direction is not None and order_direction not in ["ASC", "DESC"]: - raise ValueError("`order_direction` must be \"ASC\" or \"DESC\".") - for kwarg, label in ( - (resolution_compare, "resolution_compare"), - (min_energy_compare, "min_energy_compare"), - (max_energy_compare, "max_energy_compare")): - if kwarg not in ("lt", "gt", "eq"): - raise ValueError("`%s` must be \"lt\", \"eq\" or \"gt\"." % - label) - if monochromated is not None: - monochromated = 1 if monochromated else 0 - params = { - "type": spectrum_type, - "title": title, - "author": author, - "edge": edge, - "min_energy": min_energy, - "max_energy": max_energy, - "resolution": resolution, - "resolution_compare": resolution_compare, - "monochromated": monochromated, - "formula": formula, - "min_energy_compare": min_energy_compare, - "max_energy_compare": max_energy_compare, - "per_page": max_n, - "order": order, - "order_direction": order_direction, - } - - if isinstance(element, str): - params["element"] = element - else: - params["element[]"] = element - - request = requests.get('http://api.eelsdb.eu/spectra', params=params, verify=verify_certificate) - spectra = [] - jsons = request.json() - if "message" in jsons: - # Invalid query, EELSdb raises error. - raise IOError( - "Please report the following error to the HyperSpy developers: " - "%s" % jsons["message"]) - for json_spectrum in jsons: - download_link = json_spectrum['download_link'] - msa_string = requests.get(download_link, verify=verify_certificate).text - try: - s = dict2signal(parse_msa_string(msa_string)[0]) - emsa = s.original_metadata - s.original_metadata = s.original_metadata.__class__( - {'json': json_spectrum}) - s.original_metadata.emsa = emsa - spectra.append(s) - - except: - # parse_msa_string or dict2signal may fail if the EMSA file is not - # a valid one. - _logger.exception( - "Failed to load the spectrum.\n" - "Title: %s id: %s.\n" - "Please report this error to http://eelsdb.eu/about \n" % - (json_spectrum["title"], json_spectrum["id"])) - if not spectra: - _logger.info( - "The EELS database does not contain any spectra matching your query" - ". If you have some, why not submitting them " - "https://eelsdb.eu/submit-data/ ?\n") - else: - # Add some info from json to metadata - # Values with units are not yet supported by HyperSpy (v0.8) so - # we can't get map those fields. - for s in spectra: - if spectrum_type == "xrayabs": - s.set_signal_type("XAS") - json_md = s.original_metadata.json - s.metadata.General.title = json_md.title - if s.metadata.Signal.signal_type == "EELS": - if json_md.elements: - try: - s.add_elements(json_md.elements) - except ValueError: - _logger.exception( - "The following spectrum contains invalid chemical " - "element information:\n" - "Title: %s id: %s. Elements: %s.\n" - "Please report this error in " - "http://eelsdb.eu/about \n" % - (json_md.title, json_md.id, json_md.elements)) - if "collection" in json_md and " mrad" in json_md.collection: - beta = float(json_md.collection.replace(" mrad", "")) - s.metadata.set_item( - "Acquisition_instrument.TEM.Detector.EELS.collection_angle", - beta) - if "convergence" in json_md and " mrad" in json_md.convergence: - alpha = float(json_md.convergence.replace(" mrad", "")) - s.metadata.set_item( - "Acquisition_instrument.TEM.convergence_angle", alpha) - if "beamenergy" in json_md and " kV" in json_md.beamenergy: - beam_energy = float(json_md.beamenergy.replace(" kV", "")) - s.metadata.set_item( - "Acquisition_instrument.TEM.beam_energy", beam_energy) - # We don't yet support units, so we cannot map the thickness - # s.metadata.set_item("Sample.thickness", json_md.thickness) - s.metadata.set_item("Sample.description", json_md.description) - s.metadata.set_item("Sample.chemical_formula", json_md.formula) - s.metadata.set_item("General.author", json_md.author.name) - s.metadata.set_item("Acquisition_instrument.TEM.microscope", - json_md.microscope) - - return spectra diff --git a/hyperspy/misc/eels/effective_angle.py b/hyperspy/misc/eels/effective_angle.py deleted file mode 100644 index ee965e52a6..0000000000 --- a/hyperspy/misc/eels/effective_angle.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import math - - -def effective_angle(E0, E, alpha, beta): - """Calculates the effective collection angle - - Parameters - ---------- - E0 : float - electron beam energy in keV - E : float - energy loss in eV - alpha : float - convergence angle in mrad - beta : float - collection angle in mrad - - Returns - ------- - float : effective collection angle in mrad - - Notes - ----- - Code translated to Python from Egerton (second edition) page 420 - - """ - E0 *= 1e3 # keV to eV - if alpha == 0: - return beta - E0 *= 10. ** -3 # In KeV - E = float(E) - alpha = float(alpha) - beta = float(beta) - TGT = E0 * (1. + E0 / 1022.) / (1. + E0 / 511.) - thetaE = E / TGT - A2 = alpha * alpha * 1e-6 - B2 = beta * beta * 1e-6 - T2 = thetaE * thetaE * 1e-6 - eta1 = math.sqrt((A2 + B2 + T2) ** 2 - 4. * A2 * B2) - A2 - B2 - T2 - eta2 = 2. * B2 * \ - math.log( - 0.5 / T2 * (math.sqrt((A2 + T2 - B2) ** 2 + 4. * B2 * T2) + A2 + T2 - B2)) - eta3 = 2. * A2 * \ - math.log( - 0.5 / T2 * (math.sqrt((B2 + T2 - A2) ** 2 + 4. * A2 * T2) + B2 + T2 - A2)) -# ETA=(eta1+eta2+eta3)/A2/math.log(4./T2) - F1 = (eta1 + eta2 + eta3) / 2 / A2 / math.log(1. + B2 / T2) - F2 = F1 - if (alpha / beta) > 1: - F2 = F1 * A2 / B2 - BSTAR = thetaE * math.sqrt(math.exp(F2 * math.log(1. + B2 / T2)) - 1.) - return BSTAR # In mrad diff --git a/hyperspy/misc/eels/electron_inelastic_mean_free_path.py b/hyperspy/misc/eels/electron_inelastic_mean_free_path.py deleted file mode 100644 index b9330a7477..0000000000 --- a/hyperspy/misc/eels/electron_inelastic_mean_free_path.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import math - -def _F(electron_energy): - return (1 + electron_energy / 1022) / (1 + electron_energy / 511) ** 2 - -def _theta_E(density, electron_energy): - return 5.5 * density ** 0.3 / (_F(electron_energy) * electron_energy) - -def iMFP_Iakoubovskii(density, electron_energy): - """Estimate electron inelastic mean free path from density - - Parameters - ---------- - density : float - Material density in g/cm**3 - beam_energy : float - Electron beam energy in keV - - Notes - ----- - For details see Equation 9 in reference [*]_. - - .. [*] Iakoubovskii, K., K. Mitsuishi, Y. Nakayama, and K. Furuya. - ‘Thickness Measurements with Electron Energy Loss Spectroscopy’. - Microscopy Research and Technique 71, no. 8 (2008): 626–31. - https://doi.org/10.1002/jemt.20597 - - Returns - ------- - float - Inelastic mean free path in nanometers - """ - theta_C = 20 # mrad - inv_lambda = 11 * density ** 0.3 / (200 * _F(electron_energy) * electron_energy) * np.log(theta_C ** 2 / _theta_E(density, electron_energy) ** 2) - return 1 / inv_lambda - - -def iMFP_TPP2M(electron_energy, density, M, N_v, E_g): - """Electron inelastic mean free path using TPP-2M - - Parameters - ---------- - electron_energy : float - Electron beam energy in keV - density : float - Material density in g/cm**3 - M : float - Molar mass in g / mol - N_v : int - Number of valence electron - E_g : float - Band gap in eV - - Returns - ------- - float - Inelastic mean free path in nanometers - - Notes - ----- - For details see reference [*]_. - - .. [*] Shinotsuka, H., S. Tanuma, C. J. Powell, and D. R. Penn. ‘Calculations - of Electron Inelastic Mean Free Paths. X. Data for 41 Elemental Solids over - the 50 EV to 200 KeV Range with the Relativistic Full Penn Algorithm: - Calculations of Electron Inelastic Mean Free Paths. X’. Surface and - Interface Analysis 47, no. 9 (September 2015): 871–88. - https://doi.org/10.1002/sia.5789 - """ - E = electron_energy * 1e3 - rho = density - alpha = (1 + E / 1021999.8) / (1 + E / 510998.9)**2 - E_p = 28.816 * math.sqrt(N_v * rho / M) - gamma = 0.191 / math.sqrt(rho) - U = (E_p / 28.816) ** 2 - C = 19.7 - 9.1 * U - D = 534 - 208 * U - beta = -1 + 9.44 / math.sqrt(E_p **2 + E_g**2) + 0.69 * rho ** 0.1 - iMFP = alpha * E / (E_p ** 2 * (beta * math.log(gamma * alpha * E) - C / E + D / E**2)) - return iMFP - -def iMFP_angular_correction(density, beam_energy, alpha, beta): - """Estimate the effect of limited collection angle on EELS mean free path - - Parameters - ---------- - density : float - Material density in g/cm**3 - beam_energy : float - Electron beam energy in keV - alpha, beta : float - Convergence and collection angles in mrad. - - Notes - ----- - For details see Equation 9 in reference [*]_. - - .. [*] Iakoubovskii, K., K. Mitsuishi, Y. Nakayama, and K. Furuya. - ‘Thickness Measurements with Electron Energy Loss Spectroscopy’. - Microscopy Research and Technique 71, no. 8 (2008): 626–31. - https://doi.org/10.1002/jemt.20597 - """ - theta_C = 20 # mrad - A = alpha ** 2 + beta ** 2 + 2 * _theta_E(density, beam_energy) ** 2 + np.abs(alpha ** 2 - beta ** 2) - B = alpha ** 2 + beta ** 2 + 2 * theta_C ** 2 + np.abs(alpha ** 2 - beta ** 2) - return np.log(theta_C ** 2 / _theta_E(density, beam_energy) ** 2) / np.log(A * theta_C ** 2 / B / _theta_E(density, beam_energy) ** 2) - diff --git a/hyperspy/misc/eels/hartree_slater_gos.py b/hyperspy/misc/eels/hartree_slater_gos.py deleted file mode 100644 index 0e1b0ccf56..0000000000 --- a/hyperspy/misc/eels/hartree_slater_gos.py +++ /dev/null @@ -1,191 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import logging -import math -from pathlib import Path - -import numpy as np -from scipy import constants, integrate, interpolate - -from hyperspy.defaults_parser import preferences -from hyperspy.misc.eels.base_gos import GOSBase -from hyperspy.misc.elements import elements -from hyperspy.misc.export_dictionary import ( - export_to_dictionary, load_from_dictionary) - - -_logger = logging.getLogger(__name__) - -R = constants.value("Rydberg constant times hc in eV") -a0 = constants.value("Bohr radius") - - -class HartreeSlaterGOS(GOSBase): - - """Read Hartree-Slater Generalized Oscillator Strenght parametrized - from files. - - Parameters - ---------- - element_subshell : {str, dict} - Usually a string, for example, 'Ti_L3' for the GOS of the titanium L3 - subshell. If a dictionary is passed, it is assumed that Hartree Slater - GOS was exported using `GOS.as_dictionary`, and will be reconstructed. - - Methods - ------- - readgosfile() - Read the GOS files of the element subshell from the location - defined in Preferences. - get_qaxis_and_gos(ienergy, qmin, qmax) - given the energy axis index and qmin and qmax values returns - the qaxis and gos between qmin and qmax using linear - interpolation to include qmin and qmax in the range. - - Attributes - ---------- - energy_axis : array - The tabulated energy axis - qaxis : array - The tabulated qaxis - energy_onset: float - The energy onset for the given element subshell as obtained - from iternal tables. - - """ - - _name = 'Hartree-Slater' - - def __init__(self, element_subshell): - """ - Parameters - ---------- - - element_subshell : str - For example, 'Ti_L3' for the GOS of the titanium L3 subshell - - """ - self._whitelist = {'gos_array': None, - 'rel_energy_axis': None, - 'qaxis': None, - 'element': None, - 'subshell': None - } - if isinstance(element_subshell, dict): - self.element = element_subshell['element'] - self.subshell = element_subshell['subshell'] - self.read_elements() - self._load_dictionary(element_subshell) - else: - self.element, self.subshell = element_subshell.split('_') - self.read_elements() - self.readgosfile() - - def _load_dictionary(self, dictionary): - load_from_dictionary(self, dictionary) - self.energy_axis = self.rel_energy_axis + self.onset_energy - - def as_dictionary(self, fullcopy=True): - """Export the GOS as a dictionary. - """ - dic = {} - export_to_dictionary(self, self._whitelist, dic, fullcopy) - return dic - - def readgosfile(self): - _logger.info( - "Hartree-Slater GOS\n" - f"\tElement: {self.element} " - f"\tSubshell: {self.subshell}" - f"\tOnset Energy = {self.onset_energy}" - ) - element = self.element - subshell = self.subshell - - # Check if the Peter Rez's Hartree Slater GOS distributed by - # Gatan are available. Otherwise exit - gos_root = Path(preferences.EELS.eels_gos_files_path) - gos_file = gos_root.joinpath( - ( - elements[element]["Atomic_properties"]["Binding_energies"][subshell][ - "filename" - ] - ) - ) - - if not gos_root.is_dir(): - raise FileNotFoundError( - "Parametrized Hartree-Slater GOS files not " - f"found in {gos_root}. Please define a valid " - "location for the files in the preferences as " - "`preferences.EELS.eels_gos_files_path`." - ) - - with open(gos_file) as f: - GOS_list = f.read().replace('\r', '').split() - - # Map the parameters - info1_1 = float(GOS_list[2]) - info1_2 = float(GOS_list[3]) - ncol = int(GOS_list[5]) - info2_1 = float(GOS_list[6]) - info2_2 = float(GOS_list[7]) - nrow = int(GOS_list[8]) - self.gos_array = np.array(GOS_list[9:], dtype=np.float64) - # The division by R is not in the equations, but it seems that - # the the GOS was tabulated this way - self.gos_array = self.gos_array.reshape(nrow, ncol) / R - del GOS_list - - # Calculate the scale of the matrix - self.rel_energy_axis = self.get_parametrized_energy_axis( - info2_1, info2_2, nrow) - self.qaxis = self.get_parametrized_qaxis( - info1_1, info1_2, ncol) - self.energy_axis = self.rel_energy_axis + self.onset_energy - - def integrateq(self, onset_energy, angle, E0): - energy_shift = onset_energy - self.onset_energy - self.energy_shift = energy_shift - qint = np.zeros((self.energy_axis.shape[0])) - # Calculate the cross section at each energy position of the - # tabulated GOS - gamma = 1 + E0 / 511.06 - T = 511060 * (1 - 1 / gamma ** 2) / 2 - for i in range(0, self.gos_array.shape[0]): - E = self.energy_axis[i] + energy_shift - # Calculate the limits of the q integral - qa0sqmin = (E ** 2) / (4 * R * T) + (E ** 3) / ( - 8 * gamma ** 3 * R * T ** 2) - p02 = T / (R * (1 - 2 * T / 511060)) - pp2 = p02 - E / R * (gamma - E / 1022120) - qa0sqmax = qa0sqmin + 4 * np.sqrt(p02 * pp2) * \ - (math.sin(angle / 2)) ** 2 - qmin = math.sqrt(qa0sqmin) / a0 - qmax = math.sqrt(qa0sqmax) / a0 - # Perform the integration in a log grid - qaxis, gos = self.get_qaxis_and_gos(i, qmin, qmax) - logsqa0qaxis = np.log((a0 * qaxis) ** 2) - qint[i] = integrate.simps(gos, logsqa0qaxis) - E = self.energy_axis + energy_shift - # Energy differential cross section in (barn/eV/atom) - qint *= (4.0 * np.pi * a0 ** 2.0 * R ** 2 / E / T * - self.subshell_factor) * 1e28 - self.qint = qint - return interpolate.interp1d(E, qint, kind=3) diff --git a/hyperspy/misc/eels/hydrogenic_gos.py b/hyperspy/misc/eels/hydrogenic_gos.py deleted file mode 100644 index 53edb7ed1e..0000000000 --- a/hyperspy/misc/eels/hydrogenic_gos.py +++ /dev/null @@ -1,232 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import math -import logging - -import numpy as np -from scipy import integrate, interpolate, constants - -from hyperspy.misc.eels.base_gos import GOSBase - -_logger = logging.getLogger(__name__) - -XU = [ - .82, .52, .52, .42, .30, .29, .22, .30, .22, .16, .12, .13, .13, .14, .16, - .18, .19, .22, .14, .11, .12, .12, .12, .10, .10, .10] -# IE3=[73,99,135,164,200,245,294,347,402,455,513,575,641,710, -# 779,855,931,1021,1115,1217,1323,1436,1550,1675] - -# IE1=[118,149,189,229,270,320,377,438,500,564,628,695,769,846, -# 926,1008,1096,1194,1142,1248,1359,1476,1596,1727] - -R = constants.value("Rydberg constant times hc in eV") - - -class HydrogenicGOS(GOSBase): - - """Computes the K and L GOS using R. Egerton's routines. - - Parameters - ---------- - element_subshell : str - For example, 'Ti_L3' for the GOS of the titanium L3 subshell - - Methods - ------- - parametrize_GOS() - Parametrize the GOS to speed up the calculation. - get_qaxis_and_gos(ienergy, qmin, qmax) - Given the energy axis index and qmin and qmax values returns - the qaxis and gos between qmin and qmax using linear - interpolation to include qmin and qmax in the range. - - - Attributes - ---------- - energy_axis : array - The tabulated energy axis - qaxis : array - The tabulated qaxis - energy_onset: float - The energy onset for the given element subshell as obtained - from iternal tables. - - Notes - ----- - The Hydrogeninc GOS are calculated using R. Egerton's SIGMAK3 and - SIGMAL3 routines that has been translated from Matlab to Python by - I. Iyengar. See http://www.tem-eels.ca/ for the original code. - - """ - _name = 'hydrogenic' - - def __init__(self, element_subshell): - """ - Parameters - ---------- - - element_subshell : str - For example, 'Ti_L3' for the GOS of the titanium L3 subshell - - """ - # Check if the Peter Rez's Hartree Slater GOS distributed by - # Gatan are available. Otherwise exit - - self.element, self.subshell = element_subshell.split('_') - self.read_elements() - self.energy_shift = 0 - - if self.subshell[:1] == 'K': - self.gosfunc = self.gosfuncK - self.rel_energy_axis = self.get_parametrized_energy_axis( - 50, 3, 50) - elif self.subshell[:1] == 'L': - self.gosfunc = self.gosfuncL - self.onset_energy_L3 = self.element_dict['Atomic_properties'][ - 'Binding_energies']['L3']['onset_energy (eV)'] - self.onset_energy_L1 = self.element_dict['Atomic_properties'][ - 'Binding_energies']['L1']['onset_energy (eV)'] - self.onset_energy = self.onset_energy_L3 - relative_axis = self.get_parametrized_energy_axis( - 50, 3, 50) - dL3L2 = self.onset_energy_L1 - self.onset_energy_L3 - self.rel_energy_axis = np.hstack(( - relative_axis[:relative_axis.searchsorted(dL3L2)], - relative_axis + dL3L2)) - else: - raise ValueError( - 'The Hydrogenic GOS currently can only' - 'compute K or L shells. Try using Hartree-Slater GOS') - - self.energy_axis = self.rel_energy_axis + self.onset_energy - - info_str = ( - "\nHydrogenic GOS\n" + - ("\tElement: %s " % self.element) + - ("\tSubshell: %s " % self.subshell) + - ("\tOnset Energy = %s " % self.onset_energy)) - _logger.info(info_str) - - def integrateq(self, onset_energy, angle, E0): - energy_shift = onset_energy - self.onset_energy - self.energy_shift = energy_shift - gamma = 1 + E0 / 511.06 - T = 511060 * (1 - 1 / gamma ** 2) / 2 - qint = np.zeros((self.energy_axis.shape[0])) - for i, E in enumerate(self.energy_axis + energy_shift): - qa0sqmin = (E ** 2) / (4 * R * T) + (E ** 3) / ( - 8 * gamma ** 3 * R * T ** 2) - p02 = T / (R * (1 - 2 * T / 511060)) - pp2 = p02 - E / R * (gamma - E / 1022120) - qa0sqmax = qa0sqmin + 4 * np.sqrt(p02 * pp2) * \ - (math.sin(angle / 2)) ** 2 - - # dsbyde IS THE ENERGY-DIFFERENTIAL X-SECN (barn/eV/atom) - qint[i] = 3.5166e8 * (R / T) * (R / E) * ( - integrate.quad( - lambda x: self.gosfunc(E, np.exp(x)), - math.log(qa0sqmin), math.log(qa0sqmax))[0]) - self.qint = qint - return interpolate.interp1d(self.energy_axis + energy_shift, qint) - - def gosfuncK(self, E, qa02): - # gosfunc calculates (=DF/DE) which IS PER EV AND PER ATOM - z = self.Z - r = 13.606 - zs = 1.0 - rnk = 1 - if z != 1: - zs = z - 0.5 - rnk = 2 - - q = qa02 / zs ** 2 - kh2 = E / (r * zs ** 2) - 1 - akh = np.sqrt(np.abs(kh2)) - if akh < 0.01: - akh = 0.01 - if kh2 >= 0.0: - d = 1 - np.e ** (-2 * np.pi / kh2) - bp = np.arctan(2 * akh / (q - kh2 + 1)) - if bp < 0: - bp = bp + np.pi - c = np.e ** ((-2 / akh) * bp) - else: - d = 1 - y = -1 / akh * np.log((q + 1 - kh2 + 2 * akh) / ( - q + 1 - kh2 - 2 * akh)) - c = np.e ** y - a = ((q - kh2 + 1) ** 2 + 4 * kh2) ** 3 - return 128 * rnk * E / ( - r * zs ** 4) * c / d * (q + kh2 / 3 + 1 / 3) / (a * r) - - def gosfuncL(self, E, qa02): - # gosfunc calculates (=DF/DE) which IS PER EV AND PER ATOM - # Note: quad function only works with qa02 due to IF statements in - # function - - z = self.Z - r = 13.606 - zs = z - 0.35 * (8 - 1) - 1.7 - iz = z - 11 - if iz >= len(XU): - # Egerton does not tabulate the correction for Z>36. - # This produces XSs that are within 10% of Hartree-Slater XSs - # for these elements. - u = .1 - else: - # Egerton's correction to the Hydrogenic XS - u = XU[int(iz)] - el3 = self.onset_energy_L3 + self.energy_shift - el1 = self.onset_energy_L1 + self.energy_shift - - q = qa02 / zs ** 2 - kh2 = E / (r * zs ** 2) - 0.25 - akh = np.sqrt(np.abs(kh2)) - if kh2 >= 0.0: - d = 1 - np.exp(-2 * np.pi / akh) - bp = np.arctan(akh / (q - kh2 + 0.25)) - if bp < 0: - bp = bp + np.pi - c = np.exp((-2 / akh) * bp) - else: - d = 1 - y = -1 / akh * \ - np.log((q + 0.25 - kh2 + akh) / (q + 0.25 - kh2 - akh)) - c = np.exp(y) - - if E - el1 <= 0: - g = 2.25 * q ** 4 - (0.75 + 3 * kh2) * q ** 3 + ( - 0.59375 - 0.75 * kh2 - 0.5 * kh2 ** 2) * q * q + ( - 0.11146 + 0.85417 * kh2 + 1.8833 * kh2 * kh2 + kh2 ** 3) * \ - q + 0.0035807 + kh2 / 21.333 + kh2 * kh2 / 4.5714 + kh2 ** 3 \ - / 2.4 + kh2 ** 4 / 4 - - a = ((q - kh2 + 0.25) ** 2 + kh2) ** 5 - else: - g = q ** 3 - (5 / 3 * kh2 + 11 / 12) * q ** 2 + ( - kh2 * kh2 / 3 + 1.5 * kh2 + 65 / 48) * q + kh2 ** 3 / 3 + \ - 0.75 * kh2 * kh2 + 23 / 48 * kh2 + 5 / 64 - a = ((q - kh2 + 0.25) ** 2 + kh2) ** 4 - rf = ((E + 0.1 - el3) / 1.8 / z / z) ** u - # The following commented lines are to give a more accurate GOS - # for edges presenting white lines. However, this is not relevant - # for quantification by curve fitting. - # if np.abs(iz - 11) <= 5 and E - el3 <= 20: - # rf = 1 - return rf * 32 * g * c / a / d * E / r / r / zs ** 4 diff --git a/hyperspy/misc/eels/tools.py b/hyperspy/misc/eels/tools.py deleted file mode 100644 index ecdfae2350..0000000000 --- a/hyperspy/misc/eels/tools.py +++ /dev/null @@ -1,416 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import math -import numbers -import logging - -import numpy as np -import matplotlib.pyplot as plt -from scipy import constants - -from hyperspy.misc.array_tools import rebin -from hyperspy.misc.elements import elements as elements_db -import hyperspy.defaults_parser - -_logger = logging.getLogger(__name__) - - -def _estimate_gain(ns, cs, - weighted=False, - higher_than=None, - plot_results=False, - binning=0, - pol_order=1): - if binning > 0: - factor = 2 ** binning - remainder = np.mod(ns.shape[1], factor) - if remainder != 0: - ns = ns[:, remainder:] - cs = cs[:, remainder:] - new_shape = (ns.shape[0], ns.shape[1] / factor) - ns = rebin(ns, new_shape) - cs = rebin(cs, new_shape) - - noise = ns - cs - variance = np.var(noise, 0) - average = np.mean(cs, 0).squeeze() - - # Select only the values higher_than for the calculation - if higher_than is not None: - sorting_index_array = np.argsort(average) - average_sorted = average[sorting_index_array] - average_higher_than = average_sorted > higher_than - variance_sorted = variance.squeeze()[sorting_index_array] - variance2fit = variance_sorted[average_higher_than] - average2fit = average_sorted[average_higher_than] - else: - variance2fit = variance - average2fit = average - - fit = np.polyfit(average2fit, variance2fit, pol_order) - if weighted is True: - from hyperspy._signals.signal1D import Signal1D - from hyperspy.models.model1d import Model1D - from hyperspy.components1d import Line - s = Signal1D(variance2fit) - s.axes_manager.signal_axes[0].axis = average2fit - m = Model1D(s) - l = Line() - l.a.value = fit[1] - l.b.value = fit[0] - m.append(l) - m.fit(weights=True) - fit[0] = l.b.value - fit[1] = l.a.value - - if plot_results is True: - plt.figure() - plt.scatter(average.squeeze(), variance.squeeze()) - plt.xlabel('Counts') - plt.ylabel('Variance') - plt.plot(average2fit, np.polyval(fit, average2fit), color='red') - results = {'fit': fit, 'variance': variance.squeeze(), - 'counts': average.squeeze()} - - return results - - -def _estimate_correlation_factor(g0, gk, k): - a = math.sqrt(g0 / gk) - e = k * (a - 1) / (a - k) - c = (1 - e) ** 2 - return c - - -def estimate_variance_parameters( - noisy_signal, - clean_signal, - mask=None, - pol_order=1, - higher_than=None, - return_results=False, - plot_results=True, - weighted=False, - store_results="ask"): - """Find the scale and offset of the Poissonian noise - - By comparing an SI with its denoised version (i.e. by PCA), - this plots an - estimation of the variance as a function of the number of counts - and fits a - polynomy to the result. - - Parameters - ---------- - noisy_SI, clean_SI : signal1D.Signal1D instances - mask : numpy bool array - To define the channels that will be used in the calculation. - pol_order : int - The order of the polynomy. - higher_than: float - To restrict the fit to counts over the given value. - return_results : Bool - plot_results : Bool - store_results: {True, False, "ask"}, default "ask" - If True, it stores the result in the signal metadata - - Returns - ------- - Dictionary with the result of a linear fit to estimate the offset - and scale factor - - """ - with noisy_signal.unfolded(), clean_signal.unfolded(): - # The rest of the code assumes that the first data axis - # is the navigation axis. We transpose the data if that is not the - # case. - ns = (noisy_signal.data.copy() - if noisy_signal.axes_manager[0].index_in_array == 0 - else noisy_signal.data.T.copy()) - cs = (clean_signal.data.copy() - if clean_signal.axes_manager[0].index_in_array == 0 - else clean_signal.data.T.copy()) - - if mask is not None: - _slice = [slice(None), ] * len(ns.shape) - _slice[noisy_signal.axes_manager.signal_axes[0].index_in_array]\ - = ~mask - ns = ns[_slice] - cs = cs[_slice] - - results0 = _estimate_gain( - ns, cs, weighted=weighted, higher_than=higher_than, - plot_results=plot_results, binning=0, pol_order=pol_order) - - results2 = _estimate_gain( - ns, cs, weighted=weighted, higher_than=higher_than, - plot_results=False, binning=2, pol_order=pol_order) - - c = _estimate_correlation_factor(results0['fit'][0], - results2['fit'][0], 4) - - message = ("Gain factor: %.2f\n" % results0['fit'][0] + - "Gain offset: %.2f\n" % results0['fit'][1] + - "Correlation factor: %.2f\n" % c) - if store_results == "ask": - is_ok = "" - while is_ok not in ("Yes", "No"): - is_ok = input( - message + - "Would you like to store the results (Yes/No)?") - is_ok = is_ok == "Yes" - else: - is_ok = store_results - _logger.info(message) - if is_ok: - noisy_signal.metadata.set_item( - "Signal.Noise_properties.Variance_linear_model.gain_factor", - results0['fit'][0]) - noisy_signal.metadata.set_item( - "Signal.Noise_properties.Variance_linear_model.gain_offset", - results0['fit'][1]) - noisy_signal.metadata.set_item( - "Signal.Noise_properties.Variance_linear_model." - "correlation_factor", - c) - noisy_signal.metadata.set_item( - "Signal.Noise_properties.Variance_linear_model." + - "parameters_estimation_method", - 'HyperSpy') - - if return_results is True: - return results0 - - -def power_law_perc_area(E1, E2, r): - a = E1 - b = E2 - return 100 * ((a ** r * r - a ** r) * (a / (a ** r * r - a ** r) - - (b + a) / ((b + a) ** r * r - (b + a) ** r))) / a - - -def rel_std_of_fraction(a, std_a, b, std_b, corr_factor=1): - rel_a = std_a / a - rel_b = std_b / b - return np.sqrt(rel_a ** 2 + rel_b ** 2 - - 2 * rel_a * rel_b * corr_factor) - - -def ratio(edge_A, edge_B): - a = edge_A.intensity.value - std_a = edge_A.intensity.std - b = edge_B.intensity.value - std_b = edge_B.intensity.std - ratio = a / b - ratio_std = ratio * rel_std_of_fraction(a, std_a, b, std_b) - _logger.info("Ratio %s/%s %1.3f +- %1.3f ", - edge_A.name, - edge_B.name, - a / b, - 1.96 * ratio_std) - return ratio, ratio_std - - -def eels_constant(s, zlp, t): - r"""Calculate the constant of proportionality (k) in the relationship - between the EELS signal and the dielectric function. - dielectric function from a single scattering distribution (SSD) using - the Kramers-Kronig relations. - - .. math:: - - S(E)=\frac{I_{0}t}{\pi a_{0}m_{0}v^{2}}\ln\left[1+\left(\frac{\beta} - {\theta_{E}}\right)^{2}\right]\Im(\frac{-1}{\epsilon(E)})= - k\Im(\frac{-1}{\epsilon(E)}) - - - Parameters - ---------- - zlp: {number, BaseSignal} - If the ZLP is the same for all spectra, the intengral of the ZLP - can be provided as a number. Otherwise, if the ZLP intensity is not - the same for all spectra, it can be provided as i) a Signal - of the same dimensions as the current signal containing the ZLP - spectra for each location ii) a Signal of signal dimension 0 - and navigation_dimension equal to the current signal containing the - integrated ZLP intensity. - t: {None, number, BaseSignal} - The sample thickness in nm. If the thickness is the same for all - spectra it can be given by a number. Otherwise, it can be provided - as a Signal with signal dimension 0 and navigation_dimension equal - to the current signal. - - Returns - ------- - k: Signal instance - - """ - - # Constants and units - me = constants.value( - 'electron mass energy equivalent in MeV') * 1e3 # keV - - # Mapped parameters - try: - e0 = s.metadata.Acquisition_instrument.TEM.beam_energy - except BaseException: - raise AttributeError("Please define the beam energy." - "You can do this e.g. by using the " - "set_microscope_parameters method") - try: - beta = s.metadata.Acquisition_instrument.\ - TEM.Detector.EELS.collection_angle - except BaseException: - raise AttributeError("Please define the collection semi-angle." - "You can do this e.g. by using the " - "set_microscope_parameters method") - - axis = s.axes_manager.signal_axes[0] - eaxis = axis.axis.copy() - if eaxis[0] == 0: - # Avoid singularity at E=0 - eaxis[0] = 1e-10 - - if isinstance(zlp, hyperspy.signal.BaseSignal): - if (zlp.axes_manager.navigation_dimension == - s.axes_manager.navigation_dimension): - if zlp.axes_manager.signal_dimension == 0: - i0 = zlp.data - else: - i0 = zlp.integrate1D(axis.index_in_axes_manager).data - else: - raise ValueError('The ZLP signal dimensions are not ' - 'compatible with the dimensions of the ' - 'low-loss signal') - # The following prevents errors if the signal is a single spectrum - if len(i0) != 1: - i0 = i0.reshape( - np.insert(i0.shape, axis.index_in_array, 1)) - elif isinstance(zlp, numbers.Number): - i0 = zlp - else: - raise ValueError('The zero-loss peak input is not valid, it must be\ - in the BaseSignal class or a Number.') - - if isinstance(t, hyperspy.signal.BaseSignal): - if (t.axes_manager.navigation_dimension == - s.axes_manager.navigation_dimension) and ( - t.axes_manager.signal_dimension == 0): - t = t.data - t = t.reshape( - np.insert(t.shape, axis.index_in_array, 1)) - else: - raise ValueError('The thickness signal dimensions are not ' - 'compatible with the dimensions of the ' - 'low-loss signal') - - # Kinetic definitions - ke = e0 * (1 + e0 / 2. / me) / (1 + e0 / me) ** 2 - tgt = e0 * (2 * me + e0) / (me + e0) - k = s.__class__( - data=(t * i0 / (332.5 * ke)) * np.log(1 + (beta * tgt / eaxis) ** 2)) - k.metadata.General.title = "EELS proportionality constant K" - return k - -def get_edges_near_energy(energy, width=10, only_major=False, order='closest'): - """Find edges near a given energy that are within the given energy - window. - - Parameters - ---------- - energy : float - Energy to search, in eV - width : float - Width of window, in eV, around energy in which to find nearby - energies, i.e. a value of 10 eV (the default) means to - search +/- 5 eV. The default is 10. - only_major : bool - Whether to show only the major edges. The default is False. - order : str - Sort the edges, if 'closest', return in the order of energy difference, - if 'ascending', return in ascending order, similarly for 'descending' - - Returns - ------- - edges : list - All edges that are within the given energy window, sorted by - energy difference to the given energy. - """ - - if width < 0: - raise ValueError("Provided width needs to be >= 0.") - if order not in ('closest', 'ascending', 'descending'): - raise ValueError("order needs to be 'closest', 'ascending' or " - "'descending'") - - Emin, Emax = energy - width/2, energy + width/2 - - # find all subshells that have its energy within range - valid_edges = [] - for element, element_info in elements_db.items(): - try: - for shell, shell_info in element_info[ - 'Atomic_properties']['Binding_energies'].items(): - if only_major: - if shell_info['relevance'] != 'Major': - continue - if shell[-1] != 'a' and \ - Emin <= shell_info['onset_energy (eV)'] <= Emax: - subshell = '{}_{}'.format(element, shell) - Ediff = np.abs(shell_info['onset_energy (eV)'] - energy) - valid_edges.append((subshell, - shell_info['onset_energy (eV)'], - Ediff)) - except KeyError: - continue - - # Sort according to 'order' and return only the edges - if order == 'closest': - edges = [edge for edge, _, _ in sorted(valid_edges, key=lambda x: x[2])] - elif order == 'ascending': - edges = [edge for edge, _, _ in sorted(valid_edges, key=lambda x: x[1])] - elif order == 'descending': - edges = [edge for edge, _, _ in sorted(valid_edges, key=lambda x: x[1], - reverse=True)] - - return edges - -def get_info_from_edges(edges): - """Return the information of a sequence of edges as a list of dictionaries - - Parameters - ---------- - edges : str or iterable - the sequence of edges, each entry in the format of 'element_subshell'. - - Returns - ------- - info : list - a list of dictionaries with information corresponding to the provided - edges. - """ - - edges = np.atleast_1d(edges) - info = [] - for edge in edges: - element, subshell = edge.split('_') - d = elements_db[element]['Atomic_properties']['Binding_energies'][subshell] - info.append(d) - - return info diff --git a/hyperspy/misc/elements.py b/hyperspy/misc/elements.py deleted file mode 100644 index e0ddcc1a84..0000000000 --- a/hyperspy/misc/elements.py +++ /dev/null @@ -1,4247 +0,0 @@ -# Database -# -# The X-ray lines energies are taken from Chantler2005, -# Chantler, C.T., Olsen, K., Dragoset, R.A., Kishore, A.R., -# Kotochigova, S.A., and Zucker, D.S. -# -# The line weight, more precisely the approximate line weight for K,L M -# shells are taken from epq library -# -# The field 'threshold' and 'edge' are taken from Gatan EELS atlas -# https://eels.info/atlas (retrieved in June 2020) - -from hyperspy.misc import utils - -elements = {'Ru': {'Physical_properties': {'density (g/cm^3)': 12.37}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.33039, - 'energy (keV)': 2.6833}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 21.6566}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 19.2793}, - 'Lb2': {'weight': 0.07259, - 'energy (keV)': 2.8359}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.5585}, - 'Ln': {'weight': 0.0126, - 'energy (keV)': 2.3819}, - 'Ll': {'weight': 0.0411, - 'energy (keV)': 2.2529}, - 'Lb3': {'weight': 0.0654, - 'energy (keV)': 2.7634}, - 'Lg3': {'weight': 0.0115, - 'energy (keV)': 3.1809}, - 'Lg1': {'weight': 0.02176, - 'energy (keV)': 2.9649}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 279.0, - 'filename': 'Ru.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 279.0, - 'filename': 'Ru.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 461.0, - 'filename': 'Ru.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 483.0, - 'filename': 'Ru.M3'}}}, - 'General_properties': {'Z': 44, - 'atomic_weight': 101.07, - 'name': 'ruthenium'}}, - 'Re': {'Physical_properties': {'density (g/cm^3)': 21.02}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4408, - 'energy (keV)': 10.0098}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 69.3091}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 61.1411}, - 'M2N4': {'weight': 0.01, - 'energy (keV)': 2.4079}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.8423}, - 'Lb4': {'weight': 0.09869, - 'energy (keV)': 9.8451}, - 'La': {'weight': 1.0, - 'energy (keV)': 8.6524}, - 'Ln': {'weight': 0.0151, - 'energy (keV)': 9.027}, - 'M3O4': {'energy (keV)': 2.36124, - 'weight': 0.001}, - 'Ll': {'weight': 0.05299, - 'energy (keV)': 7.6036}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.9083}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 2.1071}, - 'Lb2': {'weight': 0.21219, - 'energy (keV)': 10.2751}, - 'Lb3': {'weight': 0.1222, - 'energy (keV)': 10.1594}, - 'M3O5': {'energy (keV)': 2.36209, - 'weight': 0.01}, - 'Lg3': {'weight': 0.0331, - 'energy (keV)': 12.0823}, - 'Lg1': {'weight': 0.08864, - 'energy (keV)': 11.685}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.4385}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1883.0, - 'filename': 'Re.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1949.0, - 'filename': 'Re.M5'}}}, - 'General_properties': {'Z': 75, - 'atomic_weight': 186.207, - 'name': 'rhenium'}}, - 'Ra': {'Physical_properties': {'density (g/cm^3)': 5.0}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4, - 'energy (keV)': 15.2359}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 100.1302}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 88.4776}, - 'M2N4': {'weight': 0.00674, - 'energy (keV)': 3.8536}, - 'Lb4': {'weight': 0.06209, - 'energy (keV)': 14.7472}, - 'La': {'weight': 1.0, - 'energy (keV)': 12.3395}, - 'Ln': {'weight': 0.0133, - 'energy (keV)': 13.6623}, - 'Ll': {'weight': 0.06429, - 'energy (keV)': 10.6224}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.9495}, - 'Mg': {'weight': 0.33505, - 'energy (keV)': 3.1891}, - 'Lb2': {'weight': 0.23579, - 'energy (keV)': 14.8417}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 18.3576}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 17.8484}, - 'Lb3': {'weight': 0.06, - 'energy (keV)': 15.4449}, - 'Mz': {'weight': 0.03512, - 'energy (keV)': 2.2258}}}, - 'General_properties': {'Z': 88, - 'atomic_weight': 226, - 'name': 'radium'}}, - 'Rb': {'Physical_properties': {'density (g/cm^3)': 1.532}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.39095, - 'energy (keV)': 1.7521}, - 'Kb': {'weight': 0.1558, - 'energy (keV)': 14.9612}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 13.3953}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.6941}, - 'Ln': {'weight': 0.01709, - 'energy (keV)': 1.5418}, - 'Ll': {'weight': 0.0441, - 'energy (keV)': 1.4823}, - 'Lb3': {'weight': 0.04709, - 'energy (keV)': 1.8266}, - 'Lg3': {'weight': 0.0058, - 'energy (keV)': 2.0651}}, - 'Binding_energies': {'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 247.0, - 'filename': 'Rb.M3'}, - 'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 110.0, - 'filename': 'Rb.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 110.0, - 'filename': 'Rb.M5'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1864.0, - 'filename': 'Rb.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1804.0, - 'filename': 'Rb.L3'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 238.0, - 'filename': 'Rb.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 2065.0, - 'filename': 'Rb.L1'}}}, - 'General_properties': {'Z': 37, - 'atomic_weight': 85.4678, - 'name': 'rubidium'}}, - 'Rn': {'Physical_properties': {'density (g/cm^3)': 0.00973}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.38463, - 'energy (keV)': 14.3156}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 94.866}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 83.7846}, - 'M2N4': {'weight': 0.00863, - 'energy (keV)': 3.5924}, - 'Lb4': {'weight': 0.06, - 'energy (keV)': 13.89}, - 'La': {'weight': 1.0, - 'energy (keV)': 11.727}, - 'Ln': {'weight': 0.0134, - 'energy (keV)': 12.8551}, - 'Ll': {'weight': 0.0625, - 'energy (keV)': 10.1374}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.80187}, - 'Mg': {'weight': 0.21845, - 'energy (keV)': 3.001}, - 'Lb2': {'weight': 0.2325, - 'energy (keV)': 14.0824}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 17.281}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 16.7705}, - 'Lb3': {'weight': 0.0607, - 'energy (keV)': 14.511}, - 'Mz': {'weight': 0.0058, - 'energy (keV)': 2.1244}}}, - 'General_properties': {'Z': 86, - 'atomic_weight': 222, - 'name': 'radon'}}, - 'Rh': {'Physical_properties': {'density (g/cm^3)': 12.45}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.33463, - 'energy (keV)': 2.8344}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 22.7237}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 20.2161}, - 'Lb2': {'weight': 0.08539, - 'energy (keV)': 3.0013}, - 'Lb4': {'weight': 0.0395, - 'energy (keV)': 2.8909}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.6968}, - 'Ln': {'weight': 0.0126, - 'energy (keV)': 2.519}, - 'Ll': {'weight': 0.0411, - 'energy (keV)': 2.3767}, - 'Lb3': {'weight': 0.06669, - 'energy (keV)': 2.9157}, - 'Lg3': {'weight': 0.0121, - 'energy (keV)': 3.364}, - 'Lg1': {'weight': 0.02623, - 'energy (keV)': 3.1436}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 308.0, - 'filename': 'Rh.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 312.0, - 'filename': 'Rh.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 496.0, - 'filename': 'Rh.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 521.0, - 'filename': 'Rh.M3'}}}, - 'General_properties': {'Z': 45, - 'atomic_weight': 102.9055, - 'name': 'rhodium'}}, - 'H': {'Physical_properties': {'density (g/cm^3)': 8.99e-5}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.0013598}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 13.598, - 'filename': ''}}}, - 'General_properties': {'Z': 1, - 'atomic_weight': 1.00794, - 'name': 'hydrogen'}}, - 'He': {'Physical_properties': {'density (g/cm^3)': 1.785e-4}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.0024587}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 24.587, - 'filename': ''}}}, - 'General_properties': {'Z': 2, - 'atomic_weight': 4.002602, - 'name': 'helium'}}, - 'Be': {'Physical_properties': {'density (g/cm^3)': 1.848}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.10258}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 111.0, - 'filename': 'Be.K1'}}}, - 'General_properties': {'Z': 4, - 'atomic_weight': 9.012182, - 'name': 'beryllium'}}, - 'Ba': {'Physical_properties': {'density (g/cm^3)': 3.51}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43048, - 'energy (keV)': 4.8275}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 36.3784}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 32.1936}, - 'Lb2': {'weight': 0.1905, - 'energy (keV)': 5.1571}, - 'Lb4': {'weight': 0.08859, - 'energy (keV)': 4.8521}, - 'La': {'weight': 1.0, - 'energy (keV)': 4.4663}, - 'Ln': {'weight': 0.0151, - 'energy (keV)': 4.3308}, - 'Ll': {'weight': 0.04299, - 'energy (keV)': 3.9542}, - 'Lb3': {'weight': 0.13779, - 'energy (keV)': 4.9266}, - 'Lg3': {'weight': 0.0331, - 'energy (keV)': 5.8091}, - 'Lg1': {'weight': 0.07487, - 'energy (keV)': 5.5311}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 781.0, - 'filename': 'Ba.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 796.0, - 'filename': 'Ba.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1062.0, - 'filename': 'Ba.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1137.0, - 'filename': 'Ba.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 90.0, - 'filename': 'Ba.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 90.0, - 'filename': 'Ba.N5'}}}, - 'General_properties': {'Z': 56, - 'atomic_weight': 137.327, - 'name': 'barium'}}, - 'Bi': {'Physical_properties': {'density (g/cm^3)': 9.78}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4, - 'energy (keV)': 13.0235}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 87.349}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 77.1073}, - 'M2N4': {'weight': 0.00863, - 'energy (keV)': 3.2327}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 2.4222}, - 'Lb4': {'weight': 0.05639, - 'energy (keV)': 12.6912}, - 'La': {'weight': 1.0, - 'energy (keV)': 10.839}, - 'Ln': {'weight': 0.0134, - 'energy (keV)': 11.712}, - 'M3O4': {'energy (keV)': 3.1504, - 'weight': 0.01}, - 'Ll': {'weight': 0.06, - 'energy (keV)': 9.4195}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.5257}, - 'Mg': {'weight': 0.21845, - 'energy (keV)': 2.7369}, - 'Lb2': {'weight': 0.2278, - 'energy (keV)': 12.9786}, - 'Lb3': {'weight': 0.0607, - 'energy (keV)': 13.2106}, - 'M3O5': {'energy (keV)': 3.1525, - 'weight': 0.01}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 15.7086}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 15.2475}, - 'Mz': {'weight': 0.0058, - 'energy (keV)': 1.9007}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2580.0, - 'filename': 'Bi.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2688.0, - 'filename': 'Bi.M5'}}}, - 'General_properties': {'Z': 83, - 'atomic_weight': 208.9804, - 'name': 'bismuth'}}, - 'Br': {'Physical_properties': {'density (g/cm^3)': 3.12}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.39175, - 'energy (keV)': 1.5259}, - 'Kb': {'weight': 0.15289, - 'energy (keV)': 13.2922}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 11.9238}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.4809}, - 'Ln': {'weight': 0.0182, - 'energy (keV)': 1.3395}, - 'Ll': {'weight': 0.0462, - 'energy (keV)': 1.2934}, - 'Lb3': {'weight': 0.04629, - 'energy (keV)': 1.6005}}, - 'Binding_energies': {'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1596.0, - 'filename': 'Br.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1550.0, - 'filename': 'Br.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1782.0, - 'filename': 'Br.L1'}}}, - 'General_properties': {'Z': 35, - 'atomic_weight': 79.904, - 'name': 'bromine'}}, - 'P': {'Physical_properties': {'density (g/cm^3)': 1.823}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.0498, - 'energy (keV)': 2.13916}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 2.0133}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 2146.0, - 'filename': 'P.K1'}, - 'L3': {'relevance': 'Major', - # overlaps - # with L2 - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1.5, - 'onset_energy (eV)': 132.0, - 'filename': 'P.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 189.0, - 'filename': 'P.L1'}}}, - 'General_properties': {'Z': 15, - 'atomic_weight': 30.973762, - 'name': 'phosphorus'}}, - 'Os': {'Physical_properties': {'density (g/cm^3)': 22.59}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43207, - 'energy (keV)': 10.3542}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 71.4136}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 62.9999}, - 'M2N4': {'weight': 0.02901, - 'energy (keV)': 2.5028}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.9138}, - 'Lb4': {'weight': 0.08369, - 'energy (keV)': 10.1758}, - 'La': {'weight': 1.0, - 'energy (keV)': 8.9108}, - 'Ln': {'weight': 0.01479, - 'energy (keV)': 9.3365}, - 'M3O4': {'energy (keV)': 2.45015, - 'weight': 0.005}, - 'Ll': {'weight': 0.05389, - 'energy (keV)': 7.8224}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.9845}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 2.1844}, - 'Lb2': {'weight': 0.2146, - 'energy (keV)': 10.5981}, - 'Lb3': {'weight': 0.1024, - 'energy (keV)': 10.5108}, - 'M3O5': {'energy (keV)': 2.45117, - 'weight': 0.01}, - 'Lg3': {'weight': 0.028, - 'energy (keV)': 12.4998}, - 'Lg1': {'weight': 0.08768, - 'energy (keV)': 12.0956}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.4919}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1960.0, - 'filename': 'Os.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2031.0, - 'filename': 'Os.M5'}}}, - 'General_properties': {'Z': 76, - 'atomic_weight': 190.23, - 'name': 'osmium'}}, - 'Ge': {'Physical_properties': {'density (g/cm^3)': 5.323}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.16704, - 'energy (keV)': 1.2191}, - 'Kb': {'weight': 0.1322, - 'energy (keV)': 10.9823}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 9.8864}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.188}, - 'Ln': {'weight': 0.02, - 'energy (keV)': 1.0678}, - 'Ll': {'weight': 0.0511, - 'energy (keV)': 1.0367}, - 'Lb3': {'weight': 0.04429, - 'energy (keV)': 1.2935}}, - 'Binding_energies': {'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1248.0, - 'filename': 'Ge.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1217.0, - 'filename': 'Ge.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1414.0, - 'filename': 'Ge.L1'}}}, - 'General_properties': {'Z': 32, - 'atomic_weight': 72.64, - 'name': 'germanium'}}, - 'Gd': {'Physical_properties': {'density (g/cm^3)': 7.901}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.44127, - 'energy (keV)': 6.7131}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 48.6951}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 42.9963}, - 'M2N4': {'weight': 0.014, - 'energy (keV)': 1.5478}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.17668}, - 'Lb4': {'weight': 0.08789, - 'energy (keV)': 6.6873}, - 'La': {'weight': 1.0, - 'energy (keV)': 6.0576}, - 'Ln': {'weight': 0.01489, - 'energy (keV)': 6.0495}, - 'Ll': {'weight': 0.04629, - 'energy (keV)': 5.362}, - 'Mb': {'weight': 0.88, - 'energy (keV)': 1.20792}, - 'Mg': {'weight': 0.261, - 'energy (keV)': 1.4035}, - 'Lb2': {'weight': 0.2014, - 'energy (keV)': 7.1023}, - 'Lb3': {'weight': 0.1255, - 'energy (keV)': 6.8316}, - 'Lg3': {'weight': 0.032, - 'energy (keV)': 8.1047}, - 'Lg1': {'weight': 0.08207, - 'energy (keV)': 7.7898}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 0.9143}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1185.0, - 'filename': 'Gd.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1217.0, - 'filename': 'Gd.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1544.0, - 'filename': 'Gd.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1688.0, - 'filename': 'Gd.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 141.0, - 'filename': 'Gd.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 141.0, - 'filename': 'Gd.N5'}}}, - 'General_properties': {'Z': 64, - 'atomic_weight': 157.25, - 'name': 'gadolinium'}}, - 'Ga': {'Physical_properties': {'density (g/cm^3)': 5.904}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.16704, - 'energy (keV)': 1.1249}, - 'Kb': {'weight': 0.1287, - 'energy (keV)': 10.2642}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 9.2517}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.098}, - 'Ln': {'weight': 0.02509, - 'energy (keV)': 0.9842}, - 'Ll': {'weight': 0.0544, - 'energy (keV)': 0.9573}, - 'Lb3': {'weight': 0.0461, - 'energy (keV)': 1.1948}}, - 'Binding_energies': {'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1142.0, - 'filename': 'Ga.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1115.0, - 'filename': 'Ga.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1298.0, - 'filename': 'Ga.L1'}}}, - 'General_properties': {'Z': 31, - 'atomic_weight': 69.723, - 'name': 'gallium'}}, - 'Pr': {'Physical_properties': {'density (g/cm^3)': 6.64}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.42872, - 'energy (keV)': 5.4893}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 40.7484}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 36.0263}, - 'M2N4': {'weight': 0.055, - 'energy (keV)': 1.2242}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 0.8936}, - 'Lb4': {'weight': 0.0864, - 'energy (keV)': 5.4974}, - 'La': {'weight': 1.0, - 'energy (keV)': 5.0333}, - 'Ln': {'weight': 0.01489, - 'energy (keV)': 4.9294}, - 'Ll': {'weight': 0.044, - 'energy (keV)': 4.4533}, - 'Mb': {'weight': 0.85, - 'energy (keV)': 0.9476}, - 'Mg': {'weight': 0.6, - 'energy (keV)': 1.129}, - 'Lb2': {'weight': 0.19519, - 'energy (keV)': 5.8511}, - 'Lb3': {'weight': 0.13089, - 'energy (keV)': 5.5926}, - 'Lg3': {'weight': 0.0321, - 'energy (keV)': 6.6172}, - 'Lg1': {'weight': 0.07687, - 'energy (keV)': 6.3272}, - 'Mz': {'weight': 0.068, - 'energy (keV)': 0.7134}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 931.0, - 'filename': 'Pr.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 951.0, - 'filename': 'Pr.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1242.0, - 'filename': 'Pr.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1337.0, - 'filename': 'Pr.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 114.0, - 'filename': 'Pr.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 114.0, - 'filename': 'Pr.N5'}}}, - 'General_properties': {'Z': 59, - 'atomic_weight': 140.90765, - 'name': 'praseodymium'}}, - 'Pt': {'Physical_properties': {'density (g/cm^3)': 21.09}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4088, - 'energy (keV)': 11.0707}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 75.7494}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 66.8311}, - 'M2N4': {'weight': 0.02901, - 'energy (keV)': 2.6957}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 2.0505}, - 'Lb4': {'weight': 0.0662, - 'energy (keV)': 10.8534}, - 'La': {'weight': 1.0, - 'energy (keV)': 9.4421}, - 'Ln': {'weight': 0.01399, - 'energy (keV)': 9.9766}, - 'M3O4': {'energy (keV)': 2.63796, - 'weight': 0.005}, - 'Ll': {'weight': 0.0554, - 'energy (keV)': 8.2677}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 2.1276}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 2.3321}, - 'Lb2': {'weight': 0.21829, - 'energy (keV)': 11.2504}, - 'Lb3': {'weight': 0.0783, - 'energy (keV)': 11.2345}, - 'M3O5': {'energy (keV)': 2.63927, - 'weight': 0.01}, - 'Lg3': {'weight': 0.0218, - 'energy (keV)': 13.3609}, - 'Lg1': {'weight': 0.08448, - 'energy (keV)': 12.9418}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.6026}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2122.0, - 'filename': 'Pt.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2202.0, - 'filename': 'Pt.M5'}}}, - 'General_properties': {'Z': 78, - 'atomic_weight': 195.084, - 'name': 'platinum'}}, - 'C': {'Physical_properties': {'density (g/cm^3)': 2.26}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.2774}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 284.0, - 'filename': 'C.K1'}, - 'K1a': {'relevance': 'Major', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 284.0, - 'filename': 'C.K1a'}}}, - 'General_properties': {'Z': 6, - 'atomic_weight': 12.0107, - 'name': 'carbon'}}, - 'Pb': {'Physical_properties': {'density (g/cm^3)': 11.34}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.3836, - 'energy (keV)': 12.6144}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 84.9381}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 74.9693}, - 'M2N4': {'weight': 0.00863, - 'energy (keV)': 3.119}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 2.3459}, - 'Lb4': {'weight': 0.055, - 'energy (keV)': 12.3066}, - 'La': {'weight': 1.0, - 'energy (keV)': 10.5512}, - 'Ln': {'weight': 0.0132, - 'energy (keV)': 11.3493}, - 'M3O4': {'energy (keV)': 3.0446, - 'weight': 0.01}, - 'Ll': {'weight': 0.0586, - 'energy (keV)': 9.1845}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.4427}, - 'Mg': {'weight': 0.21845, - 'energy (keV)': 2.6535}, - 'Lb2': {'weight': 0.2244, - 'energy (keV)': 12.6223}, - 'Lb3': {'weight': 0.06049, - 'energy (keV)': 12.7944}, - 'M3O5': {'energy (keV)': 3.0472, - 'weight': 0.01}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 15.2163}, - 'Lg1': {'weight': 0.08256, - 'energy (keV)': 14.7648}, - 'Mz': {'weight': 0.0058, - 'energy (keV)': 1.8395}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2484.0, - 'filename': 'Pb.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2586.0, - 'filename': 'Pb.M5'}}}, - 'General_properties': {'Z': 82, - 'atomic_weight': 207.2, - 'name': 'lead'}}, - 'Pa': {'Physical_properties': {'density (g/cm^3)': 15.37}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4, - 'energy (keV)': 16.7025}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 108.4272}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 95.8679}, - 'M2N4': {'weight': 0.00674, - 'energy (keV)': 4.2575}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 3.0823}, - 'Lb4': {'weight': 0.04, - 'energy (keV)': 16.1037}, - 'La': {'weight': 1.0, - 'energy (keV)': 13.2913}, - 'Ln': {'weight': 0.0126, - 'energy (keV)': 14.9468}, - 'M3O4': {'energy (keV)': 4.07712, - 'weight': 0.01}, - 'Ll': {'weight': 0.0682, - 'energy (keV)': 11.3662}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 3.24}, - 'Mg': {'weight': 0.33505, - 'energy (keV)': 3.4656}, - 'Lb2': {'weight': 0.236, - 'energy (keV)': 16.0249}, - 'Lb3': {'weight': 0.06, - 'energy (keV)': 16.9308}, - 'M3O5': {'energy (keV)': 4.08456, - 'weight': 0.01}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 20.0979}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 19.5703}, - 'Mz': {'weight': 0.03512, - 'energy (keV)': 2.4351}}}, - 'General_properties': {'Z': 91, - 'atomic_weight': 231.03586, - 'name': 'protactinium'}}, - 'Pd': {'Physical_properties': {'density (g/cm^3)': 12.023}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.34375, - 'energy (keV)': 2.9903}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 23.8188}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 21.177}, - 'Lb2': {'weight': 0.10349, - 'energy (keV)': 3.16828}, - 'Lb4': {'weight': 0.0407, - 'energy (keV)': 3.0452}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.8386}, - 'Ln': {'weight': 0.0129, - 'energy (keV)': 2.6604}, - 'Ll': {'weight': 0.0412, - 'energy (keV)': 2.5034}, - 'Lb3': {'weight': 0.0682, - 'energy (keV)': 3.0728}, - 'Lg3': {'weight': 0.0125, - 'energy (keV)': 3.5532}, - 'Lg1': {'weight': 0.03256, - 'energy (keV)': 3.32485}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 335.0, - 'filename': 'Pd.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 340.0, - 'filename': 'Pd.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 531.0, - 'filename': 'Pd.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 559.0, - 'filename': 'Pd.M3'}}}, - 'General_properties': {'Z': 46, - 'atomic_weight': 106.42, - 'name': 'palladium'}}, - 'Cd': {'Physical_properties': {'density (g/cm^3)': 8.65}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.35704, - 'energy (keV)': 3.3165}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 26.0947}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 23.1737}, - 'Lb2': {'weight': 0.1288, - 'energy (keV)': 3.5282}, - 'Lb4': {'weight': 0.0469, - 'energy (keV)': 3.3673}, - 'La': {'weight': 1.0, - 'energy (keV)': 3.1338}, - 'Ln': {'weight': 0.0132, - 'energy (keV)': 2.9568}, - 'Ll': {'weight': 0.04169, - 'energy (keV)': 2.7673}, - 'Lb3': {'weight': 0.07719, - 'energy (keV)': 3.4015}, - 'Lg3': {'weight': 0.0151, - 'energy (keV)': 3.9511}, - 'Lg1': {'weight': 0.0416, - 'energy (keV)': 3.7177}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 404.0, - 'filename': 'Cd.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 411.0, - 'filename': 'Cd.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 616.0, - 'filename': 'Cd.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 651.0, - 'filename': 'Cd.M3'}}}, - 'General_properties': {'Z': 48, - 'atomic_weight': 112.411, - 'name': 'cadmium'}}, - 'Po': {'Physical_properties': {'density (g/cm^3)': 9.196}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.38536, - 'energy (keV)': 13.4463}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 89.8031}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 79.2912}, - 'M2N4': {'weight': 0.00863, - 'energy (keV)': 3.3539}, - 'Lb4': {'weight': 0.05709, - 'energy (keV)': 13.0852}, - 'La': {'weight': 1.0, - 'energy (keV)': 11.1308}, - 'Ln': {'weight': 0.0133, - 'energy (keV)': 12.0949}, - 'Ll': {'weight': 0.0607, - 'energy (keV)': 9.6644}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.62266}, - 'Mg': {'weight': 0.21845, - 'energy (keV)': 2.8285}, - 'Lb2': {'weight': 0.2289, - 'energy (keV)': 13.3404}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 16.2343}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 15.7441}, - 'Lb3': {'weight': 0.0603, - 'energy (keV)': 13.6374}, - 'Mz': {'weight': 0.00354, - 'energy (keV)': 1.978}}}, - 'General_properties': {'Z': 84, - 'atomic_weight': 209, - 'name': 'polonium'}}, - 'Pm': {'Physical_properties': {'density (g/cm^3)': 7.264}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4308, - 'energy (keV)': 5.9613}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 43.8271}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 38.7247}, - 'M2N4': {'weight': 0.028, - 'energy (keV)': 1.351}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 0.9894}, - 'Lb4': {'weight': 0.07799, - 'energy (keV)': 5.9565}, - 'La': {'weight': 1.0, - 'energy (keV)': 5.4324}, - 'Ln': {'weight': 0.01479, - 'energy (keV)': 5.3663}, - 'Ll': {'weight': 0.0448, - 'energy (keV)': 4.8128}, - 'Mb': {'weight': 0.89, - 'energy (keV)': 1.0475}, - 'Mg': {'weight': 0.4, - 'energy (keV)': 1.2365}, - 'Lb2': {'weight': 0.196, - 'energy (keV)': 6.3389}, - 'Lb3': {'weight': 0.1247, - 'energy (keV)': 6.071}, - 'Lg3': {'weight': 0.0311, - 'energy (keV)': 7.1919}, - 'Lg1': {'weight': 0.0784, - 'energy (keV)': 6.8924}, - 'Mz': {'weight': 0.068, - 'energy (keV)': 0.7909}}}, - 'General_properties': {'Z': 61, - 'atomic_weight': 145, - 'name': 'promethium'}}, - 'Ho': {'Physical_properties': {'density (g/cm^3)': 8.795}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.45056, - 'energy (keV)': 7.5263}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 53.8765}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 47.5466}, - 'M2N4': {'weight': 0.072, - 'energy (keV)': 1.7618}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.3477}, - 'Lb4': {'weight': 0.09039, - 'energy (keV)': 7.4714}, - 'La': {'weight': 1.0, - 'energy (keV)': 6.7197}, - 'Ln': {'weight': 0.0151, - 'energy (keV)': 6.7895}, - 'Ll': {'weight': 0.04759, - 'energy (keV)': 5.9428}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.3878}, - 'Mg': {'weight': 0.1418, - 'energy (keV)': 1.5802}, - 'Lb2': {'weight': 0.23563, - 'energy (keV)': 7.9101}, - 'Lb3': {'weight': 0.06, - 'energy (keV)': 7.653}, - 'Lg3': {'weight': 0.0321, - 'energy (keV)': 9.0876}, - 'Lg1': {'weight': 0.08448, - 'energy (keV)': 8.7568}, - 'Mz': {'weight': 0.6629, - 'energy (keV)': 1.0448}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1351.0, - 'filename': 'Ho.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1391.0, - 'filename': 'Ho.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1741.0, - 'filename': 'Ho.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1923.0, - 'filename': 'Ho.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 161.0, - 'filename': 'Ho.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 161.0, - 'filename': 'Ho.N5'}}}, - 'General_properties': {'Z': 67, - 'atomic_weight': 164.93032, - 'name': 'holmium'}}, - 'Hf': {'Physical_properties': {'density (g/cm^3)': 13.31}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.46231, - 'energy (keV)': 9.023}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 63.2432}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 55.7901}, - 'M2N4': {'weight': 0.01, - 'energy (keV)': 2.1416}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.6446}, - 'Lb4': {'weight': 0.10189, - 'energy (keV)': 8.9053}, - 'La': {'weight': 1.0, - 'energy (keV)': 7.899}, - 'Ln': {'weight': 0.0158, - 'energy (keV)': 8.1385}, - 'Ll': {'weight': 0.05089, - 'energy (keV)': 6.9598}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.6993}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 1.8939}, - 'Lb2': {'weight': 0.2048, - 'energy (keV)': 9.347}, - 'Lb3': {'weight': 0.1316, - 'energy (keV)': 9.1631}, - 'Lg3': {'weight': 0.0347, - 'energy (keV)': 10.8903}, - 'Lg1': {'weight': 0.08968, - 'energy (keV)': 10.5156}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 1.2813}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1662.0, - 'filename': 'Hf.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1716.0, - 'filename': 'Hf.M5'}}}, - 'General_properties': {'Z': 72, - 'atomic_weight': 178.49, - 'name': 'hafnium'}}, - 'Hg': {'Physical_properties': {'density (g/cm^3)': 13.534}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.39504, - 'energy (keV)': 11.8238}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 80.2552}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 70.8184}, - 'M2N4': {'weight': 0.02901, - 'energy (keV)': 2.9002}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 2.1964}, - 'Lb4': {'weight': 0.0566, - 'energy (keV)': 11.5608}, - 'La': {'weight': 1.0, - 'energy (keV)': 9.989}, - 'Ln': {'weight': 0.0136, - 'energy (keV)': 10.6471}, - 'M3O4': {'energy (keV)': 2.8407, - 'weight': 0.005}, - 'Ll': {'weight': 0.05709, - 'energy (keV)': 8.7223}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.2827}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 2.4873}, - 'Lb2': {'weight': 0.2221, - 'energy (keV)': 11.9241}, - 'Lb3': {'weight': 0.06469, - 'energy (keV)': 11.9922}, - 'M3O5': {'energy (keV)': 2.8407, - 'weight': 0.01}, - 'Lg3': {'weight': 0.0184, - 'energy (keV)': 14.2683}, - 'Lg1': {'weight': 0.0832, - 'energy (keV)': 13.8304}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.7239}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2295.0, - 'filename': 'Hg.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2385.0, - 'filename': 'Hg.M5'}}}, - 'General_properties': {'Z': 80, - 'atomic_weight': 200.59, - 'name': 'mercury'}}, - 'Mg': {'Physical_properties': {'density (g/cm^3)': 1.738}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.01, - 'energy (keV)': 1.305}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 1.2536}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1305.0, - 'filename': 'Mg.K1'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 51.0, - 'filename': 'Mg.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 51.0, - 'filename': 'Mg.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 89.0, - 'filename': 'Mg.L1'}}}, - 'General_properties': {'Z': 12, - 'atomic_weight': 24.305, - 'name': 'magnesium'}}, - 'K': {'Physical_properties': {'density (g/cm^3)': 0.856}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.1039, - 'energy (keV)': 3.5896}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 3.3138}}, - 'Binding_energies': {'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 294.0, - 'filename': 'K.L3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 296.0, - 'filename': 'K.L3'}, - 'L1a': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 377.0, - 'filename': 'K.L1a'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 377.0, - 'filename': 'K.L1'}}}, - 'General_properties': {'Z': 19, - 'atomic_weight': 39.0983, - 'name': 'potassium'}}, - 'Mn': {'Physical_properties': {'density (g/cm^3)': 7.47}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.1252, - 'energy (keV)': 6.4904}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 5.8987}, - 'La': {'weight': 1.0, - 'energy (keV)': 0.63316}, - 'Ln': {'weight': 0.1898, - 'energy (keV)': 0.5675}, - 'Ll': {'weight': 0.3898, - 'energy (keV)': 0.5564}, - 'Lb3': {'weight': 0.0263, - 'energy (keV)': 0.7204}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 51.0, - 'filename': 'Mn.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 651.0, - 'filename': 'Mn.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 640.0, - 'filename': 'Mn.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 51.0, - 'filename': 'Mn.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 769.0, - 'filename': 'Mn.L1'}}}, - 'General_properties': {'Z': 25, - 'atomic_weight': 54.938045, - 'name': 'manganese'}}, - 'O': {'Physical_properties': {'density (g/cm^3)': 0.001429}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.5249}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 532.0, - 'filename': 'O.K1'}}}, - 'General_properties': {'Z': 8, - 'atomic_weight': 15.9994, - 'name': 'oxygen'}}, - 'S': {'Physical_properties': {'density (g/cm^3)': 1.96}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.06525, - 'energy (keV)': 2.46427}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 2.3072}}, - 'Binding_energies': {'L3': {'relevance': 'Major', - # overlaps - # with L2 - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1.5, - 'onset_energy (eV)': 165.0, - 'filename': 'S.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 229.0, - 'filename': 'S.L1'}}}, - 'General_properties': {'Z': 16, - 'atomic_weight': 32.065, - 'name': 'sulfur'}}, - 'W': {'Physical_properties': {'density (g/cm^3)': 19.25}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.3679, - 'energy (keV)': 9.6724}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 67.244}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 59.3182}, - 'M2N4': {'weight': 0.01, - 'energy (keV)': 2.3161}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.7756}, - 'Lb4': {'weight': 0.05649, - 'energy (keV)': 9.5249}, - 'La': {'weight': 1.0, - 'energy (keV)': 8.3976}, - 'Ln': {'weight': 0.01155, - 'energy (keV)': 8.7244}, - 'M3O4': {'energy (keV)': 2.2749, - 'weight': 0.001}, - 'Ll': {'weight': 0.04169, - 'energy (keV)': 7.3872}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.8351}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 2.0356}, - 'Lb2': {'weight': 0.21385, - 'energy (keV)': 9.9614}, - 'Lb3': {'weight': 0.07077, - 'energy (keV)': 9.8188}, - 'M3O5': {'energy (keV)': 2.281, - 'weight': 0.01}, - 'Lg3': {'weight': 0.0362, - 'energy (keV)': 11.6745}, - 'Lg1': {'weight': 0.05658, - 'energy (keV)': 11.2852}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.3839}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1809.0, - 'filename': 'W.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1872.0, - 'filename': 'W.M5'}}}, - 'General_properties': {'Z': 74, - 'atomic_weight': 183.84, - 'name': 'tungsten'}}, - 'Zn': {'Physical_properties': {'density (g/cm^3)': 7.14}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.1679, - 'energy (keV)': 1.0347}, - 'Kb': {'weight': 0.12605, - 'energy (keV)': 9.572}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 8.6389}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.0116}, - 'Ln': {'weight': 0.0368, - 'energy (keV)': 0.9069}, - 'Ll': {'weight': 0.0603, - 'energy (keV)': 0.8838}, - 'Lb3': {'weight': 0.002, - 'energy (keV)': 1.107}}, - 'Binding_energies': {'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 87.0, - 'filename': 'Zn.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1043.0, - 'filename': 'Zn.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1020.0, - 'filename': 'Zn.L3'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 87.0, - 'filename': 'Zn.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1194.0, - 'filename': 'Zn.L1'}}}, - 'General_properties': {'Z': 30, - 'atomic_weight': 65.38, - 'name': 'zinc'}}, - 'Eu': {'Physical_properties': {'density (g/cm^3)': 5.244}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43904, - 'energy (keV)': 6.4565}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 47.0384}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 41.5421}, - 'M2N4': {'weight': 0.013, - 'energy (keV)': 1.4807}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.0991}, - 'Lb4': {'weight': 0.0874, - 'energy (keV)': 6.4381}, - 'La': {'weight': 1.0, - 'energy (keV)': 5.846}, - 'Ln': {'weight': 0.015, - 'energy (keV)': 5.8171}, - 'Ll': {'weight': 0.04559, - 'energy (keV)': 5.1769}, - 'Mb': {'weight': 0.87, - 'energy (keV)': 1.15769}, - 'Mg': {'weight': 0.26, - 'energy (keV)': 1.3474}, - 'Lb2': {'weight': 0.1985, - 'energy (keV)': 6.8437}, - 'Lb3': {'weight': 0.1265, - 'energy (keV)': 6.5714}, - 'Lg3': {'weight': 0.0318, - 'energy (keV)': 7.7954}, - 'Lg1': {'weight': 0.08064, - 'energy (keV)': 7.4839}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 0.8743}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1131.0, - 'filename': 'Eu.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1161.0, - 'filename': 'Eu.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1481.0, - 'filename': 'Eu.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1614.0, - 'filename': 'Eu.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 134.0, - 'filename': 'Eu.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 134.0, - 'filename': 'Eu.N5'}}}, - 'General_properties': {'Z': 63, - 'atomic_weight': 151.964, - 'name': 'europium'}}, - 'Zr': {'Physical_properties': {'density (g/cm^3)': 6.511}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.37912, - 'energy (keV)': 2.1243}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 17.6671}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 15.7753}, - 'Lb2': {'weight': 0.0177, - 'energy (keV)': 2.2223}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.0423}, - 'Ln': {'weight': 0.0153, - 'energy (keV)': 1.8764}, - 'Ll': {'weight': 0.04209, - 'energy (keV)': 1.792}, - 'Lb3': {'weight': 0.05219, - 'energy (keV)': 2.2011}, - 'Lg3': {'weight': 0.0082, - 'energy (keV)': 2.5029}, - 'Lg1': {'weight': 0.006, - 'energy (keV)': 2.30268}}, - 'Binding_energies': {'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 344.0, - 'filename': 'Zr.M3'}, - 'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 181.0, - 'filename': 'Zr.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 181.0, - 'filename': 'Zr.M5'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 2307.0, - 'filename': 'Zr.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2222.0, - 'filename': 'Zr.L3'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 330.0, - 'filename': 'Zr.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 2532.0, - 'filename': 'Zr.L1'}}}, - 'General_properties': {'Z': 40, - 'atomic_weight': 91.224, - 'name': 'zirconium'}}, - 'Er': {'Physical_properties': {'density (g/cm^3)': 9.066}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.45263, - 'energy (keV)': 7.811}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 55.6737}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 49.1276}, - 'M2N4': {'weight': 0.0045, - 'energy (keV)': 1.8291}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.405}, - 'Lb4': {'weight': 0.0922, - 'energy (keV)': 7.7455}, - 'La': {'weight': 1.0, - 'energy (keV)': 6.9486}, - 'Ln': {'weight': 0.0153, - 'energy (keV)': 7.0578}, - 'Ll': {'weight': 0.0482, - 'energy (keV)': 6.1514}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.449}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 1.6442}, - 'Lb2': {'weight': 0.2005, - 'energy (keV)': 8.1903}, - 'Lb3': {'weight': 0.1258, - 'energy (keV)': 7.9395}, - 'Lg3': {'weight': 0.0324, - 'energy (keV)': 9.4313}, - 'Lg1': {'weight': 0.08487, - 'energy (keV)': 9.0876}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 1.0893}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1409.0, - 'filename': 'Er.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1453.0, - 'filename': 'Er.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1812.0, - 'filename': 'Er.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 2006.0, - 'filename': 'Er.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 168.0, - 'filename': 'Er.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 168.0, - 'filename': 'Er.N5'}}}, - 'General_properties': {'Z': 68, - 'atomic_weight': 167.259, - 'name': 'erbium'}}, - 'Ni': {'Physical_properties': {'density (g/cm^3)': 8.908}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.1677, - 'energy (keV)': 0.8683}, - 'Kb': {'weight': 0.1277, - 'energy (keV)': 8.2647}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 7.4781}, - 'La': {'weight': 1.0, - 'energy (keV)': 0.8511}, - 'Ln': {'weight': 0.09693, - 'energy (keV)': 0.7601}, - 'Ll': {'weight': 0.14133, - 'energy (keV)': 0.7429}, - 'Lb3': {'weight': 0.00199, - 'energy (keV)': 0.94}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 68.0, - 'filename': 'Ni.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 872.0, - 'filename': 'Ni.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 855.0, - 'filename': 'Ni.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 68.0, - 'filename': 'Ni.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1008.0, - 'filename': 'Ni.L1'}}}, - 'General_properties': {'Z': 28, - 'atomic_weight': 58.6934, - 'name': 'nickel'}}, - 'Na': {'Physical_properties': {'density (g/cm^3)': 0.968}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.01, - 'energy (keV)': 1.0721}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 1.041}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1072.0, - 'filename': 'Na.K1'}, - 'L3': {'relevance': 'Major', - # overlaps - # with L2 - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1.5, - 'onset_energy (eV)': 31.0, - 'filename': 'Na.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 63.0, - 'filename': 'Na.L1'}}}, - 'General_properties': {'Z': 11, - 'atomic_weight': 22.98976928, - 'name': 'sodium'}}, - 'Nb': {'Physical_properties': {'density (g/cm^3)': 8.57}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.32519, - 'energy (keV)': 2.2573}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 18.6226}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 16.6151}, - 'Lb2': {'weight': 0.03299, - 'energy (keV)': 2.3705}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.1659}, - 'Ln': {'weight': 0.0129, - 'energy (keV)': 1.9963}, - 'Ll': {'weight': 0.04169, - 'energy (keV)': 1.9021}, - 'Lb3': {'weight': 0.06429, - 'energy (keV)': 2.3347}, - 'Lg3': {'weight': 0.0103, - 'energy (keV)': 2.6638}, - 'Lg1': {'weight': 0.00975, - 'energy (keV)': 2.4615}}, - 'Binding_energies': {'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 378.0, - 'filename': 'Nb.M3'}, - 'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 202.3, - 'filename': 'Nb.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 205.0, - 'filename': 'Nb.M5'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 2465.0, - 'filename': 'Nb.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2371.0, - 'filename': 'Nb.L3'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 363.0, - 'filename': 'Nb.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 2698.0, - 'filename': 'Nb.L1'}}}, - 'General_properties': {'Z': 41, - 'atomic_weight': 92.90638, - 'name': 'niobium'}}, - 'Nd': {'Physical_properties': {'density (g/cm^3)': 7.01}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.42672, - 'energy (keV)': 5.722}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 42.2715}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 37.361}, - 'M2N4': {'weight': 0.052, - 'energy (keV)': 1.2853}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 0.9402}, - 'Lb4': {'weight': 0.0858, - 'energy (keV)': 5.7232}, - 'La': {'weight': 1.0, - 'energy (keV)': 5.2302}, - 'Ln': {'weight': 0.01469, - 'energy (keV)': 5.1462}, - 'Ll': {'weight': 0.04429, - 'energy (keV)': 4.6326}, - 'Mb': {'weight': 0.99, - 'energy (keV)': 0.9965}, - 'Mg': {'weight': 0.625, - 'energy (keV)': 1.1799}, - 'Lb2': {'weight': 0.1957, - 'energy (keV)': 6.0904}, - 'Lb3': {'weight': 0.12869, - 'energy (keV)': 5.8286}, - 'Lg3': {'weight': 0.0318, - 'energy (keV)': 6.9014}, - 'Lg1': {'weight': 0.07712, - 'energy (keV)': 6.604}, - 'Mz': {'weight': 0.069, - 'energy (keV)': 0.7531}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 978.0, - 'filename': 'Nd.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1000.0, - 'filename': 'Nd.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1297.0, - 'filename': 'Nd.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1403.0, - 'filename': 'Nd.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 118.0, - 'filename': 'Nd.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 118.0, - 'filename': 'Nd.N5'}}}, - 'General_properties': {'Z': 60, - 'atomic_weight': 144.242, - 'name': 'neodymium'}}, - 'Ne': {'Physical_properties': {'density (g/cm^3)': 0.0009}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.01, - 'energy (keV)': 0.8669}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 0.8486}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 867.0, - 'filename': 'Ne.K1'}}}, - 'General_properties': {'Z': 10, - 'atomic_weight': 20.1791, - 'name': 'neon'}}, - 'Fr': {'Physical_properties': {'density (g/cm^3)': 'NaN'}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.38327, - 'energy (keV)': 14.7703}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 97.474}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 86.1058}, - 'M2N4': {'weight': 0.00674, - 'energy (keV)': 3.7237}, - 'Lb4': {'weight': 0.0603, - 'energy (keV)': 14.312}, - 'La': {'weight': 1.0, - 'energy (keV)': 12.0315}, - 'Ln': {'weight': 0.0134, - 'energy (keV)': 13.2545}, - 'Ll': {'weight': 0.06339, - 'energy (keV)': 10.3792}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.88971}, - 'Mg': {'weight': 0.21845, - 'energy (keV)': 3.086}, - 'Lb2': {'weight': 0.2337, - 'energy (keV)': 14.4542}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 17.829}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 17.3032}, - 'Lb3': {'weight': 0.05969, - 'energy (keV)': 14.976}, - 'Mz': {'weight': 0.0058, - 'energy (keV)': 2.1897}}}, - 'General_properties': {'Z': 87, - 'atomic_weight': 223, - 'name': 'francium'}}, - 'Fe': {'Physical_properties': {'density (g/cm^3)': 7.874}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.1272, - 'energy (keV)': 7.058}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 6.4039}, - 'La': {'weight': 1.0, - 'energy (keV)': 0.7045}, - 'Ln': {'weight': 0.12525, - 'energy (keV)': 0.6282}, - 'Ll': {'weight': 0.3086, - 'energy (keV)': 0.6152}, - 'Lb3': {'weight': 0.02448, - 'energy (keV)': 0.7921}}, - 'Binding_energies': {'K': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 7113.0, - 'filename': 'Fe.K1'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 846.0, - 'filename': 'Fe.L1'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 721.0, - 'filename': 'Fe.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 708.0, - 'filename': 'Fe.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 57.0, - 'filename': 'Fe.M3'}, - 'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 57.0, - 'filename': 'Fe.M3'}}}, - 'General_properties': {'Z': 26, - 'atomic_weight': 55.845, - 'name': 'iron'}}, - 'B': {'Physical_properties': {'density (g/cm^3)': 2.46}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.1833}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 188.0, - 'filename': 'B.K1'}}}, - 'General_properties': {'Z': 5, - 'atomic_weight': 10.811, - 'name': 'boron'}}, - 'F': {'Physical_properties': {'density (g/cm^3)': 0.001696}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.6768}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 685.0, - 'filename': 'F.K1'}}}, - 'General_properties': {'Z': 9, - 'atomic_weight': 18.9984032, - 'name': 'fluorine'}}, - 'Sr': {'Physical_properties': {'density (g/cm^3)': 2.63}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.37975, - 'energy (keV)': 1.8718}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 15.8355}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 14.165}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.8065}, - 'Ln': {'weight': 0.01669, - 'energy (keV)': 1.6493}, - 'Ll': {'weight': 0.04309, - 'energy (keV)': 1.5821}, - 'Lb3': {'weight': 0.047, - 'energy (keV)': 1.9472}, - 'Lg3': {'weight': 0.0065, - 'energy (keV)': 2.1964}}, - 'Binding_energies': {'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 280.0, - 'filename': 'Sr.M3'}, - 'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 134.0, - 'filename': 'Sr.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 134.0, - 'filename': 'Sr.M5'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 2007.0, - 'filename': 'Sr.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1940.0, - 'filename': 'Sr.L3'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 269.0, - 'filename': 'Sr.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 2216.0, - 'filename': 'Sr.L1'}}}, - 'General_properties': {'Z': 38, - 'atomic_weight': 87.62, - 'name': 'strontium'}}, - 'N': {'Physical_properties': {'density (g/cm^3)': 0.001251}, - 'Atomic_properties': {'Xray_lines': {'Ka': {'weight': 1.0, - 'energy (keV)': 0.3924}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 401.0, - 'filename': 'N.K1'}}}, - 'General_properties': {'Z': 7, - 'atomic_weight': 14.0067, - 'name': 'nitrogen'}}, - 'Kr': {'Physical_properties': {'density (g/cm^3)': 0.00375}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.39031, - 'energy (keV)': 1.6383}, - 'Kb': {'weight': 0.1538, - 'energy (keV)': 14.1118}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 12.6507}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.586}, - 'Ln': {'weight': 0.0175, - 'energy (keV)': 1.43887}, - 'Ll': {'weight': 0.04509, - 'energy (keV)': 1.38657}, - 'Lb3': {'weight': 0.0465, - 'energy (keV)': 1.7072}, - 'Lg3': {'weight': 0.005, - 'energy (keV)': 1.921}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 89.0, - 'filename': 'Kr.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 89.0, - 'filename': 'Kr.M5'}, - 'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1727.0, - 'filename': 'Kr.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1675.0, - 'filename': 'Kr.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1921.0, - 'filename': 'Kr.L1'}}}, - 'General_properties': {'Z': 36, - 'atomic_weight': 83.798, - 'name': 'krypton'}}, - 'Si': {'Physical_properties': {'density (g/cm^3)': 2.33}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.02779, - 'energy (keV)': 1.8389}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 1.7397}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1839.0, - 'filename': 'Si.K1'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 99.8, - 'filename': 'Si.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 99.2, - 'filename': 'Si.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 149.7, - 'filename': 'Si.L1'}}}, - 'General_properties': {'Z': 14, - 'atomic_weight': 28.0855, - 'name': 'silicon'}}, - 'Sn': {'Physical_properties': {'density (g/cm^3)': 7.31}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43456, - 'energy (keV)': 3.6628}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 28.4857}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 25.2713}, - 'Lb2': {'weight': 0.14689, - 'energy (keV)': 3.9049}, - 'Lb4': {'weight': 0.0948, - 'energy (keV)': 3.7083}, - 'La': {'weight': 1.0, - 'energy (keV)': 3.444}, - 'Ln': {'weight': 0.0158, - 'energy (keV)': 3.2723}, - 'Ll': {'weight': 0.0416, - 'energy (keV)': 3.045}, - 'Lb3': {'weight': 0.1547, - 'energy (keV)': 3.7503}, - 'Lg3': {'weight': 0.0321, - 'energy (keV)': 4.3761}, - 'Lg1': {'weight': 0.058, - 'energy (keV)': 4.1322}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 485.0, - 'filename': 'Sn.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 494.0, - 'filename': 'Sn.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 714.0, - 'filename': 'Sn.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 756.0, - 'filename': 'Sn.M3'}}}, - 'General_properties': {'Z': 50, - 'atomic_weight': 118.71, - 'name': 'tin'}}, - 'Sm': {'Physical_properties': {'density (g/cm^3)': 7.353}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43512, - 'energy (keV)': 6.2058}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 45.4144}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 40.118}, - 'M2N4': {'weight': 0.012, - 'energy (keV)': 1.4117}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.0428}, - 'Lb4': {'weight': 0.08689, - 'energy (keV)': 6.1961}, - 'La': {'weight': 1.0, - 'energy (keV)': 5.636}, - 'Ln': {'weight': 0.01489, - 'energy (keV)': 5.589}, - 'Ll': {'weight': 0.04519, - 'energy (keV)': 4.9934}, - 'Mb': {'weight': 0.88, - 'energy (keV)': 1.1005}, - 'Mg': {'weight': 0.26, - 'energy (keV)': 1.2908}, - 'Lb2': {'weight': 0.19769, - 'energy (keV)': 6.5872}, - 'Lb3': {'weight': 0.12669, - 'energy (keV)': 6.317}, - 'Lg3': {'weight': 0.0318, - 'energy (keV)': 7.4894}, - 'Lg1': {'weight': 0.07951, - 'energy (keV)': 7.1828}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 0.8328}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1080.0, - 'filename': 'Sm.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1106.0, - 'filename': 'Sm.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1420.0, - 'filename': 'Sm.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1541.0, - 'filename': 'Sm.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 130.0, - 'filename': 'Sm.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 130.0, - 'filename': 'Sm.N5'}}}, - 'General_properties': {'Z': 62, - 'atomic_weight': 150.36, - 'name': 'samarium'}}, - 'V': {'Physical_properties': {'density (g/cm^3)': 6.11}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.1225, - 'energy (keV)': 5.4273}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 4.9522}, - 'La': {'weight': 1.0, - 'energy (keV)': 0.5129}, - 'Ln': {'weight': 0.2805, - 'energy (keV)': 0.454}, - 'Ll': {'weight': 0.5745, - 'energy (keV)': 0.4464}, - 'Lb3': {'weight': 0.0154, - 'energy (keV)': 0.5904}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 38.0, - 'filename': 'V.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 521.0, - 'filename': 'V.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 513.0, - 'filename': 'V.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 38.0, - 'filename': 'V.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 628.0, - 'filename': 'V.L1'}}}, - 'General_properties': {'Z': 23, - 'atomic_weight': 50.9415, - 'name': 'vanadium'}}, - 'Sc': {'Physical_properties': {'density (g/cm^3)': 2.985}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.12839, - 'energy (keV)': 4.4605}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 4.0906}, - 'La': {'weight': 0.308, - 'energy (keV)': 0.4022}, - 'Ln': {'weight': 0.488, - 'energy (keV)': 0.3529}, - 'Ll': {'weight': 1.0, - 'energy (keV)': 0.3484}, - 'Lb3': {'weight': 0.037, - 'energy (keV)': 0.4681}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 32.0, - 'filename': 'Sc.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 407.0, - 'filename': 'Sc.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 402.0, - 'filename': 'Sc.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 32.0, - 'filename': 'Sc.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 500.0, - 'filename': 'Sc.L1'}}}, - 'General_properties': {'Z': 21, - 'atomic_weight': 44.955912, - 'name': 'scandium'}}, - 'Sb': {'Physical_properties': {'density (g/cm^3)': 6.697}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4276, - 'energy (keV)': 3.8435}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 29.7256}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 26.359}, - 'Lb2': {'weight': 0.1556, - 'energy (keV)': 4.1008}, - 'Lb4': {'weight': 0.0932, - 'energy (keV)': 3.8864}, - 'La': {'weight': 1.0, - 'energy (keV)': 3.6047}, - 'Ln': {'weight': 0.0155, - 'energy (keV)': 3.4367}, - 'Ll': {'weight': 0.0419, - 'energy (keV)': 3.1885}, - 'Lb3': {'weight': 0.15099, - 'energy (keV)': 3.9327}, - 'Lg3': {'weight': 0.0321, - 'energy (keV)': 4.5999}, - 'Lg1': {'weight': 0.06064, - 'energy (keV)': 4.349}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 528.0, - 'filename': 'Sb.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 537.0, - 'filename': 'Sb.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 766.0, - 'filename': 'Sb.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 812.0, - 'filename': 'Sb.M3'}}}, - 'General_properties': {'Z': 51, - 'atomic_weight': 121.76, - 'name': 'antimony'}}, - 'Se': {'Physical_properties': {'density (g/cm^3)': 4.819}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.38848, - 'energy (keV)': 1.4195}, - 'Kb': {'weight': 0.1505, - 'energy (keV)': 12.4959}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 11.222}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.3791}, - 'Ln': {'weight': 0.0187, - 'energy (keV)': 1.2447}, - 'Ll': {'weight': 0.04759, - 'energy (keV)': 1.2043}, - 'Lb3': {'weight': 0.047, - 'energy (keV)': 1.492}}, - 'Binding_energies': {'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1476.0, - 'filename': 'Se.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1436.0, - 'filename': 'Se.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1654.0, - 'filename': 'Se.L1'}}}, - 'General_properties': {'Z': 34, - 'atomic_weight': 78.96, - 'name': 'selenium'}}, - 'Co': {'Physical_properties': {'density (g/cm^3)': 8.9}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.1277, - 'energy (keV)': 7.6494}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 6.9303}, - 'La': {'weight': 1.0, - 'energy (keV)': 0.7757}, - 'Ln': {'weight': 0.0833, - 'energy (keV)': 0.6929}, - 'Ll': {'weight': 0.2157, - 'energy (keV)': 0.6779}, - 'Lb3': {'weight': 0.0238, - 'energy (keV)': 0.8661}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 59.0, - 'filename': 'Co.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 794.0, - 'filename': 'Co.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 779.0, - 'filename': 'Co.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 59.0, - 'filename': 'Co.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 926.0, - 'filename': 'Co.L1'}}}, - 'General_properties': {'Z': 27, - 'atomic_weight': 58.933195, - 'name': 'cobalt'}}, - 'Cl': {'Physical_properties': {'density (g/cm^3)': 0.003214}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.0838, - 'energy (keV)': 2.8156}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 2.6224}}, - 'Binding_energies': {'L3': {'relevance': 'Major', - # overlaps - # with L2 - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1.5, - 'onset_energy (eV)': 200.0, - 'filename': 'Cl.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 270.0, - 'filename': 'Cl.L1'}}}, - 'General_properties': {'Z': 17, - 'atomic_weight': 35.453, - 'name': 'chlorine'}}, - 'Ca': {'Physical_properties': {'density (g/cm^3)': 1.55}, - 'Atomic_properties': {'Xray_lines': {'Ln': {'weight': 0.23, - 'energy (keV)': 0.3063}, - 'Kb': {'weight': 0.112, - 'energy (keV)': 4.0127}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 3.6917}, - 'Ll': {'weight': 1.0, - 'energy (keV)': 0.3027}, - 'La': {'weight': 0.0, - 'energy (keV)': 0.3464}}, - 'Binding_energies': {'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 350.0, - 'filename': 'Ca.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 346.0, - 'filename': 'Ca.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 438.0, - 'filename': 'Ca.L1'}}}, - 'General_properties': {'Z': 20, - 'atomic_weight': 40.078, - 'name': 'calcium'}}, - 'Ce': {'Physical_properties': {'density (g/cm^3)': 6.689}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43, - 'energy (keV)': 5.2629}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 39.2576}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 34.7196}, - 'M2N4': {'weight': 0.08, - 'energy (keV)': 1.1628}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 0.8455}, - 'Lb4': {'weight': 0.08699, - 'energy (keV)': 5.276}, - 'La': {'weight': 1.0, - 'energy (keV)': 4.8401}, - 'Ln': {'weight': 0.015, - 'energy (keV)': 4.7296}, - 'Ll': {'weight': 0.0436, - 'energy (keV)': 4.2888}, - 'Mb': {'weight': 0.91, - 'energy (keV)': 0.8154}, - 'Mg': {'weight': 0.5, - 'energy (keV)': 1.0754}, - 'Lb2': {'weight': 0.19399, - 'energy (keV)': 5.6134}, - 'Lb3': {'weight': 0.1325, - 'energy (keV)': 5.3634}, - 'Lg3': {'weight': 0.0324, - 'energy (keV)': 6.3416}, - 'Lg1': {'weight': 0.0764, - 'energy (keV)': 6.0542}, - 'Mz': {'weight': 0.07, - 'energy (keV)': 0.6761}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 883.0, - 'filename': 'Ce.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 901.0, - 'filename': 'Ce.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1185.0, - 'filename': 'Ce.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1273.0, - 'filename': 'Ce.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 110.0, - 'filename': 'Ce.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 110.0, - 'filename': 'Ce.N5'}}}, - 'General_properties': {'Z': 58, - 'atomic_weight': 140.116, - 'name': 'cerium'}}, - 'Xe': {'Physical_properties': {'density (g/cm^3)': 0.0059}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.42248, - 'energy (keV)': 4.4183}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 33.6244}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 29.7792}, - 'Lb2': {'weight': 0.17699, - 'energy (keV)': 4.7182}, - 'Lb4': {'weight': 0.08929, - 'energy (keV)': 4.4538}, - 'La': {'weight': 1.0, - 'energy (keV)': 4.1099}, - 'Ln': {'weight': 0.015, - 'energy (keV)': 3.9591}, - 'Ll': {'weight': 0.0424, - 'energy (keV)': 3.6376}, - 'Lb3': {'weight': 0.14119, - 'energy (keV)': 4.5158}, - 'Lg3': {'weight': 0.0323, - 'energy (keV)': 5.3061}, - 'Lg1': {'weight': 0.06848, - 'energy (keV)': 5.0397}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 672.0, - 'filename': 'Xe.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 685.0, - 'filename': 'Xe.M5'}}}, - 'General_properties': {'Z': 54, - 'atomic_weight': 131.293, - 'name': 'xenon'}}, - 'Tm': {'Physical_properties': {'density (g/cm^3)': 9.321}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.45831, - 'energy (keV)': 8.1023}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 57.5051}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 50.7416}, - 'M2N4': {'weight': 0.01, - 'energy (keV)': 1.9102}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.4624}, - 'Lb4': {'weight': 0.09449, - 'energy (keV)': 8.0259}, - 'La': {'weight': 1.0, - 'energy (keV)': 7.1803}, - 'Ln': {'weight': 0.0156, - 'energy (keV)': 7.3101}, - 'Ll': {'weight': 0.04889, - 'energy (keV)': 6.3412}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.5093}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 1.7049}, - 'Lb2': {'weight': 0.20059, - 'energy (keV)': 8.4684}, - 'Lb3': {'weight': 0.1273, - 'energy (keV)': 8.2312}, - 'Lg3': {'weight': 0.0329, - 'energy (keV)': 9.7791}, - 'Lg1': {'weight': 0.08615, - 'energy (keV)': 9.4373}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 1.1311}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1468.0, - 'filename': 'Tm.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1515.0, - 'filename': 'Tm.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1884.0, - 'filename': 'Tm.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 2090.0, - 'filename': 'Tm.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 180.0, - 'filename': 'Tm.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 180.0, - 'filename': 'Tm.N5'}}}, - 'General_properties': {'Z': 69, - 'atomic_weight': 168.93421, - 'name': 'thulium'}}, - 'Cs': {'Physical_properties': {'density (g/cm^3)': 1.879}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.42983, - 'energy (keV)': 4.6199}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 34.987}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 30.9727}, - 'Lb2': {'weight': 0.19589, - 'energy (keV)': 4.9354}, - 'Lb4': {'weight': 0.08869, - 'energy (keV)': 4.6493}, - 'La': {'weight': 1.0, - 'energy (keV)': 4.2864}, - 'Ln': {'weight': 0.0152, - 'energy (keV)': 4.1423}, - 'Ll': {'weight': 0.04269, - 'energy (keV)': 3.7948}, - 'Lb3': {'weight': 0.1399, - 'energy (keV)': 4.7167}, - 'Lg3': {'weight': 0.0325, - 'energy (keV)': 5.5527}, - 'Lg1': {'weight': 0.07215, - 'energy (keV)': 5.2806}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 726.0, - 'filename': 'Cs.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 740.0, - 'filename': 'Cs.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 998.0, - 'filename': 'Cs.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1065.0, - 'filename': 'Cs.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 78.0, - 'filename': 'Cs.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 78.0, - 'filename': 'Cs.N5'}}}, - 'General_properties': {'Z': 55, - 'atomic_weight': 132.9054519, - 'name': 'cesium'}}, - 'Cr': {'Physical_properties': {'density (g/cm^3)': 7.14}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.134, - 'energy (keV)': 5.9467}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 5.4147}, - 'La': {'weight': 1.0, - 'energy (keV)': 0.5722}, - 'Ln': {'weight': 0.2353, - 'energy (keV)': 0.5096}, - 'Ll': {'weight': 0.6903, - 'energy (keV)': 0.5004}, - 'Lb3': {'weight': 0.0309, - 'energy (keV)': 0.6521}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 43.0, - 'filename': 'Cr.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 584.0, - 'filename': 'Cr.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 575.0, - 'filename': 'Cr.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 43.0, - 'filename': 'Cr.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 695.0, - 'filename': 'Cr.L1'}}}, - 'General_properties': {'Z': 24, - 'atomic_weight': 51.9961, - 'name': 'chromium'}}, - 'Cu': {'Physical_properties': {'density (g/cm^3)': 8.92}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.03197, - 'energy (keV)': 0.9494}, - 'Kb': {'weight': 0.13157, - 'energy (keV)': 8.9053}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 8.0478}, - 'La': {'weight': 1.0, - 'energy (keV)': 0.9295}, - 'Ln': {'weight': 0.01984, - 'energy (keV)': 0.8312}, - 'Ll': {'weight': 0.08401, - 'energy (keV)': 0.8113}, - 'Lb3': {'weight': 0.00114, - 'energy (keV)': 1.0225}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 74.0, - 'filename': 'Cu.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 951.0, - 'filename': 'Cu.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 931.0, - 'filename': 'Cu.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 74.0, - 'filename': 'Cu.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1096.0, - 'filename': 'Cu.L1'}}}, - 'General_properties': {'Z': 29, - 'atomic_weight': 63.546, - 'name': 'copper'}}, - 'La': {'Physical_properties': {'density (g/cm^3)': 6.146}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.42631, - 'energy (keV)': 5.0421}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 37.8012}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 33.4419}, - 'M2N4': {'weight': 0.022, - 'energy (keV)': 1.1055}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 0.8173}, - 'Lb4': {'weight': 0.0872, - 'energy (keV)': 5.0619}, - 'La': {'weight': 1.0, - 'energy (keV)': 4.651}, - 'Ln': {'weight': 0.015, - 'energy (keV)': 4.5293}, - 'Ll': {'weight': 0.0432, - 'energy (keV)': 4.1214}, - 'Mb': {'weight': 0.9, - 'energy (keV)': 0.8162}, - 'Mg': {'weight': 0.4, - 'energy (keV)': 1.0245}, - 'Lb2': {'weight': 0.19579, - 'energy (keV)': 5.3838}, - 'Lb3': {'weight': 0.1341, - 'energy (keV)': 5.1429}, - 'Lg3': {'weight': 0.0329, - 'energy (keV)': 6.0749}, - 'Lg1': {'weight': 0.07656, - 'energy (keV)': 5.7917}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 0.6403}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 832.0, - 'filename': 'La.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 849.0, - 'filename': 'La.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1123.0, - 'filename': 'La.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1204.0, - 'filename': 'La.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 99.0, - 'filename': 'La.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 99.0, - 'filename': 'La.N5'}}}, - 'General_properties': {'Z': 57, - 'atomic_weight': 138.90547, - 'name': 'lanthanum'}}, - 'Li': {'Physical_properties': {'density (g/cm^3)': 0.534}, - 'Atomic_properties': {'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 55.0, - 'filename': 'Li.K1'}}}, - 'General_properties': {'atomic_weight': 6.939, - 'Z': 3, - 'name': 'lithium'}}, - 'Tl': {'Physical_properties': {'density (g/cm^3)': 11.85}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.39112, - 'energy (keV)': 12.2128}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 82.5738}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 72.8729}, - 'M2N4': {'weight': 0.00863, - 'energy (keV)': 3.0091}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 2.2708}, - 'Lb4': {'weight': 0.05419, - 'energy (keV)': 11.931}, - 'La': {'weight': 1.0, - 'energy (keV)': 10.2682}, - 'Ln': {'weight': 0.0134, - 'energy (keV)': 10.9938}, - 'M3O4': {'energy (keV)': 2.9413, - 'weight': 0.005}, - 'Ll': {'weight': 0.0578, - 'energy (keV)': 8.9534}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.3623}, - 'Mg': {'weight': 0.21845, - 'energy (keV)': 2.5704}, - 'Lb2': {'weight': 0.22289, - 'energy (keV)': 12.2713}, - 'Lb3': {'weight': 0.0607, - 'energy (keV)': 12.3901}, - 'M3O5': {'energy (keV)': 2.9435, - 'weight': 0.01}, - 'Lg3': {'weight': 0.0175, - 'energy (keV)': 14.7377}, - 'Lg1': {'weight': 0.08304, - 'energy (keV)': 14.2913}, - 'Mz': {'weight': 0.0058, - 'energy (keV)': 1.7803}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2389.0, - 'filename': 'Tl.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2485.0, - 'filename': 'Tl.M5'}}}, - 'General_properties': {'Z': 81, - 'atomic_weight': 204.3833, - 'name': 'thallium'}}, - 'Lu': {'Physical_properties': {'density (g/cm^3)': 9.841}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.46975, - 'energy (keV)': 8.7092}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 61.2902}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 54.0697}, - 'M2N4': {'weight': 0.01, - 'energy (keV)': 2.0587}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.5816}, - 'Lb4': {'weight': 0.0996, - 'energy (keV)': 8.6069}, - 'La': {'weight': 1.0, - 'energy (keV)': 7.6556}, - 'Ln': {'weight': 0.016, - 'energy (keV)': 7.8574}, - 'Ll': {'weight': 0.05009, - 'energy (keV)': 6.7529}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.6325}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 1.8286}, - 'Lb2': {'weight': 0.20359, - 'energy (keV)': 9.0491}, - 'Lb3': {'weight': 0.13099, - 'energy (keV)': 8.8468}, - 'Lg3': {'weight': 0.0342, - 'energy (keV)': 10.5111}, - 'Lg1': {'weight': 0.08968, - 'energy (keV)': 10.1438}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 1.2292}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1588.0, - 'filename': 'Lu.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1639.0, - 'filename': 'Lu.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 2024.0, - 'filename': 'Lu.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 2263.0, - 'filename': 'Lu.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Very delayed', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 195.0, - 'filename': 'Lu.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Very delayed', - 'factor': 1, - 'onset_energy (eV)': 195.0, - 'filename': 'Lu.N5'}}}, - 'General_properties': {'Z': 71, - 'atomic_weight': 174.9668, - 'name': 'lutetium'}}, - 'Th': {'Physical_properties': {'density (g/cm^3)': 11.724}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4, - 'energy (keV)': 16.2024}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 105.6049}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 93.3507}, - 'M2N4': {'weight': 0.00674, - 'energy (keV)': 4.1163}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 2.9968}, - 'Lb4': {'weight': 0.05, - 'energy (keV)': 15.6417}, - 'La': {'weight': 1.0, - 'energy (keV)': 12.9683}, - 'Ln': {'weight': 0.0134, - 'energy (keV)': 14.5109}, - 'M3O4': {'energy (keV)': 3.9518, - 'weight': 0.01}, - 'Ll': {'weight': 0.06709, - 'energy (keV)': 11.118}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 3.1464}, - 'Mg': {'weight': 0.33505, - 'energy (keV)': 3.3697}, - 'Lb2': {'weight': 0.236, - 'energy (keV)': 15.6239}, - 'Lb3': {'weight': 0.06, - 'energy (keV)': 16.426}, - 'M3O5': {'energy (keV)': 3.9582, - 'weight': 0.01}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 19.5048}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 18.9791}, - 'Mz': {'weight': 0.03512, - 'energy (keV)': 2.3647}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 3332.0, - 'filename': 'Th.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 3491.0, - 'filename': 'Th.M5'}, - 'O5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 83.0, - 'filename': 'Th.O5'}, - 'O4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 83.0, - 'filename': 'Th.O5'}}}, - 'General_properties': {'Z': 90, - 'atomic_weight': 232.03806, - 'name': 'thorium'}}, - 'Ti': {'Physical_properties': {'density (g/cm^3)': 4.507}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.11673, - 'energy (keV)': 4.9318}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 4.5109}, - 'La': {'weight': 0.694, - 'energy (keV)': 0.4555}, - 'Ln': {'weight': 0.491, - 'energy (keV)': 0.4012}, - 'Ll': {'weight': 1.0, - 'energy (keV)': 0.3952}, - 'Lb3': {'weight': 0.166, - 'energy (keV)': 0.5291}}, - 'Binding_energies': {'M2': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 35.0, - 'filename': 'Ti.M3'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 462.0, - 'filename': 'Ti.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 456.0, - 'filename': 'Ti.L3'}, - 'M3': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 35.0, - 'filename': 'Ti.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 564.0, - 'filename': 'Ti.L1'}}}, - 'General_properties': {'Z': 22, - 'atomic_weight': 47.867, - 'name': 'titanium'}}, - 'Te': {'Physical_properties': {'density (g/cm^3)': 6.24}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43183, - 'energy (keV)': 4.0295}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 30.9951}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 27.4724}, - 'Lb2': {'weight': 0.16269, - 'energy (keV)': 4.3016}, - 'Lb4': {'weight': 0.0906, - 'energy (keV)': 4.0695}, - 'La': {'weight': 1.0, - 'energy (keV)': 3.7693}, - 'Ln': {'weight': 0.0154, - 'energy (keV)': 3.606}, - 'Ll': {'weight': 0.0419, - 'energy (keV)': 3.3354}, - 'Lb3': {'weight': 0.1458, - 'energy (keV)': 4.1205}, - 'Lg3': {'weight': 0.0317, - 'energy (keV)': 4.829}, - 'Lg1': {'weight': 0.06375, - 'energy (keV)': 4.5722}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 572.0, - 'filename': 'Te.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 582.0, - 'filename': 'Te.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 819.0, - 'filename': 'Te.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 870.0, - 'filename': 'Te.M3'}}}, - 'General_properties': {'Z': 52, - 'atomic_weight': 127.6, - 'name': 'tellurium'}}, - 'Tb': {'Physical_properties': {'density (g/cm^3)': 8.219}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.44104, - 'energy (keV)': 6.9766}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 50.3844}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 44.4817}, - 'M2N4': {'weight': 0.014, - 'energy (keV)': 1.6207}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.2326}, - 'Lb4': {'weight': 0.0874, - 'energy (keV)': 6.9403}, - 'La': {'weight': 1.0, - 'energy (keV)': 6.2728}, - 'Ln': {'weight': 0.01479, - 'energy (keV)': 6.2841}, - 'Ll': {'weight': 0.0465, - 'energy (keV)': 5.5465}, - 'Mb': {'weight': 0.78, - 'energy (keV)': 1.2656}, - 'Mg': {'weight': 0.2615, - 'energy (keV)': 1.4643}, - 'Lb2': {'weight': 0.19929, - 'energy (keV)': 7.367}, - 'Lb3': {'weight': 0.124, - 'energy (keV)': 7.0967}, - 'Lg3': {'weight': 0.0315, - 'energy (keV)': 8.423}, - 'Lg1': {'weight': 0.08168, - 'energy (keV)': 8.1046}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 0.9562}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1241.0, - 'filename': 'Tb.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1275.0, - 'filename': 'Tb.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1611.0, - 'filename': 'Tb.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1768.0, - 'filename': 'Tb.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 148.0, - 'filename': 'Tb.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 148.0, - 'filename': 'Tb.N5'}}}, - 'General_properties': {'Z': 65, - 'atomic_weight': 158.92535, - 'name': 'terbium'}}, - 'Tc': {'Physical_properties': {'density (g/cm^3)': 11.5}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.32951, - 'energy (keV)': 2.5368}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 20.619}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 18.3671}, - 'Lb2': {'weight': 0.05839, - 'energy (keV)': 2.67017}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.424}, - 'Ln': {'weight': 0.0127, - 'energy (keV)': 2.2456}, - 'Ll': {'weight': 0.0412, - 'energy (keV)': 2.1293}, - 'Lb3': {'weight': 0.0644, - 'energy (keV)': 2.6175}, - 'Lg3': {'weight': 0.0111, - 'energy (keV)': 3.0036}, - 'Lg1': {'weight': 0.01744, - 'energy (keV)': 2.78619}}}, - 'General_properties': {'Z': 43, - 'atomic_weight': 98, - 'name': 'technetium'}}, - 'Ta': {'Physical_properties': {'density (g/cm^3)': 16.65}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.46248, - 'energy (keV)': 9.3429}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 65.2224}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 57.5353}, - 'M2N4': {'weight': 0.01, - 'energy (keV)': 2.2274}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.7101}, - 'Lb4': {'weight': 0.10449, - 'energy (keV)': 9.2128}, - 'La': {'weight': 1.0, - 'energy (keV)': 8.146}, - 'Ln': {'weight': 0.0158, - 'energy (keV)': 8.4281}, - 'M3O4': {'energy (keV)': 2.1883, - 'weight': 0.0001}, - 'Ll': {'weight': 0.0515, - 'energy (keV)': 7.1731}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.7682}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 1.9647}, - 'Lb2': {'weight': 0.2076, - 'energy (keV)': 9.6518}, - 'Lb3': {'weight': 0.1333, - 'energy (keV)': 9.4875}, - 'M3O5': {'energy (keV)': 2.194, - 'weight': 0.01}, - 'Lg3': {'weight': 0.0354, - 'energy (keV)': 11.277}, - 'Lg1': {'weight': 0.09071, - 'energy (keV)': 10.8948}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.3306}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1735.0, - 'filename': 'Ta.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1793.0, - 'filename': 'Ta.M5'}}}, - 'General_properties': {'Z': 73, - 'atomic_weight': 180.94788, - 'name': 'tantalum'}}, - 'Yb': {'Physical_properties': {'density (g/cm^3)': 6.57}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.46224, - 'energy (keV)': 8.4019}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 59.3825}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 52.3887}, - 'M2N4': {'weight': 0.01, - 'energy (keV)': 1.9749}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.5215}, - 'Lb4': {'weight': 0.09589, - 'energy (keV)': 8.3134}, - 'La': {'weight': 1.0, - 'energy (keV)': 7.4158}, - 'Ln': {'weight': 0.0157, - 'energy (keV)': 7.5801}, - 'Ll': {'weight': 0.0494, - 'energy (keV)': 6.5455}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 1.57}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 1.7649}, - 'Lb2': {'weight': 0.2017, - 'energy (keV)': 8.7587}, - 'Lb3': {'weight': 0.12789, - 'energy (keV)': 8.5366}, - 'Lg3': {'weight': 0.0331, - 'energy (keV)': 10.1429}, - 'Lg1': {'weight': 0.08728, - 'energy (keV)': 9.7801}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 1.1843}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1528.0, - 'filename': 'Yb.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1576.0, - 'filename': 'Yb.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1950.0, - 'filename': 'Yb.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 2173.0, - 'filename': 'Yb.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 184.0, - 'filename': 'Yb.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 184.0, - 'filename': 'Yb.N5'}}}, - 'General_properties': {'Z': 70, - 'atomic_weight': 173.054, - 'name': 'ytterbium'}}, - 'Dy': {'Physical_properties': {'density (g/cm^3)': 8.551}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.444, - 'energy (keV)': 7.2481}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 52.1129}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 45.9984}, - 'M2N4': {'weight': 0.008, - 'energy (keV)': 1.6876}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.2907}, - 'Lb4': {'weight': 0.0891, - 'energy (keV)': 7.204}, - 'La': {'weight': 1.0, - 'energy (keV)': 6.4952}, - 'Ln': {'weight': 0.01489, - 'energy (keV)': 6.5338}, - 'Ll': {'weight': 0.0473, - 'energy (keV)': 5.7433}, - 'Mb': {'weight': 0.76, - 'energy (keV)': 1.3283}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 1.5214}, - 'Lb2': {'weight': 0.2, - 'energy (keV)': 7.6359}, - 'Lb3': {'weight': 0.12529, - 'energy (keV)': 7.3702}, - 'Lg3': {'weight': 0.0319, - 'energy (keV)': 8.7529}, - 'Lg1': {'weight': 0.08295, - 'energy (keV)': 8.4264}, - 'Mz': {'weight': 0.06, - 'energy (keV)': 1.002}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1295.0, - 'filename': 'Dy.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 1332.0, - 'filename': 'Dy.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 1676.0, - 'filename': 'Dy.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 1842.0, - 'filename': 'Dy.M3'}, - 'N4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 154.0, - 'filename': 'Dy.N5'}, - 'N5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 154.0, - 'filename': 'Dy.N5'}}}, - 'General_properties': {'Z': 66, - 'atomic_weight': 162.5, - 'name': 'dysprosium'}}, - 'I': {'Physical_properties': {'density (g/cm^3)': 4.94}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.43087, - 'energy (keV)': 4.2208}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 32.2948}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 28.6123}, - 'Lb2': {'weight': 0.17059, - 'energy (keV)': 4.5075}, - 'Lb4': {'weight': 0.09189, - 'energy (keV)': 4.2576}, - 'La': {'weight': 1.0, - 'energy (keV)': 3.9377}, - 'Ln': {'weight': 0.0154, - 'energy (keV)': 3.78}, - 'Ll': {'weight': 0.0423, - 'energy (keV)': 3.485}, - 'Lb3': {'weight': 0.1464, - 'energy (keV)': 4.3135}, - 'Lg3': {'weight': 0.0327, - 'energy (keV)': 5.0654}, - 'Lg1': {'weight': 0.06704, - 'energy (keV)': 4.8025}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 620.0, - 'filename': 'I.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 631.0, - 'filename': 'I.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 875.0, - 'filename': 'I.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 930.0, - 'filename': 'I.M3'}}}, - 'General_properties': {'Z': 53, - 'atomic_weight': 126.90447, - 'name': 'iodine'}}, - 'U': {'Physical_properties': {'density (g/cm^3)': 19.05}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4, - 'energy (keV)': 17.22}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 111.3026}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 98.4397}, - 'M2N4': {'weight': 0.00674, - 'energy (keV)': 4.4018}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 3.1708}, - 'Lb4': {'weight': 0.04, - 'energy (keV)': 16.5752}, - 'La': {'weight': 1.0, - 'energy (keV)': 13.6146}, - 'Ln': {'weight': 0.01199, - 'energy (keV)': 15.3996}, - 'M3O4': {'energy (keV)': 4.1984, - 'weight': 0.01}, - 'Ll': {'weight': 0.069, - 'energy (keV)': 11.6183}, - 'Mb': {'weight': 0.6086, - 'energy (keV)': 3.3363}, - 'Mg': {'weight': 0.33505, - 'energy (keV)': 3.5657}, - 'Lb2': {'weight': 0.236, - 'energy (keV)': 16.4286}, - 'Lb3': {'weight': 0.06, - 'energy (keV)': 17.454}, - 'M3O5': {'energy (keV)': 4.2071, - 'weight': 0.01}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 20.7125}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 20.1672}, - 'Mz': {'weight': 0.03512, - 'energy (keV)': 2.5068}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 3552.0, - 'filename': 'U.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 3728.0, - 'filename': 'U.M5'}, - 'O5': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 96.0, - 'filename': 'U.O5'}, - 'O4': {'relevance': 'Major', - 'threshold': 'Broad peak', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 96.0, - 'filename': 'U.O5'}}}, - 'General_properties': {'Z': 92, - 'atomic_weight': 238.02891, - 'name': 'uranium'}}, - 'Y': {'Physical_properties': {'density (g/cm^3)': 4.472}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.39127, - 'energy (keV)': 1.9959}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 16.7381}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 14.9584}, - 'Lb2': {'weight': 0.00739, - 'energy (keV)': 2.08}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.9226}, - 'Ln': {'weight': 0.0162, - 'energy (keV)': 1.7619}, - 'Ll': {'weight': 0.0428, - 'energy (keV)': 1.6864}, - 'Lb3': {'weight': 0.05059, - 'energy (keV)': 2.0722}, - 'Lg3': {'weight': 0.0075, - 'energy (keV)': 2.3469}, - 'Lg1': {'weight': 0.00264, - 'energy (keV)': 2.1555}}, - 'Binding_energies': {'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 312.0, - 'filename': 'Y.M3'}, - 'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 160.0, - 'filename': 'Y.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 160.0, - 'filename': 'Y.M5'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 2155.0, - 'filename': 'Y.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2080.0, - 'filename': 'Y.L3'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 300.0, - 'filename': 'Y.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 2372.0, - 'filename': 'Y.L1'}}}, - 'General_properties': {'Z': 39, - 'atomic_weight': 88.90585, - 'name': 'yttrium'}}, - 'Ac': {'Physical_properties': {'density (g/cm^3)': 10.07}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4, - 'energy (keV)': 15.713}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 102.846}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 90.884}, - 'M2N4': {'weight': 0.00674, - 'energy (keV)': 3.9811}, - 'Ma': {'energy (keV)': 2.9239330000000003, - 'weight': 1.0}, - 'La': {'weight': 1.0, - 'energy (keV)': 12.652}, - 'Ln': {'weight': 0.0133, - 'energy (keV)': 14.0812}, - 'M3O4': {'energy (keV)': 3.82586, - 'weight': 0.01}, - 'Ll': {'weight': 0.06549, - 'energy (keV)': 10.869}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 3.06626}, - 'Mg': {'weight': 0.33505, - 'energy (keV)': 3.272}, - 'M3O5': {'energy (keV)': 3.83206, - 'weight': 0.01}, - 'Lb2': {'weight': 0.236, - 'energy (keV)': 15.234}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 18.95}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 18.4083}, - 'Lb3': {'weight': 0.06, - 'energy (keV)': 15.931}, - 'Mz': {'weight': 0.03512, - 'energy (keV)': 2.329}}}, - 'General_properties': {'Z': 89, - 'atomic_weight': 227, - 'name': 'actinium'}}, - 'Ag': {'Physical_properties': {'density (g/cm^3)': 10.49}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.35175, - 'energy (keV)': 3.1509}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 24.9426}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 22.1629}, - 'Lb2': {'weight': 0.1165, - 'energy (keV)': 3.3478}, - 'Lb4': {'weight': 0.0444, - 'energy (keV)': 3.2034}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.9844}, - 'Ln': {'weight': 0.0131, - 'energy (keV)': 2.8062}, - 'Ll': {'weight': 0.04129, - 'energy (keV)': 2.6336}, - 'Lb3': {'weight': 0.0737, - 'energy (keV)': 3.2344}, - 'Lg3': {'weight': 0.014, - 'energy (keV)': 3.7499}, - 'Lg1': {'weight': 0.03735, - 'energy (keV)': 3.5204}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 367.0, - 'filename': 'Ag.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 373.0, - 'filename': 'Ag.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 571.0, - 'filename': 'Ag.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 602.0, - 'filename': 'Ag.M3'}}}, - 'General_properties': {'Z': 47, - 'atomic_weight': 107.8682, - 'name': 'silver'}}, - 'Ir': {'Physical_properties': {'density (g/cm^3)': 22.56}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.4168, - 'energy (keV)': 10.708}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 73.5603}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 64.8958}, - 'M2N4': {'weight': 0.02901, - 'energy (keV)': 2.5973}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 1.9799}, - 'Lb4': {'weight': 0.07269, - 'energy (keV)': 10.5098}, - 'La': {'weight': 1.0, - 'energy (keV)': 9.1748}, - 'Ln': {'weight': 0.01429, - 'energy (keV)': 9.6504}, - 'M3O4': {'energy (keV)': 2.54264, - 'weight': 0.005}, - 'Ll': {'weight': 0.05429, - 'energy (keV)': 8.0415}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 2.0527}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 2.2558}, - 'Lb2': {'weight': 0.216, - 'energy (keV)': 10.9203}, - 'Lb3': {'weight': 0.0874, - 'energy (keV)': 10.8678}, - 'M3O5': {'energy (keV)': 2.54385, - 'weight': 0.01}, - 'Lg3': {'weight': 0.024, - 'energy (keV)': 12.9242}, - 'Lg1': {'weight': 0.08543, - 'energy (keV)': 12.5127}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.5461}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2040.0, - 'filename': 'Ir.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2116.0, - 'filename': 'Ir.M5'}}}, - 'General_properties': {'Z': 77, - 'atomic_weight': 192.217, - 'name': 'iridium'}}, - 'Al': {'Physical_properties': {'density (g/cm^3)': 2.7}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.0132, - 'energy (keV)': 1.5596}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 1.4865}}, - 'Binding_energies': {'K': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1560.0, - 'filename': 'Al.K1'}, - 'L3': {'relevance': 'Major', - # Overlaps - # with L2 - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1.5, - 'onset_energy (eV)': 73.0, - 'filename': 'Al.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 118.0, - 'filename': 'Al.L1'}}}, - 'General_properties': {'Z': 13, - 'atomic_weight': 26.9815386, - 'name': 'aluminum'}}, - 'As': {'Physical_properties': {'density (g/cm^3)': 5.727}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.16704, - 'energy (keV)': 1.3174}, - 'Kb': {'weight': 0.14589, - 'energy (keV)': 11.7262}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 10.5436}, - 'La': {'weight': 1.0, - 'energy (keV)': 1.2819}, - 'Ln': {'weight': 0.01929, - 'energy (keV)': 1.1551}, - 'Ll': {'weight': 0.04929, - 'energy (keV)': 1.1196}, - 'Lb3': {'weight': 0.04769, - 'energy (keV)': 1.386}}, - 'Binding_energies': {'L2': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 1359.0, - 'filename': 'As.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 1323.0, - 'filename': 'As.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 1526.0, - 'filename': 'As.L1'}}}, - 'General_properties': {'Z': 33, - 'atomic_weight': 74.9216, - 'name': 'arsenic'}}, - 'Ar': {'Physical_properties': {'density (g/cm^3)': 0.001784}, - 'Atomic_properties': {'Xray_lines': {'Kb': {'weight': 0.10169, - 'energy (keV)': 3.1905}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 2.9577}}, - 'Binding_energies': {'L3': {'relevance': 'Major', - # overlaps - # with L2 - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1.5, - 'onset_energy (eV)': 245.0, - 'filename': 'Ar.L3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 320.0, - 'filename': 'Ar.L1'}}}, - 'General_properties': {'Z': 18, - 'atomic_weight': 39.948, - 'name': 'argon'}}, - 'Au': {'Physical_properties': {'density (g/cm^3)': 19.3}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.40151, - 'energy (keV)': 11.4425}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 77.9819}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 68.8062}, - 'M2N4': {'weight': 0.02901, - 'energy (keV)': 2.7958}, - 'Ma': {'weight': 1.0, - 'energy (keV)': 2.1229}, - 'Lb4': {'weight': 0.0594, - 'energy (keV)': 11.205}, - 'La': {'weight': 1.0, - 'energy (keV)': 9.713}, - 'Ln': {'weight': 0.01379, - 'energy (keV)': 10.3087}, - 'M3O4': {'energy (keV)': 2.73469, - 'weight': 0.005}, - 'Ll': {'weight': 0.0562, - 'energy (keV)': 8.4938}, - 'Mb': {'weight': 0.59443, - 'energy (keV)': 2.2047}, - 'Mg': {'weight': 0.08505, - 'energy (keV)': 2.4091}, - 'Lb2': {'weight': 0.21949, - 'energy (keV)': 11.5848}, - 'Lb3': {'weight': 0.069, - 'energy (keV)': 11.6098}, - 'M3O5': {'energy (keV)': 2.73621, - 'weight': 0.01}, - 'Lg3': {'weight': 0.0194, - 'energy (keV)': 13.8074}, - 'Lg1': {'weight': 0.08407, - 'energy (keV)': 13.3816}, - 'Mz': {'weight': 0.01344, - 'energy (keV)': 1.6603}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2206.0, - 'filename': 'Au.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 2291.0, - 'filename': 'Au.M5'}}}, - 'General_properties': {'Z': 79, - 'atomic_weight': 196.966569, - 'name': 'gold'}}, - 'At': {'Physical_properties': {'density (g/cm^3)': 'NaN'}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.38048, - 'energy (keV)': 13.876}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 92.3039}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 81.5164}, - 'M2N4': {'weight': 0.00863, - 'energy (keV)': 3.4748}, - 'Lb4': {'weight': 0.05809, - 'energy (keV)': 13.485}, - 'La': {'weight': 1.0, - 'energy (keV)': 11.4268}, - 'Ln': {'weight': 0.0132, - 'energy (keV)': 12.4677}, - 'Ll': {'weight': 0.06179, - 'energy (keV)': 9.8965}, - 'Mb': {'weight': 0.64124, - 'energy (keV)': 2.71162}, - 'Mg': {'weight': 0.21845, - 'energy (keV)': 2.95061}, - 'Lb2': {'weight': 0.2305, - 'energy (keV)': 13.73812}, - 'Lg3': {'weight': 0.017, - 'energy (keV)': 16.753}, - 'Lg1': {'weight': 0.08, - 'energy (keV)': 16.2515}, - 'Lb3': {'weight': 0.06, - 'energy (keV)': 14.067}, - 'Mz': {'weight': 0.00354, - 'energy (keV)': 2.0467}}}, - 'General_properties': {'Z': 85, - 'atomic_weight': 210, - 'name': 'astatine'}}, - 'In': {'Physical_properties': {'density (g/cm^3)': 7.31}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.3616, - 'energy (keV)': 3.4872}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 27.2756}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 24.2098}, - 'Lb2': {'weight': 0.1371, - 'energy (keV)': 3.7139}, - 'Lb4': {'weight': 0.05349, - 'energy (keV)': 3.5353}, - 'La': {'weight': 1.0, - 'energy (keV)': 3.287}, - 'Ln': {'weight': 0.0132, - 'energy (keV)': 3.1124}, - 'Ll': {'weight': 0.0415, - 'energy (keV)': 2.9045}, - 'Lb3': {'weight': 0.08779, - 'energy (keV)': 3.5732}, - 'Lg3': {'weight': 0.0177, - 'energy (keV)': 4.1601}, - 'Lg1': {'weight': 0.04535, - 'energy (keV)': 3.9218}}, - 'Binding_energies': {'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 443.0, - 'filename': 'In.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 451.0, - 'filename': 'In.M5'}, - 'M3': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 664.0, - 'filename': 'In.M3'}, - 'M2': {'relevance': 'Minor', - 'threshold': '', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 702.0, - 'filename': 'In.M3'}}}, - 'General_properties': {'Z': 49, - 'atomic_weight': 114.818, - 'name': 'indium'}}, - 'Mo': {'Physical_properties': {'density (g/cm^3)': 10.28}, - 'Atomic_properties': {'Xray_lines': {'Lb1': {'weight': 0.32736, - 'energy (keV)': 2.3948}, - 'Kb': {'weight': 0.15, - 'energy (keV)': 19.6072}, - 'Ka': {'weight': 1.0, - 'energy (keV)': 17.4793}, - 'Lb2': {'weight': 0.04509, - 'energy (keV)': 2.5184}, - 'La': {'weight': 1.0, - 'energy (keV)': 2.2932}, - 'Ln': {'weight': 0.0128, - 'energy (keV)': 2.1205}, - 'Ll': {'weight': 0.0415, - 'energy (keV)': 2.0156}, - 'Lb3': {'weight': 0.06299, - 'energy (keV)': 2.4732}, - 'Lg3': {'weight': 0.0105, - 'energy (keV)': 2.8307}, - 'Lg1': {'weight': 0.01335, - 'energy (keV)': 2.6233}}, - 'Binding_energies': {'M2': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 0.5, - 'onset_energy (eV)': 410.0, - 'filename': 'Mo.M3'}, - 'M5': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 227.0, - 'filename': 'Mo.M5'}, - 'M4': {'relevance': 'Major', - 'threshold': '', - 'edge': 'Delayed maximum', - 'factor': 0.6666666666666666, - 'onset_energy (eV)': 228.0, - 'filename': 'Mo.M5'}, - 'L2': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 0.5, - 'onset_energy (eV)': 2625.0, - 'filename': 'Mo.L3'}, - 'L3': {'relevance': 'Major', - 'threshold': 'Sharp peak', - 'edge': 'Delayed maximum', - 'factor': 1, - 'onset_energy (eV)': 2520.0, - 'filename': 'Mo.L3'}, - 'M3': {'relevance': 'Minor', - 'threshold': 'Sharp peak', - 'edge': '', - 'factor': 1, - 'onset_energy (eV)': 392.0, - 'filename': 'Mo.M3'}, - 'L1': {'relevance': 'Minor', - 'threshold': '', - 'edge': 'Abrupt onset', - 'factor': 1, - 'onset_energy (eV)': 2866.0, - 'filename': 'Mo.L1'}}}, - 'General_properties': {'Z': 42, - 'atomic_weight': 95.96, - 'name': 'molybdenum'}}} - -elements_db = utils.DictionaryTreeBrowser(elements) - -# read dictionary of atomic numbers from HyperSpy, and add the elements that -# do not currently exist in the database (in case anyone is doing EDS on -# Ununpentium...) -atomic_number2name = dict((p.General_properties.Z, e) - for (e, p) in elements_db) -atomic_number2name.update({93: 'Np', 94: 'Pu', 95: 'Am', 96: 'Cm', 97: 'Bk', - 98: 'Cf', 99: 'Es', 100: 'Fm', 101: 'Md', 102: 'No', - 103: 'Lr', 104: 'Rf', 105: 'Db', 106: 'Sg', - 107: 'Bh', 108: 'Hs', 109: 'Mt', 110: 'Ds', - 111: 'Rg', 112: 'Cp', 113: 'Uut', 114: 'Uuq', - 115: 'Uup', 116: 'Uuh', 117: 'Uus', 118: 'Uuo', - 119: 'Uue'}) diff --git a/hyperspy/misc/etc/test_compilers.c b/hyperspy/misc/etc/test_compilers.c deleted file mode 100644 index 153bef9f50..0000000000 --- a/hyperspy/misc/etc/test_compilers.c +++ /dev/null @@ -1,4 +0,0 @@ -int main(void) -{ -return 0; -} diff --git a/hyperspy/misc/example_signals_loading.py b/hyperspy/misc/example_signals_loading.py deleted file mode 100644 index 7ca6afe966..0000000000 --- a/hyperspy/misc/example_signals_loading.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from pathlib import Path -import warnings - - -def _resolve_dir(): - """Returns the absolute path to this file's directory.""" - return Path(__file__).resolve().parent - - -def load_1D_EDS_SEM_spectrum(): - """ - Load an EDS-SEM spectrum - - Notes - ----- - - Sample: EDS-TM002 provided by BAM (www.webshop.bam.de) - - SEM Microscope: Nvision40 Carl Zeiss - - EDS Detector: X-max 80 from Oxford Instrument - - Signal is loaded "read-only" to ensure data access regardless of - install location - """ - from hyperspy.io import load - - file_path = _resolve_dir().joinpath( - "eds", "example_signals", "1D_EDS_SEM_Spectrum.hspy" - ) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=UserWarning) - return load(file_path, mode="r") - - -def load_1D_EDS_TEM_spectrum(): - """ - Load an EDS-TEM spectrum - - Notes - ----- - - Sample: FePt bimetallic nanoparticles - - SEM Microscope: Tecnai Osiris 200 kV D658 AnalyticalTwin - - EDS Detector: Super-X 4 detectors Brucker - - Signal is loaded "read-only" to ensure data access regardless of - install location - """ - from hyperspy.io import load - - file_path = _resolve_dir().joinpath( - "eds", "example_signals", "1D_EDS_TEM_Spectrum.hspy" - ) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=UserWarning) - return load(file_path, mode="r") - - -def load_object_hologram(): - """ - Load an object hologram image - - Notes - ----- - Sample: Fe needle with YOx nanoparticle inclusions. See reference for more - details - - Migunov, V. et al. Model-independent measurement of the charge density - distribution along an Fe atom probe needle using off-axis electron - holography without mean inner potential effects. J. Appl. Phys. 117, - 134301 (2015). https://doi.org/10.1063/1.4916609 - - TEM: FEI Titan G2 60-300 HOLO - - Boothroyd, C. et al. FEI Titan G2 60-300 HOLO. Journal of large-scale - research facilities JLSRF 2, 44 (2016). - https://doi.org/10.17815/jlsrf-2-70 - - Signal is loaded "read-only" to ensure data access regardless of - install location - """ - from hyperspy.io import load - - file_path = _resolve_dir().joinpath( - "holography", "example_signals", "01_holo_Vbp_130V_0V_bin2_crop.hdf5" - ) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=UserWarning) - return load(file_path, mode="r") - - -def load_reference_hologram(): - """ - Load a reference hologram image - - Notes - ----- - Sample: Fe needle with YOx nanoparticle inclusions. See reference for more - details - - Migunov, V. et al. Model-independent measurement of the charge density - distribution along an Fe atom probe needle using off-axis electron - holography without mean inner potential effects. J. Appl. Phys. 117, - 134301 (2015). https://doi.org/10.1063/1.4916609 - - TEM: FEI Titan G2 60-300 HOLO - - Boothroyd, C. et al. FEI Titan G2 60-300 HOLO. Journal of large-scale - research facilities JLSRF 2, 44 (2016). - https://doi.org/10.17815/jlsrf-2-70 - - Signal is loaded "read-only" to ensure data access regardless of - install location - """ - from hyperspy.io import load - - file_path = _resolve_dir().joinpath( - "holography", "example_signals", "00_ref_Vbp_130V_0V_bin2_crop.hdf5" - ) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=UserWarning) - return load(file_path, mode="r") diff --git a/hyperspy/misc/export_dictionary.py b/hyperspy/misc/export_dictionary.py index a2c47ff575..f8d7263844 100644 --- a/hyperspy/misc/export_dictionary.py +++ b/hyperspy/misc/export_dictionary.py @@ -1,61 +1,63 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from operator import attrgetter -from hyperspy.misc.utils import attrsetter from copy import deepcopy -import dill +from operator import attrgetter + +import cloudpickle from dask.array import Array +from hyperspy.misc.utils import attrsetter + def check_that_flags_make_sense(flags): # one of: fn, id, sig def do_error(f1, f2): - raise ValueError( - 'The flags "%s" and "%s" are not compatible' % - (f1, f2)) - if 'fn' in flags: - if 'id' in flags: - do_error('fn', 'id') - if 'sig' in flags: - do_error('fn', 'sig') - if 'id' in flags: + raise ValueError('The flags "%s" and "%s" are not compatible' % (f1, f2)) + + if "fn" in flags: + if "id" in flags: + do_error("fn", "id") + if "sig" in flags: + do_error("fn", "sig") + if "id" in flags: # fn done previously - if 'sig' in flags: - do_error('id', 'sig') - if 'init' in flags: - do_error('id', 'init') + if "sig" in flags: + do_error("id", "sig") + if "init" in flags: + do_error("id", "init") # all sig cases already covered def parse_flag_string(flags): - return flags.replace(' ', '').split(',') + return flags.replace(" ", "").split(",") def export_to_dictionary(target, whitelist, dic, fullcopy=True): - """ Exports attributes of target from whitelist.keys() to dictionary dic + """ + Exports attributes of target from whitelist.keys() to dictionary dic All values are references only by default. Parameters ---------- target : object must contain the (nested) attributes of the whitelist.keys() - whitelist : dictionary + whitelist : dict A dictionary, keys of which are used as attributes for exporting. Key 'self' is only available with tag 'id', when the id of the target is saved. The values are either None, or a tuple, where: @@ -72,15 +74,15 @@ def export_to_dictionary(target, whitelist, dic, fullcopy=True): * 'fn': the targeted attribute is a function, and may be pickled. A tuple of (thing, value) will be exported to the dictionary, where thing is None if function is passed as-is, and True if - dill package is used to pickle the function, with the value as + cloudpickle package is used to pickle the function, with the value as the result of the pickle. * 'id': the id of the targeted attribute is exported (e.g. id(target.name)) * 'sig': The targeted attribute is a signal, and will be converted to a dictionary if fullcopy=True - dic : dictionary + dict A dictionary where the object will be exported - fullcopy : bool + bool Copies of objects are stored, not references. If any found, functions will be pickled and signals converted to dictionaries @@ -93,35 +95,35 @@ def export_to_dictionary(target, whitelist, dic, fullcopy=True): if fullcopy: thing = deepcopy(thing) dic[key] = thing - whitelist_flags[key] = '' + whitelist_flags[key] = "" continue flags_str, value = value flags = parse_flag_string(flags_str) check_that_flags_make_sense(flags) - if key == 'self': - if 'id' not in flags: - raise ValueError( - 'Key "self" is only available with flag "id" given') + if key == "self": + if "id" not in flags: + raise ValueError('Key "self" is only available with flag "id" given') value = id(target) else: - if 'id' in flags: + if "id" in flags: value = id(attrgetter(key)(target)) # here value is either id(thing), or None (all others except 'init'), # or something for init - if 'init' not in flags and value is None: + if "init" not in flags and value is None: value = attrgetter(key)(target) # here value either id(thing), or an actual target to export - if 'sig' in flags: + if "sig" in flags: if fullcopy: from hyperspy.signal import BaseSignal + if isinstance(value, BaseSignal): value = value._to_dictionary() - value['data'] = deepcopy(value['data']) - elif 'fn' in flags: + value["data"] = deepcopy(value["data"]) + elif "fn" in flags: if fullcopy: - value = (True, dill.dumps(value)) + value = (True, cloudpickle.dumps(value)) else: value = (None, value) elif fullcopy: @@ -130,44 +132,45 @@ def export_to_dictionary(target, whitelist, dic, fullcopy=True): dic[key] = value whitelist_flags[key] = flags_str - if '_whitelist' not in dic: - dic['_whitelist'] = {} + if "_whitelist" not in dic: + dic["_whitelist"] = {} # the saved whitelist does not have any values, as they are saved in the # original dictionary. Have to restore then when loading from dictionary, # most notably all with 'init' flags!! - dic['_whitelist'].update(whitelist_flags) + dic["_whitelist"].update(whitelist_flags) def load_from_dictionary(target, dic): - """ Loads attributes of target to dictionary dic + """ + Loads attributes of target to dictionary dic The attribute list is read from dic['_whitelist'].keys() Parameters ---------- - target : object - must contain the (nested) attributes of the whitelist.keys() - dic : dictionary - A dictionary, containing field '_whitelist', which is a dictionary - with all keys that were exported, with values being flag strings. - The convention of the flags is as follows: - - * 'init': object used for initialization of the target. Will be - copied to the _whitelist after loading - * 'fn': the targeted attribute is a function, and may have been - pickled (preferably with dill package). - * 'id': the id of the original object was exported and the - attribute will not be set. The key has to be '_id_' - * 'sig': The targeted attribute was a signal, and may have been - converted to a dictionary if fullcopy=True + target : obj + must contain the (nested) attributes of the whitelist.keys() + dic : dict + A dictionary, containing field '_whitelist', which is a dictionary + with all keys that were exported, with values being flag strings. + The convention of the flags is as follows: + + * 'init': object used for initialization of the target. Will be + copied to the _whitelist after loading + * 'fn': the targeted attribute is a function, and may have been + pickled (preferably with cloudpickle package). + * 'id': the id of the original object was exported and the + attribute will not be set. The key has to be '_id_' + * 'sig': The targeted attribute was a signal, and may have been + converted to a dictionary if fullcopy=True """ new_whitelist = {} - for key, flags_str in dic['_whitelist'].items(): + for key, flags_str in dic["_whitelist"].items(): value = dic[key] flags = parse_flag_string(flags_str) - if 'id' not in flags: + if "id" not in flags: value = reconstruct_object(flags, value) - if 'init' in flags: + if "init" in flags: new_whitelist[key] = (flags_str, value) else: attrsetter(target, key, value) @@ -175,31 +178,32 @@ def load_from_dictionary(target, dic): new_whitelist[key] = (flags_str, None) else: new_whitelist[key] = None - if hasattr(target, '_whitelist'): + if hasattr(target, "_whitelist"): if isinstance(target._whitelist, dict): target._whitelist.update(new_whitelist) else: - attrsetter(target, '_whitelist', new_whitelist) + attrsetter(target, "_whitelist", new_whitelist) def reconstruct_object(flags, value): - """ Reconstructs the value (if necessary) after having saved it in a + """Reconstructs the value (if necessary) after having saved it in a dictionary """ if not isinstance(flags, list): flags = parse_flag_string(flags) - if 'sig' in flags: + if "sig" in flags: if isinstance(value, dict): from hyperspy.signal import BaseSignal + value = BaseSignal(**value) value._assign_subclass() return value - if 'fn' in flags: - ifdill, thing = value - if ifdill is None: + if "fn" in flags: + ifcloudpickle, thing = value + if ifcloudpickle is None: return thing - if ifdill in [True, 'True', b'True']: - return dill.loads(thing) + if ifcloudpickle in [True, "True", b"True"]: + return cloudpickle.loads(thing) # should not be reached raise ValueError("The object format is not recognized") if isinstance(value, Array): diff --git a/hyperspy/misc/hist_tools.py b/hyperspy/misc/hist_tools.py index 5c56fe14f6..09f5dad761 100644 --- a/hyperspy/misc/hist_tools.py +++ b/hyperspy/misc/hist_tools.py @@ -1,33 +1,50 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import logging import warnings import dask.array as da import numpy as np - -from hyperspy.docstrings.signal import HISTOGRAM_BIN_ARGS, HISTOGRAM_MAX_BIN_ARGS -from hyperspy.exceptions import VisibleDeprecationWarning +import traits.api as t + +from hyperspy.docstrings.signal import ( + HISTOGRAM_BIN_ARGS, + HISTOGRAM_MAX_BIN_ARGS, + HISTOGRAM_RANGE_ARGS, + HISTOGRAM_WEIGHTS_ARGS, +) from hyperspy.external.astropy.bayesian_blocks import bayesian_blocks from hyperspy.external.astropy.histogram import knuth_bin_width -_logger = logging.getLogger(__name__) + +def _set_histogram_metadata(signal, histogram, **kwargs): + name = signal.metadata.get_item("Signal.quantity", "value") + units = t.Undefined + if "(" in name: + name, units = name.split("(") + name = name.strip() + units = units.strip(")") + histogram.axes_manager[0].name = name + histogram.axes_manager[0].units = units + histogram.axes_manager[0].is_binned = True + histogram.metadata.General.title = signal.metadata.General.title + " histogram" + quantity = "Probability density" if kwargs.get("density") else "Count" + histogram.metadata.Signal.quantity = quantity def histogram(a, bins="fd", range=None, max_num_bins=250, weights=None, **kwargs): @@ -41,22 +58,11 @@ def histogram(a, bins="fd", range=None, max_num_bins=250, weights=None, **kwargs a : array_like Input data. The histogram is computed over the flattened array. %s - range : (float, float), optional - The lower and upper range of the bins. If not provided, range - is simply ``(a.min(), a.max())``. Values outside the range are - ignored. The first element of the range must be less than or - equal to the second. `range` affects the automatic bin - computation as well. While bin width is computed to be optimal - based on the actual data within `range`, the bin count will fill - the entire range including portions containing no data. %s - weights : array_like, optional - An array of weights, of the same shape as `a`. Each value in - `a` only contributes its associated weight towards the bin count - (instead of 1). This is currently not used by any of the bin estimators, - but may be in the future. + %s + %s **kwargs : - Passed to :py:func:`numpy.histogram` + Passed to :func:`numpy.histogram` Returns ------- @@ -68,22 +74,18 @@ def histogram(a, bins="fd", range=None, max_num_bins=250, weights=None, **kwargs See Also -------- - * :py:func:`numpy.histogram` + * :func:`numpy.histogram` """ if isinstance(a, da.Array): - return histogram_dask(a, bins=bins, max_num_bins=max_num_bins, **kwargs) - - if isinstance(bins, str): - _deprecated_bins = {"scotts": "scott", "freedman": "fd"} - new_bins = _deprecated_bins.get(bins, None) - if new_bins: - warnings.warn( - f"`bins='{bins}'` has been deprecated and will be removed " - f"in HyperSpy 2.0. Please use `bins='{new_bins}'` instead.", - VisibleDeprecationWarning, - ) - bins = new_bins + return histogram_dask( + a, + bins=bins, + range=range, + max_num_bins=max_num_bins, + weights=weights, + **kwargs, + ) _old_bins = bins @@ -105,9 +107,9 @@ def histogram(a, bins="fd", range=None, max_num_bins=250, weights=None, **kwargs if _bins_len > max_num_bins: # To avoid memory errors such as that detailed in # https://github.com/hyperspy/hyperspy/issues/784, - # we log a warning and cap the number of bins at + # we raise a warning and cap the number of bins at # a sensible value. - _logger.warning( + warnings.warn( f"Estimated number of bins using `bins='{_old_bins}'` " f"is too large ({_bins_len}). Capping the number of bins " f"at `max_num_bins={max_num_bins}`. Consider using an " @@ -116,14 +118,21 @@ def histogram(a, bins="fd", range=None, max_num_bins=250, weights=None, **kwargs "`max_num_bins` keyword argument." ) bins = max_num_bins + kwargs["range"] = range + kwargs["weights"] = weights return np.histogram(a, bins=bins, **kwargs) -histogram.__doc__ %= (HISTOGRAM_BIN_ARGS, HISTOGRAM_MAX_BIN_ARGS) +histogram.__doc__ %= ( + HISTOGRAM_BIN_ARGS, + HISTOGRAM_RANGE_ARGS.replace("range_bins : ", "range : "), + HISTOGRAM_MAX_BIN_ARGS, + HISTOGRAM_WEIGHTS_ARGS, +) -def histogram_dask(a, bins="fd", max_num_bins=250, **kwargs): +def histogram_dask(a, bins="fd", range=None, max_num_bins=250, weights=None, **kwargs): """Enhanced histogram for dask arrays. The range keyword is ignored. Reads the data at most two times - once to @@ -144,10 +153,11 @@ def histogram_dask(a, bins="fd", max_num_bins=250, **kwargs): 'scott' Less robust estimator that that takes into account data variability and data size. - + %s + %s %s **kwargs : - Passed to :py:func:`dask.histogram` + Passed to :func:`dask.array.histogram` Returns ------- @@ -159,8 +169,8 @@ def histogram_dask(a, bins="fd", max_num_bins=250, **kwargs): See Also -------- - * :py:func:`dask.histogram` - * :py:func:`numpy.histogram` + * :func:`dask.array.histogram` + * :func:`numpy.histogram` """ if not isinstance(a, da.Array): @@ -169,17 +179,6 @@ def histogram_dask(a, bins="fd", max_num_bins=250, **kwargs): if a.ndim != 1: a = a.flatten() - if isinstance(bins, str): - _deprecated_bins = {"scotts": "scott", "freedman": "fd"} - new_bins = _deprecated_bins.get(bins, None) - if new_bins: - warnings.warn( - f"`bins='{bins}'` has been deprecated and will be removed " - f"in HyperSpy 2.0. Please use `bins='{new_bins}'` instead.", - VisibleDeprecationWarning, - ) - bins = new_bins - _old_bins = bins if isinstance(bins, str): @@ -189,7 +188,7 @@ def histogram_dask(a, bins="fd", max_num_bins=250, **kwargs): _, bins = _freedman_bw_dask(a, True) else: raise ValueError(f"Unrecognized 'bins' argument: got {bins}") - elif not np.iterable(bins): + elif range is None: kwargs["range"] = da.compute(a.min(), a.max()) _bins_len = bins if not np.iterable(bins) else len(bins) @@ -199,7 +198,7 @@ def histogram_dask(a, bins="fd", max_num_bins=250, **kwargs): # https://github.com/hyperspy/hyperspy/issues/784, # we log a warning and cap the number of bins at # a sensible value. - _logger.warning( + warnings.warn( f"Estimated number of bins using `bins='{_old_bins}'` " f"is too large ({_bins_len}). Capping the number of bins " f"at `max_num_bins={max_num_bins}`. Consider using an " @@ -208,14 +207,22 @@ def histogram_dask(a, bins="fd", max_num_bins=250, **kwargs): "`max_num_bins` keyword argument." ) bins = max_num_bins - kwargs["range"] = da.compute(a.min(), a.max()) + kwargs["weights"] = weights + if range is None: + kwargs["range"] = da.compute(a.min(), a.max()) + else: + kwargs["range"] = range h, bins = da.histogram(a, bins=bins, **kwargs) return h.compute(), bins -histogram_dask.__doc__ %= HISTOGRAM_MAX_BIN_ARGS +histogram_dask.__doc__ %= ( + HISTOGRAM_RANGE_ARGS, + HISTOGRAM_MAX_BIN_ARGS, + HISTOGRAM_WEIGHTS_ARGS, +) def _scott_bw_dask(data, return_bins=True): diff --git a/hyperspy/misc/holography/__init__.py b/hyperspy/misc/holography/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hyperspy/misc/holography/example_signals/00_ref_Vbp_130V_0V_bin2_crop.hdf5 b/hyperspy/misc/holography/example_signals/00_ref_Vbp_130V_0V_bin2_crop.hdf5 deleted file mode 100644 index 41ce464ce7..0000000000 Binary files a/hyperspy/misc/holography/example_signals/00_ref_Vbp_130V_0V_bin2_crop.hdf5 and /dev/null differ diff --git a/hyperspy/misc/holography/example_signals/01_holo_Vbp_130V_0V_bin2_crop.hdf5 b/hyperspy/misc/holography/example_signals/01_holo_Vbp_130V_0V_bin2_crop.hdf5 deleted file mode 100644 index 3e3ab04800..0000000000 Binary files a/hyperspy/misc/holography/example_signals/01_holo_Vbp_130V_0V_bin2_crop.hdf5 and /dev/null differ diff --git a/hyperspy/misc/holography/reconstruct.py b/hyperspy/misc/holography/reconstruct.py deleted file mode 100644 index 0eaf02ec71..0000000000 --- a/hyperspy/misc/holography/reconstruct.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -from scipy.fftpack import fft2, ifft2, fftshift -import matplotlib.pyplot as plt -import logging - -_logger = logging.getLogger(__name__) - - -def estimate_sideband_position( - holo_data, holo_sampling, central_band_mask_radius=None, sb='lower', high_cf=True): - """ - Finds the position of the sideband and returns its position. - - Parameters - ---------- - holo_data: ndarray - The data of the hologram. - holo_sampling: tuple - The sampling rate in both image directions. - central_band_mask_radius: float, optional - The aperture radius used to mask out the centerband. - sb : str, optional - Chooses which sideband is taken. 'lower', 'upper', 'left', or 'right'. - high_cf : bool, optional - If False, the highest carrier frequency allowed for the sideband location is equal to - half of the Nyquist frequency (Default: True). - - Returns - ------- - Tuple of the sideband position (y, x), referred to the unshifted FFT. - """ - sb_position = (0, 0) - f_freq = freq_array(holo_data.shape, holo_sampling) - # If aperture radius of centerband is not given, it will be set to 5 % of - # the Nyquist frequ.: - if central_band_mask_radius is None: - central_band_mask_radius = 0.05 * np.max(f_freq) - # A small aperture masking out the centerband. - ap_cb = 1.0 - aperture_function(f_freq, central_band_mask_radius, 1e-6) - if not high_cf: # Cut out higher frequencies, if necessary: - ap_cb *= aperture_function(f_freq, - np.max(f_freq) / (2 * np.sqrt(2)), - 1e-6) - # Imitates 0: - fft_holo = fft2(holo_data) / np.prod(holo_data.shape) - fft_filtered = fft_holo * ap_cb - # Sideband position in pixels referred to unshifted FFT - cb_position = ( - fft_filtered.shape[0] // - 2, - fft_filtered.shape[1] // - 2) # cb: center band - if sb == 'lower': - fft_sb = np.abs(fft_filtered[:cb_position[0], :]) - sb_position = np.asarray( - np.unravel_index( - fft_sb.argmax(), - fft_sb.shape)) - elif sb == 'upper': - fft_sb = np.abs(fft_filtered[cb_position[0]:, :]) - sb_position = (np.unravel_index(fft_sb.argmax(), fft_sb.shape)) - sb_position = np.asarray(np.add(sb_position, (cb_position[0], 0))) - elif sb == 'left': - fft_sb = np.abs(fft_filtered[:, :cb_position[1]]) - sb_position = np.asarray( - np.unravel_index( - fft_sb.argmax(), - fft_sb.shape)) - elif sb == 'right': - fft_sb = np.abs(fft_filtered[:, cb_position[1]:]) - sb_position = (np.unravel_index(fft_sb.argmax(), fft_sb.shape)) - sb_position = np.asarray(np.add(sb_position, (0, cb_position[1]))) - # Return sideband position: - return sb_position - - -def estimate_sideband_size(sb_position, holo_shape, sb_size_ratio=0.5): - """ - Estimates the size of sideband filter - - Parameters - ---------- - holo_shape : array_like - Holographic data array - sb_position : tuple - The sideband position (y, x), referred to the non-shifted FFT. - sb_size_ratio : float, optional - Size of sideband as a fraction of the distance to central band - - Returns - ------- - sb_size : float - Size of sideband filter - - """ - - h = np.array((np.asarray(sb_position) - np.asarray([0, 0]), - np.asarray(sb_position) - np.asarray([0, holo_shape[1]]), - np.asarray(sb_position) - np.asarray([holo_shape[0], 0]), - np.asarray(sb_position) - np.asarray(holo_shape))) * sb_size_ratio - return np.min(np.linalg.norm(h, axis=1)) - - -def reconstruct(holo_data, holo_sampling, sb_size, sb_position, sb_smoothness, - output_shape=None, plotting=False): - """Core function for holographic reconstruction. - - Parameters - ---------- - holo_data : array_like - Holographic data array - holo_sampling : tuple - Sampling rate of the hologram in y and x direction. - sb_size : float - Size of the sideband filter in pixel. - sb_position : tuple - Sideband position in pixel. - sb_smoothness: float - Smoothness of the aperture in pixel. - output_shape: tuple, optional - New output shape. - plotting : bool - Plots the masked sideband used for reconstruction. - - Returns - ------- - wav : nparray - Reconstructed electron wave - - """ - - holo_size = holo_data.shape - f_sampling = np.divide( - 1, [a * b for a, b in zip(holo_size, holo_sampling)]) - - fft_exp = fft2(holo_data) / np.prod(holo_size) - - f_freq = freq_array(holo_data.shape, holo_sampling) - - sb_size *= np.mean(f_sampling) - sb_smoothness *= np.mean(f_sampling) - aperture = aperture_function(f_freq, sb_size, sb_smoothness) - - fft_shifted = np.roll(fft_exp, sb_position[0], axis=0) - fft_shifted = np.roll(fft_shifted, sb_position[1], axis=1) - - fft_aperture = fft_shifted * aperture - - if plotting: - _, axs = plt.subplots(1, 1, figsize=(4, 4)) - axs.imshow(np.abs(fftshift(fft_aperture)), clim=(0, 0.1)) - axs.scatter( - sb_position[1], - sb_position[0], - s=10, - color='red', - marker='x') - axs.set_xlim(int(holo_size[0] / 2) - sb_size / np.mean(f_sampling), int(holo_size[0] / 2) + - sb_size / np.mean(f_sampling)) - axs.set_ylim(int(holo_size[1] / 2) - sb_size / np.mean(f_sampling), int(holo_size[1] / 2) + - sb_size / np.mean(f_sampling)) - plt.show() - - if output_shape is not None: - y_min = int(holo_size[0] / 2 - output_shape[0] / 2) - y_max = int(holo_size[0] / 2 + output_shape[0] / 2) - x_min = int(holo_size[1] / 2 - output_shape[1] / 2) - x_max = int(holo_size[1] / 2 + output_shape[1] / 2) - - fft_aperture = fftshift( - fftshift(fft_aperture)[ - y_min:y_max, x_min:x_max]) - - wav = ifft2(fft_aperture) * np.prod(holo_data.shape) - - return wav - - -def aperture_function(r, apradius, rsmooth): - """ - A smooth aperture function that decays from apradius-rsmooth to apradius+rsmooth. - - Parameters - ---------- - r : ndarray - Array of input data (e.g. frequencies) - apradius : float - Radius (center) of the smooth aperture. Decay starts at apradius - rsmooth. - rsmooth : float - Smoothness in halfwidth. rsmooth = 1 will cause a decay from 1 to 0 over 2 pixel. - """ - - return 0.5 * (1. - np.tanh((np.absolute(r) - apradius) / (0.5 * rsmooth))) - - -def freq_array(shape, sampling): - """ - Makes up a frequency array. - - Parameters - ---------- - shape : tuple - The shape of the array. - sampling: tuple - The sampling rates of the array. - - Returns - ------- - Array of the frequencies. - """ - f_freq_1d_y = np.fft.fftfreq(shape[0], sampling[0]) - f_freq_1d_x = np.fft.fftfreq(shape[1], sampling[1]) - f_freq_mesh = np.meshgrid(f_freq_1d_x, f_freq_1d_y) - f_freq = np.hypot(f_freq_mesh[0], f_freq_mesh[1]) - - return f_freq diff --git a/hyperspy/misc/holography/tools.py b/hyperspy/misc/holography/tools.py deleted file mode 100644 index 9a7e094adb..0000000000 --- a/hyperspy/misc/holography/tools.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -from scipy.fftpack import fft2 -import logging - -_logger = logging.getLogger(__name__) - - -def calculate_carrier_frequency(holo_data, sb_position, scale): - """ - Calculates fringe carrier frequency of a hologram - - Parameters - ---------- - holo_data: ndarray - The data of the hologram. - sb_position: tuple - Position of the sideband with the reference to non-shifted FFT - scale: tuple - Scale of the axes that will be used for the calculation. - - Returns - ------- - Carrier frequency - """ - - shape = holo_data.shape - origins = [np.array((0, 0)), - np.array((0, shape[1])), - np.array((shape[0], shape[1])), - np.array((shape[0], 0))] - origin_index = np.argmin( - [np.linalg.norm(origin - sb_position) for origin in origins]) - return np.linalg.norm(np.multiply( - origins[origin_index] - sb_position, scale)) - - -def estimate_fringe_contrast_fourier( - holo_data, sb_position, apodization='hanning'): - """ - Estimates average fringe contrast of a hologram by dividing amplitude - of maximum pixel of sideband by amplitude of FFT's origin. - - Parameters - ---------- - holo_data: ndarray - The data of the hologram. - sb_position: tuple - Position of the sideband with the reference to non-shifted FFT - apodization: string, None - Use 'hanning', 'hamming' or None to apply apodization window in real space before FFT - Apodization is typically needed to suppress the striking due to sharp edges - of the which often results in underestimation of the fringe contrast. (Default: 'hanning') - - Returns - ------- - Fringe contrast as a float - """ - - holo_shape = holo_data.shape - - if apodization: - if apodization == 'hanning': - window_x = np.hanning(holo_shape[0]) - window_y = np.hanning(holo_shape[1]) - elif apodization == 'hamming': - window_x = np.hamming(holo_shape[0]) - window_y = np.hamming(holo_shape[1]) - window_2d = np.sqrt(np.outer(window_x, window_y)) - data = holo_data * window_2d - else: - data = holo_data - - fft_exp = fft2(data) - - return 2 * np.abs(fft_exp[tuple(sb_position)]) / np.abs(fft_exp[0, 0]) diff --git a/hyperspy/misc/io/__init__.py b/hyperspy/misc/io/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hyperspy/misc/io/fei_stream_readers.py b/hyperspy/misc/io/fei_stream_readers.py deleted file mode 100644 index 2c33c27d24..0000000000 --- a/hyperspy/misc/io/fei_stream_readers.py +++ /dev/null @@ -1,394 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import dask.array as da -import sparse - -from numba import njit - - -class DenseSliceCOO(sparse.COO): - """Just like sparse.COO, but returning a dense array on indexing/slicing""" - - def __getitem__(self, *args, **kwargs): - obj = super().__getitem__(*args, **kwargs) - try: - return obj.todense() - except AttributeError: - # Indexing, unlike slicing, returns directly the content - return obj - - -@njit(cache=True) -def _stream_to_sparse_COO_array_sum_frames( - stream_data, - last_frame, - shape, - channels, - rebin_energy=1, - first_frame=0 - ): # pragma: no cover - navigation_index = 0 - frame_number = 0 - ysize, xsize = shape - frame_size = xsize * ysize - # workaround for empty stream, numba "doesn't support" empty list, see - # https://github.com/numba/numba/pull/2184 - # add first element and remove it at the end - data_list = [0] - coords_list = [(0, 0, 0)] - data = 0 - count_channel = None - for value in stream_data: - if frame_number < first_frame: - if value != 65535: # Same spectrum - continue - else: - navigation_index += 1 - if navigation_index == frame_size: - frame_number += 1 - navigation_index = 0 - continue - # when we reach the end of the frame, reset the navigation index to 0 - if navigation_index == frame_size: - navigation_index = 0 - frame_number += 1 - if frame_number == last_frame: - break - # if different of ‘65535’, add a count to the corresponding channel - if value != 65535: # Same spectrum - if data: - if value == count_channel: # Same channel, add a count - data += 1 - else: # a new channel, same spectrum—requires new coord - # Store previous channel - coords_list.append(( - int(navigation_index // xsize), - int(navigation_index % xsize), - int(count_channel // rebin_energy)) - ) - data_list.append(data) - # Add a count to new channel - data = 1 - # Update count channel as this is a new channel - count_channel = value - - else: # First non-zero channel of spectrum - data = 1 - # Update count channel as this is a new channel - count_channel = value - - else: # Advances one pixel - if data: # Only store coordinates if the spectrum was not empty - coords_list.append(( - int(navigation_index // xsize), - int(navigation_index % xsize), - int(count_channel // rebin_energy)) - ) - data_list.append(data) - navigation_index += 1 - data = 0 - - # Store data at the end if any (there is no final 65535 to mark the end - # of the stream) - if data: # Only store coordinates if the spectrum was not empty - coords_list.append(( - int(navigation_index // xsize), - int(navigation_index % xsize), - int(count_channel // rebin_energy)) - ) - data_list.append(data) - - final_shape = (ysize, xsize, channels // rebin_energy) - # Remove first element, see comments above - coords = np.array(coords_list)[1:].T - data = np.array(data_list)[1:] - return coords, data, final_shape - - -@njit(cache=True) -def _stream_to_sparse_COO_array( - stream_data, - last_frame, - shape, - channels, - rebin_energy=1, - first_frame=0 - ): # pragma: no cover - navigation_index = 0 - frame_number = 0 - ysize, xsize = shape - frame_size = xsize * ysize - # workaround for empty stream, numba "doesn't support" empty list, see - # https://github.com/numba/numba/pull/2184 - # add first element and remove it at the end - data_list = [0] - coords = [(0, 0, 0, 0)] - data = 0 - count_channel = None - for value in stream_data: - if frame_number < first_frame: - if value != 65535: # Same spectrum - continue - else: - navigation_index += 1 - if navigation_index == frame_size: - frame_number += 1 - navigation_index = 0 - continue - # when we reach the end of the frame, reset the navigation index to 0 - if navigation_index == frame_size: - navigation_index = 0 - frame_number += 1 - if frame_number == last_frame: - break - # if different of ‘65535’, add a count to the corresponding channel - if value != 65535: # Same spectrum - if data: - if value == count_channel: # Same channel, add a count - data += 1 - else: # a new channel, same spectrum—requires new coord - # Store previous channel - coords.append(( - frame_number - first_frame, - int(navigation_index // xsize), - int(navigation_index % xsize), - int(count_channel // rebin_energy)) - ) - data_list.append(data) - # Add a count to new channel - data = 1 - # Update count channel as this is a new channel - count_channel = value - - else: # First non-zero channel of spectrum - data = 1 - # Update count channel as this is a new channel - count_channel = value - - else: # Advances one pixel - if data: # Only store coordinates if the spectrum was not empty - coords.append(( - frame_number - first_frame, - int(navigation_index // xsize), - int(navigation_index % xsize), - int(count_channel // rebin_energy)) - ) - data_list.append(data) - navigation_index += 1 - data = 0 - - # Store data at the end if any (there is no final 65535 to mark the end of - # the stream) - if data: # Only store coordinates if the spectrum was not empty - coords.append(( - frame_number - first_frame, - int(navigation_index // xsize), - int(navigation_index % xsize), - int(count_channel // rebin_energy)) - ) - data_list.append(data) - - final_shape = (last_frame - first_frame, ysize, xsize, - channels // rebin_energy) - # Remove first element, see comments above - coords = np.array(coords)[1:].T - data = np.array(data_list)[1:] - return coords, data, final_shape - - -def stream_to_sparse_COO_array( - stream_data, spatial_shape, channels, last_frame, rebin_energy=1, - sum_frames=True, first_frame=0, ): - """Returns data stored in a FEI stream as a nd COO array - - Parameters - ---------- - stream_data: numpy array - spatial_shape: tuple of ints - (ysize, xsize) - channels: ints - Number of channels in the spectrum - rebin_energy: int - Rebin the spectra. The default is 1 (no rebinning applied) - sum_frames: bool - If True, sum all the frames - - """ - if sum_frames: - coords, data, shape = _stream_to_sparse_COO_array_sum_frames( - stream_data=stream_data, - shape=spatial_shape, - channels=channels, - rebin_energy=rebin_energy, - first_frame=first_frame, - last_frame=last_frame, - ) - else: - coords, data, shape = _stream_to_sparse_COO_array( - stream_data=stream_data, - shape=spatial_shape, - channels=channels, - rebin_energy=rebin_energy, - first_frame=first_frame, - last_frame=last_frame, - ) - dense_sparse = DenseSliceCOO(coords=coords, data=data, shape=shape) - dask_sparse = da.from_array(dense_sparse, chunks="auto") - return dask_sparse - - -@njit(cache=True) -def _fill_array_with_stream_sum_frames( - spectrum_image, - stream, - first_frame, - last_frame, - rebin_energy=1 - ): # pragma: no cover - # jit speeds up this function by a factor of ~ 30 - navigation_index = 0 - frame_number = 0 - shape = spectrum_image.shape - for count_channel in np.nditer(stream): - # when we reach the end of the frame, reset the navigation index to 0 - if navigation_index == (shape[0] * shape[1]): - navigation_index = 0 - frame_number += 1 - # break the for loop when we reach the last frame we want to read - if frame_number == last_frame: - break - # if different of ‘65535’, add a count to the corresponding channel - if count_channel != 65535: - if first_frame <= frame_number: - spectrum_image[navigation_index // shape[1], - navigation_index % shape[1], - count_channel // rebin_energy] += 1 - else: - navigation_index += 1 - - -@njit(cache=True) -def _fill_array_with_stream( - spectrum_image, - stream, - first_frame, - last_frame, - rebin_energy=1 - ): # pragma: no cover - navigation_index = 0 - frame_number = 0 - shape = spectrum_image.shape - for count_channel in np.nditer(stream): - # when we reach the end of the frame, reset the navigation index to 0 - if navigation_index == (shape[1] * shape[2]): - navigation_index = 0 - frame_number += 1 - # break the for loop when we reach the last frame we want to read - if frame_number == last_frame: - break - # if different of ‘65535’, add a count to the corresponding channel - if count_channel != 65535: - if first_frame <= frame_number: - spectrum_image[frame_number - first_frame, - navigation_index // shape[2], - navigation_index % shape[2], - count_channel // rebin_energy] += 1 - else: - navigation_index += 1 - - -def stream_to_array( - stream, spatial_shape, channels, last_frame, first_frame=0, - rebin_energy=1, sum_frames=True, dtype="uint16", spectrum_image=None): - """Returns data stored in a FEI stream as a nd COO array - - Parameters - ---------- - stream: numpy array - spatial_shape: tuple of ints - (ysize, xsize) - channels: ints - Number of channels in the spectrum - rebin_energy: int - Rebin the spectra. The default is 1 (no rebinning applied) - sum_frames: bool - If True, sum all the frames - dtype: numpy dtype - dtype of the array where to store the data - number_of_frame: int or None - spectrum_image: numpy array or None - If not None, the array provided will be filled with the data in the - stream. - - """ - - frames = last_frame - first_frame - if not sum_frames: - if spectrum_image is None: - spectrum_image = np.zeros( - (frames, spatial_shape[0], spatial_shape[1], - int(channels / rebin_energy)), - dtype=dtype) - - _fill_array_with_stream( - spectrum_image=spectrum_image, - stream=stream, - first_frame=first_frame, - last_frame=last_frame, - rebin_energy=rebin_energy) - else: - if spectrum_image is None: - spectrum_image = np.zeros( - (spatial_shape[0], spatial_shape[1], - int(channels / rebin_energy)), - dtype=dtype) - _fill_array_with_stream_sum_frames( - spectrum_image=spectrum_image, - stream=stream, - first_frame=first_frame, - last_frame=last_frame, - rebin_energy=rebin_energy) - return spectrum_image - - -@njit(cache=True) -def array_to_stream(array): # pragma: no cover - """Convert an array to a FEI stream - - Parameters - ---------- - array: array - - """ - - channels = array.shape[-1] - flat_array = array.ravel() - stream_data = [] - channel = 0 - for value in flat_array: - for j in range(value): - stream_data.append(channel) - channel += 1 - if channel % channels == 0: - channel = 0 - stream_data.append(65535) - stream_data = stream_data[:-1] # Remove final mark - stream_data = np.array(stream_data) - return stream_data diff --git a/hyperspy/misc/io/tools.py b/hyperspy/misc/io/tools.py deleted file mode 100644 index 1fb3fe812e..0000000000 --- a/hyperspy/misc/io/tools.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - - -import logging -import xml.etree.ElementTree as ET -from pathlib import Path - -from hyperspy.misc.utils import DictionaryTreeBrowser - - -_logger = logging.getLogger(__name__) - - -def dump_dictionary( - file, dic, string="root", node_separator=".", value_separator=" = " -): - for key in list(dic.keys()): - if isinstance(dic[key], dict): - dump_dictionary(file, dic[key], string + node_separator + key) - else: - file.write( - string + node_separator + key + value_separator + str(dic[key]) + "\n" - ) - - -def append2pathname(filename, to_append): - """Append a string to a path name - - Parameters - ---------- - filename : str - to_append : str - - """ - p = Path(filename) - return Path(p.parent, p.stem + to_append, p.suffix) - - -def incremental_filename(filename, i=1): - """If a file with the same file name exists, returns a new filename that - does not exists. - - The new file name is created by appending `-n` (where `n` is an integer) - to path name - - Parameters - ---------- - filename : str - i : int - The number to be appended. - """ - filename = Path(filename) - - if filename.is_file(): - new_filename = append2pathname(filename, "-{i}") - if new_filename.is_file(): - return incremental_filename(filename, i + 1) - else: - return new_filename - else: - return filename - - -def ensure_directory(path): - """Check if the path exists and if it does not, creates the directory.""" - # If it's a file path, try the parent directory instead - p = Path(path) - p = p.parent if p.is_file() else p - - try: - p.mkdir(parents=True, exist_ok=False) - except FileExistsError: - _logger.debug(f"Directory {p} already exists. Doing nothing.") - - -def overwrite(fname): - """ If file exists 'fname', ask for overwriting and return True or False, - else return True. - - Parameters - ---------- - fname : str or pathlib.Path - File to check for overwriting. - - Returns - ------- - bool : - Whether to overwrite file. - - """ - if Path(fname).is_file(): - message = f"Overwrite '{fname}' (y/n)?\n" - try: - answer = input(message) - answer = answer.lower() - while (answer != "y") and (answer != "n"): - print("Please answer y or n.") - answer = input(message) - if answer.lower() == "y": - return True - elif answer.lower() == "n": - return False - except: - # We are running in the IPython notebook that does not - # support raw_input - _logger.info( - "Your terminal does not support raw input. " - "Not overwriting. " - "To overwrite the file use `overwrite=True`" - ) - return False - else: - return True - - -def xml2dtb(et, dictree): - if et.text: - dictree[et.tag] = et.text - return - else: - dictree.add_node(et.tag) - if et.attrib: - dictree[et.tag].add_dictionary(et.attrib) - for child in et: - xml2dtb(child, dictree[et.tag]) - - -def convert_xml_to_dict(xml_object): - if isinstance(xml_object, str): - xml_object = ET.fromstring(xml_object) - op = DictionaryTreeBrowser() - xml2dtb(xml_object, op) - return op diff --git a/hyperspy/misc/io/utils_readfile.py b/hyperspy/misc/io/utils_readfile.py deleted file mode 100644 index 26607de844..0000000000 --- a/hyperspy/misc/io/utils_readfile.py +++ /dev/null @@ -1,256 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2010 Stefano Mazzucco -# -# This file is part of dm3_data_plugin. -# -# dm3_data_plugin is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# dm3_data_plugin is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 -# USA - -# general functions for reading data from files - -import struct -import logging - -from hyperspy.exceptions import ByteOrderError - -_logger = logging.getLogger(__name__) - -# Declare simple TagDataType structures for faster execution. -# The variables are named as following: -# Endianness_type -# Endianness = B (big) or L (little) -# type = (u)short, (u)long, float, double, bool (unsigned char), -# byte (signed char), char - -B_short = struct.Struct('>h') -L_short = struct.Struct('H') -L_ushort = struct.Struct('l') -L_long = struct.Struct('q') -L_long_long = struct.Struct('L') -L_ulong = struct.Struct('Q') -L_ulong_long = struct.Struct('f') -L_float = struct.Struct('d') -L_double = struct.Struct('B') # use unsigned char -L_bool = struct.Struct('b') # use signed char -L_byte = struct.Struct('c') -L_char = struct.Struct('. +# along with HyperSpy. If not, see . -import __main__ -from distutils.version import LooseVersion - -from time import strftime from pathlib import Path +from time import strftime + +from packaging.version import Version + +import __main__ def get_ipython(): @@ -30,17 +31,10 @@ def get_ipython(): """ if is_it_running_from_ipython is False: return None + import IPython - ipy_version = LooseVersion(IPython.__version__) - if ipy_version < LooseVersion("0.11"): - from IPython import ipapi - ip = ipapi.get() - elif ipy_version < LooseVersion("1.0"): - from IPython.core import ipapi - ip = ipapi.get() - else: - ip = IPython.get_ipython() - return ip + + return IPython.get_ipython() def is_it_running_from_ipython(): @@ -63,9 +57,10 @@ def turn_logging_on(verbose=1): ip = get_ipython() if ip is None: return - import IPython - ipy_version = LooseVersion(IPython.__version__) - if ipy_version < LooseVersion("0.11"): + from IPython import __version__ as ipythonversion + + ipy_version = Version(ipythonversion) + if ipy_version < Version("0.11"): if verbose == 1: print("Logging is not supported by this version of IPython") return @@ -74,29 +69,33 @@ def turn_logging_on(verbose=1): print("Already logging to " + ip.logger.logfname) return - filename = Path.cwd().joinpath('hyperspy_log.py') + filename = Path.cwd().joinpath("hyperspy_log.py") new = not filename.is_file() - ip.logger.logstart(logfname=filename, logmode='append') + ip.logger.logstart(logfname=filename, logmode="append") if new: ip.logger.log_write( "#!/usr/bin/env python \n" "# ============================\n" - "# %s \n" % strftime('%Y-%m-%d') + - "# %s \n" % strftime('%H:%M') + - "# ============================\n") + "# %s \n" + % strftime("%Y-%m-%d") + + "# %s \n" % strftime("%H:%M") + + "# ============================\n" + ) if verbose == 1: print("\nLogging is active") - print("The log is stored in the hyperspy_log.py file" - " in the current directory") + print( + "The log is stored in the hyperspy_log.py file" " in the current directory" + ) def turn_logging_off(): ip = get_ipython() if ip is None: return - import IPython - ipy_version = LooseVersion(IPython.__version__) - if ipy_version < LooseVersion("0.11"): + from IPython import __version__ as ipythonversion + + ipy_version = Version(ipythonversion) + if ipy_version < Version("0.11"): print("Logging is not supported by this version of IPython") return elif ip.logger.log_active is False: diff --git a/hyperspy/misc/label_position.py b/hyperspy/misc/label_position.py deleted file mode 100644 index 922af6d720..0000000000 --- a/hyperspy/misc/label_position.py +++ /dev/null @@ -1,232 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import copy -import itertools - -import numpy as np -from hyperspy.drawing.marker import markers - -class SpectrumLabelPosition(): - ''' - A class to get the positions of labels in spectrums such as EELS, EDX, - XRF. The main method is the get_markers method which the user supplies a - dictionary of edge labels specifying its energy positions, and it returns - markers for labelling them. - ''' - - edge_label_style = {'ha' : 'center', 'va' : 'center', - 'bbox' : dict(facecolor='white', alpha=0.2)} - colour_list_label = ['black', 'darkblue', 'darkgreen', - 'darkcyan', 'darkmagenta', 'dimgray', - 'brown', 'deeppink', 'olive', - 'crimson'] - - def __init__(self, signal): - self.signal = signal - self.axis = self.signal.axes_manager.signal_axes[0] - self._ele_col_dict = {} - self.color_cycle = itertools.cycle(self.colour_list_label) - - self._set_active_figure_properties() - - def _set_active_figure_properties(self): - # set the properties which depend on the figure - self.figure = self.signal._plot.signal_plot.figure - self.figsize = self.figure.get_size_inches() - self.smin, self.smax = self.signal._plot.signal_plot.ax.get_ylim() - - self.sig_index = self._get_current_signal_index() - self.text_width, self.text_height = self._estimate_textbox_dimension() - - def _get_current_signal_index(self): - # if it is a hyperspectrum, get the correct active figure - if self.signal._plot.pointer is not None: - sig_index = self.signal._plot.pointer.indices[0] - else: - sig_index = 0 - - return sig_index - - def _check_signal_figure_changed(self): - # check if the spectrum is changed - # reset its properties if changed - current_sig_index = self._get_current_signal_index() - current_figsize = self.figure.get_size_inches() - if (current_sig_index != self.sig_index) or \ - not np.isclose(current_figsize, self.figsize).all(): - return True - else: - return False - - def _get_bbox_from_text_artist(self, text): - """ - Parameters - ---------- - text : matplotlib.text.Text instance - - """ - # get the bbox object of the textbox - ax = text.axes - fig = text.figure - r = text.figure.canvas.get_renderer() - fig.draw(r) - extent = text.get_bbox_patch().get_window_extent() - bbox_patch = extent.transformed(ax.transData.inverted()) - - return bbox_patch - - def _estimate_textbox_dimension(self, dummy_text='My_g8'): - # get the dimension of a typical textbox in the current figure - dummy_style = copy.deepcopy(self.edge_label_style) - dummy_style['bbox']['alpha'] = 0 - dummy_style['alpha'] = 0 - tx = markers.text.Text(x=(self.axis.low_value+self.axis.high_value)/2, - y=(self.smin+self.smax)/2, - text=self._text_parser(dummy_text), - **dummy_style) - self.signal.add_marker(tx) - - dummybb = self._get_bbox_from_text_artist(tx.marker) - tx.close() - - text_width = dummybb.width - text_height = dummybb.height - - return text_width, text_height - - def get_markers(self, labels): - '''Get the markers (vertical line segment and text box) for labelling - the edges - - Parameters - ---------- - labels : dictionary - A dictionary with the labels as keys and their energies as values. - E.g. for EELS edges it could be {'Mn_L2': 651.0, 'Cr_L3': 575.0}. - - Returns - ------- - vls : list - A list contains HyperSpy's vertical line segment marker - txs : list - A list contains HyperSpy's text marker - ''' - - xytext = self._get_textbox_pos(labels) - - vls = [] - txs = [] - for xyt in xytext: - vl = markers.vertical_line_segment.VerticalLineSegment(x=xyt[0], - y1=xyt[1], - y2=xyt[2], - color=xyt[4]) - tx = markers.text.Text(x=xyt[0], y=xyt[1], - text=self._text_parser(xyt[3]), color=xyt[4], - **self.edge_label_style) - - vl.events.closed.connect(self.signal._edge_marker_closed) - tx.events.closed.connect(self.signal._edge_marker_closed) - vl.auto_update = True - tx.auto_update = True - - vls.append(vl) - txs.append(tx) - - return vls, txs - - def _get_textbox_pos(self, edges, offset=None, step=None, lb=None, - ub=None): - # get the information on placing the textbox and its properties - if offset is None: - offset = self.text_height - if step is None: - step = self.text_height - if lb is None: - lb = self.smin + offset - if ub is None: - ub = self.smax - offset - - if not self._ele_col_dict: - self._ele_col_dict = self._element_colour_dict(edges) - - mid = (self.smax + self.smin) / 2 - itop = 1 - ibtm = 1 - - xytext = [] - for edge in edges: - energy = edges[edge] - - yval = self.signal.isig[float(energy)].data[self.sig_index] - if yval <= mid: # from top - y = ub - itop*step - if y <= lb: - itop = 1 - y = ub - itop*step - itop += 1 - else: # from bottom - y = lb + ibtm*step - if y >= ub: - ibtm = 1 - y = lb + ibtm*step - ibtm += 1 - - try: - c = self._ele_col_dict[edge.split('_')[0]] - except KeyError: - self._ele_col_dict[edge.split('_')[0]] = next(self.color_cycle) - c = self._ele_col_dict[edge.split('_')[0]] - - xytext.append((energy, y, yval, edge, c)) - - return xytext - - def _element_colour_dict(self, edges): - # assign a colour to each element of the edges - if isinstance(edges, dict): - edges = edges.keys() - - elements = self._unique_element_of_edges(edges) - - d = {} - for element in elements: - d[element] = next(self.color_cycle) - - return d - - def _unique_element_of_edges(self, edges): - # get the unique elements present in a sequence of edges - elements = set() - for edge in edges: - element, _ = edge.split('_') - elements.update([element]) - - return elements - - def _text_parser(self, text_edge): - # format the edge labels for LaTeX - element, subshell = text_edge.split('_') - - if subshell[-1].isdigit(): - formatted = f"{element} {subshell[0]}$_{subshell[-1]}$" - else: - formatted = f"{element} {subshell[0]}" - - return formatted diff --git a/hyperspy/misc/lowess_smooth.py b/hyperspy/misc/lowess_smooth.py index 00f47107b4..09d6b2af45 100755 --- a/hyperspy/misc/lowess_smooth.py +++ b/hyperspy/misc/lowess_smooth.py @@ -18,7 +18,8 @@ # https://gist.github.com/agramfort/850437 import numpy as np -from numba import njit + +from hyperspy.decorators import jit_ifnumba def lowess(y, x, f=2.0 / 3.0, n_iter=3): @@ -51,16 +52,16 @@ def lowess(y, x, f=2.0 / 3.0, n_iter=3): return _lowess(y, x, f, n_iter) -@njit(cache=True, nogil=True) +@jit_ifnumba(cache=True, nogil=True) def _lowess(y, x, f=2.0 / 3.0, n_iter=3): # pragma: no cover - """Lowess smoother requiring native endian datatype (for numba). - - """ + """Lowess smoother requiring native endian datatype (for numba).""" n = len(x) r = int(np.ceil(f * n)) h = np.array([np.sort(np.abs(x - x[i]))[r] for i in range(n)]) - w = np.minimum(1.0, np.maximum(np.abs((x.reshape((-1, 1)) - x.reshape((1, -1))) / h), 0.0)) - w = (1 - w ** 3) ** 3 + w = np.minimum( + 1.0, np.maximum(np.abs((x.reshape((-1, 1)) - x.reshape((1, -1))) / h), 0.0) + ) + w = (1 - w**3) ** 3 yest = np.zeros(n) delta = np.ones(n) @@ -80,8 +81,8 @@ def _lowess(y, x, f=2.0 / 3.0, n_iter=3): # pragma: no cover residuals = y - yest s = np.median(np.abs(residuals)) - #delta = np.clip(residuals / (6.0 * s), -1.0, 1.0) + # delta = np.clip(residuals / (6.0 * s), -1.0, 1.0) delta = np.minimum(1.0, np.maximum(residuals / (6.0 * s), -1.0)) - delta = (1 - delta ** 2) ** 2 + delta = (1 - delta**2) ** 2 return yest diff --git a/hyperspy/misc/machine_learning/import_sklearn.py b/hyperspy/misc/machine_learning/import_sklearn.py index 5d6c11dc09..97c7c45ece 100644 --- a/hyperspy/misc/machine_learning/import_sklearn.py +++ b/hyperspy/misc/machine_learning/import_sklearn.py @@ -1,40 +1,41 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . """ Import sklearn.* and randomized_svd from scikit-learn """ +import importlib import warnings +sklearn_spec = importlib.util.find_spec("sklearn") -try: +if sklearn_spec is None: # pragma: no cover + randomized_svd = None + sklearn_installed = False +else: with warnings.catch_warnings(): warnings.simplefilter("ignore") - import sklearn - import sklearn.decomposition - import sklearn.cluster - import sklearn.preprocessing - import sklearn.metrics - from sklearn.utils.extmath import randomized_svd + import sklearn # noqa: F401 + import sklearn.cluster # noqa: F401 + import sklearn.decomposition # noqa: F401 + import sklearn.metrics # noqa: F401 + import sklearn.preprocessing # noqa: F401 + from sklearn.utils.extmath import randomized_svd # noqa: F401 sklearn_installed = True - -except ImportError: # pragma: no cover - randomized_svd = None - sklearn_installed = False diff --git a/hyperspy/misc/machine_learning/tools.py b/hyperspy/misc/machine_learning/tools.py index 53116d6efd..ff5b265ba3 100644 --- a/hyperspy/misc/machine_learning/tools.py +++ b/hyperspy/misc/machine_learning/tools.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -47,7 +47,7 @@ def amari(W, A): P = W @ A m, _ = P.shape - P_sq = P ** 2 + P_sq = P**2 P_sq_sum_0 = np.sum(P_sq, axis=0) P_sq_max_0 = np.max(P_sq, axis=0) diff --git a/hyperspy/misc/material.py b/hyperspy/misc/material.py deleted file mode 100644 index 486c8bf2a6..0000000000 --- a/hyperspy/misc/material.py +++ /dev/null @@ -1,498 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from collections.abc import Iterable -import numpy as np -import numbers -import copy - -from hyperspy.misc.elements import elements as elements_db -from hyperspy.misc.eds.ffast_mac import ffast_mac_db as ffast_mac -from hyperspy.misc.eds import utils as utils_eds -from hyperspy.misc.utils import stack - - -def _weight_to_atomic(weight_percent, elements): - """Convert weight percent (wt%) to atomic percent (at.%). - - Parameters - ---------- - weight_percent: array of float - The weight fractions (composition) of the sample. - elements: list of str - A list of element abbreviations, e.g. ['Al','Zn'] - - Returns - ------- - atomic_percent : array of float - Composition in atomic percent. - - Calculate the atomic percent of modern bronze given its weight percent: - >>> hs.material.weight_to_atomic((88, 12), ("Cu", "Sn")) - array([ 93.19698614, 6.80301386]) - - """ - if len(elements) != len(weight_percent): - raise ValueError( - 'The number of elements must match the size of the first axis' - 'of weight_percent.') - atomic_weights = np.array( - [elements_db[element]['General_properties']['atomic_weight'] - for element in elements]) - atomic_percent = np.array( - list(map(np.divide, weight_percent, atomic_weights))) - sum_weight = atomic_percent.sum(axis=0) / 100. - for i, el in enumerate(elements): - atomic_percent[i] /= sum_weight - atomic_percent[i] = np.where(sum_weight == 0.0, 0.0, atomic_percent[i]) - return atomic_percent - - -def weight_to_atomic(weight_percent, elements='auto'): - """Convert weight percent (wt%) to atomic percent (at.%). - - Parameters - ---------- - weight_percent: list of float or list of signals - The weight fractions (composition) of the sample. - elements: list of str - A list of element abbreviations, e.g. ['Al','Zn']. If elements is - 'auto', take the elements in en each signal metadata of th - weight_percent list. - - Returns - ------- - atomic_percent : as weight_percent - Composition in atomic percent. - - Examples - -------- - Calculate the atomic percent of modern bronze given its weight percent: - >>> hs.material.weight_to_atomic((88, 12), ("Cu", "Sn")) - array([ 93.19698614, 6.80301386]) - - """ - from hyperspy.signals import BaseSignal - elements = _elements_auto(weight_percent, elements) - - if isinstance(weight_percent[0], BaseSignal): - atomic_percent = stack(weight_percent) - atomic_percent.data = _weight_to_atomic( - atomic_percent.data, elements) - atomic_percent.data = np.nan_to_num(atomic_percent.data) - atomic_percent = atomic_percent.split() - for i, el in enumerate(elements): - atomic_percent[i].metadata.General.title = 'atomic percent of ' + el - return atomic_percent - else: - return _weight_to_atomic(weight_percent, elements) - - -def _atomic_to_weight(atomic_percent, elements): - """Convert atomic percent to weight percent. - - Parameters - ---------- - atomic_percent: array - The atomic fractions (composition) of the sample. - elements: list of str - A list of element abbreviations, e.g. ['Al','Zn'] - - Returns - ------- - weight_percent : array of float - composition in weight percent. - - Examples - -------- - Calculate the weight percent of modern bronze given its atomic percent: - >>> hs.material.atomic_to_weight([93.2, 6.8], ("Cu", "Sn")) - array([ 88.00501989, 11.99498011]) - - """ - if len(elements) != len(atomic_percent): - raise ValueError( - 'The number of elements must match the size of the first axis' - 'of atomic_percent.') - atomic_weights = np.array( - [elements_db[element]['General_properties']['atomic_weight'] - for element in elements]) - weight_percent = np.array( - list(map(np.multiply, atomic_percent, atomic_weights))) - sum_atomic = weight_percent.sum(axis=0) / 100. - for i, el in enumerate(elements): - weight_percent[i] /= sum_atomic - weight_percent[i] = np.where(sum_atomic == 0.0, 0.0, weight_percent[i]) - return weight_percent - - -def atomic_to_weight(atomic_percent, elements='auto'): - """Convert atomic percent to weight percent. - - Parameters - ---------- - atomic_percent: list of float or list of signals - The atomic fractions (composition) of the sample. - elements: list of str - A list of element abbreviations, e.g. ['Al','Zn']. If elements is - 'auto', take the elements in en each signal metadata of the - atomic_percent list. - - Returns - ------- - weight_percent : as atomic_percent - composition in weight percent. - - Examples - -------- - Calculate the weight percent of modern bronze given its atomic percent: - >>> hs.material.atomic_to_weight([93.2, 6.8], ("Cu", "Sn")) - array([ 88.00501989, 11.99498011]) - - """ - from hyperspy.signals import BaseSignal - elements = _elements_auto(atomic_percent, elements) - if isinstance(atomic_percent[0], BaseSignal): - weight_percent = stack(atomic_percent, show_progressbar=False) - weight_percent.data = _atomic_to_weight( - weight_percent.data, elements) - weight_percent = weight_percent.split() - for i, el in enumerate(elements): - atomic_percent[i].metadata.General.title = 'weight percent of ' + el - return weight_percent - else: - return _atomic_to_weight(atomic_percent, elements) - - -def _density_of_mixture(weight_percent, - elements, - mean='harmonic'): - """Calculate the density a mixture of elements. - - The density of the elements is retrieved from an internal database. The - calculation is only valid if there is no interaction between the - components. - - Parameters - ---------- - weight_percent: array - A list of weight percent for the different elements. If the total - is not equal to 100, each weight percent is divided by the sum - of the list (normalization). - elements: list of str - A list of element symbols, e.g. ['Al', 'Zn'] - mean: 'harmonic' or 'weighted' - The type of mean use to estimate the density - - Returns - ------- - density: The density in g/cm3. - - Examples - -------- - Calculate the density of modern bronze given its weight percent: - >>> hs.material.density_of_mixture([88, 12],['Cu', 'Sn']) - 8.6903187973131466 - - """ - if len(elements) != len(weight_percent): - raise ValueError( - 'The number of elements must match the size of the first axis' - 'of weight_percent.') - densities = np.array( - [elements_db[element]['Physical_properties']['density (g/cm^3)'] - for element in elements]) - sum_densities = np.zeros_like(weight_percent, dtype='float') - if mean == 'harmonic': - for i, weight in enumerate(weight_percent): - sum_densities[i] = weight / densities[i] - sum_densities = sum_densities.sum(axis=0) - density = np.sum(weight_percent, axis=0) / sum_densities - return np.where(sum_densities == 0.0, 0.0, density) - elif mean == 'weighted': - for i, weight in enumerate(weight_percent): - sum_densities[i] = weight * densities[i] - sum_densities = sum_densities.sum(axis=0) - sum_weight = np.sum(weight_percent, axis=0) - density = sum_densities / sum_weight - return np.where(sum_weight == 0.0, 0.0, density) - - -def density_of_mixture(weight_percent, - elements='auto', - mean='harmonic'): - """Calculate the density of a mixture of elements. - - The density of the elements is retrieved from an internal database. The - calculation is only valid if there is no interaction between the - components. - - Parameters - ---------- - weight_percent: list of float or list of signals - A list of weight percent for the different elements. If the total - is not equal to 100, each weight percent is divided by the sum - of the list (normalization). - elements: list of str - A list of element symbols, e.g. ['Al', 'Zn']. If elements is 'auto', - take the elements in en each signal metadata of the weight_percent - list. - mean: 'harmonic' or 'weighted' - The type of mean use to estimate the density - - Returns - ------- - density: The density in g/cm3. - - Examples - -------- - Calculate the density of modern bronze given its weight percent: - >>> hs.material.density_of_mixture([88, 12],['Cu', 'Sn']) - 8.6903187973131466 - - """ - from hyperspy.signals import BaseSignal - elements = _elements_auto(weight_percent, elements) - if isinstance(weight_percent[0], BaseSignal): - density = weight_percent[0]._deepcopy_with_new_data( - _density_of_mixture(stack(weight_percent).data, - elements, mean=mean)) - return density - else: - return _density_of_mixture(weight_percent, elements, mean=mean) - - -def mass_absorption_coefficient(element, energies): - """ - Mass absorption coefficient (mu/rho) of a X-ray absorbed in a pure - material. - - The mass absorption is retrieved from the database of Chantler2005 - - Parameters - ---------- - element: str - The element symbol of the absorber, e.g. 'Al'. - energies: float or list of float or str or list of str - The energy or energies of the X-ray in keV, or the name of the X-rays, - e.g. 'Al_Ka'. - - Return - ------ - mass absorption coefficient(s) in cm^2/g - - Examples - -------- - >>> hs.material.mass_absorption_coefficient( - >>> element='Al', energies=['C_Ka','Al_Ka']) - array([ 26330.38933818, 372.02616732]) - - See also - -------- - :py:func:`~hs.material.mass_absorption_mixture` - - Note - ---- - See http://physics.nist.gov/ffast - Chantler, C.T., Olsen, K., Dragoset, R.A., Kishore, A.R., Kotochigova, - S.A., and Zucker, D.S. (2005), X-Ray Form Factor, Attenuation and - Scattering Tables (version 2.1). - """ - energies_db = np.array(ffast_mac[element].energies_keV) - macs = np.array(ffast_mac[element].mass_absorption_coefficient_cm2g) - energies = copy.copy(energies) - if isinstance(energies, str): - energies = utils_eds._get_energy_xray_line(energies) - elif isinstance(energies, Iterable): - for i, energy in enumerate(energies): - if isinstance(energy, str): - energies[i] = utils_eds._get_energy_xray_line(energy) - index = np.searchsorted(energies_db, energies) - mac_res = np.exp(np.log(macs[index - 1]) + - np.log(macs[index] / macs[index - 1]) * - (np.log(energies / energies_db[index - 1]) / - np.log(energies_db[index] / energies_db[index - 1]))) - return np.nan_to_num(mac_res) - - -def _mass_absorption_mixture(weight_percent, - elements, - energies): - """Calculate the mass absorption coefficient for X-ray absorbed in a - mixture of elements. - - The mass absorption coefficient is calculated as a weighted mean of the - weight percent and is retrieved from the database of Chantler2005. - - Parameters - ---------- - weight_percent: np.array - The composition of the absorber(s) in weight percent. The first - dimension of the matrix corresponds to the elements. - elements: list of str - The list of element symbol of the absorber, e.g. ['Al','Zn']. - energies: float or list of float or str or list of str - The energy or energies of the X-ray in keV, or the name of the X-rays, - e.g. 'Al_Ka'. - - Examples - -------- - >>> hs.material.mass_absorption_mixture( - >>> elements=['Al','Zn'], weight_percent=[50,50], energies='Al_Ka') - 2587.4161643905127 - - Return - ------ - float or array of float - mass absorption coefficient(s) in cm^2/g - - See also - -------- - :py:func:`~hs.material.mass_absorption` - - Note - ---- - See http://physics.nist.gov/ffast - Chantler, C.T., Olsen, K., Dragoset, R.A., Kishore, A.R., Kotochigova, - S.A., and Zucker, D.S. (2005), X-Ray Form Factor, Attenuation and - Scattering Tables (version 2.1). - """ - if len(elements) != len(weight_percent): - raise ValueError( - "Elements and weight_fraction should have the same length") - if isinstance(weight_percent[0], Iterable): - weight_fraction = np.array(weight_percent) - weight_fraction /= np.sum(weight_fraction, 0) - mac_res = np.zeros([len(energies)] + list(weight_fraction.shape[1:])) - for element, weight in zip(elements, weight_fraction): - mac_re = mass_absorption_coefficient(element, energies) - mac_res += np.array([weight * ma for ma in mac_re]) - return mac_res - else: - mac_res = np.array([mass_absorption_coefficient( - el, energies) for el in elements]) - mac_res = np.dot(weight_percent, mac_res) / np.sum(weight_percent, 0) - return mac_res - - -def mass_absorption_mixture(weight_percent, - elements='auto', - energies='auto'): - """Calculate the mass absorption coefficient for X-ray absorbed in a - mixture of elements. - - The mass absorption coefficient is calculated as a weighted mean of the - weight percent and is retrieved from the database of Chantler2005. - - Parameters - ---------- - weight_percent: list of float or list of signals - The composition of the absorber(s) in weight percent. The first - dimension of the matrix corresponds to the elements. - elements: list of str or 'auto' - The list of element symbol of the absorber, e.g. ['Al','Zn']. If - elements is 'auto', take the elements in each signal metadata of the - weight_percent list. - energies: list of float or list of str or 'auto' - The energy or energies of the X-ray in keV, or the name of the X-rays, - e.g. 'Al_Ka'. If 'auto', take the lines in each signal metadata of the - weight_percent list. - - Examples - -------- - >>> hs.material.mass_absorption_mixture( - >>> elements=['Al','Zn'], weight_percent=[50,50], energies='Al_Ka') - 2587.41616439 - - Return - ------ - float or array of float - mass absorption coefficient(s) in cm^2/g - - See also - -------- - :py:func:`~hs.material.mass_absorption_coefficient` - - Note - ---- - See http://physics.nist.gov/ffast - Chantler, C.T., Olsen, K., Dragoset, R.A., Kishore, A.R., Kotochigova, - S.A., and Zucker, D.S. (2005), X-Ray Form Factor, Attenuation and - Scattering Tables (version 2.1). - - """ - from hyperspy.signals import BaseSignal - elements = _elements_auto(weight_percent, elements) - energies = _lines_auto(weight_percent, energies) - if isinstance(weight_percent[0], BaseSignal): - weight_per = np.array([wt.data for wt in weight_percent]) - mac_res = stack([weight_percent[0].deepcopy()] * len(energies), - show_progressbar=False) - mac_res.data = \ - _mass_absorption_mixture(weight_per, elements, energies) - mac_res = mac_res.split() - for i, energy in enumerate(energies): - mac_res[i].metadata.set_item("Sample.xray_lines", ([energy])) - mac_res[i].metadata.General.set_item( - "title", "Absoprtion coeff of" - " %s in %s" % (energy, mac_res[i].metadata.General.title)) - if mac_res[i].metadata.has_item("Sample.elements"): - del mac_res[i].metadata.Sample.elements - return mac_res - else: - return _mass_absorption_mixture(weight_percent, elements, energies) - - -def _lines_auto(composition, xray_lines): - if isinstance(composition[0], numbers.Number): - if isinstance(xray_lines, str): - if xray_lines == 'auto': - raise ValueError("The X-ray lines needs to be provided.") - else: - if isinstance(xray_lines, str): - if xray_lines == 'auto': - xray_lines = [] - for compo in composition: - if len(compo.metadata.Sample.xray_lines) > 1: - raise ValueError( - "The signal %s contains more than one X-ray lines " - "but this function requires only one X-ray lines " - "per signal." % compo.metadata.General.title) - else: - xray_lines.append(compo.metadata.Sample.xray_lines[0]) - return xray_lines - - -def _elements_auto(composition, elements): - if isinstance(composition[0], numbers.Number): - if isinstance(elements, str): - if elements == 'auto': - raise ValueError("The elements needs to be provided.") - else: - if isinstance(elements, str): - if elements == 'auto': - elements = [] - for compo in composition: - if len(compo.metadata.Sample.elements) > 1: - raise ValueError( - "The signal %s contains more than one element " - "but this function requires only one element " - "per signal." % compo.metadata.General.title) - else: - elements.append(compo.metadata.Sample.elements[0]) - return elements diff --git a/hyperspy/misc/math_tools.py b/hyperspy/misc/math_tools.py index a4bbfa11bd..face9f16a8 100644 --- a/hyperspy/misc/math_tools.py +++ b/hyperspy/misc/math_tools.py @@ -1,28 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import math import numbers -import numpy as np -import dask.array as da - +import warnings from functools import reduce +import dask +import dask.array as da +import numpy as np +from packaging.version import Version + def symmetrize(a): return a + a.swapaxes(0, 1) - np.diag(a.diagonal()) @@ -124,15 +127,22 @@ def hann_window_nth_order(m, order): window """ if not isinstance(m, int) or m <= 0: - raise ValueError('Parameter m has to be positive integer greater than 0.') + raise ValueError("Parameter m has to be positive integer greater than 0.") if not isinstance(order, int) or order <= 0: - raise ValueError('Filter order has to be positive integer greater than 0.') - sin_arg = np.pi * (m - 1.) / m - cos_arg = 2. * np.pi / (m - 1.) * (np.arange(m)) - - return m / (order * 2 * np.pi) * sum([(-1) ** i / i * - np.sin(i * sin_arg) * (np.cos(i * cos_arg) - 1) - for i in range(1, order + 1)]) + raise ValueError("Filter order has to be positive integer greater than 0.") + sin_arg = np.pi * (m - 1.0) / m + cos_arg = 2.0 * np.pi / (m - 1.0) * (np.arange(m)) + + return ( + m + / (order * 2 * np.pi) + * sum( + [ + (-1) ** i / i * np.sin(i * sin_arg) * (np.cos(i * cos_arg) - 1) + for i in range(1, order + 1) + ] + ) + ) def optimal_fft_size(target, real=False): @@ -174,36 +184,70 @@ def optimal_fft_size(target, real=False): def check_random_state(seed, lazy=False): - """Turn a random seed into a np.random.RandomState instance. + """Turn a random seed into a RandomState or Generator instance. Parameters ---------- - seed : None or int or np.random.RandomState or dask.array.random.RandomState - If None: - Return the RandomState singleton used by - np.random or dask.array.random - If int: - Return a new RandomState instance seeded with ``seed``. - If np.random.RandomState: - Return it. - If dask.array.random.RandomState: - Return it. + seed : None or int or numpy.random.RandomState or numpy.random.Generator or \ + dask.array.random.RandomState or dask.array.random.Generator + + - If None, returns the random state singleton used by numpy.random or + dask.array.random + - If int, returns a new random state instance seeded with ``seed``. + - If numpy.random.RandomState, numpy.random.Generator or + dask.array.random.RandomState, returns seed, `i.e.` the input. lazy : bool, default False If True, and seed is ``None`` or ``int``, return - a dask.array.random.RandomState instance instead. + a dask.array.random.RandomState instance instead for dask < 2023.2.1, + otherwise returns a dask.array.random.Generator instance + + Returns + ------- + np.random.Generator instance or dask.array.random.Generator """ # Derived from `sklearn.utils.check_random_state`. # Copyright (c) 2007-2020 The scikit-learn developers. # All rights reserved. - - if seed is None or seed is np.random: - return da.random._state if lazy else np.random.mtrand._rand + dask_version = Version(dask.__version__) + if seed is None: + if lazy: + if dask_version < Version("2022.10.0"): + return da.random._state + elif dask_version < Version("2023.2.1"): + backend = da.backends.array_creation_dispatch.backend + if backend not in da.random._cached_random_states.keys(): + # Need to initialise the backend + da.random.seed() + return da.random._cached_random_states[backend] + else: + return da.random.default_rng() + else: + return np.random.default_rng() if isinstance(seed, numbers.Integral): - return da.random.RandomState(seed) if lazy else np.random.RandomState(seed) + if lazy: + try: + return da.random.default_rng(seed) + except AttributeError: + return da.random.RandomState(seed) + else: + return np.random.default_rng(seed) + + if isinstance(seed, (np.random.RandomState, da.random.RandomState)): + warnings.warn( + "Support for RandomState generators have been deprecated and will be removed " + " in HyperSpy 2.0, use `default_rng` instead.", + DeprecationWarning, + ) + return seed + + if isinstance(seed, np.random.Generator): + return seed - if isinstance(seed, (da.random.RandomState, np.random.RandomState)): + if dask_version >= Version("2023.2.1") and isinstance(seed, da.random.Generator): return seed - raise ValueError(f"{seed} cannot be used to seed a RandomState instance") + raise ValueError( + f"{seed} cannot be used to seed a RandomState or a Generator instance" + ) diff --git a/hyperspy/misc/model_tools.py b/hyperspy/misc/model_tools.py index ef3c2635de..d497d5d53a 100644 --- a/hyperspy/misc/model_tools.py +++ b/hyperspy/misc/model_tools.py @@ -1,50 +1,47 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import dask.array as da +import numpy as np -def _is_iter(val): - "Checks if value is a list or tuple" - return isinstance(val, tuple) or isinstance(val, list) - -def _iter_join(val): - "Joins values of iterable parameters for the fancy view, unless it equals None, then blank" - return "(" + ", ".join(["{:6g}".format(v) for v in val]) + ")" if val is not None else "" - - -def _non_iter(val): - "Returns formatted string for a value unless it equals None, then blank" +def _format_string(val): + """ + Returns formatted string for a value unless it equals None, then blank + """ return "{:6g}".format(val) if val is not None else "" -class current_component_values(): - """Convenience class that makes use of __repr__ methods for nice printing in the notebook - of the properties of parameters of a component - +class CurrentComponentValues: + """ + Convenience class that makes use of __repr__ methods for nice printing in + the notebook of the properties of parameters of a component. + Parameters ---------- component : hyperspy component instance only_free : bool, default False If True: Only include the free parameters in the view only_active : bool, default False - If True: Helper for current_model_values. Only include active components in the view. - Always shows values if used on an individual component. - """ + If True: Helper for ``CurrentModelValues``. Only include active + components in the view. Always shows values if used on an individual + component. + """ def __init__(self, component, only_free=False, only_active=False): self.name = component.name @@ -57,51 +54,54 @@ def __init__(self, component, only_free=False, only_active=False): def __repr__(self): # Number of digits for each label for the terminal-style view. size = { - 'name': 14, - 'free': 5, - 'value': 10, - 'std': 10, - 'bmin': 10, - 'bmax': 10, + "name": 14, + "free": 7, + "value": 10, + "std": 10, + "bmin": 10, + "bmax": 10, + "linear": 6, } # Using nested string formatting for flexibility in future updates - signature = "{{:>{name}}} | {{:>{free}}} | {{:>{value}}} | {{:>{std}}} | {{:>{bmin}}} | {{:>{bmax}}}".format( - **size) + signature = "{{:>{name}}} | {{:>{free}}} | {{:>{value}}} | {{:>{std}}} | {{:>{bmin}}} | {{:>{bmax}}} | {{:>{linear}}}".format( + **size + ) if self.only_active: text = "{0}: {1}".format(self.__class__.__name__, self.name) else: text = "{0}: {1}\nActive: {2}".format( - self.__class__.__name__, self.name, self.active) + self.__class__.__name__, self.name, self.active + ) text += "\n" - text += signature.format("Parameter Name", - "Free", "Value", "Std", "Min", "Max") + text += signature.format( + "Parameter Name", "Free", "Value", "Std", "Min", "Max", "Linear" + ) text += "\n" - text += signature.format("=" * size['name'], "=" * size['free'], "=" * - size['value'], "=" * size['std'], "=" * size['bmin'], "=" * size['bmax'],) + text += signature.format( + "=" * size["name"], + "=" * size["free"], + "=" * size["value"], + "=" * size["std"], + "=" * size["bmin"], + "=" * size["bmax"], + "=" * size["linear"], + ) text += "\n" for para in self.parameters: if not self.only_free or self.only_free and para.free: - if _is_iter(para.value): - # iterables (polynomial.value) must be handled separately - # `blank` results in a column of spaces - blank = len(para.value) * [''] - std = para.std if _is_iter(para.std) else blank - bmin = para.bmin if _is_iter(para.bmin) else blank - bmax = para.bmax if _is_iter(para.bmax) else blank - for i, (v, s, bn, bx) in enumerate( - zip(para.value, std, bmin, bmax)): - if i == 0: - text += signature.format(para.name[:size['name']], str(para.free)[:size['free']], str( - v)[:size['value']], str(s)[:size['std']], str(bn)[:size['bmin']], str(bx)[:size['bmax']]) - else: - text += signature.format("", "", str(v)[:size['value']], str( - s)[:size['std']], str(bn)[:size['bmin']], str(bx)[:size['bmax']]) - text += "\n" - else: - text += signature.format(para.name[:size['name']], str(para.free)[:size['free']], str(para.value)[ - :size['value']], str(para.std)[:size['std']], str(para.bmin)[:size['bmin']], str(para.bmax)[:size['bmax']]) - text += "\n" + free = para.free if para.twin is None else "Twinned" + ln = para._linear + text += signature.format( + para.name[: size["name"]], + str(free)[: size["free"]], + str(para.value)[: size["value"]], + str(para.std)[: size["std"]], + str(para.bmin)[: size["bmin"]], + str(para.bmax)[: size["bmax"]], + str(ln)[: size["linear"]], + ) + text += "\n" return text def _repr_html_(self): @@ -109,38 +109,34 @@ def _repr_html_(self): text = "

{0}: {1}

".format(self.__class__.__name__, self.name) else: text = "

{0}: {1}
Active: {2}

".format( - self.__class__.__name__, self.name, self.active) + self.__class__.__name__, self.name, self.active + ) para_head = """ - """ + """ text += para_head for para in self.parameters: if not self.only_free or self.only_free and para.free: - if _is_iter(para.value): - # iterables (polynomial.value) must be handled separately - # This should be removed with hyperspy 2.0 as Polynomial - # has been replaced. - value = _iter_join(para.value) - std = _iter_join(para.std) - bmin = _iter_join(para.bmin) - bmax = _iter_join(para.bmax) - else: - value = _non_iter(para.value) - std = _non_iter(para.std) - bmin = _non_iter(para.bmin) - bmax = _non_iter(para.bmax) + free = para.free if para.twin is None else "Twinned" + linear = para._linear + value = _format_string(para.value) + std = _format_string(para.std) + bmin = _format_string(para.bmin) + bmax = _format_string(para.bmax) text += """ - """.format( - para.name, para.free, value, std, bmin, bmax) + """.format( + para.name, free, value, std, bmin, bmax, linear + ) text += "
Parameter NameFreeValueStdMinMax
ValueStdMinMaxLinear
{0}{1}{2}{3}{4}{5}
{3}{4}{5}{6}
" return text -class current_model_values(): - """Convenience class that makes use of __repr__ methods for nice printing in the notebook - of the properties of parameters in components in a model - +class CurrentModelValues: + """ + Convenience class that makes use of __repr__ methods for nice printing in + the notebook of the properties of parameters in components in a model. + Parameters ---------- component : hyperspy component instance @@ -154,32 +150,90 @@ def __init__(self, model, only_free=False, only_active=False, component_list=Non self.model = model self.only_free = only_free self.only_active = only_active - self.component_list = model if component_list == None else component_list - self.model_type = str(self.model.__class__).split("'")[1].split('.')[-1] + self.component_list = model if component_list is None else component_list + self.model_type = str(self.model.__class__).split("'")[1].split(".")[-1] def __repr__(self): text = "{}: {}\n".format( - self.model_type, self.model.signal.metadata.General.title) + self.model_type, self.model.signal.metadata.General.title + ) for comp in self.component_list: if not self.only_active or self.only_active and comp.active: if not self.only_free or comp.free_parameters and self.only_free: - text += current_component_values( - component=comp, - only_free=self.only_free, - only_active=self.only_active - ).__repr__() + "\n" + text += ( + CurrentComponentValues( + component=comp, + only_free=self.only_free, + only_active=self.only_active, + ).__repr__() + + "\n" + ) return text def _repr_html_(self): - - html = "

{}: {}

".format(self.model_type, - self.model.signal.metadata.General.title) + html = "

{}: {}

".format( + self.model_type, self.model.signal.metadata.General.title + ) for comp in self.component_list: if not self.only_active or self.only_active and comp.active: if not self.only_free or comp.free_parameters and self.only_free: - html += current_component_values( - component=comp, - only_free=self.only_free, - only_active=self.only_active - )._repr_html_() + html += CurrentComponentValues( + component=comp, + only_free=self.only_free, + only_active=self.only_active, + )._repr_html_() return html + + +def _calculate_covariance( + target_signal, coefficients, component_data, residual=None, lazy=False +): + """ + Calculate covariance matrix after having performed Linear Regression. + + Parameters + ---------- + + target_signal : array-like, shape (N,) or (M, N) + The signal array to be fit to. + coefficients : array-like, shape C or (M, C) + The fitted coefficients. + component_data : array-like, shape N or (C, N) + The component data. + residual : array-like, shape (0,) or (M,) + The residual sum of squares, optional. Calculated if None. + lazy : bool + Whether the signal is lazy. + + Notes + ----- + Explanation of the array shapes in HyperSpy terms: + N : flattened signal shape + M : flattened navigation shape + C : number of components + + See https://stats.stackexchange.com/questions/62470 for more info on the + algorithm + """ + if target_signal.ndim > 1: + fit = coefficients[..., None, :] * component_data.T[None] + else: + fit = coefficients * component_data.T + + if residual is None: + residual = ((target_signal - fit.sum(-1)) ** 2).sum(-1) + + fit_dot = np.matmul(fit.swapaxes(-2, -1), fit) + + # Prefer to find another way than matrix inverse + # if target_signal shape is 1D, then fit_dot is 2D and numpy going to dask.linalg.inv is fine. + # If target_signal shape is 2D, then dask.linalg.inv will fail because fit_dot is 3D. + if lazy and target_signal.ndim > 1: + inv_fit_dot = da.map_blocks(np.linalg.inv, fit_dot, chunks=fit_dot.chunks) + else: + inv_fit_dot = np.linalg.inv(fit_dot) + + n = fit.shape[-2] # the signal axis length + k = coefficients.shape[-1] # the number of components + covariance = (1 / (n - k)) * (residual * inv_fit_dot.T).T + return covariance diff --git a/hyperspy/misc/physics_tools.py b/hyperspy/misc/physics_tools.py deleted file mode 100644 index 257a7006f2..0000000000 --- a/hyperspy/misc/physics_tools.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - - -def bragg_scattering_angle(d, E0=100): - """Calculate the first order bragg diffraction semiangle. - - Parameters - ---------- - d : float - interplanar distance in m. - E0 : float - Incident energy in keV - - Returns - ------- - float : Semiangle of scattering of the first order difracted beam. This is - two times the bragg angle. - - """ - - gamma = 1 + E0 / 511.0 - v_rel = np.sqrt(1 - 1 / gamma ** 2) - e_lambda = 2 * np.pi / (2590e9 * (gamma * v_rel)) # m - - return e_lambda / d - - -def effective_Z(Z_list, exponent=2.94): - """Effective atomic number of a compound or mixture. - - Exponent = 2.94 for X-ray absorption. - - Parameters - ---------- - Z_list : list of tuples - A list of tuples (f,Z) where f is the number of atoms of the element in - the molecule and Z its atomic number - - Returns - ------- - float - - """ - if not np.iterable(Z_list) or not np.iterable(Z_list[0]): - raise ValueError( - "Z_list should be a list of tuples (f,Z) " - "where f is the number of atoms of the element" - "in the molecule and Z its atomic number" - ) - - exponent = float(exponent) - temp = 0 - total_e = 0 - for Z in Z_list: - temp += Z[1] * Z[1] ** exponent - total_e += Z[0] * Z[1] - return (temp / total_e) ** (1 / exponent) diff --git a/hyperspy/misc/rgb_tools.py b/hyperspy/misc/rgb_tools.py deleted file mode 100644 index 6deb180111..0000000000 --- a/hyperspy/misc/rgb_tools.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -from dask.array import Array - -rgba8 = np.dtype({'names': ['R', 'G', 'B', 'A'], - 'formats': ['u1', 'u1', 'u1', 'u1']}) -rgb8 = np.dtype({'names': ['R', 'G', 'B'], - 'formats': ['u1', 'u1', 'u1']}) - -rgba16 = np.dtype({'names': ['R', 'G', 'B', 'A'], - 'formats': ['u2', 'u2', 'u2', 'u2']}) -rgb16 = np.dtype({'names': ['R', 'G', 'B'], - 'formats': ['u2', 'u2', 'u2']}) -rgb_dtypes = { - 'rgb8': rgb8, - 'rgb16': rgb16, - 'rgba8': rgba8, - 'rgba16': rgba16} - - -def is_rgba(array): - if array.dtype in (rgba8, rgba16): - return True - else: - return False - - -def is_rgb(array): - if array.dtype in (rgb8, rgb16): - return True - else: - return False - - -def is_rgbx(array): - if is_rgb(array) or is_rgba(array): - return True - else: - return False - - -def rgbx2regular_array(data, plot_friendly=False): - """Transforms a RGBx array into a standard one - - Parameters - ---------- - data : numpy array of RGBx dtype - plot_friendly : bool - If True change the dtype to float when dtype is not uint8 and - normalize the array so that it is ready to be plotted by matplotlib. - - """ - # Make sure that the data is contiguous - if isinstance(data, Array): - from dask.diagnostics import ProgressBar - # an expensive thing, but nothing to be done for now... - with ProgressBar(): - data = data.compute() - if data.flags['C_CONTIGUOUS'] is False: - if np.ma.is_masked(data): - data = data.copy(order='C') - else: - data = np.ascontiguousarray(data) - if is_rgba(data) is True: - dt = data.dtype.fields['B'][0] - data = data.view((dt, 4)) - elif is_rgb(data) is True: - dt = data.dtype.fields['B'][0] - data = data.view((dt, 3)) - else: - return data - if plot_friendly is True and data.dtype == np.dtype("uint16"): - data = data.astype("float") - data /= 2 ** 16 - 1 - return data - - -def regular_array2rgbx(data): - # Make sure that the data is contiguous - if data.flags['C_CONTIGUOUS'] is False: - if np.ma.is_masked(data): - data = data.copy(order='C') - else: - data = np.ascontiguousarray(data) - if data.shape[-1] == 3: - names = rgb8.names - elif data.shape[-1] == 4: - names = rgba8.names - else: - raise ValueError("The last dimension size of the array must be 3 or 4") - if data.dtype in (np.dtype("u1"), np.dtype("u2")): - formats = [data.dtype] * len(names) - else: - raise ValueError("The data dtype must be uint16 or uint8") - return data.view(np.dtype({"names": names, - "formats": formats})).reshape(data.shape[:-1]) diff --git a/hyperspy/misc/signal_tools.py b/hyperspy/misc/signal_tools.py index d1231f0024..0d6fac9d7a 100644 --- a/hyperspy/misc/signal_tools.py +++ b/hyperspy/misc/signal_tools.py @@ -1,29 +1,28 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging from itertools import zip_longest -import dask.array as da import numpy as np -from hyperspy.misc.axis_tools import check_axes_calibration from hyperspy.misc.array_tools import are_aligned +from hyperspy.misc.axis_tools import check_axes_calibration _logger = logging.getLogger(__name__) @@ -34,21 +33,29 @@ def _get_shapes(am, ignore_axis): ignore_axis = am[ignore_axis] except ValueError: pass - sigsh = tuple(axis.size if (ignore_axis is None or axis is not - ignore_axis) - else 1 for axis in am.signal_axes) if am.signal_dimension != 0 else () - - navsh = tuple(axis.size if (ignore_axis is None or axis is not - ignore_axis) - else 1 for axis in am.navigation_axes) if am.navigation_dimension != 0 else () + sigsh = ( + tuple( + axis.size if (ignore_axis is None or axis is not ignore_axis) else 1 + for axis in am.signal_axes + ) + if am.signal_dimension != 0 + else () + ) + + navsh = ( + tuple( + axis.size if (ignore_axis is None or axis is not ignore_axis) else 1 + for axis in am.navigation_axes + ) + if am.navigation_dimension != 0 + else () + ) return sigsh, navsh def are_signals_aligned(*args, ignore_axis=None): if len(args) < 2: - raise ValueError( - "This function requires at least two signal instances" - ) + raise ValueError("This function requires at least two signal instances") args = list(args) am = args.pop().axes_manager @@ -58,8 +65,10 @@ def are_signals_aligned(*args, ignore_axis=None): amo = args.pop().axes_manager sigsho, navsho = _get_shapes(amo, ignore_axis) - if not (are_aligned(sigsh[::-1], sigsho[::-1]) and - are_aligned(navsh[::-1], navsho[::-1])): + if not ( + are_aligned(sigsh[::-1], sigsho[::-1]) + and are_aligned(navsh[::-1], navsho[::-1]) + ): return False return True @@ -127,8 +136,12 @@ def broadcast_signals(*args, ignore_axis=None): list of signals """ + from hyperspy.signal import BaseSignal + if len(args) < 2: raise ValueError("This function requires at least two signal instances") + if any([not isinstance(a, BaseSignal) for a in args]): + raise ValueError("Arguments must be of signal type.") args = list(args) if not are_signals_aligned(*args, ignore_axis=ignore_axis): raise ValueError("The signals cannot be broadcasted") @@ -171,7 +184,7 @@ def broadcast_signals(*args, ignore_axis=None): for s in args: data = s._data_aligned_with_axes sam = s.axes_manager - sdim_diff = len(new_sig_axes) - sam.signal_dimension + sdim_diff = len(new_sig_axes) - len(sam.signal_axes) while sdim_diff > 0: slices = (slice(None),) * sam.navigation_dimension slices += (None, Ellipsis) @@ -184,10 +197,7 @@ def broadcast_signals(*args, ignore_axis=None): thisshape[_id] = newlen thisshape = tuple(thisshape) if data.shape != thisshape: - if isinstance(data, np.ndarray): - data = np.broadcast_to(data, thisshape) - else: - data = da.broadcast_to(data, thisshape) + data = np.broadcast_to(data, thisshape) ns = s._deepcopy_with_new_data(data) ns.axes_manager._axes = [ax.copy() for ax in new_axes] diff --git a/hyperspy/misc/slicing.py b/hyperspy/misc/slicing.py index cbf9e7dd83..1622c66202 100644 --- a/hyperspy/misc/slicing.py +++ b/hyperspy/misc/slicing.py @@ -1,28 +1,29 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from operator import attrgetter + +import dask.array as da import numpy as np -from dask.array import Array as dArray -from hyperspy.misc.utils import attrsetter -from hyperspy.misc.export_dictionary import parse_flag_string from hyperspy import roi +from hyperspy.misc.export_dictionary import parse_flag_string +from hyperspy.misc.utils import attrsetter def _slice_target(target, dims, both_slices, slice_nav=None, issignal=False): @@ -55,26 +56,27 @@ def _slice_target(target, dims, both_slices, slice_nav=None, issignal=False): sl = tuple(array_slices[:nav_dims]) if isinstance(target, np.ndarray): return np.atleast_1d(target[sl]) - if isinstance(target, dArray): + if isinstance(target, da.Array): return target[sl] raise ValueError( - 'tried to slice with navigation dimensions, but was neither a ' - 'signal nor an array') + "tried to slice with navigation dimensions, but was neither a " + "signal nor an array" + ) if slice_nav is False: # check explicitly if issignal: return target.isig[slices] sl = tuple(array_slices[-sig_dims:]) if isinstance(target, np.ndarray): return np.atleast_1d(target[sl]) - if isinstance(target, dArray): + if isinstance(target, da.Array): return target[sl] raise ValueError( - 'tried to slice with navigation dimensions, but was neither a ' - 'signal nor an array') + "tried to slice with navigation dimensions, but was neither a " + "signal nor an array" + ) -def copy_slice_from_whitelist( - _from, _to, dims, both_slices, isNav, order=None): +def copy_slice_from_whitelist(_from, _to, dims, both_slices, isNav, order=None): """Copies things from one object to another, according to whitelist, slicing where required. @@ -100,28 +102,26 @@ def copy_slice_from_whitelist( def make_slice_navigation_decision(flags, isnav): if isnav: - if 'inav' in flags: + if "inav" in flags: return True return None - if 'isig' in flags: + if "isig" in flags: return False return None swl = None - if hasattr(_from, '_slicing_whitelist'): + if hasattr(_from, "_slicing_whitelist"): swl = _from._slicing_whitelist if order is not None and not isinstance(order, tuple): - raise ValueError('order argument has to be None or a tuple of strings') + raise ValueError("order argument has to be None or a tuple of strings") if order is None: order = () - if hasattr(_from, '_slicing_order'): - order = order + \ - tuple(k for k in _from._slicing_order if k not in order) + if hasattr(_from, "_slicing_order"): + order = order + tuple(k for k in _from._slicing_order if k not in order) - keys = order + tuple(k for k in _from._whitelist.keys() if k not in - order) + keys = order + tuple(k for k in _from._whitelist.keys() if k not in order) for key in keys: val = _from._whitelist[key] @@ -136,24 +136,19 @@ def make_slice_navigation_decision(flags, isnav): if swl is not None and key in swl: flags.extend(parse_flag_string(swl[key])) - if 'init' in flags: + if "init" in flags: continue - if 'id' in flags: + if "id" in flags: continue - if key == 'self': + if key == "self": target = None else: target = attrgetter(key)(_from) - if 'inav' in flags or 'isig' in flags: + if "inav" in flags or "isig" in flags: slice_nav = make_slice_navigation_decision(flags, isNav) - result = _slice_target( - target, - dims, - both_slices, - slice_nav, - 'sig' in flags) + result = _slice_target(target, dims, both_slices, slice_nav, "sig" in flags) attrsetter(_to, key, result) continue else: @@ -163,7 +158,6 @@ def make_slice_navigation_decision(flags, isnav): class SpecialSlicers(object): - def __init__(self, obj, isNavigation): """Create a slice of the signal. The indexing supports integer, decimal numbers or strings (containing a decimal number and an units). @@ -175,7 +169,7 @@ def __init__(self, obj, isNavigation): array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) >>> s.axes_manager[0].scale = 0.5 >>> s.axes_manager[0].axis - array([ 0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5]) + array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5]) >>> s.isig[0.5:4.].data array([1, 2, 3, 4, 5, 6, 7]) >>> s.isig[0.5:4].data @@ -194,7 +188,6 @@ def __getitem__(self, slices, out=None): class FancySlicing(object): - def _get_array_slices(self, slices, isNavigation=None): try: len(slices) @@ -226,10 +219,8 @@ def _get_array_slices(self, slices, isNavigation=None): # Create a deepcopy of self that contains a view of self.data - nav_idx = [el.index_in_array for el in - self.axes_manager.navigation_axes] - signal_idx = [el.index_in_array for el in - self.axes_manager.signal_axes] + nav_idx = [el.index_in_array for el in self.axes_manager.navigation_axes] + signal_idx = [el.index_in_array for el in self.axes_manager.signal_axes] if not has_signal: idx = nav_idx @@ -244,9 +235,14 @@ def _get_array_slices(self, slices, isNavigation=None): # Expand the first Ellipsis ellipsis_index = _orig_slices.index(Ellipsis) _orig_slices.remove(Ellipsis) - _orig_slices = (_orig_slices[:ellipsis_index] + [slice(None), ] * - max(0, len(idx) - len(_orig_slices)) + - _orig_slices[ellipsis_index:]) + _orig_slices = ( + _orig_slices[:ellipsis_index] + + [ + slice(None), + ] + * max(0, len(idx) - len(_orig_slices)) + + _orig_slices[ellipsis_index:] + ) # Replace all the following Ellipses by : while Ellipsis in _orig_slices: _orig_slices[_orig_slices.index(Ellipsis)] = slice(None) @@ -255,16 +251,22 @@ def _get_array_slices(self, slices, isNavigation=None): if len(_orig_slices) > len(idx): raise IndexError("too many indices") - slices = np.array([slice(None,)] * - len(self.axes_manager._axes)) + slices = np.array( + [ + slice( + None, + ) + ] + * len(self.axes_manager._axes) + ) slices[idx] = _orig_slices + (slice(None),) * max( - 0, len(idx) - len(_orig_slices)) + 0, len(idx) - len(_orig_slices) + ) array_slices = [] for slice_, axis in zip(slices, self.axes_manager._axes): - if (isinstance(slice_, slice) or - len(self.axes_manager._axes) < 2): + if isinstance(slice_, slice) or len(self.axes_manager._axes) < 2: array_slices.append(axis._get_array_slices(slice_)) else: if isinstance(slice_, float): @@ -273,63 +275,57 @@ def _get_array_slices(self, slices, isNavigation=None): return tuple(array_slices) def _slicer(self, slices, isNavigation=None, out=None): + if self.axes_manager._ragged and not isNavigation: + raise RuntimeError("`isig` is not supported for ragged signal.") + array_slices = self._get_array_slices(slices, isNavigation) new_data = self.data[array_slices] - if new_data.size == 1 and new_data.dtype is np.dtype('O'): - if isinstance(new_data[0], (np.ndarray, dArray)): - return self.__class__(new_data[0]).transpose(navigation_axes=0) - else: - return new_data[0] + if ( + self.ragged + and new_data.dtype != np.dtype(object) + and isinstance(new_data, np.ndarray) + ): + # Numpy will convert the array to non-ragged, for consistency, + # we make a ragged array with only one item + data = new_data.copy() + new_data = np.empty((1,), dtype=object) + new_data[0] = data if out is None: - _obj = self._deepcopy_with_new_data(new_data, - copy_variance=True) + _obj = self._deepcopy_with_new_data(new_data, copy_variance=True) _to_remove = [] for slice_, axis in zip(array_slices, _obj.axes_manager._axes): - if (isinstance(slice_, slice) or - len(self.axes_manager._axes) < 2): + if isinstance(slice_, slice) or len(self.axes_manager._axes) < 2: axis._slice_me(slice_) else: _to_remove.append(axis.index_in_axes_manager) - for _ind in reversed(sorted(_to_remove)): - _obj._remove_axis(_ind) + _obj._remove_axis(_to_remove) else: out.data = new_data _obj = out i = 0 for slice_, axis_src in zip(array_slices, self.axes_manager._axes): axis_src = axis_src.copy() - if (isinstance(slice_, slice) or - len(self.axes_manager._axes) < 2): + if isinstance(slice_, slice) or len(self.axes_manager._axes) < 2: axis_src._slice_me(slice_) axis_dst = out.axes_manager._axes[i] i += 1 - axis_dst.update_from(axis_src, attributes=( - "scale", "offset", "size")) + axis_dst.update_from(axis_src) if hasattr(self, "_additional_slicing_targets"): for ta in self._additional_slicing_targets: try: t = attrgetter(ta)(self) if out is None: - if hasattr(t, '_slicer'): - attrsetter( - _obj, - ta, - t._slicer( - slices, - isNavigation)) + if hasattr(t, "_slicer"): + attrsetter(_obj, ta, t._slicer(slices, isNavigation)) else: target = attrgetter(ta)(_obj) - t._slicer( - slices, - isNavigation, - out=target) + t._slicer(slices, isNavigation, out=target) except AttributeError: pass - # _obj.get_dimensions_from_data() # replots, so we do it manually: - dc = _obj.data + if out is None: return _obj else: diff --git a/hyperspy/misc/test_utils.py b/hyperspy/misc/test_utils.py index 94fac7b21b..c04725ba63 100644 --- a/hyperspy/misc/test_utils.py +++ b/hyperspy/misc/test_utils.py @@ -1,24 +1,25 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import os -from contextlib import contextmanager import warnings +from contextlib import contextmanager +from unittest import mock import numpy as np @@ -27,9 +28,9 @@ def ignore_warning(message="", category=None): with warnings.catch_warnings(): if category: - warnings.filterwarnings('ignore', message, category=category) + warnings.filterwarnings("ignore", message, category=category) else: - warnings.filterwarnings('ignore', message) + warnings.filterwarnings("ignore", message) yield @@ -51,17 +52,19 @@ def wrapper(): p = signal._plot p.close() check_closing_plot(signal, check_data_changed_close) + return wrapper + return decorator2 # Adapted from: # https://github.com/gem/oq-engine/blob/master/openquake/server/tests/helpers.py def assert_deep_almost_equal(actual, expected, *args, **kwargs): - """ Assert that two complex structures have almost equal contents. + """Assert that two complex structures have almost equal contents. Compares lists, dicts and tuples recursively. Checks numeric values - using :py:func:`numpy.testing.assert_allclose` and - checks all other values with :py:func:`numpy.testing.assert_equal`. + using :func:`numpy.testing.assert_allclose` and + checks all other values with :func:`numpy.testing.assert_equal`. Accepts additional positional and keyword arguments and pass those intact to assert_allclose() (that's how you specify comparison precision). @@ -73,15 +76,15 @@ def assert_deep_almost_equal(actual, expected, *args, **kwargs): expected: list, dict or tuple Expected values. *args : - Arguments are passed to :py:func:`numpy.testing.assert_allclose` or - :py:func:`assert_deep_almost_equal`. + Arguments are passed to :func:`numpy.testing.assert_allclose` or + :func:`assert_deep_almost_equal`. **kwargs : Keyword arguments are passed to - :py:func:`numpy.testing.assert_allclose` or - :py:func:`assert_deep_almost_equal`. + :func:`numpy.testing.assert_allclose` or + :func:`assert_deep_almost_equal`. """ - is_root = not '__trace' in kwargs - trace = kwargs.pop('__trace', 'ROOT') + is_root = "__trace" not in kwargs + trace = kwargs.pop("__trace", "ROOT") try: if isinstance(expected, (int, float, complex)): np.testing.assert_allclose(expected, actual, *args, **kwargs) @@ -89,19 +92,19 @@ def assert_deep_almost_equal(actual, expected, *args, **kwargs): assert len(expected) == len(actual) for index in range(len(expected)): v1, v2 = expected[index], actual[index] - assert_deep_almost_equal(v1, v2, - __trace=repr(index), *args, **kwargs) + assert_deep_almost_equal(v1, v2, __trace=repr(index), *args, **kwargs) elif isinstance(expected, dict): assert set(expected) == set(actual) for key in expected: - assert_deep_almost_equal(expected[key], actual[key], - __trace=repr(key), *args, **kwargs) + assert_deep_almost_equal( + expected[key], actual[key], __trace=repr(key), *args, **kwargs + ) else: assert expected == actual except AssertionError as exc: - exc.__dict__.setdefault('traces', []).append(trace) + exc.__dict__.setdefault("traces", []).append(trace) if is_root: - trace = ' -> '.join(reversed(exc.traces)) + trace = " -> ".join(reversed(exc.traces)) exc = AssertionError("%s\nTRACE: %s" % (exc, trace)) raise exc @@ -117,5 +120,30 @@ def sanitize_dict(dictionary): def check_running_tests_in_CI(): - if 'CI' in os.environ: - return os.environ.get('CI') + if "CI" in os.environ: + return os.environ.get("CI") + + +def mock_event( + fig, + canvas, + button=None, + key=None, + xdata=None, + ydata=None, + inaxes=True, + artist=None, + mouseevent=None, +): + event = mock.Mock() + event.button = button + event.key = key + event.xdata, event.ydata = xdata, ydata + event.inaxes = inaxes + event.fig = fig + event.canvas = canvas + event.guiEvent = None + event.name = "MockEvent" + event.artist = artist + event.mouseevent = mouseevent + return event diff --git a/hyperspy/misc/tv_denoise.py b/hyperspy/misc/tv_denoise.py index 02ff86768e..535c371bb5 100644 --- a/hyperspy/misc/tv_denoise.py +++ b/hyperspy/misc/tv_denoise.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . # Original file from scikits-images # Modified by the HyperSpy developers to add _tv_denoise_1d @@ -22,7 +22,7 @@ import numpy as np -def _tv_denoise_3d(im, weight=100, eps=2.e-4, keep_type=False, n_iter_max=200): +def _tv_denoise_3d(im, weight=100, eps=2.0e-4, keep_type=False, n_iter_max=200): """ Perform total-variation denoising on 3-D arrays @@ -65,7 +65,7 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, keep_type=False, n_iter_max=200): >>> mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(float) >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = tv_denoise_3d(mask, weight=100) + >>> res = _tv_denoise_3d(mask, weight=100) """ im_type = im.dtype if im_type is not float: @@ -78,33 +78,33 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, keep_type=False, n_iter_max=200): gz = np.zeros_like(im) i = 0 while i < n_iter_max: - d = - px - py - pz + d = -px - py - pz d[1:] += px[:-1] d[:, 1:] += py[:, :-1] d[:, :, 1:] += pz[:, :, :-1] out = im + d - E = (d ** 2).sum() + E = (d**2).sum() gx[:-1] = np.diff(out, axis=0) gy[:, :-1] = np.diff(out, axis=1) gz[:, :, :-1] = np.diff(out, axis=2) - norm = np.sqrt(gx ** 2 + gy ** 2 + gz ** 2) + norm = np.sqrt(gx**2 + gy**2 + gz**2) E += weight * norm.sum() norm *= 0.5 / weight - norm += 1. - px -= 1. / 6. * gx + norm += 1.0 + px -= 1.0 / 6.0 * gx px /= norm - py -= 1. / 6. * gy + py -= 1.0 / 6.0 * gy py /= norm - pz -= 1 / 6. * gz + pz -= 1 / 6.0 * gz pz /= norm E /= float(im.size) if i == 0: E_init = E E_previous = E else: - if np.abs(E_previous - E) < eps * E_init: + if abs(E_previous - E) < eps * E_init: break else: E_previous = E @@ -115,7 +115,7 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, keep_type=False, n_iter_max=200): return out -def _tv_denoise_2d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): +def _tv_denoise_2d(im, weight=50, eps=2.0e-4, keep_type=False, n_iter_max=200): """ Perform total-variation denoising @@ -149,7 +149,7 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): Notes ----- The principle of total variation denoising is explained in - http://en.wikipedia.org/wiki/Total_variation_denoising + https://en.wikipedia.org/wiki/Total_variation_denoising This code is an implementation of the algorithm of Rudin, Fatemi and Osher that was proposed by Chambolle in [*]_. @@ -163,10 +163,10 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): Examples --------- - >>> import scipy - >>> ascent = scipy.ascent().astype(float) - >>> ascent += 0.5 * ascent.std()*np.random.randn(*ascent.shape) - >>> denoised_ascent = tv_denoise(ascent, weight=60.0) + >>> import skimage + >>> camera = skimage.data.camera().astype(float) + >>> camera += 0.5 * camera.std() * np.random.randn(*camera.shape) + >>> denoised_camera = _tv_denoise_2d(camera, weight=60.0) """ im_type = im.dtype if im_type is not float: @@ -183,10 +183,10 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): d[:, 1:] += py[:, :-1] out = im + d - E = (d ** 2).sum() + E = (d**2).sum() gx[:-1] = np.diff(out, axis=0) gy[:, :-1] = np.diff(out, axis=1) - norm = np.sqrt(gx ** 2 + gy ** 2) + norm = np.sqrt(gx**2 + gy**2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1 @@ -199,7 +199,7 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): E_init = E E_previous = E else: - if np.abs(E_previous - E) < eps * E_init: + if abs(E_previous - E) < eps * E_init: break else: E_previous = E @@ -210,7 +210,7 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): return out -def _tv_denoise_1d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): +def _tv_denoise_1d(im, weight=50, eps=2.0e-4, keep_type=False, n_iter_max=200): """ Perform total-variation denoising @@ -244,7 +244,7 @@ def _tv_denoise_1d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): Notes ----- The principle of total variation denoising is explained in - http://en.wikipedia.org/wiki/Total_variation_denoising + https://en.wikipedia.org/wiki/Total_variation_denoising This code is an implementation of the algorithm of Rudin, Fatemi and Osher that was proposed by Chambolle in [*]_. @@ -256,12 +256,6 @@ def _tv_denoise_1d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. - Examples - --------- - >>> import scipy - >>> ascent = scipy.misc.ascent().astype(float) - >>> ascent += 0.5 * ascent.std()*np.random.randn(*ascent.shape) - >>> denoised_ascent = tv_denoise(ascent, weight=60.0) """ im_type = im.dtype if im_type is not float: @@ -275,9 +269,9 @@ def _tv_denoise_1d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): d[1:] += px[:-1] out = im + d - E = (d ** 2).sum() + E = (d**2).sum() gx[:-1] = np.diff(out) - norm = np.abs(gx) + norm = abs(gx) E += weight * norm.sum() norm *= 0.5 / weight norm += 1 @@ -288,7 +282,7 @@ def _tv_denoise_1d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): E_init = E E_previous = E else: - if np.abs(E_previous - E) < eps * E_init: + if abs(E_previous - E) < eps * E_init: break else: E_previous = E @@ -299,7 +293,7 @@ def _tv_denoise_1d(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): return out -def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): +def tv_denoise(im, weight=50, eps=2.0e-4, keep_type=False, n_iter_max=200): """ Perform total-variation denoising on 2-d and 3-d images @@ -335,7 +329,7 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): Notes ----- The principle of total variation denoising is explained in - http://en.wikipedia.org/wiki/Total_variation_denoising + https://en.wikipedia.org/wiki/Total_variation_denoising The principle of total variation denoising is to minimize the total variation of the image, which can be roughly described as @@ -355,17 +349,16 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): Examples --------- - >>> # 2D example using ascent - >>> import scipy - >>> ascent = scipy.misc.ascent().astype(float) - >>> ascent += 0.5 * ascent.std()*np.random.randn(*ascent.shape) - >>> denoised_ascent = tv_denoise(ascent, weight=60) + >>> import skimage + >>> camera = skimage.data.camera().astype(float) + >>> camera += 0.5 * camera.std() * np.random.randn(*camera.shape) + >>> denoised_camera = tv_denoise(camera, weight=60) >>> # 3D example on synthetic data >>> x, y, z = np.ogrid[0:40, 0:40, 0:40] >>> mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(float) - >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = tv_denoise_3d(mask, weight=100) + >>> mask += 0.2 * np.random.randn(*mask.shape) + >>> res = tv_denoise(mask, weight=100) """ if im.ndim == 2: @@ -373,5 +366,4 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): elif im.ndim == 3: return _tv_denoise_3d(im, weight, eps, keep_type, n_iter_max) else: - raise ValueError( - 'only 2-d and 3-d images may be denoised with this function') + raise ValueError("only 2-d and 3-d images may be denoised with this function") diff --git a/hyperspy/misc/utils.py b/hyperspy/misc/utils.py index d9b230830d..83baa83451 100644 --- a/hyperspy/misc/utils.py +++ b/hyperspy/misc/utils.py @@ -1,80 +1,76 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from operator import attrgetter -import warnings -import inspect +import codecs import copy +import importlib +import inspect +import logging import types -from io import StringIO -import codecs -from collections.abc import Iterable, Mapping import unicodedata +from collections.abc import Iterable, Mapping from contextlib import contextmanager -import importlib -import logging +from io import StringIO +from operator import attrgetter +import dask.array as da import numpy as np -from hyperspy.misc.signal_tools import broadcast_signals -from hyperspy.exceptions import VisibleDeprecationWarning from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG from hyperspy.docstrings.utils import STACK_METADATA_ARG +from hyperspy.misc.signal_tools import broadcast_signals _logger = logging.getLogger(__name__) def attrsetter(target, attrs, value): - """ Sets attribute of the target to specified value, supports nested - attributes. Only creates a new attribute if the object supports such - behaviour (e.g. DictionaryTreeBrowser does) - - Parameters - ---------- - target : object - attrs : string - attributes, separated by periods (e.g. - 'metadata.Signal.Noise_parameters.variance' ) - value : object - - Example - ------- - First create a signal and model pair: + """ + Sets attribute of the target to specified value, supports nested + attributes. Only creates a new attribute if the object supports such + behaviour (e.g. DictionaryTreeBrowser does) - >>> s = hs.signals.Signal1D(np.arange(10)) - >>> m = s.create_model() - >>> m.signal.data - array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + Parameters + ---------- + target : object + attrs : string + attributes, separated by periods (e.g. + 'metadata.Signal.Noise_parameters.variance' ) + value : object - Now set the data of the model with attrsetter - >>> attrsetter(m, 'signal1D.data', np.arange(10)+2) - >>> self.signal.data - array([2, 3, 4, 5, 6, 7, 8, 9, 10, 10]) + Examples + -------- + First create a signal and model pair: - The behaviour is identical to - >>> self.signal.data = np.arange(10) + 2 + >>> s = hs.signals.Signal1D(np.arange(10)) + >>> m = s.create_model() + >>> m.signal.data + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + Now set the data of the model with attrsetter + >>> attrsetter(m, 'signal.data', np.arange(10)+2) + >>> m.signal.data + array([ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) """ - where = attrs.rfind('.') + where = attrs.rfind(".") if where != -1: target = attrgetter(attrs[:where])(target) - setattr(target, attrs[where + 1:], value) + setattr(target, attrs[where + 1 :], value) @contextmanager @@ -117,7 +113,7 @@ def str2num(string, **kargs): return np.loadtxt(stringIO, **kargs) -def parse_quantity(quantity, opening='(', closing=')'): +def parse_quantity(quantity, opening="(", closing=")"): """Parse quantity of the signal outputting quantity and units separately. It looks for the last matching opening and closing separator. @@ -150,16 +146,14 @@ def parse_quantity(quantity, opening='(', closing=')'): if index + 1 == len(quantity): return quantity, "" else: - quantity_name = quantity[:-index-1].strip() + quantity_name = quantity[: -index - 1].strip() quantity_units = quantity[-index:-1].strip() return quantity_name, quantity_units -_slugify_strip_re_data = ''.join( - c for c in map( - chr, np.delete( - np.arange(256), [ - 95, 32])) if not c.isalnum()).encode() +_slugify_strip_re_data = "".join( + c for c in map(chr, np.delete(np.arange(256), [95, 32])) if not c.isalnum() +).encode() def slugify(value, valid_variable_name=False): @@ -177,35 +171,26 @@ def slugify(value, valid_variable_name=False): except BaseException: # Try latin1. If this does not work an exception is raised. value = str(value, "latin1") - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore") value = value.translate(None, _slugify_strip_re_data).decode().strip() - value = value.replace(' ', '_') + value = value.replace(" ", "_") if valid_variable_name and not value.isidentifier(): - value = 'Number_' + value + value = "Number_" + value return value class DictionaryTreeBrowser: - - """A class to comfortably browse a dictionary using a CLI. + """ + A class to comfortably browse a dictionary using a CLI. In addition to accessing the values using dictionary syntax the class enables navigating a dictionary that constains nested dictionaries as attribures of nested classes. Also it is an iterator over the (key, value) items. The - `__repr__` method provides pretty tree printing. Private + ``__repr__`` method provides pretty tree printing. Private keys, i.e. keys that starts with an underscore, are not printed, counted when calling len nor iterated. - Methods - ------- - export : saves the dictionary in pretty tree printing format in a text - file. - keys : returns a list of non-private keys. - as_dictionary : returns a dictionary representation of the object. - set_item : easily set items, creating any necessary node on the way. - add_node : adds a node. - Examples -------- >>> tree = DictionaryTreeBrowser() @@ -243,15 +228,13 @@ class DictionaryTreeBrowser: """ def __init__(self, dictionary=None, double_lines=False, lazy=True): - """When creating a DictionaryTreeBrowser lazily, the dictionary is - added to the `_lazy_attributes` attribute. The first time a lazy - attribute is called or the DictionaryTreeBrowser is printed, the - DictionaryTreeBrowser processes the lazy attributes with the - `process_lazy_attributes` method. - DictionaryTreeBrowser is lazy by default, using non-lazy instances - can be useful for debugging purposes. - - """ + # When creating a DictionaryTreeBrowser lazily, the dictionary is + # added to the `_lazy_attributes` attribute. The first time a lazy + # attribute is called or the DictionaryTreeBrowser is printed, the + # DictionaryTreeBrowser processes the lazy attributes with the + # `process_lazy_attributes` method. + # DictionaryTreeBrowser is lazy by default, using non-lazy instances + # can be useful for debugging purposes. self._lazy_attributes = {} self._double_lines = double_lines @@ -264,24 +247,21 @@ def __init__(self, dictionary=None, double_lines=False, lazy=True): self._process_dictionary(dictionary, double_lines) def _process_dictionary(self, dictionary, double_lines): - """Process the provided dictionary to set the attributes - """ + """Process the provided dictionary to set the attributes""" for key, value in dictionary.items(): - if key == '_double_lines': + if key == "_double_lines": value = double_lines - self.__setattr__(key, value) + self._setattr(key, value, keep_existing=True) def process_lazy_attributes(self): - """Run the DictionaryTreeBrowser machinery for the lazy attributes. - """ + """Run the DictionaryTreeBrowser machinery for the lazy attributes.""" if len(self._lazy_attributes) > 0: _logger.debug("Processing lazy attributes DictionaryBrowserTree") self._process_dictionary(self._lazy_attributes, self._double_lines) self._lazy_attributes = {} def add_dictionary(self, dictionary, double_lines=False): - """Add new items from dictionary. - """ + """Add new items from dictionary.""" if len(self._lazy_attributes) > 0: # To simplify merging lazy and non lazy attribute, we get self # as a dictionary and update the dictionary with the attributes @@ -291,45 +271,45 @@ def add_dictionary(self, dictionary, double_lines=False): else: self._process_dictionary(dictionary, double_lines) - def export(self, filename, encoding='utf8'): - """Export the dictionary to a text file + def export(self, filename, encoding="utf8"): + """ + Export the dictionary to a text file Parameters ---------- filename : str The name of the file without the extension that is txt by default - encoding : valid encoding str + encoding : str + The encoding to be used. """ - f = codecs.open(filename, 'w', encoding=encoding) + self.process_lazy_attributes() + f = codecs.open(filename, "w", encoding=encoding) f.write(self._get_print_items(max_len=None)) f.close() - def _get_print_items(self, padding='', max_len=78): - """Prints only the attributes that are not methods - """ + def _get_print_items(self, padding="", max_len=78): + """Prints only the attributes that are not methods""" from hyperspy.defaults_parser import preferences - - - string = '' + string = "" eoi = len(self) j = 0 if preferences.General.dtb_expand_structures and self._double_lines: - s_end = '╚══ ' - s_middle = '╠══ ' - pad_middle = '║ ' + s_end = "╚══ " + s_middle = "╠══ " + pad_middle = "║ " else: - s_end = '└── ' - s_middle = '├── ' - pad_middle = '│ ' + s_end = "└── " + s_middle = "├── " + pad_middle = "│ " for key_, value in iter(sorted(self.__dict__.items())): if key_.startswith("_"): continue if not isinstance(key_, types.MethodType): - key = ensure_unicode(value['key']) - value = value['_dtb_value_'] + key = ensure_unicode(value["key"]) + value = value["_dtb_value_"] if j == eoi - 1: symbol = s_end else: @@ -338,65 +318,62 @@ def _get_print_items(self, padding='', max_len=78): if isinstance(value, list) or isinstance(value, tuple): iflong, strvalue = check_long_string(value, max_len) if iflong: - key += (" " - if isinstance(value, list) - else " ") + key += " " if isinstance(value, list) else " " value = DictionaryTreeBrowser( - {'[%d]' % i: v for i, v in enumerate(value)}, + {"[%d]" % i: v for i, v in enumerate(value)}, double_lines=True, - lazy=False) + lazy=False, + ) else: - string += "%s%s%s = %s\n" % ( - padding, symbol, key, strvalue) + string += "%s%s%s = %s\n" % (padding, symbol, key, strvalue) j += 1 continue if isinstance(value, DictionaryTreeBrowser): - string += '%s%s%s\n' % (padding, symbol, key) + string += "%s%s%s\n" % (padding, symbol, key) if j == eoi - 1: - extra_padding = ' ' + extra_padding = " " else: extra_padding = pad_middle - string += value._get_print_items( - padding + extra_padding) + string += value._get_print_items(padding + extra_padding) else: _, strvalue = check_long_string(value, max_len) - string += "%s%s%s = %s\n" % ( - padding, symbol, key, strvalue) + string += "%s%s%s = %s\n" % (padding, symbol, key, strvalue) j += 1 return string - def _get_html_print_items(self, padding='', max_len=78, recursive_level=0): - """Recursive method that creates a html string for fancy display + def _get_html_print_items(self, padding="", max_len=78, recursive_level=0): + """Recursive method that creates a html string for html display of metadata. """ recursive_level += 1 from hyperspy.defaults_parser import preferences - string = '' # Final return string + string = "" # Final return string for key_, value in iter(sorted(self.__dict__.items())): - if key_.startswith("_"): # Skip any private attributes + if key_.startswith("_"): # Skip any private attributes continue - if not isinstance(key_, types.MethodType): # If it isn't a method, then continue - key = ensure_unicode(value['key']) - value = value['_dtb_value_'] + if not isinstance( + key_, types.MethodType + ): # If it isn't a method, then continue + key = ensure_unicode(value["key"]) + value = value["_dtb_value_"] # dtb_expand_structures is a setting that sets whether to fully expand long strings if preferences.General.dtb_expand_structures: if isinstance(value, list) or isinstance(value, tuple): iflong, strvalue = check_long_string(value, max_len) if iflong: - key += (" " - if isinstance(value, list) - else " ") + key += " " if isinstance(value, list) else " " value = DictionaryTreeBrowser( - {'[%d]' % i: v for i, v in enumerate(value)}, + {"[%d]" % i: v for i, v in enumerate(value)}, double_lines=True, - lazy=False) + lazy=False, + ) else: string += add_key_value(key, strvalue) - continue # skips the next if-else + continue # skips the next if-else # If DTB, then add a details html tag if isinstance(value, DictionaryTreeBrowser): @@ -406,9 +383,14 @@ def _get_html_print_items(self, padding='', max_len=78, recursive_level=0):
  • {}
  • - """.format("open" if recursive_level < 2 else "closed", replace_html_symbols(key)) - string += value._get_html_print_items(recursive_level=recursive_level) - string += '' + """.format( + "open" if recursive_level < 2 else "closed", + replace_html_symbols(key), + ) + string += value._get_html_print_items( + recursive_level=recursive_level + ) + string += "" # Otherwise just add value else: @@ -447,7 +429,7 @@ def __getattr__(self, name): if name in keys or f"_sig_{name}" in keys: # It is a lazy attribute, we need to process the lazy attribute self.process_lazy_attributes() - return self.__dict__[name]['_dtb_value_'] + return self.__dict__[name]["_dtb_value_"] else: raise AttributeError(name) @@ -457,38 +439,54 @@ def __getattribute__(self, name): name = slugify(name, valid_variable_name=True) item = super().__getattribute__(name) - if isinstance(item, dict) and '_dtb_value_' in item and "key" in item: - return item['_dtb_value_'] + if isinstance(item, dict) and "_dtb_value_" in item and "key" in item: + return item["_dtb_value_"] else: return item def __setattr__(self, key, value): - if key in ['_double_lines', '_lazy_attributes']: + self._setattr(key, value, keep_existing=False) + + def _setattr(self, key, value, keep_existing=False): + """ + Set the value of the given attribute `key`. + + Parameters + ---------- + key : str + The key attribute to be set. + value : object + The value to assign to the given `key` attribute. + keep_existing : bool, optional + If value is of dictionary type and the node already exists, keep + existing leaf of the node being set. The default is False. + + Returns + ------- + None. + + """ + if key in ["_double_lines", "_lazy_attributes"]: super().__setattr__(key, value) return - if key == 'binned': - warnings.warn('Use of the `binned` attribute in metadata is ' - 'going to be deprecated in v2.0. Set the ' - '`axis.is_binned` attribute instead. ', - VisibleDeprecationWarning) - - if key.startswith('_sig_'): + + if key.startswith("_sig_"): key = key[5:] from hyperspy.signal import BaseSignal + value = BaseSignal(**value) slugified_key = str(slugify(key, valid_variable_name=True)) if isinstance(value, dict): - if slugified_key in self.__dict__.keys(): - self.__dict__[slugified_key]['_dtb_value_'].add_dictionary( - value, - double_lines=self._double_lines) + if slugified_key in self.__dict__.keys() and keep_existing: + self.__dict__[slugified_key]["_dtb_value_"].add_dictionary( + value, double_lines=self._double_lines + ) return else: value = DictionaryTreeBrowser( - value, - double_lines=self._double_lines, - lazy=False) - super().__setattr__(slugified_key, {'key': key, '_dtb_value_': value}) + value, double_lines=self._double_lines, lazy=False + ) + super().__setattr__(slugified_key, {"key": key, "_dtb_value_": value}) def __len__(self): if len(self._lazy_attributes) > 0: @@ -498,50 +496,123 @@ def __len__(self): return len([key for key in d.keys() if not key.startswith("_")]) def keys(self): - """Returns a list of non-private keys. - - """ - return sorted([key for key in self.__dict__.keys() - if not key.startswith("_")]) + """Returns a list of non-private keys.""" + return sorted([key for key in self.__dict__.keys() if not key.startswith("_")]) def as_dictionary(self): - """Returns its dictionary representation. - - """ + """Returns its dictionary representation.""" if len(self._lazy_attributes) > 0: return copy.deepcopy(self._lazy_attributes) par_dict = {} + from hyperspy.axes import AxesManager, BaseDataAxis from hyperspy.signal import BaseSignal + for key_, item_ in self.__dict__.items(): if not isinstance(item_, types.MethodType): if key_ in ["_db_index", "_double_lines", "_lazy_attributes"]: continue - key = item_['key'] - if isinstance(item_['_dtb_value_'], DictionaryTreeBrowser): - item = item_['_dtb_value_'].as_dictionary() - elif isinstance(item_['_dtb_value_'], BaseSignal): - item = item_['_dtb_value_']._to_dictionary() - key = '_sig_' + key - elif hasattr(item_['_dtb_value_'], '_to_dictionary'): - item = item_['_dtb_value_']._to_dictionary() + key = item_["key"] + if isinstance(item_["_dtb_value_"], DictionaryTreeBrowser): + item = item_["_dtb_value_"].as_dictionary() + elif isinstance(item_["_dtb_value_"], BaseSignal): + item = item_["_dtb_value_"]._to_dictionary() + key = "_sig_" + key + elif hasattr(item_["_dtb_value_"], "_to_dictionary"): + item = item_["_dtb_value_"]._to_dictionary() + elif isinstance(item_["_dtb_value_"], AxesManager): + item = item_["_dtb_value_"]._get_axes_dicts() + key = "_hspy_AxesManager_" + key + elif isinstance(item_["_dtb_value_"], BaseDataAxis): + item = item_["_dtb_value_"].get_axis_dictionary() + key = "_hspy_Axis_" + key + elif type(item_["_dtb_value_"]) in (list, tuple): + signals = [] + container = item_["_dtb_value_"] + # Support storing signals in containers + for i, item in enumerate(container): + if isinstance(item, BaseSignal): + signals.append(i) + if signals: + to_tuple = False + if type(container) is tuple: + container = list(container) + to_tuple = True + for i in signals: + container[i] = {"_sig_": container[i]._to_dictionary()} + if to_tuple: + container = tuple(container) + item = container else: - item = item_['_dtb_value_'] - par_dict.update({key:item}) + item = item_["_dtb_value_"] + par_dict.update({key: item}) return par_dict - def has_item(self, item_path): - """Given a path, return True if it exists. + def _nested_get_iter(self, item, wild=False): + """Recursive function to search for an item key in a nested + DictionaryTreeBrowser.""" + self.process_lazy_attributes() + for key_, item_ in self.__dict__.items(): + if not isinstance(item_, types.MethodType) and not key_.startswith("_"): + key = item_["key"] + if isinstance(item_["_dtb_value_"], DictionaryTreeBrowser): + for result in item_["_dtb_value_"]._nested_get_iter(item, wild): + yield key + "." + result[0], result[1] + else: + if key == item or ( + wild and (str(item).lower() in str(key).lower()) + ): + yield key, item_["_dtb_value_"] + + def _nested_get(self, item_path, wild=False, return_path=False): + """Search for an item key in a nested DictionaryTreeBrowser and yield a + list of values. If `wild` is `True`, looks for any key that contains + the string `item` (case insensitive). If part of a path is given, + search for matching items and then make sure that the full path is + contained.""" + if "." in item_path: + item = item_path.split(".").pop(-1) + else: + item = item_path + result = list(self._nested_get_iter(item, wild)) + # remove item where item matches, but not additional elements of item_path + if return_path: + return [i for i in result if item_path in i[0]] + else: + return [i[1] for i in result if item_path in i[0]] + + def has_item( + self, item_path, default=None, full_path=True, wild=False, return_path=False + ): + """ + Given a path, return True if it exists. May also perform a search + whether an item exists and optionally returns the full path instead of + boolean value. The nodes of the path are separated using periods. Parameters ---------- - item_path : Str + item_path : str A string describing the path with each item separated by - full stops (periods) + full stops (periods). + full_path : bool, default True + If True, the full path to the item has to be given. If + False, a search for the item key is performed (can include + additional nodes preceding they key separated by full stops). + wild : bool, default True + Only applies if ``full_path=False``. If True, searches for any items + where ``item`` matches a substring of the item key (case insensitive). + Default is ``False``. + return_path : bool, default False + Only applies if ``full_path=False``. If False, a boolean + value is returned. If True, the full path to the item is returned, + a list of paths for multiple matches, or default value if it does + not exist. + default : + The value to return for path if the item does not exist (default is ``None``). Examples -------- @@ -554,38 +625,69 @@ def has_item(self, item_path): True >>> dict_browser.has_item('To.be.or') False + >>> dict_browser.has_item('be', full_path=False) + True + >>> dict_browser.has_item('be', full_path=False, return_path=True) + 'To.be' """ - if isinstance(item_path, str): - item_path = item_path.split('.') - else: - item_path = copy.copy(item_path) - attrib = item_path.pop(0) - if hasattr(self, attrib): - if len(item_path) == 0: - return True + if full_path: + if isinstance(item_path, str): + item_path = item_path.split(".") else: - item = self[attrib] - if isinstance(item, type(self)): - return item.has_item(item_path) + item_path = copy.copy(item_path) + attrib = item_path.pop(0) + if hasattr(self, attrib): + if len(item_path) == 0: + return True else: - return False + item = self[attrib] + if isinstance(item, type(self)): + return item.has_item(item_path) + else: + return False + else: + return False else: - return False + if not return_path: + return self._nested_get(item_path, wild=wild) != [] + else: + result = self._nested_get(item_path, wild=wild, return_path=True) + if len(result) == 0: + return default + elif len(result) == 1: + return result[0][0] + return [i[0] for i in result] - def get_item(self, item_path, default=None): - """Given a path, return it's value if it exists, or default - value if missing. + def get_item( + self, item_path, default=None, full_path=True, wild=False, return_path=False + ): + """ + Given a path, return it's value if it exists, or default value if + missing. May also perform a search whether an item key exists and then + returns the value or a list of values for multiple occurences of the + key -- optionally returns the full path(s) in addition to its value(s). The nodes of the path are separated using periods. Parameters ---------- - item_path : Str + item_path : str A string describing the path with each item separated by full stops (periods) - default : - The value to return if the path does not exist. + full_path : bool, default True + If True, the full path to the item has to be given. If + False, a search for the item key is performed (can include + additional nodes preceding they key separated by full stops). + wild : bool + Only applies if ``full_path=False``. If True, searches for any items + where ``item`` matches a substring of the item key (case insensitive). + Default is False. + return_path : bool + Only applies if ``full_path=False``. Default False. If True, + returns an additional list of paths to the item(s) that match ``key``. + default : None or object, default None + The value to return if the path or item does not exist. Examples -------- @@ -597,43 +699,65 @@ def get_item(self, item_path, default=None): True >>> dict_browser.get_item('To.be.or', 'default_value') 'default_value' + >>> dict_browser.get_item('be', full_path=False) + True """ - if isinstance(item_path, str): - item_path = item_path.split('.') - else: - item_path = copy.copy(item_path) - attrib = item_path.pop(0) - if hasattr(self, attrib): - if len(item_path) == 0: - return self[attrib] + if full_path: + if isinstance(item_path, str): + item_path = item_path.split(".") else: - item = self[attrib] - if isinstance(item, type(self)): - return item.get_item(item_path, default) + item_path = copy.copy(item_path) + attrib = item_path.pop(0) + if hasattr(self, attrib): + if len(item_path) == 0: + return self[attrib] else: - return default + item = self[attrib] + if isinstance(item, type(self)): + return item.get_item(item_path, default=default) + else: + return default + else: + return default else: - return default + result = self._nested_get(item_path, wild=wild, return_path=return_path) + if len(result) == 0: + return default + elif len(result) == 1: + if return_path: + return result[0][1], result[0][0] + else: + return result[0] + else: + if return_path: + return [i[1] for i in result], [i[0] for i in result] + else: + return result def __contains__(self, item): return self.has_item(item_path=item) def copy(self): + """Returns a shallow copy using :func:`copy.copy`.""" return copy.copy(self) def deepcopy(self): + """Returns a deep copy using :func:`copy.deepcopy`.""" return copy.deepcopy(self) def set_item(self, item_path, value): - """Given the path and value, create the missing nodes in - the path and assign to the last one the value + """ + iven the path and value, create the missing nodes in + the path and assign the given value. Parameters ---------- - item_path : Str + item_path : str A string describing the path with each item separated by a - full stops (periods) + full stop (periods) + value : object + The value to assign to the given path. Examples -------- @@ -649,10 +773,9 @@ def set_item(self, item_path, value): if not self.has_item(item_path): self.add_node(item_path) if isinstance(item_path, str): - item_path = item_path.split('.') + item_path = item_path.split(".") if len(item_path) > 1: - self.__getattribute__(item_path.pop(0)).set_item( - item_path, value) + self.__getattribute__(item_path.pop(0)).set_item(item_path, value) else: self.__setattr__(item_path.pop(), value) @@ -675,7 +798,7 @@ def add_node(self, node_path): └── Second = 3 """ - keys = node_path.split('.') + keys = node_path.split(".") dtb = self for key in keys: if dtb.has_item(key) is False: @@ -696,7 +819,7 @@ def __next__(self): """ if len(self) == 0: raise StopIteration - if not hasattr(self, '_db_index'): + if not hasattr(self, "_db_index"): self._db_index = 0 elif self._db_index >= len(self) - 1: del self._db_index @@ -714,7 +837,7 @@ def __iter__(self): def strlist2enumeration(lst): lst = tuple(lst) if not lst: - return '' + return "" elif len(lst) == 1: return lst[0] elif len(lst) == 2: @@ -723,31 +846,32 @@ def strlist2enumeration(lst): return "%s, " * (len(lst) - 2) % lst[:-2] + "%s and %s" % lst[-2:] -def ensure_unicode(stuff, encoding='utf8', encoding2='latin-1'): - if not isinstance(stuff, (bytes, np.string_)): +def ensure_unicode(stuff, encoding="utf8", encoding2="latin-1"): + if not isinstance(stuff, (bytes, np.bytes_)): return stuff else: string = stuff try: string = string.decode(encoding) except BaseException: - string = string.decode(encoding2, errors='ignore') + string = string.decode(encoding2, errors="ignore") return string + def check_long_string(value, max_len): "Checks whether string is too long for printing in html metadata" - if not isinstance(value, (str, np.string_)): + if not isinstance(value, (str, np.bytes_)): value = repr(value) value = ensure_unicode(value) strvalue = str(value) _long = False if max_len is not None and len(strvalue) > 2 * max_len: right_limit = min(max_len, len(strvalue) - max_len) - strvalue = '%s ... %s' % ( - strvalue[:max_len], strvalue[-right_limit:]) + strvalue = "%s ... %s" % (strvalue[:max_len], strvalue[-right_limit:]) _long = True return _long, strvalue + def replace_html_symbols(str_value): "Escapes any &, < and > tags that would become invisible when printing html" str_value = str_value.replace("&", "&") @@ -755,6 +879,7 @@ def replace_html_symbols(str_value): str_value = str_value.replace(">", ">") return str_value + def add_key_value(key, value): "Returns the metadata value as a html string" return """ @@ -765,12 +890,12 @@ def add_key_value(key, value): def swapelem(obj, i, j): """Swaps element having index i with element having index j in object obj - IN PLACE. + in place. - Example - ------- + Examples + -------- >>> L = ['a', 'b', 'c'] - >>> spwapelem(L, 1, 2) + >>> swapelem(L, 1, 2) >>> print(L) ['a', 'c', 'b'] @@ -797,7 +922,7 @@ def rollelem(a, index, to_index=0): Returns ------- - res : list + list Output list. """ @@ -838,11 +963,17 @@ def find_subclasses(mod, cls): Returns ------- - dictonary in which key, item = subclass name, subclass + dict + Dictionary in which key, item = subclass name, subclass """ - return dict([(name, obj) for name, obj in inspect.getmembers(mod) - if inspect.isclass(obj) and issubclass(obj, cls)]) + return dict( + [ + (name, obj) + for name, obj in inspect.getmembers(mod) + if inspect.isclass(obj) and issubclass(obj, cls) + ] + ) def isiterable(obj): @@ -881,7 +1012,7 @@ def ordinal(value): Notes ----- Author: Serdar Tumgoren - http://code.activestate.com/recipes/576888-format-a-number-as-an-ordinal/ + https://code.activestate.com/recipes/576888-format-a-number-as-an-ordinal/ MIT license """ try: @@ -905,28 +1036,37 @@ def ordinal(value): def underline(line, character="-"): - """Return the line underlined. - - """ + """Return the line underlined.""" return line + "\n" + character * len(line) def closest_power_of_two(n): + """Returns next higher power of two, not the closest one as its name + suggests. + """ + # np.ceil would have to be replaced by np.round to do what the name says return int(2 ** np.ceil(np.log2(n))) -def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, - stack_metadata=True, show_progressbar=None, **kwargs): +def stack( + signal_list, + axis=None, + new_axis_name="stack_element", + lazy=None, + stack_metadata=True, + show_progressbar=None, + **kwargs, +): """Concatenate the signals in the list over a given axis or a new axis. - The title is set to that of the first signal in the list. + The title is set to that of the first signal in the list. Parameters ---------- - signal_list : list of BaseSignal instances + signal_list : list of :class:`~.api.signals.BaseSignal` List of signals to stack. - axis : {None, int, str} + axis : None, int or str If None, the signals are stacked over a new axis. The data must have the same dimensions. Otherwise the signals are stacked over the axis given by its integer index or its name. The data must have the @@ -941,7 +1081,7 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, If an axis with this name already exists it automatically append '-i', where `i` are integers, until it finds a name that is not yet in use. - lazy : {bool, None} + lazy : bool or None Returns a LazySignal if True. If None, only returns lazy result if at least one is lazy. %s @@ -949,32 +1089,25 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, Returns ------- - signal : BaseSignal instance + :class:`~.api.signals.BaseSignal` Examples -------- >>> data = np.arange(20) - >>> s = hs.stack([hs.signals.Signal1D(data[:10]), - ... hs.signals.Signal1D(data[10:])]) + >>> s = hs.stack( + ... [hs.signals.Signal1D(data[:10]), hs.signals.Signal1D(data[10:])] + ... ) >>> s - + >>> s.data array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]]) """ - from hyperspy.signals import BaseSignal - from hyperspy.axes import FunctionalDataAxis, UniformDataAxis, DataAxis - import dask.array as da from numbers import Number - for k in [k for k in ["mmap", "mmap_dir"] if k in kwargs]: - lazy = True - warnings.warn( - f"'{k}' argument is deprecated and will be removed in " - "HyperSpy v2.0. Please use 'lazy=True' instead.", - VisibleDeprecationWarning, - ) + from hyperspy.axes import DataAxis, FunctionalDataAxis, UniformDataAxis + from hyperspy.signals import BaseSignal axis_input = copy.deepcopy(axis) signal_list = list(signal_list) @@ -994,7 +1127,6 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, else: raise ValueError(f"Objects of type {type(_s)} cannot be stacked") - if lazy is None: lazy = any(_s._lazy for _s in signal_list) @@ -1017,29 +1149,45 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, if type(axis) is DataAxis: for _s in signal_list[1:]: _axis = _s.axes_manager[axis_input] - if (axis.axis[0] < axis.axis[-1] and axis.axis[-1] < _axis.axis[0]) \ - or (axis.axis[-1] < axis.axis[0] and _axis.axis[-1] < axis.axis[0]): + if ( + axis.axis[0] < axis.axis[-1] and axis.axis[-1] < _axis.axis[0] + ) or ( + axis.axis[-1] < axis.axis[0] and _axis.axis[-1] < axis.axis[0] + ): axis.axis = np.concatenate((axis.axis, _axis.axis)) else: - raise ValueError("Signals can only be stacked along a " + raise ValueError( + "Signals can only be stacked along a " "non-uniform axes if the axis values do not overlap" - " and have the correct order.") + " and have the correct order." + ) # stack axes if FunctionalDataAxis and its x axis is uniform - elif type(axis) is FunctionalDataAxis and \ - type(axis.axes_manager[axis_input].x) is UniformDataAxis: - axis.x.size = np.sum(step_sizes) + elif ( + type(axis) is FunctionalDataAxis + and type(axis.axes_manager[axis_input].x) is UniformDataAxis + ): + axis.x.size = np.sum(step_sizes) # stack axes if FunctionalDataAxis and its x axis is not uniform - elif type(axis) is FunctionalDataAxis and \ - type(axis.axes_manager[axis_input].x) is DataAxis: + elif ( + type(axis) is FunctionalDataAxis + and type(axis.axes_manager[axis_input].x) is DataAxis + ): for _s in signal_list[1:]: _axis = _s.axes_manager[axis_input] - if (axis.x.axis[0] < axis.x.axis[-1] and axis.x.axis[-1] < _axis.x.axis[0]) \ - or (axis.x.axis[-1] < axis.x.axis[0] and _axis.x.axis[-1] < axis.x.axis[0]): + if ( + axis.x.axis[0] < axis.x.axis[-1] + and axis.x.axis[-1] < _axis.x.axis[0] + ) or ( + axis.x.axis[-1] < axis.x.axis[0] + and _axis.x.axis[-1] < axis.x.axis[0] + ): axis.x.axis = np.concatenate((axis.x.axis, _axis.x.axis)) else: - raise ValueError("Signals can only be stacked along a " + raise ValueError( + "Signals can only be stacked along a " "non-uniform axes if the axis values do not overlap" - " and have the correct order.") + " and have the correct order." + ) datalist = [s.data for s in broadcasted_sigs] newdata = ( @@ -1050,7 +1198,9 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, if axis_input is None: signal = first.__class__(newdata) - signal.axes_manager._axes[1:] = copy.deepcopy(broadcasted_sigs[0].axes_manager._axes) + signal.axes_manager._axes[1:] = copy.deepcopy( + broadcasted_sigs[0].axes_manager._axes + ) axis_name = new_axis_name axis_names = [axis_.name for axis_ in signal.axes_manager._axes[1:]] j = 1 @@ -1068,26 +1218,26 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, signal.get_dimensions_from_data() # Set the metadata, if an stack_metadata is an integer, the metadata # will overwritten later - signal.metadata = first.metadata.deepcopy() - signal.metadata.General.title = f"Stack of {first.metadata.General.title}" + signal._metadata = first.metadata.deepcopy() + signal._metadata.General.title = f"Stack of {first.metadata.General.title}" # Stack metadata if isinstance(stack_metadata, bool): if stack_metadata: - signal.original_metadata.add_node('stack_elements') + signal.original_metadata.add_node("stack_elements") for i, obj in enumerate(signal_list): - signal.original_metadata.stack_elements.add_node(f'element{i}') - node = signal.original_metadata.stack_elements[f'element{i}'] + signal.original_metadata.stack_elements.add_node(f"element{i}") + node = signal.original_metadata.stack_elements[f"element{i}"] node.original_metadata = obj.original_metadata.deepcopy() node.metadata = obj.metadata.deepcopy() else: - signal.original_metadata = DictionaryTreeBrowser({}) + signal._original_metadata = DictionaryTreeBrowser({}) elif isinstance(stack_metadata, int): obj = signal_list[stack_metadata] - signal.metadata = obj.metadata.deepcopy() - signal.original_metadata = obj.original_metadata.deepcopy() + signal._metadata = obj.metadata.deepcopy() + signal._original_metadata = obj.original_metadata.deepcopy() else: - raise ValueError('`stack_metadata` must a boolean or an integer.') + raise ValueError("`stack_metadata` must a boolean or an integer.") if axis_input is None: axis_input = signal.axes_manager[-1 + 1j].index_in_axes_manager @@ -1102,7 +1252,8 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, ] ): variance = stack( - [s.metadata.Signal.Noise_properties.variance for s in signal_list], axis + [s.metadata.Signal.Noise_properties.variance for s in signal_list], + axis_input, ) signal.metadata.set_item("Signal.Noise_properties.variance", variance) else: @@ -1114,12 +1265,13 @@ def stack(signal_list, axis=None, new_axis_name="stack_element", lazy=None, return signal + stack.__doc__ %= (STACK_METADATA_ARG, SHOW_PROGRESSBAR_ARG) def shorten_name(name, req_l): if len(name) > req_l: - return name[:req_l - 2] + '..' + return name[: req_l - 2] + ".." else: return name @@ -1132,68 +1284,41 @@ def transpose(*args, signal_axes=None, navigation_axes=None, optimize=False): Examples -------- - >>> signal_iterable = [hs.signals.BaseSignal(np.random.random((2,)*(i+1))) - for i in range(3)] + >>> signal_iterable = [ + ... hs.signals.BaseSignal(np.random.random((2,)*(i+1))) for i in range(3) + ... ] >>> signal_iterable [, , ] >>> hs.transpose(*signal_iterable, signal_axes=1) - [, - , - ] - >>> hs.transpose(signal1, signal2, signal3, signal_axes=["Energy"]) + [, + , + ] + """ from hyperspy.signal import BaseSignal + if not all(map(isinstance, args, (BaseSignal for _ in args))): raise ValueError("Not all pased objects are signals") - return [sig.transpose(signal_axes=signal_axes, - navigation_axes=navigation_axes, - optimize=optimize) for sig in args] - - -def create_map_objects(function, nav_size, iterating_kwargs, **kwargs): - """To be used in _map_iterate of BaseSignal and LazySignal. - - Moved to a separate method to reduce code duplication. - """ - from hyperspy.signal import BaseSignal - from itertools import repeat - - iterators = tuple(iterating_kwargs[key]._cycle_signal() - if isinstance(iterating_kwargs[key], BaseSignal) else iterating_kwargs[key] - for key in iterating_kwargs) - # make all kwargs iterating for simplicity: - iterating = tuple(key for key in iterating_kwargs) - for k, v in kwargs.items(): - if k not in iterating: - iterating += k, - iterators += repeat(v, nav_size), - - def figure_out_kwargs(data): - _kwargs = {k: v for k, v in zip(iterating, data[1:])} - for k in iterating_kwargs: - if (isinstance(iterating_kwargs[k], BaseSignal) and - isinstance(_kwargs[k], np.ndarray) and - len(_kwargs[k]) == 1): - _kwargs[k] = _kwargs[k][0] - return data[0], _kwargs - - def func(*args): - dat, these_kwargs = figure_out_kwargs(*args) - return function(dat, **these_kwargs) - - return func, iterators - - -def process_function_blockwise(data, - *args, - function, - nav_indexes=None, - output_signal_size=None, - block_info=None, - arg_keys=None, - **kwargs): + return [ + sig.transpose( + signal_axes=signal_axes, navigation_axes=navigation_axes, optimize=optimize + ) + for sig in args + ] + + +def process_function_blockwise( + data, + *args, + function, + nav_indexes=None, + output_signal_size=None, + output_dtype=None, + arg_keys=None, + **kwargs, +): """ Convenience function for processing a function blockwise. By design, its output is used as an argument of the dask ``map_blocks`` so that the @@ -1210,10 +1335,10 @@ def process_function_blockwise(data, The function to applied to the signal axis nav_indexes : tuple The indexes of the navigation axes for the dataset. - output_signal_shape: tuple + output_signal_size: tuple The shape of the output signal. For a ragged signal, this is equal to 1 - block_info : dict - The block info as described by the ``dask.array.map_blocks`` function + output_dtype : dtype + The data type for the output. arg_keys : tuple The list of keys for the passed arguments (args). Together this makes a set of key:value pairs to be passed to the function. @@ -1223,26 +1348,28 @@ def process_function_blockwise(data, """ # Both of these values need to be passed in - dtype = block_info[None]["dtype"] + dtype = output_dtype chunk_nav_shape = tuple([data.shape[i] for i in sorted(nav_indexes)]) output_shape = chunk_nav_shape + tuple(output_signal_size) # Pre-allocating the output array - output_array = np.empty(output_shape, dtype=dtype) + output_array = np.empty(output_shape, dtype=dtype, like=data) if len(args) == 0: # There aren't any BaseSignals for iterating for nav_index in np.ndindex(chunk_nav_shape): islice = np.s_[nav_index] - output_array[islice] = function(data[islice], - **kwargs) + output_array[islice] = function(data[islice], **kwargs) else: # There are BaseSignals which iterate alongside the data for index in np.ndindex(chunk_nav_shape): islice = np.s_[index] - - iter_dict = {key: a[islice].squeeze() for key, a in zip(arg_keys,args)} - output_array[islice] = function(data[islice], - **iter_dict, - **kwargs) + iter_dict = {} + for key, a in zip(arg_keys, args): + arg_i = np.squeeze(a[islice]) + # Some functions do not handle 0-dimension NumPy arrays + if hasattr(arg_i, "shape") and arg_i.shape == (): + arg_i = arg_i[()] + iter_dict[key] = arg_i + output_array[islice] = function(data[islice], **iter_dict, **kwargs) if not (chunk_nav_shape == output_array.shape): try: output_array = output_array.squeeze(-1) @@ -1251,83 +1378,83 @@ def process_function_blockwise(data, return output_array -def guess_output_signal_size(test_signal, - function, - ragged, - **kwargs): +def _get_block_pattern(args, output_shape): + """Returns the block pattern used by the `blockwise` function for a + set of arguments give a resulting output_shape + + Parameters + ---------- + args: list + A list of all the arguments which are used for `da.blockwise` + output_shape: tuple + The output shape for the function passed to `da.blockwise` given args + """ + arg_patterns = tuple(tuple(range(a.ndim)) for a in args) + arg_shapes = tuple(a.shape for a in args) + output_pattern = tuple(range(len(output_shape))) + all_ind = arg_shapes + (output_shape,) + max_len = max((len(i) for i in all_ind)) # max number of dimensions + max_arg_len = max((len(i) for i in arg_shapes)) + adjust_chunks = {} + new_axis = {} + output_shape = output_shape + (0,) * (max_len - len(output_shape)) + for i in range(max_len): + shapes = np.array( + [s[i] if len(s) > i else -1 for s in (output_shape,) + arg_shapes] + ) + is_equal_shape = shapes == shapes[0] # if in shapes == output shapes + if not all(is_equal_shape): + if i > max_arg_len - 1: # output shape is a new axis + new_axis[i] = output_shape[i] + else: # output shape is an existing axis + adjust_chunks[i] = output_shape[i] # adjusting chunks based on output + arg_pairs = [(a, p) for a, p in zip(args, arg_patterns)] + return arg_pairs, adjust_chunks, new_axis, output_pattern + + +def guess_output_signal_size(test_data, function, ragged, **kwargs): """This function is for guessing the output signal shape and size. - It will attempt to apply the function to some test signal and then output + It will attempt to apply the function to some test data and then output the resulting signal shape and datatype. Parameters ---------- - test_signal: BaseSignal - A test signal for the function to be applied to. A signal - with 0 navigation dimensions - function: function + test_data : NumPy array + Data from a test signal for the function to be applied to. + The data must be from a signal with 0 navigation dimensions. + function : function The function to be applied to the data - ragged: bool + ragged : bool If the data is ragged then the output signal size is () and the data type is 'object' - **kwargs: dict + **kwargs : dict Any other keyword arguments passed to the function. """ if ragged: output_dtype = object output_signal_size = () else: - output = function(test_signal, **kwargs) - output_dtype = output.dtype - output_signal_size = output.shape + output = function(test_data, **kwargs) + try: + output_dtype = output.dtype + output_signal_size = output.shape + except AttributeError: + output = np.asarray(output) + output_dtype = output.dtype + output_signal_size = output.shape return output_signal_size, output_dtype -def map_result_construction(signal, - inplace, - result, - ragged, - sig_shape=None, - lazy=False): - from hyperspy.signals import BaseSignal - from hyperspy._lazy_signals import LazySignal - res = None - if inplace: - sig = signal - else: - res = sig = signal._deepcopy_with_new_data() - - if ragged: - sig.data = result - sig.axes_manager.remove(sig.axes_manager.signal_axes) - sig.__class__ = LazySignal if lazy else BaseSignal - sig.__init__(**sig._to_dictionary(add_models=True)) - else: - if not sig._lazy and sig.data.shape == result.shape and np.can_cast( - result.dtype, sig.data.dtype): - sig.data[:] = result - else: - sig.data = result - - # remove if too many axes - sig.axes_manager.remove(sig.axes_manager.signal_axes[len(sig_shape):]) - # add additional required axes - for ind in range( - len(sig_shape) - sig.axes_manager.signal_dimension, 0, -1): - sig.axes_manager._append_axis(size=sig_shape[-ind], navigate=False) - if not ragged: - sig.get_dimensions_from_data() - if not sig.axes_manager._axes: - add_scalar_axis(sig, lazy=lazy) - return res - def multiply(iterable): """Return product of sequence of numbers. Equivalent of functools.reduce(operator.mul, iterable, 1). - >>> product([2**8, 2**30]) + Example + + >>> multiply([2**8, 2**30]) 274877906944 - >>> product([]) + >>> multiply([]) 1 """ @@ -1341,23 +1468,16 @@ def iterable_not_string(thing): return isinstance(thing, Iterable) and not isinstance(thing, str) -def deprecation_warning(msg): - warnings.warn(msg, VisibleDeprecationWarning) - - def add_scalar_axis(signal, lazy=None): am = signal.axes_manager - from hyperspy.signal import BaseSignal from hyperspy._signals.lazy import LazySignal + from hyperspy.signal import BaseSignal + if lazy is None: lazy = signal._lazy signal.__class__ = LazySignal if lazy else BaseSignal am.remove(am._axes) - am._append_axis(size=1, - scale=1, - offset=0, - name="Scalar", - navigate=False) + am._append_axis(size=1, scale=1, offset=0, name="Scalar", navigate=False) def get_object_package_info(obj): @@ -1374,30 +1494,19 @@ def get_object_package_info(obj): dic["package"] = obj.__module__.split(".")[0] if dic["package"] != "__main__": try: - dic["package_version"] = importlib.import_module( - dic["package"]).__version__ + dic["package_version"] = importlib.import_module(dic["package"]).__version__ except AttributeError: dic["package_version"] = "" _logger.warning( - "The package {package} does not set its version in " + - "{package}.__version__. Please report this issue to the " + - "{package} developers.".format(package=dic["package"])) + "The package {package} does not set its version in " + + "{package}.__version__. Please report this issue to the " + + "{package} developers.".format(package=dic["package"]) + ) else: dic["package_version"] = "" return dic -def print_html(f_text, f_html): - """Print html version when in Jupyter Notebook""" - class PrettyText: - def __repr__(self): - return f_text() - - def _repr_html_(self): - return f_html() - return PrettyText() - - def is_hyperspy_signal(input_object): """ Check if an object is a Hyperspy Signal @@ -1414,27 +1523,182 @@ def is_hyperspy_signal(input_object): """ from hyperspy.signals import BaseSignal - return isinstance(input_object,BaseSignal) + + return isinstance(input_object, BaseSignal) def nested_dictionary_merge(dict1, dict2): - """ Merge dict2 into dict1 recursively - """ + """Merge dict2 into dict1 recursively""" for key, value in dict2.items(): - if (key in dict1 and isinstance(dict1[key], dict) - and isinstance(dict2[key], Mapping)): + if ( + key in dict1 + and isinstance(dict1[key], dict) + and isinstance(dict2[key], Mapping) + ): nested_dictionary_merge(dict1[key], dict2[key]) else: dict1[key] = dict2[key] -def is_binned(signal, axis=-1): - """Backwards compatibility check utility for is_binned attribute. +def is_cupy_array(array): + """ + Convenience function to determine if an array is a cupy array + + Parameters + ---------- + array : array + The array to determine whether it is a cupy array or not. + + Returns + ------- + bool + True if it is cupy array, False otherwise. + + """ + try: + import cupy as cp + + return isinstance(array, cp.ndarray) + except ImportError: + return False + + +def to_numpy(array): + """ + Returns the array as a numpy array. Raises an error when a dask array is + provided. + + Parameters + ---------- + array : numpy.ndarray or cupy.ndarray + Array to convert to numpy array. - Can be removed in v2.0. + Returns + ------- + array : numpy.ndarray + + Raises + ------ + TypeError + When the input array type is not supported. """ - if signal.metadata.has_item('Signal.binned'): - return signal.metadata.Signal.binned + if isinstance(array, np.ndarray): + return array + elif isinstance(array, da.Array): + raise TypeError( + "Implicit conversion of dask array to numpy array is not " + "supported, conversion needs to be done explicitely." + ) + elif is_cupy_array(array): # pragma: no cover + import cupy as cp + + return cp.asnumpy(array) else: - return signal.axes_manager[axis].is_binned + raise TypeError("Unsupported array type.") + + +def get_array_module(array): + """ + Returns the array module for the given array + + Parameters + ---------- + array : numpy or cupy array + Array to determine whether numpy or cupy should be used + + Returns + ------- + module : module + + """ + module = np + try: + import cupy as cp + + if isinstance(array, cp.ndarray): + module = cp + except ImportError: + pass + + return module + + +def display(obj): + """ + Convenience funtion to display an obj using IPython rendering if + installed, otherwise, fall back to python ``print`` + + Parameters + ---------- + obj : python object + Python object to be displayed + """ + try: + from IPython import display + + display.display(obj) + except ImportError: + print(obj) + + +class TupleSA(tuple): + """A tuple that can set the attributes of its items""" + + def __getitem__(self, *args, **kwargs): + item = super().__getitem__(*args, **kwargs) + try: + return type(self)(item) + except TypeError: + # When indexing, the returned object is not a tuple + return item + + def set(self, **kwargs): + """Set the attributes of its items + + Parameters + ---------- + kwargs : dict + The name of the attributes and their values. If a value is iterable, + then attribute of each item of the tuple will be set to each of the values. + """ + for key, value in kwargs.items(): + no_name = [item for item in self if not hasattr(item, key)] + if no_name: + raise AttributeError(f"'The items {no_name} have not attribute '{key}'") + else: + if isiterable(value) and not isinstance(value, str): + for item, value_ in zip(self, value): + setattr(item, key, value_) + else: + for item in self: + setattr(item, key, value) + + def get(self, *args): + """Get the attributes of its items + + Parameters + ---------- + args : list + The names of the attributes to get. + + Returns + ------- + output : dict + The name of the attributes and their values. + """ + output = dict() + for key in args: + values = list() + for item in self: + if not hasattr(item, key): + raise AttributeError(f"'The item {item} has not attribute '{key}'") + else: + values.append(getattr(item, key)) + output[key] = tuple(values) + return output + + def __add__(self, *args, **kwargs): + return type(self)(super().__add__(*args, **kwargs)) + def __mul__(self, *args, **kwargs): + return type(self)(super().__mul__(*args, **kwargs)) diff --git a/hyperspy/model.py b/hyperspy/model.py index bdd0a97d20..2cebbf9f6b 100644 --- a/hyperspy/model.py +++ b/hyperspy/model.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import copy import importlib @@ -23,24 +23,24 @@ import tempfile import warnings from contextlib import contextmanager -from distutils.version import LooseVersion from functools import partial -import dill +import cloudpickle +import dask.array as da import numpy as np -import scipy import scipy.odr as odr -from IPython.display import display, display_pretty +from dask.diagnostics import ProgressBar from scipy.linalg import svd from scipy.optimize import ( + OptimizeResult, differential_evolution, - leastsq, least_squares, + leastsq, minimize, - OptimizeResult ) from hyperspy.component import Component +from hyperspy.components1d import Expression from hyperspy.defaults_parser import preferences from hyperspy.docstrings.model import FIT_PARAMETERS_ARG from hyperspy.docstrings.signal import SHOW_PROGRESSBAR_ARG @@ -49,14 +49,22 @@ from hyperspy.extensions import ALL_EXTENSIONS from hyperspy.external.mpfit.mpfit import mpfit from hyperspy.external.progressbar import progressbar -from hyperspy.misc.export_dictionary import (export_to_dictionary, - load_from_dictionary, - parse_flag_string, - reconstruct_object) -from hyperspy.misc.model_tools import current_model_values +from hyperspy.misc.export_dictionary import ( + export_to_dictionary, + load_from_dictionary, + parse_flag_string, + reconstruct_object, +) +from hyperspy.misc.machine_learning import import_sklearn +from hyperspy.misc.model_tools import CurrentModelValues, _calculate_covariance from hyperspy.misc.slicing import copy_slice_from_whitelist -from hyperspy.misc.utils import (dummy_context_manager, shorten_name, slugify, - stash_active_state) +from hyperspy.misc.utils import ( + display, + dummy_context_manager, + shorten_name, + slugify, + stash_active_state, +) from hyperspy.signal import BaseSignal from hyperspy.ui_registry import add_gui_method @@ -64,54 +72,100 @@ _COMPONENTS = ALL_EXTENSIONS["components1D"] _COMPONENTS.update(ALL_EXTENSIONS["components1D"]) +EXSPY_HSPY_COMPONENTS = ( + "EELSArctan", + "EELSCLEdge", + "DoublePowerLaw", + "Vignetting", + "PESCoreLineShape", + "SEE", + "PESVoigt", + "VolumePlasmonDrude", +) -def _check_deprecated_optimizer(optimizer): - """Can be removed in HyperSpy 2.0""" - deprecated_optimizer_dict = { - "fmin": "Nelder-Mead", - "fmin_cg": "CG", - "fmin_ncg": "Newton-CG", - "fmin_bfgs": "BFGS", - "fmin_l_bfgs_b": "L-BFGS-B", - "fmin_tnc": "TNC", - "fmin_powell": "Powell", - "mpfit": "lm", - "leastsq": "lm", - } - check_optimizer = deprecated_optimizer_dict.get(optimizer, None) - - if check_optimizer: - warnings.warn( - f"`{optimizer}` has been deprecated and will be removed " - f"in HyperSpy 2.0. Please use `{check_optimizer}` instead.", - VisibleDeprecationWarning, - ) - optimizer = check_optimizer +def _twinned_parameter(parameter): + """ + Used in linear fitting. Since twinned parameters are not free, we need to + construct a mapping between the twinned parameter and the parameter + component to which the (non-free) twinned parameter component value needs + to be added. + + Returns + ------- + parameter when there is a twin and this twin is free + None when there is no twin or when the twin is not non-free itself, which + implies that the original parameter is not free + """ + twin = parameter.twin + if twin is None: + # there is no twin + return None + elif twin.free: + # this is the parameter we are looking for + return twin + elif twin.twin: + # recursive to find the final not twinned parameter + return _twinned_parameter(twin) + else: + # the twinned parameter is not twinned and it is not free, which means + # that the original parameter is twinned to a non-free parameter and + # therefore not free itself! + return None - return optimizer def reconstruct_component(comp_dictionary, **init_args): - _id = comp_dictionary['_id_name'] + # Restoring of Voigt and Arctan components saved with Hyperspy 4 + ): + # in HyperSpy 1.6 the old Voigt component was moved to PESVoigt + if comp_dictionary["parameters"][4]["_id_name"] == "resolution": + comp_dictionary["_id_name"] = "PESVoigt" + _logger.info( + "Legacy Voigt component converted to PESVoigt during file reading." + ) + if comp_dictionary["_id_name"] == "Arctan" and "minimum_at_zero" in comp_dictionary: + # in HyperSpy 1.6 the old Arctan component was moved to EELSArctan + if comp_dictionary["minimum_at_zero"] is True: + comp_dictionary["_id_name"] = "EELSArctan" + _logger.info( + "Legacy Arctan component converted to EELSArctan during file reading." + ) + _id = comp_dictionary["_id_name"] if _id in _COMPONENTS: _class = getattr( - importlib.import_module( - _COMPONENTS[_id]["module"]), _COMPONENTS[_id]["class"]) + importlib.import_module(_COMPONENTS[_id]["module"]), + _COMPONENTS[_id]["class"], + ) elif "_class_dump" in comp_dictionary: # When a component is not registered using the extension mechanism, - # it is serialized using dill. - _class = dill.loads(comp_dictionary['_class_dump']) + # it is serialized using cloudpickle. + try: + _class = cloudpickle.loads(comp_dictionary["_class_dump"]) + except TypeError: # pragma: no cover + # https://github.com/cloudpipe/cloudpickle/blob/master/README.md + raise TypeError( + "Pickling is not (always) supported between python " + "versions. As a result the custom class cannot be " + "loaded. Consider adding a custom Component using the " + "extension mechanism." + ) else: + # For component saved with hyperspy <2.0 and moved to exspy + if comp_dictionary["_id_name"] in EXSPY_HSPY_COMPONENTS: + comp_dictionary["package"] = "exspy" raise ImportError( - f'Loading the {comp_dictionary["class"]} component ' + - 'failed because the component is provided by the ' + - f'{comp_dictionary["package"]} Python package, but ' + - f'{comp_dictionary["package"]} is not installed.') + f'Loading the {comp_dictionary["_id_name"]} component ' + + "failed because the component is provided by the " + + f'`{comp_dictionary["package"]}` Python package, but ' + + f'{comp_dictionary["package"]} is not installed.' + ) return _class(**init_args) class ModelComponents(object): - """Container for model components. Useful to provide tab completion when running in IPython. @@ -123,12 +177,9 @@ def __init__(self, model): def __repr__(self): signature = "%4s | %19s | %19s | %19s" - ans = signature % ('#', - 'Attribute Name', - 'Component Name', - 'Component Type') + ans = signature % ("#", "Attribute Name", "Component Name", "Component Type") ans += "\n" - ans += signature % ('-' * 4, '-' * 19, '-' * 19, '-' * 19) + ans += signature % ("-" * 4, "-" * 19, "-" * 19, "-" * 19) if self._model: for i, c in enumerate(self._model): ans += "\n" @@ -140,33 +191,28 @@ def __repr__(self): name_string = shorten_name(name_string, 19) component_type = shorten_name(component_type, 19) - ans += signature % (i, - variable_name, - name_string, - component_type) + ans += signature % (i, variable_name, name_string, component_type) return ans @add_gui_method(toolkey="hyperspy.Model") class BaseModel(list): - """Model and data fitting tools applicable to signals of both one and two dimensions. Models of one-dimensional signals should use the - :py:class:`~hyperspy.models.model1d` and models of two-dimensional signals - should use the :class:`~hyperspy.models.model2d`. + :class:`~hyperspy.models.model1d.Model1D` and models of two-dimensional signals + should use the :class:`~hyperspy.models.model2d.Model2D`. A model is constructed as a linear combination of - :py:mod:`~hyperspy._components` that are added to the model using the - :py:meth:`~hyperspy.model.BaseModel.append` or - :py:meth:`~hyperspy.model.BaseModel.extend`. There are many predefined - components available in the in the :py:mod:`~hyperspy._components` - module. If needed, new components can be created easily using the code of - existing components as a template. + :mod:`~.api.model.components1D` or :mod:`~.api.model.components2D` + that are added to the model using the :meth:`append` or :meth:`extend`. + If needed, new components can be created easily created using using + :class:`~.api.model.components1D.Expression` code of existing components + as a template. Once defined, the model can be fitted to the data using :meth:`fit` or - :py:meth:`~hyperspy.model.BaseModel.multifit`. Once the optimizer reaches + :meth:`multifit`. Once the optimizer reaches the convergence criteria or the maximum number of iterations the new value of the component parameters are stored in the components. @@ -175,71 +221,182 @@ class BaseModel(list): Attributes ---------- - - signal : BaseSignal instance - It contains the data to fit. - chisq : :py:class:`~.signal.BaseSignal` of float - Chi-squared of the signal (or np.nan if not yet fit) - dof : :py:class:`~.signal.BaseSignal` of int - Degrees of freedom of the signal (0 if not yet fit) - components : :py:class:`~.model.ModelComponents` instance - The components of the model are attributes of this class. This provides - a convenient way to access the model components when working in IPython - as it enables tab completion. + signal : :class:`~.api.signals.BaseSignal` + chisq : :class:`~.api.signals.BaseSignal` + red_chisq : :class:`~.api.signals.BaseSignal` + dof : :class:`~.api.signals.BaseSignal` + components : :class:`~.model.ModelComponents` Methods ------- - set_signal_range, remove_signal range, reset_signal_range, - add signal_range. + append + Append one component to the model. + extend + Append multiple components to the model. + remove + Remove component from model. + set_signal_range_from_mask Customize the signal range to fit. - fit, multifit - Fit the model to the data at the current position or the - full dataset. - save_parameters2file, load_parameters_from_file - Save/load the parameter values to/from a file. - plot - Plot the model and the data. - enable_plot_components, disable_plot_components - Plot each component separately. (Use after `plot`.) - set_current_values_to - Set the current value of all the parameters of the given component as - the value for all the dataset. - enable_adjust_position, disable_adjust_position - Enable/disable interactive adjustment of the position of the components - that have a well defined position. (Use after `plot`). - fit_component - Fit just the given component in the given signal range, that can be - set interactively. - set_parameters_not_free, set_parameters_free - Fit the `free` status of several components and parameters at once. - - See also + fit + Fit the model to the data at the current position. + multifit + Fit the model to the data at all navigation position. + store_current_values + Store the value of the parameters at the current position. + fetch_stored_values + fetch stored values of the parameters. + save_parameters2file + Save the parameter values to a file. + load_parameters_from_file + Load the parameter values from a file. + enable_plot_components + Plot each component separately. Use after :meth`plot`. + disable_plot_components + Disable plotting each component separately. Use after :meth`plot`. + set_parameters_not_free + Fix some parameters. + set_parameters_free + Free some parameters. + set_parameters_value + Set the value of a parameter in components in a model to a specified + value. + as_signal + Generate a Signal1D instance (possible multidimensional) + from the model. + export_results + Save the value of the parameters in separate files. + plot_results + Plot the value of all parameters at all positions. + print_current_values + Print the value of the parameters at the current position. + as_dictionary + Exports the model to a dictionary that can be saved in a file. + + See Also -------- - :py:class:`~hyperspy.models.model1d.Model1D` - :py:class:`~hyperspy.models.model2d.Model2D` + hyperspy.models.model1d.Model1D, hyperspy.models.model2d.Model2D """ - def __init__(self): + # Defined in subclass + _signal_dimension = None + def __init__(self): self.events = Events() - self.events.fitted = Event(""" + self.events.fitted = Event( + """ Event that triggers after fitting changed at least one parameter. The event triggers after the fitting step was finished, and only of at least one of the parameters changed. - Arguments - --------- + Parameters + ---------- obj : Model The Model that the event belongs to - """, arguments=['obj']) + """, + arguments=["obj"], + ) + + # The private _binned attribute is created to store temporarily + # axes.is_binned or not. This avoids evaluating it during call of + # the model function, which is detrimental to the performances of + # multifit(). Setting it to None ensures that the existing behaviour + # is preserved. + self._binned = None + self.inav = ModelSpecialSlicers(self, True) + self.isig = ModelSpecialSlicers(self, False) def __hash__(self): # This is needed to simulate a hashable object so that PySide does not # raise an exception when using windows.connect return id(self) + def _get_current_data(self, onlyactive=False, component_list=None, binned=None): + """Evaluate the model numerically. Implementation requested in all sub-classes""" + raise NotImplementedError + + @property + def signal(self): + """The signal data to fit.""" + return self._signal + + @signal.setter + def signal(self, value): + if value.axes_manager.signal_dimension == self._signal_dimension: + self._signal = value + else: + raise ValueError( + f"The signal must have a signal dimension of {self._signal_dimension}." + ) + + @property + def chisq(self): + """Chi-squared of the signal (or np.nan if not yet fit).""" + return self._chisq + + @property + def dof(self): + """Degrees of freedom of the signal (0 if not yet fit)""" + return self._dof + + @property + def components(self): + """The components of the model are attributes of this class. + + This provides a convenient way to access the model components + when working in IPython as it enables tab completion. + """ + return self._components + + @property + def convolved(self): + raise NotImplementedError("This model does not support convolution.") + + @convolved.setter + def convolved(self, value): + # This is for compatibility with model saved with HyperSpy < 2.0 + if value: + raise NotImplementedError("This model does not support convolution.") + else: + _logger.warning( + "The `convolved` attribute is deprecated. It is only available in models that implement convolution." + ) + + def set_signal_range_from_mask(self, mask): + """ + Use the signal ranges as defined by the mask + + Parameters + ---------- + mask : numpy.ndarray of bool + A boolean array defining the signal range. Must be the same + shape as the reversed ``signal_shape``, i.e. ``signal_shape[::-1]``. + Where array values are ``True``, signal will be fitted, otherwise not. + + See Also + -------- + hyperspy.models.model1d.Model1D.set_signal_range, + hyperspy.models.model1d.Model1D.add_signal_range, + hyperspy.models.model1d.Model1D.remove_signal_range, + hyperspy.models.model1d.Model1D.reset_signal_range + + Examples + -------- + >>> s = hs.signals.Signal2D(np.random.rand(10, 10, 20)) + >>> mask = (s.sum() > 5) + >>> m = s.create_model() + >>> m.set_signal_range_from_mask(mask.data) + + """ + if mask.dtype != bool: + raise ValueError("`mask` argument must be an array with boolean dtype.") + if mask.shape != self.axes_manager._signal_shape_in_array: + raise ValueError( + "`mask` argument must have the same shape as `signal_shape`." + ) + self._channel_switches[:] = mask + def store(self, name=None): """Stores current model in the original signal @@ -281,45 +438,48 @@ def _load_dictionary(self, dic): * _whitelist: a dictionary with keys used as references of save attributes, for more information, see - :py:func:`~.misc.export_dictionary.load_from_dictionary` + :func:`~.misc.export_dictionary.load_from_dictionary` * components: a dictionary, with information about components of the model (see - :py:meth:`~.component.Parameter.as_dictionary` + :meth:`~.component.Parameter.as_dictionary` documentation for more details) * any field from _whitelist.keys() """ - if 'components' in dic: + if "components" in dic: while len(self) != 0: self.remove(self[0]) id_dict = {} - for comp in dic['components']: + for comp in dic["components"]: init_args = {} - for k, flags_str in comp['_whitelist'].items(): + for k, flags_str in comp["_whitelist"].items(): if not len(flags_str): continue - if 'init' in parse_flag_string(flags_str): + if "init" in parse_flag_string(flags_str): init_args[k] = reconstruct_object(flags_str, comp[k]) self.append(reconstruct_component(comp, **init_args)) id_dict.update(self[-1]._load_dictionary(comp)) # deal with twins: - for comp in dic['components']: - for par in comp['parameters']: - for tw in par['_twins']: - id_dict[tw].twin = id_dict[par['self']] - - if '_whitelist' in dic: + for comp in dic["components"]: + for par in comp["parameters"]: + for tw in par["_twins"]: + id_dict[tw].twin = id_dict[par["self"]] + + if "_whitelist" in dic: + channel_switches = dic["_whitelist"].pop("channel_switches", None) + if channel_switches: + # Before channel_switches was privatised + dic["_whitelist"]["_channel_switches"] = channel_switches load_from_dictionary(self, dic) def __repr__(self): title = self.signal.metadata.General.title - class_name = str(self.__class__).split("'")[1].split('.')[-1] + class_name = str(self.__class__).split("'")[1].split(".")[-1] if len(title): - return "<%s, title: %s>" % ( - class_name, self.signal.metadata.General.title) + return "<%s, title: %s>" % (class_name, self.signal.metadata.General.title) else: return "<%s>" % class_name @@ -344,11 +504,11 @@ def append(self, thing): Parameters ---------- - thing: `Component` instance. + thing : :class:`~hyperspy.component.Component` + The component to add to the model. """ if not isinstance(thing, Component): - raise ValueError( - "Only `Component` instances can be added to a model") + raise ValueError("Only `Component` instances can be added to a model") # Check if any of the other components in the model has the same name if thing in self: raise ValueError("Component already in model") @@ -371,8 +531,7 @@ def append(self, thing): thing._create_arrays() list.append(self, thing) thing.model = self - setattr(self.components, slugify(name_string, - valid_variable_name=True), thing) + setattr(self._components, slugify(name_string, valid_variable_name=True), thing) if self._plot_active: self._connect_parameters2update_plot(components=[thing]) self.signal._plot.signal_plot.update() @@ -399,16 +558,13 @@ def remove(self, thing): >>> s = hs.signals.Signal1D(np.empty(1)) >>> m = s.create_model() - >>> g = hs.model.components1D.Gaussian() - >>> m.append(g) - - You could remove `g` like this + >>> g1 = hs.model.components1D.Gaussian() + >>> g2 = hs.model.components1D.Gaussian() + >>> m.extend([g1, g2]) - >>> m.remove(g) + You could remove ``g1`` like this - Like this: - - >>> m.remove("Gaussian") + >>> m.remove(g1) Or like this: @@ -417,7 +573,9 @@ def remove(self, thing): """ thing = self._get_component(thing) if not np.iterable(thing): - thing = [thing, ] + thing = [ + thing, + ] for athing in thing: for parameter in athing.parameters: # Remove the parameter from its twin _twins @@ -430,15 +588,21 @@ def remove(self, thing): if self._plot_active: self.signal._plot.signal_plot.update() - def as_signal(self, component_list=None, out_of_range_to_nan=True, - show_progressbar=None, out=None, **kwargs): + def as_signal( + self, + component_list=None, + out_of_range_to_nan=True, + show_progressbar=None, + out=None, + **kwargs, + ): """Returns a recreation of the dataset using the model. By default, the signal range outside of the fitted range is filled with nans. Parameters ---------- - component_list : list of HyperSpy components, optional + component_list : list of :class:`~hyperspy.component.Component`, optional If a list of components is given, only the components given in the list is used in making the returned spectrum. The components can be specified by name, index or themselves. @@ -446,14 +610,15 @@ def as_signal(self, component_list=None, out_of_range_to_nan=True, If True the signal range outside of the fitted range is filled with nans. Default True. %s - out : {None, BaseSignal} + out : None or :class:`~hyperspy.api.signals.BaseSignal` The signal where to put the result into. Convenient for parallel processing. If None (default), creates a new one. If passed, it is assumed to be of correct shape and dtype and not checked. Returns ------- - BaseSignal : An instance of the same class as `BaseSignal`. + :class:`~hyperspy.api.signals.BaseSignal` + The model as a signal. Examples -------- @@ -470,46 +635,40 @@ def as_signal(self, component_list=None, out_of_range_to_nan=True, if show_progressbar is None: show_progressbar = preferences.General.show_progressbar - for k in [k for k in ["parallel", "max_workers"] if k in kwargs]: - warnings.warn( - f"`{k}` argument has been deprecated and will be removed in HyperSpy 2.0", - VisibleDeprecationWarning, - ) - if out is None: - data = np.empty(self.signal.data.shape, dtype='float') + data = np.empty(self.signal.data.shape, dtype="float") data.fill(np.nan) signal = self.signal.__class__( - data, - axes=self.signal.axes_manager._get_axes_dicts()) + data, axes=self.signal.axes_manager._get_axes_dicts() + ) + signal.set_signal_type(signal.metadata.Signal.signal_type) signal.metadata.General.title = ( - self.signal.metadata.General.title + " from fitted model") + self.signal.metadata.General.title + " from fitted model" + ) else: signal = out data = signal.data if not out_of_range_to_nan: # we want the full signal range, including outside the fitted - # range, we need to set all the channel_switches to True - channel_switches_backup = copy.copy(self.channel_switches) - self.channel_switches[:] = True + # range, we need to set all the _channel_switches to True + channel_switches_backup = copy.copy(self._channel_switches) + self._channel_switches[:] = True self._as_signal_iter( - component_list=component_list, - show_progressbar=show_progressbar, - data=data + component_list=component_list, show_progressbar=show_progressbar, data=data ) if not out_of_range_to_nan: - # Restore the channel_switches, previously set - self.channel_switches[:] = channel_switches_backup + # Restore the _channel_switches, previously set + self._channel_switches[:] = channel_switches_backup return signal as_signal.__doc__ %= SHOW_PROGRESSBAR_ARG - def _as_signal_iter(self, data, component_list=None, - show_progressbar=None): + def _as_signal_iter(self, data, component_list=None, show_progressbar=None): + # BUG: with lazy signal returns lazy signal with numpy array # Note that show_progressbar can be an int to determine the progressbar # position for a thread-friendly bars. Otherwise race conditions are # ugly... @@ -518,26 +677,26 @@ def _as_signal_iter(self, data, component_list=None, with stash_active_state(self if component_list else []): if component_list: - component_list = [self._get_component(x) - for x in component_list] + component_list = [self._get_component(x) for x in component_list] for component_ in self: active = component_ in component_list if component_.active_is_multidimensional: if active: - continue # Keep active_map + continue # Keep active_map component_.active_is_multidimensional = False component_.active = active maxval = self.axes_manager._get_iterpath_size() enabled = show_progressbar and (maxval != 0) - pbar = progressbar(total=maxval, disable=not enabled, - position=show_progressbar, leave=True) + pbar = progressbar( + total=maxval, disable=not enabled, position=show_progressbar, leave=True + ) for index in self.axes_manager: self.fetch_stored_values(only_fixed=False) data[self.axes_manager._getitem_tuple][ - np.where(self.channel_switches)] = self.__call__( - non_convolved=not self.convolved, onlyactive=True).ravel() + np.where(self._channel_switches) + ] = self._get_current_data(onlyactive=True).ravel() pbar.update(1) @property @@ -550,22 +709,26 @@ def _plot_active(self): def _connect_parameters2update_plot(self, components): if self._plot_active is False: return + lines = [ + line for line in (self._model_line, self._residual_line) if line is not None + ] for i, component in enumerate(components): - component.events.active_changed.connect( - self._model_line._auto_update_line, []) - for parameter in component.parameters: - parameter.events.value_changed.connect( - self._model_line._auto_update_line, []) + for line in lines: + component.events.active_changed.connect(line._auto_update_line, []) + for parameter in component.parameters: + parameter.events.value_changed.connect(line._auto_update_line, []) def _disconnect_parameters2update_plot(self, components): if self._model_line is None: return + lines = [ + line for line in (self._model_line, self._residual_line) if line is not None + ] for component in components: - component.events.active_changed.disconnect( - self._model_line._auto_update_line) - for parameter in component.parameters: - parameter.events.value_changed.disconnect( - self._model_line._auto_update_line) + for line in lines: + component.events.active_changed.disconnect(line._auto_update_line) + for parameter in component.parameters: + parameter.events.value_changed.disconnect(line._auto_update_line) def update_plot(self, render_figure=False, update_ylimits=False, **kwargs): """Update model plot. @@ -580,11 +743,11 @@ def update_plot(self, render_figure=False, update_ylimits=False, **kwargs): if self._plot_active is True and self._suspend_update is False: try: if self._model_line is not None: - self._model_line.update(render_figure=render_figure, - update_ylimits=update_ylimits) + self._model_line.update( + render_figure=render_figure, update_ylimits=update_ylimits + ) if self._plot_components: - for component in [component for component in self if - component.active is True]: + for component in self.active_components: self._update_component_line(component) except BaseException: self._disconnect_parameters2update_plot(components=self) @@ -610,7 +773,7 @@ def suspend_update(self, update_on_resume=True): es.add(p.events, f) for c in self: - if hasattr(c, '_component_line'): + if hasattr(c, "_component_line"): f = c._component_line._auto_update_line es.add(c.events, f) for p in c.parameters: @@ -627,48 +790,64 @@ def suspend_update(self, update_on_resume=True): position = c._position if position: position.events.value_changed.trigger( - obj=position, value=position.value) + obj=position, value=position.value + ) self.update_plot(render_figure=True, update_ylimits=False) def _close_plot(self): if self._plot_components is True: self.disable_plot_components() self._disconnect_parameters2update_plot(components=self) + if self._residual_line is not None: + self._residual_line = None self._model_line = None def enable_plot_components(self): + """ + Enable interactive adjustment of the position of the components + that have a well defined position. Use after + :meth:`~hyperspy.models.model1d.Model1D.plot`. + """ if self._plot is None or self._plot_components: return - for component in [component for component in self if - component.active]: + for component in self.active_components: self._plot_component(component) self._plot_components = True def disable_plot_components(self): + """ + Disable interactive adjustment of the position of the components + that have a well defined position. Use after + :meth:`~hyperspy.models.model1d.Model1D.plot`. + """ if self._plot is None: return if self._plot_components: - for component in self: + for component in self.active_components: self._disable_plot_component(component) self._plot_components = False + @property + def _free_parameters(self): + # TODO: improve the use of this property + """Get the free parameters of active components.""" + components = [c for c in self if c.active] + return tuple([p for c in components for p in c.parameters if p.free]) + def _set_p0(self): - "(Re)sets the initial values for the parameters used in the curve fitting functions" - self.p0 = () # Stores the values and is fed as initial values to the fitter - for component in self: - if component.active: - for parameter in component.free_parameters: - self.p0 = (self.p0 + (parameter.value,) - if parameter._number_of_elements == 1 - else self.p0 + parameter.value) - - def set_boundaries(self, bounded=True): - warnings.warn( - "`set_boundaries()` has been deprecated and " - "will be made private in HyperSpy 2.0.", - VisibleDeprecationWarning, - ) - self._set_boundaries(bounded=bounded) + """ + Sets the initial values for the parameters used in the curve fitting + functions + """ + # Stores the values and is fed as initial values to the fitter + self.p0 = () + for component in self.active_components: + for parameter in component.free_parameters: + self.p0 = ( + self.p0 + (parameter.value,) + if parameter._number_of_elements == 1 + else self.p0 + parameter.value + ) def _set_boundaries(self, bounded=True): """Generate the boundary list. @@ -690,31 +869,30 @@ def _set_boundaries(self, bounded=True): self.free_parameters_boundaries = None else: self.free_parameters_boundaries = [] - for component in self: - if component.active: - for param in component.free_parameters: - if param._number_of_elements == 1: - self.free_parameters_boundaries.append((param._bounds)) - else: - self.free_parameters_boundaries.extend((param._bounds)) - - def _bounds_as_tuple(self): - """Converts parameter bounds to tuples for least_squares()""" + for component in self.active_components: + for param in component.free_parameters: + if param._number_of_elements == 1: + self.free_parameters_boundaries.append((param._bounds)) + else: + self.free_parameters_boundaries.extend((param._bounds)) + + def _bounds_as_tuple(self, transpose): + """ + Converts parameter bounds to tuples for scipy optimizer. For scipy + ``least_squares``, ``transpose=True`` needs to be used, as the order of the + bounds are different. + """ if self.free_parameters_boundaries is None: return (-np.inf, np.inf) - return tuple( + bounds = tuple( (a if a is not None else -np.inf, b if b is not None else np.inf) for a, b in self.free_parameters_boundaries ) - - def set_mpfit_parameters_info(self, bounded=True): - warnings.warn( - "`set_mpfit_parameters_info()` has been deprecated and " - "will be made private in HyperSpy 2.0.", - VisibleDeprecationWarning, - ) - self._set_mpfit_parameters_info(bounded=bounded) + if transpose: + return tuple(zip(*bounds)) + else: + return bounds def _set_mpfit_parameters_info(self, bounded=True): """Generate the boundary list for mpfit. @@ -734,26 +912,25 @@ def _set_mpfit_parameters_info(self, bounded=True): self.mpfit_parinfo = None else: self.mpfit_parinfo = [] - for component in self: - if component.active: - for param in component.free_parameters: - limited = [False, False] - limits = [0, 0] - if param.bmin is not None: - limited[0] = True - limits[0] = param.bmin - if param.bmax is not None: - limited[1] = True - limits[1] = param.bmax - if param._number_of_elements == 1: - self.mpfit_parinfo.append( - {"limited": limited, "limits": limits} - ) - else: - self.mpfit_parinfo.extend( - ({"limited": limited, "limits": limits},) - * param._number_of_elements - ) + for component in self.active_components: + for param in component.free_parameters: + limited = [False, False] + limits = [0, 0] + if param.bmin is not None: + limited[0] = True + limits[0] = param.bmin + if param.bmax is not None: + limited[1] = True + limits[1] = param.bmax + if param._number_of_elements == 1: + self.mpfit_parinfo.append( + {"limited": limited, "limits": limits} + ) + else: + self.mpfit_parinfo.extend( + ({"limited": limited, "limits": limits},) + * param._number_of_elements + ) def ensure_parameters_in_bounds(self): """For all active components, snaps their free parameter values to @@ -784,7 +961,7 @@ def ensure_parameters_in_bounds(self): param.value = tuple(values) def store_current_values(self): - """ Store the parameters of the current coordinates into the + """Store the parameters of the current coordinates into the `parameter.map` array and sets the `is_set` array attribute to True. If the parameters array has not being defined yet it creates it filling @@ -860,12 +1037,12 @@ def _fetch_values_from_p0(self, p_std=None): for component in self: # Cut the parameters list if component.active is True: if p_std is not None: - comp_p_std = p_std[ - counter: counter + - component._nfree_param] + comp_p_std = p_std[counter : counter + component._nfree_param] component.fetch_values_from_array( - self.p0[counter: counter + component._nfree_param], - comp_p_std, onlyfree=True) + self.p0[counter : counter + component._nfree_param], + comp_p_std, + onlyfree=True, + ) counter += component._nfree_param def _model2plot(self, axes_manager, out_of_range2nans=True): @@ -874,23 +1051,314 @@ def _model2plot(self, axes_manager, out_of_range2nans=True): old_axes_manager = self.axes_manager self.axes_manager = axes_manager self.fetch_stored_values() - s = self.__call__(non_convolved=False, onlyactive=True) + s = self._get_current_data(onlyactive=True) if old_axes_manager is not None: self.axes_manager = old_axes_manager self.fetch_stored_values() if out_of_range2nans is True: ns = np.empty(self.axis.axis.shape) ns.fill(np.nan) - ns[np.where(self.channel_switches)] = s + ns[np.where(self._channel_switches)] = s s = ns return s def _model_function(self, param): self.p0 = param self._fetch_values_from_p0() - to_return = self.__call__(non_convolved=False, onlyactive=True) + to_return = self._get_current_data(onlyactive=True, binned=self._binned) return to_return + @property + def active_components(self): + """List all nonlinear parameters.""" + return tuple([c for c in self if c.active]) + + def _convolve_component_values(self, component_values): + raise NotImplementedError("This model does not support convolution") + + def _compute_constant_term(self, component): + """Gets the value of any (non-free) constant term""" + signal_shape = self.axes_manager.signal_shape[::-1] + data = component._constant_term * np.ones(signal_shape) + return data.T[np.where(self._channel_switches)[::-1]].T + + def _linear_fit( + self, + optimizer="lstsq", + calculate_errors=False, + only_current=True, + weights=None, + **kwargs, + ): + """ + Multivariate linear fitting + + Parameters + ---------- + optimizer : str, default is "lstsq" + + * ``'lstsq'`` - Default, least square using :func:`numpy.linalg.lstsq`. + * ``'ols'`` - Ordinary least square using + :class:`sklearn.linear_model.LinearRegression` + * ``'nnls'`` - Linear regression with positive constraints on the + regression coefficients using + :class:`sklearn.linear_model.LinearRegression` + * ``'ridge'`` - least square supporting regularisation using + :class:`sklearn.linear_model.Ridge`. The parameter + ``alpha`` controlling regularization strength can be passed + as keyword argument, see :class:`sklearn.linear_model.Ridge` + for more information. + + Only 'lstsq' suppors lazy signals. + calculate_errors : bool, default is False + If True, calculate the errors. + only_current : bool, default is True + Fit the current index only, instead of the whole navigation space. + kwargs : dict, optional + Keyword arguments are passed to the corresponding optimizer. + + Notes + ----- + More linear optimizers can be added in the future, but note that in order + to support simultaneous fitting across the dataset, the optimizer must + support "two-dimensional y" - see the ``b`` parameter in + :func:`numpy.linalg.lstsq`. + + Currently, the overhead in calculating the component data takes about + 100 times longer than actually running :func:`np.linalg.lstsq`. + That means that going pixel-by-pixel, calculating the component data + each time is not faster than the normal nonlinear methods. Linear + fitting is hence currently only useful for fitting a dataset in the + vectorized manner. + """ + if optimizer == "ridge_regression": + warnings.warn( + "`'ridge_regression'` has been renamed to `'ridge'`. " + "Use `optimizer='ridge'` instead.", + VisibleDeprecationWarning, + ) + optimizer = "ridge" + + signal_axes = self.signal.axes_manager.signal_axes + if any([not ax.is_uniform and ax.is_binned for ax in signal_axes]): + raise ValueError( + "Linear fitting doesn't support signal axes, " + "which are binned and non-uniform." + ) + + free_nonlinear_parameters = [ + p + for c in self.active_components + for p in c.parameters + if p.free and not p._linear + ] + if free_nonlinear_parameters: + raise RuntimeError( + "Not all free parameters are linear. Fit with a different " + "optimizer or set non-linear `parameters.free = False`. " + "Consider using " + "`m.set_parameters_not_free(only_nonlinear=True)`. " + "These parameters are nonlinear and free:" + + "\n\t" + + str("\n\t".join(str(p) for p in free_nonlinear_parameters)) + ) + + # We get the list of parameters; twin parameters are not free and + # their component need be combined with the component its parameter + # is twinned with - see the `twin_parameters_mapping` + parameters = [p for c in self.active_components for p in c.parameters if p.free] + + n_parameters = len(parameters) + if not n_parameters: + raise RuntimeError("Model does not contain any free components!") + + # 'parameter':'twin' taking into account the fact that the twin is not + # necessary free or the twin can be twinned itself! + twin_parameters_mapping = { + p: _twinned_parameter(p) + for c in self.active_components + for p in c.parameters + if _twinned_parameter(p) is not None + } + + # Linear parameters must be set to a nonzero value before fitting to + # avoid the entire component being zero. The value of 1 is chosen for + # no particular reason. + for parameter in parameters: + if parameter._linear and parameter.free: + parameter.value = 1.0 + + channels_signal_shape = np.count_nonzero(self._channel_switches) + comp_values = np.zeros((n_parameters, channels_signal_shape)) + constant_term = np.zeros(channels_signal_shape) + + for component in self.active_components: + # Components that can be separated into multiple linear parts, + # like "C = a*x + b" may have C._constant_term != 0, eg if b is + # not free and nonzero. For Expression components, the constant + # term is calculated automatically. Custom components with one + # parameter are fine, since either the entire component is free + # or fixed, but for custom components with more than one parameter + # we cannot automatically determine this. + + # Also consider (non-free) twinned parameters + free_parameters = [ + p + for p in component.parameters + if p.free or p in twin_parameters_mapping + ] + + if len(free_parameters) > 1: + if not isinstance(component, Expression): + raise AttributeError( + f"Component {component} has more than one free " + "parameter, which is only supported for " + "`Expression` component." + ) + free, fixed = component._separate_pseudocomponents() + for p in free_parameters: + # Use the index in the `parameters` list as reference + # to defined the position in the numpy array + index = parameters.index(p) + comp_values[index] = component._compute_expression_part( + free[p.name] + ) + constant_term += component._compute_expression_part(fixed) + + elif len(free_parameters) == 1: + p = free_parameters[0] + if p.twin: + # to get the correct `comp_values` index, we need the twin + p = twin_parameters_mapping[p] + + index = parameters.index(p) + comp_value = self._get_current_data( + component_list=[component], binned=False + ) + comp_constant_values = self._compute_constant_term(component=component) + comp_values[index] += comp_value - comp_constant_values + constant_term += comp_constant_values + + else: + # No free parameters, so component is fixed. + constant_term += self._get_current_data( + component_list=[component], binned=False + ) + + # Reshape what may potentially be Signal2D data into a long Signal1D + # shape and an nD navigation shape to a 1D nav shape + _channel_switches = np.where(self._channel_switches.ravel())[0] + if only_current: + target_signal = self.signal._get_current_data().ravel()[_channel_switches] + else: + sig_shape = self.axes_manager._signal_shape_in_array + nav_shape = self.axes_manager._navigation_shape_in_array + target_signal = self.signal.data.reshape( + (np.prod(nav_shape, dtype=int),) + (np.prod(sig_shape, dtype=int),) + )[:, _channel_switches] + + if any([ax.is_binned for ax in signal_axes]): + target_signal = target_signal / np.prod( + tuple((ax.scale for ax in signal_axes if ax.is_binned)) + ) + + target_signal = target_signal - constant_term + + if weights is not None: + comp_values = comp_values * weights + target_signal = target_signal * weights + + if self.signal._lazy and optimizer in ["ols", "nnls", "ridge"]: + raise ValueError( + f"The optimizer {optimizer} can't operate lazily, " + "use the `lstsq` optimizer instead." + ) + + if optimizer == "lstsq": + if self.signal._lazy: + xp = da + kw = {} + else: + xp = np + kw = kwargs + kw.setdefault("rcond", None) + + result, residual, *_ = np.linalg.lstsq( + xp.asanyarray(comp_values.T), target_signal.T, **kw + ) + coefficient_array = result.T + + elif optimizer in ["ols", "nnls"]: + if optimizer == "nnls": + kwargs["positive"] = True + kwargs.setdefault("fit_intercept", False) + reg = import_sklearn.sklearn.linear_model.LinearRegression(**kwargs) + results = reg.fit(X=comp_values.T, y=target_signal.T) + coefficient_array = results.coef_ + residual = None + + elif optimizer == "ridge": + # scikit-learn documentation advices against setting alpha=0.0 + # for numerical consideration + # https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Ridge.html + kwargs.setdefault("alpha", 0.01) + kwargs.setdefault("fit_intercept", False) + reg = import_sklearn.sklearn.linear_model.Ridge(**kwargs) + results = reg.fit(X=comp_values.T, y=target_signal.T) + coefficient_array = results.coef_ + residual = None + else: + raise ValueError( + f"Optimizer `'{optimizer}'` not supported. " + "Use 'lstsq', 'ols', 'nnls' or 'ridge'." + ) + + fit_output = {"x": coefficient_array} + + # TODO: reorganise to do lazy computation (coeff and error together) + if self.signal._lazy: + cm = ( + ProgressBar if kwargs.get("show_progressbar") else dummy_context_manager + ) + with cm(): + fit_output["x"] = fit_output["x"].compute() + + # Calculate errors + # We only do this if going pixel-by-pixel or if `calculate_errors =True` + # is specified in multifit. This is because it is a very large + # calculation and can eat all our ram, even when run lazily. + if calculate_errors: + covariance = _calculate_covariance( + target_signal=target_signal, + coefficients=coefficient_array, + component_data=comp_values, + residual=residual, + lazy=self.signal._lazy, + ) + std_error = np.sqrt(np.diagonal(covariance, axis1=-2, axis2=-1)) + fit_output["covar"] = covariance + fit_output["perror"] = abs(fit_output["x"]) * std_error + + if not only_current: + # The nav shape will have been flattened. We reshape it here. + fit_output["x"] = fit_output["x"].reshape(nav_shape + (n_parameters,)) + + if calculate_errors: + fit_output["covar"] = fit_output["covar"].reshape( + nav_shape + (n_parameters, n_parameters) + ) + fit_output["perror"] = fit_output["perror"].reshape( + nav_shape + (n_parameters,) + ) + + if self.signal._lazy and calculate_errors: + with cm(): + fit_output["perror"] = fit_output["perror"].compute() + + fit_output["success"] = True + + return fit_output + def _errfunc_sq(self, param, y, weights=None): if weights is None: weights = 1.0 @@ -907,7 +1375,8 @@ def _errfunc4mpfit(self, p, fjac=None, x=None, y=None, weights=None): return [0, self._jacobian(p, y).T] def _get_variance(self, only_current=True): - """Return the variance taking into account the `channel_switches`. + """ + Return the variance taking into account the `_channel_switches`. If only_current=True, the variance for the current navigation indices is returned, otherwise the variance for all navigation indices is returned. @@ -918,19 +1387,22 @@ def _get_variance(self, only_current=True): if only_current: variance = variance.data.__getitem__( self.axes_manager._getitem_tuple - )[np.where(self.channel_switches)] + )[np.where(self._channel_switches)] else: - variance = variance.data[..., np.where( - self.channel_switches)[0]] + variance = variance.data[..., np.where(self._channel_switches)[0]] else: variance = 1.0 return variance def _calculate_chisq(self): variance = self._get_variance() - d = self(onlyactive=True).ravel() - self.signal()[np.where( - self.channel_switches)] - d *= d / (1. * variance) # d = difference^2 / variance. + d = ( + self._get_current_data(onlyactive=True, binned=self._binned).ravel() + - self.signal._get_current_data(as_numpy=True)[ + np.where(self._channel_switches) + ] + ) + d *= d / (1.0 * variance) # d = difference^2 / variance. self.chisq.data[self.signal.axes_manager.indices[::-1]] = d.sum() def _set_current_degrees_of_freedom(self): @@ -938,12 +1410,14 @@ def _set_current_degrees_of_freedom(self): @property def red_chisq(self): - """:py:class:`~.signal.BaseSignal`: Reduced chi-squared. + """The Reduced chi-squared. + Calculated from ``self.chisq`` and ``self.dof``. """ - tmp = self.chisq / (- self.dof + self.channel_switches.sum() - 1) - tmp.metadata.General.title = self.signal.metadata.General.title + \ - ' reduced chi-squared' + tmp = self.chisq / (-self.dof + self._channel_switches.sum() - 1) + tmp.metadata.General.title = ( + self.signal.metadata.General.title + " reduced chi-squared" + ) return tmp def _calculate_parameter_std(self, pcov, cost, ysize): @@ -978,17 +1452,12 @@ def _calculate_parameter_std(self, pcov, cost, ysize): return p_var def _convert_variance_to_weights(self): - weights = None - variance = self.signal.get_noise_variance() - - if variance is not None: - if isinstance(variance, BaseSignal): - variance = variance.data.__getitem__(self.axes_manager._getitem_tuple)[ - np.where(self.channel_switches) - ] - - _logger.info("Setting weights to 1/variance of signal noise") + if self.signal.get_noise_variance() is None: + weights = None + else: + variance = self._get_variance(only_current=True) + _logger.info("Setting weights to 1/variance of signal noise.") # Note that we square this later in self._errfunc_sq() weights = 1.0 / np.sqrt(variance) @@ -1022,7 +1491,7 @@ def fit( ----- The chi-squared and reduced chi-squared statistics, and the degrees of freedom, are computed automatically when fitting, - only when `loss_function="ls"`. They are stored as signals: + only when ``loss_function="ls"``. They are stored as signals: ``chisq``, ``red_chisq`` and ``dof``. If the attribute ``metada.Signal.Noise_properties.variance`` @@ -1040,8 +1509,7 @@ def fit( See Also -------- - * :py:meth:`~hyperspy.model.BaseModel.multifit` - * :py:meth:`~hyperspy.model.EELSModel.fit` + multifit, fit """ cm = ( @@ -1050,84 +1518,12 @@ def fit( else dummy_context_manager ) - # --------------------------------------------- - # Deprecated arguments (remove in HyperSpy 2.0) - # --------------------------------------------- - - # Deprecate "fitter" argument - check_fitter = kwargs.pop("fitter", None) - if check_fitter: - warnings.warn( - f"`fitter='{check_fitter}'` has been deprecated and will be removed " - f"in HyperSpy 2.0. Please use `optimizer='{check_fitter}'` instead.", - VisibleDeprecationWarning, - ) - optimizer = check_fitter - - # Deprecated optimization algorithms - optimizer = _check_deprecated_optimizer(optimizer) - - # Deprecate loss_function - if loss_function == "ml": - warnings.warn( - "`loss_function='ml'` has been deprecated and will be removed in " - "HyperSpy 2.0. Please use `loss_function='ML-poisson'` instead.", - VisibleDeprecationWarning, - ) - loss_function = "ML-poisson" - - # Deprecate grad=True/False - if isinstance(grad, bool): - alt_grad = "analytical" if grad else None - warnings.warn( - f"`grad={grad}` has been deprecated and will be removed in " - f"HyperSpy 2.0. Please use `grad={alt_grad}` instead.", - VisibleDeprecationWarning, - ) - grad = alt_grad - - # Deprecate ext_bounding - ext_bounding = kwargs.pop("ext_bounding", False) - if ext_bounding: - warnings.warn( - "`ext_bounding=True` has been deprecated and will be removed " - "in HyperSpy 2.0. Please use `bounded=True` instead.", - VisibleDeprecationWarning, - ) - - # Deprecate custom min_function - min_function = kwargs.pop("min_function", None) - if min_function: - warnings.warn( - "`min_function` has been deprecated and will be removed " - "in HyperSpy 2.0. Please use `loss_function` instead.", - VisibleDeprecationWarning, - ) - loss_function = min_function - - # Deprecate custom min_function - min_function_grad = kwargs.pop("min_function_grad", None) - if min_function_grad: - warnings.warn( - "`min_function_grad` has been deprecated and will be removed " - "in HyperSpy 2.0. Please use `grad` instead.", - VisibleDeprecationWarning, - ) - grad = min_function_grad - - # --------------------------- - # End of deprecated arguments - # --------------------------- - # Supported losses and optimizers _supported_global = { "Differential Evolution": differential_evolution, } if optimizer in ["Dual Annealing", "SHGO"]: - if LooseVersion(scipy.__version__) < LooseVersion("1.2.0"): - raise ValueError(f"`optimizer='{optimizer}'` requires scipy >= 1.2.0") - from scipy.optimize import dual_annealing, shgo _supported_global.update({"Dual Annealing": dual_annealing, "SHGO": shgo}) @@ -1147,6 +1543,17 @@ def fit( "Dual Annealing", "SHGO", ] + # The bounds need to be tranposed for these optimizer + # ((min, max), (min, max)) versus ((min, min), (max, max)) + _transpose_bounds = ( + True + if optimizer + in [ + "trf", # Use least_squares + "dogbox", # Use least_squares + ] + else False + ) _supported_deriv_free = [ "Powell", "COBYLA", @@ -1244,7 +1651,7 @@ def fit( self._set_p0() old_p0 = self.p0 - if ext_bounding: + if bounded: self._enable_ext_bounding() # Get weights if metadata.Signal.Noise_properties.variance @@ -1257,7 +1664,12 @@ def fit( # Will proceed with unweighted fitting. weights = None - args = (self.signal()[np.where(self.channel_switches)], weights) + args = ( + self.signal._get_current_data(as_numpy=True)[ + np.where(self._channel_switches) + ], + weights, + ) if optimizer == "lm": if bounded: @@ -1275,7 +1687,9 @@ def fit( self.p0[:], parinfo=self.mpfit_parinfo, functkw={ - "y": self.signal()[self.channel_switches], + "y": self.signal._get_current_data()[ + self._channel_switches + ], "weights": weights, }, autoderivative=auto_deriv, @@ -1289,7 +1703,7 @@ def fit( self.p0 = self.fit_output.x ysize = len(self.fit_output.x) + self.fit_output.dof cost = self.fit_output.fnorm - pcov = self.fit_output.perror ** 2 + pcov = self.fit_output.perror**2 # Calculate estimated parameter standard deviation self.p_std = self._calculate_parameter_std(pcov, cost, ysize) @@ -1322,7 +1736,7 @@ def fit( self.p0 = self.fit_output.x ysize = len(self.fit_output.fun) - cost = np.sum(self.fit_output.fun ** 2) + cost = np.sum(self.fit_output.fun**2) pcov = self.fit_output.covar # Calculate estimated parameter standard deviation @@ -1342,7 +1756,7 @@ def _wrap_jac(*args, **kwargs): self._errfunc, self.p0[:], args=args, - bounds=self._bounds_as_tuple(), + bounds=self._bounds_as_tuple(transpose=_transpose_bounds), jac=grad, method=optimizer, **kwargs, @@ -1359,7 +1773,7 @@ def _wrap_jac(*args, **kwargs): threshold = np.finfo(float).eps * max(jac.shape) * s[0] s = s[s > threshold] VT = VT[: s.size] - pcov = np.dot(VT.T / s ** 2, VT) + pcov = np.dot(VT.T / s**2, VT) # Calculate estimated parameter standard deviation self.p_std = self._calculate_parameter_std(pcov, cost, ysize) @@ -1374,8 +1788,8 @@ def _wrap_jac(*args, **kwargs): modelo = odr.Model(fcn=self._function4odr, fjacb=odr_jacobian) mydata = odr.RealData( - self.axis.axis[np.where(self.channel_switches)], - self.signal()[np.where(self.channel_switches)], + self.axis.axis[np.where(self._channel_switches)], + self.signal._get_current_data()[np.where(self._channel_switches)], sx=None, sy=(1.0 / weights if weights is not None else None), ) @@ -1397,6 +1811,30 @@ def _wrap_jac(*args, **kwargs): self.p0 = self.fit_output.x self.p_std = self.fit_output.perror + elif optimizer in ["lstsq", "ols", "nnls", "ridge", "ridge_regression"]: + # multifit pass this kwargs when necessary + only_current = kwargs.get("only_current", True) + # Errors are calculated when specifying calculate_errors=True + # or when fitting pixel by pixel + kwargs.setdefault("calculate_errors", only_current) + + fit_output = self._linear_fit( + optimizer=optimizer, weights=weights, **kwargs + ) + self.fit_output = OptimizeResult(**fit_output) + + if only_current: + # fit_output will have only one entry + indices = () + else: + indices = self.axes_manager.indices[::-1] + + self.p0 = self.fit_output.x[indices] + if kwargs["calculate_errors"]: + self.p_std = self.fit_output.perror[indices] + else: + self.p_std = len(self.p0) * (np.nan,) + else: # scipy.optimize.* functions if loss_function == "ls": @@ -1417,7 +1855,7 @@ def _wrap_jac(*args, **kwargs): self._set_boundaries(bounded=bounded) if optimizer in _supported_global: - de_b = self._bounds_as_tuple() + de_b = self._bounds_as_tuple(transpose=_transpose_bounds) if np.any(~np.isfinite(de_b)): raise ValueError( @@ -1449,10 +1887,11 @@ def _wrap_jac(*args, **kwargs): self._fetch_values_from_p0(p_std=self.p_std) self.store_current_values() + self._calculate_chisq() self._set_current_degrees_of_freedom() - if ext_bounding: + if bounded: self._disable_ext_bounding() if np.any(old_p0 != self.p0): @@ -1513,21 +1952,19 @@ def multifit( If True, update the plot for every position as they are processed. Note that this slows down the fitting by a lot, but it allows for interactive monitoring of the fitting (if in interactive mode). - iterpath : {None, "flyback", "serpentine"}, default None - If "flyback": + iterpath : {None, ``"flyback"``, ``"serpentine"``}, default None + If ``"flyback"``: At each new row the index begins at the first column, - in accordance with the way :py:class:`numpy.ndindex` generates indices. - If "serpentine": + in accordance with the way :class:`numpy.ndindex` generates indices. + If ``"serpentine"``: Iterate through the signal in a serpentine, "snake-game"-like manner instead of beginning each new row at the first index. Works for n-dimensional navigation space, not just 2D. If None: - Currently ``None -> "flyback"``. The default argument will use - the ``"flyback"`` iterpath, but shows a warning that this will - change to ``"serpentine"`` in version 2.0. - **kwargs : keyword arguments + Use the value of :attr:`~.axes.AxesManager.iterpath`. + **kwargs : dict Any extra keyword argument will be passed to the fit method. - See the documentation for :py:meth:`~hyperspy.model.BaseModel.fit` + See the documentation for :meth:`~hyperspy.model.BaseModel.fit` for a list of valid arguments. Returns @@ -1536,7 +1973,7 @@ def multifit( See Also -------- - * :py:meth:`~hyperspy.model.BaseModel.fit` + fit """ if show_progressbar is None: @@ -1560,61 +1997,182 @@ def multifit( "The mask must be a numpy array of boolean type with " f"shape: {self.axes_manager._navigation_shape_in_array}" ) - - if iterpath is None: - if self.axes_manager.iterpath == "flyback": - # flyback is set by default in axes_manager.iterpath on signal creation - warnings.warn( - "The `iterpath` default will change from 'flyback' to 'serpentine' " - "in HyperSpy version 2.0. Change the 'iterpath' argument to other than " - "None to suppress this warning.", - VisibleDeprecationWarning, - ) - # otherwise use whatever is set at m.axes_manager.iterpath - else: - self.axes_manager.iterpath = iterpath + linear_fitting = kwargs.get("optimizer", "") in [ + "lstsq", + "ols", + "nnls", + "ridge", + "ridge_regression", + ] masked_elements = 0 if mask is None else mask.sum() maxval = self.axes_manager._get_iterpath_size(masked_elements) show_progressbar = show_progressbar and (maxval != 0) + # The _binned attribute is evaluated only once in the multifit procedure + # and stored in an instance variable + if self.axes_manager.signal_dimension == 1: + self._binned = self.axes_manager[-1].is_binned + else: + # binning Not Implemented for Model2D + self._binned = False + + try: + convolved = self.convolved + except NotImplementedError: + convolved = False + + if linear_fitting: + # Check that all non-free parameters don't change accross + # the navigation dimension. If this is the case, we can fit the + # dataset in the vectorized fashion + # Only "purely" fixed (not twinned) parameters are relevant + nonfree_parameters = [ + p for c in self.active_components for p in c.parameters if not p._free + ] + navigation_variable_nonfree_parameters = [ + p + for p in nonfree_parameters + if ( + np.any(p.map["is_set"]) + and np.any(p.map["values"] != p.map["values"][0]) + ) + ] + # Check that all active components are active for the whole + # navigation dimension + active_is_multidimensional = [ + c + for c in self + if c.active_is_multidimensional and np.any(~c._active_array) + ] + + if len(navigation_variable_nonfree_parameters) > 0: + warnings.warn( + "The model contains non-free parameters that have set " + "values that vary across the navigation indices, which " + "is not supported when fitting the dataset in a vectorized " + "fashion. Fitting proceeds by iterating over the " + "navigation dimensions, which is significantly slower " + "than if all parameters had constant values.\n" + "These parameters are:\n\t" + + "\n\t".join( + str(x) for x in navigation_variable_nonfree_parameters + ) + ) + + elif len(active_is_multidimensional) > 0: + warnings.warn( + "The model contains active components that are not active " + "for all navigation indices, which is not supported " + "when fitting the dataset in a vectorized " + "fashion. Fitting proceeds by iterating over the " + "navigation dimensions, which is significantly slower.\n" + "These components are:\n\t" + + "\n\t".join(str(c) for c in active_is_multidimensional) + ) + elif convolved: + warnings.warn( + "Using convolution is not supported when fitting the " + "dataset in a vectorized fashion. Fitting proceeds by " + "iterating over the navigation dimensions, which is " + "significantly slower." + ) + elif isinstance(self.signal.get_noise_variance(), BaseSignal): + warnings.warn( + "The noise of the signal is not homoscedastic, i.e. the " + "variance of the signal is not constant, which is not " + "supported when fitting the dataset in a vectorized " + "fashion. Fitting proceeds by iterating over the " + "navigation dimensions, which is significantly slower." + ) + else: + # We can fit the whole dataset: + # 1. do the fit + # 2. set the map values + # 3. leave earlier because we don't need to go iterate over + # the navigation indices + kwargs["only_current"] = False + # Add the 'show_progressbar' only with lazy signal to avoid + # passing it down to 'ridge_regression' + if self.signal._lazy: + kwargs["show_progressbar"] = show_progressbar + self.fit(**kwargs) + + # TODO: check what happen to linear twinned parameter + for i, para in enumerate(self._free_parameters): + para.map["values"] = self.fit_output.x[..., i] + if kwargs.get("calculate_errors", False): + std = self.fit_output.perror[..., i] + else: + std = np.nan + para.map["std"] = std + para.map["is_set"] = True + + # The (non-free) twinned parameters' .map attribute doesn't get + # set during the "all in one go" linear fitting + twinned_parameters = [ + p + for c in self + for p in c.parameters + if p._linear and p._free and p.twin + ] + for para in nonfree_parameters + twinned_parameters: + para.map["values"] = para.value + para.map["std"] = para.std + para.map["is_set"] = True + + # _binned attribute is re-set to None before early return so the + # behaviour of future fit() calls is not altered. In future + # implementation, a more elegant implementation could be found + self._binned = None + return + # Fitting in a vectorized fashion is not supported. We iterate over the + # navigation indices and fit the dataset one by one. i = 0 with self.axes_manager.events.indices_changed.suppress_callback( self.fetch_stored_values ): - if interactive_plot: - outer = dummy_context_manager - inner = self.suspend_update - else: - outer = self.suspend_update - inner = dummy_context_manager - - with outer(update_on_resume=True): - with progressbar( - total=maxval, disable=not show_progressbar, leave=True - ) as pbar: - for index in self.axes_manager: - with inner(update_on_resume=True): - if mask is None or not mask[index[::-1]]: - # first check if model has set initial values in - # parameters.map['values'][indices], - # otherwise use values from previous fit - self.fetch_stored_values(only_fixed=fetch_only_fixed) - self.fit(**kwargs) - i += 1 - pbar.update(1) - - if autosave and i % autosave_every == 0: - self.save_parameters2file(autosave_fn) - # Trigger the indices_changed event to update to current indices, - # since the callback was suppressed - self.axes_manager.events.indices_changed.trigger(self.axes_manager) + with self.axes_manager.switch_iterpath(iterpath): + if interactive_plot: + outer = dummy_context_manager + inner = self.suspend_update + else: + outer = self.suspend_update + inner = dummy_context_manager + + with outer(update_on_resume=True): + with progressbar( + total=maxval, disable=not show_progressbar, leave=True + ) as pbar: + for index in self.axes_manager: + with inner(update_on_resume=True): + if mask is None or not mask[index[::-1]]: + # first check if model has set initial values in + # parameters.map['values'][indices], + # otherwise use values from previous fit + self.fetch_stored_values( + only_fixed=fetch_only_fixed + ) + self.fit(**kwargs) + i += 1 + pbar.update(1) + + if autosave and i % autosave_every == 0: + self.save_parameters2file(autosave_fn) + # Trigger the indices_changed event to update to current indices, + # since the callback was suppressed + self.axes_manager.events.indices_changed.trigger(self.axes_manager) if autosave is True: _logger.info(f"Deleting temporary file: {autosave_fn}.npz") os.remove(autosave_fn + ".npz") - multifit.__doc__ %= (SHOW_PROGRESSBAR_ARG) + # _binned attribute is re-set to None so the behaviour of future fit() calls + # is not altered. In future implementation, a more elegant implementation + # could be found + self._binned = None + + multifit.__doc__ %= SHOW_PROGRESSBAR_ARG def save_parameters2file(self, filename): """Save the parameters array in binary format. @@ -1625,6 +2183,7 @@ def save_parameters2file(self, filename): Parameters ---------- filename : str + The file name of the file it is saved to. See Also -------- @@ -1633,8 +2192,9 @@ def save_parameters2file(self, filename): Notes ----- This method can be used to save the current state of the model in a way - that can be loaded back to recreate the it using `load_parameters_from - file`. Actually, as of HyperSpy 0.8 this is the only way to do so. + that can be loaded back to recreate it using + :meth:`~hyperspy.model.BaseModel.load_parameters_from_file`. + Actually, as of HyperSpy 0.8 this is the only way to do so. However, this is known to be brittle. For example see https://github.com/hyperspy/hyperspy/issues/341. @@ -1642,20 +2202,21 @@ def save_parameters2file(self, filename): kwds = {} i = 0 for component in self: - cname = component.name.lower().replace(' ', '_') + cname = component.name.lower().replace(" ", "_") for param in component.parameters: - pname = param.name.lower().replace(' ', '_') - kwds['%s_%s.%s' % (i, cname, pname)] = param.map + pname = param.name.lower().replace(" ", "_") + kwds["%s_%s.%s" % (i, cname, pname)] = param.map i += 1 np.savez(filename, **kwds) def load_parameters_from_file(self, filename): - """Loads the parameters array from a binary file written with the - 'save_parameters2file' function. + """Loads the parameters array from a binary file written with the + :meth:`~hyperspy.model.BaseModel.save_parameters2file` function. Parameters - --------- + ---------- filename : str + The file name of the file to load it from. See Also -------- @@ -1663,19 +2224,19 @@ def load_parameters_from_file(self, filename): Notes ----- - In combination with `save_parameters2file`, this method can be used to - recreate a model stored in a file. Actually, before HyperSpy 0.8 this - is the only way to do so. However, this is known to be brittle. For - example see https://github.com/hyperspy/hyperspy/issues/341. + In combination with :meth:`~hyperspy.model.BaseModel.save_parameters2file`, + this method can be used to recreate a model stored in a file. Actually, + before HyperSpy 0.8 this is the only way to do so. However, this is known + to be brittle. For example see https://github.com/hyperspy/hyperspy/issues/341. """ f = np.load(filename) i = 0 for component in self: # Cut the parameters list - cname = component.name.lower().replace(' ', '_') + cname = component.name.lower().replace(" ", "_") for param in component.parameters: - pname = param.name.lower().replace(' ', '_') - param.map = f['%s_%s.%s' % (i, cname, pname)] + pname = param.name.lower().replace(" ", "_") + param.map = f["%s_%s.%s" % (i, cname, pname)] i += 1 self.fetch_stored_values() @@ -1685,19 +2246,17 @@ def assign_current_values_to_all(self, components_list=None, mask=None): Parameters ---------- - component_list : list of components, optional + component_list : list of :class:`~hyperspy.component.Component`, optional If a list of components is given, the operation will be performed only in the value of the parameters of the given components. The components can be specified by name, index or themselves. - mask : boolean numpy array or None, optional + If ``None`` (default), the active components will be considered. + mask : numpy.ndarray of bool or None, optional The operation won't be performed where mask is True. """ if components_list is None: - components_list = [] - for comp in self: - if comp.active: - components_list.append(comp) + components_list = self.active_components else: components_list = [self._get_component(x) for x in components_list] @@ -1706,8 +2265,7 @@ def assign_current_values_to_all(self, components_list=None, mask=None): parameter.assign_current_value_to_all(mask=mask) def _enable_ext_bounding(self, components=None): - """ - """ + """ """ if components is None: components = self for component in components: @@ -1715,16 +2273,21 @@ def _enable_ext_bounding(self, components=None): parameter.ext_bounded = True def _disable_ext_bounding(self, components=None): - """ - """ + """ """ if components is None: components = self for component in components: for parameter in component.parameters: parameter.ext_bounded = False - def export_results(self, folder=None, format="hspy", save_std=False, - only_free=True, only_active=True): + def export_results( + self, + folder=None, + format="hspy", + save_std=False, + only_free=True, + only_active=True, + ): """Export the results of the parameters of the model to the desired folder. @@ -1735,7 +2298,7 @@ def export_results(self, folder=None, format="hspy", save_std=False, current folder is used by default. format : str The extension of the file format. It must be one of the - fileformats supported by HyperSpy. The default is "hspy". + fileformats supported by HyperSpy. The default is ``"hspy"``. save_std : bool If True, also the standard deviation will be saved. only_free : bool @@ -1751,17 +2314,17 @@ def export_results(self, folder=None, format="hspy", save_std=False, the file names modify the name attributes. """ - for component in self: - if only_active is False or component.active: - component.export(folder=folder, format=format, - save_std=save_std, only_free=only_free) + component_list = self.active_components if only_active else self + for component in component_list: + component.export( + folder=folder, format=format, save_std=save_std, only_free=only_free + ) def plot_results(self, only_free=True, only_active=True): """Plot the value of the parameters of the model Parameters ---------- - only_free : bool If True, only the value of the parameters that are free will be plotted. @@ -1775,12 +2338,13 @@ def plot_results(self, only_free=True, only_active=True): the file names modify the name attributes. """ - for component in self: - if only_active is False or component.active: - component.plot(only_free=only_free) + component_list = self.active_components if only_active else self + for component in component_list: + component.plot(only_free=only_free) - def print_current_values(self, only_free=False, only_active=False, - component_list=None, fancy=True): + def print_current_values( + self, only_free=False, only_active=False, component_list=None + ): """Prints the current values of the parameters of all components. Parameters @@ -1790,47 +2354,59 @@ def print_current_values(self, only_free=False, only_active=False, only parameters which are free will be printed. only_active : bool If True, only values of active components will be printed - component_list : None or list of components. + component_list : None or list of :class:`~hyperspy.component.Component` If None, print all components. - fancy : bool - If True, attempts to print using html rather than text in the notebook. """ - if fancy: - display(current_model_values( - model=self, only_free=only_free, only_active=only_active, - component_list=component_list)) - else: - display_pretty(current_model_values( - model=self, only_free=only_free, only_active=only_active, - component_list=component_list)) + display( + CurrentModelValues( + model=self, + only_free=only_free, + only_active=only_active, + component_list=component_list, + ) + ) - def set_parameters_not_free(self, component_list=None, - parameter_name_list=None): + def set_parameters_not_free( + self, + component_list=None, + parameter_name_list=None, + only_linear=False, + only_nonlinear=False, + ): """ Sets the parameters in a component in a model to not free. Parameters ---------- - component_list : None, or list of hyperspy components, optional + component_list : None or list of :class:`~hyperspy.component.Component`, optional If None, will apply the function to all components in the model. If list of components, will apply the functions to the components in the list. The components can be specified by name, index or themselves. - parameter_name_list : None or list of strings, optional + parameter_name_list : None or list of str, optional If None, will set all the parameters to not free. If list of strings, will set all the parameters with the same name as the strings in parameter_name_list to not free. + only_linear : bool + If True, will only set parameters that are linear to free. + only_nonlinear : bool + If True, will only set parameters that are nonlinear to free. Examples -------- + >>> s = hs.signals.Signal1D(np.random.random((10,100))) + >>> m = s.create_model() >>> v1 = hs.model.components1D.Voigt() >>> m.append(v1) >>> m.set_parameters_not_free() - >>> m.set_parameters_not_free(component_list=[v1], - parameter_name_list=['area','centre']) + >>> m.set_parameters_not_free( + ... component_list=[v1], parameter_name_list=['area','centre'] + ... ) + >>> m.set_parameters_not_free(only_linear=True) - See also + + See Also -------- set_parameters_free hyperspy.component.Component.set_parameters_free @@ -1845,35 +2421,52 @@ def set_parameters_not_free(self, component_list=None, component_list = [self._get_component(x) for x in component_list] for _component in component_list: - _component.set_parameters_not_free(parameter_name_list) + _component.set_parameters_not_free( + parameter_name_list, + only_linear=only_linear, + only_nonlinear=only_nonlinear, + ) - def set_parameters_free(self, component_list=None, - parameter_name_list=None): + def set_parameters_free( + self, + component_list=None, + parameter_name_list=None, + only_linear=False, + only_nonlinear=False, + ): """ Sets the parameters in a component in a model to free. Parameters ---------- - component_list : None, or list of hyperspy components, optional + component_list : None or list of :class:`~hyperspy.component.Component`, optional If None, will apply the function to all components in the model. If list of components, will apply the functions to the components in the list. The components can be specified by name, index or themselves. - - parameter_name_list : None or list of strings, optional + parameter_name_list : None or list of str, optional If None, will set all the parameters to not free. If list of strings, will set all the parameters with the same name as the strings in parameter_name_list to not free. + only_linear : bool + If True, will only set parameters that are linear to not free. + only_nonlinear : bool + If True, will only set parameters that are nonlinear to not free. Examples -------- + >>> s = hs.signals.Signal1D(np.random.random((10,100))) + >>> m = s.create_model() >>> v1 = hs.model.components1D.Voigt() >>> m.append(v1) + >>> m.set_parameters_free() - >>> m.set_parameters_free(component_list=[v1], - parameter_name_list=['area','centre']) + >>> m.set_parameters_free( + ... component_list=[v1], parameter_name_list=['area','centre'] + ... ) + >>> m.set_parameters_free(only_linear=True) - See also + See Also -------- set_parameters_not_free hyperspy.component.Component.set_parameters_free @@ -1888,27 +2481,29 @@ def set_parameters_free(self, component_list=None, component_list = [self._get_component(x) for x in component_list] for _component in component_list: - _component.set_parameters_free(parameter_name_list) + _component.set_parameters_free( + parameter_name_list, + only_linear=only_linear, + only_nonlinear=only_nonlinear, + ) def set_parameters_value( - self, - parameter_name, - value, - component_list=None, - only_current=False): + self, parameter_name, value, component_list=None, only_current=False + ): """ Sets the value of a parameter in components in a model to a specified value Parameters ---------- - parameter_name : string + parameter_name : str Name of the parameter whose value will be changed - value : number + value : float or int The new value of the parameter - component_list : list of hyperspy components, optional + component_list : None or list of :class:`~hyperspy.component.Component`, optional A list of components whose parameters will changed. The components - can be specified by name, index or themselves. + can be specified by name, index or themselves. If None, use all + components of the model. only_current : bool, default False If True, will only change the parameter value at the current position in the model. @@ -1916,13 +2511,17 @@ def set_parameters_value( Examples -------- + >>> s = hs.signals.Signal1D(np.random.random((10,100))) + >>> m = s.create_model() >>> v1 = hs.model.components1D.Voigt() >>> v2 = hs.model.components1D.Voigt() >>> m.extend([v1,v2]) + >>> m.set_parameters_value('area', 5) >>> m.set_parameters_value('area', 5, component_list=[v1]) - >>> m.set_parameters_value('area', 5, component_list=[v1], - only_current=True) + >>> m.set_parameters_value( + ... 'area', 5, component_list=[v1], only_current=True + ... ) """ @@ -1949,7 +2548,7 @@ def as_dictionary(self, fullcopy=True): Parameters ---------- - fullcopy : bool (optional, True) + fullcopy : bool, optional True Copies of objects are stored, not references. If any found, functions will be pickled and signals converted to dictionaries @@ -1962,22 +2561,22 @@ def as_dictionary(self, fullcopy=True): component * _whitelist: a dictionary with keys used as references for saved attributes, for more information, see - :py:func:`~hyperspy.misc.export_dictionary.export_to_dictionary` + :func:`~hyperspy.misc.export_dictionary.export_to_dictionary` * any field from _whitelist.keys() Examples -------- - >>> s = signals.Signal1D(np.random.random((10,100))) + >>> s = hs.signals.Signal1D(np.random.random((10,100))) >>> m = s.create_model() - >>> l1 = components1d.Lorentzian() - >>> l2 = components1d.Lorentzian() + >>> l1 = hs.model.components1D.Lorentzian() + >>> l2 = hs.model.components1D.Lorentzian() >>> m.append(l1) >>> m.append(l2) >>> d = m.as_dictionary() >>> m2 = s.create_model(dictionary=d) """ - dic = {'components': [c.as_dictionary(fullcopy) for c in self]} + dic = {"components": [c.as_dictionary(fullcopy) for c in self]} export_to_dictionary(self, self._whitelist, dic, fullcopy) @@ -1989,28 +2588,29 @@ def remove_empty_numpy_strings(dic): for vv in v: if isinstance(vv, dict): remove_empty_numpy_strings(vv) - elif isinstance(vv, np.string_) and len(vv) == 0: - vv = '' - elif isinstance(v, np.string_) and len(v) == 0: + elif isinstance(vv, np.bytes_) and len(vv) == 0: + vv = "" + elif isinstance(v, np.bytes_) and len(v) == 0: del dic[k] - dic[k] = '' + dic[k] = "" + remove_empty_numpy_strings(dic) return dic def set_component_active_value( - self, value, component_list=None, only_current=False): + self, value, component_list=None, only_current=False + ): """ - Sets the component 'active' parameter to a specified value + Sets the component ``'active'`` parameter to a specified value. Parameters ---------- value : bool - The new value of the 'active' parameter - component_list : list of hyperspy components, optional + The new value of the ``'active'`` parameter + component_list : list of :class:`~hyperspy.component.Component`, optional A list of components whose parameters will changed. The components can be specified by name, index or themselves. - only_current : bool, default False If True, will only change the parameter value at the current position in the model. @@ -2018,20 +2618,21 @@ def set_component_active_value( Examples -------- + >>> s = hs.signals.Signal1D(np.random.random((10,100))) + >>> m = s.create_model() >>> v1 = hs.model.components1D.Voigt() >>> v2 = hs.model.components1D.Voigt() >>> m.extend([v1,v2]) + >>> m.set_component_active_value(False) >>> m.set_component_active_value(True, component_list=[v1]) - >>> m.set_component_active_value(False, component_list=[v1], - only_current=True) + >>> m.set_component_active_value( + ... False, component_list=[v1], only_current=True + ... ) """ - - if not component_list: - component_list = [] - for _component in self: - component_list.append(_component) + if component_list is None: + component_list = self else: component_list = [self._get_component(x) for x in component_list] @@ -2039,8 +2640,7 @@ def set_component_active_value( _component.active = value if _component.active_is_multidimensional: if only_current: - _component._active_array[ - self.axes_manager.indices[::-1]] = value + _component._active_array[self.axes_manager.indices[::-1]] = value else: _component._active_array.fill(value) @@ -2060,11 +2660,12 @@ def __getitem__(self, value): else: raise ValueError( "There are several components with " - "the name \"" + str(value) + "\"") + 'the name "' + str(value) + '"' + ) else: raise ValueError( - "Component name \"" + str(value) + - "\" not found in model") + 'Component name "' + str(value) + '" not found in model' + ) else: return list.__getitem__(self, value) @@ -2073,7 +2674,7 @@ def create_samfire(self, workers=None, setup=True, **kwargs): Parameters ---------- - workers : {None, int} + workers : None or int the number of workers to initialise. If zero, all computations will be done serially. If None (default), will attempt to use (number-of-cores - 1), @@ -2084,38 +2685,37 @@ def create_samfire(self, workers=None, setup=True, **kwargs): Any that will be passed to the _setup and in turn SamfirePool. """ from hyperspy.samfire import Samfire - return Samfire(self, workers=workers, - setup=setup, **kwargs) + return Samfire(self, workers=workers, setup=setup, **kwargs) -class ModelSpecialSlicers(object): +class ModelSpecialSlicers(object): def __init__(self, model, isNavigation): self.isNavigation = isNavigation self.model = model def __getitem__(self, slices): - array_slices = self.model.signal._get_array_slices( - slices, - self.isNavigation) + array_slices = self.model.signal._get_array_slices(slices, self.isNavigation) _signal = self.model.signal._slicer(slices, self.isNavigation) # TODO: for next major release, change model creation defaults to not # automate anything. For now we explicitly look for "auto_" kwargs and # disable them: import inspect + pars = inspect.signature(_signal.create_model).parameters - kwargs = {key: False for key in pars.keys() if key.startswith('auto_')} + kwargs = {key: False for key in pars.keys() if key.startswith("auto_")} _model = _signal.create_model(**kwargs) - dims = (self.model.axes_manager.navigation_dimension, - self.model.axes_manager.signal_dimension) + dims = ( + self.model.axes_manager.navigation_dimension, + self.model.axes_manager.signal_dimension, + ) if self.isNavigation: - _model.channel_switches[:] = self.model.channel_switches + _model._channel_switches[:] = self.model._channel_switches else: - _model.channel_switches[:] = \ - np.atleast_1d( - self.model.channel_switches[ - tuple(array_slices[-dims[1]:])]) + _model._channel_switches[:] = np.atleast_1d( + self.model._channel_switches[tuple(array_slices[-dims[1] :])] + ) twin_dict = {} for comp in self.model: @@ -2124,30 +2724,27 @@ def __getitem__(self, slices): if v is None: continue flags_str, value = v - if 'init' in parse_flag_string(flags_str): + if "init" in parse_flag_string(flags_str): init_args[k] = value _model.append(comp.__class__(**init_args)) - copy_slice_from_whitelist(self.model, - _model, - dims, - (slices, array_slices), - self.isNavigation, - ) + copy_slice_from_whitelist( + self.model, + _model, + dims, + (slices, array_slices), + self.isNavigation, + ) for co, cn in zip(self.model, _model): - copy_slice_from_whitelist(co, - cn, - dims, - (slices, array_slices), - self.isNavigation) + copy_slice_from_whitelist( + co, cn, dims, (slices, array_slices), self.isNavigation + ) if _model.axes_manager.navigation_size < 2: if co.active_is_multidimensional: - cn.active = co._active_array[array_slices[:dims[0]]] + cn.active = co._active_array[array_slices[: dims[0]]] for po, pn in zip(co.parameters, cn.parameters): - copy_slice_from_whitelist(po, - pn, - dims, - (slices, array_slices), - self.isNavigation) + copy_slice_from_whitelist( + po, pn, dims, (slices, array_slices), self.isNavigation + ) twin_dict[id(po)] = ([id(i) for i in list(po._twins)], pn) for k in twin_dict.keys(): @@ -2163,4 +2760,5 @@ def __getitem__(self, slices): return _model + # vim: textwidth=80 diff --git a/hyperspy/models/__init__.py b/hyperspy/models/__init__.py index c41a8ae211..3d813a7dda 100644 --- a/hyperspy/models/__init__.py +++ b/hyperspy/models/__init__.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . diff --git a/hyperspy/models/edsmodel.py b/hyperspy/models/edsmodel.py deleted file mode 100644 index 97e1d671a8..0000000000 --- a/hyperspy/models/edsmodel.py +++ /dev/null @@ -1,931 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from __future__ import division - -import warnings -import numpy as np -import math -import logging - -from hyperspy.misc.utils import stash_active_state -from hyperspy.misc.eds.utils import _get_element_and_line - -from hyperspy.models.model1d import Model1D -from hyperspy._signals.eds import EDSSpectrum -from hyperspy.misc.elements import elements as elements_db -from hyperspy.misc.eds import utils as utils_eds -import hyperspy.components1d as create_component -from hyperspy.misc.test_utils import ignore_warning - - -_logger = logging.getLogger(__name__) - -eV2keV = 1000. -sigma2fwhm = 2 * math.sqrt(2 * math.log(2)) - - -def _get_weight(element, line, weight_line=None): - if weight_line is None: - weight_line = elements_db[ - element]['Atomic_properties']['Xray_lines'][line]['weight'] - return "x * {}".format(weight_line) - - -def _get_sigma(E, E_ref, units_factor, return_f=False): - """ - Calculates an approximate sigma value, accounting for peak broadening due - to the detector, for a peak at energy E given a known width at a reference - energy. - - The factor 2.5 is a constant derived by Fiori & Newbury as references - below. - - Parameters - ---------- - energy_resolution_MnKa : float - Energy resolution of Mn Ka in eV - E : float - Energy of the peak in keV - - Returns - ------- - float : FWHM of the peak in keV - - Notes - ----- - This method implements the equation derived by Fiori and Newbury as is - documented in the following: - - Fiori, C. E., and Newbury, D. E. (1978). In SEM/1978/I, SEM, Inc., - AFM O'Hare, Illinois, p. 401. - - Goldstein et al. (2003). "Scanning Electron Microscopy & X-ray - Microanalysis", Plenum, third edition, p 315. - """ - energy2sigma_factor = 2.5 / (eV2keV * (sigma2fwhm**2)) - if return_f: - return lambda sig_ref: math.sqrt(abs( - energy2sigma_factor * (E - E_ref) * units_factor + - np.power(sig_ref, 2))) - else: - return "sqrt(abs({} * ({} - {}) * {} + sig_ref ** 2))".format( - energy2sigma_factor, E, E_ref, units_factor) - - -def _get_offset(diff): - return "x + {}".format(diff) - - -def _get_scale(E1, E_ref1, fact): - return "{} + {} * (x - {})".format(E1, fact, E_ref1) - - -class EDSModel(Model1D): - - """Build and fit a model of an EDS Signal1D. - - Parameters - ---------- - spectrum : EDSSpectrum (or any EDSSpectrum subclass) instance. - - auto_add_lines : bool - If True, automatically add Gaussians for all X-rays generated - in the energy range by an element, using the edsmodel.add_family_lines - method. - auto_background : bool - If True, adds automatically a polynomial order 6 to the model, - using the edsmodel.add_polynomial_background method. - - Any extra arguments are passed to the Model creator. - - Example - ------- - >>> m = s.create_model() - >>> m.fit() - >>> m.fit_background() - >>> m.calibrate_energy_axis('resolution') - >>> m.calibrate_xray_lines('energy', ['Au_Ma']) - >>> m.calibrate_xray_lines('sub_weight',['Mn_La'], bound=10) - """ - - def __init__(self, spectrum, - auto_background=True, - auto_add_lines=True, - *args, **kwargs): - Model1D.__init__(self, spectrum, *args, **kwargs) - self.xray_lines = list() - self.family_lines = list() - end_energy = self.axes_manager.signal_axes[0].high_value - self.end_energy = min(end_energy, self.signal._get_beam_energy()) - self.start_energy = self.axes_manager.signal_axes[0].low_value - self.background_components = list() - if 'dictionary' in kwargs or len(args) > 1: - auto_add_lines = False - auto_background = False - d = args[1] if len(args) > 1 else kwargs['dictionary'] - if len(d['xray_lines']) > 0: - self.xray_lines.extend( - [self[name] for name in d['xray_lines']]) - if len(d['background_components']) > 0: - self.background_components.extend( - [self[name] for name in d['background_components']]) - if auto_background is True: - self.add_polynomial_background() - if auto_add_lines is True: - # Will raise an error if no elements are specified, so check: - if 'Sample.elements' in self.signal.metadata: - self.add_family_lines() - - def as_dictionary(self, fullcopy=True): - dic = super(EDSModel, self).as_dictionary(fullcopy) - dic['xray_lines'] = [c.name for c in self.xray_lines] - dic['background_components'] = [c.name for c in - self.background_components] - return dic - - @property - def units_factor(self): - units_name = self.axes_manager.signal_axes[0].units - if units_name == 'eV': - return 1000. - elif units_name == 'keV': - return 1. - else: - raise ValueError("Energy units, %s, not supported" % - str(units_name)) - - @property - def spectrum(self): - return self._signal - - @spectrum.setter - def spectrum(self, value): - if isinstance(value, EDSSpectrum): - self._signal = value - else: - raise ValueError( - "This attribute can only contain an EDSSpectrum " - "but an object of type %s was provided" % - str(type(value))) - - @property - def _active_xray_lines(self): - return [xray_line for xray_line - in self.xray_lines if xray_line.active] - - def add_family_lines(self, xray_lines='from_elements'): - """Create the Xray-lines instances and configure them appropiately - - If a X-ray line is given, all the the lines of the familiy is added. - For instance if Zn Ka is given, Zn Kb is added too. The main lines - (alpha) is added to self.xray_lines - - Parameters - ----------- - xray_lines: {None, 'from_elements', list of string} - If None, if `metadata` contains `xray_lines` list of lines use - those. If 'from_elements', add all lines from the elements contains - in `metadata`. Alternatively, provide an iterable containing - a list of valid X-ray lines symbols. (eg. ('Al_Ka','Zn_Ka')). - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - """ - # Test that signal axis is uniform - if not self.axes_manager[-1].is_uniform: - raise NotImplementedError("This function is not yet implemented " - "for non-uniform axes.") - - only_one = False - only_lines = ("Ka", "La", "Ma") - - if xray_lines is None or xray_lines == 'from_elements': - if 'Sample.xray_lines' in self.signal.metadata \ - and xray_lines != 'from_elements': - xray_lines = self.signal.metadata.Sample.xray_lines - elif 'Sample.elements' in self.signal.metadata: - xray_lines = self.signal._get_lines_from_elements( - self.signal.metadata.Sample.elements, - only_one=only_one, - only_lines=only_lines) - else: - raise ValueError( - "No elements defined, set them with `add_elements`") - - components_names = [xr.name for xr in self.xray_lines] - xray_lines = filter(lambda x: x not in components_names, xray_lines) - xray_lines, xray_not_here = self.signal.\ - _get_xray_lines_in_spectral_range(xray_lines) - for xray in xray_not_here: - warnings.warn("%s is not in the data energy range." % (xray)) - - for xray_line in xray_lines: - element, line = utils_eds._get_element_and_line(xray_line) - line_energy, line_FWHM = self.signal._get_line_energy( - xray_line, - FWHM_MnKa='auto') - component = create_component.Gaussian() - component.centre.value = line_energy - component.fwhm = line_FWHM - component.centre.free = False - component.sigma.free = False - component.name = xray_line - self.append(component) - self.xray_lines.append(component) - self[xray_line].A.map[ - 'values'] = self.signal.isig[line_energy].data * \ - line_FWHM / self.signal.axes_manager[-1].scale - self[xray_line].A.map['is_set'] = ( - np.ones(self.signal.isig[line_energy].data.shape) == 1) - component.A.ext_force_positive = True - for li in elements_db[element]['Atomic_properties']['Xray_lines']: - if line[0] in li and line != li: - xray_sub = element + '_' + li - if self.signal.\ - _get_xray_lines_in_spectral_range( - [xray_sub])[0] != []: - line_energy, line_FWHM = self.signal.\ - _get_line_energy( - xray_sub, FWHM_MnKa='auto') - component_sub = create_component.Gaussian() - component_sub.centre.value = line_energy - component_sub.fwhm = line_FWHM - component_sub.centre.free = False - component_sub.sigma.free = False - component_sub.name = xray_sub - component_sub.A.twin_function_expr = _get_weight( - element, li) - component_sub.A.twin = component.A - self.append(component_sub) - self.family_lines.append(component_sub) - self.fetch_stored_values() - - @property - def _active_background_components(self): - return [bc for bc in self.background_components - if bc.free_parameters] - - def add_polynomial_background(self, order=6): - """ - Add a polynomial background. - - the background is added to self.background_components - - Parameters - ---------- - order: int - The order of the polynomial - """ - with ignore_warning(message="The API of the `Polynomial` component"): - background = create_component.Polynomial(order=order, legacy=False) - background.name = 'background_order_' + str(order) - background.isbackground = True - self.append(background) - self.background_components.append(background) - - def free_background(self): - """ - Free the yscale of the background components. - """ - for component in self.background_components: - component.set_parameters_free() - - def fix_background(self): - """ - Fix the background components. - """ - for component in self._active_background_components: - component.set_parameters_not_free() - - def enable_xray_lines(self): - """Enable the X-ray lines components. - - """ - for component in self.xray_lines: - component.active = True - - def disable_xray_lines(self): - """Disable the X-ray lines components. - - """ - for component in self._active_xray_lines: - component.active = False - - def _make_position_adjuster(self, component, fix_it, show_label): - # Override to ensure formatting of labels of xray lines - super(EDSModel, self)._make_position_adjuster( - component, fix_it, show_label) - if show_label and component in (self.xray_lines + self.family_lines): - label = self._position_widgets[component._position][1] - label.string = (r"$\mathrm{%s}_{\mathrm{%s}}$" % - _get_element_and_line(component.name)) - - def fit_background(self, - start_energy=None, - end_energy=None, - windows_sigma=(4., 3.), - kind='single', - **kwargs): - """ - Fit the background in the energy range containing no X-ray line. - - After the fit, the background is fixed. - - Parameters - ---------- - start_energy : {float, None} - If float, limit the range of energies from the left to the - given value. - end_energy : {float, None} - If float, limit the range of energies from the right to the - given value. - windows_sigma: tuple of two float - The (lower, upper) bounds around each X-ray line, each as a float, - to define the energy range free of X-ray lines. - kind : {'single', 'multi'} - If 'single' fit only the current location. If 'multi' - use multifit. - **kwargs : extra key word arguments - All extra key word arguments are passed to fit or multifit - - See also - -------- - free_background - """ - - if end_energy is None: - end_energy = self.end_energy - if start_energy is None: - start_energy = self.start_energy - - # disactivate line - self.free_background() - with stash_active_state(self): - self.disable_xray_lines() - self.set_signal_range(start_energy, end_energy) - for component in self: - if component.isbackground is False: - self.remove_signal_range( - component.centre.value - - windows_sigma[0] * component.sigma.value, - component.centre.value + - windows_sigma[1] * component.sigma.value) - if kind == 'single': - self.fit(**kwargs) - if kind == 'multi': - self.multifit(**kwargs) - self.reset_signal_range() - self.fix_background() - - def _twin_xray_lines_width(self, xray_lines): - """ - Twin the width of the peaks - - The twinning models the energy resolution of the detector - - Parameters - ---------- - xray_lines: list of str or 'all_alpha' - The Xray lines. If 'all_alpha', fit all using all alpha lines - """ - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - - for i, xray_line in enumerate(xray_lines): - component = self[xray_line] - if i == 0: - component_ref = component - component_ref.sigma.free = True - E_ref = component_ref.centre.value - else: - component.sigma.free = True - E = component.centre.value - component.sigma.twin_inverse_function_expr = _get_sigma( - E_ref, E, self.units_factor) - component.sigma.twin_function_expr = _get_sigma( - E, E_ref, self.units_factor) - - def _set_energy_resolution(self, xray_lines, *args, **kwargs): - """ - Adjust the width of all lines and set the fitted energy resolution - to the spectrum - - Parameters - ---------- - xray_lines: list of str or 'all_alpha' - The Xray lines. If 'all_alpha', fit all using all alpha lines - """ - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - energy_Mn_Ka, FWHM_MnKa_old = self.signal._get_line_energy('Mn_Ka', - 'auto') - FWHM_MnKa_old *= eV2keV / self.units_factor - get_sigma_Mn_Ka = _get_sigma( - energy_Mn_Ka, self[xray_lines[0]].centre.value, self.units_factor, - return_f=True) - FWHM_MnKa = get_sigma_Mn_Ka(self[xray_lines[0]].sigma.value - ) * eV2keV / self.units_factor * sigma2fwhm - if FWHM_MnKa < 110: - raise ValueError("FWHM_MnKa of " + str(FWHM_MnKa) + - " smaller than" + "physically possible") - else: - self.signal.set_microscope_parameters( - energy_resolution_MnKa=FWHM_MnKa) - _logger.info("Energy resolution (FWHM at Mn Ka) changed from " + - "{:.2f} to {:.2f} eV".format( - FWHM_MnKa_old, FWHM_MnKa)) - for component in self: - if component.isbackground is False: - line_FWHM = self.signal._get_line_energy( - component.name, FWHM_MnKa='auto')[1] - component.fwhm = line_FWHM - - def _twin_xray_lines_scale(self, xray_lines): - """ - Twin the scale of the peaks - - Parameters - ---------- - xray_lines: list of str or 'all_alpha' - The Xray lines. If 'all_alpha', fit all using all alpha lines - """ - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - ax = self.signal.axes_manager[-1] - ref = [] - for i, xray_line in enumerate(xray_lines): - component = self[xray_line] - if i == 0: - component_ref = component - component_ref.centre.free = True - E_ref = component_ref.centre.value - ref.append(E_ref) - else: - component.centre.free = True - E = component.centre.value - fact = float(ax.value2index(E)) / ax.value2index(E_ref) - component.centre.twin_function_expr = _get_scale( - E, E_ref, fact) - component.centre.twin = component_ref.centre - ref.append(E) - return ref - - def _set_energy_scale(self, xray_lines, ref): - """ - Adjust the width of all lines and set the fitted energy resolution - to the spectrum - - Parameters - ---------- - xray_lines: list of str or 'all_alpha' - The X-ray lines. If 'all_alpha', fit all using all alpha lines - ref: list of float - The centres, before fitting, of the X-ray lines included - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - """ - # Test that signal axis is uniform - if not self.axes_manager[-1].is_uniform: - raise NotImplementedError("This function is not yet implemented " - "for non-uniform axes.") - - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - ax = self.signal.axes_manager[-1] - scale_old = self.signal.axes_manager[-1].scale - ind = np.argsort(np.array( - [compo.centre.value for compo in self.xray_lines]))[-1] - E = self[xray_lines[ind]].centre.value - scale = (ref[ind] - ax.offset) / ax.value2index(E) - ax.scale = scale - for i, xray_line in enumerate(xray_lines): - component = self[xray_line] - component.centre.value = ref[i] - _logger.info("Scale changed from %lf to %lf", scale_old, scale) - - def _twin_xray_lines_offset(self, xray_lines): - """ - Twin the offset of the peaks - - Parameters - ---------- - xray_lines: list of str or 'all_alpha' - The Xray lines. If 'all_alpha', fit all using all alpha lines - """ - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - ref = [] - for i, xray_line in enumerate(xray_lines): - component = self[xray_line] - if i == 0: - component_ref = component - component_ref.centre.free = True - E_ref = component_ref.centre.value - ref.append(E_ref) - else: - component.centre.free = True - E = component.centre.value - diff = E_ref - E - component.centre.twin_function_expr = _get_offset(-diff) - component.centre.twin = component_ref.centre - ref.append(E) - return ref - - def _set_energy_offset(self, xray_lines, ref): - """ - Adjust the width of all lines and set the fitted energy resolution - to the spectrum - - Parameters - ---------- - xray_lines: list of str or 'all_alpha' - The Xray lines. If 'all_alpha', fit all using all alpha lines - ref: list of float - The centres, before fitting, of the X-ray lines included - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - """ - # Test that signal axis is uniform - if not self.axes_manager[-1].is_uniform: - raise NotImplementedError("This function is not yet implemented " - "for non-uniform axes.") - - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - diff = self[xray_lines[0]].centre.value - ref[0] - offset_old = self.signal.axes_manager[-1].offset - self.signal.axes_manager[-1].offset -= diff - offset = self.signal.axes_manager[-1].offset - _logger.info("Offset changed from %lf to %lf", offset_old, offset) - for i, xray_line in enumerate(xray_lines): - component = self[xray_line] - component.centre.value = ref[i] - - def calibrate_energy_axis(self, - calibrate='resolution', - xray_lines='all_alpha', - **kwargs): - """ - Calibrate the resolution, the scale or the offset of the energy axis - by fitting. - - Parameters - ---------- - calibrate: 'resolution' or 'scale' or 'offset' - If 'resolution', fits the width of Gaussians place at all x-ray - lines. The width is given by a model of the detector resolution, - obtained by extrapolating the `energy_resolution_MnKa` in `metadata` - `metadata`. - This method will update the value of `energy_resolution_MnKa`. - If 'scale', calibrate the scale of the energy axis - If 'offset', calibrate the offset of the energy axis - xray_lines: list of str or 'all_alpha' - The Xray lines. If 'all_alpha', fit all using all alpha lines - **kwargs : extra key word arguments - All extra key word arguments are passed to fit or - multifit, depending on the value of kind. - - """ - - if calibrate == 'resolution': - free = self._twin_xray_lines_width - fix = self.fix_xray_lines_width - scale = self._set_energy_resolution - elif calibrate == 'scale': - free = self._twin_xray_lines_scale - fix = self.fix_xray_lines_energy - scale = self._set_energy_scale - elif calibrate == 'offset': - free = self._twin_xray_lines_offset - fix = self.fix_xray_lines_energy - scale = self._set_energy_offset - ref = free(xray_lines=xray_lines) - self.fit(**kwargs) - fix(xray_lines=xray_lines) - scale(xray_lines=xray_lines, ref=ref) - self.update_plot() - - def free_sub_xray_lines_weight(self, xray_lines='all', bound=0.01): - """ - Free the weight of a sub X-ray lines - - Remove the twin on the height of sub-Xray lines (non alpha) - - Parameters - ---------- - xray_lines: list of str or 'all' - The Xray lines. If 'all', fit all lines - bounds: float - Bound the height of the peak to a fraction of - its height - """ - - def free_twin(component): - component.A.twin = None - component.A.free = True - if component.A.value - bound * component.A.value <= 0: - component.A.bmin = 1e-10 - else: - component.A.bmin = component.A.value - \ - bound * component.A.value - component.A.bmax = component.A.value + \ - bound * component.A.value - component.A.ext_force_positive = True - xray_families = [ - utils_eds._get_xray_lines_family(line) for line in xray_lines] - for component in self: - if component.isbackground is False: - if xray_lines == 'all': - free_twin(component) - elif utils_eds._get_xray_lines_family( - component.name) in xray_families: - free_twin(component) - - def fix_sub_xray_lines_weight(self, xray_lines='all'): - """ - Fix the weight of a sub X-ray lines to the main X-ray lines - - Establish the twin on the height of sub-Xray lines (non alpha) - """ - - def fix_twin(component): - component.A.bmin = 0.0 - component.A.bmax = None - element, line = utils_eds._get_element_and_line(component.name) - for li in elements_db[element]['Atomic_properties']['Xray_lines']: - if line[0] in li and line != li: - xray_sub = element + '_' + li - if xray_sub in self: - component_sub = self[xray_sub] - component_sub.A.bmin = 1e-10 - component_sub.A.bmax = None - weight_line = component_sub.A.value / component.A.value - component_sub.A.twin_function_expr = _get_weight( - element, li, weight_line) - component_sub.A.twin = component.A - else: - warnings.warn("The X-ray line expected to be in the " - "model was not found") - for component in self.xray_lines: - if xray_lines == 'all' or component.name in xray_lines: - fix_twin(component) - self.fetch_stored_values() - - def free_xray_lines_energy(self, xray_lines='all', bound=0.001): - """ - Free the X-ray line energy (shift or centre of the Gaussian) - - Parameters - ---------- - xray_lines: list of str or 'all' - The Xray lines. If 'all', fit all lines - bound: float - the bound around the actual energy, in keV or eV - """ - - for component in self: - if component.isbackground is False: - if xray_lines == 'all' or component.name in xray_lines: - component.centre.free = True - component.centre.bmin = component.centre.value - bound - component.centre.bmax = component.centre.value + bound - - def fix_xray_lines_energy(self, xray_lines='all'): - """ - Fix the X-ray line energy (shift or centre of the Gaussian) - - Parameters - ---------- - xray_lines: list of str, 'all', or 'all_alpha' - The Xray lines. If 'all', fit all lines. If 'all_alpha' fit all - using all alpha lines. - bound: float - the bound around the actual energy, in keV or eV - """ - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - for component in self: - if component.isbackground is False: - if xray_lines == 'all' or component.name in xray_lines: - component.centre.twin = None - component.centre.free = False - component.centre.bmin = None - component.centre.bmax = None - - def free_xray_lines_width(self, xray_lines='all', bound=0.01): - """ - Free the X-ray line width (sigma of the Gaussian) - - Parameters - ---------- - xray_lines: list of str or 'all' - The Xray lines. If 'all', fit all lines - bound: float - the bound around the actual energy, in keV or eV - """ - - for component in self: - if component.isbackground is False: - if xray_lines == 'all' or component.name in xray_lines: - component.sigma.free = True - component.sigma.bmin = component.sigma.value - bound - component.sigma.bmax = component.sigma.value + bound - - def fix_xray_lines_width(self, xray_lines='all'): - """ - Fix the X-ray line width (sigma of the Gaussian) - - Parameters - ---------- - xray_lines: list of str, 'all', or 'all_alpha' - The Xray lines. If 'all', fit all lines. If 'all_alpha' fit all - using all alpha lines. - bound: float - the bound around the actual energy, in keV or eV - """ - if xray_lines == 'all_alpha': - xray_lines = [compo.name for compo in self.xray_lines] - for component in self: - if component.isbackground is False: - if xray_lines == 'all' or component.name in xray_lines: - component.sigma.twin = None - component.sigma.free = False - component.sigma.bmin = None - component.sigma.bmax = None - - def calibrate_xray_lines(self, - calibrate='energy', - xray_lines='all', - bound=1, - kind='single', - **kwargs): - """ - Calibrate individually the X-ray line parameters. - - The X-ray line energy, the weight of the sub-lines and the X-ray line - width can be calibrated. - - Parameters - ---------- - calibrate: 'energy' or 'sub_weight' or 'width' - If 'energy', calibrate the X-ray line energy. - If 'sub_weight', calibrate the ratio between the main line - alpha and the other sub-lines of the family - If 'width', calibrate the X-ray line width. - xray_lines: list of str or 'all' - The Xray lines. If 'all', fit all lines - bounds: float - for 'energy' and 'width' the bound in energy, in eV - for 'sub_weight' Bound the height of the peak to fraction of - its height - kind : {'single', 'multi'} - If 'single' fit only the current location. If 'multi' - use multifit. - **kwargs : extra key word arguments - All extra key word arguments are passed to fit or - multifit, depending on the value of kind. - """ - - if calibrate == 'energy': - bound = (bound / eV2keV) * self.units_factor - free = self.free_xray_lines_energy - fix = self.fix_xray_lines_energy - elif calibrate == 'sub_weight': - free = self.free_sub_xray_lines_weight - fix = self.fix_sub_xray_lines_weight - elif calibrate == 'width': - bound = (bound / eV2keV) * self.units_factor - free = self.free_xray_lines_width - fix = self.fix_xray_lines_width - - free(xray_lines=xray_lines, bound=bound) - if kind == "single": - self.fit(bounded=True, **kwargs) - elif kind == "multi": - self.multifit(bounded=True, **kwargs) - fix(xray_lines=xray_lines) - - def get_lines_intensity(self, - xray_lines=None, - plot_result=False, - only_one=True, - only_lines=("a",), - **kwargs): - """ - Return the fitted intensity of the X-ray lines. - - Return the area under the gaussian corresping to the X-ray lines - - Parameters - ---------- - xray_lines: {None, list of string} - If None, - if `metadata.Sample.elements.xray_lines` contains a - list of lines use those. - If `metadata.Sample.elements.xray_lines` is undefined - or empty but `metadata.Sample.elements` is defined, - use the same syntax as `add_line` to select a subset of lines - for the operation. - Alternatively, provide an iterable containing - a list of valid X-ray lines symbols. - plot_result : bool - If True, plot the calculated line intensities. If the current - object is a single spectrum it prints the result instead. - only_one : bool - If False, use all the lines of each element in the data spectral - range. If True use only the line at the highest energy - above an overvoltage of 2 (< beam energy / 2). - only_lines : {None, list of strings} - If not None, use only the given lines. - kwargs - The extra keyword arguments for plotting. See - `utils.plot.plot_signals` - - Returns - ------- - intensities : list - A list containing the intensities as Signal subclasses. - - Examples - -------- - >>> m.multifit() - >>> m.get_lines_intensity(["C_Ka", "Ta_Ma"]) - """ - from hyperspy import utils - - intensities = [] - - if xray_lines is None: - xray_lines = [component.name for component in self.xray_lines] - else: - xray_lines = self.signal._parse_xray_lines( - xray_lines, only_one, only_lines) - xray_lines = list(filter(lambda x: x in [a.name for a in - self], xray_lines)) - if len(xray_lines) == 0: - raise ValueError("These X-ray lines are not part of the model.") - - for xray_line in xray_lines: - element, line = utils_eds._get_element_and_line(xray_line) - line_energy = self.signal._get_line_energy(xray_line) - data_res = self[xray_line].A.map['values'] - if self.axes_manager.navigation_dimension == 0: - data_res = data_res[0] - img = self.signal.isig[0:1].integrate1D(-1) - img.data = data_res - img.metadata.General.title = ( - 'Intensity of %s at %.2f %s from %s' % - (xray_line, - line_energy, - self.signal.axes_manager.signal_axes[0].units, - self.signal.metadata.General.title)) - img.axes_manager.set_signal_dimension(0) - if plot_result and img.axes_manager.signal_dimension == 0: - print("%s at %s %s : Intensity = %.2f" - % (xray_line, - line_energy, - self.signal.axes_manager.signal_axes[0].units, - img.data)) - img.metadata.set_item("Sample.elements", ([element])) - img.metadata.set_item("Sample.xray_lines", ([xray_line])) - intensities.append(img) - if plot_result and img.axes_manager.signal_dimension != 0: - utils.plot.plot_signals(intensities, **kwargs) - return intensities - - def remove(self, thing): - thing = self._get_component(thing) - if not np.iterable(thing): - thing = [thing, ] - for comp in thing: - if comp in self.xray_lines: - self.xray_lines.remove(comp) - elif comp in self.family_lines: - self.family_lines.remove(comp) - elif comp in self.background_components: - self.background_components.remove(comp) - super().remove(thing) diff --git a/hyperspy/models/edssemmodel.py b/hyperspy/models/edssemmodel.py deleted file mode 100644 index 3d68b78bbe..0000000000 --- a/hyperspy/models/edssemmodel.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy.models.edsmodel import EDSModel - - -class EDSSEMModel(EDSModel): - - """Build and fit a model to EDS data acquired in the SEM. - - Parameters - ---------- - spectrum : EDSSEMSpectrum - - auto_add_lines : bool - If True, automatically add Gaussians for all X-rays generated - in the energy range by an element, using the edsmodel.add_family_lines - method. - auto_background : bool - If True, adds automatically a polynomial order 6 to the model, - using the edsmodel.add_polynomial_background method. - - Any extra arguments are passed to the Model constructor. - """ - - def __init__(self, spectrum, - auto_background=True, - auto_add_lines=True, - *args, **kwargs): - EDSModel.__init__(self, spectrum, auto_background, auto_add_lines, - *args, **kwargs) diff --git a/hyperspy/models/edstemmodel.py b/hyperspy/models/edstemmodel.py deleted file mode 100644 index 5fe6cf7843..0000000000 --- a/hyperspy/models/edstemmodel.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -from hyperspy.models.edsmodel import EDSModel - - -class EDSTEMModel(EDSModel): - - """Build and fit a model to EDS data acquired in the TEM. - - Parameters - ---------- - spectrum : EDSTEMSpectrum - - auto_add_lines : bool - If True, automatically add Gaussians for all X-rays generated - in the energy range by an element, using the edsmodel.add_family_lines - method. - auto_background : bool - If True, adds automatically a polynomial order 6 to the model, - using the edsmodel.add_polynomial_background method. - - Any extra arguments are passed to the Model constructor. - """ - - def __init__(self, spectrum, - auto_background=True, - auto_add_lines=True, - *args, **kwargs): - EDSModel.__init__(self, spectrum, auto_background, auto_add_lines, - *args, **kwargs) diff --git a/hyperspy/models/eelsmodel.py b/hyperspy/models/eelsmodel.py deleted file mode 100644 index de632eb3af..0000000000 --- a/hyperspy/models/eelsmodel.py +++ /dev/null @@ -1,974 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import copy -import logging -import warnings - -from hyperspy import components1d -from hyperspy._signals.eels import EELSSpectrum -from hyperspy.components1d import EELSCLEdge, PowerLaw -from hyperspy.docstrings.model import FIT_PARAMETERS_ARG -from hyperspy.models.model1d import Model1D - -_logger = logging.getLogger(__name__) - - -class EELSModel(Model1D): - - """Build an EELS model - - Parameters - ---------- - spectrum : a Signal1D (or any Signal1D subclass) instance - auto_background : bool - If True, and if spectrum is an EELS instance adds automatically - a powerlaw to the model and estimate the parameters by the - two-area method. - auto_add_edges : bool - If True, and if spectrum is an EELS instance, it will - automatically add the ionization edges as defined in the - Signal1D instance. Adding a new element to the spectrum using - the components.EELSSpectrum.add_elements method automatically - add the corresponding ionisation edges to the model. - ll : {None, EELSSpectrum} - If an EELSSPectrum is provided, it will be assumed that it is - a low-loss EELS spectrum, and it will be used to simulate the - effect of multiple scattering by convolving it with the EELS - spectrum. - GOS : {'hydrogenic', 'Hartree-Slater', None} - The GOS to use when auto adding core-loss EELS edges. - If None it will use the Hartree-Slater GOS if - they are available, otherwise it will use the hydrogenic GOS. - dictionary : {dict, None} - A dictionary to be used to recreate a model. Usually generated using - :meth:`hyperspy.model.as_dictionary` - - """ - - def __init__(self, signal1D, auto_background=True, - auto_add_edges=True, ll=None, - GOS=None, dictionary=None): - Model1D.__init__(self, signal1D) - - # When automatically setting the fine structure energy regions, - # the fine structure of an EELS edge component is automatically - # disable if the next ionisation edge onset distance to the - # higher energy side of the fine structure region is lower that - # the value of this parameter - self._min_distance_between_edges_for_fine_structure = 0 - self._preedge_safe_window_width = 2 - self.signal1D = signal1D - self._suspend_auto_fine_structure_width = False - self.convolved = False - self.low_loss = ll - self.GOS = GOS - self.edges = [] - self._background_components = [] - if dictionary is not None: - auto_background = False - auto_add_edges = False - self._load_dictionary(dictionary) - - if auto_background is True: - background = PowerLaw() - self.append(background) - - if self.signal.subshells and auto_add_edges is True: - self._add_edges_from_subshells_names() - - @property - def signal1D(self): - return self._signal - - @signal1D.setter - def signal1D(self, value): - if isinstance(value, EELSSpectrum): - self._signal = value - else: - raise ValueError( - "This attribute can only contain an EELSSpectrum " - "but an object of type %s was provided" % - str(type(value))) - - def append(self, component): - """Append component to EELS model. - - Parameters - ---------- - component - HyperSpy component1D object. - - Raises - ------ - NotImplementedError - If the signal axis is a non-uniform axis. - """ - super(EELSModel, self).append(component) - if isinstance(component, EELSCLEdge): - # Test that signal axis is uniform - if not self.axes_manager[-1].is_uniform: - raise NotImplementedError("This operation is not yet implemented " - "for non-uniform energy axes") - tem = self.signal.metadata.Acquisition_instrument.TEM - component.set_microscope_parameters( - E0=tem.beam_energy, - alpha=tem.convergence_angle, - beta=tem.Detector.EELS.collection_angle, - energy_scale=self.axis.scale) - component.energy_scale = self.axis.scale - component._set_fine_structure_coeff() - self._classify_components() - - append.__doc__ = Model1D.append.__doc__ - - def remove(self, component): - super(EELSModel, self).remove(component) - self._classify_components() - - remove.__doc__ = Model1D.remove.__doc__ - - def _classify_components(self): - """Classify components between background and ionization edge - components. - - This method should be called everytime that components are added and - removed. An ionization edge becomes background when its onset falls to - the left of the first non-masked energy channel. The ionization edges - are stored in a list in the `edges` attribute. They are sorted by - increasing `onset_energy`. The background components are stored in - `_background_components`. - - """ - self.edges = [] - self._background_components = [] - for component in self: - if isinstance(component, EELSCLEdge): - if component.onset_energy.value < \ - self.axis.axis[self.channel_switches][0]: - component.isbackground = True - if component.isbackground is not True: - self.edges.append(component) - else: - component.fine_structure_active = False - component.fine_structure_coeff.free = False - elif (isinstance(component, PowerLaw) or - component.isbackground is True): - self._background_components.append(component) - - if self.edges: - self.edges.sort(key=EELSCLEdge._onset_energy) - self.resolve_fine_structure() - if len(self._background_components) > 1: - self._backgroundtype = "mix" - elif len(self._background_components) == 1: - self._backgroundtype = \ - self._background_components[0].__repr__() - bg = self._background_components[0] - if isinstance(bg, PowerLaw) and self.edges and not \ - bg.A.map["is_set"].any(): - self.two_area_background_estimation() - - @property - def _active_edges(self): - return [edge for edge in self.edges if edge.active] - - @property - def _active_background_components(self): - return [bc for bc in self._background_components if bc.active] - - def _add_edges_from_subshells_names(self, e_shells=None): - """Create the Edge instances and configure them appropiately - Parameters - ---------- - e_shells : list of strings - """ - if self.signal._are_microscope_parameters_missing(): - raise ValueError( - "The required microscope parameters are not defined in " - "the EELS spectrum signal metadata. Use " - "``set_microscope_parameters`` to set them." - ) - if e_shells is None: - e_shells = list(self.signal.subshells) - e_shells.sort() - master_edge = EELSCLEdge(e_shells.pop(), self.GOS) - # If self.GOS was None, the GOS is set by eels_cl_edge so - # we reassing the value of self.GOS - self.GOS = master_edge.GOS._name - self.append(master_edge) - element = master_edge.element - while len(e_shells) > 0: - next_element = e_shells[-1].split('_')[0] - if next_element != element: - # New master edge - self._add_edges_from_subshells_names(e_shells=e_shells) - elif self.GOS == 'hydrogenic': - # The hydrogenic GOS includes all the L subshells in one - # so we get rid of the others - e_shells.pop() - else: - # Add the other subshells of the same element - # and couple their intensity and onset_energy to that of the - # master edge - edge = EELSCLEdge(e_shells.pop(), GOS=self.GOS) - - edge.intensity.twin = master_edge.intensity - edge.onset_energy.twin = master_edge.onset_energy - edge.onset_energy.twin_function_expr = "x + {}".format( - (edge.GOS.onset_energy - master_edge.GOS.onset_energy)) - edge.free_onset_energy = False - self.append(edge) - - def resolve_fine_structure( - self, - preedge_safe_window_width=2, - i1=0): - """Adjust the fine structure of all edges to avoid overlapping - - This function is called automatically everytime the position of an edge - changes - - Parameters - ---------- - preedge_safe_window_width : float - minimum distance between the fine structure of an ionization edge - and that of the following one. Default 2 (eV). - - """ - - if self._suspend_auto_fine_structure_width is True: - return - - if not self._active_edges: - return - - while (self._active_edges[i1].fine_structure_active is False and - i1 < len(self._active_edges) - 1): - i1 += 1 - if i1 < len(self._active_edges) - 1: - i2 = i1 + 1 - while (self._active_edges[i2].fine_structure_active is False and - i2 < len(self._active_edges) - 1): - i2 += 1 - if self._active_edges[i2].fine_structure_active is True: - distance_between_edges = ( - self._active_edges[i2].onset_energy.value - - self._active_edges[i1].onset_energy.value) - if (self._active_edges[i1].fine_structure_width > - distance_between_edges - - self._preedge_safe_window_width): - min_d = self._min_distance_between_edges_for_fine_structure - if (distance_between_edges - - self._preedge_safe_window_width) <= min_d: - _logger.info(( - "Automatically deactivating the fine structure " - "of edge number %d to avoid conflicts with edge " - "number %d") % (i2 + 1, i1 + 1)) - self._active_edges[i2].fine_structure_active = False - self._active_edges[ - i2].fine_structure_coeff.free = False - self.resolve_fine_structure(i1=i2) - else: - new_fine_structure_width = ( - distance_between_edges - - self._preedge_safe_window_width) - _logger.info(( - "Automatically changing the fine structure " - "width of edge %d from %s eV to %s eV to avoid " - "conflicts with edge number %d") % ( - i1 + 1, - self._active_edges[i1].fine_structure_width, - new_fine_structure_width, - i2 + 1)) - self._active_edges[i1].fine_structure_width = \ - new_fine_structure_width - self.resolve_fine_structure(i1=i2) - else: - self.resolve_fine_structure(i1=i2) - else: - return - - def fit(self, kind="std", **kwargs): - """Fits the model to the experimental data. - - Read more in the :ref:`User Guide `. - - Parameters - ---------- - kind : {"std", "smart"}, default "std" - If "std", performs standard fit. If "smart", - performs a smart_fit - for more details see - the :ref:`User Guide `. - %s - - Returns - ------- - None - - See Also - -------- - * :py:meth:`~hyperspy.model.BaseModel.fit` - * :py:meth:`~hyperspy.model.BaseModel.multifit` - * :py:meth:`~hyperspy.model.EELSModel.smart_fit` - - """ - if kind not in ["smart", "std"]: - raise ValueError( - f"kind must be either 'std' or 'smart', not '{kind}'" - ) - elif kind == "smart": - return self.smart_fit(**kwargs) - elif kind == "std": - return Model1D.fit(self, **kwargs) - - fit.__doc__ %= FIT_PARAMETERS_ARG - - def smart_fit(self, start_energy=None, **kwargs): - """Fits EELS edges in a cascade style. - - The fitting procedure acts in iterative manner along - the energy-loss-axis. First it fits only the background - up to the first edge. It continues by deactivating all - edges except the first one, then performs the fit. Then - it only activates the the first two, fits, and repeats - this until all edges are fitted simultanously. - - Other, non-EELSCLEdge components, are never deactivated, - and fitted on every iteration. - - Parameters - ---------- - start_energy : {float, None} - If float, limit the range of energies from the left to the - given value. - %s - - See Also - -------- - * :py:meth:`~hyperspy.model.BaseModel.fit` - * :py:meth:`~hyperspy.model.BaseModel.multifit` - * :py:meth:`~hyperspy.model.EELSModel.fit` - - """ - # Fit background - self.fit_background(start_energy, **kwargs) - - # Fit the edges - for i in range(0, len(self._active_edges)): - self._fit_edge(i, start_energy, **kwargs) - - smart_fit.__doc__ %= FIT_PARAMETERS_ARG - - def _get_first_ionization_edge_energy(self, start_energy=None): - """Calculate the first ionization edge energy. - - Returns - ------- - iee : float or None - The first ionization edge energy or None if no edge is defined in - the model. - - """ - if not self._active_edges: - return None - start_energy = self._get_start_energy(start_energy) - iee_list = [edge.onset_energy.value for edge in self._active_edges - if edge.onset_energy.value > start_energy] - iee = min(iee_list) if iee_list else None - return iee - - def _get_start_energy(self, start_energy=None): - E0 = self.axis.axis[self.channel_switches][0] - if not start_energy or start_energy < E0: - start_energy = E0 - return start_energy - - def fit_background(self, start_energy=None, only_current=True, **kwargs): - """Fit the background to the first active ionization edge - in the energy range. - - Parameters - ---------- - start_energy : {float, None}, optional - If float, limit the range of energies from the left to the - given value. Default None. - only_current : bool, optional - If True, only fit the background at the current coordinates. - Default True. - **kwargs : extra key word arguments - All extra key word arguments are passed to fit or - multifit. - - """ - - # If there is no active background compenent do nothing - if not self._active_background_components: - return - iee = self._get_first_ionization_edge_energy(start_energy=start_energy) - if iee is not None: - to_disable = [edge for edge in self._active_edges - if edge.onset_energy.value >= iee] - E2 = iee - self._preedge_safe_window_width - self.disable_edges(to_disable) - else: - E2 = None - self.set_signal_range(start_energy, E2) - if only_current: - self.fit(**kwargs) - else: - self.multifit(**kwargs) - self.channel_switches = copy.copy(self.backup_channel_switches) - if iee is not None: - self.enable_edges(to_disable) - - def two_area_background_estimation(self, E1=None, E2=None, powerlaw=None): - """Estimates the parameters of a power law background with the two - area method. - - Parameters - ---------- - E1 : float - E2 : float - powerlaw : PowerLaw component or None - If None, it will try to guess the right component from the - background components of the model - - """ - if powerlaw is None: - for component in self._active_background_components: - if isinstance(component, components1d.PowerLaw): - if powerlaw is None: - powerlaw = component - else: - _logger.warning( - 'There are more than two power law ' - 'background components defined in this model, ' - 'please use the powerlaw keyword to specify one' - ' of them') - return - else: # No power law component - return - - ea = self.axis.axis[self.channel_switches] - E1 = self._get_start_energy(E1) - if E2 is None: - E2 = self._get_first_ionization_edge_energy(start_energy=E1) - if E2 is None: - E2 = ea[-1] - else: - E2 = E2 - \ - self._preedge_safe_window_width - - if not powerlaw.estimate_parameters( - self.signal, E1, E2, only_current=False): - _logger.warning( - "The power law background parameters could not " - "be estimated.\n" - "Try choosing a different energy range for the estimation") - return - - def _fit_edge(self, edgenumber, start_energy=None, **kwargs): - backup_channel_switches = self.channel_switches.copy() - ea = self.axis.axis[self.channel_switches] - if start_energy is None: - start_energy = ea[0] - # Declare variables - active_edges = self._active_edges - edge = active_edges[edgenumber] - if (edge.intensity.twin is not None or - edge.active is False or - edge.onset_energy.value < start_energy or - edge.onset_energy.value > ea[-1]): - return 1 - # Fitting edge 'edge.name' - last_index = len(self._active_edges) - 1 # Last edge index - i = 1 - twins = [] - # find twins - while edgenumber + i <= last_index and ( - active_edges[edgenumber + i].intensity.twin is not None or - active_edges[edgenumber + i].active is False): - if active_edges[edgenumber + i].intensity.twin is not None: - twins.append(self._active_edges[edgenumber + i]) - i += 1 - if (edgenumber + i) > last_index: - nextedgeenergy = ea[-1] - else: - nextedgeenergy = ( - active_edges[edgenumber + i].onset_energy.value - - self._preedge_safe_window_width) - - # Backup the fsstate - to_activate_fs = [] - for edge_ in [edge, ] + twins: - if (edge_.fine_structure_active is True and - edge_.fine_structure_coeff.free is True): - to_activate_fs.append(edge_) - self.disable_fine_structure(to_activate_fs) - - # Smart Fitting - - # Without fine structure to determine onset_energy - edges_to_activate = [] - for edge_ in self._active_edges[edgenumber + 1:]: - if (edge_.active is True and - edge_.onset_energy.value >= nextedgeenergy): - edge_.active = False - edges_to_activate.append(edge_) - - self.set_signal_range(start_energy, nextedgeenergy) - if edge.free_onset_energy is True: - edge.onset_energy.free = True - self.fit(**kwargs) - edge.onset_energy.free = False - _logger.info("onset_energy = %s", edge.onset_energy.value) - self._classify_components() - elif edge.intensity.free is True: - self.enable_fine_structure(to_activate_fs) - self.remove_fine_structure_data(to_activate_fs) - self.disable_fine_structure(to_activate_fs) - self.fit(**kwargs) - - if len(to_activate_fs) > 0: - self.set_signal_range(start_energy, nextedgeenergy) - self.enable_fine_structure(to_activate_fs) - self.fit(**kwargs) - - self.enable_edges(edges_to_activate) - # Recover the channel_switches. Remove it or make it smarter. - self.channel_switches = backup_channel_switches - - def quantify(self): - """Prints the value of the intensity of all the independent - active EELS core loss edges defined in the model - - """ - elements = {} - for edge in self._active_edges: - if edge.active and edge.intensity.twin is None: - element = edge.element - subshell = edge.subshell - if element not in elements: - elements[element] = {} - elements[element][subshell] = edge.intensity.value - print() - print("Absolute quantification:") - print("Elem.\tIntensity") - for element in elements: - if len(elements[element]) == 1: - for subshell in elements[element]: - print("%s\t%f" % ( - element, elements[element][subshell])) - else: - for subshell in elements[element]: - print("%s_%s\t%f" % (element, subshell, - elements[element][subshell])) - - def remove_fine_structure_data(self, edges_list=None): - """Remove the fine structure data from the fitting routine as - defined in the fine_structure_width parameter of the - component.EELSCLEdge - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if (edge.isbackground is False and - edge.fine_structure_active is True): - start = edge.onset_energy.value - stop = start + edge.fine_structure_width - self.remove_signal_range(start, stop) - - def enable_edges(self, edges_list=None): - """Enable the edges listed in edges_list. If edges_list is - None (default) all the edges with onset in the spectrum energy - region will be enabled. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - - if edges_list is None: - edges_list = self.edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.active = True - self.resolve_fine_structure() - - def disable_edges(self, edges_list=None): - """Disable the edges listed in edges_list. If edges_list is None (default) - all the edges with onset in the spectrum energy region will be - disabled. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.active = False - self.resolve_fine_structure() - - def enable_background(self): - """Enable the background componets. - - """ - for component in self._background_components: - component.active = True - - def disable_background(self): - """Disable the background components. - - """ - for component in self._active_background_components: - component.active = False - - def enable_fine_structure(self, edges_list=None): - """Enable the fine structure of the edges listed in edges_list. - If edges_list is None (default) the fine structure of all the edges - with onset in the spectrum energy region will be enabled. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.fine_structure_active = True - edge.fine_structure_coeff.free = True - self.resolve_fine_structure() - - def disable_fine_structure(self, edges_list=None): - """Disable the fine structure of the edges listed in edges_list. - If edges_list is None (default) the fine structure of all the edges - with onset in the spectrum energy region will be disabled. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.fine_structure_active = False - edge.fine_structure_coeff.free = False - self.resolve_fine_structure() - - def set_all_edges_intensities_positive(self): - for edge in self._active_edges: - edge.intensity.ext_force_positive = True - edge.intensity.ext_bounded = True - - def unset_all_edges_intensities_positive(self): - for edge in self._active_edges: - edge.intensity.ext_force_positive = False - edge.intensity.ext_bounded = False - - def enable_free_onset_energy(self, edges_list=None): - """Enable the automatic freeing of the onset_energy parameter during a - smart fit for the edges listed in edges_list. - If edges_list is None (default) the onset_energy of all the edges - with onset in the spectrum energy region will be freeed. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.free_onset_energy = True - - def disable_free_onset_energy(self, edges_list=None): - """Disable the automatic freeing of the onset_energy parameter during a - smart fit for the edges listed in edges_list. - If edges_list is None (default) the onset_energy of all the edges - with onset in the spectrum energy region will not be freed. - Note that if their atribute edge.onset_energy.free is True, the - parameter will be free during the smart fit. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.free_onset_energy = True - - def fix_edges(self, edges_list=None): - """Fixes all the parameters of the edges given in edges_list. - If edges_list is None (default) all the edges will be fixed. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.intensity.free = False - edge.onset_energy.free = False - edge.fine_structure_coeff.free = False - - def free_edges(self, edges_list=None): - """Frees all the parameters of the edges given in edges_list. - If edges_list is None (default) all the edges will be freeed. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.intensity.free = True - - def fix_fine_structure(self, edges_list=None): - """Fixes all the parameters of the edges given in edges_list. - If edges_list is None (default) all the edges will be fixed. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.fine_structure_coeff.free = False - - def free_fine_structure(self, edges_list=None): - """Frees all the parameters of the edges given in edges_list. - If edges_list is None (default) all the edges will be freeed. - - Parameters - ---------- - edges_list : None or list of EELSCLEdge or list of edge names - If None, the operation is performed on all the edges in the model. - Otherwise, it will be performed only on the listed components. - - See Also - -------- - enable_edges, disable_edges, enable_background, - disable_background, enable_fine_structure, - disable_fine_structure, set_all_edges_intensities_positive, - unset_all_edges_intensities_positive, enable_free_onset_energy, - disable_free_onset_energy, fix_edges, free_edges, fix_fine_structure, - free_fine_structure - - """ - if edges_list is None: - edges_list = self._active_edges - else: - edges_list = [self._get_component(x) for x in edges_list] - for edge in edges_list: - if edge.isbackground is False: - edge.fine_structure_coeff.free = True - - def suspend_auto_fine_structure_width(self): - """Disable the automatic adjustament of the core-loss edges fine - structure width. - - See Also - -------- - resume_auto_fine_structure_width - - """ - if self._suspend_auto_fine_structure_width is False: - self._suspend_auto_fine_structure_width = True - else: - warnings.warn("Already suspended, does nothing.") - - def resume_auto_fine_structure_width(self, update=True): - """Enable the automatic adjustament of the core-loss edges fine - structure width. - - Parameters - ---------- - update : bool, optional - If True, also execute the automatic adjustment (default). - - - See Also - -------- - suspend_auto_fine_structure_width - - """ - if self._suspend_auto_fine_structure_width is True: - self._suspend_auto_fine_structure_width = False - if update is True: - self.resolve_fine_structure() - else: - warnings.warn("Not suspended, nothing to resume.") diff --git a/hyperspy/models/model1d.py b/hyperspy/models/model1d.py index f0b3d7701e..45780496a2 100644 --- a/hyperspy/models/model1d.py +++ b/hyperspy/models/model1d.py @@ -1,53 +1,61 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import copy import numpy as np -from scipy.special import huber import traits.api as t +from scipy.special import huber import hyperspy.drawing.signal1d -from hyperspy.axes import generate_uniform_axis -from hyperspy.exceptions import WrongObjectError, SignalDimensionError from hyperspy.decorators import interactive_range_selector from hyperspy.drawing.widgets import LabelWidget, VerticalLineWidget from hyperspy.events import EventSuppressor -from hyperspy.exceptions import SignalDimensionError, WrongObjectError -from hyperspy.model import BaseModel, ModelComponents, ModelSpecialSlicers +from hyperspy.exceptions import SignalDimensionError +from hyperspy.misc.utils import dummy_context_manager +from hyperspy.model import BaseModel, ModelComponents from hyperspy.signal_tools import SpanSelectorInSignal1D from hyperspy.ui_registry import DISPLAY_DT, TOOLKIT_DT, add_gui_method -from hyperspy.misc.utils import dummy_context_manager -from hyperspy.misc.utils import is_binned # remove in v2.0 @add_gui_method(toolkey="hyperspy.Model1D.fit_component") class ComponentFit(SpanSelectorInSignal1D): only_current = t.Bool(True) - iterpath = t.Enum('flyback', 'serpentine', default='serpentine', - desc='Define the iterating pattern over the navigation space.') - - def __init__(self, model, component, signal_range=None, - estimate_parameters=True, fit_independent=False, - only_current=True, iterpath='flyback', **kwargs): + iterpath = t.Enum( + "flyback", + "serpentine", + default="serpentine", + desc="Define the iterating pattern over the navigation space.", + ) + + def __init__( + self, + model, + component, + signal_range=None, + estimate_parameters=True, + fit_independent=False, + only_current=True, + iterpath="serpentine", + **kwargs, + ): if model.signal.axes_manager.signal_dimension != 1: - raise SignalDimensionError( - model.signal.axes_manager.signal_dimension, 1) + raise SignalDimensionError(model.signal.axes_manager.signal_dimension, 1) self.signal = model.signal self.axis = self.signal.axes_manager.signal_axes[0] @@ -61,18 +69,19 @@ def __init__(self, model, component, signal_range=None, self.only_current = only_current self.iterpath = iterpath if signal_range == "interactive": - if (not hasattr(self.model, '_plot') or self.model._plot is None or - not self.model._plot.is_active): + if ( + not hasattr(self.model, "_plot") + or self.model._plot is None + or not self.model._plot.is_active + ): self.model.plot() self.span_selector_switch(on=True) def _fit_fired(self): - if (self.signal_range != "interactive" and - self.signal_range is not None): + if self.signal_range != "interactive" and self.signal_range is not None: self.model.set_signal_range(*self.signal_range) elif self.signal_range == "interactive": - self.model.set_signal_range(self.ss_left_value, - self.ss_right_value) + self.model.set_signal_range(self.ss_left_value, self.ss_right_value) # Backup "free state" of the parameters and fix all but those # of the chosen component @@ -96,19 +105,21 @@ def _fit_fired(self): # the components estimate_parameters function (if it has one) only_current = self.only_current if self.estimate_parameters: - if hasattr(self.component, 'estimate_parameters'): + if hasattr(self.component, "estimate_parameters"): if self.signal_range == "interactive": self.component.estimate_parameters( self.signal, self.ss_left_value, self.ss_right_value, - only_current=only_current) + only_current=only_current, + ) elif self.signal_range is not None: self.component.estimate_parameters( self.signal, self.signal_range[0], self.signal_range[1], - only_current=only_current) + only_current=only_current, + ) if only_current: self.model.fit(**self.fit_kwargs) @@ -117,8 +128,7 @@ def _fit_fired(self): # Restore the signal range if self.signal_range is not None: - self.model.channel_switches = ( - self.model.backup_channel_switches.copy()) + self.model._channel_switches = self.model._backup_channel_switches.copy() self.model.update_plot() @@ -136,123 +146,90 @@ def apply(self): class Model1D(BaseModel): - """Model and data fitting for one dimensional signals. - A model is constructed as a linear combination of :mod:`components` that - are added to the model using :meth:`append` or :meth:`extend`. There - are many predifined components available in the in the :mod:`components` - module. If needed, new components can be created easily using the code of - existing components as a template. - - Once defined, the model can be fitted to the data using :meth:`fit` or - :meth:`multifit`. Once the optimizer reaches the convergence criteria or - the maximum number of iterations the new value of the component parameters - are stored in the components. + A model is constructed as a linear combination of + :mod:`~hyperspy.api.model.components1D` that are added to the model using + :meth:`~hyperspy.model.BaseModel.append` or :meth:`~hyperspy.model.BaseModel.extend`. + There are many predifined components available in the + :mod:`~hyperspy.api.model.components1D` module. If needed, new + components can be created easily using the + :class:`~.api.model.components1D.Expression` component or by + using the code of existing components as a template. + + Once defined, the model can be fitted to the data using + :meth:`~hyperspy.model.BaseModel.fit` or + :meth:`~hyperspy.model.BaseModel.multifit`. Once the optimizer reaches + the convergence criteria or the maximum number of iterations the new value + of the component parameters are stored in the components. It is possible to access the components in the model by their name or by the index in the model. An example is given at the end of this docstring. - Attributes - ---------- - signal : Signal1D instance - It contains the data to fit. - chisq : A Signal of floats - Chi-squared of the signal (or np.nan if not yet fit) - dof : A Signal of integers - Degrees of freedom of the signal (0 if not yet fit) - red_chisq : Signal instance - Reduced chi-squared. - components : `ModelComponents` instance - The components of the model are attributes of this class. This provides - a convinient way to access the model components when working in IPython - as it enables tab completion. - Methods ------- - extend - Append multiple components to the model. - as_signal - Generate a Signal1D instance (possible multidimensional) - from the model. - store_current_values - Store the value of the parameters at the current position. - fetch_stored_values - fetch stored values of the parameters. - update_plot - Force a plot update. (In most cases the plot should update - automatically.) - set_signal_range, remove_signal range, reset_signal_range, - add signal_range. - Customize the signal range to fit. - fit, multifit - Fit the model to the data at the current position or the - full dataset. - save_parameters2file, load_parameters_from_file - Save/load the parameter values to/from a file. - enable_plot_components, disable_plot_components - Plot each component separately. (Use after `plot`.) - set_current_values_to - Set the current value of all the parameters of the given component as - the value for all the dataset. - export_results - Save the value of the parameters in separate files. - plot_results - Plot the value of all parameters at all positions. - print_current_values - Print the value of the parameters at the current position. - enable_adjust_position, disable_adjust_position - Enable/disable interactive adjustment of the position of the components - that have a well defined position. (Use after `plot`). - set_parameters_not_free, set_parameters_free - Fit the `free` status of several components and parameters at once. - set_parameters_value - Set the value of a parameter in components in a model to a specified - value. - as_dictionary - Exports the model to a dictionary that can be saved in a file. + fit_component + enable_adjust_position + disable_adjust_position + plot + set_signal_range + remove_signal_range + reset_signal_range + add_signal_range Examples -------- In the following example we create a histogram from a normal distribution and fit it with a gaussian component. It demonstrates how to create - a model from a :class:`~._signals.signal1d.Signal1D` instance, add + a model from a :class:`~.api.signals.Signal1D` instance, add components to it, adjust the value of the parameters of the components, fit the model to the data and access the components in the model. >>> s = hs.signals.Signal1D( - np.random.normal(scale=2, size=10000)).get_histogram() + ... np.random.normal(scale=2, size=10000)).get_histogram() >>> g = hs.model.components1D.Gaussian() >>> m = s.create_model() >>> m.append(g) >>> m.print_current_values() - Components Parameter Value - Gaussian - sigma 1.000000 - A 1.000000 - centre 0.000000 + Model1D: histogram + CurrentComponentValues: Gaussian + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | True | 1.0 | None | 0.0 | None | True + centre | True | 0.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False >>> g.centre.value = 3 >>> m.print_current_values() - Components Parameter Value - Gaussian - sigma 1.000000 - A 1.000000 - centre 3.000000 + Model1D: histogram + CurrentComponentValues: Gaussian + Active: True + Parameter Name | Free | Value | Std | Min | Max | Linear + ============== | ======= | ========== | ========== | ========== | ========== | ====== + A | True | 1.0 | None | 0.0 | None | True + centre | True | 3.0 | None | None | None | False + sigma | True | 1.0 | None | 0.0 | None | False >>> g.sigma.value 1.0 - >>> m.fit() - >>> g.sigma.value + >>> m.fit() # doctest: +SKIP + >>> g.sigma.value # doctest: +SKIP 1.9779042300856682 - >>> m[0].sigma.value + >>> m[0].sigma.value # doctest: +SKIP 1.9779042300856682 - >>> m["Gaussian"].centre.value + >>> m["Gaussian"].centre.value # doctest: +SKIP -0.072121936813224569 + See Also + -------- + hyperspy.model.BaseModel, hyperspy.models.model2d.Model2D + """ + _signal_dimension = 1 + def __init__(self, signal1D, dictionary=None): - super(Model1D, self).__init__() - self.signal = signal1D + super().__init__() + self._signal = signal1D self.axes_manager = self.signal.axes_manager self._plot = None self._position_widgets = {} @@ -260,107 +237,61 @@ def __init__(self, signal1D, dictionary=None): self._plot_components = False self._suspend_update = False self._model_line = None + self._residual_line = None self.axis = self.axes_manager.signal_axes[0] - self.axes_manager.events.indices_changed.connect( - self._on_navigating, []) - self.channel_switches = np.array([True] * len(self.axis.axis)) - self.chisq = signal1D._get_navigation_signal() + self.axes_manager.events.indices_changed.connect(self._on_navigating, []) + self._channel_switches = np.array([True] * len(self.axis.axis)) + self._chisq = signal1D._get_navigation_signal() self.chisq.change_dtype("float") self.chisq.data.fill(np.nan) self.chisq.metadata.General.title = ( - self.signal.metadata.General.title + ' chi-squared') - self.dof = self.chisq._deepcopy_with_new_data( - np.zeros_like(self.chisq.data, dtype='int')) + self.signal.metadata.General.title + " chi-squared" + ) + self._dof = self.chisq._deepcopy_with_new_data( + np.zeros_like(self.chisq.data, dtype="int") + ) self.dof.metadata.General.title = ( - self.signal.metadata.General.title + ' degrees of freedom') + self.signal.metadata.General.title + " degrees of freedom" + ) self.free_parameters_boundaries = None - self._low_loss = None - self.convolved = False - self.components = ModelComponents(self) + self._components = ModelComponents(self) if dictionary is not None: self._load_dictionary(dictionary) - self.inav = ModelSpecialSlicers(self, True) - self.isig = ModelSpecialSlicers(self, False) self._whitelist = { - 'channel_switches': None, - 'convolved': None, - 'free_parameters_boundaries': None, - 'low_loss': ('sig', None), - 'chisq.data': None, - 'dof.data': None} + "_channel_switches": None, + "free_parameters_boundaries": None, + "chisq.data": None, + "dof.data": None, + } self._slicing_whitelist = { - 'channel_switches': 'isig', - 'low_loss': 'inav', - 'chisq.data': 'inav', - 'dof.data': 'inav'} - - @property - def signal(self): - return self._signal - - @signal.setter - def signal(self, value): - from hyperspy._signals.signal1d import Signal1D - if isinstance(value, Signal1D): - self._signal = value - else: - raise WrongObjectError(str(type(value)), 'Signal1D') - - @property - def low_loss(self): - return self._low_loss - - @low_loss.setter - def low_loss(self, value): - if value is not None: - if (value.axes_manager.navigation_shape != - self.signal.axes_manager.navigation_shape): - raise ValueError('The low-loss does not have the same ' - 'navigation dimension as the core-loss.') - if not value.axes_manager.signal_axes[0].is_uniform: - raise ValueError('Low loss convolution is not supported with ' - 'non-uniform signal axes.') - self._low_loss = value - self.set_convolution_axis() - self.convolved = True - else: - self._low_loss = value - self.convolution_axis = None - self.convolved = False - - # Extend the list methods to call the _touch when the model is modified - - def set_convolution_axis(self): - """ - Creates an axis to use to generate the data of the model in the precise - scale to obtain the correct axis and origin after convolution with the - lowloss spectrum. - """ - ll_axis = self.low_loss.axes_manager.signal_axes[0] - dimension = self.axis.size + ll_axis.size - 1 - step = self.axis.scale - knot_position = ll_axis.size - ll_axis.value2index(0) - 1 - self.convolution_axis = generate_uniform_axis(self.axis.offset, step, - dimension, knot_position) + "_channel_switches": "isig", + "chisq.data": "inav", + "dof.data": "inav", + } def append(self, thing): - """Add component to Model. + """ + Add component to Model. Parameters ---------- - thing: `Component` instance. + thing : :class:`~.component.Component` + The component to add to the model. """ cm = self.suspend_update if self._plot_active else dummy_context_manager with cm(update_on_resume=False): - super(Model1D, self).append(thing) + super().append(thing) if self._plot_components: self._plot_component(thing) if self._adjust_position_all: - self._make_position_adjuster(thing, self._adjust_position_all[0], - self._adjust_position_all[1]) + self._make_position_adjuster( + thing, self._adjust_position_all[0], self._adjust_position_all[1] + ) if self._plot_active: self.signal._plot.signal_plot.update() + append.__doc__ = BaseModel.append.__doc__ + def remove(self, things): things = self._get_component(things) if not np.iterable(things): @@ -370,28 +301,61 @@ def remove(self, things): if parameter in self._position_widgets: for pw in reversed(self._position_widgets[parameter]): pw.close() - if hasattr(thing, '_component_line'): + if hasattr(thing, "_component_line"): line = thing._component_line line.close() - super(Model1D, self).remove(things) + super().remove(things) self._disconnect_parameters2update_plot(things) remove.__doc__ = BaseModel.remove.__doc__ - def __call__(self, non_convolved=False, onlyactive=False, - component_list=None): - """Returns the corresponding model for the current coordinates + def _get_model_data(self, component_list=None, ignore_channel_switches=False): + """ + Return the model data at the current position Parameters ---------- - non_convolved : bool - If True it will return the deconvolved model - only_active : bool + component_list : list or None + If None, the model is constructed with all active components. Otherwise, + the model is constructed with the components in component_list. + + Returns: + -------- + model_data: `ndarray` + """ + if component_list is None: + component_list = self + slice_ = slice(None) if ignore_channel_switches else self._channel_switches + axis = self.axis.axis[slice_] + model_data = np.zeros(len(axis)) + for component in component_list: + model_data += component.function(axis) + return model_data + + def _get_current_data( + self, + onlyactive=False, + component_list=None, + binned=None, + ignore_channel_switches=False, + ): + """ + Returns the corresponding model for the current coordinates + + Parameters + ---------- + onlyactive : bool If True, only the active components will be used to build the model. component_list : list or None - If None, the sum of all the components is returned. If list, only - the provided components are returned + If None, the model is constructed with all active components. Otherwise, + the model is constructed with the components in component_list. + binned : bool or None + Specify whether the binned attribute of the signal axes needs to be + taken into account. + ignore_channel_switches: bool + If true, the entire signal axis are returned + without checking _channel_switches. cursor: 1 or 2 @@ -403,50 +367,38 @@ def __call__(self, non_convolved=False, onlyactive=False, if component_list is None: component_list = self if not isinstance(component_list, (list, tuple)): - raise ValueError( - "'Component_list' parameter need to be a list or None") + raise ValueError("'Component_list' parameter need to be a list or None") if onlyactive: component_list = [ - component for component in component_list if component.active] - - if self.convolved is False or non_convolved is True: - axis = self.axis.axis[self.channel_switches] - sum_ = np.zeros(len(axis)) - for component in component_list: - sum_ += component.function(axis) - to_return = sum_ - - else: # convolved - sum_convolved = np.zeros(len(self.convolution_axis)) - sum_ = np.zeros(len(self.axis.axis)) - for component in component_list: - if component.convolved: - sum_convolved += component.function(self.convolution_axis) - else: - sum_ += component.function(self.axis.axis) - - to_return = sum_ + np.convolve( - self.low_loss(self.axes_manager), - sum_convolved, mode="valid") - to_return = to_return[self.channel_switches] - if is_binned(self.signal): - # in v2 replace by - #if self.signal.axes_manager[-1].is_binned: - if self.signal.axes_manager[-1].is_uniform: - to_return *= self.signal.axes_manager[-1].scale + component for component in component_list if component.active + ] + model_data = self._get_model_data( + component_list=component_list, + ignore_channel_switches=ignore_channel_switches, + ) + if binned is None: + # use self.axis instead of self.signal.axes_manager[-1] + # to avoid small overhead (~10 us) which isn't negligeable when + # __call__ is called repeatably, typically when fitting! + binned = self.axis.is_binned + + if binned: + if self.axis.is_uniform: + model_data *= self.axis.scale else: - to_return *= np.gradient(self.signal.axes_manager[-1].axis) - return to_return + model_data *= np.gradient(self.axis.axis) + return model_data def _errfunc(self, param, y, weights=None): if weights is None: - weights = 1. + weights = 1.0 errfunc = self._model_function(param) - y return errfunc * weights def _set_signal_range_in_pixels(self, i1=None, i2=None): - """Use only the selected spectral range in the fitting routine. + """ + Use only the selected spectral range in the fitting routine. Parameters ---------- @@ -458,16 +410,17 @@ def _set_signal_range_in_pixels(self, i1=None, i2=None): To use the full energy range call the function without arguments. """ - self.backup_channel_switches = copy.copy(self.channel_switches) - self.channel_switches[:] = False + self._backup_channel_switches = copy.copy(self._channel_switches) + self._channel_switches[:] = False if i2 is not None: i2 += 1 - self.channel_switches[i1:i2] = True - self.update_plot() + self._channel_switches[i1:i2] = True + self.update_plot(render_figure=True) def _parse_signal_range_values(self, x1=None, x2=None): - """Parse signal range values to be used by the `set_signal_range`, - `add_signal_range` and `remove_signal_range` and return sorted indices + """ + Parse signal range values to be used by the `set_signal_range`, + `add_signal_range` and `remove_signal_range` and return sorted indices. """ try: x1, x2 = x1 @@ -478,19 +431,26 @@ def _parse_signal_range_values(self, x1=None, x2=None): @interactive_range_selector def set_signal_range(self, x1=None, x2=None): - """Use only the selected spectral range defined in its own units in the + """ + Use only the selected spectral range defined in its own units in the fitting routine. Parameters ---------- x1, x2 : None or float + + See Also + -------- + add_signal_range, remove_signal_range, reset_signal_range, + hyperspy.model.BaseModel.set_signal_range_from_mask """ indices = self._parse_signal_range_values(x1, x2) self._set_signal_range_in_pixels(*indices) def _remove_signal_range_in_pixels(self, i1=None, i2=None): - """Removes the data in the given range from the data range that - will be used by the fitting rountine + """ + Removes the data in the given range from the data range that + will be used by the fitting rountine. Parameters ---------- @@ -498,28 +458,41 @@ def _remove_signal_range_in_pixels(self, i1=None, i2=None): """ if i2 is not None: i2 += 1 - self.channel_switches[i1:i2] = False + self._channel_switches[i1:i2] = False self.update_plot() @interactive_range_selector def remove_signal_range(self, x1=None, x2=None): - """Removes the data in the given range from the data range that - will be used by the fitting rountine + """ + Removes the data in the given range from the data range that + will be used by the fitting rountine. Parameters ---------- x1, x2 : None or float + + See Also + -------- + set_signal_range, add_signal_range, reset_signal_range, + hyperspy.model.BaseModel.set_signal_range_from_mask """ indices = self._parse_signal_range_values(x1, x2) self._remove_signal_range_in_pixels(*indices) def reset_signal_range(self): - """Resets the data range""" + """ + Resets the data range + + See Also + -------- + set_signal_range, add_signal_range, remove_signal_range + """ self._set_signal_range_in_pixels() def _add_signal_range_in_pixels(self, i1=None, i2=None): - """Adds the data in the given range from the data range that - will be used by the fitting rountine + """ + Adds the data in the given range from the data range that + will be used by the fitting rountine. Parameters ---------- @@ -527,25 +500,26 @@ def _add_signal_range_in_pixels(self, i1=None, i2=None): """ if i2 is not None: i2 += 1 - self.channel_switches[i1:i2] = True + self._channel_switches[i1:i2] = True self.update_plot() @interactive_range_selector def add_signal_range(self, x1=None, x2=None): - """Adds the data in the given range from the data range that - will be used by the fitting rountine + """ + Adds the data in the given range from the data range that + will be used by the fitting rountine. Parameters ---------- x1, x2 : None or float + + See Also + -------- + set_signal_range, reset_signal_range, remove_signal_range """ indices = self._parse_signal_range_values(x1, x2) self._add_signal_range_in_pixels(*indices) - def reset_the_signal_range(self): - self.channel_switches[:] = True - self.update_plot() - def _check_analytical_jacobian(self): """Check all components have analytical gradients. @@ -572,81 +546,34 @@ def _check_analytical_jacobian(self): def _jacobian(self, param, y, weights=None): if weights is None: - weights = 1. - - if self.convolved is True: - counter = 0 - grad = np.zeros(len(self.axis.axis)) - for component in self: # Cut the parameters list - if component.active: - component.fetch_values_from_array( - param[ - counter:counter + - component._nfree_param], - onlyfree=True) - - if component.convolved: - for parameter in component.free_parameters: - par_grad = np.convolve( - parameter.grad(self.convolution_axis), - self.low_loss(self.axes_manager), - mode="valid") - - if parameter._twins: - for par in parameter._twins: - np.add(par_grad, np.convolve( - par.grad( - self.convolution_axis), - self.low_loss(self.axes_manager), - mode="valid"), par_grad) - - grad = np.vstack((grad, par_grad)) - - else: - for parameter in component.free_parameters: - par_grad = parameter.grad(self.axis.axis) - - if parameter._twins: - for par in parameter._twins: - np.add(par_grad, par.grad(self.axis.axis), par_grad) - - grad = np.vstack((grad, par_grad)) - - counter += component._nfree_param - - to_return = grad[1:, self.channel_switches] * weights + weights = 1.0 - else: - axis = self.axis.axis[self.channel_switches] - counter = 0 - grad = axis - for component in self: # Cut the parameters list - if component.active: - component.fetch_values_from_array( - param[ - counter:counter + - component._nfree_param], - onlyfree=True) - - for parameter in component.free_parameters: - par_grad = parameter.grad(axis) - if parameter._twins: - for par in parameter._twins: - np.add(par_grad, par.grad(axis), par_grad) - - grad = np.vstack((grad, par_grad)) - - counter += component._nfree_param - - to_return = grad[1:, :] * weights - - if is_binned(self.signal): - # in v2 replace by - #if self.signal.axes_manager[-1].is_binned: - if self.signal.axes_manager[-1].is_uniform: - to_return *= self.signal.axes_manager[-1].scale + axis = self.axis.axis[self._channel_switches] + counter = 0 + grad = axis + for component in self: # Cut the parameters list + if component.active: + component.fetch_values_from_array( + param[counter : counter + component._nfree_param], onlyfree=True + ) + + for parameter in component.free_parameters: + par_grad = parameter.grad(axis) + if parameter._twins: + for par in parameter._twins: + np.add(par_grad, par.grad(axis), par_grad) + + grad = np.vstack((grad, par_grad)) + + counter += component._nfree_param + + to_return = grad[1:, :] * weights + + if self.axis.is_binned: + if self.axis.is_uniform: + to_return *= self.axis.scale else: - to_return *= np.gradient(self.signal.axes_manager[-1].axis) + to_return *= np.gradient(self.axis.axis) return to_return @@ -661,7 +588,7 @@ def _poisson_likelihood_function(self, param, y, weights=None): data and parameters """ mf = self._model_function(param) - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): return -(y * np.log(mf) - mf).sum() def _gradient_ml(self, param, y, weights=None): @@ -669,8 +596,7 @@ def _gradient_ml(self, param, y, weights=None): return -(self._jacobian(param, y) * (y / mf - 1)).sum(1) def _gradient_ls(self, param, y, weights=None): - gls = (2 * self._errfunc(param, y, weights) * - self._jacobian(param, y)).sum(1) + gls = (2 * self._errfunc(param, y, weights) * self._jacobian(param, y)).sum(1) return gls def _huber_loss_function(self, param, y, weights=None, huber_delta=None): @@ -694,18 +620,27 @@ def _model2plot(self, axes_manager, out_of_range2nans=True): old_axes_manager = self.axes_manager self.axes_manager = axes_manager self.fetch_stored_values() - s = self.__call__(non_convolved=False, onlyactive=True) + s = self._get_current_data(onlyactive=True) if old_axes_manager is not None: self.axes_manager = old_axes_manager self.fetch_stored_values() if out_of_range2nans is True: ns = np.empty(self.axis.axis.shape) ns.fill(np.nan) - ns[np.where(self.channel_switches)] = s + ns[np.where(self._channel_switches)] = s s = ns return s - def plot(self, plot_components=False, **kwargs): + def _residual_for_plot(self, **kwargs): + """From an model1D object, the original signal is subtracted + by the model signal then returns the residual + """ + + return self.signal._get_current_data() - self._get_current_data( + ignore_channel_switches=True + ) + + def plot(self, plot_components=False, plot_residual=False, **kwargs): """Plot the current spectrum to the screen and a map with a cursor to explore the SI. @@ -713,9 +648,11 @@ def plot(self, plot_components=False, **kwargs): ---------- plot_components : bool If True, add a line per component to the signal figure. + plot_residual : bool + If True, add a residual line (Signal - Model) to the signal figure. **kwargs : dict All extra keyword arguements are passed to - :py:meth:`~._signals.signal1d.Signal1D.plot` + :meth:`~.api.signals.Signal1D.plot` """ # If new coordinates are assigned @@ -723,11 +660,11 @@ def plot(self, plot_components=False, **kwargs): _plot = self.signal._plot l1 = _plot.signal_plot.ax_lines[0] color = l1.line.get_color() - l1.set_line_properties(color=color, type='scatter') + l1.set_line_properties(color=color, type="scatter") l2 = hyperspy.drawing.signal1d.Signal1DLine() l2.data_function = self._model2plot - l2.set_line_properties(color='blue', type='line') + l2.set_line_properties(color="blue", type="line") # Add the line to the figure _plot.signal_plot.add_line(l2) l2.plot() @@ -735,7 +672,21 @@ def plot(self, plot_components=False, **kwargs): self._model_line = l2 self._plot = self.signal._plot + + # Optional to plot the residual of (Signal - Model) + if plot_residual: + l3 = hyperspy.drawing.signal1d.Signal1DLine() + # _residual_for_plot outputs the residual (Signal - Model) + l3.data_function = self._residual_for_plot + l3.set_line_properties(color="green", type="line") + # Add the line to the figure + _plot.signal_plot.add_line(l3) + l3.plot() + # Quick access to _residual_line if needed + self._residual_line = l3 + self._connect_parameters2update_plot(self) + if plot_components is True: self.enable_plot_components() else: @@ -763,8 +714,7 @@ def _disconnect_component_line(component): @staticmethod def _update_component_line(component): if hasattr(component, "_component_line"): - component._component_line.update(render_figure=False, - update_ylimits=False) + component._component_line.update(render_figure=False, update_ylimits=False) def _plot_component(self, component): line = hyperspy.drawing.signal1d.Signal1DLine() @@ -790,10 +740,11 @@ def enable_plot_components(self): if self._plot is None or self._plot_components: # pragma: no cover return self._plot_components = True - for component in [component for component in self if - component.active]: + for component in [component for component in self if component.active]: self._plot_component(component) + enable_plot_components.__doc__ = BaseModel.enable_plot_components.__doc__ + def disable_plot_components(self): self._plot_components = False if self._plot is None: # pragma: no cover @@ -801,30 +752,31 @@ def disable_plot_components(self): for component in self: self._disable_plot_component(component) - def enable_adjust_position( - self, components=None, fix_them=True, show_label=True): + disable_plot_components.__doc__ = BaseModel.disable_plot_components.__doc__ + + def enable_adjust_position(self, components=None, fix_them=True, show_label=True): """Allow changing the *x* position of component by dragging a vertical line that is plotted in the signal model figure Parameters ---------- - components : {None, list of components} + components : None, list of :class:`~.component.Component` If None, the position of all the active components of the model that has a well defined *x* position with a value in the axis range will get a position adjustment line. Otherwise the feature is added only to the given components. The components can be specified by name, index or themselves. - fix_them : bool + fix_them : bool, default True If True the position parameter of the components will be temporarily fixed until adjust position is disable. This can be useful to iteratively adjust the component positions and fit the model. - show_label : bool, optional + show_label : bool, default True If True, a label showing the component name is added to the plot next to the vertical line. - See also + See Also -------- disable_adjust_position @@ -842,21 +794,19 @@ def enable_adjust_position( if not components: # The model does not have components so we do nothing return - components = [ - component for component in components if component.active] + components = [component for component in components if component.active] for component in components: self._make_position_adjuster(component, fix_them, show_label) def _make_position_adjuster(self, component, fix_it, show_label): - if (component._position is None or component._position.twin): + if component._position is None or component._position.twin: return axis = self.axes_manager.signal_axes[0] # Create the vertical line and labels widgets = [VerticalLineWidget(self.axes_manager)] if show_label: label = LabelWidget(self.axes_manager) - label.string = component._get_short_description().replace( - ' component', '') + label.string = component._get_short_description().replace(" component", "") widgets.append(label) self._position_widgets[component._position] = widgets @@ -867,13 +817,13 @@ def _make_position_adjuster(self, component, fix_it, show_label): w.position = (component._position.value,) w.set_mpl_ax(self._plot.signal_plot.ax) # Create widget -> parameter connection - w.events.moved.connect(self._on_widget_moved, {'obj': 'widget'}) + w.events.moved.connect(self._on_widget_moved, {"obj": "widget"}) # Create parameter -> widget connection component._position.events.value_changed.connect( - w._set_position, dict(value='position')) + w._set_position, dict(value="position") + ) # Map relation for close event - w.events.closed.connect(self._on_position_widget_close, - {'obj': 'widget'}) + w.events.closed.connect(self._on_position_widget_close, {"obj": "widget"}) def _reverse_lookup_position_widget(self, widget): for parameter, widgets in self._position_widgets.items(): @@ -899,9 +849,9 @@ def _on_position_widget_close(self, widget): widget.events.moved.disconnect(self._on_widget_moved) def disable_adjust_position(self): - """Disables the interactive adjust position feature + """Disable the interactive adjust position feature - See also + See Also -------- enable_adjust_position @@ -910,45 +860,52 @@ def disable_adjust_position(self): for pws in list(self._position_widgets.values()): # Iteration works on a copied collection, so changes during # iteration should be ok - for pw in reversed(pws): # pws is reference, so work in reverse + for pw in reversed(pws): # pws is reference, so work in reverse pw.close() def fit_component( + self, + component, + signal_range="interactive", + estimate_parameters=True, + fit_independent=False, + only_current=True, + display=True, + toolkit=None, + **kwargs, + ): + component = self._get_component(component) + cf = ComponentFit( self, component, - signal_range="interactive", - estimate_parameters=True, - fit_independent=False, - only_current=True, - display=True, - toolkit=None, - **kwargs): - component = self._get_component(component) - cf = ComponentFit(self, component, signal_range, - estimate_parameters, fit_independent, - only_current, **kwargs) + signal_range, + estimate_parameters, + fit_independent, + only_current, + **kwargs, + ) if signal_range == "interactive": return cf.gui(display=display, toolkit=toolkit) else: cf.apply() - fit_component.__doc__ = \ - """ - Fit just the given component in the given signal range. + + fit_component.__doc__ = """ + Fit the given component in the given signal range. This method is useful to obtain starting parameters for the components. Any keyword arguments are passed to the fit method. Parameters ---------- - component : component instance + component : :class:`~hyperspy.component.Component` The component must be in the model, otherwise an exception is raised. The component can be specified by name, index or itself. - signal_range : {'interactive', (left_value, right_value), None} - If 'interactive' the signal range is selected using the span - selector on the spectrum plot. The signal range can also - be manually specified by passing a tuple of floats. If None - the current signal range is used. Note that ROIs can be used - in place of a tuple. + signal_range : str, tuple of None + If ``'interactive'`` the signal range is selected using the span + selector on the spectrum plot. The signal range can also + be manually specified by passing a tuple of floats (left, right). + If None the current signal range is used. Note that ROIs can be used + in place of a tuple. estimate_parameters : bool, default True If True will check if the component has an estimate_parameters function, and use it to estimate the @@ -968,14 +925,14 @@ def fit_component( -------- Signal range set interactivly - >>> s = hs.signals.Signal1D([0,1,2,4,8,4,2,1,0]) + >>> s = hs.signals.Signal1D([0, 1, 2, 4, 8, 4, 2, 1, 0]) >>> m = s.create_model() >>> g1 = hs.model.components1D.Gaussian() >>> m.append(g1) - >>> m.fit_component(g1) + >>> m.fit_component(g1) # doctest: +SKIP Signal range set through direct input - >>> m.fit_component(g1, signal_range=(1,7)) + >>> m.fit_component(g1, signal_range=(1, 7)) """ % (DISPLAY_DT, TOOLKIT_DT) diff --git a/hyperspy/models/model2d.py b/hyperspy/models/model2d.py index 3a552cfa89..cbb8377074 100644 --- a/hyperspy/models/model2d.py +++ b/hyperspy/models/model2d.py @@ -1,92 +1,88 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . + +import copy import numpy as np -from hyperspy._signals.signal2d import Signal2D -from hyperspy.decorators import interactive_range_selector -from hyperspy.exceptions import WrongObjectError -from hyperspy.model import BaseModel, ModelComponents, ModelSpecialSlicers +from hyperspy.model import BaseModel, ModelComponents +_SIGNAL_RANGE_VALUES = """x1, x2 : None or float + Start and end of the range in the first axis (horizontal) + in units. + y1, y2 : None or float + Start and end of the range in the second axis (vertical) + in units. + """ -class Model2D(BaseModel): - """Model and data fitting for two dimensional signals. +_SIGNAL_RANGE_PIXELS = """i1, i2 : None or float + Start and end of the range in the first axis (horizontal) + in pixels. + j1, j2 : None or float + Start and end of the range in the second axis (vertical) + in pixels. + """ - A model is constructed as a linear combination of :mod:`components2D` that - are added to the model using :meth:`append` or :meth:`extend`. There - are many predifined components available in the in the :mod:`components2D` - module. If needed, new components can be created easily using the code of - existing components as a template. - Once defined, the model can be fitted to the data using :meth:`fit` or - :meth:`multifit`. Once the optimizer reaches the convergence criteria or - the maximum number of iterations the new value of the component parameters - are stored in the components. +class Model2D(BaseModel): + """Model and data fitting for two dimensional signals. + + A model is constructed as a linear combination of + :mod:`~hyperspy.api.model.components2D` that are added to the model using + :meth:`~hyperspy.model.BaseModel.append` or + :meth:`~hyperspy.model.BaseModel.extend`. There are predifined components + available in the :mod:`~hyperspy.api.model.components2D` module + and custom components can made using the :class:`~.api.model.components1D.Expression`. + If needed, new components can be created easily using the code of existing + components as a template. + + Once defined, the model can be fitted to the data using + :meth:`~hyperspy.model.BaseModel.fit` or :meth:`~hyperspy.model.BaseModel.multifit`. + Once the optimizer reaches the convergence criteria or the maximum number + of iterations the new value of the component parameters are stored in the + components. It is possible to access the components in the model by their name or by the index in the model. An example is given at the end of this docstring. - Note that methods are not yet defined for plotting 2D models or using - gradient based optimisation methods - these will be added soon. - - Attributes - ---------- - - signal : Signal2D instance - It contains the data to fit. - chisq : A Signal of floats - Chi-squared of the signal (or np.nan if not yet fit) - dof : A Signal of integers - Degrees of freedom of the signal (0 if not yet fit) - red_chisq : Signal instance - Reduced chi-squared. - components : `ModelComponents` instance - The components of the model are attributes of this class. This provides - a convinient way to access the model components when working in IPython - as it enables tab completion. - Methods ------- + add_signal_range + remove_signal_range + reset_signal_range + set_signal_range - append - Append one component to the model. - extend - Append multiple components to the model. - remove - Remove component from model. - fit, multifit - Fit the model to the data at the current position or the full dataset. + Notes + ----- + Methods are not yet defined for plotting 2D models or using gradient based + optimisation methods. - See also + See Also -------- - Base Model - Model1D - - Example - ------- - - + hyperspy.model.BaseModel, hyperspy.models.model1d.Model1D """ + _signal_dimension = 2 + def __init__(self, signal2D, dictionary=None): - super(Model2D, self).__init__() + super().__init__() self.signal = signal2D self.axes_manager = self.signal.axes_manager self._plot = None @@ -96,107 +92,224 @@ def __init__(self, signal2D, dictionary=None): self._suspend_update = False self._model_line = None self.xaxis, self.yaxis = np.meshgrid( - self.axes_manager.signal_axes[0].axis, - self.axes_manager.signal_axes[1].axis) - self.axes_manager.events.indices_changed.connect( - self._on_navigating, []) - self.channel_switches = np.ones(self.xaxis.shape, dtype=bool) - self.chisq = signal2D._get_navigation_signal() + self.axes_manager.signal_axes[0].axis, self.axes_manager.signal_axes[1].axis + ) + self.axes_manager.events.indices_changed.connect(self._on_navigating, []) + self._channel_switches = np.ones( + self.axes_manager._signal_shape_in_array, dtype=bool + ) + self._chisq = signal2D._get_navigation_signal() self.chisq.change_dtype("float") self.chisq.data.fill(np.nan) self.chisq.metadata.General.title = ( - self.signal.metadata.General.title + ' chi-squared') - self.dof = self.chisq._deepcopy_with_new_data( - np.zeros_like(self.chisq.data, dtype='int')) + self.signal.metadata.General.title + " chi-squared" + ) + self._dof = self.chisq._deepcopy_with_new_data( + np.zeros_like(self.chisq.data, dtype="int") + ) self.dof.metadata.General.title = ( - self.signal.metadata.General.title + ' degrees of freedom') + self.signal.metadata.General.title + " degrees of freedom" + ) self.free_parameters_boundaries = None - self.convolved = False - self.components = ModelComponents(self) + self._components = ModelComponents(self) if dictionary is not None: self._load_dictionary(dictionary) - self.inav = ModelSpecialSlicers(self, True) - self.isig = ModelSpecialSlicers(self, False) self._whitelist = { - 'channel_switches': None, - 'convolved': None, - 'free_parameters_boundaries': None, - 'chisq.data': None, - 'dof.data': None} + "_channel_switches": None, + "free_parameters_boundaries": None, + "chisq.data": None, + "dof.data": None, + } self._slicing_whitelist = { - 'channel_switches': 'isig', - 'chisq.data': 'inav', - 'dof.data': 'inav'} - - @property - def signal(self): - return self._signal - - @signal.setter - def signal(self, value): - if isinstance(value, Signal2D): - self._signal = value - else: - raise WrongObjectError(str(type(value)), 'Signal2D') + "_channel_switches": "isig", + "chisq.data": "inav", + "dof.data": "inav", + } - def __call__(self, non_convolved=True, onlyactive=False): + def _get_current_data(self, onlyactive=False, component_list=None, binned=None): """Returns the corresponding 2D model for the current coordinates Parameters ---------- - only_active : bool - If true, only the active components will be used to build the + onlyactive : bool + If True, only the active components will be used to build the model. + component_list : list or None + If None, the sum of all the components is returned. If list, only + the provided components are returned + binned : None + Not Implemented for Model2D Returns ------- numpy array """ + if component_list is None: + component_list = self + if not isinstance(component_list, (list, tuple)): + raise ValueError("'Component_list' parameter needs to be a list or None.") + + if onlyactive: + component_list = [ + component for component in component_list if component.active + ] sum_ = np.zeros_like(self.xaxis) if onlyactive is True: - for component in self: # Cut the parameters list + for component in component_list: # Cut the parameters list if component.active: - np.add(sum_, component.function(self.xaxis, self.yaxis), - sum_) + np.add(sum_, component.function(self.xaxis, self.yaxis), sum_) else: - for component in self: # Cut the parameters list - np.add(sum_, component.function(self.xaxis, self.yaxis), - sum_) - return sum_[self.channel_switches] + for component in component_list: # Cut the parameters list + np.add(sum_, component.function(self.xaxis, self.yaxis), sum_) + + return sum_[self._channel_switches] def _errfunc(self, param, y, weights=None): if weights is None: - weights = 1. + weights = 1.0 errfunc = self._model_function(param).ravel() - y return errfunc * weights - def _set_signal_range_in_pixels(self, i1=None, i2=None): - raise NotImplementedError + def _set_signal_range_in_pixels( + self, + i1=None, + i2=None, + j1=None, + j2=None, + ): + """ + Use only the selected range defined in pixels in the + fitting routine. - @interactive_range_selector - def set_signal_range(self, x1=None, x2=None): - raise NotImplementedError + Parameters + ---------- + %s + """ + self._backup_channel_switches = copy.copy(self._channel_switches) - def _remove_signal_range_in_pixels(self, i1=None, i2=None): - raise NotImplementedError + self._channel_switches[:, :] = False + if i2 is not None: + i2 += 1 + if j2 is not None: + j2 += 1 + self._channel_switches[slice(i1, i2), slice(j1, j2)] = True + self.update_plot(render_figure=True) - @interactive_range_selector - def remove_signal_range(self, x1=None, x2=None): - raise NotImplementedError + _set_signal_range_in_pixels.__doc__ %= _SIGNAL_RANGE_PIXELS + + def set_signal_range(self, x1=None, x2=None, y1=None, y2=None): + """ + Use only the selected range defined in its own units in the + fitting routine. + + Parameters + ---------- + %s + + See Also + -------- + add_signal_range, remove_signal_range, reset_signal_range, + hyperspy.model.BaseModel.set_signal_range_from_mask + """ + xaxis = self.axes_manager.signal_axes[0] + yaxis = self.axes_manager.signal_axes[1] + i_indices = xaxis.value_range_to_indices(x1, x2) + j_indices = yaxis.value_range_to_indices(y1, y2) + self._set_signal_range_in_pixels(*(i_indices + j_indices)) + + set_signal_range.__doc__ %= _SIGNAL_RANGE_VALUES + + def _remove_signal_range_in_pixels(self, i1=None, i2=None, j1=None, j2=None): + """ + Removes the data in the given range (pixels) from the data + range that will be used by the fitting rountine + + Parameters + ---------- + %s + """ + if i2 is not None: + i2 += 1 + if j2 is not None: + j2 += 1 + self._channel_switches[slice(i1, i2), slice(j1, j2)] = False + self.update_plot() + + _remove_signal_range_in_pixels.__doc__ %= _SIGNAL_RANGE_PIXELS + + def remove_signal_range(self, x1=None, x2=None, y1=None, y2=None): + """ + Removes the data in the given range (calibrated values) from + the data range that will be used by the fitting rountine + + Parameters + ---------- + %s + + See Also + -------- + set_signal_range, add_signal_range, reset_signal_range, + hyperspy.model.BaseModel.set_signal_range_from_mask + """ + xaxis = self.axes_manager.signal_axes[0] + yaxis = self.axes_manager.signal_axes[1] + i_indices = xaxis.value_range_to_indices(x1, x2) + j_indices = yaxis.value_range_to_indices(y1, y2) + self._remove_signal_range_in_pixels(*i_indices, *j_indices) + + remove_signal_range.__doc__ %= _SIGNAL_RANGE_VALUES def reset_signal_range(self): - raise NotImplementedError + """ + Resets the data range. - def _add_signal_range_in_pixels(self, i1=None, i2=None): - raise NotImplementedError + See Also + -------- + set_signal_range, add_signal_range, remove_signal_range, + hyperspy.model.BaseModel.set_signal_range_from_mask + """ + self._set_signal_range_in_pixels() - @interactive_range_selector - def add_signal_range(self, x1=None, x2=None): - raise NotImplementedError + def _add_signal_range_in_pixels(self, i1=None, i2=None, j1=None, j2=None): + """ + Adds the data in the given range from the data range (pixels) + that will be used by the fitting rountine - def reset_the_signal_range(self): - raise NotImplementedError + Parameters + ---------- + %s + """ + if i2 is not None: + i2 += 1 + if j2 is not None: + j2 += 1 + self._channel_switches[slice(i1, i2), slice(j1, j2)] = True + self.update_plot() + + _add_signal_range_in_pixels.__doc__ %= _SIGNAL_RANGE_PIXELS + + def add_signal_range(self, x1=None, x2=None, y1=None, y2=None): + """ + Adds the data in the given range from the data range + (calibrated values) that will be used by the fitting rountine. + + Parameters + ---------- + %s + + See Also + -------- + set_signal_range, reset_signal_range, remove_signal_range, + hyperspy.model.BaseModel.set_signal_range_from_mask + """ + xaxis = self.axes_manager.signal_axes[0] + yaxis = self.axes_manager.signal_axes[1] + i_indices = xaxis.value_range_to_indices(x1, x2) + j_indices = yaxis.value_range_to_indices(y1, y2) + self._add_signal_range_in_pixels(*(i_indices + j_indices)) + + add_signal_range.__doc__ %= _SIGNAL_RANGE_VALUES def _check_analytical_jacobian(self): """Check all components have analytical gradients. @@ -236,14 +349,14 @@ def _model2plot(self, axes_manager, out_of_range2nans=True): old_axes_manager = self.axes_manager self.axes_manager = axes_manager self.fetch_stored_values() - s = self.__call__(non_convolved=False, onlyactive=True) + s = self._get_current_data(onlyactive=True) if old_axes_manager is not None: self.axes_manager = old_axes_manager self.fetch_stored_values() if out_of_range2nans is True: ns = np.empty(self.xaxis.shape) ns.fill(np.nan) - ns[np.where(self.channel_switches)] = s.ravel() + ns[np.where(self._channel_switches)] = s.ravel() s = ns return s @@ -261,8 +374,7 @@ def _disconnect_component_line(component): def _plot_component(self, component): raise NotImplementedError - def enable_adjust_position( - self, components=None, fix_them=True, show_label=True): + def enable_adjust_position(self, components=None, fix_them=True, show_label=True): raise NotImplementedError def disable_adjust_position(self): diff --git a/.github/ISSUE_TEMPLATE.md b/hyperspy/py.typed old mode 100755 new mode 100644 similarity index 100% rename from .github/ISSUE_TEMPLATE.md rename to hyperspy/py.typed diff --git a/hyperspy/roi.py b/hyperspy/roi.py index 2e34a56a64..67c0a96076 100644 --- a/hyperspy/roi.py +++ b/hyperspy/roi.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # # This file is part of HyperSpy. # @@ -10,80 +10,111 @@ # # HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -"""Region of interests (ROIs). - -ROIs operate on `BaseSignal` instances and include widgets for interactive -operation. +""" +Region of interests (ROIs) operate on :py:class:`~.api.signals.BaseSignal` +instances and include widgets for interactive operation. The following 1D ROIs are available: - Point1DROI - Single element ROI of a 1D signal. +.. list-table:: 1D ROIs + :widths: 25 75 - SpanROI - Interval ROI of a 1D signal. + * - :class:`~.api.roi.Point1DROI` + - Single element ROI of a 1D signal + * - :class:`~.api.roi.SpanROI` + - Interval ROI of a 1D signal The following 2D ROIs are available: - Point2DROI - Single element ROI of a 2D signal. - - RectangularROI - Rectagular ROI of a 2D signal. +.. list-table:: 2D ROIs + :widths: 25 75 - CircleROI - (Hollow) circular ROI of a 2D signal - - Line2DROI - Line profile of a 2D signal with customisable width. + * - :class:`~.api.roi.Point2DROI` + - Single element ROI of a 2D signal + * - :class:`~.api.roi.RectangularROI` + - Rectagular ROI of a 2D signal + * - :class:`~.api.roi.CircleROI` + - (Hollow) circular ROI of a 2D signal + * - :class:`~.api.roi.Line2DROI` + - Line profile of a 2D signal with customisable width """ from functools import partial -import traits.api as t import numpy as np +import traits.api as t -from hyperspy.events import Events, Event -from hyperspy.interactive import interactive +import hyperspy.api as hs from hyperspy.axes import UniformDataAxis from hyperspy.drawing import widgets +from hyperspy.events import Event, Events +from hyperspy.interactive import interactive +from hyperspy.misc.utils import is_cupy_array from hyperspy.ui_registry import add_gui_method not_set_error_msg = ( - "Some ROI parameters have not yet been set. " - "Set them before slicing a signal." - ) -class BaseROI(t.HasTraits): + "Some ROI parameters have not yet been set. " "Set them before slicing a signal." +) + +PARSE_AXES_DOCSTRING = """axes : None, str, int or :class:`hyperspy.axes.DataAxis`, default None + The axes argument specifies which axes the ROI will be applied on. + The axes in the collection can be either of the following: + + * Anything that can index the provided ``axes_manager``. + * a tuple or list of: + + - :class:`hyperspy.axes.DataAxis` + - anything that can index the provided ``axes_manager`` + + * ``None``, it will check whether the widget can be added to the + navigator, i.e. if dimensionality matches, and use it if + possible, otherwise it will try the signal space. If none of the + two attempts work, an error message will be raised. +""" + + +class BaseROI(t.HasTraits): """Base class for all ROIs. - Provides some basic functionality that is likely to be shared between all + Provides some basic functionalities that are likely to be shared between all ROIs, and serve as a common type that can be checked for. + + Attributes + ---------- + signal_map : dict + Mapping of ``signal``:(``widget``, ``axes``) to keep track to the signals + (and corresponding widget/signal axes) on which the ROI has been added. + This dictionary is populated in :meth:`BaseInteractiveROI.add_widget` + parameters : dict + Mapping of parameters name and values for all parameters of the ROI. """ def __init__(self): - """Sets up events.changed event, and inits HasTraits. - """ + """Sets up events.changed event, and inits HasTraits.""" super(BaseROI, self).__init__() self.events = Events() - self.events.changed = Event(""" + self.events.changed = Event( + """ Event that triggers when the ROI has changed. What constitues a change varies from ROI to ROI, but in general it - should correspond to the region selected by the ROI has changed. + should correspond to the region selected by the ROI being changed. - Arguments: + Parameters ---------- - roi : - The ROI that was changed. - """, arguments=['roi']) + roi : + The ROI that was changed. + """, + arguments=["roi"], + ) self.signal_map = dict() def __getitem__(self, *args, **kwargs): @@ -129,7 +160,7 @@ def _get_ranges(self): If the ROI is point base or is rectangluar in nature, these can be used to slice a signal. Extracted from - :py:meth:`~hyperspy.roi.BaseROI._make_slices` to ease implementation + :meth:`~hyperspy.roi.BaseROI._make_slices` to ease implementation in inherited ROIs. """ raise NotImplementedError() @@ -168,6 +199,7 @@ def _make_slices(self, axes_collection, axes, ranges=None): slices.append(slice(ilow, ihigh)) else: slices.append(slice(None)) + return tuple(slices) def __call__(self, signal, out=None, axes=None): @@ -181,17 +213,7 @@ def __call__(self, signal, out=None, axes=None): If the 'out' argument is supplied, the sliced output will be put into this instead of returning a Signal. See Signal.__getitem__() for more details on 'out'. - axes : specification of axes to use, default = None - The axes argument specifies which axes the ROI will be applied on. - The items in the collection can be either of the following: - - * a tuple of: - - - DataAxis. These will not be checked with signal.axes_manager. - - anything that will index signal.axes_manager - * For any other value, it will check whether the navigation - space can fit the right number of axis, and use that if it - fits. If not, it will try the signal space. + %s """ if not self.is_valid(): raise ValueError(not_set_error_msg) @@ -206,7 +228,6 @@ def __call__(self, signal, out=None, axes=None): nav_dim = signal.axes_manager.navigation_dimension if True in nav_axes: if False in nav_axes: - slicer = signal.inav[slices[:nav_dim]].isig.__getitem__ slices = slices[nav_dim:] else: @@ -217,71 +238,78 @@ def __call__(self, signal, out=None, axes=None): slices = slices[nav_dim:] roi = slicer(slices, out=out) + return roi + __call__.__doc__ %= PARSE_AXES_DOCSTRING + def _parse_axes(self, axes, axes_manager): """Utility function to parse the 'axes' argument to a list of - :py:class:`~hyperspy.axes.DataAxis`. + :class:`~hyperspy.axes.DataAxis`. Parameters ---------- - axes : specification of axes to use - The axes argument specifies which axes the ROI will be applied on. - The axes in the collection can be either of the following: - - * a tuple of: - - * :py:class:`~hyperspy.axes.DataAxis`. These will not be checked - with signal.axes_manager. - * anything that will index the signal - :py:class:`~hyperspy.axes.AxesManager` - * For any other value, it will check whether the navigation - space can fit the right number of axis, and use that if it - fits. If not, it will try the signal space. - axes_manager : :py:class:`~hyperspy.axes.AxesManager` - The AxesManager to use for parsing axes, if axes is not already a - tuple of DataAxis. + %s + axes_manager : :class:`~hyperspy.axes.AxesManager` + The AxesManager to use for parsing axes Returns ------- - :py:class:`~hyperspy.axes.DataAxis` + tuple of :class:`~hyperspy.axes.DataAxis` """ nd = self.ndim - if isinstance(axes, (tuple, list)): - axes_out = axes_manager[axes[:nd]] - else: + if axes is None: if axes_manager.navigation_dimension >= nd: axes_out = axes_manager.navigation_axes[:nd] elif axes_manager.signal_dimension >= nd: axes_out = axes_manager.signal_axes[:nd] - elif nd == 2 and axes_manager.navigation_dimension == 1 and \ - axes_manager.signal_dimension == 1: + elif ( + nd == 2 + and axes_manager.navigation_dimension == 1 + and axes_manager.signal_dimension == 1 + ): # We probably have a navigator plot including both nav and sig # axes. - axes_out = [axes_manager.signal_axes[0], - axes_manager.navigation_axes[0]] + axes_out = ( + axes_manager.signal_axes[0], + axes_manager.navigation_axes[0], + ) else: raise ValueError("Could not find valid axes configuration.") + else: + if isinstance(axes, (tuple, list)) and len(axes) > nd: + raise ValueError( + "The length of the provided `axes` is larger " + "than the dimensionality of the ROI." + ) + axes_out = axes_manager[axes] + + if not isinstance(axes_out, tuple): + axes_out = (axes_out,) return axes_out + _parse_axes.__doc__ %= PARSE_AXES_DOCSTRING + + def _get_mpl_ax(plot, axes): """ - Returns MPL Axes that contains the `axes`. + Returns matplotlib Axes that contains the hyperspy axis. The space of the first DataAxis in axes will be used to determine which - plot's matplotlib Axes to return. + plot's :class:`matplotlib.axes.Axes` to return. Parameters ---------- plot : MPL_HyperExplorer - The explorer that contains the navigation and signal plots + The explorer that contains the navigation and signal plots. axes : collection of DataAxis The axes to infer from. """ if not plot.is_active: - raise RuntimeError("The signal needs to be plotted before using this " - "function.") + raise RuntimeError( + "The signal needs to be plotted before using this " "function." + ) if axes[0].navigate: ax = plot.navigator_plot.ax @@ -294,10 +322,9 @@ def _get_mpl_ax(plot, axes): class BaseInteractiveROI(BaseROI): - """Base class for interactive ROIs, i.e. ROIs with widget interaction. The base class defines a lot of the common code for interacting with - widgets, but inhertors need to implement the following functions: + widgets, but inheritors need to implement the following functions: _get_widget_type() _apply_roi2widget(widget) @@ -324,8 +351,8 @@ def _update_widgets(self, exclude=None): """Internal function for updating the associated widgets to the geometry contained in the ROI. - Arguments - --------- + Parameters + ---------- exclude : set() A set of widgets to exclude from the update. Useful e.g. if a widget has triggered a change in the ROI: Then all widgets, @@ -366,67 +393,83 @@ def _set_from_widget(self, widget): """ raise NotImplementedError() - def interactive(self, signal, navigation_signal="same", out=None, - color="green", snap=True, **kwargs): + def interactive( + self, + signal, + navigation_signal="same", + out=None, + color="green", + snap=True, + **kwargs, + ): """Creates an interactively sliced Signal (sliced by this ROI) via - hyperspy.interactive. + :func:`~hyperspy.api.interactive`. - Arguments - --------- - signal : Signal - The source signal to slice - navigation_signal : Signal, None or "same" (default) + Parameters + ---------- + signal : hyperspy.api.signals.BaseSignal (or subclass) + The source signal to slice. + navigation_signal : hyperspy.api.signals.BaseSignal (or subclass), None or "same" (default) The signal the ROI will be added to, for navigation purposes only. Only the source signal will be sliced. If not None, it will automatically create a widget on - navigation_signal. Passing "same" is identical to passing the - same signal to 'signal' and 'navigation_signal', but is less + navigation_signal. Passing ``"same"`` is identical to passing the + same signal to ``"signal"`` and ``"navigation_signal"``, but is less ambigous, and allows "same" to be the default value. - out : Signal + out : hyperspy.api.signals.BaseSignal (or subclass) If not None, it will use 'out' as the output instead of returning a new Signal. - color : Matplotlib color specifier (default: 'green') - The color for the widget. Any format that matplotlib uses should - be ok. This will not change the color fo any widget passed with - the 'widget' argument. - snap : bool, optional - If True, the ROI will be snapped to the axes values. Default is - True. + color : matplotlib color, default: ``'green'`` + The color for the widget. Any format that matplotlib uses should be + ok. This will not change the color for any widget passed with the + 'widget' argument. + snap : bool, default True + If True, the ROI will be snapped to the axes values. **kwargs - All kwargs are passed to the roi __call__ method which is called - interactively on any roi parameter change. + All kwargs are passed to the roi ``__call__`` method which is + called interactively on any roi parameter change. + + Returns + ------- + :class:`~hyperspy.api.signals.BaseSignal` (or subclass) + Signal updated with the current ROI selection + when the ROI is changed. """ - if hasattr(signal, '_plot_kwargs'): - kwargs.update({'_plot_kwargs': signal._plot_kwargs}) + if hasattr(signal, "_plot_kwargs"): + kwargs.update({"_plot_kwargs": signal._plot_kwargs}) # in case of complex signal, it is possible to shift the signal # during plotting, if so this is currently not supported and we # raise a NotImplementedError - if signal._plot.signal_data_function_kwargs.get( - 'fft_shift', False): - raise NotImplementedError('ROIs are not supported when data ' - 'are shifted during plotting.') + if signal._plot.signal_data_function_kwargs.get("fft_shift", False): + raise NotImplementedError( + "ROIs are not supported when data " "are shifted during plotting." + ) if isinstance(navigation_signal, str) and navigation_signal == "same": navigation_signal = signal if navigation_signal is not None: if navigation_signal not in self.signal_map: - self.add_widget(navigation_signal, color=color, snap=snap, - axes=kwargs.get("axes", None)) - if (self.update not in - signal.axes_manager.events.any_axis_changed.connected): - signal.axes_manager.events.any_axis_changed.connect( - self.update, - []) + self.add_widget( + navigation_signal, + color=color, + snap=snap, + axes=kwargs.get("axes", None), + ) + if self.update not in signal.axes_manager.events.any_axis_changed.connected: + signal.axes_manager.events.any_axis_changed.connect(self.update, []) if out is None: - return interactive(self.__call__, - event=self.events.changed, - signal=signal, - **kwargs) + return interactive( + self.__call__, event=self.events.changed, signal=signal, **kwargs + ) else: - return interactive(self.__call__, - event=self.events.changed, - signal=signal, out=out, **kwargs) + return interactive( + self.__call__, + event=self.events.changed, + signal=signal, + out=out, + **kwargs, + ) def _on_widget_change(self, widget): """Callback for widgets' 'changed' event. Updates the internal state @@ -444,90 +487,95 @@ def _on_widget_change(self, widget): self._update_widgets(exclude=(widget,)) self.events.changed.trigger(self) - def add_widget(self, signal, axes=None, widget=None, color='green', - snap=True, **kwargs): + def add_widget( + self, signal, axes=None, widget=None, color="green", snap=None, **kwargs + ): """Add a widget to visually represent the ROI, and connect it so any changes in either are reflected in the other. Note that only one widget can be added per signal/axes combination. - Arguments - --------- - signal : Signal + Parameters + ---------- + signal : hyperspy.api.signals.BaseSignal (or subclass) The signal to which the widget is added. This is used to determine which plot to add the widget to, and it supplies the axes_manager for the widget. - axes : specification of axes to use, default = None - The axes argument specifies which axes the ROI will be applied on. - The DataAxis in the collection can be either of the following: - - * a tuple of: - - - DataAxis. These will not be checked with signal.axes_manager. - - anything that will index signal.axes_manager - - * For any other value, it will check whether the navigation - space can fit the right number of axis, and use that if it - fits. If not, it will try the signal space. - - widget : Widget or None (default) + %s + widget : hyperspy widget or None, default None If specified, this is the widget that will be added. If None, the - default widget will be used, as given by _get_widget_type(). - color : Matplotlib color specifier (default: 'green') + default widget will be used. + color : matplotlib color, default ``'green'`` The color for the widget. Any format that matplotlib uses should be - ok. This will not change the color fo any widget passed with the - 'widget' argument. - snap : bool, optional - If True, the ROI will be snapped to the axes values. Default is - True. - kwargs: - All keyword argument are passed to the widget constructor. + ok. This will not change the color for any widget passed with the + ``'widget'`` argument. + snap : bool or None, default None + If True, the ROI will be snapped to the axes values, non-uniform + axes are not supported. If None, it will be disabled (set to + ``False``) for signals containing non-uniform axes. + **kwargs : dict + All keyword arguments are passed to the widget constructor. + + Returns + ------- + hyperspy widget + The widget of the ROI. """ - axes = self._parse_axes(axes, signal.axes_manager,) + axes = self._parse_axes( + axes, + signal.axes_manager, + ) # Undefined if roi initialised without specifying parameters if t.Undefined in tuple(self): self._set_default_values(signal, axes=axes) + if signal._plot is None or signal._plot.signal_plot is None: + raise RuntimeError( + f"{repr(signal)} does not have an active plot. Plot the " + "signal before calling this method." + ) + if widget is None: - widget = self._get_widget_type( - axes, signal)( - signal.axes_manager, **kwargs) + widget = self._get_widget_type(axes, signal)(signal.axes_manager, **kwargs) widget.color = color # Remove existing ROI, if it exists and axes match - if signal in self.signal_map and \ - self.signal_map[signal][1] == axes: + if signal in self.signal_map and self.signal_map[signal][1] == axes: self.remove_widget(signal) + if widget.ax is None: + ax = _get_mpl_ax(signal._plot, axes) + widget.set_mpl_ax(ax) + # Set DataAxes widget.axes = axes with widget.events.changed.suppress_callback(self._on_widget_change): self._apply_roi2widget(widget) + + if snap is None: + if any(not axis.is_uniform for axis in axes): + # Disable snapping for non-uniform axes + snap = False + else: + snap = True + # We need to snap after the widget value have been set - if hasattr(widget, 'snap_all'): + if hasattr(widget, "snap_all"): widget.snap_all = snap else: widget.snap_position = snap - if widget.ax is None: - if signal._plot is None or signal._plot.signal_plot is None: - raise Exception( - f"{repr(signal)} does not have an active plot. Plot the " - "signal before calling this method.") - - ax = _get_mpl_ax(signal._plot, axes) - widget.set_mpl_ax(ax) - # Connect widget changes to on_widget_change - widget.events.changed.connect(self._on_widget_change, - {'obj': 'widget'}) + widget.events.changed.connect(self._on_widget_change, {"obj": "widget"}) # When widget closes, remove from internal list - widget.events.closed.connect(self._remove_widget, {'obj': 'widget'}) + widget.events.closed.connect(self._remove_widget, {"obj": "widget"}) self.widgets.add(widget) self.signal_map[signal] = (widget, axes) return widget + add_widget.__doc__ %= PARSE_AXES_DOCSTRING + def _remove_widget(self, widget, render_figure=True): widget.events.closed.disconnect(self._remove_widget) widget.events.changed.disconnect(self._on_widget_change) @@ -538,37 +586,38 @@ def _remove_widget(self, widget, render_figure=True): break # disconnect events which has been added when if self.update in signal.axes_manager.events.any_axis_changed.connected: - signal.axes_manager.events.any_axis_changed.disconnect( - self.update) + signal.axes_manager.events.any_axis_changed.disconnect(self.update) - def remove_widget(self, signal, render_figure=True): + def remove_widget(self, signal=None, render_figure=True): """ - Removing a widget from a signal consists in two tasks: - 1. Disconnect the interactive operations associated with this ROI - and the specified signal `signal`. - 2. Removing the widget from the plot. + Removing a widget from a signal consists of two tasks: + + 1. Disconnect the interactive operations associated with this ROI + and the specified signal ``signal``. + 2. Removing the widget from the plot. Parameters ---------- - signal : BaseSignal - The signal from the which the interactive operations will be - disconnected. - render_figure : bool, optional + signal : hyperspy.api.signals.BaseSignal (or subclass) + The signal from which the interactive operations will be + disconnected. If None, remove from all signals. + render_figure : bool, default True If False, the figure will not be rendered after removing the widget - in order to save redraw events. The default is True. - - Returns - ------- - None. + in order to save redraw events. """ - if signal in self.signal_map: - w = self.signal_map.pop(signal)[0] - self._remove_widget(w, render_figure) + if signal is None: + signal = list(self.signal_map.keys()) + elif isinstance(signal, hs.signals.BaseSignal): + signal = [signal] + for s in signal: + if s in self.signal_map: + w = self.signal_map.pop(s)[0] + self._remove_widget(w, render_figure) -class BasePointROI(BaseInteractiveROI): +class BasePointROI(BaseInteractiveROI): """Base ROI class for point ROIs, i.e. ROIs with a unit size in each of its dimensions. """ @@ -578,10 +627,10 @@ def __call__(self, signal, out=None, axes=None): axes = self.signal_map[signal][1] else: axes = self._parse_axes(axes, signal.axes_manager) - s = super(BasePointROI, self).__call__(signal=signal, out=out, - axes=axes) + s = super(BasePointROI, self).__call__(signal=signal, out=out, axes=axes) return s + def guess_vertical_or_horizontal(axes, signal): # Figure out whether to use horizontal or vertical line: if axes[0].navigate: @@ -595,27 +644,26 @@ def guess_vertical_or_horizontal(axes, signal): if plotdim == 2: # Plot is an image # axdim == 1 and plotdim == 2 indicates "spectrum stack" - if idx == 0 and axdim != 1: # Axis is horizontal + if idx == 0 and axdim != 1: # Axis is horizontal return "vertical" else: # Axis is vertical return "horizontal" elif plotdim == 1: # It is a spectrum return "vertical" else: - raise ValueError( - "Could not find valid widget type for the given `axes` value") + raise ValueError("Could not find valid widget type for the given `axes` value") + @add_gui_method(toolkey="hyperspy.Point1DROI") class Point1DROI(BasePointROI): - """Selects a single point in a 1D space. The coordinate of the point in the 1D space is stored in the 'value' trait. - `Point1DROI` can be used in place of a tuple containing the value of `value`. + ``Point1DROI`` can be used in place of a tuple containing the value of ``value``. - Example - ------- + Examples + -------- >>> roi = hs.roi.Point1DROI(0.5) >>> value, = roi @@ -623,6 +671,7 @@ class Point1DROI(BasePointROI): 0.5 """ + value = t.CFloat(t.Undefined) _ndim = 1 @@ -635,11 +684,11 @@ def _set_default_values(self, signal, axes=None): if axes is None: axes = self._parse_axes(None, signal.axes_manager) # If roi parameters are undefined, use center of axes - self.value = axes[0]._parse_value('rel0.5') + self.value = axes[0]._parse_value("rel0.5") @property def parameters(self): - return {"value" : self.value} + return {"value": self.value} def _value_changed(self, old, new): self.update() @@ -666,31 +715,29 @@ def _get_widget_type(self, axes, signal): @add_gui_method(toolkey="hyperspy.Point2DROI") class Point2DROI(BasePointROI): - """Selects a single point in a 2D space. The coordinates of the point in - the 2D space are stored in the traits 'x' and 'y'. + the 2D space are stored in the traits ``'x'`` and ``'y'``. - `Point2DROI` can be used in place of a tuple containing the coordinates - of the point `(x, y)`. + ``Point2DROI`` can be used in place of a tuple containing the coordinates + of the point (x, y). - Example - ------- + Examples + -------- >>> roi = hs.roi.Point2DROI(3, 5) >>> x, y = roi >>> print(x, y) - 3 5 + 3.0 5.0 """ + x, y = (t.CFloat(t.Undefined),) * 2 _ndim = 2 def __init__(self, x=None, y=None): super().__init__() - x, y = ( - para if para is not None - else t.Undefined for para in (x, y)) + x, y = (para if para is not None else t.Undefined for para in (x, y)) self.x, self.y = x, y @@ -703,7 +750,7 @@ def _set_default_values(self, signal, axes=None): @property def parameters(self): - return {"x":self.x, "y":self.y} + return {"x": self.x, "y": self.y} def _x_changed(self, old, new): self.update() @@ -712,7 +759,10 @@ def _y_changed(self, old, new): self.update() def _get_ranges(self): - ranges = ((self.x,), (self.y,),) + ranges = ( + (self.x,), + (self.y,), + ) return ranges def _set_from_widget(self, widget): @@ -727,32 +777,32 @@ def _get_widget_type(self, axes, signal): @add_gui_method(toolkey="hyperspy.SpanROI") class SpanROI(BaseInteractiveROI): - """Selects a range in a 1D space. The coordinates of the range in - the 1D space are stored in the traits 'left' and 'right'. + the 1D space are stored in the traits ``'left'`` and ``'right'``. - `SpanROI` can be used in place of a tuple containing the left and right values. + ``SpanROI`` can be used in place of a tuple containing the left and right values. - Example - ------- + Examples + -------- >>> roi = hs.roi.SpanROI(-3, 5) >>> left, right = roi >>> print(left, right) - 3 5 + -3.0 5.0 """ + left, right = (t.CFloat(t.Undefined),) * 2 _ndim = 1 def __init__(self, left=None, right=None): super().__init__() - self._bounds_check = True # Use responsibly! + self._bounds_check = True # Use responsibly! if left is not None and right is not None and left >= right: raise ValueError(f"`left` ({left}) must be smaller than `right` ({right}).") left, right = ( - para if para is not None - else t.Undefined for para in (left, right)) + para if para is not None else t.Undefined for para in (left, right) + ) self.left, self.right = left, right def _set_default_values(self, signal, axes=None): @@ -763,22 +813,19 @@ def _set_default_values(self, signal, axes=None): @property def parameters(self): - return {"left":self.left, "right":self.right} + return {"left": self.left, "right": self.right} def is_valid(self): - return (t.Undefined not in tuple(self) and - self.right >= self.left) + return t.Undefined not in tuple(self) and self.right >= self.left def _right_changed(self, old, new): - if self._bounds_check and \ - self.left is not t.Undefined and new <= self.left: + if self._bounds_check and self.left is not t.Undefined and new <= self.left: self.right = old else: self.update() def _left_changed(self, old, new): - if self._bounds_check and \ - self.right is not t.Undefined and new >= self.right: + if self._bounds_check and self.right is not t.Undefined and new >= self.right: self.left = old else: self.update() @@ -792,7 +839,8 @@ def _set_from_widget(self, widget): self.left, self.right = value def _apply_roi2widget(self, widget): - widget.set_bounds(left=self.left, right=self.right) + if widget.span is not None: + widget._set_span_extents(self.left, self.right) def _get_widget_type(self, axes, signal): direction = guess_vertical_or_horizontal(axes=axes, signal=signal) @@ -804,34 +852,34 @@ def _get_widget_type(self, axes, signal): raise ValueError("direction must be either horizontal or vertical") - @add_gui_method(toolkey="hyperspy.RectangularROI") class RectangularROI(BaseInteractiveROI): - """Selects a range in a 2D space. The coordinates of the range in - the 2D space are stored in the traits 'left', 'right', 'top' and 'bottom'. - Convenience properties 'x', 'y', 'width' and 'height' are also available, + the 2D space are stored in the traits ``'left'``, ``'right'``, ``'top'`` and ``'bottom'``. + Convenience properties ``'x'``, ``'y'``, ``'width'`` and ``'height'`` are also available, but cannot be used for initialization. - `RectangularROI` can be used in place of a tuple containing `(left, right, top, bottom)`. + ``RectangularROI`` can be used in place of a tuple containing (left, right, top, bottom). - Example - ------- + Examples + -------- >>> roi = hs.roi.RectangularROI(left=0, right=10, top=20, bottom=20.5) >>> left, right, top, bottom = roi >>> print(left, right, top, bottom) - 0 10 20 20.5 + 0.0 10.0 20.0 20.5 """ + top, bottom, left, right = (t.CFloat(t.Undefined),) * 4 _ndim = 2 def __init__(self, left=None, top=None, right=None, bottom=None): super(RectangularROI, self).__init__() - left, top, right, bottom = ( - para if para is not None - else t.Undefined for para in (left, top, right, bottom)) - self._bounds_check = True # Use reponsibly! + left, top, right, bottom = ( + para if para is not None else t.Undefined + for para in (left, top, right, bottom) + ) + self._bounds_check = True # Use reponsibly! self.left, self.top, self.right, self.bottom = left, top, right, bottom def __getitem__(self, *args, **kwargs): @@ -854,15 +902,22 @@ def _set_default_values(self, signal, axes=None): @property def parameters(self): - return {"left":self.left, "top":self.top, "right":self.right, "bottom":self.bottom} + return { + "left": self.left, + "top": self.top, + "right": self.right, + "bottom": self.bottom, + } def is_valid(self): - return (not t.Undefined in tuple(self) and - self.right >= self.left and self.bottom >= self.top) + return ( + t.Undefined not in tuple(self) + and self.right >= self.left + and self.bottom >= self.top + ) def _top_changed(self, old, new): - if self._bounds_check and \ - self.bottom is not t.Undefined and new >= self.bottom: + if self._bounds_check and self.bottom is not t.Undefined and new >= self.bottom: self.top = old else: self.update() @@ -932,28 +987,28 @@ def y(self, value): self.update() def _bottom_changed(self, old, new): - if self._bounds_check and \ - self.top is not t.Undefined and new <= self.top: + if self._bounds_check and self.top is not t.Undefined and new <= self.top: self.bottom = old else: self.update() def _right_changed(self, old, new): - if self._bounds_check and \ - self.left is not t.Undefined and new <= self.left: + if self._bounds_check and self.left is not t.Undefined and new <= self.left: self.right = old else: self.update() def _left_changed(self, old, new): - if self._bounds_check and \ - self.right is not t.Undefined and new >= self.right: + if self._bounds_check and self.right is not t.Undefined and new >= self.right: self.left = old else: self.update() def _get_ranges(self): - ranges = ((self.left, self.right), (self.top, self.bottom),) + ranges = ( + (self.left, self.right), + (self.top, self.bottom), + ) return ranges def _set_from_widget(self, widget): @@ -962,8 +1017,9 @@ def _set_from_widget(self, widget): (self.left, self.top), (self.right, self.bottom) = (p, p + s) def _apply_roi2widget(self, widget): - widget.set_bounds(left=self.left, bottom=self.bottom, - right=self.right, top=self.top) + widget.set_bounds( + left=self.left, bottom=self.bottom, right=self.right, top=self.top + ) def _get_widget_type(self, axes, signal): return widgets.RectangleWidget @@ -972,23 +1028,21 @@ def _get_widget_type(self, axes, signal): @add_gui_method(toolkey="hyperspy.CircleROI") class CircleROI(BaseInteractiveROI): """Selects a circular or annular region in a 2D space. The coordinates of - the center of the circle are stored in the 'cx' and 'cy' parameters and the - radius in the `r` parameter. If an internal radius is defined using the - `r_inner` parameter, then an annular region is selected instead. + the center of the circle are stored in the 'cx' and 'cy' attributes. The + radious in the `r` attribute. If an internal radius is defined using the + `r_inner` attribute, then an annular region is selected instead. `CircleROI` can be used in place of a tuple containing `(cx, cy, r)`, `(cx, cy, r, r_inner)` when `r_inner` is not `None`. """ - cx, cy, r, r_inner = (t.CFloat(t.Undefined),) * 3 + (t.CFloat(0.),) + cx, cy, r, r_inner = (t.CFloat(t.Undefined),) * 3 + (t.CFloat(0.0),) _ndim = 2 def __init__(self, cx=None, cy=None, r=None, r_inner=0): super(CircleROI, self).__init__() - cx, cy, r = ( - para if para is not None - else t.Undefined for para in (cx, cy, r)) + cx, cy, r = (para if para is not None else t.Undefined for para in (cx, cy, r)) - self._bounds_check = True # Use reponsibly! + self._bounds_check = True # Use reponsibly! self.cx, self.cy, self.r, self.r_inner = cx, cy, r, r_inner def _set_default_values(self, signal, axes=None): @@ -997,27 +1051,19 @@ def _set_default_values(self, signal, axes=None): ax0, ax1 = axes # If roi parameters are undefined, use center of axes - self.cx = ax0._parse_value('rel0.5') - self.cy = ax1._parse_value('rel0.5') + self.cx = ax0._parse_value("rel0.5") + self.cy = ax1._parse_value("rel0.5") - rx = (ax0.high_value - ax0.low_value)/2 - ry = (ax1.high_value - ax1.low_value)/2 - self.r = min(rx,ry) + rx = (ax0.high_value - ax0.low_value) / 2 + ry = (ax1.high_value - ax1.low_value) / 2 + self.r = min(rx, ry) @property def parameters(self): - return { - "cx":self.cx, - "cy":self.cy, - "r": self.r, - "r_inner": self.r_inner - } + return {"cx": self.cx, "cy": self.cy, "r": self.r, "r_inner": self.r_inner} def is_valid(self): - return ( - t.Undefined not in tuple(self) - and self.r >= self.r_inner - ) + return t.Undefined not in tuple(self) and self.r >= self.r_inner def _cx_changed(self, old, new): self.update() @@ -1032,8 +1078,7 @@ def _r_changed(self, old, new): self.update() def _r_inner_changed(self, old, new): - if self._bounds_check and \ - self.r is not t.Undefined and new >= self.r: + if self._bounds_check and self.r is not t.Undefined and new >= self.r: self.r_inner = old else: self.update() @@ -1053,48 +1098,36 @@ def _get_widget_type(self, axes, signal): return widgets.CircleWidget def __call__(self, signal, out=None, axes=None): - """Slice the signal according to the ROI, and return it. - - Arguments - --------- - signal : Signal - The signal to slice with the ROI. - out : Signal, default = None - If the 'out' argument is supplied, the sliced output will be put - into this instead of returning a Signal. See Signal.__getitem__() - for more details on 'out'. - axes : specification of axes to use, default = None - The axes argument specifies which axes the ROI will be applied on. - The items in the collection can be either of the following: - * a tuple of: - - DataAxis. These will not be checked with - signal.axes_manager. - - anything that will index signal.axes_manager - * For any other value, it will check whether the navigation - space can fit the right number of axis, and use that if it - fits. If not, it will try the signal space. - """ if not self.is_valid(): raise ValueError(not_set_error_msg) + if axes is None and signal in self.signal_map: axes = self.signal_map[signal][1] else: axes = self._parse_axes(axes, signal.axes_manager) + for axis in axes: if not axis.is_uniform: raise NotImplementedError( - "This ROI cannot operate on a non-uniform axis.") + "This ROI cannot operate on a non-uniform axis." + ) natax = signal.axes_manager._get_axes_in_natural_order() # Slice original data with a circumscribed rectangle cx = self.cx + 0.5001 * axes[0].scale cy = self.cy + 0.5001 * axes[1].scale - ranges = [[cx - self.r, cx + self.r], - [cy - self.r, cy + self.r]] + ranges = [[cx - self.r, cx + self.r], [cy - self.r, cy + self.r]] slices = self._make_slices(natax, axes, ranges) - ir = [slices[natax.index(axes[0])], - slices[natax.index(axes[1])]] + ir = [slices[natax.index(axes[0])], slices[natax.index(axes[1])]] + vx = axes[0].axis[ir[0]] - cx vy = axes[1].axis[ir[1]] - cy + + # convert to cupy array when necessary + if is_cupy_array(signal.data): + import cupy as cp + + vx, vy = cp.array(vx), cp.array(vy) + gx, gy = np.meshgrid(vx, vy) gr = gx**2 + gy**2 mask = gr > self.r**2 @@ -1122,7 +1155,6 @@ def __call__(self, signal, out=None, axes=None): nav_dim = signal.axes_manager.navigation_dimension if True in nav_axes: if False in nav_axes: - slicer = signal.inav[slices[:nav_dim]].isig.__getitem__ slices = slices[nav_dim:] else: @@ -1136,13 +1168,11 @@ def __call__(self, signal, out=None, axes=None): roi = out or roi if roi._lazy: import dask.array as da + mask = da.from_array(mask, chunks=chunks) - mask = da.broadcast_to(mask, tiles) - # By default promotes dtype to float if required - roi.data = da.where(mask, np.nan, roi.data) - else: - mask = np.broadcast_to(mask, tiles) - roi.data = np.ma.masked_array(roi.data, mask, hard_mask=True) + mask = np.broadcast_to(mask, tiles) + # roi.data = np.ma.masked_array(roi.data, mask, hard_mask=True) + roi.data = np.where(mask, np.nan, roi.data) if out is None: return roi else: @@ -1157,14 +1187,14 @@ class Line2DROI(BaseInteractiveROI): `Line2DROI` can be used in place of a tuple containing the coordinates of the two end-points of the line and the linewdith `(x1, y1, x2, y2, linewidth)`. """ - x1, y1, x2, y2, linewidth = (t.CFloat(t.Undefined),) * 4 + (t.CFloat(0.),) + x1, y1, x2, y2, linewidth = (t.CFloat(t.Undefined),) * 4 + (t.CFloat(0.0),) _ndim = 2 def __init__(self, x1=None, y1=None, x2=None, y2=None, linewidth=0): super().__init__() x1, y1, x2, y2 = ( - para if para is not None - else t.Undefined for para in (x1, y1, x2, y2)) + para if para is not None else t.Undefined for para in (x1, y1, x2, y2) + ) self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 self.linewidth = linewidth @@ -1178,7 +1208,13 @@ def _set_default_values(self, signal, axes=None): @property def parameters(self): - return {"x1":self.x1, "y1":self.y1, "x2":self.x2, "y2":self.y2, "linewidth":self.linewidth} + return { + "x1": self.x1, + "y1": self.y1, + "x2": self.x2, + "y2": self.y2, + "linewidth": self.linewidth, + } def _x1_changed(self, old, new): self.update() @@ -1268,8 +1304,8 @@ def length(self): d_row, d_col = p1 - p0 return np.hypot(d_row, d_col) - def angle(self, axis='horizontal', units='degrees'): - """"Angle between ROI line and selected axis + def angle(self, axis="horizontal", units="degrees"): + """ "Angle between ROI line and selected axis Parameters ---------- @@ -1287,77 +1323,65 @@ def angle(self, axis='horizontal', units='degrees'): Examples -------- - >>> import hyperspy.api as hs - >>> hs.roi.Line2DROI(0., 0., 1., 2., 1) - >>> r.angle() + >>> r = hs.roi.Line2DROI(0., 0., 1., 2.) + >>> print(r.angle()) 63.43494882292201 """ x = self.x2 - self.x1 y = self.y2 - self.y1 - if units == 'degrees': - conversation = 180. / np.pi - elif units == 'radians': - conversation = 1. + if units == "degrees": + conversation = 180.0 / np.pi + elif units == "radians": + conversation = 1.0 else: raise ValueError( - "Units are not recognized. Use either 'degrees' or 'radians'.") + "Units are not recognized. Use either 'degrees' or 'radians'." + ) - if axis == 'horizontal': + if axis == "horizontal": return np.arctan2(y, x) * conversation - elif axis == 'vertical': + elif axis == "vertical": return np.arctan2(x, y) * conversation else: - raise ValueError("Axis is not recognized. " - "Use either 'horizontal' or 'vertical'.") + raise ValueError( + "Axis is not recognized. " "Use either 'horizontal' or 'vertical'." + ) @staticmethod - def profile_line(img, src, dst, axes, linewidth=1, - order=1, mode='constant', cval=0.0): + def profile_line( + img, src, dst, axes, linewidth=1, order=1, mode="constant", cval=0.0 + ): """Return the intensity profile of an image measured along a scan line. Parameters ---------- - img : numeric array, shape (M, N[, C]) + img : numpy.ndarray The image, either grayscale (2D array) or multichannel (3D array, where the final axis contains the channel information). - src : 2-tuple of numeric scalar (float or int) - The start point of the scan line. - dst : 2-tuple of numeric scalar (float or int) - The end point of the scan line. + src : tuple of float, tuple of int + The start point of the scan line. Length of tuple is 2. + dst : tuple of float, tuple of int + The end point of the scan line. Length of tuple is 2. linewidth : int, optional Width of the scan, perpendicular to the line - order : int in {0, 1, 2, 3, 4, 5}, optional + order : {0, 1, 2, 3, 4, 5}, optional The order of the spline interpolation to compute image values at non-integer coordinates. 0 means nearest-neighbor interpolation. - mode : string, one of {'constant', 'nearest', 'reflect', 'wrap'}, - optional + mode : {'constant', 'nearest', 'reflect', 'wrap'}, optional How to compute any values falling outside of the image. cval : float, optional - If `mode` is 'constant', what constant value to use outside the + If ``mode='constant'``, what constant value to use outside the image. Returns ------- - return_value : array + numpy.ndarray The intensity profile along the scan line. The length of the profile is the ceil of the computed length of the scan line. - Examples - -------- - >>> x = np.array([[1, 1, 1, 2, 2, 2]]) - >>> img = np.vstack([np.zeros_like(x), x, x, x, np.zeros_like(x)]) - >>> img - array([[0, 0, 0, 0, 0, 0], - [1, 1, 1, 2, 2, 2], - [1, 1, 1, 2, 2, 2], - [1, 1, 1, 2, 2, 2], - [0, 0, 0, 0, 0, 0]]) - >>> profile_line(img, (2, 1), (2, 4)) - array([ 1., 1., 2., 2.]) - Notes ----- The destination point is included in the profile, in contrast to @@ -1367,22 +1391,27 @@ def profile_line(img, src, dst, axes, linewidth=1, for axis in axes: if not axis.is_uniform: raise NotImplementedError( - "Line profiles on data with non-uniform axes is not implemented.") + "Line profiles on data with non-uniform axes is not implemented." + ) import scipy.ndimage as nd + # Convert points coordinates from axes units to pixels - p0 = ((src[0] - axes[0].offset) / axes[0].scale, - (src[1] - axes[1].offset) / axes[1].scale) - p1 = ((dst[0] - axes[0].offset) / axes[0].scale, - (dst[1] - axes[1].offset) / axes[1].scale) + p0 = ( + (src[0] - axes[0].offset) / axes[0].scale, + (src[1] - axes[1].offset) / axes[1].scale, + ) + p1 = ( + (dst[0] - axes[0].offset) / axes[0].scale, + (dst[1] - axes[1].offset) / axes[1].scale, + ) if linewidth < 0: raise ValueError("linewidth must be positive number") linewidth_px = linewidth / np.min([ax.scale for ax in axes]) linewidth_px = int(round(linewidth_px)) # Minimum size 1 pixel linewidth_px = linewidth_px if linewidth_px >= 1 else 1 - perp_lines = Line2DROI._line_profile_coordinates(p0, p1, - linewidth=linewidth_px) + perp_lines = Line2DROI._line_profile_coordinates(p0, p1, linewidth=linewidth_px) if img.ndim > 2: idx = [ax.index_in_array for ax in axes] if idx[0] < idx[1]: @@ -1392,21 +1421,25 @@ def profile_line(img, src, dst, axes, linewidth=1, img = np.rollaxis(img, idx[1], 0) img = np.rollaxis(img, idx[0], 0) orig_shape = img.shape - img = np.reshape(img, orig_shape[0:2] + - (np.product(orig_shape[2:]),)) - pixels = [nd.map_coordinates(img[..., i].T, perp_lines, - order=order, mode=mode, cval=cval) - for i in range(img.shape[2])] + img = np.reshape(img, orig_shape[0:2] + (np.prod(orig_shape[2:]),)) + pixels = [ + nd.map_coordinates( + img[..., i].T, perp_lines, order=order, mode=mode, cval=cval + ) + for i in range(img.shape[2]) + ] i0 = min(axes[0].index_in_array, axes[1].index_in_array) pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) intensities = pixels.mean(axis=1) intensities = np.rollaxis( - np.reshape(intensities, - intensities.shape[0:1] + orig_shape[2:]), - 0, i0 + 1) + np.reshape(intensities, intensities.shape[0:1] + orig_shape[2:]), + 0, + i0 + 1, + ) else: - pixels = nd.map_coordinates(img, perp_lines, - order=order, mode=mode, cval=cval) + pixels = nd.map_coordinates( + img, perp_lines, order=order, mode=mode, cval=cval + ) intensities = pixels.mean(axis=1) return intensities @@ -1414,24 +1447,15 @@ def profile_line(img, src, dst, axes, linewidth=1, def __call__(self, signal, out=None, axes=None, order=0): """Slice the signal according to the ROI, and return it. - Arguments - --------- + Parameters + ---------- signal : Signal The signal to slice with the ROI. out : Signal, default = None If the 'out' argument is supplied, the sliced output will be put into this instead of returning a Signal. See Signal.__getitem__() for more details on 'out'. - axes : specification of axes to use, default = None - The axes argument specifies which axes the ROI will be applied on. - The items in the collection can be either of the following: - * a tuple of: - - DataAxis. These will not be checked with - signal.axes_manager. - - anything that will index signal.axes_manager - * For any other value, it will check whether the navigation - space can fit the right number of axis, and use that if it - fits. If not, it will try the signal space. + %s order : The spline interpolation order to use when extracting the line profile. 0 means nearest-neighbor interpolation, and is both the default and the fastest. @@ -1442,31 +1466,37 @@ def __call__(self, signal, out=None, axes=None, order=0): axes = self.signal_map[signal][1] else: axes = self._parse_axes(axes, signal.axes_manager) - profile = Line2DROI.profile_line(signal.data, - (self.x1, self.y1), - (self.x2, self.y2), - axes=axes, - linewidth=self.linewidth, - order=order) - length = np.linalg.norm(np.diff( - np.array(((self.x1, self.y1), (self.x2, self.y2))), axis=0), - axis=1)[0] + profile = Line2DROI.profile_line( + signal.data, + (self.x1, self.y1), + (self.x2, self.y2), + axes=axes, + linewidth=self.linewidth, + order=order, + ) + length = np.linalg.norm( + np.diff(np.array(((self.x1, self.y1), (self.x2, self.y2))), axis=0), axis=1 + )[0] if out is None: axm = signal.axes_manager.deepcopy() i0 = min(axes[0].index_in_array, axes[1].index_in_array) axm.remove([ax.index_in_array + 3j for ax in axes]) - axis = UniformDataAxis(size=profile.shape[i0], - scale=length / profile.shape[i0], - units=axes[0].units, - navigate=axes[0].navigate) + axis = UniformDataAxis( + size=profile.shape[i0], + scale=length / profile.shape[i0], + units=axes[0].units, + navigate=axes[0].navigate, + ) axis.axes_manager = axm axm._axes.insert(i0, axis) from hyperspy.signals import BaseSignal - roi = BaseSignal(profile, axes=axm._get_axes_dicts(), - metadata=signal.metadata.deepcopy( - ).as_dictionary(), - original_metadata=signal.original_metadata. - deepcopy().as_dictionary()) + + roi = BaseSignal( + profile, + axes=axm._get_axes_dicts(), + metadata=signal.metadata.deepcopy().as_dictionary(), + original_metadata=signal.original_metadata.deepcopy().as_dictionary(), + ) return roi else: out.data = profile diff --git a/hyperspy/samfire.py b/hyperspy/samfire.py index 927981480a..ece8aa1db2 100644 --- a/hyperspy/samfire.py +++ b/hyperspy/samfire.py @@ -1,43 +1,39 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging from multiprocessing import cpu_count -import dill +import cloudpickle import numpy as np -from hyperspy.misc.utils import DictionaryTreeBrowser -from hyperspy.misc.utils import slugify -from hyperspy.misc.math_tools import check_random_state from hyperspy.external.progressbar import progressbar -from hyperspy.signal import BaseSignal -from hyperspy.samfire_utils.strategy import (LocalStrategy, - GlobalStrategy) -from hyperspy.samfire_utils.local_strategies import ReducedChiSquaredStrategy +from hyperspy.misc.math_tools import check_random_state +from hyperspy.misc.utils import DictionaryTreeBrowser, slugify from hyperspy.samfire_utils.global_strategies import HistogramStrategy - +from hyperspy.samfire_utils.local_strategies import ReducedChiSquaredStrategy +from hyperspy.samfire_utils.strategy import GlobalStrategy, LocalStrategy +from hyperspy.signal import BaseSignal _logger = logging.getLogger(__name__) class StrategyList(list): - def __init__(self, samf): super(StrategyList, self).__init__() self.samf = samf @@ -57,21 +53,20 @@ def remove(self, thing): list.remove(self, thing) def __repr__(self): - signature = u"%3s | %4s | %s" + signature = "%3s | %4s | %s" ans = signature % ("A", "#", "Strategy") - ans += u"\n" - ans += signature % (u'-' * 2, u'-' * 4, u'-' * 25) + ans += "\n" + ans += signature % ("-" * 2, "-" * 4, "-" * 25) if self: for n, s in enumerate(self): - ans += u"\n" + ans += "\n" name = repr(s) - a = u" x" if self.samf._active_strategy_ind == n else u"" + a = " x" if self.samf._active_strategy_ind == n else "" ans += signature % (a, str(n), name) return ans class Samfire: - """Smart Adaptive Multidimensional Fitting (SAMFire) object SAMFire is a more robust way of fitting multidimensional datasets. By @@ -85,16 +80,16 @@ class Samfire: Attributes ---------- - model : Model instance + model : :class:`hyperspy.model.BaseModel` (or subclass) The complete model optional_components : list A list of components that can be switched off at some pixels if it returns a better Akaike's Information Criterion with correction (AICc) workers : int A number of processes that will perform the fitting parallely - pool : samfire_pool instance + pool : :class:`~.api.samfire.SamfirePool` A proxy object that manages either multiprocessing or ipyparallel pool - strategies : strategy list + strategies : list A list of strategies that will be used to select pixel fitting order and calculate required starting parameters. Strategies come in two "flavours" - local and global. Local strategies spread the starting @@ -102,7 +97,7 @@ class Samfire: Global strategies look for clusters in parameter values, and suggests most frequent values. Global strategy do not depend on pixel fitting order, hence it is randomised. - metadata : dictionary + metadata : dict A dictionary for important samfire parameters active_strategy : strategy The currently active strategy from the strategies list @@ -115,7 +110,7 @@ class Samfire: save_every : int When running, samfire saves results every time save_every good fits are found. - random_state : None or int or RandomState instance, default None + random_state : None or int or numpy.random.Generator, default None Random seed used to select the next pixels. """ @@ -137,7 +132,7 @@ def __init__(self, model, workers=None, setup=True, random_state=None, **kwargs) if workers is None: workers = max(1, cpu_count() - 1) self.model = model - self.metadata = DictionaryTreeBrowser() + self._metadata = DictionaryTreeBrowser() self._scale = 1.0 # -1 -> done pixel, use @@ -145,8 +140,8 @@ def __init__(self, model, workers=None, setup=True, random_state=None, **kwargs) # 0 -> bad fit/no info # >0 -> select when turn comes - self.metadata.add_node('marker') - self.metadata.add_node('goodness_test') + self.metadata.add_node("marker") + self.metadata.add_node("goodness_test") marker = np.empty(self.model.axes_manager.navigation_shape[::-1]) marker.fill(self._scale) @@ -158,9 +153,11 @@ def __init__(self, model, workers=None, setup=True, random_state=None, **kwargs) self._active_strategy_ind = 0 self.update_every = max(10, workers * 2) # some sensible number.... from hyperspy.samfire_utils.fit_tests import red_chisq_test + self.metadata.goodness_test = red_chisq_test(tolerance=1.0) self.metadata._gt_dump = None from hyperspy.samfire_utils.samfire_kernel import single_kernel + self.single_kernel = single_kernel self._workers = workers if len(kwargs) or setup: @@ -168,6 +165,10 @@ def __init__(self, model, workers=None, setup=True, random_state=None, **kwargs) self.refresh_database() self.random_state = check_random_state(random_state) + @property + def metadata(self): + return self._metadata + @property def active_strategy(self): return self.strategies[self._active_strategy_ind] @@ -179,19 +180,20 @@ def active_strategy(self, value): def _setup(self, **kwargs): """Set up SAMFire - configure models, set up pool if necessary""" from hyperspy.samfire_utils.samfire_pool import SamfirePool + self._figure = None - self.metadata._gt_dump = dill.dumps(self.metadata.goodness_test) + self.metadata._gt_dump = cloudpickle.dumps(self.metadata.goodness_test) self._enable_optional_components() - if hasattr(self.model, '_suspend_auto_fine_structure_width'): + if hasattr(self.model, "_suspend_auto_fine_structure_width"): self.model._suspend_auto_fine_structure_width = True - if hasattr(self, '_log'): + if hasattr(self, "_log"): self._log = [] if self._workers and self.pool is None: - if 'num_workers' not in kwargs: - kwargs['num_workers'] = self._workers + if "num_workers" not in kwargs: + kwargs["num_workers"] = self._workers if self.pool is None: self.pool = SamfirePool(**kwargs) self._workers = self.pool.num_workers @@ -204,16 +206,15 @@ def start(self, **kwargs): ---------- **kwargs : dict Any keyword arguments to be passed to - :py:meth:`~.model.BaseModel.fit` + :meth:`~.model.BaseModel.fit` """ self._setup() if self._workers and self.pool is not None: self.pool.update_parameters() - if 'min_function' in kwargs: - kwargs['min_function'] = dill.dumps(kwargs['min_function']) - if 'min_function_grad' in kwargs: - kwargs['min_function_grad'] = dill.dumps( - kwargs['min_function_grad']) + if "min_function" in kwargs: + kwargs["min_function"] = cloudpickle.dumps(kwargs["min_function"]) + if "min_function_grad" in kwargs: + kwargs["min_function_grad"] = cloudpickle.dumps(kwargs["min_function_grad"]) self._args = kwargs num_of_strat = len(self.strategies) total_size = self.model.axes_manager.navigation_size - self.pixels_done @@ -231,8 +232,7 @@ def start(self, **kwargs): self.change_strategy(self._active_strategy_ind + 1) except KeyboardInterrupt: # pragma: no cover if self.pool is not None: - _logger.warning( - 'Collecting already started pixels, please wait') + _logger.warning("Collecting already started pixels, please wait") self.pool.collect_results() def append(self, strategy): @@ -240,7 +240,8 @@ def append(self, strategy): Parameters ---------- - strategy : strategy instance + strategy : strategy + The samfire strategy to use """ self.strategies.append(strategy) @@ -249,16 +250,17 @@ def extend(self, iterable): Parameters ---------- - iterable : an iterable of strategy instances + iterable : iterable of strategy + The samfire strategies to use. """ self.strategies.extend(iterable) def remove(self, thing): - """removes given strategy from the strategies list + """Remove given strategy from the strategies list Parameters ---------- - thing : int or strategy instance + thing : int or strategy Strategy that is in current strategies list or its index. """ self.strategies.remove(thing) @@ -269,7 +271,7 @@ def _active_strategy_ind(self): @_active_strategy_ind.setter def _active_strategy_ind(self, value): - self.__active_strategy_ind = np.abs(int(value)) + self.__active_strategy_ind = abs(int(value)) def _run_active_strategy(self): if self.pool is not None: @@ -283,7 +285,7 @@ def pixels_left(self): """Returns the number of pixels that are left to solve. This number can increase as SAMFire learns more information about the data. """ - return np.sum(self.metadata.marker > 0.) + return np.sum(self.metadata.marker > 0.0) @property def pixels_done(self): @@ -296,12 +298,14 @@ def _run_active_strategy_one(self): ind = self._next_pixels(1)[0] vals = self.active_strategy.values(ind) self.running_pixels.append(ind) - isgood = self.single_kernel(self.model, - ind, - vals, - self.optional_components, - self._args, - self.metadata.goodness_test) + isgood = self.single_kernel( + self.model, + ind, + vals, + self.optional_components, + self._args, + self.metadata.goodness_test, + ) self.running_pixels.remove(ind) self.count += 1 if isgood: @@ -315,20 +319,19 @@ def backup(self, filename=None, on_count=True): Parameters ---------- - filename : {str, None} + filename : str, None, default None the filename. If None, a default value of ``backup_`` + signal_title is used. - on_count : bool - if True (default), only saves on the required count of steps + on_count : bool, default True + if True, only saves on the required count of steps """ if filename is None: title = self.model.signal.metadata.General.title - filename = slugify('backup_' + title) + filename = slugify("backup_" + title) # maybe add saving marker + strategies as well? if self.count % self.save_every == 0 or not on_count: - self.model.save(filename, - name='samfire_backup', overwrite=True) - self.model.signal.models.remove('samfire_backup') + self.model.save(filename, name="samfire_backup", overwrite=True) + self.model.signal.models.remove("samfire_backup") def update(self, ind, results=None, isgood=None): """Updates the current model with the results, received from the @@ -338,12 +341,12 @@ def update(self, ind, results=None, isgood=None): ---------- ind : tuple contains the index of the pixel of the results - results : {dict, None} + results : dict or None, default None dictionary of the results. If None, means we are updating in-place - (e.g. refreshing the marker or strategies) - isgood : {bool, None} + (e.g. refreshing the marker or strategies). + isgood : bool or None, default None if it is known if the results are good according to the - goodness-of-fit test. If None, the pixel is tested + goodness-of-fit test. If None, the pixel is tested. """ if results is not None and (isgood is None or isgood): self._swap_dict_and_model(ind, results) @@ -370,8 +373,8 @@ def refresh_database(self): calculated_pixels = np.logical_not(np.isnan(self.model.red_chisq.data)) # only include pixels that are good enough calculated_pixels = self.metadata.goodness_test.map( - self.model, - calculated_pixels) + self.model, calculated_pixels + ) self.active_strategy.refresh(True, calculated_pixels) @@ -383,19 +386,19 @@ def change_strategy(self, new_strat): Parameters ---------- - new_strat : {int | strategy} + new_strat : int or strategy index of the new strategy from the strategies list or the strategy object itself """ from numbers import Number + if not isinstance(new_strat, Number): try: new_strat = self.strategies.index(new_strat) except ValueError: - raise ValueError( - "The passed object is not in current strategies list") + raise ValueError("The passed object is not in current strategies list") - new_strat = np.abs(int(new_strat)) + new_strat = abs(int(new_strat)) if new_strat == self._active_strategy_ind: self.refresh_database() @@ -403,23 +406,21 @@ def change_strategy(self, new_strat): # TODO: make sure it's a number. Get index if object is passed? if new_strat >= len(self.strategies): - raise ValueError('too big new strategy index') + raise ValueError("too big new strategy index") current = self.active_strategy new = self.strategies[new_strat] - if isinstance(current, LocalStrategy) and isinstance( - new, LocalStrategy): + if isinstance(current, LocalStrategy) and isinstance(new, LocalStrategy): # forget ignore/done levels, keep just calculated or not new.refresh(True) else: - if isinstance(current, LocalStrategy) and isinstance( - new, GlobalStrategy): + if isinstance(current, LocalStrategy) and isinstance(new, GlobalStrategy): # if diffusion->segmenter, set previous -1 to -2 (ignored for # the next diffusion) - self.metadata.marker[ - self.metadata.marker == - - self._scale] -= self._scale + self.metadata.marker[self.metadata.marker == -self._scale] -= ( + self._scale + ) new.refresh(False) current.clean() @@ -433,7 +434,7 @@ def generate_values(self, need_inds): Parameters ---------- - need_inds: int + need_inds : int the number of pixels to be returned in the generator """ if need_inds: @@ -441,27 +442,27 @@ def generate_values(self, need_inds): for ind in self._next_pixels(need_inds): # get starting parameters / array of possible values value_dict = self.active_strategy.values(ind) - value_dict['fitting_kwargs'] = self._args - value_dict['signal.data'] = \ - self.model.signal.data[ind + (...,)] + value_dict["fitting_kwargs"] = self._args + value_dict["signal.data"] = self.model.signal.data[ind + (...,)] if self.model.signal._lazy: - value_dict['signal.data'] = value_dict[ - 'signal.data'].compute() + value_dict["signal.data"] = value_dict["signal.data"].compute() if self.model.signal.metadata.has_item( - 'Signal.Noise_properties.variance'): + "Signal.Noise_properties.variance" + ): var = self.model.signal.metadata.Signal.Noise_properties.variance if isinstance(var, BaseSignal): dat = var.data[ind + (...,)] - value_dict['variance.data'] = dat.compute( - ) if var._lazy else dat - if hasattr(self.model, - 'low_loss') and self.model.low_loss is not None: + value_dict["variance.data"] = ( + dat.compute() if var._lazy else dat + ) + if hasattr(self.model, "low_loss") and self.model.low_loss is not None: dat = self.model.low_loss.data[ind + (...,)] - value_dict['low_loss.data'] = dat.compute( - ) if self.model.low_loss._lazy else dat + value_dict["low_loss.data"] = ( + dat.compute() if self.model.low_loss._lazy else dat + ) self.running_pixels.append(ind) - self.metadata.marker[ind] = 0. + self.metadata.marker[ind] = 0.0 yield ind, value_dict def _next_pixels(self, number): @@ -470,7 +471,7 @@ def _next_pixels(self, number): if best > 0.0: ind_list = np.where(self.metadata.marker == best) while number and ind_list[0].size > 0: - i = self.random_state.randint(len(ind_list[0])) + i = self.random_state.integers(len(ind_list[0])) ind = tuple([lst[i] for lst in ind_list]) if ind not in self.running_pixels: inds.append(ind) @@ -481,14 +482,16 @@ def _next_pixels(self, number): def _swap_dict_and_model(self, m_ind, dict_, d_ind=None): if d_ind is None: - d_ind = tuple([0 for _ in dict_['dof.data'].shape]) + d_ind = tuple([0 for _ in dict_["dof.data"].shape]) m = self.model for k in dict_.keys(): - if k.endswith('.data'): + if k.endswith(".data"): item = k[:-5] - getattr(m, item).data[m_ind], dict_[k] = \ - dict_[k].copy(), getattr(m, item).data[m_ind].copy() - for comp_name, comp in dict_['components'].items(): + getattr(m, item).data[m_ind], dict_[k] = ( + dict_[k].copy(), + getattr(m, item).data[m_ind].copy(), + ) + for comp_name, comp in dict_["components"].items(): # only active components are sent if self.model[comp_name].active_is_multidimensional: self.model[comp_name]._active_array[m_ind] = True @@ -496,12 +499,14 @@ def _swap_dict_and_model(self, m_ind, dict_, d_ind=None): for param_model in self.model[comp_name].parameters: param_dict = comp[param_model.name] - param_model.map[m_ind], param_dict[d_ind] = \ - param_dict[d_ind].copy(), param_model.map[m_ind].copy() + param_model.map[m_ind], param_dict[d_ind] = ( + param_dict[d_ind].copy(), + param_model.map[m_ind].copy(), + ) for component in self.model: # switch off all that did not appear in the dictionary - if component.name not in dict_['components'].keys(): + if component.name not in dict_["components"].keys(): if component.active_is_multidimensional: component._active_array[m_ind] = False @@ -512,8 +517,7 @@ def _enable_optional_components(self): comp = self.model._get_component(c) if not comp.active_is_multidimensional: comp.active_is_multidimensional = True - if not np.all([isinstance(a, int) for a in - self.optional_components]): + if not np.all([isinstance(a, int) for a in self.optional_components]): new_list = [] for op in self.optional_components: for ic, c in enumerate(self.model): @@ -522,11 +526,14 @@ def _enable_optional_components(self): self.optional_components = new_list def _request_user_input(self): - from hyperspy.signals import Image from hyperspy.drawing.widgets import SquareWidget - mark = Image(self.metadata.marker, - axes=self.model.axes_manager._get_navigation_axes_dicts()) - mark.metadata.General.title = 'SAMFire marker' + from hyperspy.signals import Image + + mark = Image( + self.metadata.marker, + axes=self.model.axes_manager._get_navigation_axes_dicts(), + ) + mark.metadata.General.title = "SAMFire marker" def update_when_triggered(): ind = self.model.axes_manager.indices[::-1] @@ -537,40 +544,49 @@ def update_when_triggered(): self.model.plot() self.model.events.fitted.connect(update_when_triggered, []) self.model._plot.signal_plot.events.closed.connect( - lambda: self.model.events.fitted.disconnect(update_when_triggered), - []) + lambda: self.model.events.fitted.disconnect(update_when_triggered), [] + ) - mark.plot(navigator='slider') + mark.plot(navigator="slider") w = SquareWidget(self.model.axes_manager) - w.color = 'yellow' + w.color = "yellow" w.set_mpl_ax(mark._plot.signal_plot.ax) w.connect_navigate() def connect_other_navigation1(axes_manager): with mark.axes_manager.events.indices_changed.suppress_callback( - connect_other_navigation2): - for ax1, ax2 in zip(mark.axes_manager.navigation_axes, - axes_manager.navigation_axes[2:]): + connect_other_navigation2 + ): + for ax1, ax2 in zip( + mark.axes_manager.navigation_axes, axes_manager.navigation_axes[2:] + ): ax1.value = ax2.value def connect_other_navigation2(axes_manager): with self.model.axes_manager.events.indices_changed.suppress_callback( - connect_other_navigation1): - for ax1, ax2 in zip(self.model.axes_manager.navigation_axes[2:], - axes_manager.navigation_axes): + connect_other_navigation1 + ): + for ax1, ax2 in zip( + self.model.axes_manager.navigation_axes[2:], + axes_manager.navigation_axes, + ): ax1.value = ax2.value mark.axes_manager.events.indices_changed.connect( - connect_other_navigation2, {'obj': 'axes_manager'}) + connect_other_navigation2, {"obj": "axes_manager"} + ) self.model.axes_manager.events.indices_changed.connect( - connect_other_navigation1, {'obj': 'axes_manager'}) + connect_other_navigation1, {"obj": "axes_manager"} + ) - self.model._plot.signal_plot.events.closed.connect( - lambda: mark._plot.close, []) + self.model._plot.signal_plot.events.closed.connect(lambda: mark._plot.close, []) self.model._plot.signal_plot.events.closed.connect( lambda: self.model.axes_manager.events.indices_changed.disconnect( - connect_other_navigation1), []) + connect_other_navigation1 + ), + [], + ) def plot(self, on_count=False): """If possible, plot current strategy plot. Local strategies plot @@ -594,15 +610,14 @@ def plot(self, on_count=False): self._figure = self.active_strategy.plot(self._figure) def log(self, *args): - """If has a list named "_log" as attribute, appends the arguments there - """ - if hasattr(self, '_log') and isinstance(self._log, list): + """If has a list named "_log" as attribute, appends the arguments there""" + if hasattr(self, "_log") and isinstance(self._log, list): self._log.append(args) def __repr__(self): - ans = u"" + ans += "'>" return ans def stop(self): diff --git a/hyperspy/samfire_utils/fit_tests.py b/hyperspy/samfire_utils/fit_tests.py index 4a2d621b9e..e600e9d3b1 100644 --- a/hyperspy/samfire_utils/fit_tests.py +++ b/hyperspy/samfire_utils/fit_tests.py @@ -1,22 +1,35 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy.samfire_utils.goodness_of_fit_tests.red_chisq import \ - red_chisq_test -from hyperspy.samfire_utils.goodness_of_fit_tests.information_theory import \ - (AIC_test, AICc_test, BIC_test) +from hyperspy.samfire_utils.goodness_of_fit_tests.information_theory import ( + AIC_test, + AICc_test, + BIC_test, +) +from hyperspy.samfire_utils.goodness_of_fit_tests.red_chisq import red_chisq_test + +__all__ = [ + "AIC_test", + "AICc_test", + "BIC_test", + "red_chisq_test", +] + + +def __dir__(): + return sorted(__all__) diff --git a/hyperspy/samfire_utils/global_strategies.py b/hyperspy/samfire_utils/global_strategies.py index 81d9d2bca3..8c306385de 100644 --- a/hyperspy/samfire_utils/global_strategies.py +++ b/hyperspy/samfire_utils/global_strategies.py @@ -1,27 +1,36 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy.samfire_utils.strategy import GlobalStrategy from hyperspy.samfire_utils.segmenters.histogram import HistogramSegmenter +from hyperspy.samfire_utils.strategy import GlobalStrategy class HistogramStrategy(GlobalStrategy): - - def __init__(self, bins='fd'): - super().__init__('Histogram global strategy') + def __init__(self, bins="fd"): + super().__init__("Histogram global strategy") self.segmenter = HistogramSegmenter(bins) + + +__all__ = [ + "GlobalStrategy", + "HistogramStrategy", +] + + +def __dir__(): + return sorted(__all__) diff --git a/hyperspy/samfire_utils/goodness_of_fit_tests/information_theory.py b/hyperspy/samfire_utils/goodness_of_fit_tests/information_theory.py index 566698730a..e2fd567eb9 100644 --- a/hyperspy/samfire_utils/goodness_of_fit_tests/information_theory.py +++ b/hyperspy/samfire_utils/goodness_of_fit_tests/information_theory.py @@ -1,51 +1,53 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from hyperspy.utils.model_selection import AIC, AICc, BIC import numpy as np +from hyperspy.utils.model_selection import AIC, BIC, AICc + def notexp_o(x): if x > 1: - return np.exp(1.) * (x * x + 1.) * 0.5 + return np.exp(1.0) * (x * x + 1.0) * 0.5 elif x <= 1 and x > -1: return np.exp(x) else: - return 2. / ((x * x + 1.) * np.exp(1.)) + return 2.0 / ((x * x + 1.0) * np.exp(1.0)) + notexp = np.vectorize(notexp_o) -class AIC_test(object): +class AIC_test: + """Akaike information criterion test""" def __init__(self, tolerance): - self.name = 'Akaike information criterion test' + self.name = "Akaike information criterion test" self.tolerance = tolerance - self.expected = 0. + self.expected = 0.0 def test(self, model, ind): m = model.inav[ind[::-1]] m.fetch_stored_values() _aic = AIC(m) - return np.abs( - notexp(_aic) - self.expected) < notexp(self.tolerance) + return abs(notexp(_aic) - self.expected) < notexp(self.tolerance) def map(self, model, mask): ind_list = np.where(mask) @@ -56,19 +58,19 @@ def map(self, model, mask): return ans -class AICc_test(object): +class AICc_test: + """Akaike information criterion (with a correction) test""" def __init__(self, tolerance): - self.name = 'Akaike information criterion (with a correction) test' + self.name = "Akaike information criterion (with a correction) test" self.tolerance = tolerance - self.expected = 0. + self.expected = 0.0 def test(self, model, ind): m = model.inav[ind[::-1]] m.fetch_stored_values() _aicc = AICc(m) - return np.abs( - notexp(_aicc) - self.expected) < notexp(self.tolerance) + return abs(notexp(_aicc) - self.expected) < notexp(self.tolerance) def map(self, model, mask): ind_list = np.where(mask) @@ -79,19 +81,19 @@ def map(self, model, mask): return ans -class BIC_test(object): +class BIC_test: + """Bayesian information criterion test""" def __init__(self, tolerance): - self.name = 'Bayesian information criterion test' + self.name = "Bayesian information criterion test" self.tolerance = tolerance - self.expected = 0. + self.expected = 0.0 def test(self, model, ind): m = model.inav[ind[::-1]] m.fetch_stored_values() _bic = BIC(m) - return np.abs( - notexp(_bic) - self.expected) < notexp(self.tolerance) + return abs(notexp(_bic) - self.expected) < notexp(self.tolerance) def map(self, model, mask): ind_list = np.where(mask) diff --git a/hyperspy/samfire_utils/goodness_of_fit_tests/red_chisq.py b/hyperspy/samfire_utils/goodness_of_fit_tests/red_chisq.py index fcaa9409cd..626ab52be1 100644 --- a/hyperspy/samfire_utils/goodness_of_fit_tests/red_chisq.py +++ b/hyperspy/samfire_utils/goodness_of_fit_tests/red_chisq.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -22,18 +22,16 @@ class red_chisq_test(goodness_test): - def __init__(self, tolerance): - self.name = 'Reduced chi-squared test' + self.name = "Reduced chi-squared test" self.expected = 1.0 self.tolerance = tolerance def test(self, model, ind): - return np.abs( - model.red_chisq.data[ind] - self.expected) < self.tolerance + return abs(model.red_chisq.data[ind] - self.expected) < self.tolerance def map(self, model, mask): rc = model.red_chisq.data rc = np.where(np.isnan(rc), -np.inf, rc) - ans = np.abs(rc - self.expected) < self.tolerance + ans = abs(rc - self.expected) < self.tolerance return np.logical_and(mask, ans) diff --git a/hyperspy/samfire_utils/goodness_of_fit_tests/test_general.py b/hyperspy/samfire_utils/goodness_of_fit_tests/test_general.py index f1079a876d..9f0fc18641 100644 --- a/hyperspy/samfire_utils/goodness_of_fit_tests/test_general.py +++ b/hyperspy/samfire_utils/goodness_of_fit_tests/test_general.py @@ -1,30 +1,28 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np +# along with HyperSpy. If not, see . class goodness_test(object): - _tolerance = None @property def tolerance(self): + """The tolerance.""" return self._tolerance @tolerance.setter @@ -32,4 +30,4 @@ def tolerance(self, value): if value is None: self._tolerance = None else: - self._tolerance = np.abs(value) + self._tolerance = abs(value) diff --git a/hyperspy/samfire_utils/local_strategies.py b/hyperspy/samfire_utils/local_strategies.py index 3770b08a24..860f73cce5 100644 --- a/hyperspy/samfire_utils/local_strategies.py +++ b/hyperspy/samfire_utils/local_strategies.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -33,7 +33,17 @@ class ReducedChiSquaredStrategy(LocalStrategy): """ def __init__(self): - super().__init__('Reduced chi squared strategy') + super().__init__("Reduced chi squared strategy") self.weight = ReducedChiSquaredWeight() - self.radii = 3. + self.radii = 3.0 self.decay_function = exp_decay + + +__all__ = [ + "LocalStrategy", + "ReducedChiSquaredStrategy", +] + + +def __dir__(): + return sorted(__all__) diff --git a/hyperspy/samfire_utils/samfire_kernel.py b/hyperspy/samfire_utils/samfire_kernel.py index 9ae1678a9a..f99a472838 100644 --- a/hyperspy/samfire_utils/samfire_kernel.py +++ b/hyperspy/samfire_utils/samfire_kernel.py @@ -1,25 +1,27 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . def single_kernel(model, ind, values, optional_components, _args, test): from itertools import combinations, product + import numpy as np + from hyperspy.utils.model_selection import AICc def generate_values_iterator(compnames, vals, turned_on_component_inds): @@ -59,7 +61,8 @@ def generate_values_iterator(compnames, vals, turned_on_component_inds): on_comps = [i for i, _c in enumerate(model) if _c.active] name_list, iterator = generate_values_iterator( - component_names, values, on_comps) + component_names, values, on_comps + ) ifgood = False for it in iterator: @@ -67,7 +70,7 @@ def generate_values_iterator(compnames, vals, turned_on_component_inds): for (comp_n, par_n), val in zip(name_list, it): try: getattr(model[comp_n], par_n).value = val - except: + except Exception: pass model.fit(**_args) # only perform iterations until we find a solution that we think is @@ -83,14 +86,13 @@ def generate_values_iterator(compnames, vals, turned_on_component_inds): # get model with best chisq here, and test model validation new_AICc = AICc(model.inav[ind[::-1]]) - if new_AICc < AICc_fraction * best_AICc or \ - (np.abs(new_AICc - best_AICc) <= np.abs(AICc_fraction * best_AICc) - and len(model.p0) < best_dof): + if new_AICc < AICc_fraction * best_AICc or ( + abs(new_AICc - best_AICc) <= abs(AICc_fraction * best_AICc) + and len(model.p0) < best_dof + ): best_values = [ - getattr( - model[comp_n], - par_n).value for comp_n, - par_n in name_list] + getattr(model[comp_n], par_n).value for comp_n, par_n in name_list + ] best_names = name_list best_comb = combination best_AICc = new_AICc @@ -108,23 +110,24 @@ def generate_values_iterator(compnames, vals, turned_on_component_inds): for (comp_n, par_n), val in zip(best_names, best_values): try: getattr(model[comp_n], par_n).value = val - except: + except Exception: pass model.fit(**_args) return True -def multi_kernel( - ind, m_dic, values, optional_components, _args, result_q, test_dict): - import hyperspy.api as hs - from hyperspy.signal import Signal - from multiprocessing import current_process +def multi_kernel(ind, m_dic, values, optional_components, _args, result_q, test_dict): + import copy from itertools import combinations, product + from multiprocessing import current_process + + import cloudpickle + # from collections import Iterable import numpy as np - import copy + + from hyperspy.signal import Signal from hyperspy.utils.model_selection import AICc - import dill def generate_values_iterator(compnames, vals, turned_on_component_inds): turned_on_names = [compnames[i] for i in turned_on_component_inds] @@ -136,7 +139,9 @@ def generate_values_iterator(compnames, vals, turned_on_component_inds): if _comp_n in turned_on_names: for par_n, par in _comp.items(): if not isinstance(par, list): - par = [par, ] + par = [ + par, + ] tmp.append(par) name_list.append((_comp_n, par_n)) return name_list, product(*tmp) @@ -144,22 +149,22 @@ def generate_values_iterator(compnames, vals, turned_on_component_inds): def send_good_results(model, previous_switching, cur_p, result_q, ind): result = copy.deepcopy(model.as_dictionary()) for num, a_i_m in enumerate(previous_switching): - result['components'][num]['active_is_multidimensional'] = a_i_m - result['current'] = cur_p._identity + result["components"][num]["active_is_multidimensional"] = a_i_m + result["current"] = cur_p._identity result_q.put((ind, result, True)) - test = dill.loads(test_dict) + test = cloudpickle.loads(test_dict) cur_p = current_process() previous_switching = [] comb = [] AICc_fraction = 0.99 - comp_dict = m_dic['models']['z']['_dict']['components'] + comp_dict = m_dic["models"]["z"]["_dict"]["components"] for num, comp in enumerate(comp_dict): - previous_switching.append(comp['active_is_multidimensional']) - comp['active_is_multidimensional'] = False + previous_switching.append(comp["active_is_multidimensional"]) + comp["active_is_multidimensional"] = False for comp in optional_components: - comp_dict[comp]['active'] = False + comp_dict[comp]["active"] = False for num in range(len(optional_components) + 1): for comp in combinations(optional_components, num): @@ -168,7 +173,7 @@ def send_good_results(model, previous_switching, cur_p, result_q, ind): best_AICc, best_dof = np.inf, np.inf best_comb, best_values, best_names = None, None, None - component_names = [c['name'] for c in comp_dict] + component_names = [c["name"] for c in comp_dict] sig = Signal(**m_dic) sig._assign_subclass() @@ -180,7 +185,8 @@ def send_good_results(model, previous_switching, cur_p, result_q, ind): on_comps = [i for i, _c in enumerate(model) if _c.active] name_list, iterator = generate_values_iterator( - component_names, values, on_comps) + component_names, values, on_comps + ) ifgood = False for it in iterator: @@ -188,7 +194,7 @@ def send_good_results(model, previous_switching, cur_p, result_q, ind): for (comp_n, par_n), val in zip(name_list, it): try: getattr(model[comp_n], par_n).value = val - except: + except Exception: pass model.fit(**_args) # only perform iterations until we find a solution that we think is @@ -199,22 +205,16 @@ def send_good_results(model, previous_switching, cur_p, result_q, ind): if ifgood: # get model with best chisq here, and test model validation if len(comb) == 1: - send_good_results( - model, - previous_switching, - cur_p, - result_q, - ind) + send_good_results(model, previous_switching, cur_p, result_q, ind) new_AICc = AICc(model) - if new_AICc < AICc_fraction * best_AICc or \ - (np.abs(new_AICc - best_AICc) <= np.abs(AICc_fraction * best_AICc) - and len(model.p0) < best_dof): + if new_AICc < AICc_fraction * best_AICc or ( + abs(new_AICc - best_AICc) <= abs(AICc_fraction * best_AICc) + and len(model.p0) < best_dof + ): best_values = [ - getattr( - model[comp_n], - par_n).value for comp_n, - par_n in name_list] + getattr(model[comp_n], par_n).value for comp_n, par_n in name_list + ] best_names = name_list best_comb = combination best_AICc = new_AICc @@ -231,7 +231,7 @@ def send_good_results(model, previous_switching, cur_p, result_q, ind): for (comp_n, par_n), val in zip(best_names, best_values): try: getattr(model[comp_n], par_n).value = val - except: + except Exception: pass model.fit(**_args) send_good_results(model, previous_switching, cur_p, result_q, ind) diff --git a/hyperspy/samfire_utils/samfire_pool.py b/hyperspy/samfire_utils/samfire_pool.py index ca6b0f2d6b..b03b8d8fe6 100644 --- a/hyperspy/samfire_utils/samfire_pool.py +++ b/hyperspy/samfire_utils/samfire_pool.py @@ -1,30 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import time import logging +import time from multiprocessing import Manager + import numpy as np from dask.array import Array as dar -from hyperspy.utils.parallel_pool import ParallelPool from hyperspy.samfire_utils.samfire_worker import create_worker +from hyperspy.utils.parallel_pool import ParallelPool _logger = logging.getLogger(__name__) @@ -34,7 +35,7 @@ def _walk_compute(athing): this = {} for key, val in athing.items(): if isinstance(key, dar): - raise ValueError('Dask arrays should not be used as keys') + raise ValueError("Dask arrays should not be used as keys") value = _walk_compute(val) this[key] = value return this @@ -43,14 +44,14 @@ def _walk_compute(athing): elif isinstance(athing, tuple): return tuple(_walk_compute(val) for val in athing) elif isinstance(athing, dar): - _logger.debug('found a dask array!') + _logger.debug("found a dask array!") return athing.compute() else: return athing class SamfirePool(ParallelPool): - """ Creates and manages a pool of SAMFire workers. For based on + """Creates and manages a pool of SAMFire workers. For based on ParallelPool - either creates processes using multiprocessing, or connects and sets up ipyparallel load_balanced_view. @@ -68,8 +69,8 @@ class SamfirePool(ParallelPool): Attributes ---------- has_pool : bool - Boolean if the pool is available and active - pool : {ipyparallel.load_balanced_view, multiprocessing.Pool} + Whether the pool is available and active + pool : :class:`ipyparallel.LoadBalancedView` or :class:`python:multiprocessing.pool.Pool` The pool object ipython_kwargs : dict The dictionary with Ipyparallel connection arguments. @@ -91,7 +92,7 @@ class SamfirePool(ParallelPool): def __init__(self, **kwargs): """Creates a ParallelPool with additional methods for SAMFire. All arguments are passed to ParallelPool""" - super(SamfirePool, self).__init__(**kwargs) + super().__init__(**kwargs) self.samf = None self.ping = {} self.pid = {} @@ -107,7 +108,7 @@ def _timestep_set(self, value): self._timestep = value if self.has_pool and self.is_multiprocessing: for this_queue in self.workers.values(): - this_queue.put(('change_timestep', (value,))) + this_queue.put(("change_timestep", (value,))) def prepare_workers(self, samfire): """Given SAMFire object, populate the workers with the required @@ -116,20 +117,20 @@ def prepare_workers(self, samfire): Parameters ---------- - samfire : samfire - the SAMFire object that will be using the pool + samfire : :class:`~hyperspy.samfire.Samfire` + The SAMFire object that will be using the pool. """ - _logger.debug('starting prepare_workers') + _logger.debug("starting prepare_workers") self.samf = samfire mall = samfire.model model = mall.inav[mall.axes_manager.indices] - if model.signal.metadata.has_item('Signal.Noise_properties.variance'): + if model.signal.metadata.has_item("Signal.Noise_properties.variance"): var = model.signal.metadata.Signal.Noise_properties.variance if var._lazy: var.compute() - model.store('z') + model.store("z") m_dict = model.signal._to_dictionary(False) - m_dict['models'] = model.signal.models._models.as_dictionary() + m_dict["models"] = model.signal.models._models.as_dictionary() m_dict = _walk_compute(m_dict) @@ -137,37 +138,47 @@ def prepare_workers(self, samfire): if self.is_ipyparallel: from ipyparallel import Reference as ipp_Reference - _logger.debug('preparing ipyparallel workers') - direct_view = self.pool.client[:self.num_workers] + + _logger.debug("preparing ipyparallel workers") + direct_view = self.pool.client[: self.num_workers] direct_view.block = True - direct_view.execute("from hyperspy.samfire_utils.samfire_worker" - " import create_worker") - direct_view.scatter('identity', range(self.num_workers), - flatten=True) - direct_view.execute('worker = create_worker(identity)') - self.rworker = ipp_Reference('worker') - direct_view.apply(lambda worker, m_dict: - worker.create_model(m_dict, 'z'), self.rworker, - m_dict) - direct_view.apply(lambda worker, ts: worker.setup_test(ts), - self.rworker, samfire.metadata._gt_dump) - direct_view.apply(lambda worker, on: worker.set_optional_names(on), - self.rworker, optional_names) + direct_view.execute( + "from hyperspy.samfire_utils.samfire_worker" " import create_worker" + ) + direct_view.scatter("identity", range(self.num_workers), flatten=True) + direct_view.execute("worker = create_worker(identity)") + self.rworker = ipp_Reference("worker") + direct_view.apply( + lambda worker, m_dict: worker.create_model(m_dict, "z"), + self.rworker, + m_dict, + ) + direct_view.apply( + lambda worker, ts: worker.setup_test(ts), + self.rworker, + samfire.metadata._gt_dump, + ) + direct_view.apply( + lambda worker, on: worker.set_optional_names(on), + self.rworker, + optional_names, + ) if self.is_multiprocessing: - _logger.debug('preparing multiprocessing workers') + _logger.debug("preparing multiprocessing workers") manager = Manager() self.shared_queue = manager.Queue() self.result_queue = manager.Queue() for i in range(self.num_workers): this_queue = manager.Queue() self.workers[i] = this_queue - this_queue.put(('setup_test', (samfire.metadata._gt_dump,))) - this_queue.put(('create_model', (m_dict, 'z'))) - this_queue.put(('set_optional_names', (optional_names,))) - self.pool.apply_async(create_worker, args=(i, this_queue, - self.shared_queue, - self.result_queue)) + this_queue.put(("setup_test", (samfire.metadata._gt_dump,))) + this_queue.put(("create_model", (m_dict, "z"))) + this_queue.put(("set_optional_names", (optional_names,))) + self.pool.apply_async( + create_worker, + args=(i, this_queue, self.shared_queue, self.result_queue), + ) def update_parameters(self): """Updates various worker parameters. @@ -177,25 +188,34 @@ def update_parameters(self): - Parameter boundaries - Goodness test""" samfire = self.samf - optional_names = {samfire.model[c].name for c in - samfire.optional_components} - boundaries = tuple(tuple((par.bmin, par.bmax) for par in - comp.parameters) for comp in self.samf.model) + optional_names = {samfire.model[c].name for c in samfire.optional_components} + boundaries = tuple( + tuple((par.bmin, par.bmax) for par in comp.parameters) + for comp in self.samf.model + ) if self.is_multiprocessing: for this_queue in self.workers.values(): - this_queue.put(('set_optional_names', (optional_names,))) - this_queue.put(('setup_test', (samfire.metadata._gt_dump,))) - this_queue.put(('set_parameter_boundaries', (boundaries,))) + this_queue.put(("set_optional_names", (optional_names,))) + this_queue.put(("setup_test", (samfire.metadata._gt_dump,))) + this_queue.put(("set_parameter_boundaries", (boundaries,))) elif self.is_ipyparallel: - direct_view = self.pool.client[:self.num_workers] + direct_view = self.pool.client[: self.num_workers] direct_view.block = True - direct_view.apply(lambda worker, on: worker.set_optional_names(on), - self.rworker, optional_names) - direct_view.apply(lambda worker, ts: worker.setup_test(ts), - self.rworker, samfire.metadata._gt_dump) - direct_view.apply(lambda worker, ts: - worker.set_parameter_boundaries(ts), - self.rworker, boundaries) + direct_view.apply( + lambda worker, on: worker.set_optional_names(on), + self.rworker, + optional_names, + ) + direct_view.apply( + lambda worker, ts: worker.setup_test(ts), + self.rworker, + samfire.metadata._gt_dump, + ) + direct_view.apply( + lambda worker, ts: worker.set_parameter_boundaries(ts), + self.rworker, + boundaries, + ) def ping_workers(self, timeout=None): """Ping the workers and record one-way trip time and the process_id @@ -208,26 +228,30 @@ def ping_workers(self, timeout=None): ping. If None, the default timeout is used """ if self.samf is None: - _logger.error('Have to add samfire to the pool first') + _logger.error("Have to add samfire to the pool first") else: if self.is_multiprocessing: for _id, this_queue in self.workers.items(): - this_queue.put('ping') + this_queue.put("ping") self.ping[_id] = time.time() elif self.is_ipyparallel: for i in range(self.num_workers): direct_view = self.pool.client[i] - self.results.append((direct_view.apply_async(lambda worker: - worker.ping(), - self.rworker), - i)) + self.results.append( + ( + direct_view.apply_async( + lambda worker: worker.ping(), self.rworker + ), + i, + ) + ) self.ping[i] = time.time() time.sleep(0.5) self.collect_results(timeout) def __len__(self): if self.is_ipyparallel: - return self.pool.client.queue_status()['unassigned'] + return self.pool.client.queue_status()["unassigned"] elif self.is_multiprocessing: return self.shared_queue.qsize() @@ -239,17 +263,22 @@ def add_jobs(self, needed_number=None): needed_number: {None, int} The number of jobs to add. If None (default), adds `need_pixels` """ + def test_func(worker, ind, value_dict): return worker.run_pixel(ind, value_dict) + if needed_number is None: needed_number = self.need_pixels for ind, value_dict in self.samf.generate_values(needed_number): if self.is_multiprocessing: - self.shared_queue.put(('run_pixel', (ind, value_dict))) + self.shared_queue.put(("run_pixel", (ind, value_dict))) elif self.is_ipyparallel: - self.results.append((self.pool.apply_async(test_func, - self.rworker, ind, - value_dict), ind)) + self.results.append( + ( + self.pool.apply_async(test_func, self.rworker, ind, value_dict), + ind, + ) + ) def parse(self, value): """Parse the value returned from the workers. @@ -268,24 +297,27 @@ def parse(self, value): bool_if_result_converged)) """ if value is None: - keyword = 'Failed' - _logger.debug('Got None') + keyword = "Failed" + _logger.debug("Got None") else: keyword, the_rest = value samf = self.samf - if keyword == 'pong': + if keyword == "pong": _id, pid, pong_time, message = the_rest self.ping[_id] = pong_time - self.ping[_id] self.pid[_id] = pid - _logger.info('pong worker %s with time %g and message' - '"%s"' % (str(_id), self.ping[_id], message)) - elif keyword == 'Error': + _logger.info( + "pong worker %s with time %g and message" + '"%s"' % (str(_id), self.ping[_id], message) + ) + elif keyword == "Error": _id, err_message = the_rest - _logger.error('Error in worker %s\n%s' % (str(_id), err_message)) - elif keyword == 'result': + _logger.error("Error in worker %s\n%s" % (str(_id), err_message)) + elif keyword == "result": _id, ind, result, isgood = the_rest - _logger.debug('Got result from pixel {} and it is good:' - '{}'.format(ind, isgood)) + _logger.debug( + "Got result from pixel {} and it is good:" "{}".format(ind, isgood) + ) if ind in samf.running_pixels: samf.running_pixels.remove(ind) samf.update(ind, result, isgood) @@ -293,8 +325,9 @@ def parse(self, value): samf.backup() samf.log(ind, isgood, samf.count, _id) else: - _logger.error('Unusual return from some worker. The value ' - 'is:\n%s' % str(value)) + _logger.error( + "Unusual return from some worker. The value " "is:\n%s" % str(value) + ) def collect_results(self, timeout=None): """Collects and parses all results, currently not processed due to @@ -317,10 +350,11 @@ def collect_results(self, timeout=None): try: result = res.get(timeout=timeout) except TimeoutError: - _logger.info('Ind {} failed to come back in {} ' - 'seconds. Assuming failed'.format( - ind, timeout)) - result = ('result', (-1, ind, None, False)) + _logger.info( + "Ind {} failed to come back in {} " + "seconds. Assuming failed".format(ind, timeout) + ) + result = ("result", (-1, ind, None, False)) self.parse(result) self.results.remove((res, ind)) found_something = True @@ -329,13 +363,15 @@ def collect_results(self, timeout=None): elif self.is_multiprocessing: while not self.result_queue.empty(): try: - result = self.result_queue.get(block=True, - timeout=timeout) + result = self.result_queue.get(block=True, timeout=timeout) self.parse(result) found_something = True except TimeoutError: - _logger.info('Some ind failed to come back in {} ' - 'seconds.'.format(self.timeout)) + _logger.info( + "Some ind failed to come back in {} " "seconds.".format( + self.timeout + ) + ) return found_something @property @@ -343,16 +379,17 @@ def need_pixels(self): """Returns the number of pixels that should be added to the processing queue. At most is equal to the number of workers. """ - return min(self.samf.pixels_done * self.samf.metadata.marker.ndim, - self.num_workers - len(self)) + return min( + self.samf.pixels_done * self.samf.metadata.marker.ndim, + self.num_workers - len(self), + ) @property def _not_too_long(self): """Returns bool if it has been too long after receiving the last result, probably meaning some of the workers timed out or hung. """ - if not hasattr(self, '_last_time') or not isinstance(self._last_time, - float): + if not hasattr(self, "_last_time") or not isinstance(self._last_time, float): self._last_time = time.time() return (time.time() - self._last_time) <= self.timeout @@ -364,8 +401,9 @@ def run(self): Run the full procedure until no more pixels are left to run in the SAMFire. """ - while self._not_too_long and (self.samf.pixels_left or - len(self.samf.running_pixels)): + while self._not_too_long and ( + self.samf.pixels_left or len(self.samf.running_pixels) + ): # bool if got something new_result = self.collect_results() need_number = self.need_pixels @@ -384,7 +422,7 @@ def stop(self): """ if self.is_multiprocessing: for queue in self.workers.values(): - queue.put('stop_listening') + queue.put("stop_listening") self.pool.close() self.pool.terminate() self.pool.join() diff --git a/hyperspy/samfire_utils/samfire_worker.py b/hyperspy/samfire_utils/samfire_worker.py index c68e28333e..5b2cc16ec1 100644 --- a/hyperspy/samfire_utils/samfire_worker.py +++ b/hyperspy/samfire_utils/samfire_worker.py @@ -1,32 +1,30 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import logging import os -import time import sys +import time from itertools import combinations, product from queue import Empty -import dill -import numpy as np -import matplotlib -matplotlib.rcParams['backend'] = 'Agg' +import cloudpickle +import numpy as np from hyperspy.signal import BaseSignal from hyperspy.utils.model_selection import AICc @@ -35,9 +33,9 @@ class Worker: - - def __init__(self, identity, individual_queue=None, shared_queue=None, - result_queue=None): + def __init__( + self, identity, individual_queue=None, shared_queue=None, result_queue=None + ): self.identity = identity self.individual_queue = individual_queue self.shared_queue = shared_queue @@ -52,7 +50,7 @@ def __init__(self, identity, individual_queue=None, shared_queue=None, self.parameters = {} def create_model(self, signal_dict, model_letter): - _logger.debug('Creating model in worker {}'.format(self.identity)) + _logger.debug("Creating model in worker {}".format(self.identity)) sig = BaseSignal(**signal_dict) sig._assign_subclass() self.model = sig.models[model_letter].restore() @@ -62,8 +60,7 @@ def create_model(self, signal_dict, model_letter): for par in component.parameters: par.map = par.map.copy() - if self.model.signal.metadata.has_item( - 'Signal.Noise_properties.variance'): + if self.model.signal.metadata.has_item("Signal.Noise_properties.variance"): var = self.model.signal.metadata.Signal.Noise_properties.variance if isinstance(var, BaseSignal): var.data = var.data.copy() @@ -75,16 +72,18 @@ def _array_views_to_copies(self): for k, v in dct.items(): if isinstance(v, BaseSignal): v.data = v.data.copy() - if k not in ['signal', 'image', 'spectrum'] and not \ - k.startswith('_'): + if k not in ["signal", "image", "spectrum"] and not k.startswith("_"): self.parameters[k] = None if isinstance(v, np.ndarray): dct[k] = v.copy() def set_optional_names(self, optional_names): self.optional_names = optional_names - _logger.debug('Setting optional names in worker {} to ' - '{}'.format(self.identity, self.optional_names)) + _logger.debug( + "Setting optional names in worker {} to " "{}".format( + self.identity, self.optional_names + ) + ) def set_parameter_boundaries(self, received): for rec, comp in zip(received, self.model): @@ -98,35 +97,37 @@ def generate_values_iterator(self, turned_on_names): for _comp_n, _comp in self.value_dict.items(): for par_n, par in _comp.items(): if _comp_n not in turned_on_names: - par = [None, ] + par = [ + None, + ] if not isinstance(par, list): - par = [par, ] + par = [ + par, + ] tmp.append(par) name_list.append((_comp_n, par_n)) return name_list, product(*tmp) def set_values(self, name_list, iterator): for value_combination in iterator: - for (comp_name, parameter_name), value in zip(name_list, - value_combination): + for (comp_name, parameter_name), value in zip(name_list, value_combination): if value is None: self.model[comp_name].active = False else: self.model[comp_name].active = True try: - getattr(self.model[comp_name], - parameter_name).value = value + getattr(self.model[comp_name], parameter_name).value = value except BaseException: e = sys.exc_info()[0] - to_send = ('Error', - (self.identity, - 'Setting {}.{} value to {}. ' - 'Caught:\n{}'.format(comp_name, - parameter_name, - value, - e) - ) - ) + to_send = ( + "Error", + ( + self.identity, + "Setting {}.{} value to {}. " "Caught:\n{}".format( + comp_name, parameter_name, value, e + ), + ), + ) if self.result_queue is None: return to_send else: @@ -146,9 +147,10 @@ def fit(self, component_comb): def generate_component_combinations(self): all_names = {component.name for component in self.model} - names_to_skip_generators = [combinations(self.optional_names, howmany) - for howmany in - range(len(self.optional_names) + 1)] + names_to_skip_generators = [ + combinations(self.optional_names, howmany) + for howmany in range(len(self.optional_names) + 1) + ] names_to_skip = [] for _gen in names_to_skip_generators: names_to_skip.extend(list(_gen)) @@ -165,24 +167,26 @@ def run_pixel(self, ind, value_dict): self.ind = ind self.value_dict = value_dict - self.fitting_kwargs = self.value_dict.pop('fitting_kwargs', {}) - if 'min_function' in self.fitting_kwargs: - self.fitting_kwargs['min_function'] = dill.loads( - self.fitting_kwargs['min_function']) - if 'min_function_grad' in self.fitting_kwargs and isinstance( - self.fitting_kwargs['min_function_grad'], bytes): - self.fitting_kwargs['min_function_grad'] = dill.loads( - self.fitting_kwargs['min_function_grad']) - self.model.signal.data[:] = self.value_dict.pop('signal.data') - - if self.model.signal.metadata.has_item( - 'Signal.Noise_properties.variance'): + self.fitting_kwargs = self.value_dict.pop("fitting_kwargs", {}) + if "min_function" in self.fitting_kwargs: + self.fitting_kwargs["min_function"] = cloudpickle.loads( + self.fitting_kwargs["min_function"] + ) + if "min_function_grad" in self.fitting_kwargs and isinstance( + self.fitting_kwargs["min_function_grad"], bytes + ): + self.fitting_kwargs["min_function_grad"] = cloudpickle.loads( + self.fitting_kwargs["min_function_grad"] + ) + self.model.signal.data[:] = self.value_dict.pop("signal.data") + + if self.model.signal.metadata.has_item("Signal.Noise_properties.variance"): var = self.model.signal.metadata.Signal.Noise_properties.variance if isinstance(var, BaseSignal): - var.data[:] = self.value_dict.pop('variance.data') + var.data[:] = self.value_dict.pop("variance.data") - if 'low_loss.data' in self.value_dict: - self.model.low_loss.data[:] = self.value_dict.pop('low_loss.data') + if "low_loss.data" in self.value_dict: + self.model.low_loss.data[:] = self.value_dict.pop("low_loss.data") for component_comb in self.generate_component_combinations(): good_fit = self.fit(component_comb) @@ -195,21 +199,26 @@ def run_pixel(self, ind, value_dict): return self.send_results() def _collect_values(self): - result = {component.name: {parameter.name: parameter.map.copy() for - parameter in component.parameters} for - component in self.model if component.active} + result = { + component.name: { + parameter.name: parameter.map.copy() + for parameter in component.parameters + } + for component in self.model + if component.active + } return result def compare_models(self): new_AICc = AICc(self.model) AICc_test = new_AICc < (self._AICc_fraction * self.best_AICc) - AICc_absolute_test = np.abs(new_AICc - self.best_AICc) <= \ - np.abs(self._AICc_fraction * self.best_AICc) + AICc_absolute_test = abs(new_AICc - self.best_AICc) <= abs( + self._AICc_fraction * self.best_AICc + ) dof_test = len(self.model.p0) < self.best_dof if AICc_test or AICc_absolute_test and dof_test: - self.best_values = self._collect_values() self.best_AICc = new_AICc self.best_dof = len(self.model.p0) @@ -222,24 +231,23 @@ def send_results(self, current=False): for k in self.parameters.keys(): self.parameters[k] = getattr(self.model, k).data[0] if len(self.best_values): # i.e. we have a good result - _logger.debug('we have a good result in worker ' - '{}'.format(self.identity)) - result = {k + '.data': np.array(v) for k, v in - self.parameters.items()} - result['components'] = self.best_values + _logger.debug("we have a good result in worker " "{}".format(self.identity)) + result = {k + ".data": np.array(v) for k, v in self.parameters.items()} + result["components"] = self.best_values found_solution = True else: - _logger.debug("we don't have a good result in worker " - "{}".format(self.identity)) + _logger.debug( + "we don't have a good result in worker " "{}".format(self.identity) + ) result = None found_solution = False - to_send = ('result', (self.identity, self.ind, result, found_solution)) + to_send = ("result", (self.identity, self.ind, result, found_solution)) if self.individual_queue is None: return to_send self.result_queue.put(to_send) def setup_test(self, test_string): - self.fit_test = dill.loads(test_string) + self.fit_test = cloudpickle.loads(test_string) def start_listening(self): self._listening = True @@ -256,7 +264,7 @@ def parse(self, result): getattr(self, function)(*arguments) def ping(self, message=None): - to_send = ('pong', (self.identity, os.getpid(), time.time(), message)) + to_send = ("pong", (self.identity, os.getpid(), time.time(), message)) if self.result_queue is None: return to_send self.result_queue.put(to_send) @@ -278,13 +286,11 @@ def listen(self): if time_diff >= self.timestep: if not self.individual_queue.empty(): queue = self.individual_queue - elif (self.shared_queue is not None and not - self.shared_queue.empty()): + elif self.shared_queue is not None and not self.shared_queue.empty(): queue = self.shared_queue if queue is not None: try: - result = queue.get(block=True, - timeout=self.max_get_timeout) + result = queue.get(block=True, timeout=self.max_get_timeout) found_what_to_do = True self.parse(result) except Empty: @@ -295,8 +301,9 @@ def listen(self): self.sleep() -def create_worker(identity, individual_queue=None, - shared_queue=None, result_queue=None): +def create_worker( + identity, individual_queue=None, shared_queue=None, result_queue=None +): w = Worker(identity, individual_queue, shared_queue, result_queue) if individual_queue is None: return w diff --git a/hyperspy/samfire_utils/segmenters/__init__.py b/hyperspy/samfire_utils/segmenters/__init__.py index 8c5e4f8c27..ff5129c9db 100644 --- a/hyperspy/samfire_utils/segmenters/__init__.py +++ b/hyperspy/samfire_utils/segmenters/__init__.py @@ -1 +1 @@ -__author__ = 'to266' +__author__ = "to266" diff --git a/hyperspy/samfire_utils/segmenters/histogram.py b/hyperspy/samfire_utils/segmenters/histogram.py index 22e6c55dd7..6568225bba 100644 --- a/hyperspy/samfire_utils/segmenters/histogram.py +++ b/hyperspy/samfire_utils/segmenters/histogram.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np from scipy.signal import argrelextrema @@ -28,7 +28,7 @@ class HistogramSegmenter(object): the starting parameter estimates. """ - def __init__(self, bins='fd'): + def __init__(self, bins="fd"): self.database = None self.bins = bins self._min_points = 4 @@ -44,14 +44,12 @@ def most_frequent(self): for p_n, (hist, bin_edges) in comp.items(): # calculate frequent values maxima_hist_ind = argrelextrema( - np.append( - 0, - hist), - np.greater, - mode='wrap') - middles_of_maxima = 0.5 * \ - (bin_edges[maxima_hist_ind] + - bin_edges[([i - 1 for i in maxima_hist_ind[0]],)]) + np.append(0, hist), np.greater, mode="wrap" + ) + middles_of_maxima = 0.5 * ( + bin_edges[maxima_hist_ind] + + bin_edges[([i - 1 for i in maxima_hist_ind[0]],)] + ) comp_dict[p_n] = middles_of_maxima.tolist() freq[c_n] = comp_dict return freq @@ -74,9 +72,7 @@ def update(self, value_dict): comp_dict = {} for par_name, par in component.items(): if par.size <= self._min_points: - comp_dict[par_name] = np.histogram(par, - max(10, - self._min_points)) + comp_dict[par_name] = np.histogram(par, max(10, self._min_points)) else: comp_dict[par_name] = histogram(par, bins=self.bins) self.database[component_name] = comp_dict diff --git a/hyperspy/samfire_utils/strategy.py b/hyperspy/samfire_utils/strategy.py index b8d49b79ca..7075c1c6a2 100644 --- a/hyperspy/samfire_utils/strategy.py +++ b/hyperspy/samfire_utils/strategy.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -36,7 +36,7 @@ def make_sure_ind(inds, req_len=None): indices : tuple of floats """ try: - number = len(inds) # for error catching + number = len(inds) # for error catching val = () for i in inds: try: @@ -86,9 +86,8 @@ def nearest_indices(shape, ind, radii): return par, center -class SamfireStrategy(object): - """A SAMFire strategy base class. - """ +class SamfireStrategy: + """A SAMFire strategy base class.""" samf = None close_plot = None @@ -113,8 +112,7 @@ def __repr__(self): return self.name def remove(self): - """Removes this strategy from its SAMFire - """ + """Removes this strategy from its SAMFire""" self.samf.strategies.remove(self) @@ -137,8 +135,7 @@ def __init__(self, name): @property def samf(self): - """The SAMFire that owns this strategy. - """ + """The SAMFire that owns this strategy.""" return self._samf @samf.setter @@ -163,16 +160,14 @@ def weight(self, value): value.model = self.samf.model def clean(self): - """Purges the currently saved values. - """ + """Purges the currently saved values.""" self._untruncated = None self._mask_all = None self._radii_changed = True @property def radii(self): - """A tuple of >=0 floats that show the "radii of relevance" - """ + """A tuple of >=0 floats that show the "radii of relevance" """ return self._radii @radii.setter @@ -186,8 +181,7 @@ def radii(self, value): self._radii = value def _update_database(self, ind, count): - """Dummy method for compatibility - """ + """Dummy method for compatibility""" pass def refresh(self, overwrite, given_pixels=None): @@ -196,13 +190,13 @@ def refresh(self, overwrite, given_pixels=None): Parameters ---------- - overwrite : Bool + overwrite : bool If True, all but the given_pixels will be recalculated. Used when part of already calculated results has to be refreshed. If False, only use pixels with marker == -scale (by default -1) to propagate to pixels with marker >= 0. This allows "ignoring" pixels with marker < -scale (e.g. -2). - given_pixels : boolean numpy array + given_pixels : numpy.ndarray of bool Pixels with True value are assumed as correctly calculated. """ marker = self.samf.metadata.marker @@ -222,16 +216,13 @@ def refresh(self, overwrite, given_pixels=None): if given_pixels is not None: calc_pixels = np.logical_and(calc_pixels, given_pixels) todo_pixels = np.logical_or( - todo_pixels, - np.logical_xor( - marker == - - scale, - calc_pixels)) + todo_pixels, np.logical_xor(marker == -scale, calc_pixels) + ) done_number = np.sum(calc_pixels) todo_number = np.sum(todo_pixels) - marker[todo_pixels] = 0. + marker[todo_pixels] = 0.0 marker[calc_pixels] = -scale weights_all = self.decay_function(self.weight.map(calc_pixels)) @@ -242,8 +233,7 @@ def refresh(self, overwrite, given_pixels=None): for iindex in range(ind_list[0].size): ind = [one_list[iindex] for one_list in ind_list] - distances, slices, _, mask = self._get_distance_array( - shape, ind) + distances, slices, _, mask = self._get_distance_array(shape, ind) mask = np.logical_and(mask, todo_pixels[slices]) @@ -256,8 +246,7 @@ def refresh(self, overwrite, given_pixels=None): for iindex in range(ind_list[0].size): ind = [one_list[iindex] for one_list in ind_list] - distances, slices, centre, mask = self._get_distance_array( - shape, ind) + distances, slices, centre, mask = self._get_distance_array(shape, ind) mask = np.logical_and(mask, calc_pixels[slices]) @@ -278,43 +267,41 @@ def _get_distance_array(self, shape, ind): Returns ------- - ans : numpy array + ans : numpy.ndarray the array of distances slices : tuple of slices slices to slice the original marker to get the correct part of the array centre : tuple the centre index in the sliced array - mask : boolean numpy array + mask : numpy.ndarray of bool a binary mask for the values to consider """ radii = make_sure_ind(self.radii, len(ind)) # This should be unnecessary....................... - if self._untruncated is not None and self._untruncated.ndim != len( - ind): + if self._untruncated is not None and self._untruncated.ndim != len(ind): self._untruncated = None self._mask_all = None - if self._radii_changed or \ - self._untruncated is None or \ - self._mask_all is None: + if self._radii_changed or self._untruncated is None or self._mask_all is None: par = [] for radius in radii: radius_top = np.ceil(radius) - par.append(np.abs(np.arange(-radius_top, radius_top + 1))) - meshg = np.array(np.meshgrid(*par, indexing='ij')) - self._untruncated = np.sqrt(np.sum(meshg ** 2.0, axis=0)) - distance_mask = np.array( - [c / float(radii[i]) for i, c in enumerate(meshg)]) - self._mask_all = np.sum(distance_mask ** 2.0, axis=0) + par.append(abs(np.arange(-radius_top, radius_top + 1))) + meshg = np.array(np.meshgrid(*par, indexing="ij")) + self._untruncated = np.sqrt(np.sum(meshg**2.0, axis=0)) + distance_mask = np.array([c / float(radii[i]) for i, c in enumerate(meshg)]) + self._mask_all = np.sum(distance_mask**2.0, axis=0) self._radii_changed = False slices_return, centre = nearest_indices(shape, ind, np.ceil(radii)) slices_temp = () for radius, cent, _slice in zip(np.ceil(radii), centre, slices_return): - slices_temp += (slice(int(radius - cent), - int(_slice.stop - _slice.start + - radius - cent)),) + slices_temp += ( + slice( + int(radius - cent), int(_slice.stop - _slice.start + radius - cent) + ), + ) ans = self._untruncated[slices_temp].copy() mask = self._mask_all[slices_temp].copy() @@ -370,26 +357,17 @@ def values(self, ind): shape = marker.shape model = self.samf.model - distances, slices, _, mask_dist = self._get_distance_array( - shape, ind) + distances, slices, _, mask_dist = self._get_distance_array(shape, ind) # only use pixels that are calculated and "active" - mask_dist_calc = np.logical_and( - mask_dist, - marker[slices] == - - self.samf._scale) + mask_dist_calc = np.logical_and(mask_dist, marker[slices] == -self.samf._scale) distance_f = self.decay_function(distances) - weights_values = self.decay_function( - self.weight.map( - mask_dist_calc, - slices)) + weights_values = self.decay_function(self.weight.map(mask_dist_calc, slices)) ans = {} for component in model: if component.active_is_multidimensional: - mask = np.logical_and( - component._active_array[slices], - mask_dist_calc) + mask = np.logical_and(component._active_array[slices], mask_dist_calc) else: if component.active: mask = mask_dist_calc @@ -401,9 +379,8 @@ def values(self, ind): for par in component.parameters: if par.free: comp_dict[par.name] = np.average( - par.map[slices][mask]['values'], - weights=weight, - axis=0) + par.map[slices][mask]["values"], weights=weight, axis=0 + ) ans[component.name] = comp_dict return ans @@ -412,27 +389,26 @@ def plot(self, fig=None): Parameters ---------- - fig : {Image, None} + fig : :class:`hyperspy.api.signals.Signal2D` or numpy.ndarray if an already plotted image, then updates. Otherwise creates a new one. Returns ------- - fig: Image - the resulting image. If passed again, will be updated + :class:`hyperspy.api.signals.Signal2D` + The resulting 2D signal. If passed again, will be updated (computationally cheaper operation). """ marker = self.samf.metadata.marker.copy() if marker.ndim > 2: - marker = np.sum( - marker, tuple([i for i in range(0, marker.ndim - 2)])) + marker = np.sum(marker, tuple([i for i in range(0, marker.ndim - 2)])) elif marker.ndim < 2: marker = np.atleast_2d(marker) from hyperspy.signals import Signal2D - if not isinstance( - fig, Signal2D) or fig._plot.signal_plot.figure is None: + + if not isinstance(fig, Signal2D) or fig._plot.signal_plot.figure is None: fig = Signal2D(marker) fig.plot() self.close_plot = fig._plot.signal_plot.close @@ -452,16 +428,14 @@ class GlobalStrategy(SamfireStrategy): _saved_values = None def clean(self): - """Purges the currently saved values (not the database). - """ + """Purges the currently saved values (not the database).""" self._saved_values = None def __init__(self, name): self.name = name def refresh(self, overwrite, given_pixels=None): - """Refreshes the database (i.e. constructs it again from scratch) - """ + """Refreshes the database (i.e. constructs it again from scratch)""" scale = self.samf._scale mark = self.samf.metadata.marker mark = np.where(np.isnan(mark), np.inf, mark) @@ -479,14 +453,12 @@ def refresh(self, overwrite, given_pixels=None): self._update_database(None, 0) # to force to update def _update_marker(self, ind): - """Updates the SAMFire marker in the given pixel - """ + """Updates the SAMFire marker in the given pixel""" scale = self.samf._scale self.samf.metadata.marker[ind] = -scale def _package_values(self): - """Packages he current values to be sent to the segmenter - """ + """Packages he current values to be sent to the segmenter""" model = self.samf.model mask_calc = self.samf.metadata.marker < 0 ans = {} @@ -502,7 +474,7 @@ def _package_values(self): for par in component.parameters: if par.free: # only keeps active values and ravels - component_dict[par.name] = par.map[mask]['values'] + component_dict[par.name] = par.map[mask]["values"] ans[component.name] = component_dict return ans @@ -527,12 +499,12 @@ def plot(self, fig=None): Parameters ---------- - fig: {None, HistogramTilePlot} + fig : None of :class:`HistogramTilePlot` If given updates the plot. """ from hyperspy.drawing.tiles import HistogramTilePlot - kwargs = {'color': '#4C72B0'} + kwargs = {"color": "#4C72B0"} dbase = self.segmenter.database if dbase is None or not len(dbase): return fig diff --git a/hyperspy/samfire_utils/weights/red_chisq.py b/hyperspy/samfire_utils/weights/red_chisq.py index b71fcc4399..49e24261f2 100644 --- a/hyperspy/samfire_utils/weights/red_chisq.py +++ b/hyperspy/samfire_utils/weights/red_chisq.py @@ -1,35 +1,34 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np class ReducedChiSquaredWeight(object): - def __init__(self): self.expected = 1.0 self.model = None def function(self, ind): - return np.abs(self.model.red_chisq.data[ind] - self.expected) + return abs(self.model.red_chisq.data[ind] - self.expected) def map(self, mask, slices=slice(None, None)): thing = self.model.red_chisq.data[slices].copy() - thing = thing.astype('float64') + thing = thing.astype("float64") thing[np.logical_not(mask)] = np.nan - return np.abs(thing - self.expected) + return abs(thing - self.expected) diff --git a/hyperspy/signal.py b/hyperspy/signal.py index ff69fc61d5..a31de80a03 100644 --- a/hyperspy/signal.py +++ b/hyperspy/signal.py @@ -1,85 +1,189 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import copy -import warnings import inspect +import logging +import numbers +import warnings +from collections.abc import MutableMapping from contextlib import contextmanager from datetime import datetime +from functools import partial from itertools import product -import logging -from pint import UnitRegistry, UndefinedUnitError from pathlib import Path +import dask.array as da import numpy as np +import traits.api as t +from dask.diagnostics import ProgressBar +from matplotlib import pyplot as plt +from pint import UndefinedUnitError +from rsciio.utils import rgb_tools +from rsciio.utils.tools import ensure_directory from scipy import integrate from scipy import signal as sp_signal -import dask.array as da -from matplotlib import pyplot as plt -import traits.api as t -import numbers - -from hyperspy.axes import AxesManager -from hyperspy import io -from hyperspy.drawing import mpl_hie, mpl_hse, mpl_he -from hyperspy.learn.mva import MVA, LearningResults -import hyperspy.misc.utils -from hyperspy.misc.utils import DictionaryTreeBrowser -from hyperspy.drawing import signal as sigdraw -from hyperspy.defaults_parser import preferences -from hyperspy.misc.io.tools import ensure_directory -from hyperspy.misc.utils import iterable_not_string -from hyperspy.external.progressbar import progressbar -from hyperspy.exceptions import SignalDimensionError, DataDimensionError -from hyperspy.misc import rgb_tools -from hyperspy.misc.utils import underline, isiterable -from hyperspy.misc.hist_tools import histogram -from hyperspy.drawing.utils import animate_legend -from hyperspy.drawing.marker import markers_metadata_dict_to_markers -from hyperspy.misc.slicing import SpecialSlicers, FancySlicing -from hyperspy.misc.utils import slugify -from hyperspy.misc.utils import is_binned # remove in v2.0 +from scipy.interpolate import make_interp_spline +from tlz import concat + +from hyperspy.api import _ureg +from hyperspy.axes import AxesManager, create_axis +from hyperspy.docstrings.plot import ( + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT1D_DOCSTRING, + PLOT2D_KWARGS_DOCSTRING, +) from hyperspy.docstrings.signal import ( - ONE_AXIS_PARAMETER, MANY_AXIS_PARAMETER, OUT_ARG, NAN_FUNC, OPTIMIZE_ARG, - RECHUNK_ARG, SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG, - CLUSTER_SIGNALS_ARG, HISTOGRAM_BIN_ARGS, HISTOGRAM_MAX_BIN_ARGS) -from hyperspy.docstrings.plot import (BASE_PLOT_DOCSTRING, PLOT1D_DOCSTRING, - BASE_PLOT_DOCSTRING_PARAMETERS, - PLOT2D_KWARGS_DOCSTRING) + CLUSTER_SIGNALS_ARG, + HISTOGRAM_BIN_ARGS, + HISTOGRAM_MAX_BIN_ARGS, + HISTOGRAM_RANGE_ARGS, + LAZY_OUTPUT_ARG, + MANY_AXIS_PARAMETER, + NAN_FUNC, + NUM_WORKERS_ARG, + ONE_AXIS_PARAMETER, + OPTIMIZE_ARG, + OUT_ARG, + RECHUNK_ARG, + SHOW_PROGRESSBAR_ARG, +) from hyperspy.docstrings.utils import REBIN_ARGS -from hyperspy.events import Events, Event +from hyperspy.drawing import mpl_he, mpl_hie, mpl_hse +from hyperspy.drawing import signal as sigdraw +from hyperspy.drawing.markers import markers_dict_to_markers +from hyperspy.drawing.utils import animate_legend +from hyperspy.events import Event, Events +from hyperspy.exceptions import ( + DataDimensionError, + LazyCupyConversion, + SignalDimensionError, + VisibleDeprecationWarning, +) from hyperspy.interactive import interactive -from hyperspy.misc.signal_tools import (are_signals_aligned, - broadcast_signals) -from hyperspy.misc.math_tools import outer_nd, hann_window_nth_order, check_random_state -from hyperspy.exceptions import VisibleDeprecationWarning - +from hyperspy.io import assign_signal_subclass +from hyperspy.io import save as io_save +from hyperspy.learn.mva import MVA, LearningResults +from hyperspy.misc.array_tools import rebin as array_rebin +from hyperspy.misc.hist_tools import _set_histogram_metadata, histogram +from hyperspy.misc.math_tools import check_random_state, hann_window_nth_order, outer_nd +from hyperspy.misc.signal_tools import are_signals_aligned, broadcast_signals +from hyperspy.misc.slicing import FancySlicing, SpecialSlicers +from hyperspy.misc.utils import ( + DictionaryTreeBrowser, + _get_block_pattern, + add_scalar_axis, + dummy_context_manager, + guess_output_signal_size, + is_cupy_array, + isiterable, + iterable_not_string, + process_function_blockwise, + rollelem, + slugify, + to_numpy, + underline, +) _logger = logging.getLogger(__name__) +try: + import cupy as cp + + CUPY_INSTALLED = True # pragma: no cover +except ImportError: + CUPY_INSTALLED = False + + +def _dic_get_hs_obj_paths(dic, axes_managers, signals, containers, axes): + for key in dic: + if key.startswith("_sig_"): + signals.append((key, dic)) + elif key.startswith("_hspy_AxesManager_"): + axes_managers.append((key, dic)) + elif key.startswith("_hspy_Axis_"): + axes.append((key, dic)) + elif isinstance(dic[key], (list, tuple)): + signals = [] + # Support storing signals in containers + for i, item in enumerate(dic[key]): + if isinstance(item, dict) and "_sig_" in item: + signals.append(i) + if signals: + containers.append(((dic, key), signals)) + elif isinstance(dic[key], dict): + _dic_get_hs_obj_paths( + dic[key], + axes_managers=axes_managers, + signals=signals, + containers=containers, + axes=axes, + ) -class ModelManager(object): - """Container for models +def _obj_in_dict2hspy(dic, lazy): """ + Recursively walk nested dicts substituting dicts with their hyperspy + object where relevant + + Parameters + ---------- + d: dictionary + The nested dictionary + lazy: bool + + """ + from hyperspy.io import dict2signal + + axes_managers, signals, containers, axes = [], [], [], [] + _dic_get_hs_obj_paths( + dic, + axes_managers=axes_managers, + signals=signals, + containers=containers, + axes=axes, + ) + for key, dic in axes_managers: + dic[key[len("_hspy_AxesManager_") :]] = AxesManager(dic[key]) + del dic[key] + for key, dic in axes: + dic[key[len("_hspy_Axis_") :]] = create_axis(**dic[key]) + for key, dic in signals: + dic[key[len("_sig_") :]] = dict2signal(dic[key], lazy=lazy) + del dic[key] + for dickey, signals_idx in containers: + dic, key = dickey + container = dic[key] + to_tuple = False + if type(container) is tuple: + container = list(container) + to_tuple = True + for idx in signals_idx: + container[idx] = dict2signal(container[idx]["_sig_"], lazy=lazy) + if to_tuple: + dic[key] = tuple(container) - class ModelStub(object): +class ModelManager(object): + """Container for models""" + + class ModelStub(object): def __init__(self, mm, name): self._name = name self._mm = mm @@ -88,8 +192,7 @@ def __init__(self, mm, name): self.pop = lambda: mm.pop(self._name) self.restore.__doc__ = "Returns the stored model" self.remove.__doc__ = "Removes the stored model" - self.pop.__doc__ = \ - "Returns the stored model and removes it from storage" + self.pop.__doc__ = "Returns the stored model and removes it from storage" def __repr__(self): return repr(self._mm._models[self._name]) @@ -102,30 +205,32 @@ def __init__(self, signal, dictionary=None): def _add_dictionary(self, dictionary=None): if dictionary is not None: for k, v in dictionary.items(): - if k.startswith('_') or k in ['restore', 'remove']: + if k.startswith("_") or k in ["restore", "remove"]: raise KeyError("Can't add dictionary with key '%s'" % k) k = slugify(k, True) self._models.set_item(k, v) setattr(self, k, self.ModelStub(self, k)) def _set_nice_description(self, node, names): - ans = {'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'dimensions': self._signal.axes_manager._get_dimension_str(), - } + ans = { + "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "dimensions": self._signal.axes_manager._get_dimension_str(), + } node.add_dictionary(ans) for n in names: - node.add_node('components.' + n) + node.add_node("components." + n) def _save(self, name, dictionary): - - _abc = 'abcdefghijklmnopqrstuvwxyz' + _abc = "abcdefghijklmnopqrstuvwxyz" def get_letter(models): howmany = len(models) if not howmany: - return 'a' + return "a" order = int(np.log(howmany) / np.log(26)) + 1 - letters = [_abc, ] * order + letters = [ + _abc, + ] * order for comb in product(*letters): guess = "".join(comb) if guess not in models.keys(): @@ -141,10 +246,10 @@ def get_letter(models): self._models.add_node(name) node = self._models.get_item(name) - names = [c['name'] for c in dictionary['components']] + names = [c["name"] for c in dictionary["components"]] self._set_nice_description(node, names) - node.set_item('_dict', dictionary) + node.set_item("_dict", dictionary) setattr(self, name, self.ModelStub(self, name)) def store(self, model, name=None): @@ -152,12 +257,12 @@ def store(self, model, name=None): Parameters ---------- - model : :py:class:`~hyperspy.model.BaseModel` (or subclass) + model : :class:`~hyperspy.model.BaseModel` (or subclass) The model to store in the signal name : str or None The name for the model to be stored with - See also + See Also -------- remove restore @@ -167,22 +272,22 @@ def store(self, model, name=None): if model.signal is self._signal: self._save(name, model.as_dictionary()) else: - raise ValueError("The model is created from a different signal, " - "you should store it there") + raise ValueError( + "The model is created from a different signal, " + "you should store it there" + ) def _check_name(self, name, existing=False): if not isinstance(name, str): - raise KeyError('Name has to be a string') - if name.startswith('_'): + raise KeyError("Name has to be a string") + if name.startswith("_"): raise KeyError('Name cannot start with "_" symbol') - if '.' in name: + if "." in name: raise KeyError('Name cannot contain dots (".")') name = slugify(name, True) if existing: if name not in self._models: - raise KeyError( - "Model named '%s' is not currently stored" % - name) + raise KeyError("Model named '%s' is not currently stored" % name) return name def remove(self, name): @@ -193,7 +298,7 @@ def remove(self, name): name : str The name of the model to remove - See also + See Also -------- restore store @@ -212,7 +317,7 @@ def pop(self, name): name : str The name of the model to restore and remove - See also + See Also -------- restore store @@ -232,7 +337,7 @@ def restore(self, name): name : str The name of the model to restore - See also + See Also -------- remove store @@ -240,7 +345,19 @@ def restore(self, name): """ name = self._check_name(name, True) - d = self._models.get_item(name + '._dict').as_dictionary() + d = self._models.get_item(name + "._dict").as_dictionary() + for c in d["components"]: + # Restoring of polynomials saved with Hyperspy = 1.5 + # the order initialisation argument was missing from the whitelist + # We patch the dictionary to be able to load these files + if ( + c["_id_name"] == "Polynomial" + # make sure that this is not an old polynomial + and c["parameters"][0]["_id_name"] != "coefficients" + and "order" not in c.keys() + ): + c["_whitelist"]["order"] = "init" + c["order"] = len(c["parameters"]) - 1 return self._signal.create_model(dictionary=copy.deepcopy(d)) def __repr__(self): @@ -257,14 +374,23 @@ def __getitem__(self, name): class MVATools(object): # TODO: All of the plotting methods here should move to drawing - def _plot_factors_or_pchars(self, factors, comp_ids=None, - calibrate=True, avg_char=False, - same_window=True, comp_label='PC', - img_data=None, - plot_shifts=True, plot_char=4, - cmap=plt.cm.gray, quiver_color='white', - vector_scale=1, - per_row=3, ax=None): + def _plot_factors_or_pchars( + self, + factors, + comp_ids=None, + calibrate=True, + avg_char=False, + same_window=True, + comp_label="PC", + img_data=None, + plot_shifts=True, + plot_char=4, + cmap=plt.cm.gray, + quiver_color="white", + vector_scale=1, + per_row=3, + ax=None, + ): """Plot components from PCA or ICA, or peak characteristics. Parameters @@ -324,7 +450,7 @@ def _plot_factors_or_pchars(self, factors, comp_ids=None, if comp_ids is None: comp_ids = range(factors.shape[1]) - elif not hasattr(comp_ids, '__iter__'): + elif not hasattr(comp_ids, "__iter__"): comp_ids = range(comp_ids) n = len(comp_ids) @@ -348,7 +474,7 @@ def _plot_factors_or_pchars(self, factors, comp_ids=None, else: if i > 0: f = plt.figure() - plt.title('%s' % comp_label) + plt.title("%s" % comp_label) ax = f.add_subplot(111) ax = sigdraw._plot_1D_component( factors=factors, @@ -357,27 +483,32 @@ def _plot_factors_or_pchars(self, factors, comp_ids=None, ax=ax, calibrate=calibrate, comp_label=comp_label, - same_window=same_window) + same_window=same_window, + ) if same_window: - plt.legend(ncol=factors.shape[1] // 2, loc='best') + plt.legend(ncol=factors.shape[1] // 2, loc="best") elif self.axes_manager.signal_dimension == 2: if same_window: ax = f.add_subplot(rows, per_row, i + 1) else: if i > 0: f = plt.figure() - plt.title('%s' % comp_label) + plt.title("%s" % comp_label) ax = f.add_subplot(111) - sigdraw._plot_2D_component(factors=factors, - idx=comp_ids[i], - axes_manager=self.axes_manager, - calibrate=calibrate, ax=ax, - cmap=cmap, comp_label=comp_label) + sigdraw._plot_2D_component( + factors=factors, + idx=comp_ids[i], + axes_manager=self.axes_manager, + calibrate=calibrate, + ax=ax, + cmap=cmap, + comp_label=comp_label, + ) if not same_window: fig_list.append(f) if same_window: # Main title for same window - title = '%s' % comp_label + title = "%s" % comp_label if self.axes_manager.signal_dimension == 1: plt.title(title) else: @@ -391,17 +522,26 @@ def _plot_factors_or_pchars(self, factors, comp_ids=None, else: return f - def _plot_loadings(self, loadings, comp_ids, calibrate=True, - same_window=True, comp_label=None, - with_factors=False, factors=None, - cmap=plt.cm.gray, no_nans=False, per_row=3, - axes_decor='all'): + def _plot_loadings( + self, + loadings, + comp_ids, + calibrate=True, + same_window=True, + comp_label=None, + with_factors=False, + factors=None, + cmap=plt.cm.gray, + no_nans=False, + per_row=3, + axes_decor="all", + ): if same_window is None: same_window = True if comp_ids is None: comp_ids = range(loadings.shape[0]) - elif not hasattr(comp_ids, '__iter__'): + elif not hasattr(comp_ids, "__iter__"): comp_ids = range(comp_ids) n = len(comp_ids) @@ -425,7 +565,7 @@ def _plot_loadings(self, loadings, comp_ids, calibrate=True, else: if i > 0: f = plt.figure() - plt.title('%s' % comp_label) + plt.title("%s" % comp_label) ax = f.add_subplot(111) elif self.axes_manager.navigation_dimension == 2: if same_window: @@ -433,17 +573,24 @@ def _plot_loadings(self, loadings, comp_ids, calibrate=True, else: if i > 0: f = plt.figure() - plt.title('%s' % comp_label) + plt.title("%s" % comp_label) ax = f.add_subplot(111) sigdraw._plot_loading( - loadings, idx=comp_ids[i], axes_manager=self.axes_manager, - no_nans=no_nans, calibrate=calibrate, cmap=cmap, - comp_label=comp_label, ax=ax, same_window=same_window, - axes_decor=axes_decor) + loadings, + idx=comp_ids[i], + axes_manager=self.axes_manager, + no_nans=no_nans, + calibrate=calibrate, + cmap=cmap, + comp_label=comp_label, + ax=ax, + same_window=same_window, + axes_decor=axes_decor, + ) if not same_window: fig_list.append(f) if same_window: # Main title for same window - title = '%s' % comp_label + title = "%s" % comp_label if self.axes_manager.navigation_dimension == 1: plt.title(title) else: @@ -455,58 +602,66 @@ def _plot_loadings(self, loadings, comp_ids, calibrate=True, if not same_window: if with_factors: return fig_list, self._plot_factors_or_pchars( - factors, comp_ids=comp_ids, calibrate=calibrate, - same_window=same_window, comp_label=comp_label, - per_row=per_row) + factors, + comp_ids=comp_ids, + calibrate=calibrate, + same_window=same_window, + comp_label=comp_label, + per_row=per_row, + ) else: return fig_list else: if self.axes_manager.navigation_dimension == 1: - plt.legend(ncol=loadings.shape[0] // 2, loc='best') + plt.legend(ncol=loadings.shape[0] // 2, loc="best") animate_legend(f) if with_factors: - return f, self._plot_factors_or_pchars(factors, - comp_ids=comp_ids, - calibrate=calibrate, - same_window=same_window, - comp_label=comp_label, - per_row=per_row) + return f, self._plot_factors_or_pchars( + factors, + comp_ids=comp_ids, + calibrate=calibrate, + same_window=same_window, + comp_label=comp_label, + per_row=per_row, + ) else: return f - def _export_factors(self, - factors, - folder=None, - comp_ids=None, - multiple_files=True, - save_figures=False, - save_figures_format='png', - factor_prefix=None, - factor_format=None, - comp_label=None, - cmap=plt.cm.gray, - plot_shifts=True, - plot_char=4, - img_data=None, - same_window=False, - calibrate=True, - quiver_color='white', - vector_scale=1, - no_nans=True, per_row=3): - - from hyperspy._signals.signal2d import Signal2D + def _export_factors( + self, + factors, + folder=None, + comp_ids=None, + multiple_files=True, + save_figures=False, + save_figures_format="png", + factor_prefix=None, + factor_format=None, + comp_label=None, + cmap=plt.cm.gray, + plot_shifts=True, + plot_char=4, + img_data=None, + same_window=False, + calibrate=True, + quiver_color="white", + vector_scale=1, + no_nans=True, + per_row=3, + ): from hyperspy._signals.signal1d import Signal1D + from hyperspy._signals.signal2d import Signal2D if multiple_files is None: multiple_files = True if factor_format is None: - factor_format = 'hspy' + factor_format = "hspy" # Select the desired factors if comp_ids is None: comp_ids = range(factors.shape[1]) - elif not hasattr(comp_ids, '__iter__'): + elif not hasattr(comp_ids, "__iter__"): comp_ids = range(comp_ids) mask = np.zeros(factors.shape[1], dtype=np.bool) for idx in comp_ids: @@ -515,25 +670,29 @@ def _export_factors(self, if save_figures is True: plt.ioff() - fac_plots = self._plot_factors_or_pchars(factors, - comp_ids=comp_ids, - same_window=same_window, - comp_label=comp_label, - img_data=img_data, - plot_shifts=plot_shifts, - plot_char=plot_char, - cmap=cmap, - per_row=per_row, - quiver_color=quiver_color, - vector_scale=vector_scale) + fac_plots = self._plot_factors_or_pchars( + factors, + comp_ids=comp_ids, + same_window=same_window, + comp_label=comp_label, + img_data=img_data, + plot_shifts=plot_shifts, + plot_char=plot_char, + cmap=cmap, + per_row=per_row, + quiver_color=quiver_color, + vector_scale=vector_scale, + ) for idx in range(len(comp_ids)): - filename = '%s_%02i.%s' % (factor_prefix, comp_ids[idx], - save_figures_format) + filename = "%s_%02i.%s" % ( + factor_prefix, + comp_ids[idx], + save_figures_format, + ) if folder is not None: filename = Path(folder, filename) ensure_directory(filename) - _args = {'dpi': 600, - 'format': save_figures_format} + _args = {"dpi": 600, "format": save_figures_format} fac_plots[idx].savefig(filename, **_args) plt.ion() @@ -543,116 +702,139 @@ def _export_factors(self, axes_dicts = [] axes = self.axes_manager.signal_axes[::-1] shape = (axes[1].size, axes[0].size) - factor_data = np.rollaxis( - factors.reshape((shape[0], shape[1], -1)), 2) + factor_data = np.rollaxis(factors.reshape((shape[0], shape[1], -1)), 2) axes_dicts.append(axes[0].get_axis_dictionary()) axes_dicts.append(axes[1].get_axis_dictionary()) - axes_dicts.append({'name': 'factor_index', - 'scale': 1., - 'offset': 0., - 'size': int(factors.shape[1]), - 'units': 'factor', - 'index_in_array': 0, }) - s = Signal2D(factor_data, - axes=axes_dicts, - metadata={ - 'General': {'title': '%s from %s' % ( - factor_prefix, - self.metadata.General.title), - }}) + axes_dicts.append( + { + "name": "factor_index", + "scale": 1.0, + "offset": 0.0, + "size": int(factors.shape[1]), + "units": "factor", + "index_in_array": 0, + } + ) + s = Signal2D( + factor_data, + axes=axes_dicts, + metadata={ + "General": { + "title": "%s from %s" + % (factor_prefix, self.metadata.General.title), + } + }, + ) elif self.axes_manager.signal_dimension == 1: - axes = [self.axes_manager.signal_axes[0].get_axis_dictionary(), - {'name': 'factor_index', - 'scale': 1., - 'offset': 0., - 'size': int(factors.shape[1]), - 'units': 'factor', - 'index_in_array': 0, - }] - axes[0]['index_in_array'] = 1 + axes = [ + self.axes_manager.signal_axes[0].get_axis_dictionary(), + { + "name": "factor_index", + "scale": 1.0, + "offset": 0.0, + "size": int(factors.shape[1]), + "units": "factor", + "index_in_array": 0, + }, + ] + axes[0]["index_in_array"] = 1 s = Signal1D( - factors.T, axes=axes, metadata={ + factors.T, + axes=axes, + metadata={ "General": { - 'title': '%s from %s' % - (factor_prefix, self.metadata.General.title), }}) - filename = '%ss.%s' % (factor_prefix, factor_format) + "title": "%s from %s" + % (factor_prefix, self.metadata.General.title), + } + }, + ) + filename = "%ss.%s" % (factor_prefix, factor_format) if folder is not None: filename = Path(folder, filename) s.save(filename) else: # Separate files if self.axes_manager.signal_dimension == 1: - - axis_dict = self.axes_manager.signal_axes[0].\ - get_axis_dictionary() - axis_dict['index_in_array'] = 0 + axis_dict = self.axes_manager.signal_axes[0].get_axis_dictionary() + axis_dict["index_in_array"] = 0 for dim, index in zip(comp_ids, range(len(comp_ids))): - s = Signal1D(factors[:, index], - axes=[axis_dict, ], - metadata={ - "General": {'title': '%s from %s' % ( - factor_prefix, - self.metadata.General.title), - }}) - filename = '%s-%i.%s' % (factor_prefix, - dim, - factor_format) + s = Signal1D( + factors[:, index], + axes=[ + axis_dict, + ], + metadata={ + "General": { + "title": "%s from %s" + % (factor_prefix, self.metadata.General.title), + } + }, + ) + filename = "%s-%i.%s" % (factor_prefix, dim, factor_format) if folder is not None: filename = Path(folder, filename) s.save(filename) if self.axes_manager.signal_dimension == 2: axes = self.axes_manager.signal_axes - axes_dicts = [axes[0].get_axis_dictionary(), - axes[1].get_axis_dictionary()] - axes_dicts[0]['index_in_array'] = 0 - axes_dicts[1]['index_in_array'] = 1 + axes_dicts = [ + axes[0].get_axis_dictionary(), + axes[1].get_axis_dictionary(), + ] + axes_dicts[0]["index_in_array"] = 0 + axes_dicts[1]["index_in_array"] = 1 factor_data = factors.reshape( - self.axes_manager._signal_shape_in_array + [-1, ]) + self.axes_manager._signal_shape_in_array + + [ + -1, + ] + ) for dim, index in zip(comp_ids, range(len(comp_ids))): - im = Signal2D(factor_data[..., index], - axes=axes_dicts, - metadata={ - "General": {'title': '%s from %s' % ( - factor_prefix, - self.metadata.General.title), - }}) - filename = '%s-%i.%s' % (factor_prefix, - dim, - factor_format) + im = Signal2D( + factor_data[..., index], + axes=axes_dicts, + metadata={ + "General": { + "title": "%s from %s" + % (factor_prefix, self.metadata.General.title), + } + }, + ) + filename = "%s-%i.%s" % (factor_prefix, dim, factor_format) if folder is not None: filename = Path(folder, filename) im.save(filename) - def _export_loadings(self, - loadings, - folder=None, - comp_ids=None, - multiple_files=True, - loading_prefix=None, - loading_format="hspy", - save_figures_format='png', - comp_label=None, - cmap=plt.cm.gray, - save_figures=False, - same_window=False, - calibrate=True, - no_nans=True, - per_row=3): - - from hyperspy._signals.signal2d import Signal2D + def _export_loadings( + self, + loadings, + folder=None, + comp_ids=None, + multiple_files=True, + loading_prefix=None, + loading_format="hspy", + save_figures_format="png", + comp_label=None, + cmap=plt.cm.gray, + save_figures=False, + same_window=False, + calibrate=True, + no_nans=True, + per_row=3, + ): from hyperspy._signals.signal1d import Signal1D + from hyperspy._signals.signal2d import Signal2D if multiple_files is None: multiple_files = True if loading_format is None: - loading_format = 'hspy' + loading_format = "hspy" if comp_ids is None: comp_ids = range(loadings.shape[0]) - elif not hasattr(comp_ids, '__iter__'): + elif not hasattr(comp_ids, "__iter__"): comp_ids = range(comp_ids) mask = np.zeros(loadings.shape[0], dtype=np.bool) for idx in comp_ids: @@ -661,20 +843,26 @@ def _export_loadings(self, if save_figures is True: plt.ioff() - sc_plots = self._plot_loadings(loadings, comp_ids=comp_ids, - calibrate=calibrate, - same_window=same_window, - comp_label=comp_label, - cmap=cmap, no_nans=no_nans, - per_row=per_row) + sc_plots = self._plot_loadings( + loadings, + comp_ids=comp_ids, + calibrate=calibrate, + same_window=same_window, + comp_label=comp_label, + cmap=cmap, + no_nans=no_nans, + per_row=per_row, + ) for idx in range(len(comp_ids)): - filename = '%s_%02i.%s' % (loading_prefix, comp_ids[idx], - save_figures_format) + filename = "%s_%02i.%s" % ( + loading_prefix, + comp_ids[idx], + save_figures_format, + ) if folder is not None: filename = Path(folder, filename) ensure_directory(filename) - _args = {'dpi': 600, - 'format': save_figures_format} + _args = {"dpi": 600, "format": save_figures_format} sc_plots[idx].savefig(filename, **_args) plt.ion() elif multiple_files is False: @@ -684,55 +872,69 @@ def _export_loadings(self, shape = (axes[1].size, axes[0].size) loading_data = loadings.reshape((-1, shape[0], shape[1])) axes_dicts.append(axes[0].get_axis_dictionary()) - axes_dicts[0]['index_in_array'] = 1 + axes_dicts[0]["index_in_array"] = 1 axes_dicts.append(axes[1].get_axis_dictionary()) - axes_dicts[1]['index_in_array'] = 2 - axes_dicts.append({'name': 'loading_index', - 'scale': 1., - 'offset': 0., - 'size': int(loadings.shape[0]), - 'units': 'factor', - 'index_in_array': 0, }) - s = Signal2D(loading_data, - axes=axes_dicts, - metadata={ - "General": {'title': '%s from %s' % ( - loading_prefix, - self.metadata.General.title), - }}) + axes_dicts[1]["index_in_array"] = 2 + axes_dicts.append( + { + "name": "loading_index", + "scale": 1.0, + "offset": 0.0, + "size": int(loadings.shape[0]), + "units": "factor", + "index_in_array": 0, + } + ) + s = Signal2D( + loading_data, + axes=axes_dicts, + metadata={ + "General": { + "title": "%s from %s" + % (loading_prefix, self.metadata.General.title), + } + }, + ) elif self.axes_manager.navigation_dimension == 1: - cal_axis = self.axes_manager.navigation_axes[0].\ - get_axis_dictionary() - cal_axis['index_in_array'] = 1 - axes = [{'name': 'loading_index', - 'scale': 1., - 'offset': 0., - 'size': int(loadings.shape[0]), - 'units': 'comp_id', - 'index_in_array': 0, }, - cal_axis] - s = Signal2D(loadings, - axes=axes, - metadata={ - "General": {'title': '%s from %s' % ( - loading_prefix, - self.metadata.General.title), - }}) - filename = '%ss.%s' % (loading_prefix, loading_format) + cal_axis = self.axes_manager.navigation_axes[0].get_axis_dictionary() + cal_axis["index_in_array"] = 1 + axes = [ + { + "name": "loading_index", + "scale": 1.0, + "offset": 0.0, + "size": int(loadings.shape[0]), + "units": "comp_id", + "index_in_array": 0, + }, + cal_axis, + ] + s = Signal2D( + loadings, + axes=axes, + metadata={ + "General": { + "title": "%s from %s" + % (loading_prefix, self.metadata.General.title), + } + }, + ) + filename = "%ss.%s" % (loading_prefix, loading_format) if folder is not None: filename = Path(folder, filename) s.save(filename) else: # Separate files if self.axes_manager.navigation_dimension == 1: - axis_dict = self.axes_manager.navigation_axes[0].\ - get_axis_dictionary() - axis_dict['index_in_array'] = 0 + axis_dict = self.axes_manager.navigation_axes[0].get_axis_dictionary() + axis_dict["index_in_array"] = 0 for dim, index in zip(comp_ids, range(len(comp_ids))): - s = Signal1D(loadings[index], - axes=[axis_dict, ]) - filename = '%s-%i.%s' % (loading_prefix, - dim, - loading_format) + s = Signal1D( + loadings[index], + axes=[ + axis_dict, + ], + ) + filename = "%s-%i.%s" % (loading_prefix, dim, loading_format) if folder is not None: filename = Path(folder, filename) s.save(filename) @@ -742,44 +944,46 @@ def _export_loadings(self, shape = (axes[0].size, axes[1].size) loading_data = loadings.reshape((-1, shape[0], shape[1])) axes_dicts.append(axes[0].get_axis_dictionary()) - axes_dicts[0]['index_in_array'] = 0 + axes_dicts[0]["index_in_array"] = 0 axes_dicts.append(axes[1].get_axis_dictionary()) - axes_dicts[1]['index_in_array'] = 1 + axes_dicts[1]["index_in_array"] = 1 for dim, index in zip(comp_ids, range(len(comp_ids))): - s = Signal2D(loading_data[index, ...], - axes=axes_dicts, - metadata={ - "General": {'title': '%s from %s' % ( - loading_prefix, - self.metadata.General.title), - }}) - filename = '%s-%i.%s' % (loading_prefix, - dim, - loading_format) + s = Signal2D( + loading_data[index, ...], + axes=axes_dicts, + metadata={ + "General": { + "title": "%s from %s" + % (loading_prefix, self.metadata.General.title), + } + }, + ) + filename = "%s-%i.%s" % (loading_prefix, dim, loading_format) if folder is not None: filename = Path(folder, filename) s.save(filename) - def plot_decomposition_factors(self, - comp_ids=None, - calibrate=True, - same_window=True, - title=None, - cmap=plt.cm.gray, - per_row=3, - **kwargs, - ): + def plot_decomposition_factors( + self, + comp_ids=None, + calibrate=True, + same_window=True, + title=None, + cmap=plt.cm.gray, + per_row=3, + **kwargs, + ): """Plot factors from a decomposition. In case of 1D signal axis, each factors line can be toggled on and off by clicking on their corresponding line in the legend. Parameters ---------- - comp_ids : None, int, or list (of ints) + comp_ids : None, int or list of int If `comp_ids` is ``None``, maps of all components will be returned if the `output_dimension` was defined when executing - :py:meth:`~hyperspy.learn.mva.MVA.decomposition`. Otherwise it - raises a :py:exc:`ValueError`. + :meth:`~hyperspy.api.signals.BaseSignal.decomposition`. Otherwise it + raises a :exc:`ValueError`. If `comp_ids` is an int, maps of components with ids from 0 to the given value will be returned. If `comp_ids` is a list of ints, maps of components with ids contained in the list will be @@ -791,8 +995,9 @@ def plot_decomposition_factors(self, If ``True``, plots each factor to the same window. They are not scaled. Default is ``True``. title : str - Title of the plot. - cmap : :py:class:`~matplotlib.colors.Colormap` + Title of the matplotlib plot or label of the line in the legend + when the dimension of factors is 1 and ``same_window`` is ``True``. + cmap : :class:`~matplotlib.colors.Colormap` The colormap used for the factor images, or for peak characteristics. Default is the matplotlib gray colormap (``plt.cm.gray``). @@ -800,19 +1005,23 @@ def plot_decomposition_factors(self, The number of plots in each row, when the `same_window` parameter is ``True``. - See also + See Also -------- plot_decomposition_loadings, plot_decomposition_results """ if self.axes_manager.signal_dimension > 2: - raise NotImplementedError("This method cannot plot factors of " - "signals of dimension higher than 2." - "You can use " - "`plot_decomposition_results` instead.") + raise NotImplementedError( + "This method cannot plot factors of " + "signals of dimension higher than 2." + "You can use " + "`plot_decomposition_results` instead." + ) if self.learning_results.factors is None: - raise RuntimeError("No learning results found. A 'decomposition' " - "needs to be performed first.") + raise RuntimeError( + "No learning results found. A 'decomposition' " + "needs to be performed first." + ) if same_window is None: same_window = True if self.learning_results.factors is None: @@ -824,20 +1033,22 @@ def plot_decomposition_factors(self, else: raise ValueError( "Please provide the number of components to plot via the " - "`comp_ids` argument") - comp_label = kwargs.get("comp_label", None) - title = _change_API_comp_label(title, comp_label) + "`comp_ids` argument." + ) if title is None: - title = self._get_plot_title('Decomposition factors of', - same_window=same_window) + title = self._get_plot_title( + "Decomposition factors of", same_window=same_window + ) - return self._plot_factors_or_pchars(factors, - comp_ids=comp_ids, - calibrate=calibrate, - same_window=same_window, - comp_label=title, - cmap=cmap, - per_row=per_row) + return self._plot_factors_or_pchars( + factors, + comp_ids=comp_ids, + calibrate=calibrate, + same_window=same_window, + comp_label=title, + cmap=cmap, + per_row=per_row, + ) def plot_bss_factors( self, @@ -848,7 +1059,7 @@ def plot_bss_factors( cmap=plt.cm.gray, per_row=3, **kwargs, - ): + ): """Plot factors from blind source separation results. In case of 1D signal axis, each factors line can be toggled on and off by clicking on their corresponding line in the legend. @@ -856,7 +1067,7 @@ def plot_bss_factors( Parameters ---------- - comp_ids : None, int, or list (of ints) + comp_ids : None, int, or list of int If `comp_ids` is ``None``, maps of all components will be returned. If it is an int, maps of components with ids from 0 to the given value will be returned. If `comp_ids` is a list of @@ -869,8 +1080,9 @@ def plot_bss_factors( if ``True``, plots each factor to the same window. They are not scaled. Default is ``True``. title : str - Title of the plot. - cmap : :py:class:`~matplotlib.colors.Colormap` + Title of the matplotlib plot or label of the line in the legend + when the dimension of factors is 1 and ``same_window`` is ``True``. + cmap : :class:`~matplotlib.colors.Colormap` The colormap used for the factor images, or for peak characteristics. Default is the matplotlib gray colormap (``plt.cm.gray``). @@ -878,60 +1090,64 @@ def plot_bss_factors( The number of plots in each row, when the `same_window` parameter is ``True``. - See also + See Also -------- plot_bss_loadings, plot_bss_results """ if self.axes_manager.signal_dimension > 2: - raise NotImplementedError("This method cannot plot factors of " - "signals of dimension higher than 2." - "You can use " - "`plot_decomposition_results` instead.") + raise NotImplementedError( + "This method cannot plot factors of " + "signals of dimension higher than 2." + "You can use " + "`plot_decomposition_results` instead." + ) if self.learning_results.bss_factors is None: - raise RuntimeError("No learning results found. A " - "'blind_source_separation' needs to be " - "performed first.") + raise RuntimeError( + "No learning results found. A " + "'blind_source_separation' needs to be " + "performed first." + ) if same_window is None: same_window = True factors = self.learning_results.bss_factors - comp_label = kwargs.get("comp_label", None) - title = _change_API_comp_label(title, comp_label) if title is None: - title = self._get_plot_title('BSS factors of', - same_window=same_window) - - return self._plot_factors_or_pchars(factors, - comp_ids=comp_ids, - calibrate=calibrate, - same_window=same_window, - comp_label=title, - per_row=per_row) - - def plot_decomposition_loadings(self, - comp_ids=None, - calibrate=True, - same_window=True, - title=None, - with_factors=False, - cmap=plt.cm.gray, - no_nans=False, - per_row=3, - axes_decor='all', - **kwargs, - ): + title = self._get_plot_title("BSS factors of", same_window=same_window) + + return self._plot_factors_or_pchars( + factors, + comp_ids=comp_ids, + calibrate=calibrate, + same_window=same_window, + comp_label=title, + per_row=per_row, + ) + + def plot_decomposition_loadings( + self, + comp_ids=None, + calibrate=True, + same_window=True, + title=None, + with_factors=False, + cmap=plt.cm.gray, + no_nans=False, + per_row=3, + axes_decor="all", + **kwargs, + ): """Plot loadings from a decomposition. In case of 1D navigation axis, each loading line can be toggled on and off by clicking on the legended line. Parameters ---------- - comp_ids : None, int, or list (of ints) + comp_ids : None, int, or list of int If `comp_ids` is ``None``, maps of all components will be returned if the `output_dimension` was defined when executing - :py:meth:`~hyperspy.learn.mva.MVA.decomposition`. - Otherwise it raises a :py:exc:`ValueError`. + :meth:`~hyperspy.api.signals.BaseSignal.decomposition`. + Otherwise it raises a :exc:`ValueError`. If `comp_ids` is an int, maps of components with ids from 0 to the given value will be returned. If `comp_ids` is a list of ints, maps of components with ids contained in the list will be @@ -943,11 +1159,12 @@ def plot_decomposition_loadings(self, if ``True``, plots each factor to the same window. They are not scaled. Default is ``True``. title : str - Title of the plot. + Title of the matplotlib plot or label of the line in the legend + when the dimension of loadings is 1 and ``same_window`` is ``True``. with_factors : bool If ``True``, also returns figure(s) with the factors for the given comp_ids. - cmap : :py:class:`~matplotlib.colors.Colormap` + cmap : :class:`~matplotlib.colors.Colormap` The colormap used for the loadings images, or for peak characteristics. Default is the matplotlib gray colormap (``plt.cm.gray``). @@ -966,19 +1183,23 @@ def plot_decomposition_loadings(self, If ``None``, no axis decorations will be shown, but ticks/frame will. - See also + See Also -------- plot_decomposition_factors, plot_decomposition_results """ if self.axes_manager.navigation_dimension > 2: - raise NotImplementedError("This method cannot plot loadings of " - "dimension higher than 2." - "You can use " - "`plot_decomposition_results` instead.") + raise NotImplementedError( + "This method cannot plot loadings of " + "dimension higher than 2." + "You can use " + "`plot_decomposition_results` instead." + ) if self.learning_results.loadings is None: - raise RuntimeError("No learning results found. A 'decomposition' " - "needs to be performed first.") + raise RuntimeError( + "No learning results found. A 'decomposition' " + "needs to be performed first." + ) if same_window is None: same_window = True if self.learning_results.loadings is None: @@ -995,12 +1216,13 @@ def plot_decomposition_loadings(self, else: raise ValueError( "Please provide the number of components to plot via the " - "`comp_ids` argument") - comp_label = kwargs.get("comp_label", None) - title = _change_API_comp_label(title, comp_label) + "`comp_ids` argument" + ) + if title is None: title = self._get_plot_title( - 'Decomposition loadings of', same_window=same_window) + "Decomposition loadings of", same_window=same_window + ) return self._plot_loadings( loadings, @@ -1012,28 +1234,30 @@ def plot_decomposition_loadings(self, cmap=cmap, no_nans=no_nans, per_row=per_row, - axes_decor=axes_decor) - - def plot_bss_loadings(self, - comp_ids=None, - calibrate=True, - same_window=True, - title=None, - with_factors=False, - cmap=plt.cm.gray, - no_nans=False, - per_row=3, - axes_decor='all', - **kwargs, - ): + axes_decor=axes_decor, + ) + + def plot_bss_loadings( + self, + comp_ids=None, + calibrate=True, + same_window=True, + title=None, + with_factors=False, + cmap=plt.cm.gray, + no_nans=False, + per_row=3, + axes_decor="all", + **kwargs, + ): """Plot loadings from blind source separation results. In case of 1D navigation axis, each loading line can be toggled on and off by clicking on their corresponding line in the legend. Parameters ---------- - comp_ids : None, int, or list (of ints) - If `comp_ids` is ``None``, maps of all components will be + comp_ids : None, int or list of int + If ``comp_ids=None``, maps of all components will be returned. If it is an int, maps of components with ids from 0 to the given value will be returned. If `comp_ids` is a list of ints, maps of components with ids contained in the list will be @@ -1044,14 +1268,13 @@ def plot_bss_loadings(self, same_window : bool If ``True``, plots each factor to the same window. They are not scaled. Default is ``True``. - comp_label : str - Will be deprecated in 2.0, please use `title` instead title : str - Title of the plot. + Title of the matplotlib plot or label of the line in the legend + when the dimension of loadings is 1 and ``same_window`` is ``True``. with_factors : bool If `True`, also returns figure(s) with the factors for the given `comp_ids`. - cmap : :py:class:`~matplotlib.colors.Colormap` + cmap : :class:`~matplotlib.colors.Colormap` The colormap used for the loading image, or for peak characteristics,. Default is the matplotlib gray colormap (``plt.cm.gray``). @@ -1069,27 +1292,28 @@ def plot_bss_loadings(self, If ``'off'``, all decorations and frame will be disabled If ``None``, no axis decorations will be shown, but ticks/frame will - See also + See Also -------- plot_bss_factors, plot_bss_results """ if self.axes_manager.navigation_dimension > 2: - raise NotImplementedError("This method cannot plot loadings of " - "dimension higher than 2." - "You can use " - "`plot_bss_results` instead.") + raise NotImplementedError( + "This method cannot plot loadings of " + "dimension higher than 2." + "You can use " + "`plot_bss_results` instead." + ) if self.learning_results.bss_loadings is None: - raise RuntimeError("No learning results found. A " - "'blind_source_separation' needs to be " - "performed first.") + raise RuntimeError( + "No learning results found. A " + "'blind_source_separation' needs to be " + "performed first." + ) if same_window is None: same_window = True - comp_label = kwargs.get("comp_label", None) - title = _change_API_comp_label(title, comp_label) if title is None: - title = self._get_plot_title( - 'BSS loadings of', same_window=same_window) + title = self._get_plot_title("BSS loadings of", same_window=same_window) loadings = self.learning_results.bss_loadings.T if with_factors: factors = self.learning_results.bss_factors @@ -1105,38 +1329,41 @@ def plot_bss_loadings(self, cmap=cmap, no_nans=no_nans, per_row=per_row, - axes_decor=axes_decor) + axes_decor=axes_decor, + ) - def _get_plot_title(self, base_title='Loadings', same_window=True): + def _get_plot_title(self, base_title="Loadings", same_window=True): title_md = self.metadata.General.title title = "%s %s" % (base_title, title_md) - if title_md == '': # remove the 'of' if 'title' is a empty string - title = title.replace(' of ', '') + if title_md == "": # remove the 'of' if 'title' is a empty string + title = title.replace(" of ", "") if not same_window: - title = title.replace('loadings', 'loading') + title = title.replace("loadings", "loading") return title - def export_decomposition_results(self, comp_ids=None, - folder=None, - calibrate=True, - factor_prefix='factor', - factor_format="hspy", - loading_prefix='loading', - loading_format="hspy", - comp_label=None, - cmap=plt.cm.gray, - same_window=False, - multiple_files=True, - no_nans=True, - per_row=3, - save_figures=False, - save_figures_format='png'): - """Export results from a decomposition to any of the supported - formats. + def export_decomposition_results( + self, + comp_ids=None, + folder=None, + calibrate=True, + factor_prefix="factor", + factor_format="hspy", + loading_prefix="loading", + loading_format="hspy", + comp_label=None, + cmap=plt.cm.gray, + same_window=False, + multiple_files=True, + no_nans=True, + per_row=3, + save_figures=False, + save_figures_format="png", + ): + """Export results from a decomposition to any of the supported formats. Parameters ---------- - comp_ids : None, int, or list (of ints) + comp_ids : None, int or list of int If None, returns all components/loadings. If an int, returns components/loadings with ids from 0 to the given value. @@ -1158,14 +1385,14 @@ def export_decomposition_results(self, comp_ids=None, The extension of the format that you wish to save to. default is ``'hspy'``. The format determines the kind of output: - * For image formats (``'tif'``, ``'png'``, ``'jpg'``, etc.), - plots are created using the plotting flags as below, and saved - at 600 dpi. One plot is saved per loading. - * For multidimensional formats (``'rpl'``, ``'hspy'``), arrays - are saved in single files. All loadings are contained in the - one file. - * For spectral formats (``'msa'``), each loading is saved to a - separate file. + * For image formats (``'tif'``, ``'png'``, ``'jpg'``, etc.), + plots are created using the plotting flags as below, and saved + at 600 dpi. One plot is saved per loading. + * For multidimensional formats (``'rpl'``, ``'hspy'``), arrays + are saved in single files. All loadings are contained in the + one file. + * For spectral formats (``'msa'``), each loading is saved to a + separate file. multiple_files : bool If ``True``, one file will be created for each factor and loading. @@ -1176,86 +1403,93 @@ def export_decomposition_results(self, comp_ids=None, If ``True`` the same figures that are obtained when using the plot methods will be saved with 600 dpi resolution - Note - ---- - The following parameters are only used when ``save_figures = True``: + Notes + ----- + The following parameters are only used when ``save_figures = True`` Other Parameters ---------------- - calibrate : :py:class:`bool` + calibrate : :class:`bool` If ``True``, calibrates plots where calibration is available from the axes_manager. If ``False``, plots are in pixels/channels. - same_window : :py:class:`bool` + same_window : :class:`bool` If ``True``, plots each factor to the same window. - comp_label : :py:class:`str` + comp_label : :class:`str` the label that is either the plot title (if plotting in separate windows) or the label in the legend (if plotting in the same window) - cmap : :py:class:`~matplotlib.colors.Colormap` + cmap : :class:`~matplotlib.colors.Colormap` The colormap used for images, such as factors, loadings, or for peak characteristics. Default is the matplotlib gray colormap (``plt.cm.gray``). - per_row : :py:class:`int` + per_row : :class:`int` The number of plots in each row, when the `same_window` parameter is ``True``. - save_figures_format : :py:class:`str` + save_figures_format : :class:`str` The image format extension. - See also + See Also -------- get_decomposition_factors, get_decomposition_loadings """ factors = self.learning_results.factors loadings = self.learning_results.loadings.T - self._export_factors(factors, - folder=folder, - comp_ids=comp_ids, - calibrate=calibrate, - multiple_files=multiple_files, - factor_prefix=factor_prefix, - factor_format=factor_format, - comp_label=comp_label, - save_figures=save_figures, - cmap=cmap, - no_nans=no_nans, - same_window=same_window, - per_row=per_row, - save_figures_format=save_figures_format) - self._export_loadings(loadings, - comp_ids=comp_ids, folder=folder, - calibrate=calibrate, - multiple_files=multiple_files, - loading_prefix=loading_prefix, - loading_format=loading_format, - comp_label=comp_label, - cmap=cmap, - save_figures=save_figures, - same_window=same_window, - no_nans=no_nans, - per_row=per_row) - - def export_cluster_results(self, - cluster_ids=None, - folder=None, - calibrate=True, - center_prefix='cluster_center', - center_format="hspy", - membership_prefix='cluster_label', - membership_format="hspy", - comp_label=None, - cmap=plt.cm.gray, - same_window=False, - multiple_files=True, - no_nans=True, - per_row=3, - save_figures=False, - save_figures_format='png'): + self._export_factors( + factors, + folder=folder, + comp_ids=comp_ids, + calibrate=calibrate, + multiple_files=multiple_files, + factor_prefix=factor_prefix, + factor_format=factor_format, + comp_label=comp_label, + save_figures=save_figures, + cmap=cmap, + no_nans=no_nans, + same_window=same_window, + per_row=per_row, + save_figures_format=save_figures_format, + ) + self._export_loadings( + loadings, + comp_ids=comp_ids, + folder=folder, + calibrate=calibrate, + multiple_files=multiple_files, + loading_prefix=loading_prefix, + loading_format=loading_format, + comp_label=comp_label, + cmap=cmap, + save_figures=save_figures, + same_window=same_window, + no_nans=no_nans, + per_row=per_row, + ) + + def export_cluster_results( + self, + cluster_ids=None, + folder=None, + calibrate=True, + center_prefix="cluster_center", + center_format="hspy", + membership_prefix="cluster_label", + membership_format="hspy", + comp_label=None, + cmap=plt.cm.gray, + same_window=False, + multiple_files=True, + no_nans=True, + per_row=3, + save_figures=False, + save_figures_format="png", + ): """Export results from a cluster analysis to any of the supported formats. Parameters ---------- - cluster_ids : None, int, or list of ints + cluster_ids : None, int or list of int if None, returns all clusters/centers. if int, returns clusters/centers with ids from 0 to given int. @@ -1265,41 +1499,43 @@ def export_cluster_results(self, The path to the folder where the file will be saved. If `None` the current folder is used by default. - center_prefix : string + center_prefix : str The prefix that any exported filenames for cluster centers begin with - center_format : string + center_format : str The extension of the format that you wish to save to. Default is "hspy". See `loading format` for more details. - label_prefix : string + label_prefix : str The prefix that any exported filenames for cluster labels begin with - label_format : string + label_format : str The extension of the format that you wish to save to. default is "hspy". The format determines the kind of output. - * For image formats (``'tif'``, ``'png'``, ``'jpg'``, etc.), - plots are created using the plotting flags as below, and saved - at 600 dpi. One plot is saved per loading. - * For multidimensional formats (``'rpl'``, ``'hspy'``), arrays - are saved in single files. All loadings are contained in the - one file. - * For spectral formats (``'msa'``), each loading is saved to a - separate file. + * For image formats (``'tif'``, ``'png'``, ``'jpg'``, etc.), + plots are created using the plotting flags as below, and saved + at 600 dpi. One plot is saved per loading. + * For multidimensional formats (``'rpl'``, ``'hspy'``), arrays + are saved in single files. All loadings are contained in the + one file. + * For spectral formats (``'msa'``), each loading is saved to a + separate file. - multiple_files : bool + multiple_files : bool, default False If True, on exporting a file per center will be created. Otherwise only two files will be created, one for the centers and another for the membership. The default value can be chosen in the preferences. - save_figures : bool + save_figures : bool, default False If True the same figures that are obtained when using the plot methods will be saved with 600 dpi resolution - Plotting options (for save_figures = True ONLY) - ---------------------------------------------- + Other Parameters + ---------------- + These parameters are plotting options and only used when + ``save_figures=True``. calibrate : bool if True, calibrates plots where calibration is available @@ -1307,75 +1543,83 @@ def export_cluster_results(self, the axes_manager. If False, plots are in pixels/channels. same_window : bool if True, plots each factor to the same window. - comp_label : string, the label that is either the plot title + comp_label : str + The label that is either the plot title (if plotting in separate windows) or the label in the legend (if plotting in the same window) - cmap : The colormap used for the factor image, or for peak + cmap : matplotlib.colors.Colormap + The colormap used for the factor image, or for peak characteristics, the colormap used for the scatter plot of some peak characteristic. - per_row : int, the number of plots in each row, when the - same_window - parameter is True. + per_row : int + the number of plots in each row, when the ``same_window=True``. save_figures_format : str The image format extension. See Also -------- get_cluster_signals, - get_cluster_labels. + get_cluster_labels """ factors = self.learning_results.cluster_centers.T loadings = self.learning_results.cluster_labels - self._export_factors(factors, - folder=folder, - comp_ids=cluster_ids, - calibrate=calibrate, - multiple_files=multiple_files, - factor_prefix=center_prefix, - factor_format=center_format, - comp_label=comp_label, - save_figures=save_figures, - cmap=cmap, - no_nans=no_nans, - same_window=same_window, - per_row=per_row, - save_figures_format=save_figures_format) - self._export_loadings(loadings, - comp_ids=cluster_ids, - folder=folder, - calibrate=calibrate, - multiple_files=multiple_files, - loading_prefix=membership_prefix, - loading_format=membership_format, - comp_label=comp_label, - cmap=cmap, - save_figures=save_figures, - same_window=same_window, - no_nans=no_nans, - per_row=per_row) - - def export_bss_results(self, - comp_ids=None, - folder=None, - calibrate=True, - multiple_files=True, - save_figures=False, - factor_prefix='bss_factor', - factor_format="hspy", - loading_prefix='bss_loading', - loading_format="hspy", - comp_label=None, cmap=plt.cm.gray, - same_window=False, - no_nans=True, - per_row=3, - save_figures_format='png'): + self._export_factors( + factors, + folder=folder, + comp_ids=cluster_ids, + calibrate=calibrate, + multiple_files=multiple_files, + factor_prefix=center_prefix, + factor_format=center_format, + comp_label=comp_label, + save_figures=save_figures, + cmap=cmap, + no_nans=no_nans, + same_window=same_window, + per_row=per_row, + save_figures_format=save_figures_format, + ) + self._export_loadings( + loadings, + comp_ids=cluster_ids, + folder=folder, + calibrate=calibrate, + multiple_files=multiple_files, + loading_prefix=membership_prefix, + loading_format=membership_format, + comp_label=comp_label, + cmap=cmap, + save_figures=save_figures, + same_window=same_window, + no_nans=no_nans, + per_row=per_row, + ) + + def export_bss_results( + self, + comp_ids=None, + folder=None, + calibrate=True, + multiple_files=True, + save_figures=False, + factor_prefix="bss_factor", + factor_format="hspy", + loading_prefix="bss_loading", + loading_format="hspy", + comp_label=None, + cmap=plt.cm.gray, + same_window=False, + no_nans=True, + per_row=3, + save_figures_format="png", + ): """Export results from ICA to any of the supported formats. Parameters ---------- - comp_ids : None, int, or list (of ints) + comp_ids : None, int or list of int If None, returns all components/loadings. If an int, returns components/loadings with ids from 0 to the given value. @@ -1397,14 +1641,14 @@ def export_bss_results(self, The extension of the format that you wish to save to. default is ``'hspy'``. The format determines the kind of output: - * For image formats (``'tif'``, ``'png'``, ``'jpg'``, etc.), - plots are created using the plotting flags as below, and saved - at 600 dpi. One plot is saved per loading. - * For multidimensional formats (``'rpl'``, ``'hspy'``), arrays - are saved in single files. All loadings are contained in the - one file. - * For spectral formats (``'msa'``), each loading is saved to a - separate file. + * For image formats (``'tif'``, ``'png'``, ``'jpg'``, etc.), + plots are created using the plotting flags as below, and saved + at 600 dpi. One plot is saved per loading. + * For multidimensional formats (``'rpl'``, ``'hspy'``), arrays + are saved in single files. All loadings are contained in the + one file. + * For spectral formats (``'msa'``), each loading is saved to a + separate file. multiple_files : bool If ``True``, one file will be created for each factor and loading. @@ -1415,79 +1659,85 @@ def export_bss_results(self, If ``True``, the same figures that are obtained when using the plot methods will be saved with 600 dpi resolution - Note - ---- - The following parameters are only used when ``save_figures = True``: + Notes + ----- + The following parameters are only used when ``save_figures = True`` Other Parameters ---------------- - calibrate : :py:class:`bool` + calibrate : :class:`bool` If ``True``, calibrates plots where calibration is available from the axes_manager. If ``False``, plots are in pixels/channels. - same_window : :py:class:`bool` + same_window : :class:`bool` If ``True``, plots each factor to the same window. - comp_label : :py:class:`str` + comp_label : :class:`str` the label that is either the plot title (if plotting in separate windows) or the label in the legend (if plotting in the same window) - cmap : :py:class:`~matplotlib.colors.Colormap` + cmap : :class:`~matplotlib.colors.Colormap` The colormap used for images, such as factors, loadings, or for peak characteristics. Default is the matplotlib gray colormap (``plt.cm.gray``). - per_row : :py:class:`int` + per_row : :class:`int` The number of plots in each row, when the `same_window` parameter is ``True``. - save_figures_format : :py:class:`str` + save_figures_format : :class:`str` The image format extension. - See also + See Also -------- get_bss_factors, get_bss_loadings """ factors = self.learning_results.bss_factors loadings = self.learning_results.bss_loadings.T - self._export_factors(factors, - folder=folder, - comp_ids=comp_ids, - calibrate=calibrate, - multiple_files=multiple_files, - factor_prefix=factor_prefix, - factor_format=factor_format, - comp_label=comp_label, - save_figures=save_figures, - cmap=cmap, - no_nans=no_nans, - same_window=same_window, - per_row=per_row, - save_figures_format=save_figures_format) - - self._export_loadings(loadings, - comp_ids=comp_ids, - folder=folder, - calibrate=calibrate, - multiple_files=multiple_files, - loading_prefix=loading_prefix, - loading_format=loading_format, - comp_label=comp_label, - cmap=cmap, - save_figures=save_figures, - same_window=same_window, - no_nans=no_nans, - per_row=per_row, - save_figures_format=save_figures_format) + self._export_factors( + factors, + folder=folder, + comp_ids=comp_ids, + calibrate=calibrate, + multiple_files=multiple_files, + factor_prefix=factor_prefix, + factor_format=factor_format, + comp_label=comp_label, + save_figures=save_figures, + cmap=cmap, + no_nans=no_nans, + same_window=same_window, + per_row=per_row, + save_figures_format=save_figures_format, + ) + + self._export_loadings( + loadings, + comp_ids=comp_ids, + folder=folder, + calibrate=calibrate, + multiple_files=multiple_files, + loading_prefix=loading_prefix, + loading_format=loading_format, + comp_label=comp_label, + cmap=cmap, + save_figures=save_figures, + same_window=same_window, + no_nans=no_nans, + per_row=per_row, + save_figures_format=save_figures_format, + ) def _get_loadings(self, loadings): if loadings is None: raise RuntimeError("No learning results found.") from hyperspy.api import signals - data = loadings.T.reshape( - (-1,) + self.axes_manager.navigation_shape[::-1]) + + data = loadings.T.reshape((-1,) + self.axes_manager.navigation_shape[::-1]) if data.shape[0] > 1: signal = signals.BaseSignal( data, axes=( - [{"size": data.shape[0], "navigate": True}] + - self.axes_manager._get_navigation_axes_dicts())) + [{"size": data.shape[0], "navigate": True}] + + self.axes_manager._get_navigation_axes_dicts() + ), + ) for axis in signal.axes_manager._axes[1:]: axis.navigate = False else: @@ -1499,8 +1749,9 @@ def _get_factors(self, factors): raise RuntimeError("No learning results found.") signal = self.__class__( factors.T.reshape((-1,) + self.axes_manager.signal_shape[::-1]), - axes=[{"size": factors.shape[-1], "navigate": True}] + - self.axes_manager._get_signal_axes_dicts()) + axes=[{"size": factors.shape[-1], "navigate": True}] + + self.axes_manager._get_signal_axes_dicts(), + ) signal.set_signal_type(self.metadata.Signal.signal_type) for axis in signal.axes_manager._axes[1:]: axis.navigate = False @@ -1511,9 +1762,9 @@ def get_decomposition_loadings(self): Returns ------- - signal : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + signal : :class:`~hyperspy.signal.BaseSignal` (or subclass) - See also + See Also -------- get_decomposition_factors, export_decomposition_results @@ -1522,8 +1773,9 @@ def get_decomposition_loadings(self): raise RuntimeError("Run a decomposition first.") signal = self._get_loadings(self.learning_results.loadings) signal.axes_manager._axes[0].name = "Decomposition component index" - signal.metadata.General.title = "Decomposition loadings of " + \ - self.metadata.General.title + signal.metadata.General.title = ( + "Decomposition loadings of " + self.metadata.General.title + ) return signal def get_decomposition_factors(self): @@ -1531,9 +1783,9 @@ def get_decomposition_factors(self): Returns ------- - signal : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + signal : :class:`~hyperspy.signal.BaseSignal` (or subclass) - See also + See Also -------- get_decomposition_loadings, export_decomposition_results @@ -1542,8 +1794,9 @@ def get_decomposition_factors(self): raise RuntimeError("Run a decomposition first.") signal = self._get_factors(self.learning_results.factors) signal.axes_manager._axes[0].name = "Decomposition component index" - signal.metadata.General.title = ("Decomposition factors of " + - self.metadata.General.title) + signal.metadata.General.title = ( + "Decomposition factors of " + self.metadata.General.title + ) return signal def get_bss_loadings(self): @@ -1551,18 +1804,16 @@ def get_bss_loadings(self): Returns ------- - signal : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + :class:`~hyperspy.signal.BaseSignal` (or subclass) - See also + See Also -------- get_bss_factors, export_bss_results """ - signal = self._get_loadings( - self.learning_results.bss_loadings) + signal = self._get_loadings(self.learning_results.bss_loadings) signal.axes_manager[0].name = "BSS component index" - signal.metadata.General.title = ("BSS loadings of " + - self.metadata.General.title) + signal.metadata.General.title = "BSS loadings of " + self.metadata.General.title return signal def get_bss_factors(self): @@ -1570,28 +1821,29 @@ def get_bss_factors(self): Returns ------- - signal : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + :class:`~hyperspy.signal.BaseSignal` (or subclass) - See also + See Also -------- get_bss_loadings, export_bss_results """ signal = self._get_factors(self.learning_results.bss_factors) signal.axes_manager[0].name = "BSS component index" - signal.metadata.General.title = ("BSS factors of " + - self.metadata.General.title) + signal.metadata.General.title = "BSS factors of " + self.metadata.General.title return signal - def plot_bss_results(self, - factors_navigator="smart_auto", - loadings_navigator="smart_auto", - factors_dim=2, - loadings_dim=2,): + def plot_bss_results( + self, + factors_navigator="smart_auto", + loadings_navigator="smart_auto", + factors_dim=2, + loadings_dim=2, + ): """Plot the blind source separation factors and loadings. - Unlike :py:meth:`~hyperspy.signal.MVATools.plot_bss_factors` and - :py:meth:`~hyperspy.signal.MVATools.plot_bss_loadings`, + Unlike :meth:`~hyperspy.api.signals.BaseSignal.plot_bss_factors` and + :meth:`~hyperspy.api.signals.BaseSignal.plot_bss_loadings`, this method displays one component at a time. Therefore it provides a more compact visualization than then other two methods. The loadings and factors are displayed in different windows and each @@ -1601,14 +1853,14 @@ def plot_bss_results(self, Parameters ---------- - factors_navigator : str, None, or :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + factors_navigator : str, None, or :class:`~hyperspy.api.signals.BaseSignal` (or subclass) One of: ``'smart_auto'``, ``'auto'``, ``None``, ``'spectrum'`` or a - :py:class:`~hyperspy.signal.BaseSignal` object. + :class:`~hyperspy.api.signals.BaseSignal` object. ``'smart_auto'`` (default) displays sliders if the navigation dimension is less than 3. For a description of the other options - see the :py:meth:`~hyperspy.signal.BaseSignal.plot` documentation + see the :meth:`~hyperspy.api.signals.BaseSignal.plot` documentation for details. - loadings_navigator : str, None, or :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + loadings_navigator : str, None, or :class:`~hyperspy.api.signals.BaseSignal` (or subclass) See the `factors_navigator` parameter factors_dim : int Currently HyperSpy cannot plot a signal when the signal dimension is @@ -1619,28 +1871,33 @@ def plot_bss_results(self, loadings_dim : int See the ``factors_dim`` parameter - See also + See Also -------- plot_bss_factors, plot_bss_loadings, plot_decomposition_results """ factors = self.get_bss_factors() loadings = self.get_bss_loadings() - _plot_x_results(factors=factors, loadings=loadings, - factors_navigator=factors_navigator, - loadings_navigator=loadings_navigator, - factors_dim=factors_dim, - loadings_dim=loadings_dim) - - def plot_decomposition_results(self, - factors_navigator="smart_auto", - loadings_navigator="smart_auto", - factors_dim=2, - loadings_dim=2): + _plot_x_results( + factors=factors, + loadings=loadings, + factors_navigator=factors_navigator, + loadings_navigator=loadings_navigator, + factors_dim=factors_dim, + loadings_dim=loadings_dim, + ) + + def plot_decomposition_results( + self, + factors_navigator="smart_auto", + loadings_navigator="smart_auto", + factors_dim=2, + loadings_dim=2, + ): """Plot the decomposition factors and loadings. - Unlike :py:meth:`~hyperspy.signal.MVATools.plot_decomposition_factors` - and :py:meth:`~hyperspy.signal.MVATools.plot_decomposition_loadings`, + Unlike :meth:`~hyperspy.api.signals.BaseSignal.plot_decomposition_factors` + and :meth:`~hyperspy.api.signals.BaseSignal.plot_decomposition_loadings`, this method displays one component at a time. Therefore it provides a more compact visualization than then other two methods. The loadings and factors are displayed in different windows and each has its own @@ -1649,45 +1906,45 @@ def plot_decomposition_results(self, Parameters ---------- - factors_navigator : str, None, or :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + factors_navigator : str, None, or :class:`~hyperspy.api.signals.BaseSignal` (or subclass) One of: ``'smart_auto'``, ``'auto'``, ``None``, ``'spectrum'`` or a - :py:class:`~hyperspy.signal.BaseSignal` object. + :class:`~hyperspy.api.signals.BaseSignal` object. ``'smart_auto'`` (default) displays sliders if the navigation dimension is less than 3. For a description of the other options - see the :py:meth:`~hyperspy.signal.BaseSignal.plot` documentation + see the :meth:`~hyperspy.api.signals.BaseSignal.plot` documentation for details. - loadings_navigator : str, None, or :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + loadings_navigator : str, None, or :class:`~hyperspy.api.signals.BaseSignal` (or subclass) See the `factors_navigator` parameter - factors_dim : int + factors_dim, loadings_dim : int Currently HyperSpy cannot plot a signal when the signal dimension is higher than two. Therefore, to visualize the BSS results when the factors or the loadings have signal dimension greater than 2, the data can be viewed as spectra (or images) by setting this parameter to 1 (or 2). (The default is 2) - loadings_dim : int - See the ``factors_dim`` parameter - See also + See Also -------- - plot_decomposition_factors, plot_decomposition_loadings, - plot_bss_results + plot_decomposition_factors, plot_decomposition_loadings, plot_bss_results """ factors = self.get_decomposition_factors() loadings = self.get_decomposition_loadings() - _plot_x_results(factors=factors, loadings=loadings, - factors_navigator=factors_navigator, - loadings_navigator=loadings_navigator, - factors_dim=factors_dim, - loadings_dim=loadings_dim) + _plot_x_results( + factors=factors, + loadings=loadings, + factors_navigator=factors_navigator, + loadings_navigator=loadings_navigator, + factors_dim=factors_dim, + loadings_dim=loadings_dim, + ) def get_cluster_labels(self, merged=False): """Return cluster labels as a Signal. Parameters ---------- - merged : bool + merged : bool, default False If False the cluster label signal has a navigation axes of length number_of_clusters and the signal along the the navigation direction is binary - 0 the point is not in the cluster, 1 it is @@ -1702,22 +1959,25 @@ def get_cluster_labels(self, merged=False): Returns ------- - signal Hyperspy signal of cluster labels + :class:`~hyperspy.api.signals.BaseSignal` + The cluster labels """ if self.learning_results.cluster_labels is None: - raise RuntimeError( - "Cluster analysis needs to be performed first.") + raise RuntimeError("Cluster analysis needs to be performed first.") if merged: - data = (np.arange(1, self.learning_results.number_of_clusters + 1) - [:, np.newaxis] * - self.learning_results.cluster_labels ).sum(0) - 1 + data = ( + np.arange(1, self.learning_results.number_of_clusters + 1)[ + :, np.newaxis + ] + * self.learning_results.cluster_labels + ).sum(0) - 1 label_signal = self._get_loadings(data) else: - label_signal = self._get_loadings( - self.learning_results.cluster_labels.T) + label_signal = self._get_loadings(self.learning_results.cluster_labels.T) label_signal.axes_manager._axes[0].name = "Cluster index" label_signal.metadata.General.title = ( - "Cluster labels of " + self.metadata.General.title) + "Cluster labels of " + self.metadata.General.title + ) return label_signal def _get_cluster_signals_factors(self, signal): @@ -1727,9 +1987,9 @@ def _get_cluster_signals_factors(self, signal): members = self.learning_results.cluster_labels.sum(1, keepdims=True) cs = self.learning_results.cluster_sum_signals / members elif signal == "sum": - cs=self.learning_results.cluster_sum_signals + cs = self.learning_results.cluster_sum_signals elif signal == "centroid": - cs=self.learning_results.cluster_centroid_signals + cs = self.learning_results.cluster_centroid_signals return cs def get_cluster_signals(self, signal="mean"): @@ -1746,11 +2006,13 @@ def get_cluster_signals(self, signal="mean"): """ cs = self._get_cluster_signals_factors(signal=signal) signal = self._get_factors(cs.T) - signal.axes_manager._axes[0].name="Cluster index" + signal.axes_manager._axes[0].name = "Cluster index" signal.metadata.General.title = ( - f"Cluster {signal} signals of {self.metadata.General.title}") + f"Cluster {signal} signals of {self.metadata.General.title}" + ) return signal - get_cluster_signals.__doc__ %= (CLUSTER_SIGNALS_ARG) + + get_cluster_signals.__doc__ %= CLUSTER_SIGNALS_ARG def get_cluster_distances(self): """Euclidian distances to the centroid of each cluster @@ -1769,42 +2031,42 @@ def get_cluster_distances(self): raise RuntimeError("Cluster analysis needs to be performed first.") distance_signal = self._get_loadings(self.learning_results.cluster_distances.T) distance_signal.axes_manager._axes[0].name = "Cluster index" - distance_signal.metadata.General.title = \ + distance_signal.metadata.General.title = ( "Cluster distances of " + self.metadata.General.title + ) return distance_signal - def plot_cluster_signals( self, signal="mean", cluster_ids=None, calibrate=True, same_window=True, - comp_label="Cluster centers", - per_row=3): + title=None, + per_row=3, + ): """Plot centers from a cluster analysis. Parameters ---------- %s - cluster_ids : None, int, or list of ints - if None, returns maps of all clusters. - if int, returns maps of clusters with ids from 0 to given + cluster_ids : None, int, or list of int + If None, returns maps of all clusters. + If int, returns maps of clusters with ids from 0 to given int. - if list of ints, returns maps of clusters with ids in + If list of ints, returns maps of clusters with ids in given list. - calibrate : - if True, calibrates plots where calibration is available + calibrate : bool, default True + If True, calibrates plots where calibration is available from the axes_manager. If False, plots are in pixels/channels. - same_window : bool - if True, plots each center to the same window. They are + same_window : bool, default True + If True, plots each center to the same window. They are not scaled. - comp_label : string - the label that is either the plot title (if plotting in - separate windows) or the label in the legend (if plotting - in the same window) - per_row : int - the number of plots in each row, when the same_window parameter is + title : None or str, default None + Title of the matplotlib plot or label of the line in the legend + when the dimension of loadings is 1 and ``same_window`` is ``True``. + per_row : int, default 3 + The number of plots in each row, when the same_window parameter is True. See Also @@ -1813,22 +2075,31 @@ def plot_cluster_signals( """ if self.axes_manager.signal_dimension > 2: - raise NotImplementedError("This method cannot plot factors of " - "signals of dimension higher than 2.") + raise NotImplementedError( + "This method cannot plot factors of " + "signals of dimension higher than 2." + ) cs = self._get_cluster_signals_factors(signal=signal) if same_window is None: same_window = True factors = cs.T + if cluster_ids is None: cluster_ids = range(factors.shape[1]) - return self._plot_factors_or_pchars(factors, - comp_ids=cluster_ids, - calibrate=calibrate, - same_window=same_window, - comp_label=comp_label, - per_row=per_row) - plot_cluster_signals.__doc__ %= (CLUSTER_SIGNALS_ARG) + if title is None: + title = self._get_plot_title("Cluster centers of", same_window=same_window) + + return self._plot_factors_or_pchars( + factors, + comp_ids=cluster_ids, + calibrate=calibrate, + same_window=same_window, + comp_label=title, + per_row=per_row, + ) + + plot_cluster_signals.__doc__ %= CLUSTER_SIGNALS_ARG def plot_cluster_labels( self, @@ -1839,9 +2110,10 @@ def plot_cluster_labels( cmap=plt.cm.gray, no_nans=False, per_row=3, - axes_decor='all', + axes_decor="all", title=None, - **kwargs): + **kwargs, + ): """Plot cluster labels from a cluster analysis. In case of 1D navigation axis, each loading line can be toggled on and off by clicking on the legended line. @@ -1849,7 +2121,7 @@ def plot_cluster_labels( Parameters ---------- - cluster_ids : None, int, or list of ints + cluster_ids : None, int, or list of int if None (default), returns maps of all components using the number_of_cluster was defined when executing ``cluster``. Otherwise it raises a ValueError. @@ -1863,12 +2135,13 @@ def plot_cluster_labels( same_window : bool if True, plots each factor to the same window. They are not scaled. Default is True. - title : string - Title of the plot. + title : str + Title of the matplotlib plot or label of the line in the legend + when the dimension of labels is 1 and ``same_window`` is ``True``. with_centers : bool If True, also returns figure(s) with the cluster centers for the given cluster_ids. - cmap : matplotlib colormap + cmap : matplotlib.colors.Colormap The colormap used for the factor image, or for peak characteristics, the colormap used for the scatter plot of some peak characteristic. @@ -1877,7 +2150,7 @@ def plot_cluster_labels( per_row : int the number of plots in each row, when the same_window parameter is True. - axes_decor : {'all', 'ticks', 'off', None}, optional + axes_decor : None or str {``'all'``, ``'ticks'``, ``'off'``}, default ``'all'`` Controls how the axes are displayed on each image; default is 'all' If 'all', both ticks and axis labels will be shown If 'ticks', no axis labels will be shown, but ticks/labels will @@ -1886,14 +2159,16 @@ def plot_cluster_labels( See Also -------- - plot_cluster_signals, plot_cluster_results. + plot_cluster_signals, plot_cluster_results """ if self.axes_manager.navigation_dimension > 2: - raise NotImplementedError("This method cannot plot labels of " - "dimension higher than 2." - "You can use " - "`plot_cluster_results` instead.") + raise NotImplementedError( + "This method cannot plot labels of " + "dimension higher than 2." + "You can use " + "`plot_cluster_results` instead." + ) if same_window is None: same_window = True labels = self.learning_results.cluster_labels.astype("uint") @@ -1905,23 +2180,21 @@ def plot_cluster_labels( if cluster_ids is None: cluster_ids = range(labels.shape[0]) - comp_label = kwargs.get("comp_label", None) - title = _change_API_comp_label(title, comp_label) if title is None: - title = self._get_plot_title( - 'Cluster labels of', same_window=same_window) - - return self._plot_loadings(labels, - comp_ids=cluster_ids, - with_factors=with_centers, - factors=centers, - same_window=same_window, - comp_label=title, - cmap=cmap, - no_nans=no_nans, - per_row=per_row, - axes_decor=axes_decor) + title = self._get_plot_title("Cluster labels of", same_window=same_window) + return self._plot_loadings( + labels, + comp_ids=cluster_ids, + with_factors=with_centers, + factors=centers, + same_window=same_window, + comp_label=title, + cmap=cmap, + no_nans=no_nans, + per_row=per_row, + axes_decor=axes_decor, + ) def plot_cluster_distances( self, @@ -1932,18 +2205,18 @@ def plot_cluster_distances( cmap=plt.cm.gray, no_nans=False, per_row=3, - axes_decor='all', + axes_decor="all", title=None, - **kwargs): + **kwargs, + ): """Plot the euclidian distances to the centroid of each cluster. - In case of 1D navigation axis, - each line can be toggled on and off by clicking on the legended - line. + In case of 1D navigation axis, each line can be toggled on and + off by clicking on the corresponding line in the legend. Parameters ---------- - cluster_ids : None, int, or list of ints + cluster_ids : None, int, or list of int if None (default), returns maps of all components using the number_of_cluster was defined when executing ``cluster``. Otherwise it raises a ValueError. @@ -1957,12 +2230,13 @@ def plot_cluster_distances( same_window : bool if True, plots each factor to the same window. They are not scaled. Default is True. - title : string - Title of the plot. + title : str + Title of the matplotlib plot or label of the line in the legend + when the dimension of distance is 1 and ``same_window`` is ``True``. with_centers : bool If True, also returns figure(s) with the cluster centers for the given cluster_ids. - cmap : matplotlib colormap + cmap : matplotlib.colors.Colormap The colormap used for the factor image, or for peak characteristics, the colormap used for the scatter plot of some peak characteristic. @@ -1971,7 +2245,7 @@ def plot_cluster_distances( per_row : int the number of plots in each row, when the same_window parameter is True. - axes_decor : {'all', 'ticks', 'off', None}, optional + axes_decor : None or str {'all', 'ticks', 'off'}, optional Controls how the axes are displayed on each image; default is 'all' If 'all', both ticks and axis labels will be shown If 'ticks', no axis labels will be shown, but ticks/labels will @@ -1984,10 +2258,12 @@ def plot_cluster_distances( """ if self.axes_manager.navigation_dimension > 2: - raise NotImplementedError("This method cannot plot labels of " - "dimension higher than 2." - "You can use " - "`plot_cluster_results` instead.") + raise NotImplementedError( + "This method cannot plot labels of " + "dimension higher than 2." + "You can use " + "`plot_cluster_results` instead." + ) if same_window is None: same_window = True distances = self.learning_results.cluster_distances @@ -1999,33 +2275,35 @@ def plot_cluster_distances( if cluster_ids is None: cluster_ids = range(distances.shape[0]) - comp_label = kwargs.get("comp_label", None) - title = _change_API_comp_label(title, comp_label) if title is None: title = self._get_plot_title( - 'Cluster distances of', same_window=same_window) - - return self._plot_loadings(distances, - comp_ids=cluster_ids, - with_factors=with_centers, - factors=centers, - same_window=same_window, - comp_label=title, - cmap=cmap, - no_nans=no_nans, - per_row=per_row, - axes_decor=axes_decor) - - - def plot_cluster_results(self, - centers_navigator="smart_auto", - labels_navigator="smart_auto", - centers_dim=2, - labels_dim=2, - ): + "Cluster distances of", same_window=same_window + ) + + return self._plot_loadings( + distances, + comp_ids=cluster_ids, + with_factors=with_centers, + factors=centers, + same_window=same_window, + comp_label=title, + cmap=cmap, + no_nans=no_nans, + per_row=per_row, + axes_decor=axes_decor, + ) + + def plot_cluster_results( + self, + centers_navigator="smart_auto", + labels_navigator="smart_auto", + centers_dim=2, + labels_dim=2, + ): """Plot the cluster labels and centers. - Unlike `plot_cluster_labels` and `plot_cluster_signals`, this + Unlike :meth:`~hyperspy.api.signals.BaseSignal.plot_cluster_labels` and + :meth:`~hyperspy.api.signals.BaseSignal.plot_cluster_signals`, this method displays one component at a time. Therefore it provides a more compact visualization than then other two methods. The labels and centers are displayed in different @@ -2035,50 +2313,55 @@ def plot_cluster_results(self, Parameters ---------- - centers_navigator, labels_navigator : {"smart_auto", - "auto", None, "spectrum", Signal} - "smart_auto" (default) displays sliders if the navigation + centers_navigator, labels_navigator : None, \ + {``"smart_auto"`` | ``"auto"`` | ``"spectrum"``} or \ + :class:`~hyperspy.api.signals.BaseSignal`, default ``"smart_auto"`` + ``"smart_auto"`` displays sliders if the navigation dimension is less than 3. For a description of the other options - see `plot` documentation for details. - labels_dim, centers_dims : int + see ``plot`` documentation for details. + labels_dim, centers_dims : int, default 2 Currently HyperSpy cannot plot signals of dimension higher than two. Therefore, to visualize the clustering results when the centers or the labels have signal dimension greater than 2 we can view the data as spectra(images) by setting this parameter - to 1(2). (Default 2) + to 1(2) See Also -------- - plot_cluster_signals, plot_cluster_labels. + plot_cluster_signals, plot_cluster_labels """ centers = self.get_cluster_signals() distances = self.get_cluster_distances() self.get_cluster_labels(merged=True).plot() - _plot_x_results(factors=centers, - loadings=distances, - factors_navigator=centers_navigator, - loadings_navigator=labels_navigator, - factors_dim=centers_dim, - loadings_dim=labels_dim) - + _plot_x_results( + factors=centers, + loadings=distances, + factors_navigator=centers_navigator, + loadings_navigator=labels_navigator, + factors_dim=centers_dim, + loadings_dim=labels_dim, + ) -def _plot_x_results(factors, loadings, factors_navigator, loadings_navigator, - factors_dim, loadings_dim): +def _plot_x_results( + factors, loadings, factors_navigator, loadings_navigator, factors_dim, loadings_dim +): factors.axes_manager._axes[0] = loadings.axes_manager._axes[0] if loadings.axes_manager.signal_dimension > 2: - loadings.axes_manager.set_signal_dimension(loadings_dim) + loadings.axes_manager._set_signal_dimension(loadings_dim) if factors.axes_manager.signal_dimension > 2: - factors.axes_manager.set_signal_dimension(factors_dim) - if (loadings_navigator == "smart_auto" and - loadings.axes_manager.navigation_dimension < 3): + factors.axes_manager._set_signal_dimension(factors_dim) + if ( + loadings_navigator == "smart_auto" + and loadings.axes_manager.navigation_dimension < 3 + ): loadings_navigator = "slider" else: loadings_navigator = "auto" - if (factors_navigator == "smart_auto" and - (factors.axes_manager.navigation_dimension < 3 or - loadings_navigator is not None)): + if factors_navigator == "smart_auto" and ( + factors.axes_manager.navigation_dimension < 3 or loadings_navigator is not None + ): factors_navigator = None else: factors_navigator = "auto" @@ -2086,26 +2369,9 @@ def _plot_x_results(factors, loadings, factors_navigator, loadings_navigator, factors.plot(navigator=factors_navigator) -def _change_API_comp_label(title, comp_label): - if comp_label is not None: - if title is None: - title = comp_label - warnings.warn("The 'comp_label' argument will be deprecated " - "in 2.0, please use 'title' instead", - VisibleDeprecationWarning) - else: - warnings.warn("The 'comp_label' argument will be deprecated " - "in 2.0, Since you are already using the 'title'", - "argument, 'comp_label' is ignored.", - VisibleDeprecationWarning) - return title - - class SpecialSlicersSignal(SpecialSlicers): - def __setitem__(self, i, j): - """x.__setitem__(i, y) <==> x[i]=y - """ + """x.__setitem__(i, y) <==> x[i]=y""" if isinstance(j, BaseSignal): j = j.data array_slices = self.obj._get_array_slices(i, self.isNavigation) @@ -2116,7 +2382,6 @@ def __len__(self): class BaseSetMetadataItems(t.HasTraits): - def __init__(self, signal): for key, value in self.mapping.items(): if signal.metadata.has_item(key): @@ -2129,11 +2394,41 @@ def store(self, *args, **kwargs): self.signal.metadata.set_item(key, getattr(self, value)) -class BaseSignal(FancySlicing, - MVA, - MVATools,): +class BaseSignal( + FancySlicing, + MVA, + MVATools, +): + """ + + Attributes + ---------- + ragged : bool + Whether the signal is ragged or not. + isig + Signal indexer/slicer. + inav + Navigation indexer/slicer. + metadata : hyperspy.misc.utils.DictionaryTreeBrowser + The metadata of the signal structured as documented in + :ref:`metadata_structure`. + original_metadata : hyperspy.misc.utils.DictionaryTreeBrowser + All metadata read when loading the data. + + + Examples + -------- + General signal created from a numpy or cupy array. + + >>> data = np.ones((10, 10)) + >>> s = hs.signals.BaseSignal(data) + + """ _dtype = "real" + # When _signal_dimension=-1, the signal dimension of BaseSignal is defined + # by the dimension of the array, and this is implemented by the default + # value of navigate=False in BaseDataAxis _signal_dimension = -1 _signal_type = "" _lazy = False @@ -2143,15 +2438,16 @@ class BaseSignal(FancySlicing, ] def __init__(self, data, **kwds): - """Create a Signal from a numpy array. + """ + Create a signal instance. Parameters ---------- - data : :py:class:`numpy.ndarray` + data : numpy.ndarray The signal data. It can be an array of any dimensions. axes : [dict/axes], optional List of either dictionaries or axes objects to define the axes (see - the documentation of the :py:class:`~hyperspy.axes.AxesManager` + the documentation of the :class:`~hyperspy.axes.AxesManager` class for more details). attributes : dict, optional A dictionary whose items are stored as attributes. @@ -2164,23 +2460,26 @@ class for more details). that will to stores in the ``original_metadata`` attribute. It typically contains all the parameters that has been imported from the original data file. - + ragged : bool or None, optional + Define whether the signal is ragged or not. Overwrite the + ``ragged`` value in the ``attributes`` dictionary. If None, it does + nothing. Default is None. """ # the 'full_initialisation' keyword is private API to be used by the # _assign_subclass method. Purposely not exposed as public API. # Its purpose is to avoid creating new attributes, which breaks events # and to reduce overhead when changing 'signal_type'. - if kwds.get('full_initialisation', True): + if kwds.get("full_initialisation", True): self._create_metadata() self.models = ModelManager(self) self.learning_results = LearningResults() - kwds['data'] = data - self._load_dictionary(kwds) + kwds["data"] = data self._plot = None self.inav = SpecialSlicersSignal(self, True) self.isig = SpecialSlicersSignal(self, False) self.events = Events() - self.events.data_changed = Event(""" + self.events.data_changed = Event( + """ Event that triggers when the data has changed The event trigger when the data is ready for consumption by any @@ -2190,12 +2489,21 @@ class for more details). Note: The event only fires at certain specific times, not everytime that the `BaseSignal.data` array changes values. - Arguments: + Parameters + ---------- obj: The signal that owns the data. - """, arguments=['obj']) + """, + arguments=["obj"], + ) + self._load_dictionary(kwds) + + if self._signal_dimension >= 0: + # We don't explicitly set the signal_dimension of ragged because + # we can't predict it in advance + self.axes_manager._set_signal_dimension(self._signal_dimension) def _create_metadata(self): - self.metadata = DictionaryTreeBrowser() + self._metadata = DictionaryTreeBrowser() mp = self.metadata mp.add_node("_HyperSpy") mp.add_node("General") @@ -2206,7 +2514,7 @@ def _create_metadata(self): folding.signal_unfolded = False folding.original_shape = None folding.original_axes_manager = None - self.original_metadata = DictionaryTreeBrowser() + self._original_metadata = DictionaryTreeBrowser() self.tmp_parameters = DictionaryTreeBrowser() def __repr__(self): @@ -2214,26 +2522,28 @@ def __repr__(self): unfolded = "unfolded " else: unfolded = "" - string = '<' + string = "<" string += self.__class__.__name__ string += ", title: %s" % self.metadata.General.title string += ", %sdimensions: %s" % ( unfolded, - self.axes_manager._get_dimension_str()) + self.axes_manager._get_dimension_str(), + ) - string += '>' + string += ">" return string def _binary_operator_ruler(self, other, op_name): - exception_message = ( - "Invalid dimensions for this operation") + exception_message = "Invalid dimensions for this operation." if isinstance(other, BaseSignal): # Both objects are signals oam = other.axes_manager sam = self.axes_manager - if sam.navigation_shape == oam.navigation_shape and \ - sam.signal_shape == oam.signal_shape: + if ( + sam.navigation_shape == oam.navigation_shape + and sam.signal_shape == oam.signal_shape + ): # They have the same signal shape. # The signal axes are aligned but there is # no guarantee that data axes area aligned so we make sure that @@ -2245,8 +2555,7 @@ def _binary_operator_ruler(self, other, op_name): self.axes_manager._sort_axes() return self else: - ns = self._deepcopy_with_new_data( - getattr(sdata, op_name)(odata)) + ns = self._deepcopy_with_new_data(getattr(sdata, op_name)(odata)) ns.axes_manager._sort_axes() return ns else: @@ -2274,8 +2583,7 @@ def _binary_operator_ruler(self, other, op_name): getattr(self.data, op_name)(other) return self else: - return self._deepcopy_with_new_data( - getattr(self.data, op_name)(other)) + return self._deepcopy_with_new_data(getattr(self.data, op_name)(other)) def _unary_operator_ruler(self, op_name): return self._deepcopy_with_new_data(getattr(self.data, op_name)()) @@ -2288,17 +2596,21 @@ def _check_signal_dimension_equals_two(self): if self.axes_manager.signal_dimension != 2: raise SignalDimensionError(self.axes_manager.signal_dimension, 2) - def _deepcopy_with_new_data(self, data=None, copy_variance=False, - copy_navigator=False, - copy_learning_results=False): + def _deepcopy_with_new_data( + self, + data=None, + copy_variance=False, + copy_navigator=False, + copy_learning_results=False, + ): """Returns a deepcopy of itself replacing the data. - This method has an advantage over the default :py:func:`copy.deepcopy` + This method has an advantage over the default :func:`copy.deepcopy` in that it does not copy the data, which can save memory. Parameters ---------- - data : None or :py:class:`numpy.ndarray` + data : None or :class:`numpy.ndarray` copy_variance : bool Whether to copy the variance of the signal to the new copy copy_navigator : bool @@ -2308,7 +2620,7 @@ def _deepcopy_with_new_data(self, data=None, copy_variance=False, Returns ------- - ns : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + ns : :class:`~hyperspy.signal.BaseSignal` (or subclass) The newly copied signal """ @@ -2324,7 +2636,7 @@ def _deepcopy_with_new_data(self, data=None, copy_variance=False, if not copy_variance and "Noise_properties" in self.metadata.Signal: old_np = self.metadata.Signal.Noise_properties del self.metadata.Signal.Noise_properties - if not copy_navigator and self.metadata.has_item('_HyperSpy.navigator'): + if not copy_navigator and self.metadata.has_item("_HyperSpy.navigator"): old_navigator = self.metadata._HyperSpy.navigator del self.metadata._HyperSpy.navigator if not copy_learning_results: @@ -2345,11 +2657,12 @@ def _deepcopy_with_new_data(self, data=None, copy_variance=False, if old_learning_results is not None: self.learning_results = old_learning_results - def as_lazy(self, copy_variance=True, copy_navigator=True, - copy_learning_results=True): + def as_lazy( + self, copy_variance=True, copy_navigator=True, copy_learning_results=True + ): """ Create a copy of the given Signal as a - :py:class:`~hyperspy._signals.lazy.LazySignal`. + :class:`~hyperspy._signals.lazy.LazySignal`. Parameters ---------- @@ -2365,15 +2678,15 @@ def as_lazy(self, copy_variance=True, copy_navigator=True, Returns ------- - res : :py:class:`~hyperspy._signals.lazy.LazySignal` + res : :class:`~hyperspy._signals.lazy.LazySignal` The same signal, converted to be lazy """ res = self._deepcopy_with_new_data( self.data, copy_variance=copy_variance, copy_navigator=copy_navigator, - copy_learning_results=copy_learning_results - ) + copy_learning_results=copy_learning_results, + ) res._lazy = True res._assign_subclass() return res @@ -2395,19 +2708,88 @@ def _print_summary(self): @property def data(self): - """The underlying data structure as a :py:class:`numpy.ndarray` (or - :py:class:`dask.array.Array`, if the Signal is lazy).""" + """The underlying data structure as a :class:`numpy.ndarray` (or + :class:`dask.array.Array`, if the Signal is lazy).""" return self._data @data.setter def data(self, value): - from dask.array import Array - if isinstance(value, Array): - if not value.ndim: - value = value.reshape((1,)) - self._data = value + # Object supporting __array_function__ protocol (NEP-18) or the + # array API standard doesn't need to be cast to numpy array + if not ( + hasattr(value, "__array_function__") + or hasattr(value, "__array_namespace__") + ): + value = np.asanyarray(value) + self._data = np.atleast_1d(value) + + @property + def metadata(self): + """The metadata of the signal.""" + return self._metadata + + @property + def original_metadata(self): + """The original metadata of the signal.""" + return self._original_metadata + + @property + def ragged(self): + """Whether the signal is ragged or not.""" + return self.axes_manager._ragged + + @ragged.setter + def ragged(self, value): + # nothing needs to be done! + if self.ragged == value: + return + + if value: + if self.data.dtype != object: + raise ValueError("The array is not ragged.") + axes = [ + axis + for axis in self.axes_manager.signal_axes + if axis.index_in_array not in list(range(self.data.ndim)) + ] + self.axes_manager.remove(axes) + self.axes_manager._set_signal_dimension(0) else: - self._data = np.atleast_1d(np.asanyarray(value)) + if self._lazy: + raise NotImplementedError( + "Conversion of a lazy ragged signal to its non-ragged " + "counterpart is not supported. Make the required " + "non-ragged dask array manually and make a new lazy " + "signal." + ) + + error = "The signal can't be converted to a non-ragged signal." + try: + # Check that we can actually make a non-ragged array + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # As of numpy 1.20, it raises a VisibleDeprecationWarning + # and in the future, it will raise an error + data = np.array(self.data.tolist()) + except Exception: + raise ValueError(error) + + if data.dtype == object: + raise ValueError(error) + + self.data = data + # Add axes which were previously in the ragged dimension + axes = [ + idx + for idx in range(self.data.ndim) + if idx not in self.axes_manager.navigation_indices_in_array + ] + for index in axes: + axis = {"index_in_array": index, "size": self.data.shape[index]} + self.axes_manager._append_axis(**axis) + self.axes_manager._update_attributes() + + self.axes_manager._ragged = value def _load_dictionary(self, file_data_dict): """Load data from dictionary. @@ -2422,7 +2804,7 @@ def _load_dictionary(self, file_data_dict): * data: the signal data. It can be an array of any dimensions. * axes: a dictionary to define the axes (see the documentation of - the :py:class:`~hyperspy.axes.AxesManager` class for more details). + the :class:`~hyperspy.axes.AxesManager` class for more details). * attributes: a dictionary whose items are stored as attributes. * metadata: a dictionary containing a set of parameters that will @@ -2432,52 +2814,70 @@ def _load_dictionary(self, file_data_dict): that will to stores in the `original_metadata` attribute. It typically contains all the parameters that has been imported from the original data file. + * ragged: a bool, defining whether the signal is ragged or not. + Overwrite the attributes['ragged'] entry """ - self.data = file_data_dict['data'] + self.data = file_data_dict["data"] oldlazy = self._lazy - if 'models' in file_data_dict: - self.models._add_dictionary(file_data_dict['models']) - if 'axes' not in file_data_dict: - file_data_dict['axes'] = self._get_undefined_axes_list() - self.axes_manager = AxesManager( - file_data_dict['axes']) - if 'metadata' not in file_data_dict: - file_data_dict['metadata'] = {} - if 'original_metadata' not in file_data_dict: - file_data_dict['original_metadata'] = {} - if 'attributes' in file_data_dict: - for key, value in file_data_dict['attributes'].items(): - if hasattr(self, key): - if isinstance(value, dict): - for k, v in value.items(): - setattr(getattr(self, key), k, v) - else: - setattr(self, key, value) - self.original_metadata.add_dictionary( - file_data_dict['original_metadata']) - self.metadata.add_dictionary( - file_data_dict['metadata']) + attributes = file_data_dict.get("attributes", {}) + ragged = file_data_dict.get("ragged") + if ragged is not None: + attributes["ragged"] = ragged + if "axes" not in file_data_dict: + file_data_dict["axes"] = self._get_undefined_axes_list( + attributes.get("ragged", False) + ) + self.axes_manager = AxesManager(file_data_dict["axes"]) + # Setting `ragged` attributes requires the `axes_manager` + for key, value in attributes.items(): + if hasattr(self, key): + if isinstance(value, dict): + for k, v in value.items(): + setattr(getattr(self, key), k, v) + else: + setattr(self, key, value) + if "models" in file_data_dict: + self.models._add_dictionary(file_data_dict["models"]) + if "metadata" not in file_data_dict: + file_data_dict["metadata"] = {} + else: + # Get all hspy object back from their dictionary representation + _obj_in_dict2hspy(file_data_dict["metadata"], lazy=self._lazy) + if "original_metadata" not in file_data_dict: + file_data_dict["original_metadata"] = {} + + self.original_metadata.add_dictionary(file_data_dict["original_metadata"]) + self.metadata.add_dictionary(file_data_dict["metadata"]) if "title" not in self.metadata.General: - self.metadata.General.title = '' - if (self._signal_type or not self.metadata.has_item("Signal.signal_type")): + self.metadata.General.title = "" + if self._signal_type or not self.metadata.has_item("Signal.signal_type"): self.metadata.Signal.signal_type = self._signal_type if "learning_results" in file_data_dict: - self.learning_results.__dict__.update( - file_data_dict["learning_results"]) + self.learning_results.__dict__.update(file_data_dict["learning_results"]) if self._lazy is not oldlazy: self._assign_subclass() -# TODO: try to find a way to use dask ufuncs when called with lazy data (e.g. -# np.log(s) -> da.log(s.data) wrapped. - def __array__(self, dtype=None): + # TODO: try to find a way to use dask ufuncs when called with lazy data (e.g. + # np.log(s) -> da.log(s.data) wrapped. + def __array__(self, dtype=None, copy=None): + # The copy parameter was added in numpy 2.0 + if dtype is not None and dtype != self.data.dtype: + if copy is not None and not copy: + raise ValueError( + f"Converting array from {self.data.dtype} to " + f"{dtype} requires a copy." + ) if dtype: - return self.data.astype(dtype) + array = self.data.astype(dtype) else: - return self.data + array = self.data + if copy: + array = np.copy(array) - def __array_wrap__(self, array, context=None): + return array + def __array_wrap__(self, array, context=None, return_scalar=False): signal = self._deepcopy_with_new_data(array) if context is not None: # ufunc, argument of the ufunc, domain of the ufunc @@ -2503,13 +2903,15 @@ def get_title(signal, i=0): title_strs.append(str(obj)) signal.metadata.General.title = "%s(%s)" % ( - uf.__name__, ", ".join(title_strs)) + uf.__name__, + ", ".join(title_strs), + ) return signal def squeeze(self): """Remove single-dimensional entries from the shape of an array - and the axes. See :py:func:`numpy.squeeze` for more details. + and the axes. See :func:`numpy.squeeze` for more details. Returns ------- @@ -2518,7 +2920,8 @@ def squeeze(self): Examples -------- - >>> s = hs.signals.Signal2D(np.random.random((2,1,1,6,8,8))) + >>> s = hs.signals.Signal2D(np.random.random((2, 1, 1, 6, 8, 8))) + >>> s >>> s = s.squeeze() >>> s @@ -2533,8 +2936,9 @@ def squeeze(self): self.data = self.data.squeeze() return self - def _to_dictionary(self, add_learning_results=True, add_models=False, - add_original_metadata=True): + def _to_dictionary( + self, add_learning_results=True, add_models=False, add_original_metadata=True + ): """Returns a dictionary that can be used to recreate the signal. All items but `data` are copies. @@ -2557,62 +2961,78 @@ def _to_dictionary(self, add_learning_results=True, add_models=False, The dictionary that can be used to recreate the signal """ - dic = {'data': self.data, - 'axes': self.axes_manager._get_axes_dicts(), - 'metadata': copy.deepcopy(self.metadata.as_dictionary()), - 'tmp_parameters': self.tmp_parameters.as_dictionary(), - 'attributes': {'_lazy': self._lazy}, - } + dic = { + "data": self.data, + "axes": self.axes_manager._get_axes_dicts(), + "metadata": copy.deepcopy(self.metadata.as_dictionary()), + "tmp_parameters": self.tmp_parameters.as_dictionary(), + "attributes": {"_lazy": self._lazy, "ragged": self.axes_manager._ragged}, + } if add_original_metadata: - dic['original_metadata'] = copy.deepcopy( + dic["original_metadata"] = copy.deepcopy( self.original_metadata.as_dictionary() - ) - if add_learning_results and hasattr(self, 'learning_results'): - dic['learning_results'] = copy.deepcopy( - self.learning_results.__dict__) + ) + if add_learning_results and hasattr(self, "learning_results"): + dic["learning_results"] = copy.deepcopy(self.learning_results.__dict__) if add_models: - dic['models'] = self.models._models.as_dictionary() + dic["models"] = self.models._models.as_dictionary() return dic - def _get_undefined_axes_list(self): + def _get_undefined_axes_list(self, ragged=False): + """Returns default list of axes construct from the data array shape.""" axes = [] for s in self.data.shape: - axes.append({'size': int(s), }) + axes.append( + { + "size": int(s), + } + ) + # With ragged signal with navigation dimension 0 and signal dimension 0 + # we return an empty list to avoid getting a navigation axis of size 1, + # which is incorrect, because it corresponds to the ragged dimension + if ragged and len(axes) == 1 and axes[0]["size"] == 1: + axes = [] return axes - def __call__(self, axes_manager=None, fft_shift=False): + def _get_current_data(self, axes_manager=None, fft_shift=False, as_numpy=False): if axes_manager is None: axes_manager = self.axes_manager - value = np.atleast_1d(self.data.__getitem__( - axes_manager._getitem_tuple)) - if isinstance(value, da.Array): - value = np.asarray(value) + indices = axes_manager._getitem_tuple + if self._lazy: + value = self._get_cache_dask_chunk(indices) + else: + value = self.data.__getitem__(indices) + if as_numpy: + value = to_numpy(value) + value = np.atleast_1d(value) if fft_shift: value = np.fft.fftshift(value) return value @property def navigator(self): - return self.metadata.get_item('_HyperSpy.navigator') + return self.metadata.get_item("_HyperSpy.navigator") @navigator.setter def navigator(self, navigator): - self.metadata.set_item('_HyperSpy.navigator', navigator) + self.metadata.set_item("_HyperSpy.navigator", navigator) - def plot(self, navigator="auto", axes_manager=None, plot_markers=True, - **kwargs): + def plot(self, navigator="auto", axes_manager=None, plot_markers=True, **kwargs): """%s %s %s %s """ + if self.axes_manager.ragged: + raise RuntimeError("Plotting ragged signal is not supported.") if self._plot is not None: self._plot.close() - if 'power_spectrum' in kwargs: + if "power_spectrum" in kwargs: if not np.issubdtype(self.data.dtype, np.complexfloating): - raise ValueError('The parameter `power_spectrum` required a ' - 'signal with complex data type.') - del kwargs['power_spectrum'] + raise ValueError( + "The parameter `power_spectrum` required a " + "signal with complex data type." + ) if axes_manager is None: axes_manager = self.axes_manager @@ -2621,7 +3041,26 @@ def plot(self, navigator="auto", axes_manager=None, plot_markers=True, navigator = None else: navigator = "slider" + + from hyperspy.defaults_parser import preferences + + if ( + "fig" not in kwargs.keys() + and preferences.Plot.use_subfigure + and axes_manager.navigation_dimension > 0 + and axes_manager.signal_dimension in [1, 2] + ): + # Create default subfigure + fig = plt.figure(figsize=(15, 7), layout="constrained") + subfigs = fig.subfigures(1, 2) + kwargs["fig"] = subfigs[1] + kwargs["navigator_kwds"] = dict(fig=subfigs[0]) + if axes_manager.signal_dimension == 0: + if axes_manager.navigation_dimension == 0: + # 0d signal without navigation axis: don't make a figure + # and instead, we display the value + return self._plot = mpl_he.MPL_HyperExplorer() elif axes_manager.signal_dimension == 1: # Hyperspectrum @@ -2634,145 +3073,198 @@ def plot(self, navigator="auto", axes_manager=None, plot_markers=True, "Try e.g. 's.transpose(signal_axes=1).plot()' for " "plotting as a 1D signal, or " "'s.transpose(signal_axes=(1,2)).plot()' " - "for plotting as a 2D signal.") + "for plotting as a 2D signal." + ) self._plot.axes_manager = axes_manager - self._plot.signal_data_function = self.__call__ + self._plot.signal_data_function = partial(self._get_current_data, as_numpy=True) if self.metadata.has_item("Signal.quantity"): self._plot.quantity_label = self.metadata.Signal.quantity if self.metadata.General.title: title = self.metadata.General.title self._plot.signal_title = title - elif self.tmp_parameters.has_item('filename'): + elif self.tmp_parameters.has_item("filename"): self._plot.signal_title = self.tmp_parameters.filename + def sum_wrapper(s, axis): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=UserWarning, module="hyperspy" + ) + return s.sum(axis) + def get_static_explorer_wrapper(*args, **kwargs): if np.issubdtype(navigator.data.dtype, np.complexfloating): - return np.abs(navigator()) + return abs(navigator._get_current_data(as_numpy=True)) else: - return navigator() + return navigator._get_current_data(as_numpy=True) def get_1D_sum_explorer_wrapper(*args, **kwargs): - navigator = self # Sum over all but the first navigation axis. - am = navigator.axes_manager - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning, - module='hyperspy' - ) - navigator = navigator.sum( - am.signal_axes + am.navigation_axes[1:] - ) - return np.nan_to_num(navigator.data).squeeze() - - def get_dynamic_explorer_wrapper(*args, **kwargs): - navigator.axes_manager.indices = self.axes_manager.indices[ - navigator.axes_manager.signal_dimension:] - navigator.axes_manager._update_attributes() - if np.issubdtype(navigator().dtype, np.complexfloating): - return np.abs(navigator()) - else: - return navigator() + am = self.axes_manager + navigator = sum_wrapper(self, am.signal_axes + am.navigation_axes[1:]) + return np.nan_to_num(to_numpy(navigator.data)).squeeze() + + def get_dynamic_image_explorer(*args, **kwargs): + am = self.axes_manager + nav_ind = am.indices[2:] # image at first 2 nav indices + slices = [slice(None)] * len(am.navigation_axes) + slices[2:] = nav_ind + new_nav = navigator.transpose( + signal_axes=len(am.navigation_axes) + ) # roll axes to signal axes + ind = new_nav.isig.__getitem__( + slices=slices + ) # Get the value from the nav reverse because hyperspy + return np.nan_to_num(to_numpy(ind.data)).squeeze() + + # function to disconnect when closing the navigator + function_to_disconnect = None + connected_event_copy = self.events.data_changed.connected.copy() if not isinstance(navigator, BaseSignal) and navigator == "auto": if self.navigator is not None: navigator = self.navigator - elif (self.axes_manager.navigation_dimension > 1 and - np.any(np.array([not axis.is_uniform for axis in - self.axes_manager.navigation_axes]))): + elif self.axes_manager.navigation_dimension > 1 and np.any( + np.array( + [not axis.is_uniform for axis in self.axes_manager.navigation_axes] + ) + ): navigator = "slider" - elif (self.axes_manager.navigation_dimension == 1 and - self.axes_manager.signal_dimension == 1): - if (self.axes_manager.navigation_axes[0].is_uniform and - self.axes_manager.signal_axes[0].is_uniform): - navigator = "data" - else: - navigator = "spectrum" + elif ( + self.axes_manager.navigation_dimension == 1 + and self.axes_manager.signal_dimension == 1 + ): + navigator = "data" elif self.axes_manager.navigation_dimension > 0: if self.axes_manager.signal_dimension == 0: navigator = self.deepcopy() else: navigator = interactive( - self.sum, - self.events.data_changed, - self.axes_manager.events.any_axis_changed, - self.axes_manager.signal_axes) + f=sum_wrapper, + event=self.events.data_changed, + recompute_out_event=self.axes_manager.events.any_axis_changed, + s=self, + axis=self.axes_manager.signal_axes, + ) + # Sets are not ordered, to retrieve the function to disconnect + # take the difference with the previous copy + function_to_disconnect = list( + self.events.data_changed.connected - connected_event_copy + )[0] if navigator.axes_manager.navigation_dimension == 1: navigator = interactive( - navigator.as_signal1D, - navigator.events.data_changed, - navigator.axes_manager.events.any_axis_changed, 0) + f=navigator.as_signal1D, + event=navigator.events.data_changed, + recompute_out_event=navigator.axes_manager.events.any_axis_changed, + spectral_axis=0, + ) else: navigator = interactive( - navigator.as_signal2D, - navigator.events.data_changed, - navigator.axes_manager.events.any_axis_changed, - (0, 1)) + f=navigator.as_signal2D, + event=navigator.events.data_changed, + recompute_out_event=navigator.axes_manager.events.any_axis_changed, + image_axes=(0, 1), + ) else: navigator = None # Navigator properties if axes_manager.navigation_axes: - # check first if we have a signal to avoid comparion of signal with + # check first if we have a signal to avoid comparison of signal with # string if isinstance(navigator, BaseSignal): + def is_shape_compatible(navigation_shape, shape): - return (navigation_shape == shape or - navigation_shape[:2] == shape or - (navigation_shape[0],) == shape - ) + return ( + navigation_shape == shape + or navigation_shape[:2] == shape + or (navigation_shape[0],) == shape + ) + # Static navigator - if is_shape_compatible(axes_manager.navigation_shape, - navigator.axes_manager.signal_shape): - self._plot.navigator_data_function = get_static_explorer_wrapper + if is_shape_compatible( + axes_manager.navigation_shape, navigator.axes_manager.signal_shape + ): + if len(axes_manager.navigation_shape) > 2: + self._plot.navigator_data_function = get_dynamic_image_explorer + else: + self._plot.navigator_data_function = get_static_explorer_wrapper # Static transposed navigator - elif is_shape_compatible(axes_manager.navigation_shape, - navigator.axes_manager.navigation_shape): + elif is_shape_compatible( + axes_manager.navigation_shape, + navigator.axes_manager.navigation_shape, + ): navigator = navigator.T - self._plot.navigator_data_function = get_static_explorer_wrapper - # Dynamic navigator - elif (axes_manager.navigation_shape == - navigator.axes_manager.signal_shape + - navigator.axes_manager.navigation_shape): - self._plot.navigator_data_function = get_dynamic_explorer_wrapper + if len(axes_manager.navigation_shape) > 2: + self._plot.navigator_data_function = get_dynamic_image_explorer + else: + self._plot.navigator_data_function = get_static_explorer_wrapper else: raise ValueError( "The dimensions of the provided (or stored) navigator " - "are not compatible with this signal.") - elif navigator == "slider": - self._plot.navigator_data_function = "slider" + "are not compatible with this signal." + ) + elif isinstance(navigator, str): + if navigator == "slider": + self._plot.navigator_data_function = "slider" + elif navigator == "data": + if np.issubdtype(self.data.dtype, np.complexfloating): + self._plot.navigator_data_function = ( + lambda axes_manager=None: to_numpy(abs(self.data)) + ) + else: + self._plot.navigator_data_function = ( + lambda axes_manager=None: to_numpy(self.data) + ) + elif navigator == "spectrum": + self._plot.navigator_data_function = get_1D_sum_explorer_wrapper elif navigator is None: self._plot.navigator_data_function = None - elif navigator == "data": - if np.issubdtype(self.data.dtype, np.complexfloating): - self._plot.navigator_data_function = lambda axes_manager=None: np.abs( - self.data) - else: - self._plot.navigator_data_function = lambda axes_manager=None: self.data - elif navigator == "spectrum": - self._plot.navigator_data_function = get_1D_sum_explorer_wrapper else: raise ValueError( 'navigator must be one of "spectrum","auto", ' - '"slider", None, a Signal instance') + '"slider", None, a Signal instance' + ) self._plot.plot(**kwargs) self.events.data_changed.connect(self.update_plot, []) - p = self._plot.signal_plot if self._plot.signal_plot else self._plot.navigator_plot + # Disconnect event when closing signal + p = ( + self._plot.signal_plot + if self._plot.signal_plot + else self._plot.navigator_plot + ) p.events.closed.connect( - lambda: self.events.data_changed.disconnect(self.update_plot), - []) + lambda: self.events.data_changed.disconnect(self.update_plot), [] + ) + # Disconnect events to the navigator when closing navigator + if function_to_disconnect is not None: + self._plot.navigator_plot.events.closed.connect( + lambda: self.events.data_changed.disconnect(function_to_disconnect), [] + ) + self._plot.navigator_plot.events.closed.connect( + lambda: self.axes_manager.events.any_axis_changed.disconnect( + function_to_disconnect + ), + [], + ) if plot_markers: - if self.metadata.has_item('Markers'): + if self.metadata.has_item("Markers"): self._plot_permanent_markers() - plot.__doc__ %= (BASE_PLOT_DOCSTRING, BASE_PLOT_DOCSTRING_PARAMETERS, - PLOT1D_DOCSTRING, PLOT2D_KWARGS_DOCSTRING) + plot.__doc__ %= ( + BASE_PLOT_DOCSTRING, + BASE_PLOT_DOCSTRING_PARAMETERS, + PLOT1D_DOCSTRING, + PLOT2D_KWARGS_DOCSTRING, + ) - def save(self, filename=None, overwrite=None, extension=None, - **kwds): + def save( + self, filename=None, overwrite=None, extension=None, file_format=None, **kwds + ): """Saves the signal in the specified format. The function gets the format from the specified extension (see @@ -2835,26 +3327,38 @@ def save(self, filename=None, overwrite=None, extension=None, Nexus file only. Define the default dataset in the file. If set to True the signal or first signal in the list of signals will be defined as the default (following Nexus v3 data rules). + write_dataset : bool, optional + Only for hspy files. If True, write the dataset, otherwise, don't + write it. Useful to save attributes without having to write the + whole dataset. Default is True. + close_file : bool, optional + Only for hdf5-based files and some zarr store. Close the file after + writing. Default is True. + file_format: string + The file format of choice to save the file. If not given, it is inferred + from the file extension. """ if filename is None: - if (self.tmp_parameters.has_item('filename') and - self.tmp_parameters.has_item('folder')): + if self.tmp_parameters.has_item( + "filename" + ) and self.tmp_parameters.has_item("folder"): filename = Path( - self.tmp_parameters.folder, - self.tmp_parameters.filename) - extension = (self.tmp_parameters.extension - if not extension - else extension) - elif self.metadata.has_item('General.original_filename'): + self.tmp_parameters.folder, self.tmp_parameters.filename + ) + extension = ( + self.tmp_parameters.extension if not extension else extension + ) + elif self.metadata.has_item("General.original_filename"): filename = self.metadata.General.original_filename else: - raise ValueError('File name not defined') + raise ValueError("File name not defined") - filename = Path(filename) - if extension is not None: - filename = filename.with_suffix(f".{extension}") - io.save(filename, self, overwrite=overwrite, **kwds) + if not isinstance(filename, MutableMapping): + filename = Path(filename) + if extension is not None: + filename = filename.with_suffix(f".{extension}") + io_save(filename, self, overwrite=overwrite, file_format=file_format, **kwds) def _replot(self): if self._plot is not None: @@ -2902,21 +3406,20 @@ def crop(self, axis, start=None, end=None, convert_units=False): ``None`` the method crops from/to the low/high end of the axis. convert_units : bool Default is ``False``. If ``True``, convert the units using the - :py:meth:`~hyperspy.axes.AxesManager.convert_units` method - of the :py:class:`~hyperspy.axes.AxesManager`. If ``False``, + :meth:`~hyperspy.axes.AxesManager.convert_units` method + of the :class:`~hyperspy.axes.AxesManager`. If ``False``, does nothing. """ axis = self.axes_manager[axis] i1, i2 = axis._get_index(start), axis._get_index(end) # To prevent an axis error, which may confuse users if i1 is not None and i2 is not None and not i1 != i2: - raise ValueError("The `start` and `end` values need to be " - "different.") + raise ValueError("The `start` and `end` values need to be " "different.") # We take a copy to guarantee the continuity of the data self.data = self.data[ - (slice(None),) * axis.index_in_array + (slice(i1, i2), - Ellipsis)] + (slice(None),) * axis.index_in_array + (slice(i1, i2), Ellipsis) + ] axis.crop(i1, i2) self.get_dimensions_from_data() @@ -2925,6 +3428,110 @@ def crop(self, axis, start=None, end=None, convert_units=False): if convert_units: self.axes_manager.convert_units(axis) + def interpolate_on_axis(self, new_axis, axis=0, inplace=False, degree=1): + """ + Replaces the given ``axis`` with the provided ``new_axis`` + and interpolates data accordingly using + :func:`scipy.interpolate.make_interp_spline`. + + Parameters + ---------- + new_axis : :class:`hyperspy.axes.UniformDataAxis`, + :class:`hyperspy.axes.DataAxis`, :class:`hyperspy.axes.FunctionalDataAxis` + or str + Axis which replaces the one specified by the ``axis`` argument. + If this new axis exceeds the range of the old axis, + a warning is raised that the data will be extrapolated. + If ``"uniform"``, convert the axis specified by the ``axis`` + parameter to a uniform axis with the same number of data points. + axis : int or str, default 0 + Specifies the axis which will be replaced using the index of the + axis in the `axes_manager`. The axis can be specified using the index of the + axis in `axes_manager` or the axis name. + inplace : bool, default False + If ``True`` the data of `self` is replaced by the result and + the axis is changed inplace. Otherwise `self` is not changed + and a new signal with the changes incorporated is returned. + degree: int, default 1 + Specifies the B-Spline degree of the used interpolator. + + Returns + ------- + :class:`~.api.signals.BaseSignal` (or subclass) + A copy of the object with the axis exchanged and the data interpolated. + This only occurs when inplace is set to ``False``, otherwise nothing is returned. + + Examples + -------- + >>> s = hs.data.luminescence_signal(uniform=False) + >>> s2 = s.interpolate_on_axis("uniform", -1, inplace=False) + >>> hs.plot.plot_spectra( + ... [s, s2], + ... legend=["FunctionalAxis", "Interpolated"], + ... drawstyle='steps-mid', + ... ) + + + Specifying a uniform axis: + + >>> s = hs.data.luminescence_signal(uniform=False) + >>> new_axis = s.axes_manager[-1].copy() + >>> new_axis.convert_to_uniform_axis() + >>> s3 = s.interpolate_on_axis(new_axis, -1, inplace=False) + >>> hs.plot.plot_spectra( + ... [s, s3], + ... legend=["FunctionalAxis", "Interpolated"], + ... drawstyle='steps-mid', + ... ) + + """ + old_axis = self.axes_manager[axis] + axis_idx = old_axis.index_in_array + interpolator = make_interp_spline( + old_axis.axis, + self.data, + axis=axis_idx, + k=degree, + ) + + if new_axis == "uniform": + if old_axis.is_uniform: + raise ValueError(f"The axis {old_axis.name} is already uniform.") + if inplace: + old_axis.convert_to_uniform_axis(log_scale_error=False) + new_axis = old_axis + else: + new_axis = old_axis.copy() + new_axis.convert_to_uniform_axis(log_scale_error=False) + else: + if old_axis.navigate != new_axis.navigate: + raise ValueError( + "The navigate attribute of new_axis differs from the to be replaced axis." + ) + if ( + old_axis.low_value > new_axis.low_value + or old_axis.high_value < new_axis.high_value + ): + _logger.warning( + "The specified new axis exceeds the range of the to be replaced old axis. " + "The data will be extrapolated if not specified otherwise via fill_value/bounds_error" + ) + + new_data = interpolator(new_axis.axis) + + if inplace: + self.data = new_data + s = self + else: + s = self._deepcopy_with_new_data(new_data) + + if new_axis is not old_axis: + # user specifies a new_axis != "uniform" + s.axes_manager.set_axis(new_axis, axis_idx) + + if not inplace: + return s + def swap_axes(self, axis1, axis2, optimize=False): """Swap two axes in the signal. @@ -2936,10 +3543,10 @@ def swap_axes(self, axis1, axis2, optimize=False): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A copy of the object with the axes swapped. - See also + See Also -------- rollaxis """ @@ -2976,38 +3583,38 @@ def rollaxis(self, axis, to_axis, optimize=False): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) Output signal. - See also + See Also -------- - :py:func:`numpy.roll`, swap_axes + :func:`numpy.roll`, swap_axes Examples -------- - >>> s = hs.signals.Signal1D(np.ones((5,4,3,6))) + >>> s = hs.signals.Signal1D(np.ones((5, 4, 3, 6))) >>> s - + >>> s.rollaxis(3, 1) - - >>> s.rollaxis(2,0) - + + >>> s.rollaxis(2, 0) + """ axis = self.axes_manager[axis].index_in_array to_index = self.axes_manager[to_axis].index_in_array if axis == to_index: return self.deepcopy() - new_axes_indices = hyperspy.misc.utils.rollelem( + new_axes_indices = rollelem( [axis_.index_in_array for axis_ in self.axes_manager._axes], index=axis, - to_index=to_index) + to_index=to_index, + ) s = self._deepcopy_with_new_data(self.data.transpose(new_axes_indices)) - s.axes_manager._axes = hyperspy.misc.utils.rollelem( - s.axes_manager._axes, - index=axis, - to_index=to_index) + s.axes_manager._axes = rollelem( + s.axes_manager._axes, index=axis, to_index=to_index + ) s.axes_manager._update_attributes() if optimize: s._make_sure_data_is_contiguous() @@ -3017,9 +3624,7 @@ def rollaxis(self, axis, to_axis, optimize=False): @property def _data_aligned_with_axes(self): - """Returns a view of `data` with is axes aligned with the Signal axes. - - """ + """Returns a view of `data` with is axes aligned with the Signal axes.""" if self.axes_manager.axes_are_aligned_with_data: return self.data else: @@ -3032,39 +3637,48 @@ def _data_aligned_with_axes(self): return data def _validate_rebin_args_and_get_factors(self, new_shape=None, scale=None): - if new_shape is None and scale is None: raise ValueError("One of new_shape, or scale must be specified") - elif new_shape is None and scale is None: + elif new_shape is not None and scale is not None: raise ValueError( - "Only one out of new_shape or scale should be specified. " - "Not both.") + "Only one out of new_shape or scale should be specified. " "Not both." + ) elif new_shape: if len(new_shape) != len(self.data.shape): raise ValueError("Wrong new_shape size") - for axis in self.axes_manager._axes: - if axis.is_uniform is False: + for i, axis in enumerate(self.axes_manager._axes): + # If the shape doesn't change, there is nothing to do + # and we don't need to raise an error + if axis.is_uniform is False and new_shape[i] != self.data.shape[i]: raise NotImplementedError( - "Rebinning of non-uniform axes is not yet implemented.") - new_shape_in_array = np.array([new_shape[axis.index_in_axes_manager] - for axis in self.axes_manager._axes]) + "Rebinning of non-uniform axes is not yet implemented. " + "An alternative is to interpolate the data on an uniform axis " + "using `interpolate_on_axis` before rebinning." + ) + new_shape_in_array = np.array( + [ + new_shape[axis.index_in_axes_manager] + for axis in self.axes_manager._axes + ] + ) factors = np.array(self.data.shape) / new_shape_in_array else: if len(scale) != len(self.data.shape): raise ValueError("Wrong scale size") - for axis in self.axes_manager._axes: - if axis.is_uniform is False: + for i, axis in enumerate(self.axes_manager._axes): + if axis.is_uniform is False and scale[i] != 1: raise NotImplementedError( - "Rebinning of non-uniform axes is not yet implemented.") - factors = np.array([scale[axis.index_in_axes_manager] - for axis in self.axes_manager._axes]) + "Rebinning of non-uniform axes is not yet implemented." + ) + factors = np.array( + [scale[axis.index_in_axes_manager] for axis in self.axes_manager._axes] + ) return factors # Factors are in array order - def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, - out=None): + def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, out=None): """ Rebin the signal into a smaller or larger shape, based on linear - interpolation. Specify **either** `new_shape` or `scale`. Scale of 1 + interpolation. Specify **either** ``new_shape`` or ``scale``. Scale of 1 means no binning and scale less than one results in up-sampling. Parameters @@ -3074,7 +3688,7 @@ def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + ~hyperspy.api.signals.BaseSignal The resulting cropped signal. Raises @@ -3084,27 +3698,27 @@ def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, Examples -------- - >>> spectrum = hs.signals.EDSTEMSpectrum(np.ones([4, 4, 10])) + >>> spectrum = hs.signals.Signal1D(np.ones([4, 4, 10])) >>> spectrum.data[1, 2, 9] = 5 >>> print(spectrum) - - >>> print ('Sum = ', sum(sum(sum(spectrum.data)))) + + >>> print ('Sum =', sum(sum(sum(spectrum.data)))) Sum = 164.0 >>> scale = [2, 2, 5] >>> test = spectrum.rebin(scale) >>> print(test) - - >>> print('Sum = ', sum(sum(sum(test.data)))) - Sum = 164.0 + + >>> print('Sum =', sum(sum(sum(test.data)))) + Sum = 164.0 - >>> s = hs.signals.Signal1D(np.ones((2, 5, 10), dtype=np.uint8) + >>> s = hs.signals.Signal1D(np.ones((2, 5, 10), dtype=np.uint8)) >>> print(s) >>> print(s.data.dtype) uint8 - Use dtype=np.unit16 to specify a dtype + Use ``dtype=np.unit16`` to specify a dtype >>> s2 = s.rebin(scale=(5, 2, 1), dtype=np.uint16) >>> print(s2.data.dtype) @@ -3116,13 +3730,13 @@ def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, >>> print(s3.data.dtype) uint8 - By default `dtype=None`, the dtype is determined by the behaviour of + By default ``dtype=None``, the dtype is determined by the behaviour of numpy.sum, in this case, unsigned integer of the same precision as - the platform interger + the platform integer >>> s4 = s.rebin(scale=(5, 2, 1)) - >>> print(s4.data.dtype) - uint64 + >>> print(s4.data.dtype) # doctest: +SKIP + uint32 """ # TODO: Adapt so that it works if a non_uniform_axis exists, but is not @@ -3130,10 +3744,10 @@ def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, # linear grid factors = self._validate_rebin_args_and_get_factors( new_shape=new_shape, - scale=scale,) + scale=scale, + ) s = out or self._deepcopy_with_new_data(None, copy_variance=True) - data = hyperspy.misc.array_tools.rebin( - self.data, scale=factors, crop=crop, dtype=dtype) + data = array_rebin(self.data, scale=factors, crop=crop, dtype=dtype) if out: if out._lazy: @@ -3143,18 +3757,18 @@ def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, else: s.data = data s.get_dimensions_from_data() - for axis, axis_src in zip(s.axes_manager._axes, - self.axes_manager._axes): + for axis, axis_src in zip(s.axes_manager._axes, self.axes_manager._axes): factor = factors[axis.index_in_array] - axis.scale = axis_src.scale * factor - axis.offset = axis_src.offset + (factor - 1) * axis_src.scale / 2 - if s.metadata.has_item('Signal.Noise_properties.variance'): - if isinstance(s.metadata.Signal.Noise_properties.variance, - BaseSignal): + if factor != 1: + # Change scale, offset only when necessary + axis.scale = axis_src.scale * factor + axis.offset = axis_src.offset + (factor - 1) * axis_src.scale / 2 + if s.metadata.has_item("Signal.Noise_properties.variance"): + if isinstance(s.metadata.Signal.Noise_properties.variance, BaseSignal): var = s.metadata.Signal.Noise_properties.variance s.metadata.Signal.Noise_properties.variance = var.rebin( - new_shape=new_shape, scale=scale, crop=crop, out=out, - dtype=dtype) + new_shape=new_shape, scale=scale, crop=crop, out=out, dtype=dtype + ) if out is None: return s else: @@ -3162,24 +3776,21 @@ def rebin(self, new_shape=None, scale=None, crop=True, dtype=None, rebin.__doc__ %= (REBIN_ARGS, OUT_ARG) - def split(self, - axis='auto', - number_of_parts='auto', - step_sizes='auto'): + def split(self, axis="auto", number_of_parts="auto", step_sizes="auto"): """Splits the data into several signals. The split can be defined by giving the `number_of_parts`, a homogeneous step size, or a list of customized step sizes. By default (``'auto'``), - the function is the reverse of :py:func:`~hyperspy.misc.utils.stack`. + the function is the reverse of :func:`~hyperspy.api.stack`. Parameters ---------- axis %s If ``'auto'`` and if the object has been created with - :py:func:`~hyperspy.misc.utils.stack` (and ``stack_metadata=True``), + :func:`~hyperspy.api.stack` (and ``stack_metadata=True``), this method will return the former list of signals (information stored in `metadata._HyperSpy.Stacking_history`). - If it was not created with :py:func:`~hyperspy.misc.utils.stack`, + If it was not created with :func:`~hyperspy.api.stack`, the last navigation axis will be used. number_of_parts : str or int Number of parts in which the spectrum image will be split. The @@ -3189,26 +3800,21 @@ def split(self, `number_of_parts` equals the length of the axis, `step_sizes` equals one, and the axis is suppressed from each sub-spectrum. - step_sizes : str, list (of ints), or int + step_sizes : str, list (of int), or int Size of the split parts. If ``'auto'``, the `step_sizes` equals one. If an int is given, the splitting is homogeneous. Examples -------- - >>> s = hs.signals.Signal1D(random.random([4,3,2])) + >>> s = hs.signals.Signal1D(np.random.random([4, 3, 2])) >>> s - + >>> s.split() - [, - , - , - ] + [, , , ] >>> s.split(step_sizes=2) - [, - ] - >>> s.split(step_sizes=[1,2]) - [, - ] + [, ] + >>> s.split(step_sizes=[1, 2]) + [, ] Raises ------ @@ -3217,11 +3823,11 @@ def split(self, Returns ------- - splitted : list + list of :class:`~hyperspy.api.signals.BaseSignal` A list of the split signals """ - if number_of_parts != 'auto' and step_sizes != 'auto': + if number_of_parts != "auto" and step_sizes != "auto": raise ValueError( "You can define step_sizes or number_of_parts but not both." ) @@ -3229,35 +3835,35 @@ def split(self, shape = self.data.shape signal_dict = self._to_dictionary(add_learning_results=False) - if axis == 'auto': - mode = 'auto' - if hasattr(self.metadata._HyperSpy, 'Stacking_history'): + if axis == "auto": + mode = "auto" + if hasattr(self.metadata._HyperSpy, "Stacking_history"): stack_history = self.metadata._HyperSpy.Stacking_history axis_in_manager = stack_history.axis step_sizes = stack_history.step_sizes else: - axis_in_manager = self.axes_manager[-1 + - 1j].index_in_axes_manager + axis_in_manager = self.axes_manager[-1 + 1j].index_in_axes_manager else: - mode = 'manual' + mode = "manual" axis_in_manager = self.axes_manager[axis].index_in_axes_manager axis = self.axes_manager[axis_in_manager].index_in_array len_axis = self.axes_manager[axis_in_manager].size if self.axes_manager[axis].is_uniform is False: raise NotImplementedError( - "Splitting of signals over a non-uniform axis is not implemented") + "Splitting of signals over a non-uniform axis is not implemented" + ) - if number_of_parts == 'auto' and step_sizes == 'auto': + if number_of_parts == "auto" and step_sizes == "auto": step_sizes = 1 number_of_parts = len_axis - elif step_sizes == 'auto': + elif step_sizes == "auto": if number_of_parts > shape[axis]: - raise ValueError( - "The number of parts is greater than the axis size." - ) + raise ValueError("The number of parts is greater than the axis size.") - step_sizes = ([shape[axis] // number_of_parts, ] * number_of_parts) + step_sizes = [ + shape[axis] // number_of_parts, + ] * number_of_parts if isinstance(step_sizes, numbers.Integral): step_sizes = [step_sizes] * int(len_axis / step_sizes) @@ -3265,37 +3871,36 @@ def split(self, splitted = [] cut_index = np.array([0] + step_sizes).cumsum() - axes_dict = signal_dict['axes'] + axes_dict = signal_dict["axes"] for i in range(len(cut_index) - 1): - axes_dict[axis]['offset'] = self.axes_manager._axes[ - axis].index2value(cut_index[i]) - axes_dict[axis]['size'] = cut_index[i + 1] - cut_index[i] + axes_dict[axis]["offset"] = self.axes_manager._axes[axis].index2value( + cut_index[i] + ) + axes_dict[axis]["size"] = cut_index[i + 1] - cut_index[i] data = self.data[ - (slice(None), ) * axis + - (slice(cut_index[i], cut_index[i + 1]), Ellipsis)] - signal_dict['data'] = data - splitted += self.__class__(**signal_dict), + (slice(None),) * axis + + (slice(cut_index[i], cut_index[i + 1]), Ellipsis) + ] + signal_dict["data"] = data + splitted += (self.__class__(**signal_dict),) - if number_of_parts == len_axis \ - or step_sizes == [1] * len_axis: + if number_of_parts == len_axis or step_sizes == [1] * len_axis: for i, signal1D in enumerate(splitted): signal1D.data = signal1D.data[ - signal1D.axes_manager._get_data_slice([(axis, 0)])] + signal1D.axes_manager._get_data_slice([(axis, 0)]) + ] signal1D._remove_axis(axis_in_manager) - if mode == 'auto' and hasattr( - self.original_metadata, 'stack_elements'): + if mode == "auto" and hasattr(self.original_metadata, "stack_elements"): for i, spectrum in enumerate(splitted): - se = self.original_metadata.stack_elements['element' + str(i)] - spectrum.metadata = copy.deepcopy( - se['metadata']) - spectrum.original_metadata = copy.deepcopy( - se['original_metadata']) + se = self.original_metadata.stack_elements["element" + str(i)] + spectrum._metadata = copy.deepcopy(se["metadata"]) + spectrum._original_metadata = copy.deepcopy(se["original_metadata"]) spectrum.metadata.General.title = se.metadata.General.title return splitted - split.__doc__ %= (ONE_AXIS_PARAMETER) + split.__doc__ %= ONE_AXIS_PARAMETER def _unfold(self, steady_axes, unfolded_axis): """Modify the shape of the data by specifying the axes whose @@ -3310,7 +3915,7 @@ def _unfold(self, steady_axes, unfolded_axis): The index of the axis over which all the rest of the axes (except the steady axes) will be unfolded - See also + See Also -------- fold @@ -3318,9 +3923,9 @@ def _unfold(self, steady_axes, unfolded_axis): ----- WARNING: this private function does not modify the signal subclass and it is intended for internal use only. To unfold use the public - :py:meth:`~hyperspy.signal.BaseSignal.unfold`, - :py:meth:`~hyperspy.signal.BaseSignal.unfold_navigation_space`, - :py:meth:`~hyperspy.signal.BaseSignal.unfold_signal_space` instead. + :meth:`~hyperspy.signal.BaseSignal.unfold`, + :meth:`~hyperspy.signal.BaseSignal.unfold_navigation_space`, + :meth:`~hyperspy.signal.BaseSignal.unfold_signal_space` instead. It doesn't make sense to perform an unfolding when `dim` < 2 """ @@ -3342,13 +3947,13 @@ def _unfold(self, steady_axes, unfolded_axis): new_shape[unfolded_axis] = -1 self.data = self.data.reshape(new_shape) self.axes_manager = self.axes_manager.deepcopy() - uname = '' - uunits = '' + uname = "" + uunits = "" to_remove = [] for axis, dim in zip(self.axes_manager._axes, new_shape): if dim == 1: - uname += ',' + str(axis) - uunits = ',' + str(axis.units) + uname += "," + str(axis) + uunits = "," + str(axis.units) to_remove.append(axis) ua = self.axes_manager._axes[unfolded_axis] ua.name = str(ua) + uname @@ -3378,8 +3983,8 @@ def unfold(self, unfold_navigation=True, unfold_signal=True): Whether or not one of the axes needed unfolding (and that unfolding was performed) - Note - ---- + Notes + ----- It doesn't make sense to perform an unfolding when the total number of dimensions is < 2. """ @@ -3398,17 +4003,17 @@ def unfolded(self, unfold_navigation=True, unfold_signal=True): signal be unfolded for the scope of the `with` block, before automatically refolding when passing out of scope. - See also + See Also -------- unfold, fold Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) >>> with s.unfolded(): - # Do whatever needs doing while unfolded here - pass + ... # Do whatever needs doing while unfolded here + ... pass """ unfolded = self.unfold(unfold_navigation, unfold_signal) @@ -3434,12 +4039,11 @@ def unfold_navigation_space(self): else: needed_unfolding = True steady_axes = [ - axis.index_in_array for axis in - self.axes_manager.signal_axes] - unfolded_axis = ( - self.axes_manager.navigation_axes[0].index_in_array) + axis.index_in_array for axis in self.axes_manager.signal_axes + ] + unfolded_axis = self.axes_manager.navigation_axes[0].index_in_array self._unfold(steady_axes, unfolded_axis) - if self.metadata.has_item('Signal.Noise_properties.variance'): + if self.metadata.has_item("Signal.Noise_properties.variance"): variance = self.metadata.Signal.Noise_properties.variance if isinstance(variance, BaseSignal): variance.unfold_navigation_space() @@ -3460,12 +4064,12 @@ def unfold_signal_space(self): else: needed_unfolding = True steady_axes = [ - axis.index_in_array for axis in - self.axes_manager.navigation_axes] + axis.index_in_array for axis in self.axes_manager.navigation_axes + ] unfolded_axis = self.axes_manager.signal_axes[0].index_in_array self._unfold(steady_axes, unfolded_axis) self.metadata._HyperSpy.Folding.signal_unfolded = True - if self.metadata.has_item('Signal.Noise_properties.variance'): + if self.metadata.has_item("Signal.Noise_properties.variance"): variance = self.metadata.Signal.Noise_properties.variance if isinstance(variance, BaseSignal): variance.unfold_signal_space() @@ -3484,17 +4088,19 @@ def fold(self): folding.unfolded = False folding.signal_unfolded = False self._assign_subclass() - if self.metadata.has_item('Signal.Noise_properties.variance'): + if self.metadata.has_item("Signal.Noise_properties.variance"): variance = self.metadata.Signal.Noise_properties.variance if isinstance(variance, BaseSignal): variance.fold() def _make_sure_data_is_contiguous(self): - if self.data.flags['C_CONTIGUOUS'] is False: - _logger.info("{0!r} data is replaced by its optimized copy, see " - "optimize parameter of ``Basesignal.transpose`` " - "for more information.".format(self)) - self.data = np.ascontiguousarray(self.data) + if self.data.flags["C_CONTIGUOUS"] is False: + _logger.info( + "{0!r} data is replaced by its optimized copy, see " + "optimize parameter of ``Basesignal.transpose`` " + "for more information.".format(self) + ) + self.data = np.ascontiguousarray(self.data, like=self.data) def _iterate_signal(self, iterpath=None): """Iterates over the signal data. It is faster than using the signal @@ -3512,15 +4118,12 @@ def _iterate_signal(self, iterpath=None): """ original_index = self.axes_manager.indices - if iterpath is None: - _logger.warning('The default iterpath will change in HyperSpy 2.0.') - with self.axes_manager.switch_iterpath(iterpath): self.axes_manager.indices = tuple( [0 for _ in self.axes_manager.navigation_axes] - ) + ) for _ in self.axes_manager: - yield self() + yield self._get_current_data() # restore original index self.axes_manager.indices = original_index @@ -3534,15 +4137,13 @@ def _cycle_signal(self): """ if self.axes_manager.navigation_size < 2: while True: - yield self() + yield self._get_current_data() return # pragma: no cover self._make_sure_data_is_contiguous() - axes = [axis.index_in_array for - axis in self.axes_manager.signal_axes] + axes = [axis.index_in_array for axis in self.axes_manager.signal_axes] if axes: - unfolded_axis = ( - self.axes_manager.navigation_axes[0].index_in_array) + unfolded_axis = self.axes_manager.navigation_axes[0].index_in_array new_shape = [1] * len(self.data.shape) for axis in axes: new_shape[axis] = self.data.shape[axis] @@ -3560,7 +4161,7 @@ def _cycle_signal(self): Ni = data.shape[unfolded_axis] while True: getitem[unfolded_axis] = i - yield(data[tuple(getitem)]) + yield (data[tuple(getitem)]) i += 1 i = 0 if i == Ni else i @@ -3569,15 +4170,14 @@ def _remove_axis(self, axes): axes = am[axes] if not np.iterable(axes): axes = (axes,) - if am.navigation_dimension + am.signal_dimension > len(axes): + if am.navigation_dimension + am.signal_dimension >= len(axes): old_signal_dimension = am.signal_dimension am.remove(axes) if old_signal_dimension != am.signal_dimension: self._assign_subclass() - else: + if not self.axes_manager._axes and not self.ragged: # Create a "Scalar" axis because the axis is the last one left and # HyperSpy does not # support 0 dimensions - from hyperspy.misc.utils import add_scalar_axis add_scalar_axis(self) def _ma_workaround(self, s, function, axes, ar_axes, out): @@ -3596,21 +4196,31 @@ def _ma_workaround(self, s, function, axes, ar_axes, out): data = self.data.reshape(new_shape).squeeze() if out: - data = np.atleast_1d(function(data, axis=ar_axes[0],)) + data = np.atleast_1d( + function( + data, + axis=ar_axes[0], + ) + ) if data.shape == out.data.shape: out.data[:] = data out.events.data_changed.trigger(obj=out) else: raise ValueError( "The output shape %s does not match the shape of " - "`out` %s" % (data.shape, out.data.shape)) + "`out` %s" % (data.shape, out.data.shape) + ) else: - s.data = function(data, axis=ar_axes[0],) + s.data = function( + data, + axis=ar_axes[0], + ) s._remove_axis([ax.index_in_axes_manager for ax in axes]) return s - def _apply_function_on_data_and_remove_axis(self, function, axes, - out=None, **kwargs): + def _apply_function_on_data_and_remove_axis( + self, function, axes, out=None, **kwargs + ): axes = self.axes_manager[axes] if not np.iterable(axes): axes = (axes,) @@ -3635,27 +4245,42 @@ def _apply_function_on_data_and_remove_axis(self, function, axes, s = out or self._deepcopy_with_new_data(None) if np.ma.is_masked(self.data): - return self._ma_workaround(s=s, function=function, axes=axes, - ar_axes=ar_axes, out=out) + return self._ma_workaround( + s=s, function=function, axes=axes, ar_axes=ar_axes, out=out + ) if out: if np_out: - function(self.data, axis=ar_axes, out=out.data,) + function( + self.data, + axis=ar_axes, + out=out.data, + ) else: - data = np.atleast_1d(function(self.data, axis=ar_axes,)) + data = np.atleast_1d( + function( + self.data, + axis=ar_axes, + ) + ) if data.shape == out.data.shape: out.data[:] = data else: raise ValueError( "The output shape %s does not match the shape of " - "`out` %s" % (data.shape, out.data.shape)) + "`out` %s" % (data.shape, out.data.shape) + ) out.events.data_changed.trigger(obj=out) else: s.data = np.atleast_1d( - function(self.data, axis=ar_axes,)) + function( + self.data, + axis=ar_axes, + ) + ) s._remove_axis([ax.index_in_axes_manager for ax in axes]) return s - def sum(self, axis=None, out=None, rechunk=True): + def sum(self, axis=None, out=None, rechunk=False): """Sum the data over the given axes. Parameters @@ -3666,29 +4291,29 @@ def sum(self, axis=None, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + ~hyperspy.signal.BaseSignal A new Signal containing the sum of the provided Signal along the specified axes. - Note - ---- + Notes + ----- If you intend to calculate the numerical integral of an unbinned signal, - please use the :py:meth:`integrate1D` function instead. To avoid - erroneous misuse of the `sum` function as integral, it raises a warning - when working with an unbinned, non-uniform axis. + please use the :meth:`~.api.signals.BaseSignal.integrate1D` function + instead. To avoid erroneous misuse of the `sum` function as integral, + it raises a warning when working with an unbinned, non-uniform axis. - See also + See Also -------- max, min, mean, std, var, indexmax, indexmin, valuemax, valuemin Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.sum(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.sum(0) + """ @@ -3698,17 +4323,21 @@ def sum(self, axis=None, out=None, rechunk=True): axes = self.axes_manager[axis] if not np.iterable(axes): axes = (axes,) - if any([not ax.is_uniform and not is_binned(self, ax) for ax in axes]): - warnings.warn("You are summing over an unbinned, non-uniform axis. " - "The result can not be used as an approximation of " - "the integral of the signal. For this functionality, " - "use integrate1D instead.") + if any([not ax.is_uniform and not ax.is_binned for ax in axes]): + warnings.warn( + "You are summing over an unbinned, non-uniform axis. " + "The result can not be used as an approximation of " + "the integral of the signal. For this functionality, " + "use integrate1D instead." + ) return self._apply_function_on_data_and_remove_axis( - np.sum, axis, out=out, rechunk=rechunk) + np.sum, axis, out=out, rechunk=rechunk + ) + sum.__doc__ %= (MANY_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def max(self, axis=None, out=None, rechunk=True): + def max(self, axis=None, out=None, rechunk=False): """Returns a signal with the maximum of the signal along at least one axis. @@ -3720,31 +4349,33 @@ def max(self, axis=None, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the maximum of the provided Signal over the specified axes - See also + See Also -------- min, sum, mean, std, var, indexmax, indexmin, valuemax, valuemin Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.max(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.max(0) + """ if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.max, axis, out=out, rechunk=rechunk) + np.max, axis, out=out, rechunk=rechunk + ) + max.__doc__ %= (MANY_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def min(self, axis=None, out=None, rechunk=True): + def min(self, axis=None, out=None, rechunk=False): """Returns a signal with the minimum of the signal along at least one axis. @@ -3756,31 +4387,33 @@ def min(self, axis=None, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the minimum of the provided Signal over the specified axes - See also + See Also -------- max, sum, mean, std, var, indexmax, indexmin, valuemax, valuemin Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.min(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.min(0) + """ if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.min, axis, out=out, rechunk=rechunk) + np.min, axis, out=out, rechunk=rechunk + ) + min.__doc__ %= (MANY_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def mean(self, axis=None, out=None, rechunk=True): + def mean(self, axis=None, out=None, rechunk=False): """Returns a signal with the average of the signal along at least one axis. @@ -3792,31 +4425,33 @@ def mean(self, axis=None, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the mean of the provided Signal over the specified axes - See also + See Also -------- max, min, sum, std, var, indexmax, indexmin, valuemax, valuemin Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.mean(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.mean(0) + """ if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.mean, axis, out=out, rechunk=rechunk) + np.mean, axis, out=out, rechunk=rechunk + ) + mean.__doc__ %= (MANY_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def std(self, axis=None, out=None, rechunk=True): + def std(self, axis=None, out=None, rechunk=False): """Returns a signal with the standard deviation of the signal along at least one axis. @@ -3828,31 +4463,33 @@ def std(self, axis=None, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the standard deviation of the provided Signal over the specified axes - See also + See Also -------- max, min, sum, mean, var, indexmax, indexmin, valuemax, valuemin Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.std(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.std(0) + """ if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.std, axis, out=out, rechunk=rechunk) + np.std, axis, out=out, rechunk=rechunk + ) + std.__doc__ %= (MANY_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def var(self, axis=None, out=None, rechunk=True): + def var(self, axis=None, out=None, rechunk=False): """Returns a signal with the variances of the signal along at least one axis. @@ -3864,93 +4501,107 @@ def var(self, axis=None, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the variance of the provided Signal over the specified axes - See also + See Also -------- max, min, sum, mean, std, indexmax, indexmin, valuemax, valuemin Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.var(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.var(0) + """ if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.var, axis, out=out, rechunk=rechunk) + np.var, axis, out=out, rechunk=rechunk + ) + var.__doc__ %= (MANY_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def nansum(self, axis=None, out=None, rechunk=True): - """%s - """ + def nansum(self, axis=None, out=None, rechunk=False): + """%s""" if axis is None: axis = self.axes_manager.navigation_axes axes = self.axes_manager[axis] if not np.iterable(axes): axes = (axes,) if any([not ax.is_uniform for ax in axes]): - warnings.warn("You are summing over a non-uniform axis. The result " - "can not be used as an approximation of the " - "integral of the signal. For this functionaliy, " - "use integrate1D instead.") + warnings.warn( + "You are summing over a non-uniform axis. The result " + "can not be used as an approximation of the " + "integral of the signal. For this functionaliy, " + "use integrate1D instead." + ) return self._apply_function_on_data_and_remove_axis( - np.nansum, axis, out=out, rechunk=rechunk) - nansum.__doc__ %= (NAN_FUNC.format('sum')) + np.nansum, axis, out=out, rechunk=rechunk + ) - def nanmax(self, axis=None, out=None, rechunk=True): - """%s - """ + nansum.__doc__ %= NAN_FUNC.format("sum") + + def nanmax(self, axis=None, out=None, rechunk=False): + """%s""" if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.nanmax, axis, out=out, rechunk=rechunk) - nanmax.__doc__ %= (NAN_FUNC.format('max')) + np.nanmax, axis, out=out, rechunk=rechunk + ) + + nanmax.__doc__ %= NAN_FUNC.format("max") - def nanmin(self, axis=None, out=None, rechunk=True): + def nanmin(self, axis=None, out=None, rechunk=False): """%s""" if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.nanmin, axis, out=out, rechunk=rechunk) - nanmin.__doc__ %= (NAN_FUNC.format('min')) + np.nanmin, axis, out=out, rechunk=rechunk + ) + + nanmin.__doc__ %= NAN_FUNC.format("min") - def nanmean(self, axis=None, out=None, rechunk=True): - """%s """ + def nanmean(self, axis=None, out=None, rechunk=False): + """%s""" if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.nanmean, axis, out=out, rechunk=rechunk) - nanmean.__doc__ %= (NAN_FUNC.format('mean')) + np.nanmean, axis, out=out, rechunk=rechunk + ) - def nanstd(self, axis=None, out=None, rechunk=True): + nanmean.__doc__ %= NAN_FUNC.format("mean") + + def nanstd(self, axis=None, out=None, rechunk=False): """%s""" if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.nanstd, axis, out=out, rechunk=rechunk) - nanstd.__doc__ %= (NAN_FUNC.format('std')) + np.nanstd, axis, out=out, rechunk=rechunk + ) - def nanvar(self, axis=None, out=None, rechunk=True): + nanstd.__doc__ %= NAN_FUNC.format("std") + + def nanvar(self, axis=None, out=None, rechunk=False): """%s""" if axis is None: axis = self.axes_manager.navigation_axes return self._apply_function_on_data_and_remove_axis( - np.nanvar, axis, out=out, rechunk=rechunk) - nanvar.__doc__ %= (NAN_FUNC.format('var')) + np.nanvar, axis, out=out, rechunk=rechunk + ) - def diff(self, axis, order=1, out=None, rechunk=True): - """Returns a signal with the `n`-th order discrete difference along + nanvar.__doc__ %= NAN_FUNC.format("var") + + def diff(self, axis, order=1, out=None, rechunk=False): + """Returns a signal with the ``n``-th order discrete difference along given axis. `i.e.` it calculates the difference between consecutive - values in the given axis: `out[n] = a[n+1] - a[n]`. See - :py:func:`numpy.diff` for more details. + values in the given axis: ``out[n] = a[n+1] - a[n]``. See + :func:`numpy.diff` for more details. Parameters ---------- @@ -3962,39 +4613,41 @@ def diff(self, axis, order=1, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) or None + ~hyperspy.api.signals.BaseSignal or None Note that the size of the data on the given ``axis`` decreases by the given ``order``. `i.e.` if ``axis`` is ``"x"`` and ``order`` is 2, the `x` dimension is N, ``der``'s `x` dimension is N - 2. - Note - ---- + Notes + ----- If you intend to calculate the numerical derivative, please use the - proper :py:meth:`derivative` function instead. To avoid erroneous - misuse of the `diff` function as derivative, it raises an error when - when working with a non-uniform axis. + proper :meth:`~hyperspy.api.signals.BaseSignal.derivative` function + instead. To avoid erroneous misuse of the ``diff`` function as derivative, + it raises an error when when working with a non-uniform axis. - See also + See Also -------- - derivative, integrate1D, integrate_simpson + hyperspy.api.signals.BaseSignal.derivative, + hyperspy.api.signals.BaseSignal.integrate1D, + hyperspy.api.signals.BaseSignal.integrate_simpson Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.diff(-1).data.shape - (64,64,1023) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.diff(0) + + """ if not self.axes_manager[axis].is_uniform: raise NotImplementedError( - "Performing a numerical difference on a non-uniform axis " - "is not implemented. Consider using `derivative` instead." - ) + "Performing a numerical difference on a non-uniform axis " + "is not implemented. Consider using `derivative` instead." + ) s = out or self._deepcopy_with_new_data(None) - data = np.diff(self.data, n=order, - axis=self.axes_manager[axis].index_in_array) + data = np.diff(self.data, n=order, axis=self.axes_manager[axis].index_in_array) if out is not None: out.data[:] = data else: @@ -4007,6 +4660,7 @@ def diff(self, axis, order=1, out=None, rechunk=True): return s else: out.events.data_changed.trigger(obj=out) + diff.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) def derivative(self, axis, order=1, out=None, **kwargs): @@ -4027,11 +4681,11 @@ def derivative(self, axis, order=1, out=None, **kwargs): The order of the derivative. %s **kwargs : dict - All extra keyword arguments are passed to :py:func:`numpy.gradient` + All extra keyword arguments are passed to :func:`numpy.gradient` Returns ------- - der : :py:class:`~hyperspy.signal.BaseSignal` + :class:`~hyperspy.api.signals.BaseSignal` Note that the size of the data on the given ``axis`` decreases by the given ``order``. `i.e.` if ``axis`` is ``"x"`` and ``order`` is 2, if the `x` dimension is N, then ``der``'s `x` dimension is N - 2. @@ -4041,29 +4695,42 @@ def derivative(self, axis, order=1, out=None, **kwargs): This function uses numpy.gradient to perform the derivative. See its documentation for implementation details. - See also + See Also -------- integrate1D, integrate_simpson """ # rechunk was a valid keyword up to HyperSpy 1.6 if "rechunk" in kwargs: + # We miss the deprecation cycle for 2.0 + warnings.warn( + "The `rechunk` parameter is not used since HyperSpy 1.7 and" + "will be removed in HyperSpy 3.0.", + VisibleDeprecationWarning, + ) del kwargs["rechunk"] n = order der_data = self.data + axis = self.axes_manager[axis] + if isinstance(axis, tuple): + axis = axis[0] while n: der_data = np.gradient( - der_data, self.axes_manager[axis].axis, - axis=self.axes_manager[axis].index_in_array, **kwargs) + der_data, + axis.axis, + axis=axis.index_in_array, + **kwargs, + ) n -= 1 if out: out.data = der_data out.events.data_changed.trigger(obj=out) else: return self._deepcopy_with_new_data(der_data) + derivative.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG) - def integrate_simpson(self, axis, out=None): + def integrate_simpson(self, axis, out=None, rechunk=False): """Calculate the integral of a Signal along an axis using `Simpson's rule `_. @@ -4071,31 +4738,31 @@ def integrate_simpson(self, axis, out=None): ---------- axis %s %s + %s Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the integral of the provided Signal along the specified axis. - See also + See Also -------- - derivative, integrate1D + hyperspy.api.signals.BaseSignal.derivative, + hyperspy.api.signals.BaseSignal.integrate1D Examples -------- - >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.integrate_simpson(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.integrate_simpson(0) + """ axis = self.axes_manager[axis] s = out or self._deepcopy_with_new_data(None) - data = integrate.simps(y=self.data, x=axis.axis, - axis=axis.index_in_array) + data = integrate.simpson(y=self.data, x=axis.axis, axis=axis.index_in_array) if out is not None: out.data[:] = data out.events.data_changed.trigger(obj=out) @@ -4103,7 +4770,8 @@ def integrate_simpson(self, axis, out=None): s.data = data s._remove_axis(axis.index_in_axes_manager) return s - integrate_simpson.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG) + + integrate_simpson.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) def fft(self, shift=False, apodization=False, real_fft_only=False, **kwargs): """Compute the discrete Fourier Transform. @@ -4119,21 +4787,21 @@ def fft(self, shift=False, apodization=False, real_fft_only=False, **kwargs): (default is ``False``). apodization : bool or str Apply an - `apodization window `_ + `apodization window `_ before calculating the FFT in order to suppress streaks. Valid string values are {``'hann'`` or ``'hamming'`` or ``'tukey'``} If ``True`` or ``'hann'``, applies a Hann window. If ``'hamming'`` or ``'tukey'``, applies Hamming or Tukey windows, respectively (default is ``False``). real_fft_only : bool, default False - If ``True`` and data is real-valued, uses :py:func:`numpy.fft.rfftn` - instead of :py:func:`numpy.fft.fftn` + If ``True`` and data is real-valued, uses :func:`numpy.fft.rfftn` + instead of :func:`numpy.fft.fftn` **kwargs : dict - other keyword arguments are described in :py:func:`numpy.fft.fftn` + other keyword arguments are described in :func:`numpy.fft.fftn` Returns ------- - s : :py:class:`~hyperspy._signals.complex_signal.ComplexSignal` + s : :class:`~hyperspy._signals.complex_signal.ComplexSignal` A Signal containing the result of the FFT algorithm Raises @@ -4143,23 +4811,24 @@ def fft(self, shift=False, apodization=False, real_fft_only=False, **kwargs): Examples -------- - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) + >>> import skimage + >>> im = hs.signals.Signal2D(skimage.data.camera()) >>> im.fft() >>> # Use following to plot power spectrum of `im`: >>> im.fft(shift=True, apodization=True).plot(power_spectrum=True) - Note - ---- + Notes + ----- Requires a uniform axis. For further information see the documentation - of :py:func:`numpy.fft.fftn` + of :func:`numpy.fft.fftn` """ if self.axes_manager.signal_dimension == 0: raise AttributeError("Signal dimension must be at least one.") - if apodization == True: - apodization = 'hann' + if apodization is True: + apodization = "hann" if apodization: im_fft = self.apply_apodization(window=apodization, inplace=False) @@ -4168,9 +4837,8 @@ def fft(self, shift=False, apodization=False, real_fft_only=False, **kwargs): ax = self.axes_manager axes = ax.signal_indices_in_array if any([not axs.is_uniform for axs in self.axes_manager[axes]]): - raise NotImplementedError( - "Not implemented for non-uniform axes.") - use_real_fft = real_fft_only and (self.data.dtype.kind != 'c') + raise NotImplementedError("Not implemented for non-uniform axes.") + use_real_fft = real_fft_only and (self.data.dtype.kind != "c") if use_real_fft: fft_f = np.fft.rfftn @@ -4178,30 +4846,30 @@ def fft(self, shift=False, apodization=False, real_fft_only=False, **kwargs): fft_f = np.fft.fftn if shift: - im_fft = self._deepcopy_with_new_data(np.fft.fftshift( - fft_f(im_fft.data, axes=axes, **kwargs), axes=axes)) - else: im_fft = self._deepcopy_with_new_data( - fft_f(self.data, axes=axes, **kwargs)) + np.fft.fftshift(fft_f(im_fft.data, axes=axes, **kwargs), axes=axes) + ) + else: + im_fft = self._deepcopy_with_new_data(fft_f(self.data, axes=axes, **kwargs)) im_fft.change_dtype("complex") - im_fft.metadata.General.title = 'FFT of {}'.format( - im_fft.metadata.General.title) - im_fft.metadata.set_item('Signal.FFT.shifted', shift) - if hasattr(self.metadata.Signal, 'quantity'): - self.metadata.Signal.__delattr__('quantity') + im_fft.metadata.General.title = "FFT of {}".format( + im_fft.metadata.General.title + ) + im_fft.metadata.set_item("Signal.FFT.shifted", shift) + if hasattr(self.metadata.Signal, "quantity"): + self.metadata.Signal.__delattr__("quantity") - ureg = UnitRegistry() for axis in im_fft.axes_manager.signal_axes: - axis.scale = 1. / axis.size / axis.scale + axis.scale = 1.0 / axis.size / axis.scale axis.offset = 0.0 try: - units = ureg.parse_expression(str(axis.units))**(-1) - axis.units = '{:~}'.format(units.units) + units = _ureg.parse_expression(str(axis.units)) ** (-1) + axis.units = "{:~}".format(units.units) except UndefinedUnitError: - _logger.warning('Units are not set or cannot be recognized') + _logger.warning("Units are not set or cannot be recognized") if shift: - axis.offset = -axis.high_value / 2. + axis.offset = -axis.high_value / 2.0 return im_fft def ifft(self, shift=None, return_real=True, **kwargs): @@ -4225,11 +4893,11 @@ def ifft(self, shift=None, return_real=True, **kwargs): If ``True``, returns only the real part of the inverse FFT. If ``False``, returns all parts. **kwargs : dict - other keyword arguments are described in :py:func:`numpy.fft.ifftn` + other keyword arguments are described in :func:`numpy.fft.ifftn` - Return - ------ - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + Returns + ------- + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A Signal containing the result of the inverse FFT algorithm Raises @@ -4239,16 +4907,16 @@ def ifft(self, shift=None, return_real=True, **kwargs): Examples -------- - >>> import scipy - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) + >>> import skimage + >>> im = hs.signals.Signal2D(skimage.data.camera()) >>> imfft = im.fft() >>> imfft.ifft() - Note - ---- + Notes + ----- Requires a uniform axis. For further information see the documentation - of :py:func:`numpy.fft.ifftn` + of :func:`numpy.fft.ifftn` """ if self.axes_manager.signal_dimension == 0: @@ -4256,38 +4924,41 @@ def ifft(self, shift=None, return_real=True, **kwargs): ax = self.axes_manager axes = ax.signal_indices_in_array if any([not axs.is_uniform for axs in self.axes_manager[axes]]): - raise NotImplementedError( - "Not implemented for non-uniform axes.") + raise NotImplementedError("Not implemented for non-uniform axes.") if shift is None: - shift = self.metadata.get_item('Signal.FFT.shifted', False) + shift = self.metadata.get_item("Signal.FFT.shifted", False) if shift: - im_ifft = self._deepcopy_with_new_data(np.fft.ifftn( - np.fft.ifftshift(self.data, axes=axes), axes=axes, **kwargs)) + im_ifft = self._deepcopy_with_new_data( + np.fft.ifftn( + np.fft.ifftshift(self.data, axes=axes), axes=axes, **kwargs + ) + ) else: - im_ifft = self._deepcopy_with_new_data(np.fft.ifftn( - self.data, axes=axes, **kwargs)) + im_ifft = self._deepcopy_with_new_data( + np.fft.ifftn(self.data, axes=axes, **kwargs) + ) - im_ifft.metadata.General.title = 'iFFT of {}'.format( - im_ifft.metadata.General.title) - if im_ifft.metadata.has_item('Signal.FFT'): + im_ifft.metadata.General.title = "iFFT of {}".format( + im_ifft.metadata.General.title + ) + if im_ifft.metadata.has_item("Signal.FFT"): del im_ifft.metadata.Signal.FFT if return_real: im_ifft = im_ifft.real - ureg = UnitRegistry() for axis in im_ifft.axes_manager.signal_axes: - axis.scale = 1. / axis.size / axis.scale + axis.scale = 1.0 / axis.size / axis.scale try: - units = ureg.parse_expression(str(axis.units)) ** (-1) - axis.units = '{:~}'.format(units.units) + units = _ureg.parse_expression(str(axis.units)) ** (-1) + axis.units = "{:~}".format(units.units) except UndefinedUnitError: - _logger.warning('Units are not set or cannot be recognized') - axis.offset = 0. + _logger.warning("Units are not set or cannot be recognized") + axis.offset = 0.0 return im_ifft - def integrate1D(self, axis, out=None): + def integrate1D(self, axis, out=None, rechunk=False): """Integrate the signal over the given axis. The integration is performed using @@ -4300,37 +4971,38 @@ def integrate1D(self, axis, out=None): ---------- axis %s %s + %s Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the integral of the provided Signal along the specified axis. - See also + See Also -------- integrate_simpson, derivative Examples -------- - >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.integrate1D(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.integrate1D(0) + """ - if is_binned(self, axis=axis): - # in v2 replace by - # self.axes_manager[axis].is_binned - return self.sum(axis=axis, out=out) + axis = self.axes_manager[axis] + if isinstance(axis, tuple): + axis = axis[0] + if axis.is_binned: + return self.sum(axis=axis, out=out, rechunk=rechunk) else: - return self.integrate_simpson(axis=axis, out=out) + return self.integrate_simpson(axis=axis, out=out, rechunk=rechunk) - integrate1D.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG) + integrate1D.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def indexmin(self, axis, out=None, rechunk=True): + def indexmin(self, axis, out=None, rechunk=False): """Returns a signal with the index of the minimum along an axis. Parameters @@ -4341,29 +5013,30 @@ def indexmin(self, axis, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the indices of the minimum along the specified axis. Note: the data `dtype` is always ``int``. - See also + See Also -------- max, min, sum, mean, std, var, indexmax, valuemax, valuemin Examples -------- - >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.indexmin(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.indexmin(0) + """ return self._apply_function_on_data_and_remove_axis( - np.argmin, axis, out=out, rechunk=rechunk) + np.argmin, axis, out=out, rechunk=rechunk + ) + indexmin.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def indexmax(self, axis, out=None, rechunk=True): + def indexmax(self, axis, out=None, rechunk=False): """Returns a signal with the index of the maximum along an axis. Parameters @@ -4374,29 +5047,30 @@ def indexmax(self, axis, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the indices of the maximum along the specified axis. Note: the data `dtype` is always ``int``. - See also + See Also -------- max, min, sum, mean, std, var, indexmin, valuemax, valuemin Examples -------- - >>> import numpy as np >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.indexmax(-1).data.shape - (64,64) + >>> s + + >>> s.indexmax(0) + """ return self._apply_function_on_data_and_remove_axis( - np.argmax, axis, out=out, rechunk=rechunk) + np.argmax, axis, out=out, rechunk=rechunk + ) + indexmax.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def valuemax(self, axis, out=None, rechunk=True): + def valuemax(self, axis, out=None, rechunk=False): """Returns a signal with the value of coordinates of the maximum along an axis. @@ -4408,22 +5082,26 @@ def valuemax(self, axis, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + s : :class:`~hyperspy.signal.BaseSignal` (or subclass) A new Signal containing the calibrated coordinate values of the maximum along the specified axis. - See also + See Also -------- - max, min, sum, mean, std, var, indexmax, indexmin, valuemin + hyperspy.api.signals.BaseSignal.max, hyperspy.api.signals.BaseSignal.min, + hyperspy.api.signals.BaseSignal.sum, hyperspy.api.signals.BaseSignal.mean, + hyperspy.api.signals.BaseSignal.std, hyperspy.api.signals.BaseSignal.var, + hyperspy.api.signals.BaseSignal.indexmax, hyperspy.api.signals.BaseSignal.indexmin, + hyperspy.api.signals.BaseSignal.valuemin Examples -------- >>> import numpy as np - >>> s = BaseSignal(np.random.random((64,64,1024))) - >>> s.data.shape - (64,64,1024) - >>> s.valuemax(-1).data.shape - (64,64) + >>> s = BaseSignal(np.random.random((64, 64, 1024))) + >>> s + + >>> s.valuemax(0) + """ idx = self.indexmax(axis) @@ -4434,9 +5112,10 @@ def valuemax(self, axis, out=None, rechunk=True): else: out.data[:] = data out.events.data_changed.trigger(obj=out) + valuemax.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def valuemin(self, axis, out=None, rechunk=True): + def valuemin(self, axis, out=None, rechunk=False): """Returns a signal with the value of coordinates of the minimum along an axis. @@ -4448,13 +5127,17 @@ def valuemin(self, axis, out=None, rechunk=True): Returns ------- - s : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + :class:`~hyperspy.api.signals.BaseSignal` or subclass A new Signal containing the calibrated coordinate values of the minimum along the specified axis. - See also + See Also -------- - max, min, sum, mean, std, var, indexmax, indexmin, valuemax + hyperspy.api.signals.BaseSignal.max, hyperspy.api.signals.BaseSignal.min, + hyperspy.api.signals.BaseSignal.sum, hyperspy.api.signals.BaseSignal.mean, + hyperspy.api.signals.BaseSignal.std, hyperspy.api.signals.BaseSignal.var, + hyperspy.api.signals.BaseSignal.indexmax, hyperspy.api.signals.BaseSignal.indexmin, + hyperspy.api.signals.BaseSignal.valuemax """ idx = self.indexmin(axis) @@ -4465,16 +5148,18 @@ def valuemin(self, axis, out=None, rechunk=True): else: out.data[:] = data out.events.data_changed.trigger(obj=out) + valuemin.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, RECHUNK_ARG) - def get_histogram(self, bins='fd', range_bins=None, max_num_bins=250, out=None, - **kwargs): + def get_histogram( + self, bins="fd", range_bins=None, max_num_bins=250, out=None, **kwargs + ): """Return a histogram of the signal data. More sophisticated algorithms for determining the bins can be used by passing a string as the ``bins`` argument. Other than the ``'blocks'`` and ``'knuth'`` methods, the available algorithms are the same as - :py:func:`numpy.histogram`. + :func:`numpy.histogram`. Note: The lazy version of the algorithm only supports ``"scott"`` and ``"fd"`` as a string argument for ``bins``. @@ -4482,26 +5167,28 @@ def get_histogram(self, bins='fd', range_bins=None, max_num_bins=250, out=None, Parameters ---------- %s - range_bins : tuple or None, optional - the minimum and maximum range for the histogram. If - `range_bins` is ``None``, (``x.min()``, ``x.max()``) will be used. + %s %s %s %s **kwargs other keyword arguments (weight and density) are described in - :py:func:`numpy.histogram`. + :func:`numpy.histogram`. Returns ------- - hist_spec : :py:class:`~hyperspy._signals.signal1d.Signal1D` + hist_spec : :class:`~.api.signals.Signal1D` A 1D spectrum instance containing the histogram. - See also + Notes + ----- + See :func:`numpy.histogram` for more details on the + meaning of the returned values. + + See Also -------- - * print_summary_statistics - * :py:func:`numpy.histogram` - * :py:func:`dask.histogram` + hyperspy.api.signals.BaseSignal.print_summary_statistics, + numpy.histogram, dask.array.histogram Examples -------- @@ -4513,14 +5200,11 @@ def get_histogram(self, bins='fd', range_bins=None, max_num_bins=250, out=None, """ from hyperspy import signals + data = self.data[~np.isnan(self.data)].flatten() hist, bin_edges = histogram( - data, - bins=bins, - max_num_bins=max_num_bins, - range=range_bins, - **kwargs + data, bins=bins, max_num_bins=max_num_bins, range=range_bins, **kwargs ) if out is None: @@ -4532,7 +5216,7 @@ def get_histogram(self, bins='fd', range_bins=None, max_num_bins=250, out=None, else: hist_spec.data = hist - if isinstance(bins, str) and bins == 'blocks': + if isinstance(bins, str) and bins == "blocks": hist_spec.axes_manager.signal_axes[0].axis = bin_edges[:-1] warnings.warn( "The option `bins='blocks'` is not fully supported in this " @@ -4545,28 +5229,34 @@ def get_histogram(self, bins='fd', range_bins=None, max_num_bins=250, out=None, hist_spec.axes_manager[0].offset = bin_edges[0] hist_spec.axes_manager[0].size = hist.shape[-1] - hist_spec.axes_manager[0].name = 'value' - hist_spec.axes_manager[0].is_binned = True - hist_spec.metadata.General.title = (self.metadata.General.title + - " histogram") + _set_histogram_metadata(self, hist_spec, **kwargs) + if out is None: return hist_spec else: out.events.data_changed.trigger(obj=out) - get_histogram.__doc__ %= (HISTOGRAM_BIN_ARGS, HISTOGRAM_MAX_BIN_ARGS, OUT_ARG, RECHUNK_ARG) + get_histogram.__doc__ %= ( + HISTOGRAM_BIN_ARGS, + HISTOGRAM_RANGE_ARGS, + HISTOGRAM_MAX_BIN_ARGS, + OUT_ARG, + RECHUNK_ARG, + ) def map( self, function, show_progressbar=None, - parallel=None, - max_workers=None, + num_workers=None, inplace=True, ragged=None, + navigation_chunks=None, output_signal_size=None, output_dtype=None, - **kwargs + lazy_output=None, + silence_warnings=False, + **kwargs, ): """Apply a function to the signal data at all the navigation coordinates. @@ -4587,19 +5277,49 @@ def map( ---------- function : :std:term:`function` - Any function that can be applied to the signal. - %s + Any function that can be applied to the signal. This function should + not alter any mutable input arguments or input data. So do not do + operations which alter the input, without copying it first. + For example, instead of doing `image *= mask`, rather do + `image = image * mask`. Likewise, do not do `image[5, 5] = 10` + directly on the input data or arguments, but make a copy of it + first. For example via `image = copy.deepcopy(image)`. %s %s inplace : bool, default True - if ``True``, the data is replaced by the result. Otherwise + If ``True``, the data is replaced by the result. Otherwise a new Signal with the results is returned. ragged : None or bool, default None Indicates if the results for each navigation pixel are of identical shape (and/or numpy arrays to begin with). If ``None``, - the appropriate choice is made while processing. If True in case - of lazy signal, the signal will be compute at the end of the - mapping. Note: ``None`` is not allowed for Lazy signals! + the output signal will be ragged only if the original signal is ragged. + navigation_chunks : str, None, int or tuple of int, default ``None`` + Set the navigation_chunks argument to a tuple of integers to split + the navigation axes into chunks. This can be useful to enable + using multiple cores with signals which are less that 100 MB. + This argument is passed to :meth:`~._signals.lazy.LazySignal.rechunk`. + output_signal_size : None, tuple + Since the size and dtype of the signal dimension of the output + signal can be different from the input signal, this output signal + size must be calculated somehow. If both ``output_signal_size`` + and ``output_dtype`` is ``None``, this is automatically determined. + However, if for some reason this is not working correctly, this + can be specified via ``output_signal_size`` and ``output_dtype``. + The most common reason for this failing is due to the signal size + being different for different navigation positions. If this is the + case, use ragged=True. None is default. + output_dtype : None, numpy.dtype + See docstring for output_signal_size for more information. + Default None. + %s + silence_warnings : bool, str or tuple, list of str + If ``True``, don't warn when one of the signal axes are non-uniform, + or the scales of the signal axes differ. If ``"non-uniform"``, + ``"scales"`` or ``"units"`` are in the list the warning will be + silenced when using non-uniform axis, different scales or different + units of the signal axes, respectively. + If ``False``, all warnings will be added to the logger. + Default is ``False``. **kwargs : dict All extra keyword arguments are passed to the provided function @@ -4611,11 +5331,13 @@ def map( such, most functions are not able to operate on the result and the data should be used directly. - This method is similar to Python's :py:func:`python:map` that can - also be utilized with a :py:class:`~hyperspy.signal.BaseSignal` + This method is similar to Python's :func:`python:map` that can + also be utilized with a :class:`~hyperspy.signal.BaseSignal` instance for similar purposes. However, this method has the advantage of being faster because it iterates the underlying numpy data array - instead of the :py:class:`~hyperspy.signal.BaseSignal`. + instead of the :class:`~hyperspy.signal.BaseSignal`. + + Currently requires a uniform axis. Examples -------- @@ -4630,36 +5352,99 @@ def map( parameter is variable: >>> im = hs.signals.Signal2D(np.random.random((10, 64, 64))) - >>> sigmas = hs.signals.BaseSignal(np.linspace(2,5,10)).T + >>> sigmas = hs.signals.BaseSignal(np.linspace(2, 5, 10)).T >>> im.map(scipy.ndimage.gaussian_filter, sigma=sigmas) - Note - ---- - Currently requires a uniform axis. + Rotate the two signal dimensions, with different amount as a function + of navigation index. Delay the calculation by getting the output + lazily. The calculation is then done using the compute method. + + >>> from scipy.ndimage import rotate + >>> s = hs.signals.Signal2D(np.random.random((5, 4, 40, 40))) + >>> s_angle = hs.signals.BaseSignal(np.linspace(0, 90, 20).reshape(5, 4)).T + >>> s.map(rotate, angle=s_angle, reshape=False, lazy_output=True) + >>> s.compute() + + Rotate the two signal dimensions, with different amount as a function + of navigation index. In addition, the output is returned as a new + signal, instead of replacing the old signal. + + >>> s = hs.signals.Signal2D(np.random.random((5, 4, 40, 40))) + >>> s_angle = hs.signals.BaseSignal(np.linspace(0, 90, 20).reshape(5, 4)).T + >>> s_rot = s.map(rotate, angle=s_angle, reshape=False, inplace=False) + + If you want some more control over computing a signal that isn't lazy + you can always set lazy_output to True and then compute the signal setting + the scheduler to 'threading', 'processes', 'single-threaded' or 'distributed'. + + Additionally, you can set the navigation_chunks argument to a tuple of + integers to split the navigation axes into chunks. This can be useful if your + signal is less that 100 mb but you still want to use multiple cores. + + >>> s = hs.signals.Signal2D(np.random.random((5, 4, 40, 40))) + >>> s_angle = hs.signals.BaseSignal(np.linspace(0, 90, 20).reshape(5, 4)).T + >>> s.map( + ... rotate, angle=s_angle, reshape=False, lazy_output=True, + ... inplace=True, navigation_chunks=(2,2) + ... ) + >>> s.compute() """ - if self.axes_manager.navigation_shape == () and self._lazy: - _logger.info("Converting signal to a non-lazy signal because there are no nav dimensions") - self.compute() + if lazy_output is None: + lazy_output = self._lazy + if ragged is None: + ragged = self.ragged + if isinstance(silence_warnings, str): + silence_warnings = (silence_warnings,) + + if isinstance(silence_warnings, (list, tuple)): + for key in silence_warnings: + if isinstance(key, str): + if key not in ["non-uniform", "units", "scales"]: + raise ValueError( + f"'{key}' is not a valid string for the " + "`silence_warnings` parameter." + ) + else: + raise TypeError( + "`silence_warnings` must be a boolean or a list, tuple of string." + ) + + def _silence_warnings(silence_warnings, key): + # Return True when it should silence warning, + # otherwise False + # Warn only when `silence_warnings` is False + # or when the key is not in `silence_warnings` + return silence_warnings is True or ( + isinstance(silence_warnings, (tuple, list)) and key in silence_warnings + ) - # Sepate ndkwargs depending on if they are BaseSignals. + # Separate arguments to pass to the mapping function: + # ndkwargs dictionary contains iterating arguments which must be signals. + # kwargs dictionary contains non-iterating arguments + self_nav_shape = self.axes_manager.navigation_shape ndkwargs = {} ndkeys = [key for key in kwargs if isinstance(kwargs[key], BaseSignal)] for key in ndkeys: - if kwargs[key].axes_manager.navigation_shape == self.axes_manager.navigation_shape: + nd_nav_shape = kwargs[key].axes_manager.navigation_shape + if nd_nav_shape == self_nav_shape: ndkwargs[key] = kwargs.pop(key) - elif kwargs[key].axes_manager.navigation_shape == () or kwargs[key].axes_manager.navigation_shape == (1,): - kwargs[key] = np.squeeze(kwargs[key].data) # this really isn't an iterating signal. + elif nd_nav_shape == () or nd_nav_shape == (1,): + # This really isn't an iterating signal. + kwargs[key] = np.squeeze(kwargs[key].data) else: - raise ValueError(f'The size of the navigation_shape for the kwarg {key} ' - f'(<{kwargs[key].axes_manager.navigation_shape}> must be consistent' - f'with the size of the mapped signal ' - f'<{self.axes_manager.navigation_shape}>') - # TODO: Consider support for non-uniform signal axis + raise ValueError( + f"The size of the navigation_shape for the kwarg {key} " + f"(<{nd_nav_shape}> must be consistent " + f"with the size of the mapped signal " + f"<{self_nav_shape}>" + ) if any([not ax.is_uniform for ax in self.axes_manager.signal_axes]): - _logger.warning( - "At least one axis of the signal is non-uniform. Can your " - "`function` operate on non-uniform axes?") + if not _silence_warnings(silence_warnings, "non-uniform"): + _logger.warning( + "At least one axis of the signal is non-uniform. Can your " + "`function` operate on non-uniform axes?", + ) else: # Check if the signal axes have inhomogeneous scales and/or units and # display in warning if yes. @@ -4668,11 +5453,16 @@ def map( for i in range(len(self.axes_manager.signal_axes)): scale.add(self.axes_manager.signal_axes[i].scale) units.add(self.axes_manager.signal_axes[i].units) - if len(units) != 1 or len(scale) != 1: + if len(scale) != 1 and not _silence_warnings(silence_warnings, "scales"): _logger.warning( - "The function you applied does not take into " - "account the difference of units and of scales in-between" - " axes.") + "The function you applied does not take into account " + "the difference of scales in-between axes." + ) + if len(units) != 1 and not _silence_warnings(silence_warnings, "units"): + _logger.warning( + "The function you applied does not take into account " + "the difference of units in-between axes." + ) # If the function has an axis argument and the signal dimension is 1, # we suppose that it can operate on the full array and we don't # iterate over the coordinates. @@ -4683,244 +5473,279 @@ def map( if not isinstance(function, np.ufunc): fargs = inspect.signature(function).parameters.keys() else: - _logger.warning(f"The function `{function.__name__}` can " - "direcly operate on hyperspy signals and it " - "is not necessary to use `map`.") + _logger.warning( + f"The function `{function.__name__}` can directly operate " + "on hyperspy signals and it is not necessary to use `map`." + ) except TypeError as error: # This is probably a Cython function that is not supported by # inspect. _logger.warning(error) - if not ndkwargs and (self.axes_manager.signal_dimension == 1 and - "axis" in fargs): - kwargs['axis'] = self.axes_manager.signal_axes[-1].index_in_array - - res = self._map_all(function, inplace=inplace, **kwargs) - # If the function has an axes argument + # If the function has an `axes` or `axis` argument # we suppose that it can operate on the full array and we don't # iterate over the coordinates. - elif not ndkwargs and "axes" in fargs and not parallel: - kwargs['axes'] = tuple([axis.index_in_array for axis in - self.axes_manager.signal_axes]) - res = self._map_all(function, inplace=inplace, **kwargs) + # We use _map_all only when the user doesn't specify axis/axes + if ( + not ndkwargs + and not lazy_output + and self.axes_manager.signal_dimension == 1 + and "axis" in fargs + and "axis" not in kwargs.keys() + ): + kwargs["axis"] = self.axes_manager.signal_axes[-1].index_in_array + result = self._map_all(function, inplace=inplace, **kwargs) + elif ( + not ndkwargs + and not lazy_output + and "axes" in fargs + and "axes" not in kwargs.keys() + ): + kwargs["axes"] = tuple( + [axis.index_in_array for axis in self.axes_manager.signal_axes] + ) + result = self._map_all(function, inplace=inplace, **kwargs) else: - if self._lazy: - kwargs["output_signal_size"] = output_signal_size - kwargs["output_dtype"] = output_dtype + if show_progressbar is None: + from hyperspy.defaults_parser import preferences + + show_progressbar = preferences.General.show_progressbar # Iteration over coordinates. - res = self._map_iterate(function, iterating_kwargs=ndkwargs, - show_progressbar=show_progressbar, - parallel=parallel, - max_workers=max_workers, - ragged=ragged, - inplace=inplace, - **kwargs) - if inplace: + result = self._map_iterate( + function, + iterating_kwargs=ndkwargs, # function argument(s) (iterating) + show_progressbar=show_progressbar, + ragged=ragged, + inplace=inplace, + lazy_output=lazy_output, + num_workers=num_workers, + output_dtype=output_dtype, + output_signal_size=output_signal_size, + navigation_chunks=navigation_chunks, + **kwargs, # function argument(s) (non-iterating) + ) + if not inplace: + return result + else: self.events.data_changed.trigger(obj=self) - return res - map.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) + map.__doc__ %= (SHOW_PROGRESSBAR_ARG, LAZY_OUTPUT_ARG, NUM_WORKERS_ARG) def _map_all(self, function, inplace=True, **kwargs): - """The function has to have either 'axis' or 'axes' keyword argument, - and hence support operating on the full dataset efficiently. - - Replaced for lazy signals""" + """ + The function has to have either 'axis' or 'axes' keyword argument, + and hence support operating on the full dataset efficiently and remove + the signal axes. + """ + old_shape = self.data.shape newdata = function(self.data, **kwargs) if inplace: self.data = newdata + if self.data.shape != old_shape: + self.axes_manager.remove(self.axes_manager.signal_axes) + self._lazy = False + self._assign_subclass() return None - return self._deepcopy_with_new_data(newdata) + else: + sig = self._deepcopy_with_new_data(newdata) + if sig.data.shape != old_shape: + sig.axes_manager.remove(sig.axes_manager.signal_axes) + sig._lazy = False + sig._assign_subclass() + return sig def _map_iterate( self, function, - iterating_kwargs=(), + iterating_kwargs=None, show_progressbar=None, - parallel=None, - max_workers=None, - ragged=None, + ragged=False, inplace=True, + output_signal_size=None, + output_dtype=None, + lazy_output=None, + num_workers=None, + navigation_chunks="auto", **kwargs, ): - """Iterates the signal navigation space applying the function. - - Parameters - ---------- - function : :std:term:`function` - the function to apply - iterating_kwargs : tuple (of tuples) - A tuple with structure (('key1', value1), ('key2', value2), ..) - where the key-value pairs will be passed as kwargs for the - function to be mapped, and the values will be iterated together - with the signal navigation. The value needs to be a signal - instance because passing array can be ambigous and will be removed - in HyperSpy 2.0. - %s - %s - %s - inplace : bool, default True - if ``True``, the data is replaced by the result. Otherwise - a new signal with the results is returned. - ragged : None or bool, default None - Indicates if results for each navigation pixel are of identical - shape (and/or numpy arrays to begin with). If ``None``, - an appropriate choice is made while processing. Note: ``None`` is - not allowed for Lazy signals! - **kwargs : dict - Additional keyword arguments passed to :std:term:`function` - - Notes - ----- - This method is replaced for lazy signals. - - Examples - -------- - - Pass a larger array of different shape - - >>> s = hs.signals.Signal1D(np.arange(20.).reshape((20,1))) - >>> def func(data, value=0): - ... return data + value - >>> # pay attention that it's a tuple of tuples - need commas - >>> s._map_iterate(func, - ... iterating_kwargs=(('value', - ... np.random.rand(5,400).flat),)) - >>> s.data.T - array([[ 0.82869603, 1.04961735, 2.21513949, 3.61329091, - 4.2481755 , 5.81184375, 6.47696867, 7.07682618, - 8.16850697, 9.37771809, 10.42794054, 11.24362699, - 12.11434077, 13.98654036, 14.72864184, 15.30855499, - 16.96854373, 17.65077064, 18.64925703, 19.16901297]]) - - Storing function result to other signal (e.g. calculated shifts) - - >>> s = hs.signals.Signal1D(np.arange(20.).reshape((5,4))) - >>> def func(data): # the original function - ... return data.sum() - >>> result = s._get_navigation_signal().T - >>> def wrapped(*args, data=None): - ... return func(data) - >>> result._map_iterate(wrapped, - ... iterating_kwargs=(('data', s),)) - >>> result.data - array([ 6., 22., 38., 54., 70.]) - - """ - from os import cpu_count - from hyperspy.misc.utils import create_map_objects, map_result_construction - - if show_progressbar is None: - show_progressbar = preferences.General.show_progressbar + if lazy_output is None: + lazy_output = self._lazy - if parallel is None: - parallel = preferences.General.parallel + if not self._lazy: + s_input = self.as_lazy() + s_input.rechunk(nav_chunks=navigation_chunks) + else: + s_input = self - if isinstance(iterating_kwargs, (tuple, list)): + # unpacking keyword arguments + if iterating_kwargs is None: + iterating_kwargs = {} + elif isinstance(iterating_kwargs, (tuple, list)): iterating_kwargs = dict((k, v) for k, v in iterating_kwargs) - size = max(1, self.axes_manager.navigation_size) - func, iterators = create_map_objects(function, size, iterating_kwargs, **kwargs) - iterators = (self._iterate_signal(),) + iterators - res_shape = self.axes_manager._navigation_shape_in_array + nav_indexes = s_input.axes_manager.navigation_indices_in_array + chunk_span = np.equal(s_input.data.chunksize, s_input.data.shape) + chunk_span = [ + chunk_span[i] for i in s_input.axes_manager.signal_indices_in_array + ] - # no navigation - if not len(res_shape): - res_shape = (1,) + if not all(chunk_span): + _logger.info( + "The chunk size needs to span the full signal size, rechunking..." + ) - # pre-allocate some space - res_data = np.empty(res_shape, dtype="O") - shapes = set() + old_sig = s_input.rechunk(inplace=False, nav_chunks=None) + else: + old_sig = s_input - if show_progressbar: - pbar = progressbar(total=size, leave=True, disable=not show_progressbar) + os_am = old_sig.axes_manager - # We set this value to equal cpu_count, with a maximum - # of 32 cores, since the earlier default value was inappropriate - # for many-core machines. - if max_workers is None: - max_workers = min(32, cpu_count()) + autodetermine = ( + output_signal_size is None or output_dtype is None + ) # try to guess output dtype and sig size? + if autodetermine and is_cupy_array(self.data): + raise ValueError( + "Autodetermination of `output_signal_size` and " + "`output_dtype` is not supported for cupy array." + ) - # Avoid any overhead of additional threads - if max_workers < 2: - parallel = False + args, arg_keys = old_sig._get_iterating_kwargs(iterating_kwargs) + + if autodetermine: # trying to guess the output d-type and size from one signal + testing_kwargs = {} + for ikey, key in enumerate(arg_keys): + test_ind = (0,) * len(os_am.navigation_axes) + # For discussion on if squeeze is necessary, see + # https://github.com/hyperspy/hyperspy/pull/2981 + testing_kwargs[key] = np.squeeze(args[ikey][test_ind].compute())[()] + testing_kwargs = {**kwargs, **testing_kwargs} + test_data = np.array( + old_sig.inav[(0,) * len(os_am.navigation_shape)].data.compute() + ) + temp_output_signal_size, temp_output_dtype = guess_output_signal_size( + test_data=test_data, + function=function, + ragged=ragged, + **testing_kwargs, + ) + if output_signal_size is None: + output_signal_size = temp_output_signal_size + if output_dtype is None: + output_dtype = temp_output_dtype + output_shape = self.axes_manager._navigation_shape_in_array + output_signal_size + arg_pairs, adjust_chunks, new_axis, output_pattern = _get_block_pattern( + (old_sig.data,) + args, output_shape + ) - # parallel or sequential mapping - if parallel: - from concurrent.futures import ThreadPoolExecutor + axes_changed = len(new_axis) != 0 or len(adjust_chunks) != 0 + + mapped = da.blockwise( + process_function_blockwise, + output_pattern, + *concat(arg_pairs), + adjust_chunks=adjust_chunks, + new_axes=new_axis, + align_arrays=False, + dtype=output_dtype, + concatenate=True, + arg_keys=arg_keys, + function=function, + output_dtype=output_dtype, + nav_indexes=nav_indexes, + output_signal_size=output_signal_size, + **kwargs, + ) - with ThreadPoolExecutor(max_workers=max_workers) as executor: - for ind, res in zip( - range(res_data.size), executor.map(func, zip(*iterators)) - ): - res = np.asarray(res) - res_data.flat[ind] = res - - if ragged is False: - shapes.add(res.shape) - if len(shapes) != 1: - raise ValueError( - "The result shapes are not identical, but ragged=False" - ) - else: - try: - shapes.add(res.shape) - except AttributeError: - shapes.add(None) + data_stored = False - if show_progressbar: - pbar.update(1) + cm = ProgressBar if show_progressbar else dummy_context_manager + if inplace: + if ( + not self._lazy + and not lazy_output + and (mapped.shape == self.data.shape) + and (mapped.dtype == self.data.dtype) + ): + # da.store is used to avoid unnecessary amount of memory usage. + # By using it here, the contents in mapped is written directly to + # the existing NumPy array, avoiding a potential doubling of memory use. + with cm(): + da.store( + mapped, + self.data, + dtype=mapped.dtype, + compute=True, + num_workers=num_workers, + lock=False, + ) + data_stored = True + else: + self.data = mapped + sig = self else: - from builtins import map + sig = s_input._deepcopy_with_new_data(mapped) - for ind, res in zip(range(res_data.size), map(func, zip(*iterators))): - res = np.asarray(res) - res_data.flat[ind] = res + am = sig.axes_manager + sig._lazy = lazy_output - if ragged is False: - shapes.add(res.shape) - if len(shapes) != 1: - raise ValueError( - "The result shapes are not identical, but ragged=False" - ) - else: - try: - shapes.add(res.shape) - except AttributeError: - shapes.add(None) - - if show_progressbar: - pbar.update(1) - - # Combine data if required - shapes = list(shapes) - suitable_shapes = len(shapes) == 1 and shapes[0] is not None - ragged = ragged or not suitable_shapes - sig_shape = None + if ragged: + axes_dicts = self.axes_manager._get_navigation_axes_dicts() + sig.axes_manager.__init__(axes_dicts) + sig.axes_manager._ragged = True + elif axes_changed: + am.remove(am.signal_axes[len(output_signal_size) :]) + for ind in range(len(output_signal_size) - am.signal_dimension, 0, -1): + am._append_axis(size=output_signal_size[-ind], navigate=False) if not ragged: - sig_shape = () if shapes[0] == (1,) else shapes[0] - res_data = np.stack(res_data.ravel()).reshape( - self.axes_manager._navigation_shape_in_array + sig_shape - ) - - res = map_result_construction(self, inplace, res_data, ragged, sig_shape) - - return res - - _map_iterate.__doc__ %= (SHOW_PROGRESSBAR_ARG, PARALLEL_ARG, MAX_WORKERS_ARG) + sig.axes_manager._ragged = False + if output_signal_size == () and am.navigation_dimension == 0: + add_scalar_axis(sig) + sig.get_dimensions_from_data() + sig._assign_subclass() + + if not lazy_output and not data_stored: + with cm(): + sig.data = sig.data.compute(num_workers=num_workers) + + return sig + + def _get_iterating_kwargs(self, iterating_kwargs): + nav_chunks = self.get_chunk_size(self.axes_manager.navigation_axes) + args, arg_keys = (), () + for key in iterating_kwargs: + if iterating_kwargs[key]._lazy: + axes = iterating_kwargs[key].axes_manager.navigation_axes + if iterating_kwargs[key].get_chunk_size(axes) != nav_chunks: + iterating_kwargs[key].rechunk(nav_chunks=nav_chunks, sig_chunks=-1) + chunk_span = np.equal( + iterating_kwargs[key].data.chunksize, + iterating_kwargs[key].data.shape, + ) + chunk_span = [ + chunk_span[i] + for i in iterating_kwargs[key].axes_manager.signal_indices_in_array + ] + if not all(chunk_span): + iterating_kwargs[key].rechunk(nav_chunks=nav_chunks, sig_chunks=-1) + else: + iterating_kwargs[key] = iterating_kwargs[key].as_lazy() + iterating_kwargs[key].rechunk(nav_chunks=nav_chunks, sig_chunks=-1) + args += (iterating_kwargs[key].data,) + arg_keys += (key,) + return args, arg_keys def copy(self): """ Return a "shallow copy" of this Signal using the - standard library's :py:func:`~copy.copy` function. Note: this will + standard library's :func:`~copy.copy` function. Note: this will return a copy of the signal, but it will not duplicate the underlying data in memory, and both Signals will reference the same data. See Also -------- - :py:meth:`~hyperspy.signal.BaseSignal.deepcopy` + :meth:`~hyperspy.api.signals.BaseSignal.deepcopy` """ try: backup_plot = self._plot @@ -4940,38 +5765,39 @@ def __deepcopy__(self, memo): # copy.deepcopy( # self.models._models.as_dictionary())) - # The Signal subclasses might change the view on init + # The Signal subclass might change the view on init # The following code just copies the original view - for oaxis, caxis in zip(self.axes_manager._axes, - dc.axes_manager._axes): + for oaxis, caxis in zip(self.axes_manager._axes, dc.axes_manager._axes): caxis.navigate = oaxis.navigate - if dc.metadata.has_item('Markers'): + if dc.metadata.has_item("Markers"): temp_marker_dict = dc.metadata.Markers.as_dictionary() - markers_dict = markers_metadata_dict_to_markers( - temp_marker_dict, - dc.axes_manager) + markers_dict = {} + for key in temp_marker_dict: + markers_dict[key] = markers_dict_to_markers( + temp_marker_dict[key], + ) dc.metadata.Markers = markers_dict return dc def deepcopy(self): """ Return a "deep copy" of this Signal using the - standard library's :py:func:`~copy.deepcopy` function. Note: this means + standard library's :func:`~copy.deepcopy` function. Note: this means the underlying data structure will be duplicated in memory. See Also -------- - :py:meth:`~hyperspy.signal.BaseSignal.copy` + :meth:`~hyperspy.api.signals.BaseSignal.copy` """ return copy.deepcopy(self) - def change_dtype(self, dtype, rechunk=True): + def change_dtype(self, dtype, rechunk=False): """Change the data type of a Signal. Parameters ---------- - dtype : str or :py:class:`numpy.dtype` + dtype : str or :class:`numpy.dtype` Typecode string or data-type to which the Signal's data array is cast. In addition to all the standard numpy :ref:`arrays.dtypes`, HyperSpy supports four extra dtypes for RGB images: ``'rgb8'``, @@ -4991,32 +5817,39 @@ def change_dtype(self, dtype, rechunk=True): Examples -------- - >>> s = hs.signals.Signal1D([1,2,3,4,5]) + >>> s = hs.signals.Signal1D([1, 2, 3, 4, 5]) >>> s.data array([1, 2, 3, 4, 5]) >>> s.change_dtype('float') >>> s.data - array([ 1., 2., 3., 4., 5.]) - + array([1., 2., 3., 4., 5.]) """ if not isinstance(dtype, np.dtype): if dtype in rgb_tools.rgb_dtypes: if self.axes_manager.signal_dimension != 1: raise AttributeError( - "Only 1D signals can be converted " - "to RGB images.") + "Only 1D signals can be converted " "to RGB images." + ) if "8" in dtype and self.data.dtype.name != "uint8": raise AttributeError( "Only signals with dtype uint8 can be converted to " - "rgb8 images") + "rgb8 images" + ) elif "16" in dtype and self.data.dtype.name != "uint16": raise AttributeError( "Only signals with dtype uint16 can be converted to " - "rgb16 images") + "rgb16 images" + ) + replot = self._plot is not None and self._plot.is_active + if replot: + # Close the figure to avoid error with events + self._plot.close() self.data = rgb_tools.regular_array2rgbx(self.data) self.axes_manager.remove(-1) - self.axes_manager.set_signal_dimension(2) + self.axes_manager._set_signal_dimension(2) self._assign_subclass() + if replot: + self.plot() return else: dtype = np.dtype(dtype) @@ -5024,29 +5857,37 @@ def change_dtype(self, dtype, rechunk=True): ddtype = self.data.dtype.fields["B"][0] if ddtype != dtype: - raise ValueError( - "It is only possibile to change to %s." % - ddtype) + raise ValueError("It is only possibile to change to %s." % ddtype) + replot = self._plot is not None and self._plot.is_active + if replot: + # Close the figure to avoid error with events + self._plot.close() self.data = rgb_tools.rgbx2regular_array(self.data) self.axes_manager._append_axis( size=self.data.shape[-1], scale=1, offset=0, name="RGB index", - navigate=False,) - self.axes_manager.set_signal_dimension(1) + navigate=False, + ) + self.axes_manager._set_signal_dimension(1) self._assign_subclass() + if replot: + self.plot() return else: self.data = self.data.astype(dtype) self._assign_subclass() - change_dtype.__doc__ %= (RECHUNK_ARG) - def estimate_poissonian_noise_variance(self, - expected_value=None, - gain_factor=None, - gain_offset=None, - correlation_factor=None): + change_dtype.__doc__ %= RECHUNK_ARG + + def estimate_poissonian_noise_variance( + self, + expected_value=None, + gain_factor=None, + gain_offset=None, + correlation_factor=None, + ): r"""Estimate the Poissonian noise variance of the signal. The variance is stored in the @@ -5071,7 +5912,7 @@ def estimate_poissonian_noise_variance(self, Parameters ---------- - expected_value : :py:data:`None` or :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + expected_value : None or :class:`~hyperspy.signal.BaseSignal` (or subclass) If ``None``, the signal data is taken as the expected value. Note that this may be inaccurate where the value of `data` is small. gain_factor : None or float @@ -5097,12 +5938,10 @@ def estimate_poissonian_noise_variance(self, if expected_value is None: expected_value = self dc = expected_value.data if expected_value._lazy else expected_value.data.copy() - if self.metadata.has_item( - "Signal.Noise_properties.Variance_linear_model"): + if self.metadata.has_item("Signal.Noise_properties.Variance_linear_model"): vlm = self.metadata.Signal.Noise_properties.Variance_linear_model else: - self.metadata.add_node( - "Signal.Noise_properties.Variance_linear_model") + self.metadata.add_node("Signal.Noise_properties.Variance_linear_model") vlm = self.metadata.Signal.Noise_properties.Variance_linear_model if gain_factor is None: @@ -5126,19 +5965,20 @@ def estimate_poissonian_noise_variance(self, raise ValueError("`gain_factor` must be positive.") if correlation_factor < 0: raise ValueError("`correlation_factor` must be positive.") - variance = self._estimate_poissonian_noise_variance(dc, gain_factor, - gain_offset, - correlation_factor) + variance = self._estimate_poissonian_noise_variance( + dc, gain_factor, gain_offset, correlation_factor + ) - variance = BaseSignal(variance, attributes={'_lazy': self._lazy}) + variance = BaseSignal(variance, attributes={"_lazy": self._lazy}) variance.axes_manager = self.axes_manager - variance.metadata.General.title = ("Variance of " + self.metadata.General.title) + variance.metadata.General.title = "Variance of " + self.metadata.General.title self.set_noise_variance(variance) @staticmethod - def _estimate_poissonian_noise_variance(dc, gain_factor, gain_offset, - correlation_factor): + def _estimate_poissonian_noise_variance( + dc, gain_factor, gain_offset, correlation_factor + ): variance = (dc * gain_factor + gain_offset) * correlation_factor variance = np.clip(variance, gain_offset * correlation_factor, np.inf) return variance @@ -5150,7 +5990,7 @@ def set_noise_variance(self, variance): Parameters ---------- - variance : None or float or :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + variance : None or float or :class:`~hyperspy.signal.BaseSignal` (or subclass) Value or values of the noise variance. A value of None is equivalent to clearing the variance. @@ -5191,7 +6031,7 @@ def get_noise_variance(self): Returns ------- - variance : None or float or :py:class:`~hyperspy.signal.BaseSignal` (or subclasses) + variance : None or float or :class:`~hyperspy.signal.BaseSignal` (or subclass) Noise variance of the signal, if set. Otherwise returns None. @@ -5201,9 +6041,9 @@ def get_noise_variance(self): return None - def get_current_signal(self, auto_title=True, auto_filename=True): + def get_current_signal(self, auto_title=True, auto_filename=True, as_numpy=False): """Returns the data at the current coordinates as a - :py:class:`~hyperspy.signal.BaseSignal` subclass. + :class:`~hyperspy.signal.BaseSignal` subclass. The signal subclass is the same as that of the current object. All the axes navigation attributes are set to ``False``. @@ -5212,66 +6052,77 @@ def get_current_signal(self, auto_title=True, auto_filename=True): ---------- auto_title : bool If ``True``, the current indices (in parentheses) are appended to - the title, separated by a space. + the title, separated by a space, otherwise the title of the signal + is used unchanged. auto_filename : bool If ``True`` and `tmp_parameters.filename` is defined (which is always the case when the Signal has been read from a file), the filename stored in the metadata is modified by appending an underscore and the current indices in parentheses. + as_numpy : bool or None + Only with cupy array. If ``True``, return the current signal + as numpy array, otherwise return as cupy array. Returns ------- - cs : :py:class:`~hyperspy.signal.BaseSignal` (or subclass) + cs : :class:`~hyperspy.signal.BaseSignal` (or subclass) The data at the current coordinates as a Signal Examples -------- - >>> im = hs.signals.Signal2D(np.zeros((2,3, 32,32))) + >>> im = hs.signals.Signal2D(np.zeros((2, 3, 32, 32))) >>> im - - >>> im.axes_manager.indices = 2,1 + + >>> im.axes_manager.indices = (2, 1) >>> im.get_current_signal() - + """ - metadata = self.metadata.deepcopy() # Check if marker update - if metadata.has_item('Markers'): - marker_name_list = metadata.Markers.keys() - markers_dict = metadata.Markers.__dict__ + if self.metadata.has_item("Markers"): + markers_dict = self.metadata.Markers + marker_name_list = self.metadata.Markers.keys() + new_markers_dict = metadata.Markers for marker_name in marker_name_list: - marker = markers_dict[marker_name]['_dtb_value_'] - if marker.auto_update: - marker.axes_manager = self.axes_manager - key_dict = {} - for key in marker.data.dtype.names: - key_dict[key] = marker.get_data_position(key) - marker.set_data(**key_dict) - - cs = self.__class__( - self(), + marker = markers_dict[marker_name] + new_marker = new_markers_dict[marker_name] + kwargs = marker.get_current_kwargs(only_variable_length=False) + new_marker.kwargs = kwargs + + class_ = assign_signal_subclass( + dtype=self.data.dtype, + signal_dimension=self.axes_manager.signal_dimension, + signal_type=self._signal_type, + lazy=False, + ) + + cs = class_( + self._get_current_data(as_numpy=as_numpy), axes=self.axes_manager._get_signal_axes_dicts(), metadata=metadata.as_dictionary(), - attributes={'_lazy': False}) + ) - if cs.metadata.has_item('Markers'): + # reinitialize the markers from dictionary + if cs.metadata.has_item("Markers"): temp_marker_dict = cs.metadata.Markers.as_dictionary() - markers_dict = markers_metadata_dict_to_markers( - temp_marker_dict, - cs.axes_manager) + markers_dict = {} + for key, item in temp_marker_dict.items(): + markers_dict[key] = markers_dict_to_markers(item) + markers_dict[key]._signal = cs cs.metadata.Markers = markers_dict - if auto_filename is True and self.tmp_parameters.has_item('filename'): - cs.tmp_parameters.filename = (self.tmp_parameters.filename + - '_' + - str(self.axes_manager.indices)) + if auto_filename is True and self.tmp_parameters.has_item("filename"): + cs.tmp_parameters.filename = ( + self.tmp_parameters.filename + "_" + str(self.axes_manager.indices) + ) cs.tmp_parameters.extension = self.tmp_parameters.extension cs.tmp_parameters.folder = self.tmp_parameters.folder if auto_title is True: - cs.metadata.General.title = (cs.metadata.General.title + - ' ' + str(self.axes_manager.indices)) + cs.metadata.General.title = ( + cs.metadata.General.title + " " + str(self.axes_manager.indices) + ) cs.axes_manager._set_axis_attribute_values("navigate", False) return cs @@ -5280,52 +6131,57 @@ def _get_navigation_signal(self, data=None, dtype=None): Parameters ---------- - data : None or :py:class:`numpy.ndarray`, optional + data : None or :class:`numpy.ndarray`, optional If ``None``, the resulting Signal data is an array of the same `dtype` as the current one filled with zeros. If a numpy array, the array must have the correct dimensions. - dtype : :py:class:`numpy.dtype`, optional + dtype : :class:`numpy.dtype`, optional The desired data-type for the data array when `data` is ``None``, e.g., ``numpy.int8``. The default is the data type of the current signal data. """ - from dask.array import Array if data is not None: - ref_shape = (self.axes_manager._navigation_shape_in_array - if self.axes_manager.navigation_dimension != 0 - else (1,)) + ref_shape = ( + self.axes_manager._navigation_shape_in_array + if self.axes_manager.navigation_dimension != 0 + else (1,) + ) if data.shape != ref_shape: raise ValueError( - ("data.shape %s is not equal to the current navigation " - "shape in array which is %s") % - (str(data.shape), str(ref_shape))) + ( + "data.shape %s is not equal to the current navigation " + "shape in array which is %s" + ) + % (str(data.shape), str(ref_shape)) + ) else: if dtype is None: dtype = self.data.dtype if self.axes_manager.navigation_dimension == 0: - data = np.array([0, ], dtype=dtype) + data = np.array( + [ + 0, + ], + dtype=dtype, + ) else: data = np.zeros( - self.axes_manager._navigation_shape_in_array, - dtype=dtype) + self.axes_manager._navigation_shape_in_array, dtype=dtype + ) if self.axes_manager.navigation_dimension == 0: s = BaseSignal(data) elif self.axes_manager.navigation_dimension == 1: from hyperspy._signals.signal1d import Signal1D - s = Signal1D(data, - axes=self.axes_manager._get_navigation_axes_dicts()) + + s = Signal1D(data, axes=self.axes_manager._get_navigation_axes_dicts()) elif self.axes_manager.navigation_dimension == 2: from hyperspy._signals.signal2d import Signal2D - s = Signal2D(data, - axes=self.axes_manager._get_navigation_axes_dicts()) + + s = Signal2D(data, axes=self.axes_manager._get_navigation_axes_dicts()) else: - s = BaseSignal( - data, - axes=self.axes_manager._get_navigation_axes_dicts()) - s.axes_manager.set_signal_dimension( - self.axes_manager.navigation_dimension) - if isinstance(data, Array): + s = BaseSignal(data, axes=self.axes_manager._get_navigation_axes_dicts()).T + if isinstance(data, da.Array): s = s.as_lazy() return s @@ -5334,42 +6190,46 @@ def _get_signal_signal(self, data=None, dtype=None): Parameters ---------- - data : None or :py:class:`numpy.ndarray`, optional + data : None or :class:`numpy.ndarray`, optional If ``None``, the resulting Signal data is an array of the same `dtype` as the current one filled with zeros. If a numpy array, the array must have the correct dimensions. - dtype : :py:class:`numpy.dtype`, optional + dtype : :class:`numpy.dtype`, optional The desired data-type for the data array when `data` is ``None``, e.g., ``numpy.int8``. The default is the data type of the current signal data. """ - from dask.array import Array if data is not None: - ref_shape = (self.axes_manager._signal_shape_in_array - if self.axes_manager.signal_dimension != 0 - else (1,)) + ref_shape = ( + self.axes_manager._signal_shape_in_array + if self.axes_manager.signal_dimension != 0 + else (1,) + ) if data.shape != ref_shape: raise ValueError( "data.shape %s is not equal to the current signal shape in" - " array which is %s" % (str(data.shape), str(ref_shape))) + " array which is %s" % (str(data.shape), str(ref_shape)) + ) else: if dtype is None: dtype = self.data.dtype if self.axes_manager.signal_dimension == 0: - data = np.array([0, ], dtype=dtype) + data = np.array( + [ + 0, + ], + dtype=dtype, + ) else: - data = np.zeros( - self.axes_manager._signal_shape_in_array, - dtype=dtype) + data = np.zeros(self.axes_manager._signal_shape_in_array, dtype=dtype) if self.axes_manager.signal_dimension == 0: s = BaseSignal(data) s.set_signal_type(self.metadata.Signal.signal_type) else: - s = self.__class__(data, - axes=self.axes_manager._get_signal_axes_dicts()) - if isinstance(data, Array): + s = self.__class__(data, axes=self.axes_manager._get_signal_axes_dicts()) + if isinstance(data, da.Array): s = s.as_lazy() return s @@ -5394,7 +6254,7 @@ def as_signal1D(self, spectral_axis, out=None, optimize=True): array and the data is made contiguous for efficient iteration over spectra. By default, the method ensures the data is stored optimally, hence often making a copy of the data. See - :py:meth:`~hyperspy.signal.BaseSignal.transpose` for a more general + :meth:`~hyperspy.api.signals.BaseSignal.transpose` for a more general method with more options. Parameters @@ -5403,19 +6263,21 @@ def as_signal1D(self, spectral_axis, out=None, optimize=True): %s %s - See also + See Also -------- - as_signal2D, transpose, :py:func:`hyperspy.misc.utils.transpose` + hyperspy.api.signals.BaseSignal.as_signal2D, + hyperspy.api.signals.BaseSignal.transpose, + hyperspy.api.transpose Examples -------- - >>> img = hs.signals.Signal2D(np.ones((3,4,5,6))) + >>> img = hs.signals.Signal2D(np.ones((3, 4, 5, 6))) >>> img - + >>> img.as_signal1D(-1+1j) - + >>> img.as_signal1D(0) - + """ sp = self.transpose(signal_axes=[spectral_axis], optimize=optimize) @@ -5427,12 +6289,15 @@ def as_signal1D(self, spectral_axis, out=None, optimize=True): else: out.data[:] = sp.data out.events.data_changed.trigger(obj=out) - as_signal1D.__doc__ %= (ONE_AXIS_PARAMETER, OUT_ARG, - OPTIMIZE_ARG.replace('False', 'True')) + + as_signal1D.__doc__ %= ( + ONE_AXIS_PARAMETER, + OUT_ARG, + OPTIMIZE_ARG.replace("False", "True"), + ) def as_signal2D(self, image_axes, out=None, optimize=True): - """Convert a signal to image ( - :py:class:`~hyperspy._signals.signal2d.Signal2D`). + """Convert a signal to a :class:`~hyperspy.api.signals.Signal2D`. The chosen image axes are moved to the last indices in the array and the data is made contiguous for efficient @@ -5440,7 +6305,7 @@ def as_signal2D(self, image_axes, out=None, optimize=True): Parameters ---------- - image_axes : tuple (of int, str or :py:class:`~hyperspy.axes.DataAxis`) + image_axes : tuple (of int, str or :class:`~hyperspy.axes.DataAxis`) Select the image axes. Note that the order of the axes matters and it is given in the "natural" i.e. `X`, `Y`, `Z`... order. %s @@ -5451,27 +6316,28 @@ def as_signal2D(self, image_axes, out=None, optimize=True): DataDimensionError When `data.ndim` < 2 - See also + See Also -------- - as_signal1D, transpose, :py:func:`hyperspy.misc.utils.transpose` - + hyperspy.api.signals.BaseSignal.as_signal1D, + hyperspy.api.signals.BaseSignal.transpose, + hyperspy.api.transpose Examples -------- - >>> s = hs.signals.Signal1D(np.ones((2,3,4,5))) + >>> s = hs.signals.Signal1D(np.ones((2, 3, 4, 5))) >>> s - - >>> s.as_signal2D((0,1)) - - - >>> s.to_signal2D((1,2)) - + + >>> s.as_signal2D((0, 1)) + + >>> s.to_signal2D((1, 2)) + """ if self.data.ndim < 2: raise DataDimensionError( - "A Signal dimension must be >= 2 to be converted to a Signal2D") + "A Signal dimension must be >= 2 to be converted to a Signal2D" + ) im = self.transpose(signal_axes=image_axes, optimize=optimize) if out is None: return im @@ -5481,17 +6347,19 @@ def as_signal2D(self, image_axes, out=None, optimize=True): else: out.data[:] = im.data out.events.data_changed.trigger(obj=out) - as_signal2D.__doc__ %= (OUT_ARG, OPTIMIZE_ARG.replace('False', 'True')) + + as_signal2D.__doc__ %= (OUT_ARG, OPTIMIZE_ARG.replace("False", "True")) def _assign_subclass(self): mp = self.metadata - self.__class__ = hyperspy.io.assign_signal_subclass( + self.__class__ = assign_signal_subclass( dtype=self.data.dtype, signal_dimension=self.axes_manager.signal_dimension, signal_type=mp.Signal.signal_type if "Signal.signal_type" in mp else self._signal_type, - lazy=self._lazy) + lazy=self._lazy, + ) if self._alias_signal_types: # In case legacy types exist: mp.Signal.signal_type = self._signal_type # set to default! self.__init__(self.data, full_initialisation=False) @@ -5505,20 +6373,23 @@ def set_signal_type(self, signal_type=""): contains e.g. electron energy-loss spectroscopy data, photoemission spectroscopy data, etc. - When setting `signal_type` to a "known" type, HyperSpy converts the + When setting ``signal_type`` to a "known" type, HyperSpy converts the current signal to the most appropriate - :py:class:`hyperspy.signal.BaseSignal` subclass. Known signal types are + :class:`~.api.signals.BaseSignal` subclass. Known signal types are signal types that have a specialized - :py:class:`hyperspy.signal.BaseSignal` subclass associated, usually + :class:`~.api.signals.BaseSignal` subclass associated, usually providing specific features for the analysis of that type of signal. - HyperSpy ships with a minimal set of known signal types. External - packages can register extra signal types. To print a list of + HyperSpy ships only with generic signal types. External packages + can register domain-specific signal types, usually associated with + specific measurement techniques. To print a list of registered signal types in the current installation, call - :py:meth:`hyperspy.utils.print_known_signal_types`, and see + :func:`~.api.print_known_signal_types`, and see the developer guide for details on how to add new signal_types. - A non-exhaustive list of HyperSpy extensions is also maintained - here: https://github.com/hyperspy/hyperspy-extensions-list. + A non-exhaustive list of HyperSpy extensions is maintained + here: https://github.com/hyperspy/hyperspy-extensions-list. See also the + `HyperSpy Website `_ for an overview of the + ecosystem. Parameters ---------- @@ -5534,44 +6405,46 @@ def set_signal_type(self, signal_type=""): See Also -------- - * :py:meth:`hyperspy.utils.print_known_signal_types` + hyperspy.api.print_known_signal_types Examples -------- - Let's first print all known signal types: + Let's first print all known signal types (assuming the extensions + `eXSpy `_ and `holoSpy + `_ are installed): >>> s = hs.signals.Signal1D([0, 1, 2, 3]) >>> s - >>> hs.print_known_signal_types() + >>> hs.print_known_signal_types() # doctest: +SKIP +--------------------+---------------------+--------------------+----------+ | signal_type | aliases | class name | package | +--------------------+---------------------+--------------------+----------+ - | DielectricFunction | dielectric function | DielectricFunction | hyperspy | - | EDS_SEM | | EDSSEMSpectrum | hyperspy | - | EDS_TEM | | EDSTEMSpectrum | hyperspy | - | EELS | TEM EELS | EELSSpectrum | hyperspy | - | hologram | | HologramImage | hyperspy | - | MySignal | | MySignal | hspy_ext | + | DielectricFunction | dielectric function | DielectricFunction | exspy | + | EDS_SEM | | EDSSEMSpectrum | exspy | + | EDS_TEM | | EDSTEMSpectrum | exspy | + | EELS | TEM EELS | EELSSpectrum | exspy | + | hologram | | HologramImage | holospy | +--------------------+---------------------+--------------------+----------+ - We can set the `signal_type` using the `signal_type`: + We can set the ``signal_type`` using the ``signal_type``: - >>> s.set_signal_type("EELS") - >>> s + >>> s.set_signal_type("EELS") # doctest: +SKIP + >>> s # doctest: +SKIP - >>> s.set_signal_type("EDS_SEM") - >>> s + >>> s.set_signal_type("EDS_SEM") # doctest: +SKIP + >>> s # doctest: +SKIP or any of its aliases: - >>> s.set_signal_type("TEM EELS") - >>> s + >>> s.set_signal_type("TEM EELS") # doctest: +SKIP + >>> s # doctest: +SKIP - To set the `signal_type` to `undefined`, simply call the method without arguments: + To set the ``signal_type`` to "undefined", simply call the method without + arguments: >>> s.set_signal_type() >>> s @@ -5579,11 +6452,7 @@ def set_signal_type(self, signal_type=""): """ if signal_type is None: - warnings.warn( - "`s.set_signal_type(signal_type=None)` is deprecated. " - "Use `s.set_signal_type(signal_type='')` instead.", - VisibleDeprecationWarning - ) + raise TypeError("The `signal_type` argument must be of string type.") self.metadata.Signal.signal_type = signal_type # _assign_subclass takes care of matching aliases with their @@ -5603,7 +6472,7 @@ def set_signal_origin(self, origin): """ self.metadata.Signal.signal_origin = origin - def print_summary_statistics(self, formatter="%.3g", rechunk=True): + def print_summary_statistics(self, formatter="%.3g", rechunk=False): """Prints the five-number summary statistics of the data, the mean, and the standard deviation. @@ -5617,13 +6486,14 @@ def print_summary_statistics(self, formatter="%.3g", rechunk=True): The number formatter to use for the output %s - See also + See Also -------- get_histogram """ _mean, _std, _min, _q1, _q2, _q3, _max = self._calculate_summary_statistics( - rechunk=rechunk) + rechunk=rechunk + ) print(underline("Summary statistics")) print("mean:\t" + formatter % _mean) print("std:\t" + formatter % _std) @@ -5633,7 +6503,8 @@ def print_summary_statistics(self, formatter="%.3g", rechunk=True): print("median:\t" + formatter % _q2) print("Q3:\t" + formatter % _q3) print("max:\t" + formatter % _max) - print_summary_statistics.__doc__ %= (RECHUNK_ARG) + + print_summary_statistics.__doc__ %= RECHUNK_ARG def _calculate_summary_statistics(self, **kwargs): data = self.data @@ -5670,28 +6541,34 @@ def is_rgbx(self): return rgb_tools.is_rgbx(self.data) def add_marker( - self, marker, plot_on_signal=True, plot_marker=True, - permanent=False, plot_signal=True, render_figure=True): + self, + marker, + plot_on_signal=True, + plot_marker=True, + permanent=False, + plot_signal=True, + render_figure=True, + ): """ Add one or several markers to the signal or navigator plot and plot the signal, if not yet plotted (by default) Parameters ---------- - marker : :py:mod:`hyperspy.drawing.marker` object or iterable + marker : :mod:`~hyperspy.api.plot.markers` object or iterable The marker or iterable (list, tuple, ...) of markers to add. See the :ref:`plot.markers` section in the User Guide if you want to add a large number of markers as an iterable, since this will be much faster. For signals with navigation dimensions, the markers can be made to change for different navigation indices. See the examples for info. - plot_on_signal : bool - If ``True`` (default), add the marker to the signal. + plot_on_signal : bool, default True + If ``True``, add the marker to the signal. If ``False``, add the marker to the navigator - plot_marker : bool - If ``True`` (default), plot the marker. - permanent : bool - If ``False`` (default), the marker will only appear in the current + plot_marker : bool, default True + If ``True``, plot the marker. + permanent : bool, default True + If ``False``, the marker will only appear in the current plot. If ``True``, the marker will be added to the `metadata.Markers` list, and be plotted with ``plot(plot_markers=True)``. If the signal is saved as a HyperSpy @@ -5700,88 +6577,67 @@ def add_marker( Examples -------- - >>> import scipy.misc - >>> im = hs.signals.Signal2D(scipy.misc.ascent()) - >>> m = hs.markers.rectangle(x1=150, y1=100, x2=400, - >>> y2=400, color='red') + >>> im = hs.data.wave_image() + >>> m = hs.plot.markers.Rectangles( + ... offsets=[(1.0, 1.5)], widths=(0.5,), heights=(0.7,) + ... ) >>> im.add_marker(m) - Adding to a 1D signal, where the point will change - when the navigation index is changed: - - >>> s = hs.signals.Signal1D(np.random.random((3, 100))) - >>> marker = hs.markers.point((19, 10, 60), (0.2, 0.5, 0.9)) - >>> s.add_marker(marker, permanent=True, plot_marker=True) - Add permanent marker: - >>> s = hs.signals.Signal2D(np.random.random((100, 100))) - >>> marker = hs.markers.point(50, 60, color='red') + >>> rng = np.random.default_rng(1) + >>> s = hs.signals.Signal2D(rng.random((100, 100))) + >>> marker = hs.plot.markers.Points(offsets=[(50, 60)]) >>> s.add_marker(marker, permanent=True, plot_marker=True) - Add permanent marker to signal with 2 navigation dimensions. - The signal has navigation dimensions (3, 2), as the dimensions - gets flipped compared to the output from :py:func:`numpy.random.random`. - To add a vertical line marker which changes for different navigation - indices, the list used to make the marker must be a nested list: - 2 lists with 3 elements each (2 x 3): - - >>> s = hs.signals.Signal1D(np.random.random((2, 3, 10))) - >>> marker = hs.markers.vertical_line([[1, 3, 5], [2, 4, 6]]) - >>> s.add_marker(marker, permanent=True) - - Add permanent marker which changes with navigation position, and - do not add it to a current plot: - - >>> s = hs.signals.Signal2D(np.random.randint(10, size=(3, 100, 100))) - >>> marker = hs.markers.point((10, 30, 50), (30, 50, 60), color='red') - >>> s.add_marker(marker, permanent=True, plot_marker=False) - >>> s.plot(plot_markers=True) #doctest: +SKIP - Removing a permanent marker: - >>> s = hs.signals.Signal2D(np.random.randint(10, size=(100, 100))) - >>> marker = hs.markers.point(10, 60, color='red') + >>> rng = np.random.default_rng(1) + >>> s = hs.signals.Signal2D(rng.integers(10, size=(100, 100))) + >>> marker = hs.plot.markers.Points(offsets=[(10, 60)]) >>> marker.name = "point_marker" >>> s.add_marker(marker, permanent=True) >>> del s.metadata.Markers.point_marker Adding many markers as a list: - >>> from numpy.random import random - >>> s = hs.signals.Signal2D(np.random.randint(10, size=(100, 100))) + >>> rng = np.random.default_rng(1) + >>> s = hs.signals.Signal2D(rng.integers(10, size=(100, 100))) >>> marker_list = [] - >>> for i in range(100): - >>> marker = hs.markers.point(random()*100, random()*100, color='red') - >>> marker_list.append(marker) + >>> for i in range(10): + ... marker = hs.plot.markers.Points(rng.random(2)) + ... marker_list.append(marker) >>> s.add_marker(marker_list, permanent=True) """ + if not plot_marker and not permanent: + warnings.warn("`plot_marker=False` and `permanent=False` does nothing") + return + if isiterable(marker): marker_list = marker else: marker_list = [marker] markers_dict = {} + if permanent: - if not self.metadata.has_item('Markers'): - self.metadata.add_node('Markers') + # Make a list of existing marker to check if this is not already + # added to the metadata. + if not self.metadata.has_item("Markers"): + self.metadata.add_node("Markers") marker_object_list = [] for marker_tuple in list(self.metadata.Markers): marker_object_list.append(marker_tuple[1]) name_list = self.metadata.Markers.keys() + marker_name_suffix = 1 for m in marker_list: - marker_data_shape = m._get_data_shape()[::-1] - if (not (len(marker_data_shape) == 0)) and ( - marker_data_shape != self.axes_manager.navigation_shape): - raise ValueError( - "Navigation shape of the marker must be 0 or the " - "inverse navigation shape as this signal. If the " - "navigation dimensions for the signal is (2, 3), " - "the marker dimension must be (3, 2).") - if (m.signal is not None) and (m.signal is not self): + if (m._signal is not None) and (m._signal is not self): raise ValueError("Markers can not be added to several signals") + m._plot_on_signal = plot_on_signal + m._signal = self + if plot_marker: if self._plot is None or not self._plot.is_active: self.plot() @@ -5789,32 +6645,35 @@ def add_marker( self._plot.signal_plot.add_marker(m) else: if self._plot.navigator_plot is None: - self.plot() + raise ValueError( + "Attempted to plot marker on navigator_plot when none is" + "active." + ) self._plot.navigator_plot.add_marker(m) m.plot(render_figure=False) + if permanent: for marker_object in marker_object_list: if m is marker_object: raise ValueError("Marker already added to signal") - name = m.name + name = m.name if m.name != "" else m.__class__.__name__ temp_name = name + # If it already exists in the list, add a suffix while temp_name in name_list: temp_name = name + str(marker_name_suffix) marker_name_suffix += 1 m.name = temp_name markers_dict[m.name] = m - m.signal = self marker_object_list.append(m) name_list.append(m.name) - if not plot_marker and not permanent: - _logger.warning( - "plot_marker=False and permanent=False does nothing") + if permanent: - self.metadata.Markers = markers_dict + self.metadata.Markers.add_dictionary(markers_dict) + if plot_marker and render_figure: self._render_figure() - def _render_figure(self, plot=['signal_plot', 'navigation_plot']): + def _render_figure(self, plot=["signal_plot", "navigation_plot"]): for p in plot: if hasattr(self._plot, p): p = getattr(self._plot, p) @@ -5824,63 +6683,49 @@ def _plot_permanent_markers(self): marker_name_list = self.metadata.Markers.keys() markers_dict = self.metadata.Markers.__dict__ for marker_name in marker_name_list: - marker = markers_dict[marker_name]['_dtb_value_'] - if marker.plot_marker: - if marker._plot_on_signal: - self._plot.signal_plot.add_marker(marker) - else: - self._plot.navigator_plot.add_marker(marker) - marker.plot(render_figure=False) + marker = markers_dict[marker_name]["_dtb_value_"] + if marker._plot_on_signal: + self._plot.signal_plot.add_marker(marker) + else: + self._plot.navigator_plot.add_marker(marker) + marker._signal = self + marker.plot(render_figure=False) self._render_figure() def add_poissonian_noise(self, keep_dtype=True, random_state=None): """Add Poissonian noise to the data. - This method works in-place. The resulting data type is ``int64``. - If this is different from the original data type then a warning - is added to the log. + This method works in-place. Parameters ---------- keep_dtype : bool, default True - If ``True``, keep the original data type of the signal data. For - example, if the data type was initially ``'float64'``, the result of - the operation (usually ``'int64'``) will be converted to - ``'float64'``. - random_state : None or int or RandomState instance, default None + This parameter is deprecated and will be removed in HyperSpy 3.0. + Currently, it does not have any effect. This method always + keeps the original dtype of the signal. + random_state : None, int or numpy.random.Generator, default None Seed for the random generator. - Note - ---- - This method uses :py:func:`numpy.random.poisson` - (or :py:func:`dask.array.random.poisson` for lazy signals) + Notes + ----- + This method uses :func:`numpy.random.poisson` + (or :func:`dask.array.random.poisson` for lazy signals) to generate the Poissonian noise. """ kwargs = {} random_state = check_random_state(random_state, lazy=self._lazy) + if not keep_dtype: + warnings.warn( + "The `keep_dtype` parameter is deprecated and will be removed in HyperSpy 3.0.", + VisibleDeprecationWarning, + ) if self._lazy: kwargs["chunks"] = self.data.chunks - original_dtype = self.data.dtype - - self.data = random_state.poisson(lam=self.data, **kwargs) - - if self.data.dtype != original_dtype: - if keep_dtype: - _logger.warning( - f"Changing data type from {self.data.dtype} " - f"to the original {original_dtype}" - ) - # Don't change the object if possible - self.data = self.data.astype(original_dtype, copy=False) - else: - _logger.warning( - f"The data type changed from {original_dtype} " - f"to {self.data.dtype}" - ) - + self.data[:] = random_state.poisson(lam=self.data, **kwargs) + self.events.data_changed.trigger(obj=self) self.events.data_changed.trigger(obj=self) def add_gaussian_noise(self, std, random_state=None): @@ -5888,19 +6733,19 @@ def add_gaussian_noise(self, std, random_state=None): The operation is performed in-place (*i.e.* the data of the signal is modified). This method requires the signal to have a float data type, - otherwise it will raise a :py:exc:`TypeError`. + otherwise it will raise a :exc:`TypeError`. Parameters ---------- std : float The standard deviation of the Gaussian noise. - random_state : None or int or RandomState instance, default None + random_state : None, int or numpy.random.Generator, default None Seed for the random generator. - Note - ---- - This method uses :py:func:`numpy.random.normal` (or - :py:func:`dask.array.random.normal` for lazy signals) + Notes + ----- + This method uses :func:`numpy.random.normal` (or + :func:`dask.array.random.normal` for lazy signals) to generate the noise. """ @@ -5921,17 +6766,12 @@ def add_gaussian_noise(self, std, random_state=None): noise = random_state.normal(loc=0, scale=std, size=self.data.shape, **kwargs) - if self._lazy: - # With lazy data we can't keep the same array object - self.data = self.data + noise - else: - # Don't change the object - self.data += noise + self.data += noise + self.events.data_changed.trigger(obj=self) self.events.data_changed.trigger(obj=self) - def transpose(self, signal_axes=None, - navigation_axes=None, optimize=False): + def transpose(self, signal_axes=None, navigation_axes=None, optimize=False): """Transposes the signal to have the required signal and navigation axes. @@ -5943,8 +6783,8 @@ def transpose(self, signal_axes=None, The number (or indices) of axes to convert to navigation axes %s - Note - ---- + Notes + ----- With the exception of both axes parameters (`signal_axes` and `navigation_axes` getting iterables, generally one has to be ``None`` (i.e. "floating"). The other one specifies either the required number @@ -5952,9 +6792,9 @@ def transpose(self, signal_axes=None, If both are iterables, full control is given as long as all axes are assigned to one space only. - See also + See Also -------- - T, as_signal2D, as_signal1D, :py:func:`hyperspy.misc.utils.transpose` + T, as_signal2D, as_signal1D Examples -------- @@ -5988,13 +6828,18 @@ def transpose(self, signal_axes=None, """ + if self.axes_manager.ragged: + raise RuntimeError("Signal with ragged dimension can't be " "transposed.") + am = self.axes_manager ax_list = am._axes if isinstance(signal_axes, int): if navigation_axes is not None: - raise ValueError("The navigation_axes are not None, even " - "though just a number was given for " - "signal_axes") + raise ValueError( + "The navigation_axes are not None, even " + "though just a number was given for " + "signal_axes" + ) if len(ax_list) < signal_axes: raise ValueError("Too many signal axes requested") if signal_axes < 0: @@ -6008,28 +6853,32 @@ def transpose(self, signal_axes=None, elif iterable_not_string(signal_axes): signal_axes = tuple(am[ax] for ax in signal_axes) if navigation_axes is None: - navigation_axes = tuple(ax for ax in ax_list - if ax not in signal_axes)[::-1] + navigation_axes = tuple(ax for ax in ax_list if ax not in signal_axes)[ + ::-1 + ] elif iterable_not_string(navigation_axes): # want to keep the order navigation_axes = tuple(am[ax] for ax in navigation_axes) intersection = set(signal_axes).intersection(navigation_axes) if len(intersection): - raise ValueError("At least one axis found in both spaces:" - " {}".format(intersection)) + raise ValueError( + "At least one axis found in both spaces:" " {}".format( + intersection + ) + ) if len(am._axes) != (len(signal_axes) + len(navigation_axes)): - raise ValueError("Not all current axes were assigned to a " - "space") + raise ValueError("Not all current axes were assigned to a " "space") else: - raise ValueError("navigation_axes has to be None or an iterable" - " when signal_axes is iterable") + raise ValueError( + "navigation_axes has to be None or an iterable" + " when signal_axes is iterable" + ) elif signal_axes is None: if isinstance(navigation_axes, int): if len(ax_list) < navigation_axes: raise ValueError("Too many navigation axes requested") if navigation_axes < 0: - raise ValueError( - "Can't have negative number of navigation axes") + raise ValueError("Can't have negative number of navigation axes") elif navigation_axes == 0: navigation_axes = () signal_axes = ax_list[::-1] @@ -6037,16 +6886,15 @@ def transpose(self, signal_axes=None, signal_axes = ax_list[navigation_axes:][::-1] navigation_axes = ax_list[:navigation_axes][::-1] elif iterable_not_string(navigation_axes): - navigation_axes = tuple(am[ax] for ax in - navigation_axes) - signal_axes = tuple(ax for ax in ax_list - if ax not in navigation_axes)[::-1] + navigation_axes = tuple(am[ax] for ax in navigation_axes) + signal_axes = tuple(ax for ax in ax_list if ax not in navigation_axes)[ + ::-1 + ] elif navigation_axes is None: signal_axes = am.navigation_axes navigation_axes = am.signal_axes else: - raise ValueError( - "The passed navigation_axes argument is not valid") + raise ValueError("The passed navigation_axes argument is not valid") else: raise ValueError("The passed signal_axes argument is not valid") # translate to axes idx from actual objects for variance @@ -6056,12 +6904,12 @@ def transpose(self, signal_axes=None, signal_axes = signal_axes[::-1] navigation_axes = navigation_axes[::-1] # get data view - array_order = tuple( - ax.index_in_array for ax in navigation_axes) + array_order = tuple(ax.index_in_array for ax in navigation_axes) array_order += tuple(ax.index_in_array for ax in signal_axes) newdata = self.data.transpose(array_order) - res = self._deepcopy_with_new_data(newdata, copy_variance=True, - copy_learning_results=True) + res = self._deepcopy_with_new_data( + newdata, copy_variance=True, copy_learning_results=True + ) # reconfigure the axes of the axesmanager: ram = res.axes_manager @@ -6079,36 +6927,38 @@ def transpose(self, signal_axes=None, var = res.get_noise_variance() if isinstance(var, BaseSignal): - var = var.transpose(signal_axes=idx_sig, - navigation_axes=idx_nav, - optimize=optimize) + var = var.transpose( + signal_axes=idx_sig, navigation_axes=idx_nav, optimize=optimize + ) res.set_noise_variance(var) if optimize: res._make_sure_data_is_contiguous() - if res.metadata.has_item('Markers'): + if res.metadata.has_item("Markers"): # The markers might fail if the navigation dimensions are changed # so the safest is simply to not carry them over from the # previous signal. del res.metadata.Markers return res - transpose.__doc__ %= (OPTIMIZE_ARG) + + transpose.__doc__ %= OPTIMIZE_ARG @property def T(self): """The transpose of the signal, with signal and navigation spaces swapped. Enables calling - :py:meth:`~hyperspy.signal.BaseSignal.transpose` with the default + :meth:`~hyperspy.api.signals.BaseSignal.transpose` with the default parameters as a property of a Signal. """ return self.transpose() - def apply_apodization(self, window='hann', - hann_order=None, tukey_alpha=0.5, inplace=False): + def apply_apodization( + self, window="hann", hann_order=None, tukey_alpha=0.5, inplace=False + ): """ Apply an `apodization window - `_ to a Signal. + `_ to a Signal. Parameters ---------- @@ -6122,7 +6972,7 @@ def apply_apodization(self, window='hann', tukey_alpha : float, optional Only used if ``window='tukey'`` (default is 0.5). From the documentation of - :py:func:`scipy.signal.windows.tukey`: + :func:`scipy.signal.windows.tukey`: - Shape parameter of the Tukey window, representing the fraction of the window inside the cosine tapered region. If @@ -6135,29 +6985,36 @@ def apply_apodization(self, window='hann', Returns ------- - out : :py:class:`~hyperspy.signal.BaseSignal` (or subclasses), optional + out : :class:`~hyperspy.signal.BaseSignal` (or subclass), optional If ``inplace=False``, returns the apodized signal of the same type as the provided Signal. Examples -------- >>> import hyperspy.api as hs - >>> holo = hs.datasets.example_signals.object_hologram() - >>> holo.apply_apodization('tukey', tukey_alpha=0.1).plot() + >>> wave = hs.data.wave_image() + >>> wave.apply_apodization('tukey', tukey_alpha=0.1).plot() """ - if window == 'hanning' or window == 'hann': + if window == "hanning" or window == "hann": if hann_order: - def window_function( - m): return hann_window_nth_order(m, hann_order) + + def window_function(m): + return hann_window_nth_order(m, hann_order) else: - def window_function(m): return np.hanning(m) - elif window == 'hamming': - def window_function(m): return np.hamming(m) - elif window == 'tukey': - def window_function(m): return sp_signal.tukey(m, tukey_alpha) + + def window_function(m): + return np.hanning(m) + elif window == "hamming": + + def window_function(m): + return np.hamming(m) + elif window == "tukey": + + def window_function(m): + return sp_signal.windows.tukey(m, tukey_alpha) else: - raise ValueError('Wrong type parameter value.') + raise ValueError("Wrong type parameter value.") windows_1d = [] @@ -6166,8 +7023,7 @@ def window_function(m): return sp_signal.tukey(m, tukey_alpha) for axis, axis_index in zip(self.axes_manager.signal_axes, axes): if isinstance(self.data, da.Array): chunks = self.data.chunks[axis_index] - window_da = da.from_array(window_function(axis.size), - chunks=(chunks, )) + window_da = da.from_array(window_function(axis.size), chunks=(chunks,)) windows_1d.append(window_da) else: windows_1d.append(window_function(axis.size)) @@ -6182,7 +7038,8 @@ def window_function(m): return sp_signal.tukey(m, tukey_alpha) # Iterate over all dimensions of the data for i in range(self.data.ndim): if any( - i == axes): # If current dimension represents one of signal axis, all elements in window + i == axes + ): # If current dimension represents one of signal axis, all elements in window # along current axis to be subscribed slice_w.append(slice(None)) else: # If current dimension is navigation one, new axis is absent in window and should be created @@ -6214,17 +7071,25 @@ def _check_navigation_mask(self, mask): """ if isinstance(mask, BaseSignal): if mask.axes_manager.signal_dimension != 0: - raise ValueError("The navigation mask signal must have the " - "`signal_dimension` equal to 0.") - elif (mask.axes_manager.navigation_shape != - self.axes_manager.navigation_shape): - raise ValueError("The navigation mask signal must have the " - "same `navigation_shape` as the current " - "signal.") + raise ValueError( + "The navigation mask signal must have the " + "`signal_dimension` equal to 0." + ) + elif ( + mask.axes_manager.navigation_shape != self.axes_manager.navigation_shape + ): + raise ValueError( + "The navigation mask signal must have the " + "same `navigation_shape` as the current " + "signal." + ) if isinstance(mask, np.ndarray) and ( - mask.shape != self.axes_manager.navigation_shape): - raise ValueError("The shape of the navigation mask array must " - "match `navigation_shape`.") + mask.shape != self.axes_manager.navigation_shape + ): + raise ValueError( + "The shape of the navigation mask array must " + "match `navigation_shape`." + ) def _check_signal_mask(self, mask): """ @@ -6247,16 +7112,65 @@ def _check_signal_mask(self, mask): """ if isinstance(mask, BaseSignal): if mask.axes_manager.navigation_dimension != 0: - raise ValueError("The signal mask signal must have the " - "`navigation_dimension` equal to 0.") - elif (mask.axes_manager.signal_shape != - self.axes_manager.signal_shape): - raise ValueError("The signal mask signal must have the same " - "`signal_shape` as the current signal.") + raise ValueError( + "The signal mask signal must have the " + "`navigation_dimension` equal to 0." + ) + elif mask.axes_manager.signal_shape != self.axes_manager.signal_shape: + raise ValueError( + "The signal mask signal must have the same " + "`signal_shape` as the current signal." + ) if isinstance(mask, np.ndarray) and ( - mask.shape != self.axes_manager.signal_shape): - raise ValueError("The shape of signal mask array must match " - "`signal_shape`.") + mask.shape != self.axes_manager.signal_shape + ): + raise ValueError( + "The shape of signal mask array must match " "`signal_shape`." + ) + + def to_device(self): + """ + Transfer data array from host to GPU device memory using cupy.asarray. + Lazy signals are not supported by this method, see user guide for + information on how to process data lazily using the GPU. + + Raises + ------ + BaseException + Raise expection if cupy is not installed. + BaseException + Raise expection if signal is lazy. + + Returns + ------- + None. + + """ + if self._lazy: + raise LazyCupyConversion + + if not CUPY_INSTALLED: + raise BaseException("cupy is required.") + else: # pragma: no cover + self.data = cp.asarray(self.data) + + def to_host(self): + """ + Transfer data array from GPU device to host memory. + + Raises + ------ + BaseException + Raise expection if signal is lazy. + + Returns + ------- + None. + + """ + if self._lazy: # pragma: no cover + raise LazyCupyConversion + self.data = to_numpy(self.data) ARITHMETIC_OPERATORS = ( @@ -6305,11 +7219,11 @@ def _check_signal_mask(self, mask): ) for name in ARITHMETIC_OPERATORS + INPLACE_OPERATORS + COMPARISON_OPERATORS: exec( - ("def %s(self, other):\n" % name) + - (" return self._binary_operator_ruler(other, \'%s\')\n" % - name)) + ("def %s(self, other):\n" % name) + + (" return self._binary_operator_ruler(other, '%s')\n" % name) + ) exec("%s.__doc__ = np.ndarray.%s.__doc__" % (name, name)) - exec("setattr(BaseSignal, \'%s\', %s)" % (name, name)) + exec("setattr(BaseSignal, '%s', %s)" % (name, name)) # The following commented line enables the operators with swapped # operands. They should be defined only for commutative operators # but for simplicity we don't support this at all atm. @@ -6320,7 +7234,7 @@ def _check_signal_mask(self, mask): # Implement unary arithmetic operations for name in UNARY_OPERATORS: exec( - ("def %s(self):" % name) + - (" return self._unary_operator_ruler(\'%s\')" % name)) + ("def %s(self):" % name) + (" return self._unary_operator_ruler('%s')" % name) + ) exec("%s.__doc__ = int.%s.__doc__" % (name, name)) - exec("setattr(BaseSignal, \'%s\', %s)" % (name, name)) + exec("setattr(BaseSignal, '%s', %s)" % (name, name)) diff --git a/hyperspy/signal_tools.py b/hyperspy/signal_tools.py index 2c0aef34e8..941c91a766 100644 --- a/hyperspy/signal_tools.py +++ b/hyperspy/signal_tools.py @@ -1,52 +1,199 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import logging -import functools import copy +import functools +import logging -import numpy as np -from scipy import interpolate -from scipy import signal as sp_signal +import matplotlib import matplotlib.colors -import matplotlib.pyplot as plt import matplotlib.text as mpl_text +import numpy as np import traits.api as t +from scipy import interpolate +from scipy import signal as sp_signal -from hyperspy import drawing -from hyperspy.docstrings.signal import HISTOGRAM_MAX_BIN_ARGS -from hyperspy.exceptions import SignalDimensionError +from hyperspy import components1d, drawing from hyperspy.axes import AxesManager, UniformDataAxis -from hyperspy.drawing.widgets import VerticalLineWidget -from hyperspy import components1d from hyperspy.component import Component -from hyperspy.ui_registry import add_gui_method -from hyperspy.misc.test_utils import ignore_warning -from hyperspy.misc.label_position import SpectrumLabelPosition -from hyperspy.misc.eels.tools import get_edges_near_energy, get_info_from_edges -from hyperspy.drawing.figure import BlittedFigure +from hyperspy.docstrings.signal import HISTOGRAM_MAX_BIN_ARGS +from hyperspy.drawing._markers.circles import Circles +from hyperspy.drawing._widgets.range import SpanSelector +from hyperspy.drawing.markers import convert_positions +from hyperspy.drawing.signal1d import Signal1DFigure +from hyperspy.drawing.widgets import Line2DWidget, VerticalLineWidget +from hyperspy.exceptions import SignalDimensionError from hyperspy.misc.array_tools import numba_histogram -from hyperspy.misc.utils import is_binned # remove in v2.0 - +from hyperspy.misc.math_tools import check_random_state +from hyperspy.ui_registry import add_gui_method _logger = logging.getLogger(__name__) +class LineInSignal2D(t.HasTraits): + """ + Adds a vertical draggable line to a spectrum that reports its + position to the position attribute of the class. + + Attributes + ---------- + x0, y0, x1, y1 : floats + Position of the line in scaled units. + length : float + Length of the line in scaled units. + on : bool + Turns on and off the line + color : wx.Colour + The color of the line. It automatically redraws the line. + + """ + + x0, y0, x1, y1 = t.Float(0.0), t.Float(0.0), t.Float(1.0), t.Float(1.0) + length = t.Float(1.0) + is_ok = t.Bool(False) + on = t.Bool(False) + # The following is disabled because as of traits 4.6 the Color trait + # imports traitsui (!) + # try: + # color = t.Color("black") + # except ModuleNotFoundError: # traitsui is not installed + # pass + color_str = t.Str("black") + + def __init__(self, signal): + if signal.axes_manager.signal_dimension != 2: + raise SignalDimensionError(signal.axes_manager.signal_dimension, 2) + + self.signal = signal + if (self.signal._plot is None) or (not self.signal._plot.is_active): + self.signal.plot() + axis_dict0 = signal.axes_manager.signal_axes[0].get_axis_dictionary() + axis_dict1 = signal.axes_manager.signal_axes[1].get_axis_dictionary() + am = AxesManager([axis_dict1, axis_dict0]) + am._axes[0].navigate = True + am._axes[1].navigate = True + self.axes_manager = am + self.on_trait_change(self.switch_on_off, "on") + + def draw(self): + self.signal._plot.signal_plot.figure.canvas.draw_idle() + + def _get_initial_position(self): + am = self.axes_manager + d0 = (am[0].high_value - am[0].low_value) / 10 + d1 = (am[1].high_value - am[1].low_value) / 10 + position = ( + (am[0].low_value + d0, am[1].low_value + d1), + (am[0].high_value - d0, am[1].high_value - d1), + ) + return position + + def switch_on_off(self, obj, trait_name, old, new): + if not self.signal._plot.is_active: + return + + if new is True and old is False: + self._line = Line2DWidget(self.axes_manager) + self._line.position = self._get_initial_position() + self._line.set_mpl_ax(self.signal._plot.signal_plot.ax) + self._line.linewidth = 1 + self._color_changed("black", "black") + self.update_position() + self._line.events.changed.connect(self.update_position) + # There is not need to call draw because setting the + # color calls it. + + elif new is False and old is True: + self._line.close() + self._line = None + self.draw() + + def update_position(self, *args, **kwargs): + if not self.signal._plot.is_active: + return + pos = self._line.position + (self.x0, self.y0), (self.x1, self.y1) = pos + self.length = np.linalg.norm(np.diff(pos, axis=0), axis=1)[0] + + def _color_changed(self, old, new): + if self.on is False: + return + self.draw() + + +@add_gui_method(toolkey="hyperspy.Signal2D.calibrate") +class Signal2DCalibration(LineInSignal2D): + new_length = t.Float(t.Undefined, label="New length") + scale = t.Float() + units = t.Unicode() + + def __init__(self, signal): + super(Signal2DCalibration, self).__init__(signal) + if signal.axes_manager.signal_dimension != 2: + raise SignalDimensionError(signal.axes_manager.signal_dimension, 2) + self.units = self.signal.axes_manager.signal_axes[0].units + self.scale = self.signal.axes_manager.signal_axes[0].scale + self.on = True + + def _new_length_changed(self, old, new): + # If the line position is invalid or the new length is not defined do + # nothing + if ( + np.isnan(self.x0) + or np.isnan(self.y0) + or np.isnan(self.x1) + or np.isnan(self.y1) + or self.new_length is t.Undefined + ): + return + self.scale = self.signal._get_signal2d_scale( + self.x0, self.y0, self.x1, self.y1, self.new_length + ) + + def _length_changed(self, old, new): + # If the line position is invalid or the new length is not defined do + # nothing + if ( + np.isnan(self.x0) + or np.isnan(self.y0) + or np.isnan(self.x1) + or np.isnan(self.y1) + or self.new_length is t.Undefined + ): + return + self.scale = self.signal._get_signal2d_scale( + self.x0, self.y0, self.x1, self.y1, self.new_length + ) + + def apply(self): + if self.new_length is t.Undefined: + _logger.warning("Input a new length before pressing apply.") + return + x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1 + if np.isnan(x0) or np.isnan(y0) or np.isnan(x1) or np.isnan(y1): + _logger.warning("Line position is not valid") + return + self.signal._calibrate( + x0=x0, y0=y0, x1=x1, y1=y1, new_length=self.new_length, units=self.units + ) + self.signal._replot() + + class SpanSelectorInSignal1D(t.HasTraits): ss_left_value = t.Float(np.nan) ss_right_value = t.Float(np.nan) @@ -54,46 +201,75 @@ class SpanSelectorInSignal1D(t.HasTraits): def __init__(self, signal): if signal.axes_manager.signal_dimension != 1: - raise SignalDimensionError( - signal.axes_manager.signal_dimension, 1) + raise SignalDimensionError(signal.axes_manager.signal_dimension, 1) + + # Plot the signal (or model) if it is not already plotted + if signal._plot is None or not signal._plot.is_active: + signal.plot() + + from hyperspy.model import BaseModel + + if isinstance(signal, BaseModel): + signal = signal.signal self.signal = signal self.axis = self.signal.axes_manager.signal_axes[0] self.span_selector = None - if signal._plot is None or not signal._plot.is_active: - signal.plot() + self.span_selector_switch(on=True) + self.signal._plot.signal_plot.events.closed.connect(self.disconnect, []) + def on_disabling_span_selector(self): - pass + self.disconnect() def span_selector_switch(self, on): if not self.signal._plot.is_active: return if on is True: - self.span_selector = \ - drawing.widgets.ModifiableSpanSelector( - self.signal._plot.signal_plot.ax, - onselect=self.update_span_selector_traits, - onmove_callback=self.update_span_selector_traits,) + if self.span_selector is None: + ax = self.signal._plot.signal_plot.ax + self.span_selector = SpanSelector( + ax=ax, + onselect=lambda *args, **kwargs: None, + onmove_callback=self.span_selector_changed, + direction="horizontal", + interactive=True, + ignore_event_outside=True, + drag_from_anywhere=True, + props={"alpha": 0.25, "color": "r"}, + handle_props={"alpha": 0.5, "color": "r"}, + useblit=ax.figure.canvas.supports_blit, + ) + self.connect() elif self.span_selector is not None: self.on_disabling_span_selector() - self.span_selector.turn_off() + self.span_selector.disconnect_events() + self.span_selector.clear() self.span_selector = None - def update_span_selector_traits(self, *args, **kwargs): + def span_selector_changed(self, *args, **kwargs): if not self.signal._plot.is_active: return - x0 = self.span_selector.rect.get_x() + + x0, x1 = sorted(self.span_selector.extents) + + # typically during initialisation + if x0 == x1: + return + + # range of span selector invalid if x0 < self.axis.low_value: x0 = self.axis.low_value - self.ss_left_value = x0 - x1 = self.ss_left_value + self.span_selector.rect.get_width() - if x1 > self.axis.high_value: + if x1 > self.axis.high_value or x1 < self.axis.low_value: x1 = self.axis.high_value - self.ss_right_value = x1 + + if np.diff(self.axis.value2index(np.array([x0, x1]))) == 0: + return + + self.ss_left_value, self.ss_right_value = x0, x1 def reset_span_selector(self): self.span_selector_switch(False) @@ -102,13 +278,40 @@ def reset_span_selector(self): self.span_selector_switch(True) @property - def is_span_selector_valid(self): - return (not np.isnan(self.ss_left_value) and - not np.isnan(self.ss_right_value) and - self.ss_left_value <= self.ss_right_value) + def _is_valid_range(self): + return ( + self.span_selector is not None + and not np.isnan([self.ss_left_value, self.ss_right_value]).any() + ) -class LineInSignal1D(t.HasTraits): + def _reset_span_selector_background(self): + if self.span_selector is not None: + # For matplotlib backend supporting blit, we need to reset the + # background when the data displayed on the figure is changed, + # otherwise, when the span selector is updated, old background is + # restore + self.span_selector.background = None + # Trigger callback + self.span_selector_changed() + def connect(self): + for event in [ + self.signal.events.data_changed, + self.signal.axes_manager.events.indices_changed, + ]: + event.connect(self._reset_span_selector_background, []) + + def disconnect(self): + function = self._reset_span_selector_background + for event in [ + self.signal.events.data_changed, + self.signal.axes_manager.events.indices_changed, + ]: + if function in event.connected: + event.disconnect(function) + + +class LineInSignal1D(t.HasTraits): """Adds a vertical draggable line to a spectrum that reports its position to the position attribute of the class. @@ -123,6 +326,7 @@ class LineInSignal1D(t.HasTraits): The color of the line. It automatically redraws the line. """ + position = t.Float() is_ok = t.Bool(False) on = t.Bool(False) @@ -136,21 +340,23 @@ class LineInSignal1D(t.HasTraits): def __init__(self, signal): if signal.axes_manager.signal_dimension != 1: - raise SignalDimensionError( - signal.axes_manager.signal_dimension, 1) + raise SignalDimensionError(signal.axes_manager.signal_dimension, 1) self.signal = signal self.signal.plot() axis_dict = signal.axes_manager.signal_axes[0].get_axis_dictionary() - am = AxesManager([axis_dict, ]) + am = AxesManager( + [ + axis_dict, + ] + ) am._axes[0].navigate = True # Set the position of the line in the middle of the spectral # range by default am._axes[0].index = int(round(am._axes[0].size / 2)) self.axes_manager = am - self.axes_manager.events.indices_changed.connect( - self.update_position, []) - self.on_trait_change(self.switch_on_off, 'on') + self.axes_manager.events.indices_changed.connect(self.update_position, []) + self.on_trait_change(self.switch_on_off, "on") def draw(self): self.signal._plot.signal_plot.figure.canvas.draw_idle() @@ -181,65 +387,68 @@ def _color_changed(self, old, new): if self.on is False: return - self._line.patch.set_color((self.color.Red() / 255., - self.color.Green() / 255., - self.color.Blue() / 255.,)) + self._line.patch.set_color( + ( + self.color.Red() / 255.0, + self.color.Green() / 255.0, + self.color.Blue() / 255.0, + ) + ) self.draw() @add_gui_method(toolkey="hyperspy.Signal1D.calibrate") class Signal1DCalibration(SpanSelectorInSignal1D): - left_value = t.Float(t.Undefined, label='New left value') - right_value = t.Float(t.Undefined, label='New right value') + left_value = t.Float(t.Undefined, label="New left value") + right_value = t.Float(t.Undefined, label="New right value") offset = t.Float() scale = t.Float() units = t.Unicode() def __init__(self, signal): - super(Signal1DCalibration, self).__init__(signal) + super().__init__(signal) if signal.axes_manager.signal_dimension != 1: - raise SignalDimensionError( - signal.axes_manager.signal_dimension, 1) + raise SignalDimensionError(signal.axes_manager.signal_dimension, 1) if not isinstance(self.axis, UniformDataAxis): - raise NotImplementedError("The calibration tool supports only uniform axes.") + raise NotImplementedError( + "The calibration tool supports only uniform axes." + ) self.units = self.axis.units self.scale = self.axis.scale self.offset = self.axis.offset self.last_calibration_stored = True + self.span_selector.snap_values = self.axis.axis def _left_value_changed(self, old, new): - if self.span_selector is not None and \ - self.span_selector.range is None: - return - else: + if self._is_valid_range and self.right_value is not t.Undefined: self._update_calibration() def _right_value_changed(self, old, new): - if self.span_selector.range is None: - return - else: + if self._is_valid_range and self.left_value is not t.Undefined: self._update_calibration() def _update_calibration(self, *args, **kwargs): # If the span selector or the new range values are not defined do # nothing - if np.isnan(self.ss_left_value) or np.isnan(self.ss_right_value) or\ - t.Undefined in (self.left_value, self.right_value): + if not self._is_valid_range or self.signal._plot.signal_plot is None: return lc = self.axis.value2index(self.ss_left_value) rc = self.axis.value2index(self.ss_right_value) self.offset, self.scale = self.axis.calibrate( - (self.left_value, self.right_value), (lc, rc), - modify_calibration=False) + (self.left_value, self.right_value), (lc, rc), modify_calibration=False + ) def apply(self): - if np.isnan(self.ss_left_value) or np.isnan(self.ss_right_value): - _logger.warning("Select a range by clicking on the signal figure " - "and dragging before pressing Apply.") + if not self._is_valid_range: + _logger.warning( + "Select a range by clicking on the signal figure " + "and dragging before pressing Apply." + ) return elif self.left_value is t.Undefined or self.right_value is t.Undefined: - _logger.warning("Select the new left and right values before " - "pressing apply.") + _logger.warning( + "Select the new left and right values before " "pressing apply." + ) return axis = self.axis axis.scale = self.scale @@ -250,208 +459,6 @@ def apply(self): self.span_selector_switch(on=True) self.last_calibration_stored = True -@add_gui_method(toolkey="hyperspy.EELSSpectrum.print_edges_table") -class EdgesRange(SpanSelectorInSignal1D): - units = t.Unicode() - edges_list = t.Tuple() - only_major = t.Bool() - order = t.Unicode('closest') - complementary = t.Bool(True) - - def __init__(self, signal, active=None): - if signal.axes_manager.signal_dimension != 1: - raise SignalDimensionError( - signal.axes_manager.signal_dimension, 1) - - if active is None: - super(EdgesRange, self).__init__(signal) - self.active_edges = [] - else: - # if active is provided, it is non-interactive mode - # so fix the active_edges and don't initialise the span selector - self.signal = signal - self.axis = self.signal.axes_manager.signal_axes[0] - self.active_edges = list(active) - - self.active_complementary_edges = [] - self.units = self.axis.units - self.slp = SpectrumLabelPosition(self.signal) - self.btns = [] - - self._get_edges_info_within_energy_axis() - - self.signal.axes_manager.events.indices_changed.connect(self._on_figure_changed, - []) - self.signal._plot.signal_plot.events.closed.connect( - lambda: self.signal.axes_manager.events.indices_changed.disconnect( - self._on_figure_changed), []) - - def _get_edges_info_within_energy_axis(self): - mid_energy = (self.axis.low_value + self.axis.high_value) / 2 - rng = self.axis.high_value - self.axis.low_value - self.edge_all = np.asarray(get_edges_near_energy(mid_energy, rng, - order=self.order)) - info = get_info_from_edges(self.edge_all) - - energy_all = [] - relevance_all = [] - description_all = [] - for d in info: - onset = d['onset_energy (eV)'] - relevance = d['relevance'] - threshold = d['threshold'] - edge_ = d['edge'] - description = threshold + '. '*(threshold !='' and edge_ !='') + edge_ - - energy_all.append(onset) - relevance_all.append(relevance) - description_all.append(description) - - self.energy_all = np.asarray(energy_all) - self.relevance_all = np.asarray(relevance_all) - self.description_all = np.asarray(description_all) - - def _on_figure_changed(self): - self.slp._set_active_figure_properties() - self._plot_labels() - self.signal._plot.signal_plot.update() - - def update_table(self): - figure_changed = self.slp._check_signal_figure_changed() - if figure_changed: - self._on_figure_changed() - - if self.span_selector is not None: - energy_mask = (self.ss_left_value <= self.energy_all) & \ - (self.energy_all <= self.ss_right_value) - if self.only_major: - relevance_mask = self.relevance_all == 'Major' - else: - relevance_mask = np.ones(len(self.edge_all), bool) - - mask = energy_mask & relevance_mask - self.edges_list = tuple(self.edge_all[mask]) - energy = tuple(self.energy_all[mask]) - relevance = tuple(self.relevance_all[mask]) - description = tuple(self.description_all[mask]) - else: - self.edges_list = () - energy, relevance, description = (), (), () - - self._keep_valid_edges() - - return self.edges_list, energy, relevance, description - - def _keep_valid_edges(self): - edge_all = list(self.signal._edge_markers.keys()) - for edge in edge_all: - if (edge not in self.edges_list): - if edge in self.active_edges: - self.active_edges.remove(edge) - elif edge in self.active_complementary_edges: - self.active_complementary_edges.remove(edge) - self.signal.remove_EELS_edges_markers([edge]) - elif (edge not in self.active_edges): - self.active_edges.append(edge) - - self.on_complementary() - self._plot_labels() - - def update_active_edge(self, change): - state = change['new'] - edge = change['owner'].description - - if state: - self.active_edges.append(edge) - else: - if edge in self.active_edges: - self.active_edges.remove(edge) - if edge in self.active_complementary_edges: - self.active_complementary_edges.remove(edge) - self.signal.remove_EELS_edges_markers([edge]) - - figure_changed = self.slp._check_signal_figure_changed() - if figure_changed: - self._on_figure_changed() - self.on_complementary() - self._plot_labels() - - def on_complementary(self): - - if self.complementary: - self.active_complementary_edges = \ - self.signal.get_complementary_edges(self.active_edges, - self.only_major) - else: - self.active_complementary_edges = [] - - def check_btn_state(self): - - edges = [btn.description for btn in self.btns] - for btn in self.btns: - edge = btn.description - if btn.value is False: - if edge in self.active_edges: - self.active_edges.remove(edge) - self.signal.remove_EELS_edges_markers([edge]) - if edge in self.active_complementary_edges: - btn.value = True - - if btn.value is True and self.complementary: - comp = self.signal.get_complementary_edges(self.active_edges, - self.only_major) - for cedge in comp: - if cedge in edges: - pos = edges.index(cedge) - self.btns[pos].value = True - - def _plot_labels(self, active=None, complementary=None): - # plot selected and/or complementary edges - if active is None: - active = self.active_edges - if complementary is None: - complementary = self.active_complementary_edges - - edges_on_signal = set(self.signal._edge_markers.keys()) - edges_to_show = set(set(active).union(complementary)) - edge_keep = edges_on_signal.intersection(edges_to_show) - edge_remove = edges_on_signal.difference(edge_keep) - edge_add = edges_to_show.difference(edge_keep) - - self._clear_markers(edge_remove) - - # all edges to be shown on the signal - edge_dict = self.signal._get_edges(edges_to_show, ('Major', 'Minor')) - vm_new, tm_new = self.slp.get_markers(edge_dict) - for k, edge in enumerate(edge_dict.keys()): - v = vm_new[k] - t = tm_new[k] - - if edge in edge_keep: - # update position of vertical line segment - self.signal._edge_markers[edge][0].data = v.data - self.signal._edge_markers[edge][0].update() - - # update position of text box - self.signal._edge_markers[edge][1].data = t.data - self.signal._edge_markers[edge][1].update() - elif edge in edge_add: - # first argument as dictionary for consistency - self.signal.plot_edges_label({edge: edge_dict[edge]}, - vertical_line_marker=[v], - text_marker=[t]) - - def _clear_markers(self, edges=None): - if edges is None: - edges = list(self.signal._edge_markers.keys()) - - self.signal.remove_EELS_edges_markers(list(edges)) - - for edge in edges: - if edge in self.active_edges: - self.active_edges.remove(edge) - if edge in self.active_complementary_edges: - self.active_complementary_edges.remove(edge) class Signal1DRangeSelector(SpanSelectorInSignal1D): on_close = t.List() @@ -474,11 +481,11 @@ def line_color_rgb(self): if hasattr(self, "line_color"): try: # PyQt and WX - return np.array(self.line_color.Get()) / 255. + return np.array(self.line_color.Get()) / 255.0 except AttributeError: try: # PySide - return np.array(self.line_color.getRgb()) / 255. + return np.array(self.line_color.getRgb()) / 255.0 except BaseException: return matplotlib.colors.to_rgb(self.line_color_ipy) else: @@ -499,17 +506,16 @@ def plot(self): hse = self.signal._plot l1 = hse.signal_plot.ax_lines[0] self.original_color = l1.line.get_color() - l1.set_line_properties(color=self.original_color, - type='scatter') + l1.set_line_properties(color=self.original_color, type="scatter") + l2 = drawing.signal1d.Signal1DLine() l2.data_function = self.model2plot - l2.set_line_properties( - color=self.line_color_rgb, - type='line') + l2.set_line_properties(color=self.line_color_rgb, type="line") # Add the line to the figure hse.signal_plot.add_line(l2) l2.plot() + self.data_line = l1 self.smooth_line = l2 self.smooth_diff_line = None @@ -520,16 +526,14 @@ def update_lines(self): self.smooth_diff_line.update() def turn_diff_line_on(self, diff_order): - self.signal._plot.signal_plot.create_right_axis() self.smooth_diff_line = drawing.signal1d.Signal1DLine() self.smooth_diff_line.axes_manager = self.signal.axes_manager self.smooth_diff_line.data_function = self.diff_model2plot self.smooth_diff_line.set_line_properties( - color=self.line_color_rgb, - type='line') - self.signal._plot.signal_plot.add_line(self.smooth_diff_line, - ax='right') + color=self.line_color_rgb, type="line" + ) + self.signal._plot.signal_plot.add_line(self.smooth_diff_line, ax="right") def _line_color_ipy_changed(self): if hasattr(self, "line_color"): @@ -554,11 +558,9 @@ def _differential_order_changed(self, old, new): self.smooth_diff_line.update(force_replot=False) def _line_color_changed(self, old, new): - self.smooth_line.line_properties = { - 'color': self.line_color_rgb} + self.smooth_line.line_properties = {"color": self.line_color_rgb} if self.smooth_diff_line is not None: - self.smooth_diff_line.line_properties = { - 'color': self.line_color_rgb} + self.smooth_diff_line.line_properties = {"color": self.line_color_rgb} try: # it seems that changing the properties can be done before the # first rendering event, which can cause issue with blitting @@ -579,22 +581,18 @@ def close(self): if self.differential_order != 0: self.turn_diff_line_off() self.smooth_line.close() - self.data_line.set_line_properties( - color=self.original_color, - type='line') + self.data_line.set_line_properties(color=self.original_color, type="line") @add_gui_method(toolkey="hyperspy.Signal1D.smooth_savitzky_golay") class SmoothingSavitzkyGolay(Smoothing): - polynomial_order = t.Int( 3, desc="The order of the polynomial used to fit the samples." - "`polyorder` must be less than `window_length`.") + "`polyorder` must be less than `window_length`.", + ) - window_length = t.Int( - 5, - desc="`window_length` must be a positive odd integer.") + window_length = t.Int(5, desc="`window_length` must be a positive odd integer.") increase_window_length = t.Button(orientation="horizontal", label="+") decrease_window_length = t.Button(orientation="horizontal", label="-") @@ -616,14 +614,16 @@ def _decrease_window_length_fired(self): self.window_length = nwl else: _logger.warning( - "The window length must be greater than the polynomial order") + "The window length must be greater than the polynomial order" + ) def _polynomial_order_changed(self, old, new): if self.window_length <= new: self.window_length = new + 2 if new % 2 else new + 1 _logger.warning( - "Polynomial order must be < window length. " - "Window length set to %i.", self.window_length) + "Polynomial order must be < window length. " "Window length set to %i.", + self.window_length, + ) self.update_lines() def _window_length_changed(self, old, new): @@ -634,48 +634,49 @@ def _differential_order_changed(self, old, new): self.polynomial_order += 1 _logger.warning( "Differential order must be <= polynomial order. " - "Polynomial order set to %i.", self.polynomial_order) - super( - SmoothingSavitzkyGolay, - self)._differential_order_changed( - old, - new) + "Polynomial order set to %i.", + self.polynomial_order, + ) + super()._differential_order_changed(old, new) def diff_model2plot(self, axes_manager=None): - self.single_spectrum.data = self.signal().copy() + self.single_spectrum.data = self.signal._get_current_data().copy() self.single_spectrum.smooth_savitzky_golay( polynomial_order=self.polynomial_order, window_length=self.window_length, - differential_order=self.differential_order) + differential_order=self.differential_order, + ) return self.single_spectrum.data def model2plot(self, axes_manager=None): - self.single_spectrum.data = self.signal().copy() + self.single_spectrum.data = self.signal._get_current_data().copy() self.single_spectrum.smooth_savitzky_golay( polynomial_order=self.polynomial_order, window_length=self.window_length, - differential_order=0) + differential_order=0, + ) return self.single_spectrum.data def apply(self): self.signal.smooth_savitzky_golay( polynomial_order=self.polynomial_order, window_length=self.window_length, - differential_order=self.differential_order) + differential_order=self.differential_order, + ) self.signal._replot() @add_gui_method(toolkey="hyperspy.Signal1D.smooth_lowess") class SmoothingLowess(Smoothing): - smoothing_parameter = t.Range(low=0.001, - high=0.99, - value=0.1, - ) - number_of_iterations = t.Range(low=1, - value=1) + smoothing_parameter = t.Range( + low=0.001, + high=0.99, + value=0.1, + ) + number_of_iterations = t.Range(low=1, value=1) def __init__(self, *args, **kwargs): - super(SmoothingLowess, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _smoothing_parameter_changed(self, old, new): if new == 0: @@ -687,18 +688,20 @@ def _number_of_iterations_changed(self, old, new): self.update_lines() def model2plot(self, axes_manager=None): - self.single_spectrum.data = self.signal().copy() + self.single_spectrum.data = self.signal._get_current_data().copy() self.single_spectrum.smooth_lowess( smoothing_parameter=self.smoothing_parameter, number_of_iterations=self.number_of_iterations, - show_progressbar=False) + show_progressbar=False, + ) return self.single_spectrum.data def apply(self): self.signal.smooth_lowess( smoothing_parameter=self.smoothing_parameter, - number_of_iterations=self.number_of_iterations) + number_of_iterations=self.number_of_iterations, + ) self.signal._replot() @@ -710,23 +713,22 @@ def _smoothing_parameter_changed(self, old, new): self.update_lines() def model2plot(self, axes_manager=None): - self.single_spectrum.data = self.signal().copy() + self.single_spectrum.data = self.signal._get_current_data().copy() self.single_spectrum.smooth_tv( - smoothing_parameter=self.smoothing_parameter, - show_progressbar=False) + smoothing_parameter=self.smoothing_parameter, show_progressbar=False + ) return self.single_spectrum.data def apply(self): - self.signal.smooth_tv( - smoothing_parameter=self.smoothing_parameter) + self.signal.smooth_tv(smoothing_parameter=self.smoothing_parameter) self.signal._replot() @add_gui_method(toolkey="hyperspy.Signal1D.smooth_butterworth") class ButterworthFilter(Smoothing): - cutoff_frequency_ratio = t.Range(0.01, 1., 0.01) - type = t.Enum('low', 'high') + cutoff_frequency_ratio = t.Range(0.01, 1.0, 0.01) + type = t.Enum("low", "high") order = t.Int(2) def _cutoff_frequency_ratio_changed(self, old, new): @@ -739,14 +741,12 @@ def _order_changed(self, old, new): self.update_lines() def model2plot(self, axes_manager=None): - b, a = sp_signal.butter(self.order, self.cutoff_frequency_ratio, - self.type) - smoothed = sp_signal.filtfilt(b, a, self.signal()) + b, a = sp_signal.butter(self.order, self.cutoff_frequency_ratio, self.type) + smoothed = sp_signal.filtfilt(b, a, self.signal._get_current_data()) return smoothed def apply(self): - b, a = sp_signal.butter(self.order, self.cutoff_frequency_ratio, - self.type) + b, a = sp_signal.butter(self.order, self.cutoff_frequency_ratio, self.type) f = functools.partial(sp_signal.filtfilt, b, a) self.signal.map(f) @@ -761,48 +761,60 @@ class ImageContrastEditor(t.HasTraits): mpl_help = "See the matplotlib SymLogNorm for more information." ss_left_value = t.Float() ss_right_value = t.Float() - bins = t.Int(100, desc="Number of bins used for the histogram.") + bins = t.Int( + 100, + desc="Number of bins used for the histogram.", + auto_set=False, + enter_set=True, + ) gamma = t.Range(0.1, 3.0, 1.0) - vmin_percentile = t.Range(0.0, 100.0, 0) - vmax_percentile = t.Range(0.0, 100.0, 100) - - norm = t.Enum( - 'Linear', - 'Power', - 'Log', - 'Symlog', - default='Linear') - linthresh = t.Range(0.0, 1.0, 0.01, exclude_low=True, exclude_high=False, - desc="Range of value closed to zero, which are " - f"linearly extrapolated. {mpl_help}") - linscale = t.Range(0.0, 10.0, 0.1, exclude_low=False, exclude_high=False, - desc="Number of decades to use for each half of " - f"the linear range. {mpl_help}") - auto = t.Bool(True, - desc="Adjust automatically the display when changing " - "navigator indices. Unselect to keep the same display.") + percentile_range = t.Range(0.0, 100.0) + vmin_percentile = t.Float(0.0) + vmax_percentile = t.Float(100.0) + + norm = t.Enum("Linear", "Power", "Log", "Symlog", default="Linear") + linthresh = t.Range( + 0.0, + 1.0, + 0.01, + exclude_low=True, + exclude_high=False, + desc="Range of value closed to zero, which are " + f"linearly extrapolated. {mpl_help}", + ) + linscale = t.Range( + 0.0, + 10.0, + 0.1, + exclude_low=False, + exclude_high=False, + desc="Number of decades to use for each half of " + f"the linear range. {mpl_help}", + ) + auto = t.Bool( + True, + desc="Adjust automatically the display when changing " + "navigator indices. Unselect to keep the same display.", + ) def __init__(self, image): - super(ImageContrastEditor, self).__init__() + super().__init__() self.image = image - self.hspy_fig = BlittedFigure() - self.hspy_fig.create_figure() - self.create_axis() + self._init_plot() # self._vmin and self._vmax are used to compute the histogram - # by default, the image display used these, except when there is a span - # selector on the histogram + # by default, the image display uses these, except when there is a + # span selector on the histogram. This is implemented in the + # `_get_current_range` method. self._vmin, self._vmax = self.image._vmin, self.image._vmax self.gamma = self.image.gamma self.linthresh = self.image.linthresh self.linscale = self.image.linscale if self.image._vmin_percentile is not None: - self.vmin_percentile = float( - self.image._vmin_percentile.split('th')[0]) + self.vmin_percentile = float(self.image._vmin_percentile.split("th")[0]) if self.image._vmax_percentile is not None: - self.vmax_percentile = float( - self.image._vmax_percentile.split('th')[0]) + self.vmax_percentile = float(self.image._vmax_percentile.split("th")[0]) # Copy the original value to be used when resetting the display self.vmin_original = self._vmin @@ -813,127 +825,106 @@ def __init__(self, image): self.vmin_percentile_original = self.vmin_percentile self.vmax_percentile_original = self.vmax_percentile - if self.image.norm == 'auto': - self.norm = 'Linear' + if self.image.norm == "auto": + self.norm = "Linear" else: self.norm = self.image.norm.capitalize() self.norm_original = copy.deepcopy(self.norm) - self.span_selector = None - self.plot_histogram() + self.span_selector = SpanSelector( + self.ax, + onselect=self._update_image_contrast, + onmove_callback=self._update_image_contrast, + direction="horizontal", + interactive=True, + ignore_event_outside=False, + drag_from_anywhere=True, + props={"alpha": 0.25, "color": "r"}, + handle_props={"alpha": 0.5, "color": "r"}, + useblit=self.ax.figure.canvas.supports_blit, + ) - # After the figure have been rendered to follow the same pattern as - # for other tools - self.span_selector_switch(on=True) + self.plot_histogram() if self.image.axes_manager is not None: - self.image.axes_manager.events.indices_changed.connect( - self._reset, []) + self.image.axes_manager.events.indices_changed.connect(self._reset, []) self.hspy_fig.events.closed.connect( lambda: self.image.axes_manager.events.indices_changed.disconnect( - self._reset), []) + self._reset + ), + [], + ) # Disconnect update image to avoid image flickering and reconnect # it when necessary in the close method. self.image.disconnect() - def create_axis(self): - self.ax = self.hspy_fig.figure.add_subplot(111) - self.hspy_fig.ax = self.ax + def _init_plot(self): + figsize = matplotlib.rcParamsDefault.get("figure.figsize") + figsize = figsize[0], figsize[1] / 3 + self.hspy_fig = Signal1DFigure(figsize=figsize) + self.ax = self.hspy_fig.ax + self.ax.set_xticks([]) + self.ax.set_yticks([]) + self.ax.figure.subplots_adjust(0, 0, 1, 1) def _gamma_changed(self, old, new): if self._vmin == self._vmax: return self.image.gamma = new - if hasattr(self, "hist"): - vmin, vmax = self._get_current_range() - self.image.update( - data_changed=False, auto_contrast=False, vmin=vmin, vmax=vmax) - self.update_line() + self._reset(auto=False, indices_changed=False, update_histogram=False) + self.update_line() def _vmin_percentile_changed(self, old, new): if isinstance(new, str): - new = float(new.split('th')[0]) + new = float(new.split("th")[0]) self.image.vmin = f"{new}th" - # Before the tool is fully initialised - if hasattr(self, "hist"): - self._reset(indices_changed=False) - self._reset_span_selector() + self._reset(auto=True, indices_changed=False) + self._clear_span_selector() def _vmax_percentile_changed(self, old, new): if isinstance(new, str): - new = float(new.split('th')[0]) + new = float(new.split("th")[0]) self.image.vmax = f"{new}th" - # Before the tool is fully initialised - if hasattr(self, "hist"): - self._reset(indices_changed=False) - self._reset_span_selector() + self._reset(auto=True, indices_changed=False) + self._clear_span_selector() def _auto_changed(self, old, new): # Do something only if auto is ticked - if new and hasattr(self, "hist"): - self._reset(indices_changed=False) - self._reset_span_selector() + if new: + self._reset(indices_changed=False, update_histogram=False) + self._clear_span_selector() + + def _bins_changed(self, old, new): + if old != new: + self.update_histogram(clear_selector=False) def _norm_changed(self, old, new): - if hasattr(self, "hist"): - self.image.norm = new.lower() - self._reset(indices_changed=False) + self.image.norm = new.lower() + self._reset(auto=False, indices_changed=False, update_histogram=False) + self.update_line() def _linthresh_changed(self, old, new): self.image.linthresh = new - if hasattr(self, "hist"): - self._reset(indices_changed=False) + self._reset(auto=False, indices_changed=False, update_histogram=False) def _linscale_changed(self, old, new): self.image.linscale = new - if hasattr(self, "hist"): - self._reset(indices_changed=False) - - def span_selector_switch(self, on): - if on is True: - self.span_selector = \ - drawing.widgets.ModifiableSpanSelector( - self.ax, - onselect=self.update_span_selector, - onmove_callback=self.update_span_selector, - rectprops={"alpha":0.25, "color":'r'}) - self.span_selector.bounds_check = True - - elif self.span_selector is not None: - self.span_selector.turn_off() - self.span_selector = None + self._reset(auto=False, indices_changed=False, update_histogram=False) def update_span_selector_traits(self, *args, **kwargs): - self.ss_left_value = self.span_selector.rect.get_x() - self.ss_right_value = self.ss_left_value + \ - self.span_selector.rect.get_width() - + self.ss_left_value, self.ss_right_value = sorted(self._get_current_range()) self.update_line() - def update_span_selector(self, *args, **kwargs): - self.update_span_selector_traits() - # switch off auto when using span selector - if self.auto: - self.auto = False - vmin, vmax = self._get_current_range() - self.image.update(data_changed=False, auto_contrast=False, - vmin=vmin, vmax=vmax) + def _update_image_contrast(self, *args, **kwargs): + self.update_span_selector_traits(*args, **kwargs) + self._reset(auto=False, indices_changed=False, update_histogram=False) def _get_data(self): return self.image._current_data def _get_histogram(self, data): - return numba_histogram(data, bins=self.bins, - ranges=(self._vmin, self._vmax)) - - def _set_xaxis(self): - self.xaxis = np.linspace(self._vmin, self._vmax, self.bins) - if self.span_selector is not None: - # Set this attribute to restrict the span selector to the xaxis - self.span_selector.step_ax = UniformDataAxis(size=len(self.xaxis), - offset=self.xaxis[1], - scale=self.xaxis[1]-self.xaxis[0]) + return numba_histogram(data, bins=self.bins, ranges=(self._vmin, self._vmax)) def plot_histogram(self, max_num_bins=250): """Plot a histogram of the data. @@ -954,7 +945,7 @@ def plot_histogram(self, max_num_bins=250): data = np.ma.masked_outside(data, self._vmin, self._vmax).compressed() # Sturges rule - sturges_bin_width = data.ptp() / (np.log2(data.size) + 1.0) + sturges_bin_width = np.ptp(data) / (np.log2(data.size) + 1.0) iqr = np.subtract(*np.percentile(data, [75, 25])) fd_bin_width = 2.0 * iqr * data.size ** (-1.0 / 3.0) @@ -965,64 +956,88 @@ def plot_histogram(self, max_num_bins=250): # limited variance: fd_bin_width may be zero bin_width = sturges_bin_width - self.bins = min(int(np.ceil(data.ptp() / bin_width)), max_num_bins) - - self.hist_data = self._get_histogram(data) - self._set_xaxis() - self.hist = self.ax.fill_between(self.xaxis, self.hist_data, - step="mid") - self.ax.set_xlim(self._vmin, self._vmax) - self.ax.set_ylim(0, self.hist_data.max()) - self.ax.set_xticks([]) - self.ax.set_yticks([]) - self.line = self.ax.plot(*self._get_line(), color='#ff7f0e', - animated=self.ax.figure.canvas.supports_blit)[0] - plt.tight_layout(pad=0) - self.ax.figure.canvas.draw() + self.bins = min(int(np.ceil(np.ptp(data) / bin_width)), max_num_bins) + self.update_histogram() + self._setup_line() plot_histogram.__doc__ %= HISTOGRAM_MAX_BIN_ARGS - def update_histogram(self): + def update_histogram(self, clear_selector=True): if self._vmin == self._vmax: return - color = self.hist.get_facecolor() - self.hist.remove() + + if hasattr(self, "hist"): + self.hist.remove() + + self.xaxis = UniformDataAxis( + scale=(self._vmax - self._vmin) / self.bins, + offset=self._vmin, + size=self.bins, + ) self.hist_data = self._get_histogram(self._get_data()) - self.hist = self.ax.fill_between(self.xaxis, self.hist_data, - step="mid", color=color) + + # We don't use blitting for the histogram because it will be part + # included in the background + self.hist = self.ax.fill_between( + self.xaxis.axis, + self.hist_data, + step="mid", + color="C0", + ) self.ax.set_xlim(self._vmin, self._vmax) if self.hist_data.max() != 0: self.ax.set_ylim(0, self.hist_data.max()) + + if self.auto and self._is_selector_visible and clear_selector: + # in auto mode, the displayed contrast cover the full range + # and we need to reset the span selector + # no need to clear the line, it will updated + self.span_selector.clear() + self.update_line() - self.ax.figure.canvas.draw_idle() - def _get_line(self): + self.ax.figure.canvas.draw() + + def _setup_line(self): + self.hspy_fig.axis = self.xaxis + self.line = drawing.signal1d.Signal1DLine() + self.line.data_function = self._get_data_function + self.line.set_line_properties(color="C1", type="line") + # Add the line to the figure + self.hspy_fig.add_line(self.line) + self.line.plot() + + def _set_xaxis_line(self): cmin, cmax = self._get_current_range() - xaxis = np.linspace(cmin, cmax, self.bins) + self.line.axis = np.linspace(cmin, cmax, self.bins) + + def _get_data_function(self, *args, **kwargs): + xaxis = self.xaxis.axis + cmin, cmax = xaxis[0], xaxis[-1] max_hist = self.hist_data.max() if self.image.norm == "linear": - values = ((xaxis-cmin)/(cmax-cmin)) * max_hist + values = ((xaxis - cmin) / (cmax - cmin)) * max_hist elif self.image.norm == "symlog": v = self._sym_log_transform(xaxis) - values = (v-v[0]) / (v[-1]-v[0]) * max_hist + values = (v - v[0]) / (v[-1] - v[0]) * max_hist elif self.image.norm == "log": v = np.log(xaxis) - values = (v-v[0]) / (v[-1]-v[0]) * max_hist + values = (v - v[0]) / (v[-1] - v[0]) * max_hist else: # if "auto" or "power" use the self.gamma value - values = ((xaxis-cmin)/(cmax-cmin)) ** self.gamma * max_hist + values = ((xaxis - cmin) / (cmax - cmin)) ** self.gamma * max_hist - return xaxis, values + return values def _sym_log_transform(self, arr): # adapted from matploltib.colors.SymLogNorm arr = arr.copy() - _linscale_adj = (self.linscale / (1.0 - np.e ** -1)) + _linscale_adj = self.linscale / (1.0 - np.e**-1) with np.errstate(invalid="ignore"): - masked = np.abs(arr) > self.linthresh + masked = abs(arr) > self.linthresh sign = np.sign(arr[masked]) - log = (_linscale_adj + np.log(np.abs(arr[masked]) / self.linthresh)) + log = _linscale_adj + np.log(abs(arr[masked]) / self.linthresh) log *= sign * self.linthresh arr[masked] = log arr[~masked] *= _linscale_adj @@ -1030,10 +1045,13 @@ def _sym_log_transform(self, arr): return arr def update_line(self): - if self._vmin == self._vmax: + if not hasattr(self, "line") or self._vmin == self._vmax: return - self.line.set_data(*self._get_line()) - self.hspy_fig.render_figure() + self._set_xaxis_line() + self.line.update(render_figure=True) + if not self.line.line.get_visible(): + # when the selector have been cleared, line is not visible anymore + self.line.line.set_visible(True) def apply(self): if self.ss_left_value == self.ss_right_value: @@ -1043,21 +1061,14 @@ def apply(self): # When we apply the selected range and update the xaxis self._vmin, self._vmax = self._get_current_range() # Remove the span selector and set the new one ready to use - self.span_selector_switch(False) - self.span_selector_switch(True) + self._clear_span_selector() self._reset(auto=False, indices_changed=False) def reset(self): # Reset the display as original self._reset_original_settings() - self._reset_span_selector() - - def _reset_span_selector(self): - if self.span_selector and self.span_selector.rect.get_x() > 0: - # Remove the span selector and set the new one ready to use - self.span_selector_switch(False) - self.span_selector_switch(True) - self._reset(indices_changed=False) + self._clear_span_selector() + self._reset(indices_changed=False) def _reset_original_settings(self): if self.vmin_percentile_original is not None: @@ -1071,10 +1082,17 @@ def _reset_original_settings(self): self.linthresh = self.linthresh_original self.linscale = self.linscale_original + @property + def _is_selector_visible(self): + if hasattr(self, "span_selector"): + return self.span_selector.artists[0].get_visible() + def _get_current_range(self): - if self.span_selector and self.span_selector._get_span_width() != 0: + # Get the range from the span selector if it is displayed otherwise + # fallback to the _vmin/_vmax cache values + if self._is_selector_visible and np.diff(self.span_selector.extents) > 0: # if we have a span selector, use it to set the display - return self.ss_left_value, self.ss_right_value + return self.span_selector.extents else: return self._vmin, self._vmax @@ -1089,7 +1107,7 @@ def close(self): self.image.connect() self.hspy_fig.close() - def _reset(self, auto=None, indices_changed=True): + def _reset(self, auto=None, indices_changed=True, update_histogram=True): # indices_changed is used for the connection to the indices_changed # event of the axes_manager, which will require to update the displayed # image @@ -1098,26 +1116,34 @@ def _reset(self, auto=None, indices_changed=True): auto = self.auto if auto: + # Update the image display, which calculates the _vmin/_vmax self.image.update(data_changed=indices_changed, auto_contrast=auto) self._vmin, self._vmax = self.image._vmin, self.image._vmax - self._set_xaxis() else: vmin, vmax = self._get_current_range() - self.image.update(data_changed=indices_changed, auto_contrast=auto, - vmin=vmin, vmax=vmax) + self.image.update( + data_changed=indices_changed, auto_contrast=auto, vmin=vmin, vmax=vmax + ) - self.update_histogram() - self.update_span_selector_traits() + if update_histogram and hasattr(self, "hist"): + self.update_histogram() + self.update_span_selector_traits() + + def _clear_span_selector(self): + if hasattr(self, "span_selector"): + self.span_selector.clear() + if hasattr(self, "line"): + self.line.line.set_visible(False) + self.hspy_fig.render_figure() def _show_help_fired(self): from pyface.message_dialog import information - _help = _IMAGE_CONTRAST_EDITOR_HELP.replace("PERCENTILE", - _PERCENTILE_TRAITSUI) - _ = information(None, _help, title="Help"), + _help = _IMAGE_CONTRAST_EDITOR_HELP.replace("PERCENTILE", _PERCENTILE_TRAITSUI) + _ = (information(None, _help, title="Help"),) -_IMAGE_CONTRAST_EDITOR_HELP = \ -""" + +_IMAGE_CONTRAST_EDITOR_HELP = """

    Image contrast editor

    This tool provides controls to adjust the contrast of the image.

    @@ -1159,93 +1185,69 @@ def _show_help_fired(self): """ -_PERCENTILE_TRAITSUI = \ -"""

    vmin percentile: The percentile value defining the number of +_PERCENTILE_TRAITSUI = """

    vmin percentile: The percentile value defining the number of pixels left out of the lower bounds.

    vmax percentile: The percentile value defining the number of pixels left out of the upper bounds.

    """ -_PERCENTILE_IPYWIDGETS = \ -"""

    vmin/vmax percentile: The percentile values defining the number of +_PERCENTILE_IPYWIDGETS = """

    vmin/vmax percentile: The percentile values defining the number of pixels left out of the lower and upper bounds.

    """ IMAGE_CONTRAST_EDITOR_HELP_IPYWIDGETS = _IMAGE_CONTRAST_EDITOR_HELP.replace( - "PERCENTILE", _PERCENTILE_IPYWIDGETS) - - -@add_gui_method(toolkey="hyperspy.Signal1D.integrate_in_range") -class IntegrateArea(SpanSelectorInSignal1D): - integrate = t.Button() - - def __init__(self, signal, signal_range=None): - if signal.axes_manager.signal_dimension != 1: - raise SignalDimensionError( - signal.axes.signal_dimension, 1) - - self.signal = signal - self.axis = self.signal.axes_manager.signal_axes[0] - self.span_selector = None - if (not hasattr(self.signal, '_plot') or self.signal._plot is None or - not self.signal._plot.is_active): - self.signal.plot() - self.span_selector_switch(on=True) - - def apply(self): - integrated_spectrum = self.signal._integrate_in_range_commandline( - signal_range=( - self.ss_left_value, - self.ss_right_value) - ) - # Replaces the original signal inplace with the new integrated spectrum - plot = False - if self.signal._plot and integrated_spectrum.axes_manager.shape != (): - self.signal._plot.close() - plot = True - self.signal.__init__(**integrated_spectrum._to_dictionary()) - self.signal._assign_subclass() - self.signal.axes_manager.set_signal_dimension(0) - - if plot is True: - self.signal.plot() + "PERCENTILE", _PERCENTILE_IPYWIDGETS +) @add_gui_method(toolkey="hyperspy.Signal1D.remove_background") class BackgroundRemoval(SpanSelectorInSignal1D): background_type = t.Enum( - 'Doniach', - 'Exponential', - 'Gaussian', - 'Lorentzian', - 'Offset', - 'Polynomial', - 'Power law', - 'Skew normal', - 'Split Voigt', - 'Voigt', - default='Power law') + "Doniach", + "Exponential", + "Gaussian", + "Lorentzian", + "Offset", + "Polynomial", + "Power law", + "Skew normal", + "Split Voigt", + "Voigt", + default="Power law", + ) polynomial_order = t.Range(1, 10) - fast = t.Bool(True, - desc=("Perform a fast (analytic, but possibly less accurate)" - " estimation of the background. Otherwise use " - "non-linear least squares.")) + fast = t.Bool( + True, + desc=( + "Perform a fast (analytic, but possibly less accurate)" + " estimation of the background. Otherwise use " + "non-linear least squares." + ), + ) zero_fill = t.Bool( False, - desc=("Set all spectral channels lower than the lower \n" - "bound of the fitting range to zero (this is the \n" - "default behavior of Gatan's DigitalMicrograph). \n" - "Otherwise leave the pre-fitting region as-is \n" - "(useful for inspecting quality of background fit).")) + desc=( + "Set all spectral channels lower than the lower \n" + "bound of the fitting range to zero (this is the \n" + "default behavior of Gatan's DigitalMicrograph). \n" + "Otherwise leave the pre-fitting region as-is \n" + "(useful for inspecting quality of background fit)." + ), + ) background_estimator = t.Instance(Component) - bg_line_range = t.Enum('from_left_range', - 'full', - 'ss_range', - default='full') + bg_line_range = t.Enum("from_left_range", "full", "ss_range", default="full") red_chisq = t.Float(np.nan) - def __init__(self, signal, background_type='Power law', polynomial_order=2, - fast=True, plot_remainder=True, zero_fill=False, - show_progressbar=None, model=None): + def __init__( + self, + signal, + background_type="Power law", + polynomial_order=2, + fast=True, + plot_remainder=True, + zero_fill=False, + show_progressbar=None, + model=None, + ): super().__init__(signal) # setting the polynomial order will change the backgroud_type to # polynomial, so we set it before setting the background type @@ -1262,29 +1264,24 @@ def __init__(self, signal, background_type='Power law', polynomial_order=2, figure.tight_layout(rect=[0, 0, 0.95, 1]) if model is None: from hyperspy.models.model1d import Model1D + model = Model1D(signal) self.model = model self.polynomial_order = polynomial_order - if background_type in ['Power Law', 'PowerLaw']: - background_type = 'Power law' - if background_type in ['Skew Normal', 'SkewNormal']: - background_type = 'Skew normal' - if background_type in ['Split voigt', 'SplitVoigt']: - background_type = 'Split Voigt' + if background_type in ["Power Law", "PowerLaw"]: + background_type = "Power law" + if background_type in ["Skew Normal", "SkewNormal"]: + background_type = "Skew normal" + if background_type in ["Split voigt", "SplitVoigt"]: + background_type = "Split Voigt" self.background_type = background_type self.zero_fill = zero_fill self.show_progressbar = show_progressbar self.set_background_estimator() - self.signal.axes_manager.events.indices_changed.connect(self._fit, []) - # This is also disconnected when disabling the span selector but we - # disconnect also when closing the figure, because in this case, - # `on_disabling_span_selector` will not be called. - self.signal._plot.signal_plot.events.closed.connect(self.disconnect, []) - def on_disabling_span_selector(self): # Disconnect event - self.disconnect() + super().on_disabling_span_selector() if self.bg_line is not None: self.bg_line.close() self.bg_line = None @@ -1298,14 +1295,14 @@ def set_background_estimator(self): for component in self.model: self.model.remove(component) self.background_estimator, self.bg_line_range = _get_background_estimator( - self.background_type, self.polynomial_order) + self.background_type, self.polynomial_order + ) if self.model is not None and len(self.model) == 0: self.model.append(self.background_estimator) - if not self.fast and self.is_span_selector_valid: + if not self.fast and self._is_valid_range: self.background_estimator.estimate_parameters( - self.signal, self.ss_left_value, - self.ss_right_value, - only_current=True) + self.signal, self.ss_left_value, self.ss_right_value, only_current=True + ) def _polynomial_order_changed(self, old, new): self.set_background_estimator() @@ -1316,64 +1313,51 @@ def _background_type_changed(self, old, new): self.span_selector_changed() def _fast_changed(self, old, new): - if self.span_selector is None or not self.is_span_selector_valid: + if not self._is_valid_range: return self._fit() self._update_line() - def _ss_left_value_changed(self, old, new): - if not (np.isnan(self.ss_right_value) or np.isnan(self.ss_left_value)): - self.span_selector_changed() - - def _ss_right_value_changed(self, old, new): - if not (np.isnan(self.ss_right_value) or np.isnan(self.ss_left_value)): - self.span_selector_changed() - def create_background_line(self): self.bg_line = drawing.signal1d.Signal1DLine() self.bg_line.data_function = self.bg_to_plot - self.bg_line.set_line_properties( - color='blue', - type='line', - scaley=False) + self.bg_line.set_line_properties(color="blue", type="line", scaley=False) self.signal._plot.signal_plot.add_line(self.bg_line) - self.bg_line.autoscale = '' + self.bg_line.autoscale = "" self.bg_line.plot() def create_remainder_line(self): self.rm_line = drawing.signal1d.Signal1DLine() self.rm_line.data_function = self.rm_to_plot - self.rm_line.set_line_properties( - color='green', - type='line', - scaley=False) - self.signal._plot.signal_plot.create_right_axis(color='green', - adjust_layout=False) - self.signal._plot.signal_plot.add_line(self.rm_line, ax='right') + self.rm_line.set_line_properties(color="green", type="line", scaley=False) + self.signal._plot.signal_plot.create_right_axis( + color="green", adjust_layout=False + ) + self.signal._plot.signal_plot.add_line(self.rm_line, ax="right") self.rm_line.plot() def bg_to_plot(self, axes_manager=None, fill_with=np.nan): - if self.bg_line_range == 'from_left_range': + if self.bg_line_range == "from_left_range": bg_array = np.zeros(self.axis.axis.shape) bg_array[:] = fill_with from_index = self.axis.value2index(self.ss_left_value) bg_array[from_index:] = self.background_estimator.function( - self.axis.axis[from_index:]) + self.axis.axis[from_index:] + ) to_return = bg_array - elif self.bg_line_range == 'full': + elif self.bg_line_range == "full": to_return = self.background_estimator.function(self.axis.axis) - elif self.bg_line_range == 'ss_range': + elif self.bg_line_range == "ss_range": bg_array = np.zeros(self.axis.axis.shape) bg_array[:] = fill_with from_index = self.axis.value2index(self.ss_left_value) to_index = self.axis.value2index(self.ss_right_value) bg_array[from_index:] = self.background_estimator.function( - self.axis.axis[from_index:to_index]) + self.axis.axis[from_index:to_index] + ) to_return = bg_array - if is_binned(self.signal) is True: - # in v2 replace by - #if self.axis.is_binned is True: + if self.axis.is_binned: if self.axis.is_uniform: to_return *= self.axis.scale else: @@ -1381,51 +1365,51 @@ def bg_to_plot(self, axes_manager=None, fill_with=np.nan): return to_return def rm_to_plot(self, axes_manager=None, fill_with=np.nan): - return self.signal() - self.bg_line.line.get_ydata() + return self.signal._get_current_data() - self.bg_line.line.get_ydata() - def span_selector_changed(self): - if not self.is_span_selector_valid: + def span_selector_changed(self, *args, **kwargs): + super().span_selector_changed() + if not self._is_valid_range: return try: self._fit() self._update_line() - except: + except Exception: pass def _fit(self): - if not self.is_span_selector_valid: + if not self._is_valid_range: return - # Set signal range here to set correctly the channel_switches for + # Set signal range here to set correctly the _channel_switches for # the chisq calculation when using fast self.model.set_signal_range(self.ss_left_value, self.ss_right_value) if self.fast: self.background_estimator.estimate_parameters( - self.signal, self.ss_left_value, - self.ss_right_value, - only_current=True) + self.signal, self.ss_left_value, self.ss_right_value, only_current=True + ) # Calculate chisq self.model._calculate_chisq() else: self.model.fit() - self.red_chisq = self.model.red_chisq.data[ - self.model.axes_manager.indices] + self.red_chisq = self.model.red_chisq.data[self.model.axes_manager.indices][0] def _update_line(self): if self.bg_line is None: self.create_background_line() else: - self.bg_line.update(render_figure=False, update_ylimits=False) + self.bg_line.update( + render_figure=not self.plot_remainder, update_ylimits=False + ) if self.plot_remainder: if self.rm_line is None: self.create_remainder_line() else: - self.rm_line.update(render_figure=True, - update_ylimits=True) + self.rm_line.update(render_figure=True, update_ylimits=True) def apply(self): - if not self.is_span_selector_valid: + if not self._is_valid_range: return - return_model = (self.model is not None) + return_model = self.model is not None result = self.signal._remove_background_cli( signal_range=(self.ss_left_value, self.ss_right_value), background_estimator=self.background_estimator, @@ -1433,12 +1417,14 @@ def apply(self): zero_fill=self.zero_fill, show_progressbar=self.show_progressbar, model=self.model, - return_model=return_model) + return_model=return_model, + ) new_spectra = result[0] if return_model else result self.signal.data = new_spectra.data self.signal.events.data_changed.trigger(self) def disconnect(self): + super().disconnect() axes_manager = self.signal.axes_manager for f in [self._fit, self.model._on_navigating]: if f in axes_manager.events.indices_changed.connected: @@ -1469,40 +1455,37 @@ def _get_background_estimator(background_type, polynomial_order=1): The range to draw the component (used in the BackgroundRemoval tool) """ - background_type = background_type.lower().replace(' ', '') - if background_type == 'doniach': + background_type = background_type.lower().replace(" ", "") + if background_type == "doniach": background_estimator = components1d.Doniach() - bg_line_range = 'full' - elif background_type == 'gaussian': + bg_line_range = "full" + elif background_type == "gaussian": background_estimator = components1d.Gaussian() - bg_line_range = 'full' - elif background_type == 'lorentzian': + bg_line_range = "full" + elif background_type == "lorentzian": background_estimator = components1d.Lorentzian() - bg_line_range = 'full' - elif background_type == 'offset': + bg_line_range = "full" + elif background_type == "offset": background_estimator = components1d.Offset() - bg_line_range = 'full' - elif background_type == 'polynomial': - with ignore_warning(message="The API of the `Polynomial` component"): - background_estimator = components1d.Polynomial( - order=polynomial_order, legacy=False) - bg_line_range = 'full' - elif background_type == 'powerlaw': + bg_line_range = "full" + elif background_type == "polynomial": + background_estimator = components1d.Polynomial(order=polynomial_order) + bg_line_range = "full" + elif background_type == "powerlaw": background_estimator = components1d.PowerLaw() - bg_line_range = 'from_left_range' - elif background_type == 'exponential': + bg_line_range = "from_left_range" + elif background_type == "exponential": background_estimator = components1d.Exponential() - bg_line_range = 'from_left_range' - elif background_type == 'skewnormal': + bg_line_range = "from_left_range" + elif background_type == "skewnormal": background_estimator = components1d.SkewNormal() - bg_line_range = 'full' - elif background_type == 'splitvoigt': + bg_line_range = "full" + elif background_type == "splitvoigt": background_estimator = components1d.SplitVoigt() - bg_line_range = 'full' - elif background_type == 'voigt': - with ignore_warning(message="The API of the `Voigt` component"): - background_estimator = components1d.Voigt(legacy=False) - bg_line_range = 'full' + bg_line_range = "full" + elif background_type == "voigt": + background_estimator = components1d.Voigt() + bg_line_range = "full" else: raise ValueError(f"Background type '{background_type}' not recognized.") @@ -1511,34 +1494,32 @@ def _get_background_estimator(background_type, polynomial_order=1): SPIKES_REMOVAL_INSTRUCTIONS = ( "To remove spikes from the data:\n\n" - - " 1. Click \"Show derivative histogram\" to " + ' 1. Click "Show derivative histogram" to ' "determine at what magnitude the spikes are present.\n" " 2. Enter a suitable threshold (lower than the " "lowest magnitude outlier in the histogram) in the " - "\"Threshold\" box, which will be the magnitude " + '"Threshold" box, which will be the magnitude ' "from which to search. \n" - " 3. Click \"Find next\" to find the first spike.\n" + ' 3. Click "Find next" to find the first spike.\n' " 4. If desired, the width and position of the " "boundaries used to replace the spike can be " "adjusted by clicking and dragging on the displayed " "plot.\n " " 5. View the spike (and the replacement data that " - "will be added) and click \"Remove spike\" in order " + 'will be added) and click "Remove spike" in order ' "to alter the data as shown. The tool will " "automatically find the next spike to replace.\n" " 6. Repeat this process for each spike throughout " "the dataset, until the end of the dataset is " "reached.\n" - " 7. Click \"OK\" when finished to close the spikes " + ' 7. Click "OK" when finished to close the spikes ' "removal tool.\n\n" - "Note: Various settings can be configured in " - "the \"Advanced settings\" section. Hover the " + 'the "Advanced settings" section. Hover the ' "mouse over each parameter for a description of what " "it does." - - "\n") + "\n" +) @add_gui_method(toolkey="hyperspy.SimpleMessage") @@ -1550,10 +1531,17 @@ def __init__(self, text=""): class SpikesRemoval: - - def __init__(self, signal, navigation_mask=None, signal_mask=None, - threshold='auto', default_spike_width=5, add_noise=True, - max_num_bins=1000): + def __init__( + self, + signal, + navigation_mask=None, + signal_mask=None, + threshold="auto", + default_spike_width=5, + add_noise=True, + max_num_bins=1000, + random_state=None, + ): self.ss_left_value = np.nan self.ss_right_value = np.nan self.default_spike_width = default_spike_width @@ -1561,36 +1549,42 @@ def __init__(self, signal, navigation_mask=None, signal_mask=None, self.signal_mask = signal_mask self.navigation_mask = navigation_mask self.interpolated_line = None - self.coordinates = [coordinate for coordinate in - signal.axes_manager._am_indices_generator() - if (navigation_mask is None or not - navigation_mask[coordinate[::-1]])] + self.coordinates = [ + coordinate + for coordinate in signal.axes_manager._am_indices_generator() + if (navigation_mask is None or not navigation_mask[coordinate[::-1]]) + ] self.signal = signal self.axis = self.signal.axes_manager.signal_axes[0] if len(self.coordinates) > 1: signal.axes_manager.indices = self.coordinates[0] - if threshold == 'auto': + if threshold == "auto": # Find the first zero of the spikes diagnosis plot - hist = signal._get_spikes_diagnosis_histogram_data( - signal_mask=signal_mask, - navigation_mask=navigation_mask, - max_num_bins=max_num_bins) + hist = signal._spikes_diagnosis( + signal_mask=signal_mask, + navigation_mask=navigation_mask, + max_num_bins=max_num_bins, + show_plot=False, + use_gui=False, + ) zero_index = np.where(hist.data == 0)[0] if zero_index.shape[0] > 0: index = zero_index[0] else: index = hist.data.shape[0] - 1 threshold = np.ceil(hist.axes_manager[0].index2value(index)) - _logger.info(f'Threshold value: {threshold}') + _logger.info(f"Threshold value: {threshold}") self.argmax = None self.derivmax = None - self.kind = "linear" - self._temp_mask = np.zeros(self.signal().shape, dtype='bool') + self.spline_order = 1 + self._temp_mask = np.zeros(self.signal._get_current_data().shape, dtype="bool") self.index = 0 self.threshold = threshold md = self.signal.metadata from hyperspy.signal import BaseSignal + self._rng = check_random_state(random_state) + if "Signal.Noise_properties" in md: if "Signal.Noise_properties.variance" in md: self.noise_variance = md.Signal.Noise_properties.variance @@ -1605,14 +1599,14 @@ def __init__(self, signal, navigation_mask=None, signal_mask=None, def detect_spike(self): axis = self.signal.axes_manager.signal_axes[-1].axis - derivative = np.gradient(self.signal(), axis) + derivative = np.gradient(self.signal._get_current_data(), axis) if self.signal_mask is not None: derivative[self.signal_mask] = 0 if self.argmax is not None: left, right = self.get_interpolation_range() # Don't search for spikes in the are where one has # been found next time `find` is called. - self._temp_mask[left:right + 1] = True + self._temp_mask[left : right + 1] = True derivative[self._temp_mask] = 0 if abs(derivative.max()) >= self.threshold: self.argmax = derivative.argmax() @@ -1626,8 +1620,9 @@ def find(self, back=False): spike = self.detect_spike() with self.signal.axes_manager.events.indices_changed.suppress(): while not spike and ( - (self.index < ncoordinates - 1 and back is False) or - (self.index > 0 and back is True)): + (self.index < ncoordinates - 1 and back is False) + or (self.index > 0 and back is True) + ): if back is False: self.index += 1 else: @@ -1644,12 +1639,12 @@ def _index_changed(self, old, new): def get_interpolation_range(self): axis = self.signal.axes_manager.signal_axes[0] - if np.isnan(self.ss_left_value) or np.isnan(self.ss_right_value): - left = self.argmax - self.default_spike_width - right = self.argmax + self.default_spike_width - else: + if hasattr(self, "span_selector") and self._is_valid_range: left = axis.value2index(self.ss_left_value) right = axis.value2index(self.ss_right_value) + else: + left = self.argmax - self.default_spike_width + right = self.argmax + self.default_spike_width # Clip to the axis dimensions nchannels = self.signal.axes_manager.signal_shape[0] @@ -1659,13 +1654,10 @@ def get_interpolation_range(self): return left, right def get_interpolated_spectrum(self, axes_manager=None): - data = self.signal().copy() + data = self.signal._get_current_data().copy() axis = self.signal.axes_manager.signal_axes[0] left, right = self.get_interpolation_range() - if self.kind == 'linear': - pad = 1 - else: - pad = self.spline_order + pad = self.spline_order ileft = left - pad iright = right + pad ileft = np.clip(ileft, 0, len(data)) @@ -1688,62 +1680,67 @@ def get_interpolated_spectrum(self, axes_manager=None): # Interpolate x = np.hstack((axis.axis[ileft:left], axis.axis[right:iright])) y = np.hstack((data[ileft:left], data[right:iright])) - intp = interpolate.interp1d(x, y, kind=self.kind) + intp = interpolate.make_interp_spline(x, y, k=self.spline_order) data[left:right] = intp(axis.axis[left:right]) # Add noise if self.add_noise is True: if self.noise_type == "white": - data[left:right] += np.random.normal( - scale=np.sqrt(self.noise_variance), - size=right - left) + data[left:right] += self._rng.normal( + scale=np.sqrt(self.noise_variance), size=right - left + ) elif self.noise_type == "heteroscedastic": noise_variance = self.noise_variance( - axes_manager=self.signal.axes_manager)[left:right] - noise = [np.random.normal(scale=np.sqrt(item)) - for item in noise_variance] + axes_manager=self.signal.axes_manager + )[left:right] + noise = [ + self._rng.normal(scale=np.sqrt(item)) for item in noise_variance + ] data[left:right] += noise else: - data[left:right] = np.random.poisson( - np.clip(data[left:right], 0, np.inf)) + data[left:right] = self._rng.poisson( + np.clip(data[left:right], 0, np.inf) + ) return data def remove_all_spikes(self): spike = self.find() while spike: - self.signal()[:] = self.get_interpolated_spectrum() + self.signal._get_current_data()[:] = self.get_interpolated_spectrum() spike = self.find() @add_gui_method(toolkey="hyperspy.Signal1D.spikes_removal_tool") class SpikesRemovalInteractive(SpikesRemoval, SpanSelectorInSignal1D): - interpolator_kind = t.Enum( - 'Linear', - 'Spline', - default='Linear', - desc="the type of interpolation to use when\n" - "replacing the signal where a spike has been replaced") - threshold = t.Float(400, desc="the derivative magnitude threshold above\n" - "which to find spikes") + threshold = t.Float( + 400, desc="the derivative magnitude threshold above\n" "which to find spikes" + ) click_to_show_instructions = t.Button() show_derivative_histogram = t.Button() - spline_order = t.Range(1, 10, 3, - desc="the order of the spline used to\n" - "connect the reconstructed data") + spline_order = t.Range( + 1, + 10, + 1, + desc="the order of the spline used to\n" "connect the reconstructed data", + ) interpolator = None - default_spike_width = t.Int(5, - desc="the width over which to do the interpolation\n" - "when removing a spike (this can be " - "adjusted for each\nspike by clicking " - "and dragging on the display during\n" - "spike replacement)") + default_spike_width = t.Int( + 5, + desc="the width over which to do the interpolation\n" + "when removing a spike (this can be " + "adjusted for each\nspike by clicking " + "and dragging on the display during\n" + "spike replacement)", + ) index = t.Int(0) - add_noise = t.Bool(True, - desc="whether to add noise to the interpolated\nportion" - "of the spectrum. The noise properties defined\n" - "in the Signal metadata are used if present," - "otherwise\nshot noise is used as a default") + add_noise = t.Bool( + True, + desc="whether to add noise to the interpolated\nportion" + "of the spectrum. The noise properties defined\n" + "in the Signal metadata are used if present," + "otherwise\nshot noise is used as a default", + ) def __init__(self, signal, max_num_bins=1000, **kwargs): SpanSelectorInSignal1D.__init__(self, signal=signal) @@ -1760,13 +1757,17 @@ def _threshold_changed(self, old, new): def _click_to_show_instructions_fired(self): from pyface.message_dialog import information - _ = information(None, SPIKES_REMOVAL_INSTRUCTIONS, - title="Instructions"), + + _ = (information(None, SPIKES_REMOVAL_INSTRUCTIONS, title="Instructions"),) def _show_derivative_histogram_fired(self): - self.signal.spikes_diagnosis(signal_mask=self.signal_mask, - navigation_mask=self.navigation_mask, - max_num_bins=self.max_num_bins) + self.signal._spikes_diagnosis( + signal_mask=self.signal_mask, + navigation_mask=self.navigation_mask, + max_num_bins=self.max_num_bins, + show_plot=True, + use_gui=True, + ) def _reset_line(self): if self.interpolated_line is not None: @@ -1780,7 +1781,7 @@ def find(self, back=False): if spike is False: m = SimpleMessage() - m.text = 'End of dataset reached' + m.text = "End of dataset reached" try: m.gui() except (NotImplementedError, ImportError): @@ -1794,21 +1795,22 @@ def find(self, back=False): return else: minimum = max(0, self.argmax - 50) - maximum = min(len(self.signal()) - 1, self.argmax + 50) + maximum = min(len(self.signal._get_current_data()) - 1, self.argmax + 50) thresh_label = DerivativeTextParameters( - text=r"$\mathsf{\delta}_\mathsf{max}=$", - color="black") - self.ax.legend([thresh_label], [repr(int(self.derivmax))], - handler_map={DerivativeTextParameters: - DerivativeTextHandler()}, - loc='best') + text=r"$\mathsf{\delta}_\mathsf{max}=$", color="black" + ) + self.ax.legend( + [thresh_label], + [repr(int(self.derivmax))], + handler_map={DerivativeTextParameters: DerivativeTextHandler()}, + loc="best", + ) self.ax.set_xlim( - self.signal.axes_manager.signal_axes[0].index2value( - minimum), - self.signal.axes_manager.signal_axes[0].index2value( - maximum)) - self.signal._plot.pointer._set_indices( - self.coordinates[self.index]) + self.signal.axes_manager.signal_axes[0].index2value(minimum), + self.signal.axes_manager.signal_axes[0].index2value(maximum), + ) + if self.signal._plot.navigator_plot is not None: + self.signal._plot.pointer._set_indices(self.coordinates[self.index]) self.update_plot() self.create_interpolation_line() @@ -1823,14 +1825,17 @@ def update_plot(self): self.signal._plot.pointer._on_navigate(self.signal.axes_manager) def update_signal_mask(self): - if hasattr(self, 'mask_filling'): + if hasattr(self, "mask_filling"): self.mask_filling.remove() if self.signal_mask is not None: - self.mask_filling = self.ax.fill_between(self.axis.axis, - self.signal(), 0, - where=self.signal_mask, - facecolor='blue', - alpha=0.5) + self.mask_filling = self.ax.fill_between( + self.axis.axis, + self.signal._get_current_data(), + 0, + where=self.signal_mask, + facecolor="blue", + alpha=0.5, + ) def update_spectrum_line(self): self.line.auto_update = True @@ -1838,44 +1843,30 @@ def update_spectrum_line(self): self.line.auto_update = False def on_disabling_span_selector(self): + super().on_disabling_span_selector() if self.interpolated_line is not None: self.interpolated_line.close() self.interpolated_line = None def _spline_order_changed(self, old, new): - self.kind = self.spline_order - self.span_selector_changed() + if new != old: + self.spline_order = new + self.span_selector_changed() def _add_noise_changed(self, old, new): self.span_selector_changed() - def _interpolator_kind_changed(self, old, new): - if new == 'linear': - self.kind = new - else: - self.kind = self.spline_order - self.span_selector_changed() - - def _ss_left_value_changed(self, old, new): - if not (np.isnan(self.ss_right_value) or np.isnan(self.ss_left_value)): - self.span_selector_changed() - - def _ss_right_value_changed(self, old, new): - if not (np.isnan(self.ss_right_value) or np.isnan(self.ss_left_value)): - self.span_selector_changed() - def create_interpolation_line(self): self.interpolated_line = drawing.signal1d.Signal1DLine() self.interpolated_line.data_function = self.get_interpolated_spectrum - self.interpolated_line.set_line_properties( - color='blue', - type='line') + self.interpolated_line.set_line_properties(color="blue", type="line") self.signal._plot.signal_plot.add_line(self.interpolated_line) self.interpolated_line.auto_update = False - self.interpolated_line.autoscale = '' + self.interpolated_line.autoscale = "" self.interpolated_line.plot() - def span_selector_changed(self): + def span_selector_changed(self, *args, **kwargs): + super().span_selector_changed() if self.interpolated_line is None: return else: @@ -1884,7 +1875,7 @@ def span_selector_changed(self): def apply(self): if not self.interpolated_line: # No spike selected return - self.signal()[:] = self.get_interpolated_spectrum() + self.signal._get_current_data()[:] = self.get_interpolated_spectrum() self.signal.events.data_changed.trigger(obj=self.signal) self.update_spectrum_line() self.interpolated_line.close() @@ -1896,49 +1887,50 @@ def apply(self): @add_gui_method(toolkey="hyperspy.Signal2D.find_peaks") class PeaksFinder2D(t.HasTraits): method = t.Enum( - 'Local max', - 'Max', - 'Minmax', - 'Zaefferer', - 'Stat', - 'Laplacian of Gaussian', - 'Difference of Gaussian', - 'Template matching', - default='Local Max') + "Local max", + "Max", + "Minmax", + "Zaefferer", + "Stat", + "Laplacian of Gaussian", + "Difference of Gaussian", + "Template matching", + default="Local Max", + ) # For "Local max" method local_max_distance = t.Range(1, 20, value=3) - local_max_threshold = t.Range(0, 20., value=10) + local_max_threshold = t.Range(0, 20.0, value=10) # For "Max" method - max_alpha = t.Range(0, 6., value=3) + max_alpha = t.Range(0, 6.0, value=3) max_distance = t.Range(1, 20, value=10) # For "Minmax" method - minmax_distance = t.Range(0, 6., value=3) - minmax_threshold = t.Range(0, 20., value=10) + minmax_distance = t.Range(0, 6.0, value=3) + minmax_threshold = t.Range(0, 20.0, value=10) # For "Zaefferer" method zaefferer_grad_threshold = t.Range(0, 0.2, value=0.1) zaefferer_window_size = t.Range(2, 80, value=40) - zaefferer_distance_cutoff = t.Range(1, 100., value=50) + zaefferer_distance_cutoff = t.Range(1, 100.0, value=50) # For "Stat" method - stat_alpha = t.Range(0, 2., value=1) + stat_alpha = t.Range(0, 2.0, value=1) stat_window_radius = t.Range(5, 20, value=10) stat_convergence_ratio = t.Range(0, 0.1, value=0.05) # For "Laplacian of Gaussian" method - log_min_sigma = t.Range(0, 2., value=1) - log_max_sigma = t.Range(0, 100., value=50) + log_min_sigma = t.Range(0, 2.0, value=1) + log_max_sigma = t.Range(0, 100.0, value=50) log_num_sigma = t.Range(0, 20, value=10) log_threshold = t.Range(0, 0.4, value=0.2) - log_overlap = t.Range(0, 1., value=0.5) + log_overlap = t.Range(0, 1.0, value=0.5) log_log_scale = t.Bool(False) # For "Difference of Gaussian" method - dog_min_sigma = t.Range(0, 2., value=1) - dog_max_sigma = t.Range(0, 100., value=50) + dog_min_sigma = t.Range(0, 2.0, value=1) + dog_max_sigma = t.Range(0, 100.0, value=50) dog_sigma_ratio = t.Range(0, 3.2, value=1.6) dog_threshold = t.Range(0, 0.4, value=0.2) - dog_overlap = t.Range(0, 1., value=0.5) + dog_overlap = t.Range(0, 1.0, value=0.5) # For "Cross correlation" method xc_template = None - xc_distance = t.Range(0, 100., value=5.) - xc_threshold = t.Range(0, 10., value=0.5) + xc_distance = t.Range(0, 100.0, value=5.0) + xc_threshold = t.Range(0, 10.0, value=0.5) random_navigation_position = t.Button() compute_over_navigation_axes = t.Button() @@ -1947,90 +1939,94 @@ class PeaksFinder2D(t.HasTraits): def __init__(self, signal, method, peaks=None, **kwargs): self._attribute_argument_mapping_local_max = { - 'local_max_distance': 'min_distance', - 'local_max_threshold': 'threshold_abs', - } + "local_max_distance": "min_distance", + "local_max_threshold": "threshold_abs", + } self._attribute_argument_mapping_max = { - 'max_alpha': 'alpha', - 'max_distance': 'distance', - } + "max_alpha": "alpha", + "max_distance": "distance", + } self._attribute_argument_mapping_local_minmax = { - 'minmax_distance': 'distance', - 'minmax_threshold': 'threshold', - } + "minmax_distance": "distance", + "minmax_threshold": "threshold", + } self._attribute_argument_mapping_local_zaefferer = { - 'zaefferer_grad_threshold': 'grad_threshold', - 'zaefferer_window_size': 'window_size', - 'zaefferer_distance_cutoff': 'distance_cutoff', - } + "zaefferer_grad_threshold": "grad_threshold", + "zaefferer_window_size": "window_size", + "zaefferer_distance_cutoff": "distance_cutoff", + } self._attribute_argument_mapping_local_stat = { - 'stat_alpha': 'alpha', - 'stat_window_radius': 'window_radius', - 'stat_convergence_ratio': 'convergence_ratio', - } + "stat_alpha": "alpha", + "stat_window_radius": "window_radius", + "stat_convergence_ratio": "convergence_ratio", + } self._attribute_argument_mapping_local_log = { - 'log_min_sigma': 'min_sigma', - 'log_max_sigma': 'max_sigma', - 'log_num_sigma': 'num_sigma', - 'log_threshold': 'threshold', - 'log_overlap': 'overlap', - 'log_log_scale': 'log_scale', - } + "log_min_sigma": "min_sigma", + "log_max_sigma": "max_sigma", + "log_num_sigma": "num_sigma", + "log_threshold": "threshold", + "log_overlap": "overlap", + "log_log_scale": "log_scale", + } self._attribute_argument_mapping_local_dog = { - 'dog_min_sigma': 'min_sigma', - 'dog_max_sigma': 'max_sigma', - 'dog_sigma_ratio': 'sigma_ratio', - 'dog_threshold': 'threshold', - 'dog_overlap': 'overlap', - } + "dog_min_sigma": "min_sigma", + "dog_max_sigma": "max_sigma", + "dog_sigma_ratio": "sigma_ratio", + "dog_threshold": "threshold", + "dog_overlap": "overlap", + } self._attribute_argument_mapping_local_xc = { - 'xc_template': 'template', - 'xc_distance': 'distance', - 'xc_threshold': 'threshold', - } + "xc_template": "template", + "xc_distance": "distance", + "xc_threshold": "threshold", + } self._attribute_argument_mapping_dict = { - 'local_max': self._attribute_argument_mapping_local_max, - 'max': self._attribute_argument_mapping_max, - 'minmax': self._attribute_argument_mapping_local_minmax, - 'zaefferer': self._attribute_argument_mapping_local_zaefferer, - 'stat': self._attribute_argument_mapping_local_stat, - 'laplacian_of_gaussian': self._attribute_argument_mapping_local_log, - 'difference_of_gaussian': self._attribute_argument_mapping_local_dog, - 'template_matching': self._attribute_argument_mapping_local_xc, - } + "local_max": self._attribute_argument_mapping_local_max, + "max": self._attribute_argument_mapping_max, + "minmax": self._attribute_argument_mapping_local_minmax, + "zaefferer": self._attribute_argument_mapping_local_zaefferer, + "stat": self._attribute_argument_mapping_local_stat, + "laplacian_of_gaussian": self._attribute_argument_mapping_local_log, + "difference_of_gaussian": self._attribute_argument_mapping_local_dog, + "template_matching": self._attribute_argument_mapping_local_xc, + } if signal.axes_manager.signal_dimension != 2: - raise SignalDimensionError( - signal.axes.signal_dimension, 2) + raise SignalDimensionError(signal.axes.signal_dimension, 2) self._set_parameters_observer() - self.on_trait_change(self.set_random_navigation_position, - 'random_navigation_position') + self.on_trait_change( + self.set_random_navigation_position, "random_navigation_position" + ) self.signal = signal self.peaks = peaks + self.markers = None if self.signal._plot is None or not self.signal._plot.is_active: self.signal.plot() if self.signal.axes_manager.navigation_size > 0: self.show_navigation_sliders = True self.signal.axes_manager.events.indices_changed.connect( - self._update_peak_finding, []) + self._update_peak_finding, [] + ) self.signal._plot.signal_plot.events.closed.connect(self.disconnect, []) # Set initial parameters: # As a convenience, if the template argument is provided, we keep it # even if the method is different, to be able to use it later. - if 'template' in kwargs.keys(): - self.xc_template = kwargs['template'] + if "template" in kwargs.keys(): + self.xc_template = kwargs["template"] if method is not None: - method_dict = {'local_max':'Local max', - 'max':'Max', - 'minmax':'Minmax', - 'zaefferer':'Zaefferer', - 'stat':'Stat', - 'laplacian_of_gaussian':'Laplacian of Gaussian', - 'difference_of_gaussian':'Difference of Gaussian', - 'template_matching':'Template matching'} + method_dict = { + "local_max": "Local max", + "max": "Max", + "minmax": "Minmax", + "zaefferer": "Zaefferer", + "stat": "Stat", + "laplacian_of_gaussian": "Laplacian of Gaussian", + "difference_of_gaussian": "Difference of Gaussian", + "template_matching": "Template matching", + } self.method = method_dict[method] self._parse_paramaters_initial_values(**kwargs) self._update_peak_finding() @@ -2038,7 +2034,8 @@ def __init__(self, signal, method, peaks=None, **kwargs): def _parse_paramaters_initial_values(self, **kwargs): # Get the attribute to argument mapping for the current method arg_mapping = self._attribute_argument_mapping_dict[ - self._normalise_method_name(self.method)] + self._normalise_method_name(self.method) + ] for attr, arg in arg_mapping.items(): if arg in kwargs.keys(): setattr(self, attr, kwargs[arg]) @@ -2050,7 +2047,7 @@ def _update_peak_finding(self, method=None): self._plot_markers() def _method_changed(self, old, new): - if new == 'Template matching' and self.xc_template is None: + if new == "Template matching" and self.xc_template is None: raise RuntimeError('The "template" argument is required.') self._update_peak_finding(method=new) @@ -2069,45 +2066,40 @@ def _get_parameters(self, method): return {arg: getattr(self, attr) for attr, arg in arg_mapping.items()} def _normalise_method_name(self, method): - return method.lower().replace(' ', '_') + return method.lower().replace(" ", "_") def _find_peaks_current_index(self, method): method = self._normalise_method_name(method) - self.peaks.data = self.signal.find_peaks(method, current_index=True, - interactive=False, - **self._get_parameters(method)) + self.peaks.data = self.signal.find_peaks( + method, + current_index=True, + interactive=False, + **self._get_parameters(method), + ) def _plot_markers(self): - if self.signal._plot is not None and self.signal._plot.is_active: - self.signal._plot.signal_plot.remove_markers(render_figure=True) - peaks_markers = self._peaks_to_marker() - self.signal.add_marker(peaks_markers, render_figure=True) - - def _peaks_to_marker(self, markersize=20, add_numbers=True, - color='red'): - # make marker_list for current index - from hyperspy.drawing._markers.point import Point - - x_axis = self.signal.axes_manager.signal_axes[0] - y_axis = self.signal.axes_manager.signal_axes[1] - - if np.isnan(self.peaks.data).all(): - marker_list = [] + offsets = self.peaks.data + offsets = convert_positions(offsets, self.signal.axes_manager.signal_axes) + if self.markers is None: + self.markers = Circles( + offsets=offsets, + edgecolor="red", + facecolors="none", + sizes=20, + units="points", + ) else: - marker_list = [Point(x=x_axis.index2value(int(round(x))), - y=y_axis.index2value(int(round(y))), - color=color, - size=markersize) - for x, y in zip(self.peaks.data[:, 1], self.peaks.data[:, 0])] - - return marker_list + self.markers.offsets = offsets def compute_navigation(self): method = self._normalise_method_name(self.method) with self.signal.axes_manager.events.indices_changed.suppress(): self.peaks.data = self.signal.find_peaks( - method, interactive=False, current_index=False, - **self._get_parameters(method)) + method, + interactive=False, + current_index=False, + **self._get_parameters(method), + ) def close(self): # remove markers @@ -2122,24 +2114,21 @@ def disconnect(self): am.events.indices_changed.disconnect(self._update_peak_finding) def set_random_navigation_position(self): - index = np.random.randint(0, self.signal.axes_manager._max_index) - self.signal.axes_manager.indices = np.unravel_index(index, - tuple(self.signal.axes_manager._navigation_shape_in_array))[::-1] + index = self._rng.integers(0, self.signal.axes_manager._max_index) + self.signal.axes_manager.indices = np.unravel_index( + index, tuple(self.signal.axes_manager._navigation_shape_in_array) + )[::-1] # For creating a text handler in legend (to label derivative magnitude) class DerivativeTextParameters(object): - def __init__(self, text, color): self.my_text = text self.my_color = color class DerivativeTextHandler(object): - def legend_artist(self, legend, orig_handle, fontsize, handlebox): - patch = mpl_text.Text( - text=orig_handle.my_text, - color=orig_handle.my_color) + patch = mpl_text.Text(text=orig_handle.my_text, color=orig_handle.my_color) handlebox.add_artist(patch) return patch diff --git a/hyperspy/signals.py b/hyperspy/signals.py index 2b0b10484b..5588a670d0 100644 --- a/hyperspy/signals.py +++ b/hyperspy/signals.py @@ -1,70 +1,59 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . """ The Signal class and its specialized subclasses: - BaseSignal - For generic data with arbitrary signal_dimension. All other signal - classes inherit from this one. It should only be used with none of - the others is appropriated. - Signal1D - For generic data with signal_dimension equal 1, i.e. spectral data of - n-dimensions. The signal is unbinned by default. - Signal2D - For generic data with signal_dimension equal 2, i.e. image data of - n-dimensions. The signal is unbinned by default. - ComplexSignal - For generic complex data with arbitrary signal_dimension. - ComplexSignal1D - For generic complex data with signal_dimension equal 1, i.e. spectral - data of n-dimensions. The signal is unbinned by default. - ComplexSignal2D - For generic complex data with signal_dimension equal 2, i.e. image - data of n-dimensions. The signal is unbinned by default. - EELSSpectrum - For electron energy-loss data with signal_dimension equal 1, i.e. - spectral data of n-dimensions. The signal is binned by default. - EDSTEMSpectrum - For electron energy-dispersive X-rays data acquired in a transmission - electron microscopy with signal_dimension equal 1, i.e. - spectral data of n-dimensions. The signal is binned by default. - EDSSEMSpectrum - For electron energy-dispersive X-rays data acquired in a scanning - electron microscopy with signal_dimension equal 1, i.e. - spectral data of n-dimensions. The signal is binned by default. - DielectricFunction - For dielectric function data with signal_dimension equal 1. The signal - is unbinned by default. - HolographyImage - For 2D-images taken via electron holography. Electron wave as - ComplexSignal2D can be reconstructed from them. +BaseSignal + For generic data with arbitrary signal_dimension. All other signal + classes inherit from this one. It should only be used with none of + the others is appropriated. +Signal1D + For generic data with signal_dimension equal 1, i.e. spectral data of + n-dimensions. The signal is unbinned by default. +Signal2D + For generic data with signal_dimension equal 2, i.e. image data of + n-dimensions. The signal is unbinned by default. +ComplexSignal + For generic complex data with arbitrary signal_dimension. +ComplexSignal1D + For generic complex data with signal_dimension equal 1, i.e. spectral + data of n-dimensions. The signal is unbinned by default. +ComplexSignal2D + For generic complex data with signal_dimension equal 2, i.e. image + data of n-dimensions. The signal is unbinned by default. """ -# -*- coding: utf-8 -*- -from hyperspy.extensions import EXTENSIONS as _EXTENSIONS import importlib -_g = globals() -for _signal, _specs in _EXTENSIONS["signals"].items(): - if not _specs["lazy"]: - _g[_signal] = getattr( - importlib.import_module( - _specs["module"]), _signal) +from hyperspy.extensions import EXTENSIONS as EXTENSIONS_ + +__all__ = [ + signal_ for signal_, specs_ in EXTENSIONS_["signals"].items() if not specs_["lazy"] +] + + +def __dir__(): + return sorted(__all__) + -del importlib +def __getattr__(name): + if name in __all__: + spec = EXTENSIONS_["signals"][name] + return getattr(importlib.import_module(spec["module"]), name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/hyperspy/tests/__init__.py b/hyperspy/tests/__init__.py index ea46bbf873..4f4fb16138 100644 --- a/hyperspy/tests/__init__.py +++ b/hyperspy/tests/__init__.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import os import warnings @@ -24,27 +24,25 @@ preferences.General.show_progressbar = True # Check if we should fail on external deprecation messages -fail_on_external = os.environ.pop('FAIL_ON_EXTERNAL_DEPRECATION', False) +fail_on_external = os.environ.pop("FAIL_ON_EXTERNAL_DEPRECATION", False) if isinstance(fail_on_external, str): - fail_on_external = (fail_on_external.lower() in - ['true', 't', '1', 'yes', 'y', 'set']) + fail_on_external = fail_on_external.lower() in ["true", "t", "1", "yes", "y", "set"] if fail_on_external: - warnings.filterwarnings( - 'error', category=DeprecationWarning) + warnings.filterwarnings("error", category=DeprecationWarning) # Travis setup has these warnings, so ignore: warnings.filterwarnings( - 'ignore', + "ignore", r"BaseException\.message has been deprecated as of Python 2\.6", - DeprecationWarning) + DeprecationWarning, + ) # Don't care about warnings in hyperspy in this mode! - warnings.filterwarnings('default', module="hyperspy") + warnings.filterwarnings("default", module="hyperspy") else: # Fall-back filter: Error - warnings.simplefilter('error') + warnings.simplefilter("error") warnings.filterwarnings( - 'ignore', "Failed to import the optional scikit image package", - UserWarning) + "ignore", "Failed to import the optional scikit image package", UserWarning + ) # We allow extrernal warnings: - warnings.filterwarnings('default', - module="(?!hyperspy)") + warnings.filterwarnings("default", module="(?!hyperspy)") diff --git a/hyperspy/tests/axes/test_axes_manager.py b/hyperspy/tests/axes/test_axes_manager.py index 054d080e84..e29ae9ebc2 100644 --- a/hyperspy/tests/axes/test_axes_manager.py +++ b/hyperspy/tests/axes/test_axes_manager.py @@ -1,34 +1,42 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from unittest import mock import numpy as np +import pytest -from hyperspy.axes import AxesManager, _serpentine_iter, _flyback_iter, GeneratorLen +from hyperspy.axes import ( + AxesManager, + BaseDataAxis, + GeneratorLen, + _flyback_iter, + _serpentine_iter, +) from hyperspy.defaults_parser import preferences +from hyperspy.misc.utils import TupleSA from hyperspy.signals import BaseSignal, Signal1D, Signal2D -import pytest def generator(): for i in range(3): - yield((0,0,i)) + yield ((0, 0, i)) + class TestAxesManager: def setup_method(self, method): @@ -105,14 +113,56 @@ def test_create_axis_from_object(self): def test_set_axis(self): am = self.am axis = am[-1].copy() - am.set_axis(axis,2) + am.set_axis(axis, 2) assert am[-2].offset == am[-1].offset assert am[-2].scale == am[-1].scale + def test_set_attributes(self): + am = self.am + am.signal_axes.set(name=("kx", "ky"), offset=(1, 2), scale=3, units="nm^{-1}") + assert am.signal_axes[0].name == "kx" + assert am.signal_axes[1].name == "ky" + assert am.signal_axes[0].offset == 1 + assert am.signal_axes[1].offset == 2 + assert am.signal_axes[0].scale == 3 + assert am.signal_axes[1].scale == 3 + assert am.signal_axes[0].units == "nm^{-1}" + assert am.signal_axes[1].units == "nm^{-1}" + am.navigation_axes.set(name=("x", "y"), offset=10, scale=(10, 20), units="nm") + assert am.navigation_axes[0].name == "x" + assert am.navigation_axes[1].name == "y" + assert am.navigation_axes[0].offset == 10 + assert am.navigation_axes[1].offset == 10 + assert am.navigation_axes[0].scale == 10 + assert am.navigation_axes[1].scale == 20 + assert am.navigation_axes[0].units == "nm" + assert am.navigation_axes[1].units == "nm" + with pytest.raises(AttributeError): + am.signal_axes.set( + names=("kx", "kx"), offset=(1, 2), scale=3, units="nm^{-1}" + ) + def test_all_uniform(self): - assert self.am.all_uniform == True + assert self.am.all_uniform is True self.am[-1].convert_to_non_uniform_axis() - assert self.am.all_uniform == False + assert self.am.all_uniform is False + + def test_get_axis(self): + am = self.am + assert am[0] == am["d"] + assert am[-1] == am["b"] + axis = am[1] + assert am[axis] == axis + with pytest.raises(ValueError): + axis = BaseDataAxis() + am[axis] + assert isinstance(am.navigation_axes, TupleSA) + assert isinstance(am.signal_axes, TupleSA) + + assert am["nav"] == am.navigation_axes + assert isinstance(am["nav"], TupleSA) + assert am["sig"] == am.signal_axes + assert isinstance(am["nav"], TupleSA) class TestAxesManagerScaleOffset: @@ -375,11 +425,11 @@ def setup_method(self, method): self.am = s.axes_manager def test_iterpath_property(self): - self.am._iterpath = "abc" # with underscore + self.am._iterpath = "abc" # with underscore assert self.am.iterpath == "abc" with pytest.raises(ValueError): - self.am.iterpath = "blahblah" # w/o underscore + self.am.iterpath = "blahblah" # w/o underscore path = "flyback" self.am.iterpath = path @@ -398,16 +448,23 @@ def test_wrong_iterpath(self): def test_wrong_custom_iterpath(self): class A: pass + with pytest.raises(TypeError): self.am.iterpath = A() def test_wrong_custom_iterpath2(self): with pytest.raises(TypeError): - self.am.iterpath = [0,1,2,3,4,] # indices are not iterable + self.am.iterpath = [ + 0, + 1, + 2, + 3, + 4, + ] # indices are not iterable def test_wrong_custom_iterpath3(self): with pytest.raises(ValueError): - self.am.iterpath = [(0,)] # not enough dimensions + self.am.iterpath = [(0,)] # not enough dimensions def test_flyback(self): self.am.iterpath = "flyback" @@ -430,7 +487,7 @@ def test_serpentine(self): break def test_custom_iterpath(self): - iterpath = [(0,1,1), (1,1,1)] + iterpath = [(0, 1, 1), (1, 1, 1)] self.am.iterpath = iterpath assert self.am._iterpath == iterpath assert self.am.iterpath == iterpath @@ -444,7 +501,6 @@ def test_custom_iterpath(self): break def test_custom_iterpath_generator(self): - iterpath = generator() self.am.iterpath = iterpath assert self.am._iterpath == iterpath @@ -453,28 +509,30 @@ def test_custom_iterpath_generator(self): for i, _ in enumerate(self.am): if i == 0: - assert self.am.indices == (0,0,0) + assert self.am.indices == (0, 0, 0) if i == 1: - assert self.am.indices == (0,0,1) + assert self.am.indices == (0, 0, 1) break def test_get_iterpath_size1(self): assert self.am._get_iterpath_size() == self.am.navigation_size - assert self.am._get_iterpath_size(masked_elements = 1) == self.am.navigation_size - 1 + assert ( + self.am._get_iterpath_size(masked_elements=1) == self.am.navigation_size - 1 + ) def test_get_iterpath_size2(self): self.am.iterpath = generator() - assert self.am._get_iterpath_size() == None - assert self.am._get_iterpath_size(masked_elements = 1) == None + assert self.am._get_iterpath_size() is None + assert self.am._get_iterpath_size(masked_elements=1) is None def test_get_iterpath_size3(self): - self.am.iterpath =[(0,0,0), (0,0,1)] + self.am.iterpath = [(0, 0, 0), (0, 0, 1)] assert self.am._get_iterpath_size() == 2 - assert self.am._get_iterpath_size(masked_elements = 1) == 2 + assert self.am._get_iterpath_size(masked_elements=1) == 2 def test_GeneratorLen(self): gen = GeneratorLen(gen=generator(), length=3) - assert list(gen) == [(0,0,0),(0,0,1),(0,0,2)] + assert list(gen) == [(0, 0, 0), (0, 0, 1), (0, 0, 2)] def test_GeneratorLen_iterpath(self): gen = GeneratorLen(gen=generator(), length=3) @@ -494,7 +552,7 @@ def test_wrong_iterpath_signal2D(self): self.am.iterpath = "blahblah" def test_custom_iterpath_signal2D(self): - indices = [(0,1,1), (1,1,1)] + indices = [(0, 1, 1), (1, 1, 1)] self.am.iterpath = indices for i, _ in enumerate(self.am): if i == 0: @@ -516,8 +574,8 @@ def test_serpentine_signal2D(self): def test_switch_iterpath(self): s = self.s s.axes_manager.iterpath = "serpentine" - with s.axes_manager.switch_iterpath('flyback'): - assert s.axes_manager.iterpath == 'flyback' + with s.axes_manager.switch_iterpath("flyback"): + assert s.axes_manager.iterpath == "flyback" for i, _ in enumerate(s.axes_manager): if i == 3: assert self.am.indices == (0, 1, 0) @@ -525,16 +583,49 @@ def test_switch_iterpath(self): if i == 9: assert self.am.indices == (0, 0, 1) break - assert s.axes_manager.iterpath == 'serpentine' + assert s.axes_manager.iterpath == "serpentine" def test_iterpath_function_flyback(): - for i, indices in enumerate(_flyback_iter((3,3,3))): + for i, indices in enumerate(_flyback_iter((3, 3, 3))): if i == 3: assert indices == (0, 1, 0) def test_iterpath_function_serpentine(): - for i, indices in enumerate(_serpentine_iter((3,3,3))): + for i, indices in enumerate(_serpentine_iter((3, 3, 3))): if i == 3: assert indices == (2, 1, 0) + + +def TestAxesManagerRagged(): + def setup_method(self, method): + axes_list = [ + { + "name": "a", + "navigate": True, + "offset": 0.0, + "scale": 1.3, + "size": 2, + "units": "aa", + }, + ] + + self.am = AxesManager(axes_list) + self.am._ragged = True + + def test_ragged_property(self): + assert self.am.ragged + with pytest.raises(AttributeError): + self.am.ragged = False + self.am._ragged = False + assert not self.am.ragged + + def test_reprs(self): + expected_string = "\n" + " Name | size | index | offset | scale | units \n" + "================ | ====== | ====== | ======= | ======= | ====== \n" + " a | 2 | 0 | 0 | 1.3 | aa \n" + "---------------- | ------ | ------ | ------- | ------- | ------ \n" + " Ragged axis | Variable length" + assert self.am.__repr__() == expected_string diff --git a/hyperspy/tests/axes/test_conversion_units.py b/hyperspy/tests/axes/test_conversion_units.py index 45537daa74..2897a1457f 100644 --- a/hyperspy/tests/axes/test_conversion_units.py +++ b/hyperspy/tests/axes/test_conversion_units.py @@ -1,464 +1,482 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np import pytest import traits.api as t -from hyperspy.axes import (DataAxis, UniformDataAxis, AxesManager, - UnitConversion, _ureg) +from hyperspy.api import _ureg +from hyperspy.axes import AxesManager, DataAxis, UniformDataAxis, UnitConversion from hyperspy.misc.test_utils import assert_deep_almost_equal class TestUnitConversion: - def setup_method(self, method): self.uc = UnitConversion() - self._set_units_scale_size(units='m', scale=1E-3) + self._set_units_scale_size(units="m", scale=1e-3) - def _set_units_scale_size(self, units=t.Undefined, scale=1.0, size=100, - offset=0.0): + def _set_units_scale_size(self, units=t.Undefined, scale=1.0, size=100, offset=0.0): self.uc.units = units self.uc.scale = scale self.uc.offset = offset self.uc.size = size def test_units_setter(self): - self.uc.units = ' m' - assert self.uc.units == ' m' - self.uc.units = 'um' - assert self.uc.units == 'um' - self.uc.units = 'µm' - assert self.uc.units == 'µm' - self.uc.units = 'km' - assert self.uc.units == 'km' + self.uc.units = " m" + assert self.uc.units == " m" + self.uc.units = "um" + assert self.uc.units == "um" + self.uc.units = "µm" + assert self.uc.units == "µm" + self.uc.units = "km" + assert self.uc.units == "km" def test_ignore_conversion(self): assert self.uc._ignore_conversion(t.Undefined) with pytest.warns(UserWarning, match="not supported for conversion."): - assert self.uc._ignore_conversion('unit_not_supported') - assert not self.uc._ignore_conversion('m') + assert self.uc._ignore_conversion("unit_not_supported") + assert not self.uc._ignore_conversion("m") def test_converted_compact_scale_units(self): - self.uc.units = 'toto' + self.uc.units = "toto" with pytest.warns(UserWarning, match="not supported for conversion."): self.uc._convert_compact_units() - assert self.uc.units == 'toto' - np.testing.assert_almost_equal(self.uc.scale, 1.0E-3) + assert self.uc.units == "toto" + np.testing.assert_almost_equal(self.uc.scale, 1.0e-3) def test_convert_to_units(self): self._set_units_scale_size(t.Undefined, 1.0) - out = self.uc._convert_units('nm') + out = self.uc._convert_units("nm") assert out is None assert self.uc.units == t.Undefined np.testing.assert_almost_equal(self.uc.scale, 1.0) - self._set_units_scale_size('m', 1.0E-3) - out = self.uc._convert_units('µm') + self._set_units_scale_size("m", 1.0e-3) + out = self.uc._convert_units("µm") assert out is None - assert self.uc.units == 'µm' - np.testing.assert_almost_equal(self.uc.scale, 1E3) + assert self.uc.units == "µm" + np.testing.assert_almost_equal(self.uc.scale, 1e3) - self._set_units_scale_size('µm', 0.5) - out = self.uc._convert_units('nm') + self._set_units_scale_size("µm", 0.5) + out = self.uc._convert_units("nm") assert out is None - assert self.uc.units == 'nm' + assert self.uc.units == "nm" np.testing.assert_almost_equal(self.uc.scale, 500) - self._set_units_scale_size('µm', 5) - out = self.uc._convert_units('cm') + self._set_units_scale_size("µm", 5) + out = self.uc._convert_units("cm") assert out is None - assert self.uc.units == 'cm' + assert self.uc.units == "cm" np.testing.assert_almost_equal(self.uc.scale, 0.0005) - self._set_units_scale_size('1/µm', 5) - out = self.uc._convert_units('1/nm') + self._set_units_scale_size("1/µm", 5) + out = self.uc._convert_units("1/nm") assert out is None - assert self.uc.units == '1 / nm' + assert self.uc.units == "1 / nm" np.testing.assert_almost_equal(self.uc.scale, 0.005) - self._set_units_scale_size('eV', 5) - out = self.uc._convert_units('keV') + self._set_units_scale_size("eV", 5) + out = self.uc._convert_units("keV") assert out is None - assert self.uc.units == 'keV' + assert self.uc.units == "keV" np.testing.assert_almost_equal(self.uc.scale, 0.005) def test_convert_to_units_not_in_place(self): self._set_units_scale_size(t.Undefined, 1.0) - out = self.uc.convert_to_units('nm', inplace=False) + out = self.uc.convert_to_units("nm", inplace=False) assert out is None # unit conversion is ignored assert self.uc.units == t.Undefined np.testing.assert_almost_equal(self.uc.scale, 1.0) - self._set_units_scale_size('m', 1.0E-3) - out = self.uc.convert_to_units('µm', inplace=False) - assert out == (1E3, 0.0, 'µm') - assert self.uc.units == 'm' - np.testing.assert_almost_equal(self.uc.scale, 1.0E-3) + self._set_units_scale_size("m", 1.0e-3) + out = self.uc.convert_to_units("µm", inplace=False) + assert out == (1e3, 0.0, "µm") + assert self.uc.units == "m" + np.testing.assert_almost_equal(self.uc.scale, 1.0e-3) np.testing.assert_almost_equal(self.uc.offset, 0.0) - self._set_units_scale_size('µm', 0.5) - out = self.uc.convert_to_units('nm', inplace=False) - assert out[1:] == (0.0, 'nm') + self._set_units_scale_size("µm", 0.5) + out = self.uc.convert_to_units("nm", inplace=False) + assert out[1:] == (0.0, "nm") np.testing.assert_almost_equal(out[0], 500.0) - assert self.uc.units == 'µm' + assert self.uc.units == "µm" np.testing.assert_almost_equal(self.uc.scale, 0.5) def test_get_compact_unit(self): ##### Imaging ##### # typical setting for high resolution image - self._set_units_scale_size('m', 12E-12, 2048, 2E-9) + self._set_units_scale_size("m", 12e-12, 2048, 2e-9) self.uc._convert_compact_units() - assert self.uc.units == 'nm' + assert self.uc.units == "nm" np.testing.assert_almost_equal(self.uc.scale, 0.012) np.testing.assert_almost_equal(self.uc.offset, 2.0) # typical setting for nm resolution image - self._set_units_scale_size('m', 0.5E-9, 1024) + self._set_units_scale_size("m", 0.5e-9, 1024) self.uc._convert_compact_units() - assert self.uc.units == 'nm' + assert self.uc.units == "nm" np.testing.assert_almost_equal(self.uc.scale, 0.5) np.testing.assert_almost_equal(self.uc.offset, 0.0) ##### Diffraction ##### # typical TEM diffraction - self._set_units_scale_size('1/m', 0.1E9, 1024) + self._set_units_scale_size("1/m", 0.1e9, 1024) self.uc._convert_compact_units() - assert self.uc.units == '1 / nm' + assert self.uc.units == "1 / nm" np.testing.assert_almost_equal(self.uc.scale, 0.1) # typical TEM diffraction - self._set_units_scale_size('1/m', 0.01E9, 256) + self._set_units_scale_size("1/m", 0.01e9, 256) self.uc._convert_compact_units() - assert self.uc.units == '1 / µm' + assert self.uc.units == "1 / µm" np.testing.assert_almost_equal(self.uc.scale, 10.0) # high camera length diffraction - self._set_units_scale_size('1/m', 0.1E9, 4096) + self._set_units_scale_size("1/m", 0.1e9, 4096) self.uc._convert_compact_units() - assert self.uc.units == '1 / nm' + assert self.uc.units == "1 / nm" np.testing.assert_almost_equal(self.uc.scale, 0.1) # typical EDS resolution - self._set_units_scale_size('eV', 50, 4096, 0.0) + self._set_units_scale_size("eV", 50, 4096, 0.0) self.uc._convert_compact_units() - assert self.uc.units == 'keV' + assert self.uc.units == "keV" np.testing.assert_almost_equal(self.uc.scale, 0.05) np.testing.assert_almost_equal(self.uc.offset, 0.0) ##### Spectroscopy ##### # typical EELS resolution - self._set_units_scale_size('eV', 0.2, 2048, 200.0) + self._set_units_scale_size("eV", 0.2, 2048, 200.0) self.uc._convert_compact_units() - assert self.uc.units == 'eV' + assert self.uc.units == "eV" np.testing.assert_almost_equal(self.uc.scale, 0.2) np.testing.assert_almost_equal(self.uc.offset, 200.0) # typical EELS resolution - self._set_units_scale_size('eV', 1.0, 2048, 500.0) + self._set_units_scale_size("eV", 1.0, 2048, 500.0) self.uc._convert_compact_units() - assert self.uc.units == 'eV' + assert self.uc.units == "eV" np.testing.assert_almost_equal(self.uc.scale, 1.0) np.testing.assert_almost_equal(self.uc.offset, 500) # typical high resolution EELS resolution - self._set_units_scale_size('eV', 0.05, 100) + self._set_units_scale_size("eV", 0.05, 100) self.uc._convert_compact_units() - assert self.uc.units == 'eV' + assert self.uc.units == "eV" assert self.uc.scale == 0.05 def test_get_set_quantity(self): with pytest.raises(ValueError): - self.uc._get_quantity('size') + self.uc._get_quantity("size") with pytest.raises(ValueError): - self.uc._set_quantity('size', 10) - + self.uc._set_quantity("size", 10) -class TestUniformDataAxis: +class TestUniformDataAxis: def setup_method(self, method): - self.axis = UniformDataAxis(size=2048, scale=12E-12, units='m', - offset=5E-9) + self.axis = UniformDataAxis(size=2048, scale=12e-12, units="m", offset=5e-9) def test_scale_offset_as_quantity_property(self): - assert self.axis.scale_as_quantity == 12E-12 * _ureg('m') - assert self.axis.offset_as_quantity == 5E-9 * _ureg('m') + assert self.axis.scale_as_quantity == 12e-12 * _ureg("m") + assert self.axis.offset_as_quantity == 5e-9 * _ureg("m") def test_scale_as_quantity_setter_string(self): - self.axis.scale_as_quantity = '2.5 nm' + self.axis.scale_as_quantity = "2.5 nm" assert self.axis.scale == 2.5 np.testing.assert_almost_equal(self.axis.offset, 5.0) - assert self.axis.units == 'nm' + assert self.axis.units == "nm" # Test that the axis array has been recomputed np.testing.assert_almost_equal(self.axis.axis[1], 7.5) def test_scale_as_quantity_setter_string_no_previous_units(self): - axis = UniformDataAxis(size=2048, scale=12E-12, offset=5.0) - axis.scale_as_quantity = '2.5 nm' + axis = UniformDataAxis(size=2048, scale=12e-12, offset=5.0) + axis.scale_as_quantity = "2.5 nm" assert axis.scale == 2.5 # the units haven't been set previously, so the offset is not converted np.testing.assert_almost_equal(axis.offset, 5.0) - assert axis.units == 'nm' + assert axis.units == "nm" def test_offset_as_quantity_setter_string(self): - self.axis.offset_as_quantity = '5e-3 mm' + self.axis.offset_as_quantity = "5e-3 mm" assert self.axis.scale == 12e-9 assert self.axis.offset == 5e-3 - assert self.axis.units == 'mm' + assert self.axis.units == "mm" def test_offset_as_quantity_setter_string_no_units(self): - self.axis.offset_as_quantity = '5e-3' + self.axis.offset_as_quantity = "5e-3" assert self.axis.offset == 5e-3 - assert self.axis.scale == 12E-12 - assert self.axis.units == 'm' + assert self.axis.scale == 12e-12 + assert self.axis.units == "m" def test_scale_offset_as_quantity_setter_float(self): self.axis.scale_as_quantity = 2.5e-9 assert self.axis.scale == 2.5e-9 - assert self.axis.units == 'm' + assert self.axis.units == "m" def test_scale_offset_as_quantity_setter_pint_quantity(self): - self.axis.scale_as_quantity = _ureg.parse_expression('2.5 nm') + self.axis.scale_as_quantity = _ureg.parse_expression("2.5 nm") assert self.axis.scale == 2.5 - assert self.axis.units == 'nm' + assert self.axis.units == "nm" - self.axis.offset_as_quantity = _ureg.parse_expression('5e-3 mm') + self.axis.offset_as_quantity = _ureg.parse_expression("5e-3 mm") assert self.axis.offset == 5e-3 - assert self.axis.units == 'mm' + assert self.axis.units == "mm" def test_convert_to_compact_units(self): self.axis.convert_to_units(units=None) np.testing.assert_almost_equal(self.axis.scale, 0.012) - assert self.axis.units == 'nm' + assert self.axis.units == "nm" np.testing.assert_almost_equal(self.axis.offset, 5.0) def test_convert_to_units(self): - self.axis.convert_to_units(units='µm') - np.testing.assert_almost_equal(self.axis.scale, 12E-6) - assert self.axis.units == 'µm' + self.axis.convert_to_units(units="µm") + np.testing.assert_almost_equal(self.axis.scale, 12e-6) + assert self.axis.units == "µm" np.testing.assert_almost_equal(self.axis.offset, 0.005) def test_units_not_supported_by_pint_warning_raised(self): # raising a warning, not converting scale - self.axis.units = 'toto' + self.axis.units = "toto" with pytest.warns(UserWarning, match="not supported for conversion."): - self.axis.convert_to_units('m') - np.testing.assert_almost_equal(self.axis.scale, 12E-12) - assert self.axis.units == 'toto' + self.axis.convert_to_units("m") + np.testing.assert_almost_equal(self.axis.scale, 12e-12) + assert self.axis.units == "toto" def test_units_not_supported_by_pint_warning_raised2(self): # raising a warning, not converting scale - self.axis.units = 'µm' + self.axis.units = "µm" with pytest.warns(UserWarning, match="not supported for conversion."): - self.axis.convert_to_units('toto') - np.testing.assert_almost_equal(self.axis.scale, 12E-12) - assert self.axis.units == 'µm' + self.axis.convert_to_units("toto") + np.testing.assert_almost_equal(self.axis.scale, 12e-12) + assert self.axis.units == "µm" class TestAxesManager: - def setup_method(self, method): self.axes_list = [ - {'_type': 'UniformDataAxis', - 'name': 'x', - 'navigate': True, - 'is_binned': False, - 'offset': 0.0, - 'scale': 1.5E-9, - 'size': 1024, - 'units': 'm'}, - {'_type': 'UniformDataAxis', - 'name': 'y', - 'navigate': True, - 'is_binned': False, - 'offset': 0.0, - 'scale': 0.5E-9, - 'size': 1024, - 'units': 'm'}, - {'_type': 'UniformDataAxis', - 'name': 'energy', - 'navigate': False, - 'is_binned': False, - 'offset': 0.0, - 'scale': 5.0, - 'size': 4096, - 'units': 'eV'}] + { + "_type": "UniformDataAxis", + "name": "x", + "navigate": True, + "is_binned": False, + "offset": 0.0, + "scale": 1.5e-9, + "size": 1024, + "units": "m", + }, + { + "_type": "UniformDataAxis", + "name": "y", + "navigate": True, + "is_binned": False, + "offset": 0.0, + "scale": 0.5e-9, + "size": 1024, + "units": "m", + }, + { + "_type": "UniformDataAxis", + "name": "energy", + "navigate": False, + "is_binned": False, + "offset": 0.0, + "scale": 5.0, + "size": 4096, + "units": "eV", + }, + ] self.am = AxesManager(self.axes_list) self.axes_list2 = [ - {'name': 'x', - 'navigate': True, - 'is_binned': False, - 'offset': 0.0, - 'scale': 1.5E-9, - 'size': 1024, - 'units': 'm'}, - {'name': 'energy', - 'navigate': False, - 'is_binned': False, - 'offset': 0.0, - 'scale': 2.5, - 'size': 4096, - 'units': 'eV'}, - {'name': 'energy2', - 'navigate': False, - 'is_binned': False, - 'offset': 0.0, - 'scale': 5.0, - 'size': 4096, - 'units': 'eV'}] + { + "name": "x", + "navigate": True, + "is_binned": False, + "offset": 0.0, + "scale": 1.5e-9, + "size": 1024, + "units": "m", + }, + { + "name": "energy", + "navigate": False, + "is_binned": False, + "offset": 0.0, + "scale": 2.5, + "size": 4096, + "units": "eV", + }, + { + "name": "energy2", + "navigate": False, + "is_binned": False, + "offset": 0.0, + "scale": 5.0, + "size": 4096, + "units": "eV", + }, + ] self.am2 = AxesManager(self.axes_list2) def test_compact_unit(self): self.am.convert_units() - assert self.am['x'].units == 'nm' - np.testing.assert_almost_equal(self.am['x'].scale, 1.5) - assert self.am['y'].units == 'nm' - np.testing.assert_almost_equal(self.am['y'].scale, 0.5) - assert self.am['energy'].units == 'keV' - np.testing.assert_almost_equal(self.am['energy'].scale, 0.005) + assert self.am["x"].units == "nm" + np.testing.assert_almost_equal(self.am["x"].scale, 1.5) + assert self.am["y"].units == "nm" + np.testing.assert_almost_equal(self.am["y"].scale, 0.5) + assert self.am["energy"].units == "keV" + np.testing.assert_almost_equal(self.am["energy"].scale, 0.005) def test_convert_to_navigation_units(self): - self.am.convert_units(axes='navigation', units='mm') - np.testing.assert_almost_equal(self.am['x'].scale, 1.5E-6) - assert self.am['x'].units == 'mm' - np.testing.assert_almost_equal(self.am['y'].scale, 0.5E-6) - assert self.am['y'].units == 'mm' - np.testing.assert_almost_equal(self.am['energy'].scale, - self.axes_list[-1]['scale']) + self.am.convert_units(axes="navigation", units="mm") + np.testing.assert_almost_equal(self.am["x"].scale, 1.5e-6) + assert self.am["x"].units == "mm" + np.testing.assert_almost_equal(self.am["y"].scale, 0.5e-6) + assert self.am["y"].units == "mm" + np.testing.assert_almost_equal( + self.am["energy"].scale, self.axes_list[-1]["scale"] + ) def test_convert_units_axes_integer(self): # convert only the first axis - self.am.convert_units(axes=0, units='nm', same_units=False) + self.am.convert_units(axes=0, units="nm", same_units=False) np.testing.assert_almost_equal(self.am[0].scale, 0.5) - assert self.am[0].units == 'nm' - np.testing.assert_almost_equal(self.am['x'].scale, 1.5E-9) - assert self.am['x'].units == 'm' - np.testing.assert_almost_equal(self.am['energy'].scale, - self.axes_list[-1]['scale']) - - self.am.convert_units(axes=0, units='nm', same_units=True) + assert self.am[0].units == "nm" + np.testing.assert_almost_equal(self.am["x"].scale, 1.5e-9) + assert self.am["x"].units == "m" + np.testing.assert_almost_equal( + self.am["energy"].scale, self.axes_list[-1]["scale"] + ) + + self.am.convert_units(axes=0, units="nm", same_units=True) np.testing.assert_almost_equal(self.am[0].scale, 0.5) - assert self.am[0].units == 'nm' - np.testing.assert_almost_equal(self.am['x'].scale, 1.5) - assert self.am['x'].units == 'nm' + assert self.am[0].units == "nm" + np.testing.assert_almost_equal(self.am["x"].scale, 1.5) + assert self.am["x"].units == "nm" def test_convert_to_navigation_units_list(self): - self.am.convert_units(axes='navigation', units=['mm', 'nm'], - same_units=False) - np.testing.assert_almost_equal(self.am['x'].scale, 1.5) - assert self.am['x'].units == 'nm' - np.testing.assert_almost_equal(self.am['y'].scale, 0.5E-6) - assert self.am['y'].units == 'mm' - np.testing.assert_almost_equal(self.am['energy'].scale, - self.axes_list[-1]['scale']) + self.am.convert_units(axes="navigation", units=["mm", "nm"], same_units=False) + np.testing.assert_almost_equal(self.am["x"].scale, 1.5) + assert self.am["x"].units == "nm" + np.testing.assert_almost_equal(self.am["y"].scale, 0.5e-6) + assert self.am["y"].units == "mm" + np.testing.assert_almost_equal( + self.am["energy"].scale, self.axes_list[-1]["scale"] + ) def test_convert_to_navigation_units_list_same_units(self): - self.am.convert_units(axes='navigation', units=['mm', 'nm'], - same_units=True) - assert self.am['x'].units == 'mm' - np.testing.assert_almost_equal(self.am['x'].scale, 1.5e-6) - assert self.am['y'].units == 'mm' - np.testing.assert_almost_equal(self.am['y'].scale, 0.5e-6) - assert self.am['energy'].units == 'eV' - np.testing.assert_almost_equal(self.am['energy'].scale, 5) + self.am.convert_units(axes="navigation", units=["mm", "nm"], same_units=True) + assert self.am["x"].units == "mm" + np.testing.assert_almost_equal(self.am["x"].scale, 1.5e-6) + assert self.am["y"].units == "mm" + np.testing.assert_almost_equal(self.am["y"].scale, 0.5e-6) + assert self.am["energy"].units == "eV" + np.testing.assert_almost_equal(self.am["energy"].scale, 5) def test_convert_to_navigation_units_different(self): # Don't convert the units since the units of the navigation axes are # different - self.axes_list.insert(0, - {'name': 'time', - 'navigate': True, - 'is_binned': False, - 'offset': 0.0, - 'scale': 1.5, - 'size': 20, - 'units': 's'}) + self.axes_list.insert( + 0, + { + "name": "time", + "navigate": True, + "is_binned": False, + "offset": 0.0, + "scale": 1.5, + "size": 20, + "units": "s", + }, + ) am = AxesManager(self.axes_list) - am.convert_units(axes='navigation', same_units=True) - assert am['time'].units == 's' - np.testing.assert_almost_equal(am['time'].scale, 1.5) - assert am['x'].units == 'nm' - np.testing.assert_almost_equal(am['x'].scale, 1.5) - assert am['y'].units == 'nm' - np.testing.assert_almost_equal(am['y'].scale, 0.5) - assert am['energy'].units == 'eV' - np.testing.assert_almost_equal(am['energy'].scale, 5) + am.convert_units(axes="navigation", same_units=True) + assert am["time"].units == "s" + np.testing.assert_almost_equal(am["time"].scale, 1.5) + assert am["x"].units == "nm" + np.testing.assert_almost_equal(am["x"].scale, 1.5) + assert am["y"].units == "nm" + np.testing.assert_almost_equal(am["y"].scale, 0.5) + assert am["energy"].units == "eV" + np.testing.assert_almost_equal(am["energy"].scale, 5) def test_convert_to_navigation_units_Undefined(self): - self.axes_list[0]['units'] = t.Undefined + self.axes_list[0]["units"] = t.Undefined am = AxesManager(self.axes_list) - am.convert_units(axes='navigation', same_units=True) - assert am['x'].units == t.Undefined - np.testing.assert_almost_equal(am['x'].scale, 1.5E-9) - assert am['y'].units == 'm' - np.testing.assert_almost_equal(am['y'].scale, 0.5E-9) - assert am['energy'].units == 'eV' - np.testing.assert_almost_equal(am['energy'].scale, 5) + am.convert_units(axes="navigation", same_units=True) + assert am["x"].units == t.Undefined + np.testing.assert_almost_equal(am["x"].scale, 1.5e-9) + assert am["y"].units == "m" + np.testing.assert_almost_equal(am["y"].scale, 0.5e-9) + assert am["energy"].units == "eV" + np.testing.assert_almost_equal(am["energy"].scale, 5) def test_convert_to_signal_units(self): - self.am.convert_units(axes='signal', units='keV') - np.testing.assert_almost_equal(self.am['x'].scale, self.axes_list[0]['scale']) - assert self.am['x'].units == self.axes_list[0]['units'] - np.testing.assert_almost_equal(self.am['y'].scale, self.axes_list[1]['scale']) - assert self.am['y'].units == self.axes_list[1]['units'] - np.testing.assert_almost_equal(self.am['energy'].scale, 0.005) - assert self.am['energy'].units == 'keV' + self.am.convert_units(axes="signal", units="keV") + np.testing.assert_almost_equal(self.am["x"].scale, self.axes_list[0]["scale"]) + assert self.am["x"].units == self.axes_list[0]["units"] + np.testing.assert_almost_equal(self.am["y"].scale, self.axes_list[1]["scale"]) + assert self.am["y"].units == self.axes_list[1]["units"] + np.testing.assert_almost_equal(self.am["energy"].scale, 0.005) + assert self.am["energy"].units == "keV" def test_convert_to_units_list(self): - self.am.convert_units(units=['µm', 'nm', 'meV'], same_units=False) - np.testing.assert_almost_equal(self.am['x'].scale, 1.5) - assert self.am['x'].units == 'nm' - np.testing.assert_almost_equal(self.am['y'].scale, 0.5E-3) - assert self.am['y'].units == 'µm' - np.testing.assert_almost_equal(self.am['energy'].scale, 5E3) - assert self.am['energy'].units == 'meV' + self.am.convert_units(units=["µm", "nm", "meV"], same_units=False) + np.testing.assert_almost_equal(self.am["x"].scale, 1.5) + assert self.am["x"].units == "nm" + np.testing.assert_almost_equal(self.am["y"].scale, 0.5e-3) + assert self.am["y"].units == "µm" + np.testing.assert_almost_equal(self.am["energy"].scale, 5e3) + assert self.am["energy"].units == "meV" def test_convert_to_units_list_same_units(self): - self.am2.convert_units(units=['µm', 'eV', 'meV'], same_units=True) - np.testing.assert_almost_equal(self.am2['x'].scale, 0.0015) - assert self.am2['x'].units == 'µm' - np.testing.assert_almost_equal(self.am2['energy'].scale, - self.axes_list2[1]['scale']) - assert self.am2['energy'].units == self.axes_list2[1]['units'] - np.testing.assert_almost_equal(self.am2['energy2'].scale, - self.axes_list2[2]['scale']) - assert self.am2['energy2'].units == self.axes_list2[2]['units'] + self.am2.convert_units(units=["µm", "eV", "meV"], same_units=True) + np.testing.assert_almost_equal(self.am2["x"].scale, 0.0015) + assert self.am2["x"].units == "µm" + np.testing.assert_almost_equal( + self.am2["energy"].scale, self.axes_list2[1]["scale"] + ) + assert self.am2["energy"].units == self.axes_list2[1]["units"] + np.testing.assert_almost_equal( + self.am2["energy2"].scale, self.axes_list2[2]["scale"] + ) + assert self.am2["energy2"].units == self.axes_list2[2]["units"] def test_convert_to_units_list_signal2D(self): - self.am2.convert_units(units=['µm', 'eV', 'meV'], same_units=False) - np.testing.assert_almost_equal(self.am2['x'].scale, 0.0015) - assert self.am2['x'].units == 'µm' - np.testing.assert_almost_equal(self.am2['energy'].scale, 2500) - assert self.am2['energy'].units == 'meV' - np.testing.assert_almost_equal(self.am2['energy2'].scale, 5.0) - assert self.am2['energy2'].units == 'eV' + self.am2.convert_units(units=["µm", "eV", "meV"], same_units=False) + np.testing.assert_almost_equal(self.am2["x"].scale, 0.0015) + assert self.am2["x"].units == "µm" + np.testing.assert_almost_equal(self.am2["energy"].scale, 2500) + assert self.am2["energy"].units == "meV" + np.testing.assert_almost_equal(self.am2["energy2"].scale, 5.0) + assert self.am2["energy2"].units == "eV" @pytest.mark.parametrize("same_units", (True, False)) def test_convert_to_units_unsupported_units(self, same_units): with pytest.warns(UserWarning, match="not supported for conversion."): - self.am.convert_units('navigation', units='toto', - same_units=same_units) - assert_deep_almost_equal(self.am._get_axes_dicts(), - self.axes_list) + self.am.convert_units("navigation", units="toto", same_units=same_units) + assert_deep_almost_equal(self.am._get_axes_dicts(), self.axes_list) def test_conversion_non_uniform_axis(self): - self.am._axes[0] = DataAxis(axis=np.arange(16)**2) + self.am._axes[0] = DataAxis(axis=np.arange(16) ** 2) with pytest.raises(NotImplementedError): self.am.convert_units() + + def test_initialize_UnitConversion_bug(self): + uc = UnitConversion(units="m", scale=1.0, offset=0) + assert uc.offset == 0 diff --git a/hyperspy/tests/axes/test_data_axis.py b/hyperspy/tests/axes/test_data_axis.py index 94e6db6e08..21e04ba811 100755 --- a/hyperspy/tests/axes/test_data_axis.py +++ b/hyperspy/tests/axes/test_data_axis.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import copy import math @@ -22,18 +22,22 @@ from unittest import mock import numpy as np -from numpy.testing import assert_allclose -import traits.api as t import pytest +import traits.api as t +from numpy.testing import assert_allclose -from hyperspy.axes import (BaseDataAxis, DataAxis, FunctionalDataAxis, - UniformDataAxis, create_axis) -from hyperspy.signals import Signal1D +from hyperspy.axes import ( + BaseDataAxis, + DataAxis, + FunctionalDataAxis, + UniformDataAxis, + create_axis, +) from hyperspy.misc.test_utils import assert_deep_almost_equal +from hyperspy.signals import Signal1D class TestBaseDataAxis: - def setup_method(self, method): self.axis = BaseDataAxis() @@ -47,37 +51,41 @@ def test_initialisation_BaseDataAxis_default(self): assert not self.axis.is_uniform def test_initialisation_BaseDataAxis(self): - axis = BaseDataAxis(name='named axis', units='s', navigate=True) - assert axis.name == 'named axis' - assert axis.units == 's' + axis = BaseDataAxis(name="named axis", units="s", navigate=True) + assert axis.name == "named axis" + assert axis.units == "s" assert axis.navigate assert not self.axis.is_uniform - assert_deep_almost_equal(axis.get_axis_dictionary(), - {'_type': 'BaseDataAxis', - 'name': 'named axis', - 'units': 's', - 'navigate': True, - 'is_binned': False}) + assert_deep_almost_equal( + axis.get_axis_dictionary(), + { + "_type": "BaseDataAxis", + "name": "named axis", + "units": "s", + "navigate": True, + "is_binned": False, + }, + ) def test_error_BaseDataAxis(self): with pytest.raises(NotImplementedError): self.axis._slice_me(1) with pytest.raises(ValueError): - self.axis._parse_value_from_string('') + self.axis._parse_value_from_string("") with pytest.raises(ValueError): - self.axis._parse_value_from_string('spam') + self.axis._parse_value_from_string("spam") - #Note: The following methods from BaseDataAxis rely on the self.axis.axis - #numpy array to be initialized, and are tested in the subclasses: - #BaseDataAxis.value2index --> tested in FunctionalDataAxis - #BaseDataAxis.index2value --> NOT EXPLICITLY TESTED - #BaseDataAxis.value_range_to_indices --> tested in UniformDataAxis - #BaseDataAxis.update_from --> tested in DataAxis and FunctionalDataAxis + # Note: The following methods from BaseDataAxis rely on the self.axis.axis + # numpy array to be initialized, and are tested in the subclasses: + # BaseDataAxis.value2index --> tested in FunctionalDataAxis + # BaseDataAxis.index2value --> NOT EXPLICITLY TESTED + # BaseDataAxis.value_range_to_indices --> tested in UniformDataAxis + # BaseDataAxis.update_from --> tested in DataAxis and FunctionalDataAxis -class TestDataAxis: +class TestDataAxis: def setup_method(self, method): - self._axis = np.arange(16)**2 + self._axis = np.arange(16) ** 2 self.axis = DataAxis(axis=self._axis) def _test_initialisation_parameters(self, axis): @@ -92,12 +100,12 @@ def test_create_axis(self): self._test_initialisation_parameters(axis) def test_axis_value(self): - assert_allclose(self.axis.axis, np.arange(16)**2) + assert_allclose(self.axis.axis, np.arange(16) ** 2) assert self.axis.size == 16 assert not self.axis.is_uniform def test_update_axes(self): - values = np.arange(20)**2 + values = np.arange(20) ** 2 self.axis.axis = values.tolist() self.axis.update_axis() assert self.axis.size == 20 @@ -110,7 +118,7 @@ def test_update_axes2(self): assert_allclose(self.axis.axis, values) def test_update_axis_from_list(self): - values = np.arange(16)**2 + values = np.arange(16) ** 2 self.axis.axis = values.tolist() self.axis.update_axis() assert_allclose(self.axis.axis, values) @@ -143,23 +151,22 @@ def test_value_changed_event(self): def test_deepcopy(self): ac = copy.deepcopy(self.axis) - np.testing.assert_allclose(ac.axis, np.arange(16)**2) - ac.name = 'name changed' - assert ac.name == 'name changed' + np.testing.assert_allclose(ac.axis, np.arange(16) ** 2) + ac.name = "name changed" + assert ac.name == "name changed" assert self.axis.name != ac.name def test_slice_me(self): assert self.axis._slice_me(slice(1, 5)) == slice(1, 5) assert self.axis.size == 4 - np.testing.assert_allclose(self.axis.axis, np.arange(1, 5)**2) + np.testing.assert_allclose(self.axis.axis, np.arange(1, 5) ** 2) def test_slice_me_step(self): assert self.axis._slice_me(slice(0, 10, 2)) == slice(0, 10, 2) assert self.axis.size == 5 - np.testing.assert_allclose(self.axis.axis, np.arange(0, 10, 2)**2) + np.testing.assert_allclose(self.axis.axis, np.arange(0, 10, 2) ** 2) def test_convert_to_uniform_axis(self): - scale = (self.axis.high_value - self.axis.low_value) / self.axis.size is_binned = self.axis.is_binned navigate = self.axis.navigate self.axis.name = "parrot" @@ -171,14 +178,22 @@ def test_convert_to_uniform_axis(self): assert s.axes_manager[0].name == "parrot" assert s.axes_manager[0].units == "plumage" assert s.axes_manager[0].size == 16 - assert s.axes_manager[0].scale == scale + np.testing.assert_allclose(s.axes_manager[0].scale, 15) assert s.axes_manager[0].offset == 0 assert s.axes_manager[0].low_value == 0 - assert s.axes_manager[0].high_value == 15 * scale + assert s.axes_manager[0].high_value == 15 * 15 assert index_in_array == s.axes_manager[0].index_in_array assert is_binned == s.axes_manager[0].is_binned assert navigate == s.axes_manager[0].navigate + def test_convert_to_uniform_axis_keep_bounds_False(self): + # estimated offset, scale using numpy polyfit + s = Signal1D(np.arange(10), axes=[self.axis]) + s.axes_manager[0].convert_to_uniform_axis(keep_bounds=False) + assert s.axes_manager[0].size == 16 + np.testing.assert_allclose(s.axes_manager[0].scale, 15) + np.testing.assert_allclose(s.axes_manager[0].offset, -35.0) + def test_value2index(self): assert self.axis.value2index(10.15) == 3 assert self.axis.value2index(60) == 8 @@ -191,33 +206,32 @@ def test_value2index(self): # test rounding on negative value assert self.axis.value2index(-1.5, rounding=round) == 1 - def test_value2index_error(self): with pytest.raises(ValueError): self.axis.value2index(226) def test_parse_value_from_relative_string(self): ax = self.axis - assert ax._parse_value_from_string('rel0.0') == 0.0 - assert ax._parse_value_from_string('rel0.5') == 112.5 - assert ax._parse_value_from_string('rel1.0') == 225.0 + assert ax._parse_value_from_string("rel0.0") == 0.0 + assert ax._parse_value_from_string("rel0.5") == 112.5 + assert ax._parse_value_from_string("rel1.0") == 225.0 with pytest.raises(ValueError): - ax._parse_value_from_string('rela0.5') + ax._parse_value_from_string("rela0.5") with pytest.raises(ValueError): - ax._parse_value_from_string('rel1.5') + ax._parse_value_from_string("rel1.5") with pytest.raises(ValueError): - ax._parse_value_from_string('abcd') + ax._parse_value_from_string("abcd") def test_parse_value_from_string_with_units(self): ax = self.axis - ax.units = 'nm' + ax.units = "nm" with pytest.raises(ValueError): - ax._parse_value_from_string('0.02 um') + ax._parse_value_from_string("0.02 um") @pytest.mark.parametrize("use_indices", (False, True)) def test_crop(self, use_indices): axis = DataAxis(axis=self._axis) - start, end = 4., 196. + start, end = 4.0, 196.0 if use_indices: start = axis.value2index(start) end = axis.value2index(end) @@ -243,44 +257,41 @@ def test_crop_reverses_indexing(self): def test_error_DataAxis(self): with pytest.raises(ValueError): - _ = DataAxis(axis=np.arange(16)**2, _type='UniformDataAxis') + _ = DataAxis(axis=np.arange(16) ** 2, _type="UniformDataAxis") with pytest.raises(AttributeError): self.axis.index_in_axes_manager() with pytest.raises(IndexError): self.axis._get_positive_index(-17) with pytest.raises(ValueError): - self.axis._get_array_slices(slice_=slice(1,2,1.5)) + self.axis._get_array_slices(slice_=slice(1, 2, 1.5)) with pytest.raises(IndexError): - self.axis._get_array_slices(slice_=slice(225,-1.1,1)) + self.axis._get_array_slices(slice_=slice(225, -1.1, 1)) with pytest.raises(IndexError): - self.axis._get_array_slices(slice_=slice(225.1,0,1)) + self.axis._get_array_slices(slice_=slice(225.1, 0, 1)) def test_update_from(self): ax2 = DataAxis(units="plumage", name="parrot", axis=np.arange(16)) self.axis.update_from(ax2, attributes=("units", "name")) - assert ((ax2.units, ax2.name) == - (self.axis.units, self.axis.name)) + assert (ax2.units, ax2.name) == (self.axis.units, self.axis.name) def test_calibrate(self): with pytest.raises(TypeError, match="only for uniform axes"): - self.axis.calibrate(value_tuple=(11,12), index_tuple=(0,5)) + self.axis.calibrate(value_tuple=(11, 12), index_tuple=(0, 5)) class TestFunctionalDataAxis: - def setup_method(self, method): expression = "x ** power" self.axis = FunctionalDataAxis( size=10, expression=expression, - power=2,) + power=2, + ) def test_initialisation_parameters(self): axis = self.axis assert axis.power == 2 - np.testing.assert_allclose( - axis.axis, - np.arange(10)**2) + np.testing.assert_allclose(axis.axis, np.arange(10) ** 2) def test_create_axis(self): axis = create_axis(**self.axis.get_axis_dictionary()) @@ -288,14 +299,11 @@ def test_create_axis(self): def test_initialisation_errors(self): expression = "x ** power" - with pytest.raises(ValueError, match="Please provide"): - self.axis = FunctionalDataAxis( - expression=expression, - power=2,) with pytest.raises(ValueError, match="The values of"): self.axis = FunctionalDataAxis( size=10, - expression=expression,) + expression=expression, + ) @pytest.mark.parametrize("use_indices", (True, False)) def test_crop(self, use_indices): @@ -306,8 +314,8 @@ def test_crop(self, use_indices): end = -1 axis.crop(start, end) assert axis.size == 7 - np.testing.assert_almost_equal(axis.axis[0], 4.) - np.testing.assert_almost_equal(axis.axis[-1], 64.) + np.testing.assert_almost_equal(axis.axis[0], 4.0) + np.testing.assert_almost_equal(axis.axis[-1], 64.0) def test_convert_to_non_uniform_axis(self): axis = np.copy(self.axis.axis) @@ -335,78 +343,84 @@ def test_convert_to_non_uniform_axis(self): assert is_binned == s.axes_manager[0].is_binned assert navigate == s.axes_manager[0].navigate + def test_convert_to_uniform_axis(self): + ax = FunctionalDataAxis(size=10, expression="a * x + b", a=2, b=100) + axis_before = copy.deepcopy(ax.axis) + ax.convert_to_uniform_axis() + np.testing.assert_allclose(axis_before, ax.axis) + def test_update_from(self): ax2 = FunctionalDataAxis(size=2, units="nm", expression="x ** power", power=3) self.axis.update_from(ax2, attributes=("units", "power")) - assert ((ax2.units, ax2.power) == - (self.axis.units, self.axis.power)) + assert (ax2.units, ax2.power) == (self.axis.units, self.axis.power) def test_slice_me(self): assert self.axis._slice_me(slice(1, 5)) == slice(1, 5) assert self.axis.size == 4 - np.testing.assert_allclose(self.axis.axis, np.arange(1, 5)**2) + np.testing.assert_allclose(self.axis.axis, np.arange(1, 5) ** 2) def test_calibrate(self): with pytest.raises(TypeError, match="only for uniform axes"): - self.axis.calibrate(value_tuple=(11,12), index_tuple=(0,5)) + self.axis.calibrate(value_tuple=(11, 12), index_tuple=(0, 5)) def test_functional_value2index(self): - #Tests for value2index - #Works as intended + # Tests for value2index + # Works as intended assert self.axis.value2index(44.7) == 7 assert self.axis.value2index(2.5, rounding=round) == 1 assert self.axis.value2index(2.5, rounding=math.ceil) == 2 assert self.axis.value2index(2.5, rounding=math.floor) == 1 # Returns integer assert isinstance(self.axis.value2index(45), (int, np.integer)) - #Input None --> output None - assert self.axis.value2index(None) == None - #NaN in --> error out + # Input None --> output None + assert self.axis.value2index(None) is None + # NaN in --> error out with pytest.raises(ValueError): self.axis.value2index(np.nan) - #Values in out of bounds --> error out (both sides of axis) + # Values in out of bounds --> error out (both sides of axis) with pytest.raises(ValueError): self.axis.value2index(-2) with pytest.raises(ValueError): self.axis.value2index(111) - #str in --> error out + # str in --> error out with pytest.raises(TypeError): self.axis.value2index("69") - #Empty str in --> error out + # Empty str in --> error out with pytest.raises(TypeError): self.axis.value2index("") - #Tests with array Input - #Array in --> array out - arval = np.array([[0,4],[16.,36.]]) - assert np.all(self.axis.value2index(arval) == np.array([[0,2],[4,6]])) - #One value out of bound in array in --> error out (both sides) - arval[1,1] = 111 + # Tests with array Input + # Array in --> array out + arval = np.array([[0, 4], [16.0, 36.0]]) + assert np.all(self.axis.value2index(arval) == np.array([[0, 2], [4, 6]])) + # One value out of bound in array in --> error out (both sides) + arval[1, 1] = 111 with pytest.raises(ValueError): self.axis.value2index(arval) - arval[1,1] = -0.3 + arval[1, 1] = -0.3 with pytest.raises(ValueError): self.axis.value2index(arval) - #One NaN in array in --> error out - arval[1,1] = np.nan + # One NaN in array in --> error out + arval[1, 1] = np.nan with pytest.raises(ValueError): self.axis.value2index(arval) - #Single-value-array-in --> scalar out + # Single-value-array-in --> scalar out arval = np.array([1.0]) assert np.isscalar(self.axis.value2index(arval)) class TestReciprocalDataAxis: - def setup_method(self, method): expression = "a / (x + 1) + b" - self.axis = FunctionalDataAxis(size=10, expression=expression, - a=0.1, b=10) + self.axis = FunctionalDataAxis(size=10, expression=expression, a=0.1, b=10) def _test_initialisation_parameters(self, axis): assert axis.a == 0.1 assert axis.b == 10 - def func(x): return 0.1 / (x + 1) + 10 + + def func(x): + return 0.1 / (x + 1) + 10 + np.testing.assert_allclose(axis.axis, func(np.arange(10))) def test_initialisation_parameters(self): @@ -417,7 +431,6 @@ def test_create_axis(self): assert isinstance(axis, FunctionalDataAxis) self._test_initialisation_parameters(axis) - @pytest.mark.parametrize("use_indices", (True, False)) def test_crop(self, use_indices): axis = self.axis @@ -432,14 +445,16 @@ def test_crop(self, use_indices): class TestUniformDataAxis: - def setup_method(self, method): self.axis = UniformDataAxis(size=10, scale=0.1, offset=10) def _test_initialisation_parameters(self, axis): assert axis.scale == 0.1 assert axis.offset == 10 - def func(x): return axis.scale * x + axis.offset + + def func(x): + return axis.scale * x + axis.offset + np.testing.assert_allclose(axis.axis, func(np.arange(10))) def test_initialisation_parameters(self): @@ -452,9 +467,7 @@ def test_create_axis(self): def test_value_range_to_indices_in_range(self): assert self.axis.is_uniform - assert ( - self.axis.value_range_to_indices( - 10.1, 10.8) == (1, 8)) + assert self.axis.value_range_to_indices(10.1, 10.8) == (1, 8) def test_value_range_to_indices_endpoints(self): assert self.axis.value_range_to_indices(10, 10.9) == (0, 9) @@ -482,97 +495,95 @@ def test_deepcopy_on_trait_change(self): assert ac.axis[0] == ac.offset def test_uniform_value2index(self): - #Tests for value2index - #Works as intended + # Tests for value2index + # Works as intended assert self.axis.value2index(10.15) == 1 assert self.axis.value2index(10.17, rounding=math.floor) == 1 assert self.axis.value2index(10.13, rounding=math.ceil) == 2 # Test that output is integer assert isinstance(self.axis.value2index(10.15), (int, np.integer)) - #Endpoint left - assert self.axis.value2index(10.) == 0 - #Endpoint right + # Endpoint left + assert self.axis.value2index(10.0) == 0 + # Endpoint right assert self.axis.value2index(10.9) == 9 - #Input None --> output None - assert self.axis.value2index(None) == None - #NaN in --> error out + # Input None --> output None + assert self.axis.value2index(None) is None + # NaN in --> error out with pytest.raises(ValueError): self.axis.value2index(np.nan) - #Values in out of bounds --> error out (both sides of axis) + # Values in out of bounds --> error out (both sides of axis) with pytest.raises(ValueError): self.axis.value2index(-2) with pytest.raises(ValueError): self.axis.value2index(111) - #str without unit in --> error out + # str without unit in --> error out with pytest.raises(ValueError): self.axis.value2index("69") - #Empty str in --> error out + # Empty str in --> error out with pytest.raises(ValueError): self.axis.value2index("") - #Value with unit when axis is unitless --> Error out + # Value with unit when axis is unitless --> Error out with pytest.raises(ValueError): self.axis.value2index("0.0101um") - #Tests with array Input - #Arrays work as intended + # Tests with array Input + # Arrays work as intended arval = np.array([[10.15, 10.15], [10.24, 10.28]]) - assert np.all(self.axis.value2index(arval) \ - == np.array([[1, 1], [2, 3]])) - assert np.all(self.axis.value2index(arval, rounding=math.floor) \ - == np.array([[1, 1], [2, 2]])) - assert np.all(self.axis.value2index(arval, rounding=math.ceil)\ - == np.array([[2, 2], [3, 3]])) - #List in --> array out - assert np.all(self.axis.value2index(arval.tolist()) \ - == np.array([[1, 1], [2, 3]])) - #One value out of bound in array in --> error out (both sides) + assert np.all(self.axis.value2index(arval) == np.array([[1, 1], [2, 3]])) + assert np.all( + self.axis.value2index(arval, rounding=math.floor) + == np.array([[1, 1], [2, 2]]) + ) + assert np.all( + self.axis.value2index(arval, rounding=math.ceil) + == np.array([[2, 2], [3, 3]]) + ) + # List in --> array out + assert np.all( + self.axis.value2index(arval.tolist()) == np.array([[1, 1], [2, 3]]) + ) + # One value out of bound in array in --> error out (both sides) arval[1, 1] = 111 with pytest.raises(ValueError): self.axis.value2index(arval) arval[1, 1] = -0.3 with pytest.raises(ValueError): self.axis.value2index(arval) - #One NaN in array in --> error out - if platform.machine() != 'aarch64': + # One NaN in array in --> error out + if platform.machine() != "aarch64" and platform.machine() != "arm64": # Skip aarch64 platform because it doesn't raise error arval[1, 1] = np.nan with pytest.raises(ValueError): self.axis.value2index(arval) - #Copy of axis with units + # Copy of axis with units axis = copy.deepcopy(self.axis) - axis.units = 'nm' + axis.units = "nm" - #Value with unit in --> OK out + # Value with unit in --> OK out assert axis.value2index("0.0101um") == 1 - #Value with relative units in --> OK out + # Value with relative units in --> OK out assert self.axis.value2index("rel0.5") == 4 - #Works with arrays of values with units in + # Works with arrays of values with units in np.testing.assert_allclose( - axis.value2index(['0.01um', '0.0101um', '0.0103um']), - np.array([0, 1, 3]) - ) - #Raises errors if a weird unit is passed in + axis.value2index(["0.01um", "0.0101um", "0.0103um"]), np.array([0, 1, 3]) + ) + # Raises errors if a weird unit is passed in with pytest.raises(BaseException): - axis.value2index(["0.01uma", '0.0101uma', '0.0103uma']) - #Values + axis.value2index(["0.01uma", "0.0101uma", "0.0103uma"]) + # Values np.testing.assert_allclose( - self.axis.value2index(["rel0.0", "rel0.5", "rel1.0"]), - np.array([0, 4, 9]) - ) + self.axis.value2index(["rel0.0", "rel0.5", "rel1.0"]), np.array([0, 4, 9]) + ) def test_slice_me(self): - assert ( - self.axis._slice_me(slice(np.float32(10.2), 10.4, 2)) == - slice(2, 4, 2) - ) + assert self.axis._slice_me(slice(np.float32(10.2), 10.4, 2)) == slice(2, 4, 2) def test_update_from(self): ax2 = UniformDataAxis(size=2, units="nm", scale=0.5) self.axis.update_from(ax2, attributes=("units", "scale")) - assert ((ax2.units, ax2.scale) == - (self.axis.units, self.axis.scale)) + assert (ax2.units, ax2.scale) == (self.axis.units, self.axis.scale) def test_value_changed_event(self): ax = self.axis @@ -626,14 +637,14 @@ def test_convert_to_functional_data_axis(self): self.axis.units = "plumage" s = Signal1D(np.arange(10), axes=[self.axis]) index_in_array = s.axes_manager[0].index_in_array - s.axes_manager[0].convert_to_functional_data_axis(expression = 'x**2') + s.axes_manager[0].convert_to_functional_data_axis(expression="x**2") assert isinstance(s.axes_manager[0], FunctionalDataAxis) assert s.axes_manager[0].name == "parrot" assert s.axes_manager[0].units == "plumage" assert s.axes_manager[0].size == 10 assert s.axes_manager[0].low_value == 10**2 - assert s.axes_manager[0].high_value == (10 + 0.1 * 9)**2 - assert s.axes_manager[0]._expression == 'x**2' + assert s.axes_manager[0].high_value == (10 + 0.1 * 9) ** 2 + assert s.axes_manager[0]._expression == "x**2" assert isinstance(s.axes_manager[0].x, UniformDataAxis) np.testing.assert_allclose(s.axes_manager[0].axis, axis**2) with pytest.raises(AttributeError): @@ -692,22 +703,22 @@ def test_crop_reverses_indexing(self, mixed): def test_parse_value(self): ax = copy.deepcopy(self.axis) - ax.units = 'nm' + ax.units = "nm" # slicing by index assert ax._parse_value(5) == 5 - assert type(ax._parse_value(5)) is int + assert isinstance(ax._parse_value(5), int) # slicing by calibrated value assert ax._parse_value(10.5) == 10.5 - assert type(ax._parse_value(10.5)) is float + assert isinstance(ax._parse_value(10.5), float) # slicing by unit - assert ax._parse_value('10.5nm') == 10.5 - np.testing.assert_almost_equal(ax._parse_value('10500pm'), 10.5) + assert ax._parse_value("10.5nm") == 10.5 + np.testing.assert_almost_equal(ax._parse_value("10500pm"), 10.5) def test_parse_value_from_relative_string(self): ax = self.axis - assert ax._parse_value_from_string('rel0.0') == 10.0 - assert ax._parse_value_from_string('rel0.5') == 10.45 - assert ax._parse_value_from_string('rel1.0') == 10.9 + assert ax._parse_value_from_string("rel0.0") == 10.0 + assert ax._parse_value_from_string("rel0.5") == 10.45 + assert ax._parse_value_from_string("rel1.0") == 10.9 def test_slice_empty_string(self): ax = self.axis @@ -715,17 +726,17 @@ def test_slice_empty_string(self): ax._parse_value("") def test_calibrate(self): - offset, scale = self.axis.calibrate(value_tuple=(11,12), \ - index_tuple=(0,5), modify_calibration=False) + offset, scale = self.axis.calibrate( + value_tuple=(11, 12), index_tuple=(0, 5), modify_calibration=False + ) assert scale == 0.2 assert offset == 11 - self.axis.calibrate(value_tuple=(11,12), index_tuple=(0,5)) + self.axis.calibrate(value_tuple=(11, 12), index_tuple=(0, 5)) assert self.axis.scale == 0.2 assert self.axis.offset == 11 class TestUniformDataAxisValueRangeToIndicesNegativeScale: - def setup_method(self, method): self.axis = UniformDataAxis(size=10, scale=-0.1, offset=10) @@ -755,18 +766,17 @@ def test_rounding_consistency_axis_type(): ax = UniformDataAxis(scale=scale, offset=offset, size=3) ua_idx = ax.value2index(value) nua_idx = super(type(ax), ax).value2index(value) - print('scale', scale) - print('offset', offset) - print('Axis values:', ax.axis) + print("scale", scale) + print("offset", offset) + print("Axis values:", ax.axis) print(f"value: {value} --> uniform: {ua_idx}, non-uniform: {nua_idx}") print("\n") assert nua_idx == ua_idx -@pytest.mark.parametrize('shift', (0.05, 0.025)) +@pytest.mark.parametrize("shift", (0.05, 0.025)) def test_rounding_consistency_axis_type_half(shift): - - axis = UniformDataAxis(size=20, scale=0.1, offset=-1.0); + axis = UniformDataAxis(size=20, scale=0.1, offset=-1.0) test_vals = axis.axis[:-1] + shift uaxis_indices = axis.value2index(test_vals) diff --git a/hyperspy/tests/component/data/hs14_model.hspy b/hyperspy/tests/component/data/hs14_model.hspy new file mode 100644 index 0000000000..db1aa4feaf Binary files /dev/null and b/hyperspy/tests/component/data/hs14_model.hspy differ diff --git a/hyperspy/tests/component/data/hs15_model.hspy b/hyperspy/tests/component/data/hs15_model.hspy new file mode 100644 index 0000000000..9c561cd61b Binary files /dev/null and b/hyperspy/tests/component/data/hs15_model.hspy differ diff --git a/hyperspy/tests/component/data/hs16_model.hspy b/hyperspy/tests/component/data/hs16_model.hspy new file mode 100644 index 0000000000..20b93a6c17 Binary files /dev/null and b/hyperspy/tests/component/data/hs16_model.hspy differ diff --git a/hyperspy/tests/component/data/hs2.0_custom_component.hspy b/hyperspy/tests/component/data/hs2.0_custom_component.hspy new file mode 100644 index 0000000000..076d980def Binary files /dev/null and b/hyperspy/tests/component/data/hs2.0_custom_component.hspy differ diff --git a/hyperspy/tests/component/data/hs211_model_polynomial_reload.hspy b/hyperspy/tests/component/data/hs211_model_polynomial_reload.hspy new file mode 100644 index 0000000000..a917225cd2 Binary files /dev/null and b/hyperspy/tests/component/data/hs211_model_polynomial_reload.hspy differ diff --git a/hyperspy/tests/component/test_EELSarctan.py b/hyperspy/tests/component/test_EELSarctan.py deleted file mode 100644 index 38d40217cc..0000000000 --- a/hyperspy/tests/component/test_EELSarctan.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - -from hyperspy.components1d import EELSArctan - - -def test_function2(): - g = EELSArctan() - g.A.value = 10 - g.k.value = 2 - g.x0.value = 1 - np.testing.assert_allclose(g.function(0), 4.63647609) - np.testing.assert_allclose(g.function(1), 10*np.pi/2) - np.testing.assert_allclose(g.function(1e4), 10*np.pi,1e-4) diff --git a/hyperspy/tests/component/test_arctan.py b/hyperspy/tests/component/test_arctan.py index 12b619ab2a..9eb9f95a32 100644 --- a/hyperspy/tests/component/test_arctan.py +++ b/hyperspy/tests/component/test_arctan.py @@ -1,26 +1,24 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import pytest import numpy as np from hyperspy.components1d import Arctan -from hyperspy.exceptions import VisibleDeprecationWarning def test_function(): @@ -30,28 +28,4 @@ def test_function(): g.x0.value = 1 np.testing.assert_allclose(g.function(0), -11.07148718) np.testing.assert_allclose(g.function(1), 0) - np.testing.assert_allclose(g.function(1e4), 10*np.pi/2,1e-4) - -# Legacy tests -def test_function_legacyF(): - g = Arctan(minimum_at_zero=False) - g.A.value = 10 - g.k.value = 2 - g.x0.value = 1 - np.testing.assert_allclose(g.function(0), -11.07148718) - np.testing.assert_allclose(g.function(1), 0) - np.testing.assert_allclose(g.function(1e4), 10*np.pi/2,1e-4) - -def test_function_legacyT(): - with pytest.warns( - VisibleDeprecationWarning, - match="component will change in v2.0.", - ): - g = Arctan(minimum_at_zero=True) - - g.A.value = 10 - g.k.value = 2 - g.x0.value = 1 - np.testing.assert_allclose(g.function(0), 4.63647609) - np.testing.assert_allclose(g.function(1), 10*np.pi/2) - np.testing.assert_allclose(g.function(1e4), 10*np.pi,1e-4) + np.testing.assert_allclose(g.function(1e4), 10 * np.pi / 2, 1e-4) diff --git a/hyperspy/tests/component/test_backcompatibility.py b/hyperspy/tests/component/test_backcompatibility.py new file mode 100644 index 0000000000..00c62d3c4b --- /dev/null +++ b/hyperspy/tests/component/test_backcompatibility.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import importlib +import pathlib + +import numpy as np +import pytest + +import hyperspy.api as hs +from hyperspy.exceptions import VisibleDeprecationWarning + +DIRPATH = pathlib.Path(__file__).parent / "data" +EXSPY_SPEC = importlib.util.find_spec("exspy") + +""" +The reference files from hyperspy v1.4 have been created using: + + import hyperspy + import hyperspy.api as hs + + s = hs.signals.Signal1D(range(100)) + m = s.create_model() + + gaussian = hs.model.components1D.Gaussian(A=13, centre=9, sigma=2) + arctan = hs.model.components1D.Arctan(minimum_at_zero=False, A=5, k=1.5, x0=75.5) + arctan_eels = hs.model.components1D.Arctan(minimum_at_zero=True, A=5, k=1.5, x0=22.5) + voigt = hs.model.components1D.Voigt() + voigt.area.value = 100 + voigt.centre.value = 50 + voigt.FWHM.value = 1.5 + voigt.gamma.value = 2.5 + polynomial = hs.model.components1D.Polynomial() + polynomial.coefficients.value = [0.01, -0.5, 25] + + + m.extend([gaussian, arctan, arctan_eels, polynomial, voigt]) + m.plot() + + m.store('a') + + version = "".join(hyperspy.__version__.split('.')[:2]) + print("version:", version) + s.save(f'hs{version}_model.hspy', overwrite=True) +""" + + +@pytest.mark.parametrize( + ("versionfile"), ("hs14_model.hspy", "hs15_model.hspy", "hs16_model.hspy") +) +def test_model_backcompatibility(versionfile): + if EXSPY_SPEC is not None: + with pytest.warns(VisibleDeprecationWarning): + # binned deprecated warning + s = hs.load(DIRPATH / versionfile) + + m = s.models.restore("a") + + assert len(m) == 5 + + g = m[0] + assert g.name == "Gaussian" + assert len(g.parameters) == 3 + assert g.A.value == 13 + assert g.centre.value == 9 + assert g.sigma.value == 2 + + a = m[1] + assert a.name == "Arctan" + assert len(a.parameters) == 3 + assert a.A.value == 5 + assert a.k.value == 1.5 + assert a.x0.value == 75.5 + + a_eels = m[2] + assert len(a_eels.parameters) == 3 + assert a_eels.A.value == 5 + assert a_eels.k.value == 1.5 + assert a_eels.x0.value == 22.5 + + p = m[3] + assert p.name == "Polynomial" + assert len(p.parameters) == 3 + assert p.a0.value == 25.0 + assert p.a1.value == -0.5 + assert p.a2.value == 0.01 + + p = m[4] + assert len(p.parameters) == 8 + assert p.area.value == 100.0 + assert p.centre.value == 50.0 + assert p.FWHM.value == 1.5 + assert p.resolution.value == 0.0 + else: + with pytest.warns(VisibleDeprecationWarning): + with pytest.raises(ImportError): + s = hs.load(DIRPATH / versionfile) + m = s.models.restore("a") + + +def test_loading_components_exspy_not_installed(): + with pytest.warns(VisibleDeprecationWarning): + # warning is for old binning API + s = hs.load(DIRPATH / "hs16_model.hspy") + + if EXSPY_SPEC is None: + # This should raise an ImportError with + # a suitable error message + with pytest.raises(ImportError) as err: + _ = s.models.restore("a") + assert "exspy is not installed" in str(err.value) + else: + # The model contains components using numexpr + pytest.importorskip("numexpr") + # This should work fine + _ = s.models.restore("a") + + +def test_load_polynomial(tmp_path): + # For hyperspy >= 1.5 (with new polynomial API) and <= 2.1.1 + # the polynomial component was saved incorrectly: + # saving initialisation polynomial order was missing + + s2 = hs.load(DIRPATH / "hs211_model_polynomial_reload.hspy") + assert s2.metadata.get_item("General.FileIO.0.hyperspy_version") == "2.1.1" + + m2 = s2.models.restore("a") + polynomial = m2[0] + for i, p in enumerate(polynomial.parameters): + np.testing.assert_allclose(p.value, i, atol=0.002) + + # Test for current version + c = hs.model.components1D.Polynomial(order=3) + c.function(np.arange(10)) + + for i, p in enumerate(c.parameters): + p.value = i + + s = hs.signals.Signal1D(c.function(np.arange(100))) + + m = s.create_model() + m.append(hs.model.components1D.Polynomial(order=3)) + m.fit() + + fname = tmp_path / "polynomial_save_load_cycle.hspy" + m.save(fname) + + s2 = hs.load(fname) + m2 = s2.models.restore("a") + polynomial = m2[0] + for i, p in enumerate(polynomial.parameters): + np.testing.assert_allclose(p.value, i, atol=0.002) diff --git a/hyperspy/tests/component/test_bleasdale.py b/hyperspy/tests/component/test_bleasdale.py index 6a3d173bdb..98d57becf1 100644 --- a/hyperspy/tests/component/test_bleasdale.py +++ b/hyperspy/tests/component/test_bleasdale.py @@ -1,28 +1,29 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - +# along with HyperSpy. If not, see . import numpy as np +import pytest from hyperspy.components1d import Bleasdale def test_function(): + pytest.importorskip("numexpr") g = Bleasdale() g.a.value = 1 g.b.value = 2 @@ -30,9 +31,16 @@ def test_function(): assert g.function(-0.5) == 0 assert g.function(0) == 1 assert g.function(12) == 0.2 - np.testing.assert_allclose(g.function(-.48),5) + np.testing.assert_allclose(g.function(-0.48), 5) assert g.grad_a(0) == -0.5 assert g.grad_b(0) == 0 assert g.grad_c(0) == 0 assert g.grad_a(-1) == 0 assert g.grad_b(-1) == 0 + + +def test_module_error(): + with pytest.raises(ValueError): + Bleasdale(module="numpy") + with pytest.raises(ValueError): + Bleasdale(module="scipy") diff --git a/hyperspy/tests/component/test_component.py b/hyperspy/tests/component/test_component.py index 7acd2638bc..5c7747b048 100644 --- a/hyperspy/tests/component/test_component.py +++ b/hyperspy/tests/component/test_component.py @@ -1,39 +1,41 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -import pytest +import pathlib from unittest import mock import numpy as np +import pytest -from hyperspy.axes import AxesManager -from hyperspy.component import Component, _get_scaling_factor +import hyperspy.api as hs from hyperspy._signals.signal1d import Signal1D +from hyperspy.axes import AxesManager +from hyperspy.component import Component, Parameter, _get_scaling_factor +DIRPATH = pathlib.Path(__file__).parent / "data" -class TestMultidimensionalActive: +class TestMultidimensionalActive: def setup_method(self, method): self.c = Component(["parameter"]) - self.c._axes_manager = AxesManager([{"size": 3, - "navigate": True}, - {"size": 2, - "navigate": True}]) + self.c._axes_manager = AxesManager( + [{"size": 3, "navigate": True}, {"size": 2, "navigate": True}] + ) def test_enable_pixel_switching_current_on(self): c = self.c @@ -74,7 +76,7 @@ def test_disable_pixel_switching_current_off(self): def test_update_number_free_parameters(): - c = Component(['one', 'two', 'three']) + c = Component(["one", "two", "three"]) c.one.free = False c.two.free = True c.three.free = True @@ -85,11 +87,11 @@ def test_update_number_free_parameters(): assert c._nfree_param == 5 # check that only the correct parameters are in the list _AND_ the list is # name-ordered - assert [c.three, c.two] == c.free_parameters + assert [c.three, c.two] == c._free_parameters + assert (c.three, c.two) == c.free_parameters class TestGeneralMethods: - def setup_method(self, method): self.c = Component(["one", "two"]) self.c.one.free = False @@ -101,10 +103,10 @@ def test_export_free(self): c = self.c c.one.export = mock.MagicMock() c.two.export = mock.MagicMock() - c.free_parameters = {c.two, } - call_args = {'folder': 'folder1', - 'format': 'format1', - 'save_std': 'save_std1'} + c._free_parameters = { + c.two, + } + call_args = {"folder": "folder1", "format": "format1", "save_std": "save_std1"} c.export(only_free=True, **call_args) assert c.two.export.call_args[1] == call_args assert not c.one.export.called @@ -113,10 +115,10 @@ def test_export_all_no_twins(self): c = self.c c.one.export = mock.MagicMock() c.two.export = mock.MagicMock() - c.free_parameters = {c.two, } - call_args = {'folder': 'folder1', - 'format': 'format1', - 'save_std': 'save_std1'} + c._free_parameters = { + c.two, + } + call_args = {"folder": "folder1", "format": "format1", "save_std": "save_std1"} c.export(only_free=False, **call_args) assert c.two.export.call_args[1] == call_args assert c.one.export.call_args[1] == call_args @@ -126,10 +128,10 @@ def test_export_all_twins(self): c.one.export = mock.MagicMock() c.two.export = mock.MagicMock() c.two.twin = c.one - c.free_parameters = {c.two, } - call_args = {'folder': 'folder1', - 'format': 'format1', - 'save_std': 'save_std1'} + c._free_parameters = { + c.two, + } + call_args = {"folder": "folder1", "format": "format1", "save_std": "save_std1"} c.export(only_free=False, **call_args) assert c.one.export.call_args[1] == call_args assert not c.two.export.called @@ -151,7 +153,7 @@ def test_fetch_from_array(self): def test_fetch_from_array_free(self): arr = np.array([30, 20, 10]) arr_std = np.array([30.5, 20.5, 10.5]) - self.c.one.value = 1. + self.c.one.value = 1.0 self.c.one.std = np.nan self.c.fetch_values_from_array(arr, p_std=arr_std, onlyfree=True) assert self.c.one.value == 1 @@ -177,7 +179,7 @@ def test_fetch_stored_values_all(self): def test_fetch_stored_values_all_twinned_bad(self): c = self.c - c.one._twin = 1. + c.one._twin = 1.0 c.one.fetch = mock.MagicMock() c.two.fetch = mock.MagicMock() c.fetch_stored_values() @@ -198,8 +200,12 @@ def test_set_parameters_free_all(self): assert self.c.one.free assert self.c.two.free + def test_set_parameters_free_only_linear_only_nonlinear(self): + with pytest.raises(ValueError): + self.c.set_parameters_free(only_linear=True, only_nonlinear=True) + def test_set_parameters_free_name(self): - self.c.set_parameters_free(['one']) + self.c.set_parameters_free(["one"]) assert self.c.one.free assert self.c.two.free @@ -208,32 +214,35 @@ def test_set_parameters_not_free_all(self): assert not self.c.one.free assert not self.c.two.free + def test_set_parameters_not_free_only_linear_only_nonlinear(self): + with pytest.raises(ValueError): + self.c.set_parameters_not_free(only_linear=True, only_nonlinear=True) + def test_set_parameters_not_free_name(self): self.c.one.free = True - self.c.set_parameters_not_free(['two']) + self.c.set_parameters_not_free(["two"]) assert self.c.one.free assert not self.c.two.free class TestCallMethods: - def setup_method(self, method): self.c = Component(["one", "two"]) c = self.c c.model = mock.MagicMock() - c.model.__call__ = mock.MagicMock() - c.model.channel_switches = np.array([True, False, True]) + c.model._get_current_data = mock.MagicMock() + c.model._channel_switches = np.array([True, False, True]) c.model.axis.axis = np.array([0.1, 0.2, 0.3]) c.function = mock.MagicMock() - c.function.return_value = np.array([1.3, ]) - c.model.signal.axes_manager.signal_axes = [mock.MagicMock(), ] - c.model.signal.axes_manager.signal_axes[0].scale = 2. - - def test_call(self): - c = self.c - assert 1.3 == c() - np.testing.assert_array_equal(c.function.call_args[0][0], - np.array([0.1, 0.3])) + c.function.return_value = np.array( + [ + 1.3, + ] + ) + c.model.signal.axes_manager.signal_axes = [ + mock.MagicMock(), + ] + c.model.signal.axes_manager.signal_axes[0].scale = 2.0 def test_plotting_not_active_component(self): c = self.c @@ -246,30 +255,44 @@ def test_plotting_active_component_notbinned(self): c = self.c c.active = True c.model.signal.axes_manager[-1].is_binned = False - c.model.__call__.return_value = np.array([1.3]) + c.model._get_current_data.return_value = np.array([1.3]) res = c._component2plot(c.model.axes_manager, out_of_range2nans=False) - np.testing.assert_array_equal(res, np.array([1.3, ])) + np.testing.assert_array_equal( + res, + np.array( + [ + 1.3, + ] + ), + ) def test_plotting_active_component_binned(self): c = self.c c.active = True c.model.signal.axes_manager[-1].is_binned = True - c.model.__call__.return_value = np.array([1.3]) + c.model._get_current_data.return_value = np.array([1.3]) res = c._component2plot(c.model.axes_manager, out_of_range2nans=False) - np.testing.assert_array_equal(res, np.array([1.3, ])) + np.testing.assert_array_equal( + res, + np.array( + [ + 1.3, + ] + ), + ) def test_plotting_active_component_out_of_range(self): c = self.c c.active = True c.model.signal.axes_manager[-1].is_binned = False - c.model.__call__.return_value = np.array([1.1, 1.3]) + c.model._get_current_data.return_value = np.array([1.1, 1.3]) res = c._component2plot(c.model.axes_manager, out_of_range2nans=True) np.testing.assert_array_equal(res, np.array([1.1, np.nan, 1.3])) -@pytest.mark.parametrize('is_binned', [True, False]) -@pytest.mark.parametrize('non_uniform', [True, False]) -@pytest.mark.parametrize('dim', [1, 2, 3]) +@pytest.mark.parametrize("is_binned", [True, False]) +@pytest.mark.parametrize("non_uniform", [True, False]) +@pytest.mark.parametrize("dim", [1, 2, 3]) def test_get_scaling_parameter(is_binned, non_uniform, dim): shape = [10 + i for i in range(dim)] signal = Signal1D(np.arange(np.prod(shape)).reshape(shape[::-1])) @@ -285,3 +308,75 @@ def test_get_scaling_parameter(is_binned, non_uniform, dim): assert np.all(scaling_factor == 0.5) else: assert scaling_factor == 1 + + +def test_linear_parameter_initialisation(): + C = Component(["one", "two"], ["one"]) + P = Parameter() + + assert C.one._linear + assert not C.two._linear + assert not P._linear + + +def test_set_name(): + c = Component(["one", "two"], ["one"]) + c.name = "test" + assert c.name == "test" + assert c._name == "test" + + +def test_set_name_error(): + c = Component(["one", "two"], ["one"]) + with pytest.raises(ValueError): + c.name = 1 + + +def test_loading_non_expression_custom_component(tmp_path): + # non-expression based custom component uses serialisation + # to save the components. + + import hyperspy.api as hs + from hyperspy.component import Component + + class CustomComponent(Component): + def __init__(self, p1=1, p2=2): + Component.__init__(self, ("p1", "p2")) + + self.p1.value = p1 + self.p2.value = p2 + + self.p1.grad = self.grad_p1 + self.p2.grad = self.grad_p2 + + def function(self, x): + p1 = self.p1.value + p2 = self.p2.value + return p1 + x * p2 + + def grad_p1(self, x): + return 0 + + def grad_p2(self, x): + return x + + s = hs.signals.Signal1D(range(10)) + m = s.create_model() + + c = CustomComponent() + m.append(c) + m.store("a") + + s.save(tmp_path / "hs2.0_custom_component.hspy") + + s = hs.load(tmp_path / "hs2.0_custom_component.hspy") + _ = s.models.restore("a") + + +def test_load_component_previous_python(): + s = hs.load(DIRPATH / "hs2.0_custom_component.hspy") + import sys + + if sys.version_info[0] == 3.11: + with pytest.raises(TypeError): + _ = s.models.restore("a") diff --git a/hyperspy/tests/component/test_component_active_array.py b/hyperspy/tests/component/test_component_active_array.py index 0506f29475..302622e7a8 100644 --- a/hyperspy/tests/component/test_component_active_array.py +++ b/hyperspy/tests/component/test_component_active_array.py @@ -1,4 +1,4 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # # This file is part of HyperSpy. # @@ -13,7 +13,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -24,18 +24,15 @@ class TestParametersAsSignals: - def setup_method(self, method): self.gaussian = Gaussian() - self.gaussian._axes_manager = Signal1D( - np.zeros((3, 3, 1))).axes_manager + self.gaussian._axes_manager = Signal1D(np.zeros((3, 3, 1))).axes_manager def test_always_active(self): g = self.gaussian g.active_is_multidimensional = False g._create_arrays() - np.testing.assert_array_equal(g.A.as_signal('values').data, - np.zeros((3, 3))) + np.testing.assert_array_equal(g.A.as_signal("values").data, np.zeros((3, 3))) def test_some_inactive(self): g = self.gaussian @@ -43,7 +40,7 @@ def test_some_inactive(self): g._create_arrays() g._active_array[2, 0] = False g._active_array[0, 0] = False - assert np.isnan(g.A.as_signal('values').data[[0, 2], [0]]).all() + assert np.isnan(g.A.as_signal("values").data[[0, 2], [0]]).all() def test_stash_array(self): g = self.gaussian @@ -54,11 +51,12 @@ def test_stash_array(self): with stash_active_state([g]): g.active_is_multidimensional = False assert not g._active_is_multidimensional - np.testing.assert_array_equal(g.A.as_signal('values').data, - np.zeros((3, 3))) + np.testing.assert_array_equal( + g.A.as_signal("values").data, np.zeros((3, 3)) + ) assert g._active_array is None assert g._active_is_multidimensional np.testing.assert_allclose( - g._active_array, np.array([[0, 1, 1], [1, 1, 1], [0, 1, 1]], - dtype=bool)) - assert np.isnan(g.A.as_signal('values').data[[0, 2], [0]]).all() + g._active_array, np.array([[0, 1, 1], [1, 1, 1], [0, 1, 1]], dtype=bool) + ) + assert np.isnan(g.A.as_signal("values").data[[0, 2], [0]]).all() diff --git a/hyperspy/tests/component/test_component_print_current.py b/hyperspy/tests/component/test_component_print_current.py index 1f35804244..c67e41fc04 100644 --- a/hyperspy/tests/component/test_component_print_current.py +++ b/hyperspy/tests/component/test_component_print_current.py @@ -1,4 +1,4 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # # This file is part of HyperSpy. # @@ -13,52 +13,84 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import numpy as np import pytest -from hyperspy.datasets.example_signals import EDS_SEM_Spectrum -from hyperspy.misc.model_tools import (_is_iter, _iter_join, _non_iter, - current_component_values, - current_model_values) +import hyperspy.api as hs +from hyperspy.misc.model_tools import ( + CurrentComponentValues, + CurrentModelValues, + _format_string, +) class TestSetParameters: - def setup_method(self): - self.model = EDS_SEM_Spectrum().create_model() - self.component = self.model[1] + model = hs.signals.Signal1D(np.arange(100)).create_model() + p0 = hs.model.components1D.Polynomial(order=6) + g1 = hs.model.components1D.Gaussian() + g2 = hs.model.components1D.Gaussian() + g3 = hs.model.components1D.Gaussian() + g4 = hs.model.components1D.Gaussian() + model.extend([p0, g1, g2, g3, g4]) + component = model[1] # We use bmin instead of A because it's a bit more exotic - self.component.A.bmin = 1.23456789012 - self.component_not_free = self.model[3] - self.component_not_free.set_parameters_not_free() - self.component_not_free.A.bmin = 9.87654321098 - self.component_inactive = self.model[4] - self.component_inactive.active = False - self.component_inactive.A.bmin = 5.67890123456 + component.A.bmin = 1.23456789012 + component_twinned = model[2] + component_twinned.A.twin = g1.A + component_not_free = model[3] + component_not_free.set_parameters_not_free() + component_not_free.A.bmin = 9.87654321098 + component_inactive = model[4] + component_inactive.active = False + component_inactive.A.bmin = 5.67890123456 + self.model = model + self.component = component + self.component_not_free = component_not_free + self.component_inactive = component_inactive @pytest.mark.parametrize("only_free, only_active", [(True, False), (True, False)]) def test_component_current_component_values(self, only_free, only_active): - "Many decimals aren't printed, few decimals are" - string_representation = str(current_component_values(self.component, only_free, only_active).__repr__()) - html_representation = str(current_component_values(self.component, only_free, only_active)._repr_html_()) + """Many decimals aren't printed, few decimals are""" + string_representation = str( + CurrentComponentValues(self.component, only_free, only_active).__repr__() + ) + html_representation = str( + CurrentComponentValues(self.component, only_free, only_active)._repr_html_() + ) assert "1.234" in string_representation assert "1.23456789012" not in string_representation assert "1.234" in html_representation assert "1.23456789012" not in html_representation - def test_component_current_component_values_only_free(self, only_free=True, only_active=False): - "Parameters with free=False values should not be present in repr" - string_representation = str(current_component_values(self.component_not_free, only_free, only_active).__repr__()) - html_representation = str(current_component_values(self.component_not_free, only_free, only_active)._repr_html_()) + def test_component_current_component_values_only_free( + self, only_free=True, only_active=False + ): + """ "Parameters with free=False values should not be present in repr""" + string_representation = str( + CurrentComponentValues( + self.component_not_free, only_free, only_active + ).__repr__() + ) + html_representation = str( + CurrentComponentValues( + self.component_not_free, only_free, only_active + )._repr_html_() + ) assert "9.87" not in string_representation assert "9.87" not in html_representation @pytest.mark.parametrize("only_free, only_active", [(True, False), (True, False)]) def test_component_current_model_values(self, only_free, only_active): - "Many decimals aren't printed, few decimals are" - string_representation = str(current_model_values(self.model, only_free, only_active).__repr__()) - html_representation = str(current_model_values(self.model, only_free, only_active)._repr_html_()) + """Many decimals aren't printed, few decimals are""" + string_representation = str( + CurrentModelValues(self.model, only_free, only_active).__repr__() + ) + html_representation = str( + CurrentModelValues(self.model, only_free, only_active)._repr_html_() + ) assert "1.234" in string_representation assert "1.23456789012" not in string_representation assert "1.234" in html_representation @@ -76,8 +108,14 @@ def test_component_current_model_values(self, only_free, only_active): @pytest.mark.parametrize("only_free, only_active", [(True, False), (True, False)]) def test_component_current_model_values_comp_list(self, only_free, only_active): comp_list = [self.component, self.component_not_free, self.component_inactive] - string_representation = str(current_model_values(self.model, only_free, only_active, comp_list).__repr__()) - html_representation = str(current_model_values(self.model, only_free, only_active, comp_list)._repr_html_()) + string_representation = str( + CurrentModelValues(self.model, only_free, only_active, comp_list).__repr__() + ) + html_representation = str( + CurrentModelValues( + self.model, only_free, only_active, comp_list + )._repr_html_() + ) assert "1.234" in string_representation assert "1.23456789012" not in string_representation assert "1.234" in html_representation @@ -90,35 +128,35 @@ def test_component_current_model_values_comp_list(self, only_free, only_active): assert "5.67" not in string_representation assert "5.67" not in html_representation - @pytest.mark.parametrize("fancy", (True, False)) - def test_model_current_model_values(self, fancy): - self.model.print_current_values(fancy=fancy) - - @pytest.mark.parametrize("fancy", (True, False)) - def test_component_print_current_values(self, fancy): - self.model[0].print_current_values(fancy=fancy) + def test_model_current_model_values(self): + self.model.print_current_values() - @pytest.mark.parametrize("fancy", (True, False)) - def test_model_print_current_values(self, fancy): - self.model.print_current_values(fancy=fancy) + def test_component_print_current_values(self): + self.model[0].print_current_values() - def test_zero_in_fancy_print(self): - "Ensure parameters with value=0 are printed too" - assert "a1True 0" in current_component_values(self.model[0])._repr_html_() + def test_model_print_current_values(self): + self.model.print_current_values() - def test_zero_in_normal_print(self): - "Ensure parameters with value=0 are printed too" - assert " a0 | True | 0 |" in str(current_component_values(self.model[0]).__repr__) + def test_zero_in_html_print(self): + """Ensure parameters with value=0 are printed too""" + assert ( + "a1True 0" + in CurrentComponentValues(self.model[0])._repr_html_() + ) - def test_related_tools(self): - assert _is_iter([1,2,3]) - assert _is_iter((1,2,3)) - assert not _is_iter(1) + def test_zero_in_normal_print(self): + """Ensure parameters with value=0 are printed too""" + assert " a0 | True | 0 |" in str( + CurrentComponentValues(self.model[0]).__repr__ + ) - assert _iter_join([1.2345678, 5.67890]) == '(1.23457, 5.6789)' - assert _iter_join([1.2345678, 5.67890]) == '(1.23457, 5.6789)' - assert _iter_join([1, 5]) == '( 1, 5)' + def test_twinned_in_print(self): + assert ( + " A | Twinned |" + in str(CurrentComponentValues(self.model[2]).__repr__()).split("\n")[4] + ) - assert _non_iter(None) == "" - assert _non_iter(5) == ' 5' - assert _non_iter(5.123456789) == '5.12346' + def test_related_tools(self): + assert _format_string(None) == "" + assert _format_string(5) == " 5" + assert _format_string(5.123456789) == "5.12346" diff --git a/hyperspy/tests/component/test_component_set_parameters.py b/hyperspy/tests/component/test_component_set_parameters.py index e064e821cb..99348d536c 100644 --- a/hyperspy/tests/component/test_component_set_parameters.py +++ b/hyperspy/tests/component/test_component_set_parameters.py @@ -1,4 +1,4 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # # This file is part of HyperSpy. # @@ -13,14 +13,13 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from hyperspy.components1d import Gaussian class TestSetParameters: - def setup_method(self, method): self.gaussian = Gaussian() @@ -32,7 +31,7 @@ def test_set_parameters_not_free1(self): def test_set_parameters_not_free2(self): g = self.gaussian - g.set_parameters_not_free(parameter_name_list=['A']) + g.set_parameters_not_free(parameter_name_list=["A"]) free_parameters = len(g.free_parameters) parameters = len(g.parameters) - 1 assert free_parameters == parameters @@ -50,7 +49,31 @@ def test_set_parameters_free2(self): g.A.free = False g.centre.free = False g.sigma.free = False - g.set_parameters_free(parameter_name_list=['A']) + g.set_parameters_free(parameter_name_list=["A"]) free_parameters = len(g.free_parameters) parameters = len(g.parameters) - 2 assert free_parameters == parameters + + def test_set_parameters_not_free_linearity(self): + g = self.gaussian + + g.set_parameters_not_free(only_nonlinear=True) + assert not g.sigma.free + assert not g.centre.free + assert g.A.free + + g.set_parameters_not_free(only_linear=True) + assert not g.sigma.free + assert not g.centre.free + assert not g.A.free + + g.set_parameters_free(only_linear=True) + assert not g.sigma.free + assert not g.centre.free + assert g.A.free + + g.set_parameters_free(only_nonlinear=True) + g.set_parameters_not_free(only_linear=True) + assert g.sigma.free + assert g.centre.free + assert not g.A.free diff --git a/hyperspy/tests/component/test_components.py b/hyperspy/tests/component/test_components.py index 41adba00b9..d07565cdd0 100644 --- a/hyperspy/tests/component/test_components.py +++ b/hyperspy/tests/component/test_components.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import inspect import itertools @@ -25,40 +25,40 @@ import hyperspy.api as hs from hyperspy import components1d from hyperspy.component import Component -from hyperspy.misc.test_utils import ignore_warning -from hyperspy.models.model1d import Model1D TRUE_FALSE_2_TUPLE = [p for p in itertools.product((True, False), repeat=2)] def get_components1d_name_list(): - components1d_name_list = [] for c_name in dir(components1d): obj = getattr(components1d, c_name) if inspect.isclass(obj) and issubclass(obj, Component): components1d_name_list.append(c_name) - - # Remove EELSCLEdge, since it is tested elsewhere more appropriate - components1d_name_list.remove('EELSCLEdge') return components1d_name_list -@pytest.mark.filterwarnings("ignore:invalid value encountered in true_divide:RuntimeWarning") -@pytest.mark.filterwarnings("ignore:divide by zero encountered in true_divide:RuntimeWarning") +@pytest.mark.filterwarnings( + "ignore:invalid value encountered in true_divide:RuntimeWarning" +) +@pytest.mark.filterwarnings( + "ignore:divide by zero encountered in true_divide:RuntimeWarning" +) @pytest.mark.filterwarnings("ignore:invalid value encountered in cos:RuntimeWarning") -@pytest.mark.filterwarnings("ignore:The API of the") -@pytest.mark.parametrize('component_name', get_components1d_name_list()) +@pytest.mark.parametrize("component_name", get_components1d_name_list()) def test_creation_components1d(component_name): s = hs.signals.Signal1D(np.zeros(1024)) s.axes_manager[0].offset = 100 s.axes_manager[0].scale = 0.01 kwargs = {} - if component_name == 'ScalableFixedPattern': - kwargs['signal1D'] = s - elif component_name == 'Expression': - kwargs.update({'expression': "a*x+b", "name": "linear"}) + if component_name == "ScalableFixedPattern": + kwargs["signal1D"] = s + elif component_name == "Expression": + kwargs.update({"expression": "a*x+b", "name": "linear"}) + elif component_name == "Bleasdale": + # This component only works with numexpr. + pytest.importorskip("numexpr") component = getattr(components1d, component_name)(**kwargs) component.function(np.arange(1, 100)) @@ -71,543 +71,9 @@ def test_creation_components1d(component_name): m2 = s.create_model() m2._load_dictionary(model_dict) - -class TestPowerLaw: - - def setup_method(self, method): - s = hs.signals.Signal1D(np.zeros(1024)) - s.axes_manager[0].offset = 100 - s.axes_manager[0].scale = 0.01 - m = s.create_model() - m.append(hs.model.components1D.PowerLaw()) - m[0].A.value = 1000 - m[0].r.value = 4 - self.m = m - self.s = s - - @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) - def test_estimate_parameters(self, only_current, binned): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - assert s.axes_manager[-1].is_binned == binned - g = hs.model.components1D.PowerLaw() - g.estimate_parameters(s, None, None, only_current=only_current) - assert g._axes_manager[-1].is_binned == binned - A_value = 1008.4913 if binned else 1006.4378 - r_value = 4.001768 if binned else 4.001752 - np.testing.assert_allclose(g.A.value, A_value) - np.testing.assert_allclose(g.r.value, r_value) - - if only_current: - A_value, r_value = 0, 0 - # Test that it all works when calling it with a different signal - s2 = hs.stack((s, s)) - g.estimate_parameters(s2, None, None, only_current=only_current) - assert g._axes_manager[-1].is_binned == binned - np.testing.assert_allclose(g.A.map["values"][1], A_value) - np.testing.assert_allclose(g.r.map["values"][1], r_value) - - def test_EDS_missing_data(self): - g = hs.model.components1D.PowerLaw() - s = self.m.as_signal() - s2 = hs.signals.EDSTEMSpectrum(s.data) - g.estimate_parameters(s2, None, None) - - def test_function_grad_cutoff(self): - pl = self.m[0] - pl.left_cutoff.value = 105.0 - axis = self.s.axes_manager[0].axis - for attr in ['function', 'grad_A', 'grad_r', 'grad_origin']: - values = getattr(pl, attr)((axis)) - np.testing.assert_allclose(values[:501], np.zeros((501))) - assert getattr(pl, attr)((axis))[500] == 0 - getattr(pl, attr)((axis))[502] > 0 - - def test_exception_gradient_calculation(self): - # if this doesn't warn, it means that sympy can compute the gradients - # and the power law component can be updated. - with pytest.warns(UserWarning): - hs.model.components1D.PowerLaw(compute_gradients=True) - - -class TestDoublePowerLaw: - - def setup_method(self, method): - s = hs.signals.Signal1D(np.zeros(1024)) - s.axes_manager[0].offset = 100 - s.axes_manager[0].scale = 0.1 - m = s.create_model() - m.append(hs.model.components1D.DoublePowerLaw()) - m[0].A.value = 1000 - m[0].r.value = 4 - m[0].ratio.value = 200 - self.m = m - - @pytest.mark.parametrize(("binned"), (True, False)) - def test_fit(self, binned): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - assert s.axes_manager[-1].is_binned == binned - g = hs.model.components1D.DoublePowerLaw() - # Fix the ratio parameter to test the fit - g.ratio.free = False - g.ratio.value = 200 - m = s.create_model() - m.append(g) - m.fit_component(g, signal_range=(None, None)) - np.testing.assert_allclose(g.A.value, 1000.0) - np.testing.assert_allclose(g.r.value, 4.0) - np.testing.assert_allclose(g.ratio.value, 200.) - -class TestOffset: - - def setup_method(self, method): - s = hs.signals.Signal1D(np.zeros(10)) - s.axes_manager[0].scale = 0.01 - m = s.create_model() - m.append(hs.model.components1D.Offset()) - m[0].offset.value = 10 - self.m = m - - @pytest.mark.parametrize(("uniform"), (True, False)) - @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) - def test_estimate_parameters(self, only_current, binned, uniform): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - if not uniform: - s.axes_manager[-1].convert_to_non_uniform_axis() - assert s.axes_manager[-1].is_binned == binned - o = hs.model.components1D.Offset() - o.estimate_parameters(s, None, None, only_current=only_current) - assert o._axes_manager[-1].is_binned == binned - assert o._axes_manager[-1].is_uniform == uniform - np.testing.assert_allclose(o.offset.value, 10) - - @pytest.mark.parametrize(("uniform"), (True, False)) - @pytest.mark.parametrize(("binned"), (True, False)) - def test_function_nd(self, binned, uniform): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - s = hs.stack([s] * 2) - o = hs.model.components1D.Offset() - o.estimate_parameters(s, None, None, only_current=False) - assert o._axes_manager[-1].is_binned == binned - axis = s.axes_manager.signal_axes[0] - factor = axis.scale if binned else 1 - np.testing.assert_allclose(o.function_nd(axis.axis) * factor, s.data) - - -@pytest.mark.filterwarnings("ignore:The API of the `Polynomial` component") -class TestDeprecatedPolynomial: - - def setup_method(self, method): - s = hs.signals.Signal1D(np.zeros(1024)) - s.axes_manager[0].offset = -5 - s.axes_manager[0].scale = 0.01 - m = s.create_model() - m.append(hs.model.components1D.Polynomial(order=2)) - coeff_values = (0.5, 2, 3) - self.m = m - s_2d = hs.signals.Signal1D(np.arange(1000).reshape(10, 100)) - self.m_2d = s_2d.create_model() - self.m_2d.append(hs.model.components1D.Polynomial(order=2)) - s_3d = hs.signals.Signal1D(np.arange(1000).reshape(2, 5, 100)) - self.m_3d = s_3d.create_model() - self.m_3d.append(hs.model.components1D.Polynomial(order=2)) - # if same component is pased, axes_managers get mixed up, tests - # sometimes randomly fail - for _m in [self.m, self.m_2d, self.m_3d]: - _m[0].coefficients.value = coeff_values - - def test_gradient(self): - c = self.m[0] - np.testing.assert_array_almost_equal(c.grad_coefficients(1), - np.array([[6, ], [4.5], [3.5]])) - assert c.grad_coefficients(np.arange(10)).shape == (3, 10) - - @pytest.mark.parametrize(("uniform"), (True, False)) - @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) - def test_estimate_parameters(self, only_current, binned, uniform): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - if not uniform: - s.axes_manager[-1].convert_to_non_uniform_axis() - assert s.axes_manager[-1].is_binned == binned - assert s.axes_manager[-1].is_uniform == uniform - g = hs.model.components1D.Polynomial(order=2) - g.estimate_parameters(s, None, None, only_current=only_current) - assert g._axes_manager[-1].is_binned == binned - np.testing.assert_allclose(g.coefficients.value[0], 0.5) - np.testing.assert_allclose(g.coefficients.value[1], 2) - np.testing.assert_allclose(g.coefficients.value[2], 3) - - def test_2d_signal(self): - # This code should run smoothly, any exceptions should trigger failure - s = self.m_2d.as_signal() - model = Model1D(s) - p = hs.model.components1D.Polynomial(order=2) - model.append(p) - p.estimate_parameters(s, 0, 100, only_current=False) - np.testing.assert_allclose(p.coefficients.map['values'], - np.tile([0.5, 2, 3], (10, 1))) - - @pytest.mark.filterwarnings("ignore:The API of the `Polynomial`") - def test_3d_signal(self): - # This code should run smoothly, any exceptions should trigger failure - s = self.m_3d.as_signal() - model = Model1D(s) - p = hs.model.components1D.Polynomial(order=2) - model.append(p) - p.estimate_parameters(s, 0, 100, only_current=False) - np.testing.assert_allclose(p.coefficients.map['values'], - np.tile([0.5, 2, 3], (2, 5, 1))) - - @pytest.mark.filterwarnings("ignore:The API of the") - def test_conversion_dictionary_to_polynomial2(self): - from hyperspy._components.polynomial import convert_to_polynomial - s = hs.signals.Signal1D(np.zeros(1024)) - s.axes_manager[0].offset = -5 - s.axes_manager[0].scale = 0.01 - poly = hs.model.components1D.Polynomial(order=2, legacy=True) - poly.coefficients.value = [1, 2, 3] - poly.coefficients.value = [1, 2, 3] - poly.coefficients._bounds = ((None, None), (10, 0.0), (None, None)) - poly_dict = poly.as_dictionary(True) - poly2_dict = convert_to_polynomial(poly_dict) - - poly2 = hs.model.components1D.Polynomial(order=2, legacy=False) - _ = poly2._load_dictionary(poly2_dict) - assert poly2.a2.value == 1 - assert poly2.a2._bounds == (None, None) - assert poly2.a1.value == 2 - assert poly2.a1._bounds == (10, 0.0) - assert poly2.a0.value == 3 - - -class TestPolynomial: - - def setup_method(self, method): - s = hs.signals.Signal1D(np.zeros(1024)) - s.axes_manager[0].offset = -5 - s.axes_manager[0].scale = 0.01 - m = s.create_model() - m.append(hs.model.components1D.Polynomial(order=2, legacy=False)) - coeff_values = (0.5, 2, 3) - self.m = m - s_2d = hs.signals.Signal1D(np.arange(1000).reshape(10, 100)) - self.m_2d = s_2d.create_model() - self.m_2d.append(hs.model.components1D.Polynomial(order=2, legacy=False)) - s_3d = hs.signals.Signal1D(np.arange(1000).reshape(2, 5, 100)) - self.m_3d = s_3d.create_model() - self.m_3d.append(hs.model.components1D.Polynomial(order=2, legacy=False)) - data = 50*np.ones(100) - s_offset = hs.signals.Signal1D(data) - self.m_offset = s_offset.create_model() - - # if same component is pased, axes_managers get mixed up, tests - # sometimes randomly fail - for _m in [self.m, self.m_2d, self.m_3d]: - _m[0].a2.value = coeff_values[0] - _m[0].a1.value = coeff_values[1] - _m[0].a0.value = coeff_values[2] - - def test_gradient(self): - poly = self.m[0] - np.testing.assert_allclose(poly.a2.grad(np.arange(3)), np.array([0, 1, 4])) - np.testing.assert_allclose(poly.a1.grad(np.arange(3)), np.array([0, 1, 2])) - np.testing.assert_allclose(poly.a0.grad(np.arange(3)), np.array([1, 1, 1])) - - def test_fitting(self): - s_2d = self.m_2d.signal - s_2d.data += 100 * np.array([np.random.randint(50, size=10)]*100).T - m_2d = s_2d.create_model() - m_2d.append(hs.model.components1D.Polynomial(order=1, legacy=False)) - m_2d.multifit(iterpath='serpentine', grad='analytical') - np.testing.assert_allclose(m_2d.red_chisq.data.sum(), 0.0, atol=1E-7) - - @pytest.mark.parametrize(("order"), (2, 12)) - @pytest.mark.parametrize(("uniform"), (True, False)) - @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) - def test_estimate_parameters(self, only_current, binned, uniform, order): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - s.axes_manager[-1].is_binned = binned - if not uniform: - s.axes_manager[-1].convert_to_non_uniform_axis() - p = hs.model.components1D.Polynomial(order=order, legacy=False) - p.estimate_parameters(s, None, None, only_current=only_current) - assert p._axes_manager[-1].is_binned == binned - assert p._axes_manager[-1].is_uniform == uniform - np.testing.assert_allclose(p.parameters[2].value, 0.5) - np.testing.assert_allclose(p.parameters[1].value, 2) - np.testing.assert_allclose(p.parameters[0].value, 3) - - def test_zero_order(self): - m = self.m_offset - with pytest.raises(ValueError): - m.append(hs.model.components1D.Polynomial(order=0, legacy=False)) - - def test_2d_signal(self): - # This code should run smoothly, any exceptions should trigger failure - s = self.m_2d.as_signal() - model = Model1D(s) - p = hs.model.components1D.Polynomial(order=2, legacy=False) - model.append(p) - p.estimate_parameters(s, 0, 100, only_current=False) - np.testing.assert_allclose(p.a2.map['values'], 0.5) - np.testing.assert_allclose(p.a1.map['values'], 2) - np.testing.assert_allclose(p.a0.map['values'], 3) - - def test_3d_signal(self): - # This code should run smoothly, any exceptions should trigger failure - s = self.m_3d.as_signal() - model = Model1D(s) - p = hs.model.components1D.Polynomial(order=2, legacy=False) - model.append(p) - p.estimate_parameters(s, 0, 100, only_current=False) - np.testing.assert_allclose(p.a2.map['values'], 0.5) - np.testing.assert_allclose(p.a1.map['values'], 2) - np.testing.assert_allclose(p.a0.map['values'], 3) - - def test_function_nd(self): - s = self.m.as_signal() - s = hs.stack([s]*2) - p = hs.model.components1D.Polynomial(order=2, legacy=False) - p.estimate_parameters(s, None, None, only_current=False) - axis = s.axes_manager.signal_axes[0] - np.testing.assert_allclose(p.function_nd(axis.axis), s.data) - - -class TestGaussian: - - def setup_method(self, method): - s = hs.signals.Signal1D(np.zeros(1024)) - s.axes_manager[0].offset = -5 - s.axes_manager[0].scale = 0.01 - m = s.create_model() - m.append(hs.model.components1D.Gaussian()) - m[0].sigma.value = 0.5 - m[0].centre.value = 1 - m[0].A.value = 2 - self.m = m - - @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) - def test_estimate_parameters_binned(self, only_current, binned): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - assert s.axes_manager[-1].is_binned == binned - g = hs.model.components1D.Gaussian() - g.estimate_parameters(s, None, None, only_current=only_current) - assert g._axes_manager[-1].is_binned == binned - np.testing.assert_allclose(g.sigma.value, 0.5) - np.testing.assert_allclose(g.A.value, 2) - np.testing.assert_allclose(g.centre.value, 1) - - @pytest.mark.parametrize("binned", (True, False)) - def test_function_nd(self, binned): - self.m.signal.axes_manager[-1].is_binned = binned - s = self.m.as_signal() - s2 = hs.stack([s] * 2) - g = hs.model.components1D.Gaussian() - g.estimate_parameters(s2, None, None, only_current=False) - assert g._axes_manager[-1].is_binned == binned - axis = s.axes_manager.signal_axes[0] - factor = axis.scale if binned else 1 - np.testing.assert_allclose(g.function_nd(axis.axis) * factor, s2.data) - - -class TestExpression: - - def setup_method(self, method): - self.g = hs.model.components1D.Expression( - expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2)", - name="Gaussian", - position="x0", - height=1, - fwhm=1, - x0=0, - module="numpy") - - def test_name(self): - assert self.g.name == "Gaussian" - - def test_position(self): - assert self.g._position is self.g.x0 - - def test_f(self): - assert self.g.function(0) == 1 - - def test_grad_height(self): - np.testing.assert_allclose( - self.g.grad_height(2), - 1.5258789062500007e-05) - - def test_grad_x0(self): - np.testing.assert_allclose( - self.g.grad_x0(2), - 0.00016922538587889289) - - def test_grad_fwhm(self): - np.testing.assert_allclose( - self.g.grad_fwhm(2), - 0.00033845077175778578) - - def test_function_nd(self): - assert self.g.function_nd(0) == 1 - - -def test_expression_symbols(): - with pytest.raises(ValueError): - hs.model.components1D.Expression(expression="10.0", name="offset") - with pytest.raises(ValueError): - hs.model.components1D.Expression(expression="10", name="offset") - with pytest.raises(ValueError): - hs.model.components1D.Expression(expression="10*offset", name="Offset") - - -def test_expression_substitution(): - expr = 'A / B; A = x+2; B = x-c' - comp = hs.model.components1D.Expression(expr, name='testcomp', - autodoc=True, - c=2) - assert ''.join(p.name for p in comp.parameters) == 'c' - assert comp.function(1) == -3 - - -class TestScalableFixedPattern: - - def setup_method(self, method): - s = hs.signals.Signal1D(np.linspace(0., 100., 10)) - s1 = hs.signals.Signal1D(np.linspace(0., 1., 10)) - s.axes_manager[0].scale = 0.1 - s1.axes_manager[0].scale = 0.1 - self.s = s - self.pattern = s1 - - def test_position(self): - s1 = self.pattern - fp = hs.model.components1D.ScalableFixedPattern(s1) - assert fp._position is fp.shift - - def test_both_unbinned(self): - s = self.s - s1 = self.pattern - s.axes_manager[-1].is_binned = False - s1.axes_manager[-1].is_binned = False - m = s.create_model() - fp = hs.model.components1D.ScalableFixedPattern(s1) - m.append(fp) - with ignore_warning(message="invalid value encountered in sqrt", - category=RuntimeWarning): - m.fit() - assert abs(fp.yscale.value - 100) <= 0.1 - - @pytest.mark.parametrize(("uniform"), (True, False)) - def test_both_binned(self, uniform): - s = self.s - s1 = self.pattern - s.axes_manager[-1].is_binned = True - s1.axes_manager[-1].is_binned = True - if not uniform: - s.axes_manager[0].convert_to_non_uniform_axis() - s1.axes_manager[0].convert_to_non_uniform_axis() - m = s.create_model() - fp = hs.model.components1D.ScalableFixedPattern(s1) - m.append(fp) - with ignore_warning(message="invalid value encountered in sqrt", - category=RuntimeWarning): - m.fit() - assert abs(fp.yscale.value - 100) <= 0.1 - - def test_pattern_unbinned_signal_binned(self): - s = self.s - s1 = self.pattern - s.axes_manager[-1].is_binned = True - s1.axes_manager[-1].is_binned = False - m = s.create_model() - fp = hs.model.components1D.ScalableFixedPattern(s1) - m.append(fp) - with ignore_warning(message="invalid value encountered in sqrt", - category=RuntimeWarning): - m.fit() - assert abs(fp.yscale.value - 1000) <= 1 - - def test_pattern_binned_signal_unbinned(self): - s = self.s - s1 = self.pattern - s.axes_manager[-1].is_binned = False - s1.axes_manager[-1].is_binned = True - m = s.create_model() - fp = hs.model.components1D.ScalableFixedPattern(s1) - m.append(fp) - with ignore_warning(message="invalid value encountered in sqrt", - category=RuntimeWarning): - m.fit() - assert abs(fp.yscale.value - 10) <= .1 - - def test_function(self): - s = self.s - s1 = self.pattern - fp = hs.model.components1D.ScalableFixedPattern(s1, interpolate=False) - m = s.create_model() - m.append(fp) - m.fit(grad='analytical') - x = s.axes_manager[0].axis - np.testing.assert_allclose(s.data, fp.function(x)) - np.testing.assert_allclose(fp.function(x), fp.function_nd(x)) - - def test_function_nd(self): - s = self.s - s1 = self.pattern - fp = hs.model.components1D.ScalableFixedPattern(s1) - s_multi = hs.stack([s] * 3) - m = s_multi.create_model() - m.append(fp) - fp.yscale.map['values'] = [1.0, 0.5, 1.0] - fp.xscale.map['values'] = [1.0, 1.0, 0.75] - results = fp.function_nd(s.axes_manager[0].axis) - expected = np.array([s1.data * v for v in [1, 0.5, 0.75]]) - np.testing.assert_allclose(results, expected) - - @pytest.mark.parametrize('interpolate', [True, False]) - def test_recreate_component(self, interpolate): - s = self.s - s1 = self.pattern - fp = hs.model.components1D.ScalableFixedPattern(s1, - interpolate=interpolate) - m = s.create_model() - m.append(fp) - model_dict = m.as_dictionary() - - m2 = s.create_model() - m2._load_dictionary(model_dict) - assert m2[0].interpolate == interpolate - np.testing.assert_allclose(m2[0].signal.data, s1.data) - - -class TestHeavisideStep: - - def setup_method(self, method): - self.c = hs.model.components1D.HeavisideStep() - - def test_integer_values(self): - c = self.c - np.testing.assert_array_almost_equal(c.function(np.array([-1, 0, 2])), - np.array([0, 0.5, 1])) - - def test_float_values(self): - c = self.c - np.testing.assert_array_almost_equal(c.function(np.array([-0.5, 0.5, 2])), - np.array([0, 1, 1])) - - def test_not_sorted(self): - c = self.c - np.testing.assert_array_almost_equal(c.function(np.array([3, -0.1, 0])), - np.array([1, 0, 0.5])) - - def test_gradients(self): - c = self.c - np.testing.assert_array_almost_equal(c.A.grad(np.array([3, -0.1, 0])), - np.array([1, 0, 0.5])) -# np.testing.assert_array_almost_equal(c.n.grad(np.array([3, -0.1, 0])), -# np.array([1, 1, 1])) + # For Expression based component which uses sympy to compute gradient + # automatically, check that the gradient are working + for parameter in component.parameters: + grad = getattr(component, f"grad_{parameter.name}", None) + if grad is not None: + grad(np.arange(1, 100)) diff --git a/hyperspy/tests/component/test_components2D.py b/hyperspy/tests/component/test_components2D.py index 83c75b4e3f..476554d7ab 100644 --- a/hyperspy/tests/component/test_components2D.py +++ b/hyperspy/tests/component/test_components2D.py @@ -1,37 +1,33 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np import hyperspy.api as hs -GAUSSIAN2D_EXPR = \ - "exp(-((x-x0)**2 / (2 * sx ** 2) + (y-y0)**2 / (2 * sy ** 2)))" +GAUSSIAN2D_EXPR = "exp(-((x-x0)**2 / (2 * sx ** 2) + (y-y0)**2 / (2 * sy ** 2)))" class TestGaussian2D: - def setup_method(self, method): g = hs.model.components2D.Gaussian2D( - centre_x=-5., - centre_y=-5., - sigma_x=1., - sigma_y=2.) + centre_x=-5.0, centre_y=-5.0, sigma_x=1.0, sigma_y=2.0 + ) x = np.arange(-10, 10, 0.01) y = np.arange(-10, 10, 0.01) X, Y = np.meshgrid(x, y) @@ -50,185 +46,410 @@ def test_values(self): class TestExpression2D: - def setup_method(self, method): - self.array0 = np.array([[1.69189792e-10, 3.72665317e-06, 1.50343919e-03, - 1.11089965e-02, 1.50343919e-03], - [2.06115362e-09, 4.53999298e-05, 1.83156389e-02, - 1.35335283e-01, 1.83156389e-02], - [9.23744966e-09, 2.03468369e-04, 8.20849986e-02, - 6.06530660e-01, 8.20849986e-02], - [1.52299797e-08, 3.35462628e-04, 1.35335283e-01, - 1.00000000e+00, 1.35335283e-01], - [9.23744966e-09, 2.03468369e-04, 8.20849986e-02, - 6.06530660e-01, 8.20849986e-02]]) + self.array0 = np.array( + [ + [ + 1.69189792e-10, + 3.72665317e-06, + 1.50343919e-03, + 1.11089965e-02, + 1.50343919e-03, + ], + [ + 2.06115362e-09, + 4.53999298e-05, + 1.83156389e-02, + 1.35335283e-01, + 1.83156389e-02, + ], + [ + 9.23744966e-09, + 2.03468369e-04, + 8.20849986e-02, + 6.06530660e-01, + 8.20849986e-02, + ], + [ + 1.52299797e-08, + 3.35462628e-04, + 1.35335283e-01, + 1.00000000e00, + 1.35335283e-01, + ], + [ + 9.23744966e-09, + 2.03468369e-04, + 8.20849986e-02, + 6.06530660e-01, + 8.20849986e-02, + ], + ] + ) def test_with_rotation(self): g = hs.model.components2D.Expression( - GAUSSIAN2D_EXPR, name="gaussian2d", add_rotation=True, - position=("x0", "y0"),) + GAUSSIAN2D_EXPR, + name="gaussian2d", + add_rotation=True, + position=("x0", "y0"), + ) g.rotation_angle.value = np.radians(20) - g.sy.value = .1 + g.sy.value = 0.1 g.sx.value = 0.5 g.sy.value = 1 g.x0.value = 1 g.y0.value = 1 - l = np.linspace(-2, 2, 5) - x, y = np.meshgrid(l, l) + values = np.linspace(-2, 2, 5) + x, y = np.meshgrid(values, values) np.testing.assert_allclose( g.function(x, y), - np.array([[6.68025544e-06, 8.55249949e-04, 6.49777231e-03, - 2.92959352e-03, 7.83829650e-05], - [1.87435095e-05, 5.01081110e-03, 7.94944111e-02, - 7.48404874e-02, 4.18126855e-03], - [1.43872256e-05, 8.03140097e-03, 2.66058420e-01, - 5.23039095e-01, 6.10186705e-02], - [3.02114453e-06, 3.52162323e-03, 2.43604733e-01, - 1.00000000e+00, 2.43604733e-01], - [1.73553850e-07, 4.22437802e-04, 6.10186705e-02, - 5.23039095e-01, 2.66058420e-01]]) + np.array( + [ + [ + 6.68025544e-06, + 8.55249949e-04, + 6.49777231e-03, + 2.92959352e-03, + 7.83829650e-05, + ], + [ + 1.87435095e-05, + 5.01081110e-03, + 7.94944111e-02, + 7.48404874e-02, + 4.18126855e-03, + ], + [ + 1.43872256e-05, + 8.03140097e-03, + 2.66058420e-01, + 5.23039095e-01, + 6.10186705e-02, + ], + [ + 3.02114453e-06, + 3.52162323e-03, + 2.43604733e-01, + 1.00000000e00, + 2.43604733e-01, + ], + [ + 1.73553850e-07, + 4.22437802e-04, + 6.10186705e-02, + 5.23039095e-01, + 2.66058420e-01, + ], + ] + ), ) np.testing.assert_allclose( g.grad_sx(x, y), - np.array([[1.20880828e-04, 3.17536657e-03, 1.04030848e-03, - 2.17878745e-02, 2.00221335e-03], - [4.99616174e-04, 4.02985917e-02, 2.05882951e-02, - 2.47378292e-01, 7.18407981e-02], - [5.30432684e-04, 1.12636930e-01, 5.34932340e-01, - 4.32214309e-01, 6.38979998e-01], - [1.47232153e-04, 7.62766363e-02, 1.31908984e+00, - 0.00000000e+00, 1.31908984e+00], - [1.08041078e-05, 1.30732490e-02, 6.38979998e-01, - 4.32214309e-01, 5.34932340e-01]]) - + np.array( + [ + [ + 1.20880828e-04, + 3.17536657e-03, + 1.04030848e-03, + 2.17878745e-02, + 2.00221335e-03, + ], + [ + 4.99616174e-04, + 4.02985917e-02, + 2.05882951e-02, + 2.47378292e-01, + 7.18407981e-02, + ], + [ + 5.30432684e-04, + 1.12636930e-01, + 5.34932340e-01, + 4.32214309e-01, + 6.38979998e-01, + ], + [ + 1.47232153e-04, + 7.62766363e-02, + 1.31908984e00, + 0.00000000e00, + 1.31908984e00, + ], + [ + 1.08041078e-05, + 1.30732490e-02, + 6.38979998e-01, + 4.32214309e-01, + 5.34932340e-01, + ], + ] + ), ) def test_with_rotation_center_tuple(self): g = hs.model.components2D.Expression( - GAUSSIAN2D_EXPR, "gaussian2d", add_rotation=True, - position=("x0", "y0"), module="numpy", rotation_center=(1, 2)) + GAUSSIAN2D_EXPR, + "gaussian2d", + add_rotation=True, + position=("x0", "y0"), + module="numpy", + rotation_center=(1, 2), + ) g.rotation_angle.value = np.radians(30) - g.sx.value = .1 - g.sy.value = .1 + g.sx.value = 0.1 + g.sy.value = 0.1 g.x0.value = 0 g.y0.value = 0 - l = np.linspace(-2, 2, 5) - x, y = np.meshgrid(l, l) + values = np.linspace(-2, 2, 5) + x, y = np.meshgrid(values, values) np.testing.assert_allclose( g.function(x, y), - np.array([[1.77220718e-208, 1.97005871e-181, 1.00498099e-181, - 2.35261319e-209, 2.52730239e-264], - [9.94006394e-101, 6.64536056e-081, 2.03874037e-088, - 2.87024715e-123, 1.85434555e-185], - [1.07437572e-033, 4.31966683e-021, 7.96999992e-036, - 6.74808249e-078, 2.62190196e-147], - [2.23777036e-007, 5.41095940e-002, 6.00408779e-024, - 3.05726999e-073, 7.14388660e-150], - [8.98187837e-022, 1.30614268e-023, 8.71621777e-053, - 2.66919022e-109, 3.75098197e-193]]) + np.array( + [ + [ + 1.77220718e-208, + 1.97005871e-181, + 1.00498099e-181, + 2.35261319e-209, + 2.52730239e-264, + ], + [ + 9.94006394e-101, + 6.64536056e-081, + 2.03874037e-088, + 2.87024715e-123, + 1.85434555e-185, + ], + [ + 1.07437572e-033, + 4.31966683e-021, + 7.96999992e-036, + 6.74808249e-078, + 2.62190196e-147, + ], + [ + 2.23777036e-007, + 5.41095940e-002, + 6.00408779e-024, + 3.05726999e-073, + 7.14388660e-150, + ], + [ + 8.98187837e-022, + 1.30614268e-023, + 8.71621777e-053, + 2.66919022e-109, + 3.75098197e-193, + ], + ] + ), ) def test_with_rotation_no_position(self): g = hs.model.components2D.Expression( - GAUSSIAN2D_EXPR, "gaussian2d", add_rotation=True, module="numpy") + GAUSSIAN2D_EXPR, "gaussian2d", add_rotation=True, module="numpy" + ) g.rotation_angle.value = np.radians(45) - g.sx.value = .5 - g.sy.value = .1 + g.sx.value = 0.5 + g.sy.value = 0.1 g.x0.value = 0 g.y0.value = 0 - l = np.linspace(-2, 2, 5) - x, y = np.meshgrid(l, l) + values = np.linspace(-2, 2, 5) + x, y = np.meshgrid(values, values) np.testing.assert_allclose( g.function(x, y), - np.array([[9.64172248e-175, 5.46609733e-099, 5.03457536e-045, - 7.53374790e-013, 1.83156389e-002], - [1.89386646e-098, 3.13356463e-044, 8.42346375e-012, - 3.67879441e-001, 2.61025584e-012], - [2.63952332e-044, 1.27462190e-011, 1.00000000e+000, - 1.27462190e-011, 2.63952332e-044], - [2.61025584e-012, 3.67879441e-001, 8.42346375e-012, - 3.13356463e-044, 1.89386646e-098], - [1.83156389e-002, 7.53374790e-013, 5.03457536e-045, - 5.46609733e-099, 9.64172248e-175]]) + np.array( + [ + [ + 9.64172248e-175, + 5.46609733e-099, + 5.03457536e-045, + 7.53374790e-013, + 1.83156389e-002, + ], + [ + 1.89386646e-098, + 3.13356463e-044, + 8.42346375e-012, + 3.67879441e-001, + 2.61025584e-012, + ], + [ + 2.63952332e-044, + 1.27462190e-011, + 1.00000000e000, + 1.27462190e-011, + 2.63952332e-044, + ], + [ + 2.61025584e-012, + 3.67879441e-001, + 8.42346375e-012, + 3.13356463e-044, + 1.89386646e-098, + ], + [ + 1.83156389e-002, + 7.53374790e-013, + 5.03457536e-045, + 5.46609733e-099, + 9.64172248e-175, + ], + ] + ), ) def test_with_rotation_no_position_init_values(self): g = hs.model.components2D.Expression( - GAUSSIAN2D_EXPR, "gaussian2d", add_rotation=True, module="numpy", - sx=.5, sy=.1, x0=0, y0=0, rotation_angle=np.radians(45)) - l = np.linspace(-2, 2, 5) - x, y = np.meshgrid(l, l) + GAUSSIAN2D_EXPR, + "gaussian2d", + add_rotation=True, + module="numpy", + sx=0.5, + sy=0.1, + x0=0, + y0=0, + rotation_angle=np.radians(45), + ) + values = np.linspace(-2, 2, 5) + x, y = np.meshgrid(values, values) np.testing.assert_allclose( g.function(x, y), - np.array([[9.64172248e-175, 5.46609733e-099, 5.03457536e-045, - 7.53374790e-013, 1.83156389e-002], - [1.89386646e-098, 3.13356463e-044, 8.42346375e-012, - 3.67879441e-001, 2.61025584e-012], - [2.63952332e-044, 1.27462190e-011, 1.00000000e+000, - 1.27462190e-011, 2.63952332e-044], - [2.61025584e-012, 3.67879441e-001, 8.42346375e-012, - 3.13356463e-044, 1.89386646e-098], - [1.83156389e-002, 7.53374790e-013, 5.03457536e-045, - 5.46609733e-099, 9.64172248e-175]]) + np.array( + [ + [ + 9.64172248e-175, + 5.46609733e-099, + 5.03457536e-045, + 7.53374790e-013, + 1.83156389e-002, + ], + [ + 1.89386646e-098, + 3.13356463e-044, + 8.42346375e-012, + 3.67879441e-001, + 2.61025584e-012, + ], + [ + 2.63952332e-044, + 1.27462190e-011, + 1.00000000e000, + 1.27462190e-011, + 2.63952332e-044, + ], + [ + 2.61025584e-012, + 3.67879441e-001, + 8.42346375e-012, + 3.13356463e-044, + 1.89386646e-098, + ], + [ + 1.83156389e-002, + 7.53374790e-013, + 5.03457536e-045, + 5.46609733e-099, + 9.64172248e-175, + ], + ] + ), ) def test_no_rotation(self): g = hs.model.components2D.Expression( - GAUSSIAN2D_EXPR, name="gaussian2d", add_rotation=False, - position=("x0", "y0"),) - g.sy.value = .1 + GAUSSIAN2D_EXPR, + name="gaussian2d", + add_rotation=False, + position=("x0", "y0"), + ) + g.sy.value = 0.1 g.sx.value = 0.5 g.sy.value = 1 g.x0.value = 1 g.y0.value = 1 - l = np.linspace(-2, 2, 5) - x, y = np.meshgrid(l, l) + values = np.linspace(-2, 2, 5) + x, y = np.meshgrid(values, values) np.testing.assert_allclose(g.function(x, y), self.array0) np.testing.assert_allclose( g.grad_sx(x, y), - np.array([[1.21816650e-08, 1.19252902e-04, 1.20275135e-02, - 0.00000000e+00, 1.20275135e-02], - [1.48403061e-07, 1.45279775e-03, 1.46525111e-01, - 0.00000000e+00, 1.46525111e-01], - [6.65096376e-07, 6.51098781e-03, 6.56679989e-01, - 0.00000000e+00, 6.56679989e-01], - [1.09655854e-06, 1.07348041e-02, 1.08268227e+00, - 0.00000000e+00, 1.08268227e+00], - [6.65096376e-07, 6.51098781e-03, 6.56679989e-01, - 0.00000000e+00, 6.56679989e-01]]) - + np.array( + [ + [ + 1.21816650e-08, + 1.19252902e-04, + 1.20275135e-02, + 0.00000000e00, + 1.20275135e-02, + ], + [ + 1.48403061e-07, + 1.45279775e-03, + 1.46525111e-01, + 0.00000000e00, + 1.46525111e-01, + ], + [ + 6.65096376e-07, + 6.51098781e-03, + 6.56679989e-01, + 0.00000000e00, + 6.56679989e-01, + ], + [ + 1.09655854e-06, + 1.07348041e-02, + 1.08268227e00, + 0.00000000e00, + 1.08268227e00, + ], + [ + 6.65096376e-07, + 6.51098781e-03, + 6.56679989e-01, + 0.00000000e00, + 6.56679989e-01, + ], + ] + ), ) def test_no_function_nd(self): g = hs.model.components2D.Expression( - GAUSSIAN2D_EXPR, name="gaussian2d", add_rotation=False, - position=("x0", "y0"),) - g.sy.value = .1 + GAUSSIAN2D_EXPR, + name="gaussian2d", + add_rotation=False, + position=("x0", "y0"), + ) + g.sy.value = 0.1 g.sx.value = 0.5 g.sy.value = 1 g.x0.value = 1 g.y0.value = 1 - l = np.linspace(-2, 2, 5) - x, y = np.meshgrid(l, l) + values = np.linspace(-2, 2, 5) + x, y = np.meshgrid(values, values) np.testing.assert_allclose(g.function_nd(x, y), self.array0) def test_no_function_nd_signal(self): g = hs.model.components2D.Expression( - GAUSSIAN2D_EXPR, name="gaussian2d", add_rotation=False, - position=("x0", "y0"),) - g.sy.value = .1 + GAUSSIAN2D_EXPR, + name="gaussian2d", + add_rotation=False, + position=("x0", "y0"), + ) + g.sy.value = 0.1 g.sx.value = 0.5 g.sy.value = 1 g.x0.value = 1 g.y0.value = 1 - l = np.arange(0, 3) - x, y = np.meshgrid(l, l) + values = np.arange(0, 3) + x, y = np.meshgrid(values, values) s = hs.signals.Signal2D(g.function(x, y)) - s2 = hs.stack([s]*2) + s2 = hs.stack([s] * 2) m = s2.create_model() m.append(g) - # HyperSpy 2.0: remove setting iterpath='serpentine' - m.multifit(iterpath='serpentine') + m.multifit() res = g.function_nd(x, y) assert res.shape == (2, 3, 3) np.testing.assert_allclose(res, s2.data) diff --git a/hyperspy/tests/component/test_doniach.py b/hyperspy/tests/component/test_doniach.py index a705783958..1ec8dc1ea1 100644 --- a/hyperspy/tests/component/test_doniach.py +++ b/hyperspy/tests/component/test_doniach.py @@ -1,26 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2016 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np import pytest -from hyperspy.components1d import Doniach from hyperspy._signals.signal1d import Signal1D +from hyperspy.components1d import Doniach sqrt2pi = np.sqrt(2 * np.pi) sigma2fwhm = 2 * np.sqrt(2 * np.log(2)) @@ -44,7 +44,7 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): s = Signal1D(np.empty((200,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] - axis.scale = .05 + axis.scale = 0.05 axis.offset = -5 g1 = Doniach(centre=1, A=5, sigma=1, alpha=0.5) s.data = g1.function(axis.axis) @@ -59,8 +59,9 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g2.sigma.value, 2.331764, 0.01) np.testing.assert_allclose(g1.A.value, g2.A.value * factor, 0.3) diff --git a/hyperspy/tests/component/test_double_power_law.py b/hyperspy/tests/component/test_double_power_law.py deleted file mode 100644 index 7099eba053..0000000000 --- a/hyperspy/tests/component/test_double_power_law.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - -from hyperspy.components1d import DoublePowerLaw - - -def test_function(): - g = DoublePowerLaw() - g.A.value = 3 - g.r.value = 2 - g.origin.value = 1 - g.shift.value = 2 - g.ratio.value = 2 - assert np.isinf(g.function(1)) - assert np.isinf(g.function(3)) - assert g.function(-1) == 0 - assert g.function(0) == 0 - assert g.function(2) == 9 - np.testing.assert_allclose(g.function(10), 0.15948602) - assert g.grad_A(2) == 3 - np.testing.assert_allclose(g.grad_r(4), -0.3662041) - assert g.grad_origin(2) == -6 - assert g.grad_shift(2) == -12 - assert g.grad_ratio(2) == 3 diff --git a/hyperspy/tests/component/test_erf.py b/hyperspy/tests/component/test_erf.py index ce3528de8d..17fbe2c911 100644 --- a/hyperspy/tests/component/test_erf.py +++ b/hyperspy/tests/component/test_erf.py @@ -1,39 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . - -from distutils.version import LooseVersion - -import pytest -import sympy import numpy as np from hyperspy.components1d import Erf -pytestmark = pytest.mark.skipif(LooseVersion(sympy.__version__) < - LooseVersion("1.3"), - reason="This test requires SymPy >= 1.3") def test_function(): g = Erf() g.A.value = 1 g.sigma.value = 2 g.origin.value = 3 - assert g.function(3) == 0. - np.testing.assert_allclose(g.function(15),0.5) - np.testing.assert_allclose(g.function(1.951198),-0.2,rtol=1e-6) + assert g.function(3) == 0.0 + np.testing.assert_allclose(g.function(15), 0.5) + np.testing.assert_allclose(g.function(1.951198), -0.2, rtol=1e-6) diff --git a/hyperspy/tests/component/test_exponential.py b/hyperspy/tests/component/test_exponential.py index 77f6cc9e87..9a06ebb59b 100644 --- a/hyperspy/tests/component/test_exponential.py +++ b/hyperspy/tests/component/test_exponential.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import itertools @@ -30,12 +30,12 @@ def test_function(): g = Exponential() - g.A.value = 10000. - g.tau.value = 200. + g.A.value = 10000.0 + g.tau.value = 200.0 - test_value = 200. + test_value = 200.0 test_result = g.A.value * np.exp(-test_value / g.tau.value) - np.testing.assert_allclose(g.function(0.), g.A.value) + np.testing.assert_allclose(g.function(0.0), g.A.value) np.testing.assert_allclose(g.function(test_value), test_result) @@ -47,7 +47,7 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] axis.scale = 0.2 - axis.offset = 15. + axis.offset = 15.0 g1 = Exponential(A=10005.7, tau=214.3) s.data = g1.function(axis.axis) if not uniform: @@ -61,8 +61,9 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g1.A.value, g2.A.value * factor, rtol=0.05) np.testing.assert_allclose(g1.tau.value, g2.tau.value) @@ -84,8 +85,17 @@ def test_function_nd(binned, lazy): if lazy: s2 = s2.as_lazy() g2 = Exponential() - factor = axis.scale if binned else 1. + factor = axis.scale if binned else 1.0 g2.estimate_parameters(s2, axis.low_value, axis.high_value, False) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g2.function_nd(axis.axis) * factor, s2.data, rtol=0.05) + + +class TestLinearExponential: + def setup_method(self, method): + self.E = Exponential() + + def test_properties(self): + assert self.E.A._linear + assert not self.E.tau._linear diff --git a/hyperspy/tests/component/test_expression.py b/hyperspy/tests/component/test_expression.py new file mode 100644 index 0000000000..737cf4280e --- /dev/null +++ b/hyperspy/tests/component/test_expression.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import numpy as np +import pytest + +import hyperspy.api as hs + + +class TestExpression: + def setup_method(self, method): + self.g = hs.model.components1D.Expression( + expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2)", + name="Gaussian", + position="x0", + height=1, + fwhm=1, + x0=0, + module="numpy", + ) + + def test_name(self): + assert self.g.name == "Gaussian" + + def test_position(self): + assert self.g._position is self.g.x0 + + def test_f(self): + assert self.g.function(0) == 1 + + def test_grad_height(self): + np.testing.assert_allclose(self.g.grad_height(2), 1.5258789062500007e-05) + + def test_grad_x0(self): + np.testing.assert_allclose(self.g.grad_x0(2), 0.00016922538587889289) + + def test_grad_fwhm(self): + np.testing.assert_allclose(self.g.grad_fwhm(2), 0.00033845077175778578) + + def test_function_nd(self): + assert self.g.function_nd(0) == 1 + + +def test_expression_symbols(): + with pytest.raises(ValueError): + hs.model.components1D.Expression(expression="10.0", name="offset") + with pytest.raises(ValueError): + hs.model.components1D.Expression(expression="10", name="offset") + with pytest.raises(ValueError): + hs.model.components1D.Expression(expression="10*offset", name="Offset") + + +def test_expression_substitution(): + expr = "A / B; A = x+2; B = x-c" + comp = hs.model.components1D.Expression(expr, name="testcomp", autodoc=True, c=2) + assert "".join(p.name for p in comp.parameters) == "c" + assert comp.function(1) == -3 + + +def test_separate_pseudocomponents(): + A = hs.model.components1D.Expression("a*b*x+c**2*x", "test") + free, fixed = A._separate_pseudocomponents() + assert list(free.keys()) == ["a", "b", "c"] + assert list(fixed.keys()) == ["function", "parameters"] + + A.a.free = False + A.b.free = False + + free, fixed = A._separate_pseudocomponents() + assert list(free.keys()) == ["c"] + + +def test_separate_pseudocomponents_expression_rename_parameters(): + lorentzian = hs.model.components1D.Lorentzian() + free, fixed = lorentzian._separate_pseudocomponents() + assert list(free.keys()) == ["A", "centre", "gamma"] + assert list(fixed.keys()) == ["function", "parameters"] + + lorentzian.centre.free = False + lorentzian.gamma.free = False + + free, fixed = lorentzian._separate_pseudocomponents() + assert list(free.keys()) == ["A"] + + +def test_linear_rename_parameters(): + # with the lorentzian component, the gamma component is rename + lorentzian = hs.model.components1D.Lorentzian() + assert lorentzian.A._linear + assert not lorentzian.gamma._linear + assert not lorentzian.centre._linear + + g = hs.model.components1D.Expression( + expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2)", + name="Gaussian", + rename_pars={"height": "O"}, + ) + assert not hasattr(g, "height") + assert g.O._linear + assert not g.fwhm._linear + assert not g.x0._linear + + +def test_constant_term_rename_parameters(): + g = hs.model.components1D.Expression( + expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2) + 10", + name="Gaussian", + fwhm=1.0, + ) + assert g._constant_term == 10.0 + + g = hs.model.components1D.Expression( + expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2) + 10", + name="Gaussian", + rename_pars={"height": "O"}, + fwhm=1.0, + ) + assert g._constant_term == 10.0 + + g = hs.model.components1D.Expression( + expression="height * exp(-(x - x0) ** 2 * 4 * log(2)/ fwhm ** 2) + 10", + name="Gaussian", + rename_pars={"x0": "O"}, + fwhm=1.0, + ) + assert g._constant_term == 10.0 diff --git a/hyperspy/tests/component/test_gaussian.py b/hyperspy/tests/component/test_gaussian.py index 47ad2eb51c..e347e5e402 100644 --- a/hyperspy/tests/component/test_gaussian.py +++ b/hyperspy/tests/component/test_gaussian.py @@ -1,29 +1,27 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import itertools import numpy as np import pytest -from hyperspy.components1d import Gaussian -from hyperspy.signals import Signal1D -from hyperspy.utils import stack +import hyperspy.api as hs sqrt2pi = np.sqrt(2 * np.pi) sigma2fwhm = 2 * np.sqrt(2 * np.log(2)) @@ -32,7 +30,7 @@ def test_function(): - g = Gaussian() + g = hs.model.components1D.Gaussian() g.centre.value = 1 g.sigma.value = 2 / sigma2fwhm g.A.value = 3 * sqrt2pi * g.sigma.value @@ -44,26 +42,27 @@ def test_function(): @pytest.mark.parametrize(("uniform"), (True, False)) @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) def test_estimate_parameters_binned(only_current, binned, lazy, uniform): - s = Signal1D(np.empty((100,))) + s = hs.signals.Signal1D(np.empty((100,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] axis.scale = 1 axis.offset = -20 - g1 = Gaussian(50015.156, 10/sigma2fwhm, 10) + g1 = hs.model.components1D.Gaussian(50015.156, 10 / sigma2fwhm, 10) s.data = g1.function(axis.axis) if not uniform: axis.convert_to_non_uniform_axis() if lazy: s = s.as_lazy() - g2 = Gaussian() + g2 = hs.model.components1D.Gaussian() if binned and uniform: factor = axis.scale elif binned: factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g1.A.value, g2.A.value * factor) assert abs(g2.centre.value - g1.centre.value) <= 1e-3 @@ -71,14 +70,14 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): def test_estimate_parameters_negative_scale(): - s = Signal1D(np.empty((100,))) + s = hs.signals.Signal1D(np.empty((100,))) axis = s.axes_manager.signal_axes[0] axis.scale = -1 axis.offset = 100 - g1 = Gaussian(50015.156, 15/sigma2fwhm, 50) + g1 = hs.model.components1D.Gaussian(50015.156, 15 / sigma2fwhm, 50) s.data = g1.function(axis.axis) - g2 = Gaussian() + g2 = hs.model.components1D.Gaussian() with pytest.raises(ValueError): g2.estimate_parameters(s, 40, 60) assert g2.estimate_parameters(s, 90, 10) @@ -90,17 +89,17 @@ def test_estimate_parameters_negative_scale(): @pytest.mark.parametrize(("lazy"), (True, False)) @pytest.mark.parametrize(("binned"), (True, False)) def test_function_nd(binned, lazy): - s = Signal1D(np.empty((100,))) + s = hs.signals.Signal1D(np.empty((100,))) axis = s.axes_manager.signal_axes[0] axis.scale = 1 axis.offset = -20 - g1 = Gaussian(50015.156, 10/sigma2fwhm, 10) + g1 = hs.model.components1D.Gaussian(50015.156, 10 / sigma2fwhm, 10) s.data = g1.function(axis.axis) s.axes_manager.signal_axes[0].is_binned = binned - s2 = stack([s] * 2) + s2 = hs.stack([s] * 2) if lazy: s2 = s2.as_lazy() - g2 = Gaussian() + g2 = hs.model.components1D.Gaussian() factor = axis.scale if binned else 1 g2.estimate_parameters(s2, axis.low_value, axis.high_value, False) assert g2._axes_manager[-1].is_binned == binned @@ -108,35 +107,75 @@ def test_function_nd(binned, lazy): def test_util_fwhm_set(): - g1 = Gaussian() + g1 = hs.model.components1D.Gaussian() g1.fwhm = 1.0 np.testing.assert_allclose(g1.sigma.value, 1.0 / sigma2fwhm) def test_util_fwhm_get(): - g1 = Gaussian() + g1 = hs.model.components1D.Gaussian() g1.sigma.value = 1.0 np.testing.assert_allclose(g1.fwhm, 1.0 * sigma2fwhm) def test_util_fwhm_getset(): - g1 = Gaussian() + g1 = hs.model.components1D.Gaussian() g1.fwhm = 1.0 np.testing.assert_allclose(g1.fwhm, 1.0) + def test_util_height_set(): - g1 = Gaussian() + g1 = hs.model.components1D.Gaussian() g1.sigma.value = 3.0 - g1.height = 2.0/sqrt2pi + g1.height = 2.0 / sqrt2pi np.testing.assert_allclose(g1.A.value, 6) + def test_util_height_get(): - g1 = Gaussian() + g1 = hs.model.components1D.Gaussian() g1.sigma.value = 4.0 - g1.A.value = sqrt2pi*8 + g1.A.value = sqrt2pi * 8 np.testing.assert_allclose(g1.height, 2) + def test_util_height_getset(): - g1 = Gaussian() + g1 = hs.model.components1D.Gaussian() g1.height = 4.0 np.testing.assert_allclose(g1.height, 4.0) + + +class TestGaussian: + def setup_method(self, method): + s = hs.signals.Signal1D(np.zeros(1024)) + s.axes_manager[0].offset = -5 + s.axes_manager[0].scale = 0.01 + m = s.create_model() + m.append(hs.model.components1D.Gaussian()) + m[0].sigma.value = 0.5 + m[0].centre.value = 1 + m[0].A.value = 2 + self.m = m + + @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) + def test_estimate_parameters_binned(self, only_current, binned): + self.m.signal.axes_manager[-1].is_binned = binned + s = self.m.as_signal() + assert s.axes_manager[-1].is_binned == binned + g = hs.model.components1D.Gaussian() + g.estimate_parameters(s, None, None, only_current=only_current) + assert g._axes_manager[-1].is_binned == binned + np.testing.assert_allclose(g.sigma.value, 0.5) + np.testing.assert_allclose(g.A.value, 2) + np.testing.assert_allclose(g.centre.value, 1) + + @pytest.mark.parametrize("binned", (True, False)) + def test_function_nd(self, binned): + self.m.signal.axes_manager[-1].is_binned = binned + s = self.m.as_signal() + s2 = hs.stack([s] * 2) + g = hs.model.components1D.Gaussian() + g.estimate_parameters(s2, None, None, only_current=False) + assert g._axes_manager[-1].is_binned == binned + axis = s.axes_manager.signal_axes[0] + factor = axis.scale if binned else 1 + np.testing.assert_allclose(g.function_nd(axis.axis) * factor, s2.data) diff --git a/hyperspy/tests/component/test_gaussian2d.py b/hyperspy/tests/component/test_gaussian2d.py index bf16a62e6f..6eaa260e98 100644 --- a/hyperspy/tests/component/test_gaussian2d.py +++ b/hyperspy/tests/component/test_gaussian2d.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import math @@ -29,10 +29,10 @@ def test_function(): g = Gaussian2D() g.A.value = 14 - g.sigma_x.value = 1. - g.sigma_y.value = 2. - g.centre_x.value = -5. - g.centre_y.value = -5. + g.sigma_x.value = 1.0 + g.sigma_y.value = 2.0 + g.centre_x.value = -5.0 + g.centre_y.value = -5.0 np.testing.assert_allclose(g.function(-5, -5), 1.1140846) np.testing.assert_allclose(g.function(-2, -3), 0.007506643) assert g._is2D diff --git a/hyperspy/tests/component/test_gaussianhf.py b/hyperspy/tests/component/test_gaussianhf.py index e43dcdf9a6..22bdb1ec7c 100644 --- a/hyperspy/tests/component/test_gaussianhf.py +++ b/hyperspy/tests/component/test_gaussianhf.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import itertools @@ -39,23 +39,24 @@ def test_function(): assert g.function(2) == 1.5 assert g.function(1) == 3 + def test_integral_as_signal(): s = Signal1D(np.zeros((2, 3, 100))) - g1 = GaussianHF(fwhm=3.33, centre=20.) + g1 = GaussianHF(fwhm=3.33, centre=20.0) h_ref = np.linspace(0.1, 3.0, s.axes_manager.navigation_size) - for d, h in zip(s._iterate_signal(), h_ref): + for d, h in zip(s._iterate_signal("flyback"), h_ref): g1.height.value = h d[:] = g1.function(s.axes_manager.signal_axes[0].axis) m = s.create_model() g2 = GaussianHF() m.append(g2) g2.estimate_parameters(s, 0, 100, True) - # HyperSpy 2.0: remove setting iterpath='serpentine' - m.multifit(iterpath='serpentine') + m.multifit() s_out = g2.integral_as_signal() ref = (h_ref * 3.33 * sqrt2pi / sigma2fwhm).reshape(s_out.data.shape) np.testing.assert_allclose(s_out.data, ref) + @pytest.mark.parametrize(("lazy"), (True, False)) @pytest.mark.parametrize(("uniform"), (True, False)) @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) @@ -63,7 +64,7 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): s = Signal1D(np.empty((100,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] - axis.scale = 2. + axis.scale = 2.0 axis.offset = -30 g1 = GaussianHF(50015.156, 23, 10) s.data = g1.function(axis.axis) @@ -78,19 +79,21 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g1.height.value, g2.height.value * factor) assert abs(g2.centre.value - g1.centre.value) <= 1e-3 assert abs(g2.fwhm.value - g1.fwhm.value) <= 0.1 + @pytest.mark.parametrize(("lazy"), (True, False)) @pytest.mark.parametrize(("binned"), (True, False)) def test_function_nd(binned, lazy): s = Signal1D(np.empty((100,))) axis = s.axes_manager.signal_axes[0] - axis.scale = 2. + axis.scale = 2.0 axis.offset = -30 g1 = GaussianHF(50015.156, 23, 10) s.data = g1.function(axis.axis) @@ -105,32 +108,37 @@ def test_function_nd(binned, lazy): # TODO: sort out while the rtol to be so high... np.testing.assert_allclose(g2.function_nd(axis.axis) * factor, s2.data, rtol=0.05) + def test_util_sigma_set(): g1 = GaussianHF() g1.sigma = 1.0 np.testing.assert_allclose(g1.fwhm.value, 1.0 * sigma2fwhm) + def test_util_sigma_get(): g1 = GaussianHF() g1.fwhm.value = 1.0 np.testing.assert_allclose(g1.sigma, 1.0 / sigma2fwhm) + def test_util_sigma_getset(): g1 = GaussianHF() g1.sigma = 1.0 np.testing.assert_allclose(g1.sigma, 1.0) + def test_util_fwhm_set(): g1 = GaussianHF(fwhm=0.33) g1.A = 1.0 - np.testing.assert_allclose(g1.height.value, 1.0 * sigma2fwhm / ( - 0.33 * sqrt2pi)) + np.testing.assert_allclose(g1.height.value, 1.0 * sigma2fwhm / (0.33 * sqrt2pi)) + def test_util_fwhm_get(): g1 = GaussianHF(fwhm=0.33) g1.height.value = 1.0 np.testing.assert_allclose(g1.A, 1.0 * sqrt2pi * 0.33 / sigma2fwhm) + def test_util_fwhm_getset(): g1 = GaussianHF(fwhm=0.33) g1.A = 1.0 diff --git a/hyperspy/tests/component/test_heaviside.py b/hyperspy/tests/component/test_heaviside.py index 7feb97134b..a15227259e 100644 --- a/hyperspy/tests/component/test_heaviside.py +++ b/hyperspy/tests/component/test_heaviside.py @@ -1,29 +1,62 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2016 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import numpy as np -from hyperspy.components1d import HeavisideStep +import hyperspy.api as hs def test_function(): - g = HeavisideStep() + g = hs.model.components1D.HeavisideStep() g.A.value = 3 g.n.value = 2 - assert g.function(1) == 0. + assert g.function(1) == 0.0 assert g.function(2) == 1.5 - assert g.function(3) == 3. + assert g.function(3) == 3.0 + + +class TestHeavisideStep: + def setup_method(self, method): + self.c = hs.model.components1D.HeavisideStep() + + def test_integer_values(self): + c = self.c + np.testing.assert_array_almost_equal( + c.function(np.array([-1, 0, 2])), np.array([0, 0.5, 1]) + ) + + def test_float_values(self): + c = self.c + np.testing.assert_array_almost_equal( + c.function(np.array([-0.5, 0.5, 2])), np.array([0, 1, 1]) + ) + + def test_not_sorted(self): + c = self.c + np.testing.assert_array_almost_equal( + c.function(np.array([3, -0.1, 0])), np.array([1, 0, 0.5]) + ) + + # def test_gradients(self): + # c = self.c + # np.testing.assert_array_almost_equal( + # c.A.grad(np.array([3, -0.1, 0])), np.array([1, 0, 0.5]) + # ) + # np.testing.assert_array_almost_equal( + # c.n.grad(np.array([3, -0.1, 0])), np.array([1, 1, 1]) + # ) diff --git a/hyperspy/tests/component/test_logistic.py b/hyperspy/tests/component/test_logistic.py index 019a4c3d13..09298b7982 100644 --- a/hyperspy/tests/component/test_logistic.py +++ b/hyperspy/tests/component/test_logistic.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -28,5 +28,5 @@ def test_function(): g.c.value = 3 g.origin.value = 4 np.testing.assert_allclose(g.function(10), 1) - np.testing.assert_allclose(g.function(4), 1/3) + np.testing.assert_allclose(g.function(4), 1 / 3) np.testing.assert_allclose(g.function(0), 3.07209674e-06) diff --git a/hyperspy/tests/component/test_lorentzian.py b/hyperspy/tests/component/test_lorentzian.py index 2be8a61cd0..8ab5143c2d 100644 --- a/hyperspy/tests/component/test_lorentzian.py +++ b/hyperspy/tests/component/test_lorentzian.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import itertools @@ -44,7 +44,7 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): s = Signal1D(np.empty((250,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] - axis.scale = .2 + axis.scale = 0.2 axis.offset = -15 g1 = Lorentzian(52342, 2, 10) s.data = g1.function(axis.axis) @@ -59,10 +59,11 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned - np.testing.assert_allclose(g1.A.value, g2.A.value * factor,0.1) + np.testing.assert_allclose(g1.A.value, g2.A.value * factor, 0.1) assert abs(g2.centre.value - g1.centre.value) <= 0.2 assert abs(g2.gamma.value - g1.gamma.value) <= 0.1 @@ -72,7 +73,7 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): def test_function_nd(binned, lazy): s = Signal1D(np.empty((250,))) axis = s.axes_manager.signal_axes[0] - axis.scale = .2 + axis.scale = 0.2 axis.offset = -15 g1 = Lorentzian(52342, 2, 10) s.data = g1.function(axis.axis) @@ -84,7 +85,7 @@ def test_function_nd(binned, lazy): factor = axis.scale if binned else 1 g2.estimate_parameters(s2, axis.low_value, axis.high_value, False) assert g2._axes_manager[-1].is_binned == binned - np.testing.assert_allclose(g2.function_nd(axis.axis) * factor, s2.data,0.16) + np.testing.assert_allclose(g2.function_nd(axis.axis) * factor, s2.data, 0.16) def test_util_gamma_getset(): @@ -114,14 +115,14 @@ def test_util_fwhm_getset(): def test_util_height_set(): g1 = Lorentzian() g1.gamma.value = 4.0 - g1.height = 2.0/np.pi + g1.height = 2.0 / np.pi np.testing.assert_allclose(g1.A.value, 8) def test_util_height_get(): g1 = Lorentzian() g1.gamma.value = 3.0 - g1.A.value = np.pi*1.5 + g1.A.value = np.pi * 1.5 np.testing.assert_allclose(g1.height, 0.5) diff --git a/hyperspy/tests/component/test_offset.py b/hyperspy/tests/component/test_offset.py new file mode 100644 index 0000000000..53bb7a51d7 --- /dev/null +++ b/hyperspy/tests/component/test_offset.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import itertools + +import numpy as np +import pytest + +import hyperspy.api as hs + +TRUE_FALSE_2_TUPLE = [p for p in itertools.product((True, False), repeat=2)] + + +class TestOffset: + def setup_method(self, method): + s = hs.signals.Signal1D(np.zeros(10)) + s.axes_manager[0].scale = 0.01 + m = s.create_model() + m.append(hs.model.components1D.Offset()) + m[0].offset.value = 10 + self.m = m + + @pytest.mark.parametrize(("uniform"), (True, False)) + @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) + def test_estimate_parameters(self, only_current, binned, uniform): + self.m.signal.axes_manager[-1].is_binned = binned + s = self.m.as_signal() + if not uniform: + s.axes_manager[-1].convert_to_non_uniform_axis() + assert s.axes_manager[-1].is_binned == binned + o = hs.model.components1D.Offset() + o.estimate_parameters(s, None, None, only_current=only_current) + assert o._axes_manager[-1].is_binned == binned + assert o._axes_manager[-1].is_uniform == uniform + np.testing.assert_allclose(o.offset.value, 10) + + @pytest.mark.parametrize(("uniform"), (True, False)) + @pytest.mark.parametrize(("binned"), (True, False)) + def test_function_nd(self, binned, uniform): + self.m.signal.axes_manager[-1].is_binned = binned + s = self.m.as_signal() + s = hs.stack([s] * 2) + o = hs.model.components1D.Offset() + o.estimate_parameters(s, None, None, only_current=False) + assert o._axes_manager[-1].is_binned == binned + axis = s.axes_manager.signal_axes[0] + factor = axis.scale if binned else 1 + np.testing.assert_allclose(o.function_nd(axis.axis) * factor, s.data) + + def test_constant_term(self): + m = self.m + o = m[0] + o.offset.free = True + assert o._constant_term == 0 + + o.offset.free = False + assert o._constant_term == o.offset.value diff --git a/hyperspy/tests/component/test_pes_core_line_shape.py b/hyperspy/tests/component/test_pes_core_line_shape.py deleted file mode 100755 index 721731171b..0000000000 --- a/hyperspy/tests/component/test_pes_core_line_shape.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import pytest - -import hyperspy.api as hs -from hyperspy.components1d import PESCoreLineShape - - -def test_PESCoreLineShape(): - core_line = PESCoreLineShape(A=10, FWHM=1.5, origin=0.5) - x = np.linspace(-5, 15, 10) - np.testing.assert_allclose( - core_line.function(x), - np.array([8.97054744e-04, 0.365234208, 7.09463858, 6.57499512, - 0.290714653, 6.13260141e-04, 6.17204216e-08, 2.96359844e-13, - 6.78916184e-20, 7.42026292e-28]) - ) - assert core_line._position is core_line.origin - - -def test_PESCoreLineShape_shirley(): - core_line = PESCoreLineShape(A=10, FWHM=1.5, origin=0.5) - core_line.Shirley = True - core_line.shirley.value = 0.01 - x = np.linspace(-5, 15, 10) - np.testing.assert_allclose( - core_line.function(x), - np.array([0.144159014, 0.504843825, 7.16330182, 6.57790840, - 0.290720786, 6.13260758e-04, 6.17204245e-08, 2.96359844e-13, - 6.78916184e-20, 7.42026292e-28]) - ) - np.testing.assert_allclose(core_line.function(x), core_line.function_nd(x)) - - -@pytest.mark.parametrize('Shirley', [False, True]) -def test_PESCoreLineShape_fit(Shirley): - # Component parameter values - A = 10 - FWHM = 0.5 - origin = 5.0 - shirley = 0.01 if Shirley else 0.0 - - offset, scale, size = 0, 0.1, 100 - x = np.linspace(offset, scale*size, size) - comp = PESCoreLineShape(A=A, FWHM=FWHM, origin=origin) - comp.Shirley = Shirley - comp.shirley.value = shirley - - s = hs.signals.Signal1D(comp.function(x)) - axis = s.axes_manager[0] - axis.offset, axis.scale = offset, scale - s.add_gaussian_noise(0.1, random_state=1) - m = s.create_model() - core_line = PESCoreLineShape(A=1, FWHM=1.5, origin=0.5) - core_line.Shirley = Shirley - m.append(core_line) - m.fit(grad='analytical') - np.testing.assert_allclose(core_line.A.value, A, rtol=0.1) - np.testing.assert_allclose(abs(core_line.FWHM.value), FWHM, rtol=0.1) - np.testing.assert_allclose(core_line.origin.value, origin, rtol=0.1) - np.testing.assert_allclose(core_line.shirley.value, shirley, rtol=0.1) - - -@pytest.mark.parametrize('Shirley', [False, True]) -def test_PESCoreLineShape_function_nd(Shirley): - A, FWHM, origin = 10, 1.5, 0. - core_line = PESCoreLineShape(A=A, FWHM=FWHM, origin=origin) - core_line.Shirley = Shirley - core_line.shirley.value = 0.01 if Shirley else 0.0 - x = np.linspace(-5, 15, 1000) - s = hs.signals.Signal1D(np.array([x]*2)) - - # Manually set to test function_nd - core_line._axes_manager = s.axes_manager - core_line._create_arrays() - core_line.A.map['values'] = [A] * 2 - core_line.FWHM.map['values'] = [FWHM] * 2 - core_line.origin.map['values'] = [origin] * 2 - core_line.shirley.map['values'] = [core_line.shirley.value] * 2 - - values = core_line.function_nd(x) - assert values.shape == (2, len(x)) - for v in values: - np.testing.assert_allclose(v, core_line.function(x), rtol=0.5) - - -@pytest.mark.parametrize('Shirley', [False, True]) -def test_recreate_component(Shirley): - core_line = PESCoreLineShape(A=10, FWHM=1.5, origin=0.5) - core_line.Shirley = Shirley - - s = hs.signals.Signal1D(np.zeros(10)) - m = s.create_model() - m.append(core_line) - model_dict = m.as_dictionary() - - m2 = s.create_model() - m2._load_dictionary(model_dict) - assert m2[0].Shirley == Shirley - - diff --git a/hyperspy/tests/component/test_pes_see.py b/hyperspy/tests/component/test_pes_see.py deleted file mode 100755 index 633d0bba14..0000000000 --- a/hyperspy/tests/component/test_pes_see.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - -import hyperspy.api as hs -from hyperspy.components1d import SEE -from hyperspy.misc.test_utils import ignore_warning - - -def test_see(): - see = SEE(A=10, Phi=1.5, B=0.5) - x = np.linspace(-5, 15, 10) - np.testing.assert_allclose( - see.function(x), - np.array([0.0, 0.0, 0.0, 8.4375, 0.342983001, 0.0675685032, - 0.0236279967, 0.010861538, 0.005860978, 0.003514161]) - ) - np.testing.assert_allclose(see.function(x), see.function_nd(x)) - - _ = SEE(A=10, Phi=1.5, B=0.5, sigma=0) - - -def test_see_fit(): - # Component parameter values - A = 10 - Phi = 2.5 - B = 0.5 - - offset, scale, size = 0, 0.1, 100 - x = np.linspace(offset, scale*size, size) - s = hs.signals.Signal1D(SEE(A=A, Phi=Phi, B=B).function(x)) - axis = s.axes_manager[0] - axis.offset, axis.scale = offset, scale - s.add_gaussian_noise(0.1, random_state=1) - m = s.create_model() - see = SEE(A=1, Phi=1.5, B=0.5) - m.append(see) - with ignore_warning(message="divide by zero", - category=RuntimeWarning): - m.fit(grad='analytical') - np.testing.assert_allclose(see.A.value, A, rtol=0.1) - np.testing.assert_allclose(see.Phi.value, Phi, rtol=0.1) - np.testing.assert_allclose(see.B.value, B, rtol=0.1) - - -def test_see_function_nd(): - A, Phi, B = 10, 1.5, 0.5 - see = SEE(A=A, Phi=Phi, B=B) - x = np.linspace(-5, 15, 10) - s = hs.signals.Signal1D(np.array([x]*2)) - - # Manually set to test function_nd - see._axes_manager = s.axes_manager - see._create_arrays() - see.A.map['values'] = [A] * 2 - see.Phi.map['values'] = [Phi] * 2 - see.B.map['values'] = [B] * 2 - - values = see.function_nd(x) - assert values.shape == (2, 10) - for v in values: - np.testing.assert_allclose(v, see.function(x)) diff --git a/hyperspy/tests/component/test_pes_voigt.py b/hyperspy/tests/component/test_pes_voigt.py deleted file mode 100644 index 1b671241de..0000000000 --- a/hyperspy/tests/component/test_pes_voigt.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2016 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import itertools - -import numpy as np -import pytest - -#Legacy test, to be removed in v2.0 -from hyperspy.components1d import PESVoigt, Voigt -from hyperspy.exceptions import VisibleDeprecationWarning -from hyperspy.signals import Signal1D - -TRUE_FALSE_2_TUPLE = [p for p in itertools.product((True, False), repeat=2)] - - -def test_function(): - g = PESVoigt() - g.area.value = 5 - g.FWHM.value = 0.5 - g.gamma.value = 0.2 - g.centre.value = 1 - np.testing.assert_allclose(g.function(0), 0.35380168) - np.testing.assert_allclose(g.function(1), 5.06863535) - assert g._position is g.centre - - -@pytest.mark.parametrize(("lazy"), (True, False)) -@pytest.mark.parametrize(("uniform"), (True, False)) -@pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) -def test_estimate_parameters_binned(only_current, binned, lazy, uniform): - s = Signal1D(np.empty((200,))) - s.axes_manager.signal_axes[0].is_binned = binned - axis = s.axes_manager.signal_axes[0] - axis.scale = .05 - axis.offset = -5 - g1 = PESVoigt() - g1.centre.value = 1 - g1.area.value = 5. - g1.gamma.value = 0.001 - g1.FWHM.value = 0.5 - s.data = g1.function(axis.axis) - if not uniform: - axis.convert_to_non_uniform_axis() - if lazy: - s = s.as_lazy() - g2 = PESVoigt() - if binned and uniform: - factor = axis.scale - elif binned: - factor = np.gradient(axis.axis) - else: - factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) - assert g2._axes_manager[-1].is_binned == binned - np.testing.assert_allclose(g2.FWHM.value, 1, 0.5) - np.testing.assert_allclose(g1.area.value, g2.area.value * factor, 0.04) - np.testing.assert_allclose(g2.centre.value, 1, 1e-3) - - -def test_legacy(): - """Legacy test, to be removed in v2.0.""" - with pytest.warns( - VisibleDeprecationWarning, - match="API of the `Voigt` component will change", - ): - g = Voigt(legacy=True) - g.area.value = 5 - g.FWHM.value = 0.5 - g.gamma.value = 0.2 - g.centre.value = 1 - np.testing.assert_allclose(g.function(0), 0.35380168) - np.testing.assert_allclose(g.function(1), 5.06863535) diff --git a/hyperspy/tests/component/test_polynomial.py b/hyperspy/tests/component/test_polynomial.py new file mode 100644 index 0000000000..892150d454 --- /dev/null +++ b/hyperspy/tests/component/test_polynomial.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import itertools + +import numpy as np +import pytest + +import hyperspy.api as hs +from hyperspy.models.model1d import Model1D + +TRUE_FALSE_2_TUPLE = [p for p in itertools.product((True, False), repeat=2)] + + +class TestPolynomial: + def setup_method(self, method): + s = hs.signals.Signal1D(np.zeros(1024)) + s.axes_manager[0].offset = -5 + s.axes_manager[0].scale = 0.01 + m = s.create_model() + m.append(hs.model.components1D.Polynomial(order=2)) + coeff_values = (0.5, 2, 3) + self.m = m + s_2d = hs.signals.Signal1D(np.arange(1000).reshape(10, 100)) + self.m_2d = s_2d.create_model() + self.m_2d.append(hs.model.components1D.Polynomial(order=2)) + s_3d = hs.signals.Signal1D(np.arange(1000).reshape(2, 5, 100)) + self.m_3d = s_3d.create_model() + self.m_3d.append(hs.model.components1D.Polynomial(order=2)) + data = 50 * np.ones(100) + s_offset = hs.signals.Signal1D(data) + self.m_offset = s_offset.create_model() + + # if same component is pased, axes_managers get mixed up, tests + # sometimes randomly fail + for _m in [self.m, self.m_2d, self.m_3d]: + _m[0].a2.value = coeff_values[0] + _m[0].a1.value = coeff_values[1] + _m[0].a0.value = coeff_values[2] + + def test_gradient(self): + poly = self.m[0] + np.testing.assert_allclose(poly.a2.grad(np.arange(3)), np.array([0, 1, 4])) + np.testing.assert_allclose(poly.a1.grad(np.arange(3)), np.array([0, 1, 2])) + np.testing.assert_allclose(poly.a0.grad(np.arange(3)), np.array([1, 1, 1])) + + def test_fitting(self): + s_2d = self.m_2d.signal + s_2d.data += 100 * np.array([np.random.randint(50, size=10)] * 100).T + m_2d = s_2d.create_model() + m_2d.append(hs.model.components1D.Polynomial(order=1)) + m_2d.multifit(grad="analytical") + np.testing.assert_allclose(m_2d.red_chisq.data.sum(), 0.0, atol=1e-7) + + @pytest.mark.parametrize(("order"), (2, 12)) + @pytest.mark.parametrize(("uniform"), (True, False)) + @pytest.mark.parametrize(("mapnone"), (True, False)) + @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) + def test_estimate_parameters(self, only_current, binned, uniform, order, mapnone): + self.m.signal.axes_manager[-1].is_binned = binned + s = self.m.as_signal() + s.axes_manager[-1].is_binned = binned + if not uniform: + s.axes_manager[-1].convert_to_non_uniform_axis() + p = hs.model.components1D.Polynomial(order=order) + if mapnone: + p.parameters[0].map = None + p.estimate_parameters(s, None, None, only_current=only_current) + assert p._axes_manager[-1].is_binned == binned + assert p._axes_manager[-1].is_uniform == uniform + np.testing.assert_allclose(p.parameters[2].value, 0.5) + np.testing.assert_allclose(p.parameters[1].value, 2) + np.testing.assert_allclose(p.parameters[0].value, 3) + + def test_zero_order(self): + m = self.m_offset + with pytest.raises(ValueError): + m.append(hs.model.components1D.Polynomial(order=0)) + + def test_2d_signal(self): + # This code should run smoothly, any exceptions should trigger failure + s = self.m_2d.as_signal() + model = Model1D(s) + p = hs.model.components1D.Polynomial(order=2) + model.append(p) + p.estimate_parameters(s, 0, 100, only_current=False) + np.testing.assert_allclose(p.a2.map["values"], 0.5) + np.testing.assert_allclose(p.a1.map["values"], 2) + np.testing.assert_allclose(p.a0.map["values"], 3) + + def test_3d_signal(self): + # This code should run smoothly, any exceptions should trigger failure + s = self.m_3d.as_signal() + model = Model1D(s) + p = hs.model.components1D.Polynomial(order=2) + model.append(p) + p.estimate_parameters(s, 0, 100, only_current=False) + np.testing.assert_allclose(p.a2.map["values"], 0.5) + np.testing.assert_allclose(p.a1.map["values"], 2) + np.testing.assert_allclose(p.a0.map["values"], 3) + + def test_function_nd(self): + s = self.m.as_signal() + s = hs.stack([s] * 2) + p = hs.model.components1D.Polynomial(order=2) + p.estimate_parameters(s, None, None, only_current=False) + axis = s.axes_manager.signal_axes[0] + np.testing.assert_allclose(p.function_nd(axis.axis), s.data) diff --git a/hyperspy/tests/component/test_powerlaw.py b/hyperspy/tests/component/test_powerlaw.py index 05cf750396..9e7db736f5 100644 --- a/hyperspy/tests/component/test_powerlaw.py +++ b/hyperspy/tests/component/test_powerlaw.py @@ -1,77 +1,144 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import itertools import numpy as np import pytest -from hyperspy.components1d import PowerLaw -from hyperspy.signals import Signal1D -from hyperspy.utils import stack +import hyperspy.api as hs TRUE_FALSE_2_TUPLE = [p for p in itertools.product((True, False), repeat=2)] def test_function(): - g = PowerLaw() + g = hs.model.components1D.PowerLaw() g.A.value = 1 g.r.value = 2 g.origin.value = 3 assert g.function(2) == 1 assert g.function(1) == 0.25 + +def test_linear_override(): + g = hs.model.components1D.PowerLaw() + for para in g.parameters: + if para is g.A: + assert para._linear + else: + assert not para._linear + + @pytest.mark.parametrize(("lazy"), (True, False)) @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) def test_estimate_parameters_binned(only_current, binned, lazy): - s = Signal1D(np.empty((100,))) + s = hs.signals.Signal1D(np.empty((100,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] axis.scale = 0.02 axis.offset = 1 - g1 = PowerLaw(50015.156, 1.2) + g1 = hs.model.components1D.PowerLaw(50015.156, 1.2) s.data = g1.function(axis.axis) if lazy: s = s.as_lazy() - g2 = PowerLaw() + g2 = hs.model.components1D.PowerLaw() factor = axis.scale if binned else 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned # error of the estimate function is rather large, esp. when binned=FALSE np.testing.assert_allclose(g1.A.value, g2.A.value * factor, rtol=0.05) assert abs(g2.r.value - g1.r.value) <= 2e-2 + @pytest.mark.parametrize(("lazy"), (True, False)) @pytest.mark.parametrize(("binned"), (True, False)) def test_function_nd(binned, lazy): - s = Signal1D(np.empty((100,))) + s = hs.signals.Signal1D(np.empty((100,))) axis = s.axes_manager.signal_axes[0] axis.scale = 0.02 axis.offset = 1 - g1 = PowerLaw(50015.156, 1.2) + g1 = hs.model.components1D.PowerLaw(50015.156, 1.2) s.data = g1.function(axis.axis) s.axes_manager.signal_axes[0].is_binned = binned - s2 = stack([s] * 2) + s2 = hs.stack([s] * 2) if lazy: s = s.as_lazy() - g2 = PowerLaw() + g2 = hs.model.components1D.PowerLaw() factor = axis.scale if binned else 1 g2.estimate_parameters(s2, axis.low_value, axis.high_value, False) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g2.function_nd(axis.axis) * factor, s2.data, rtol=0.05) + + +class TestPowerLaw: + def setup_method(self, method): + s = hs.signals.Signal1D(np.zeros(1024)) + s.axes_manager[0].offset = 100 + s.axes_manager[0].scale = 0.01 + m = s.create_model() + m.append(hs.model.components1D.PowerLaw()) + m[0].A.value = 1000 + m[0].r.value = 4 + self.m = m + self.s = s + + @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) + def test_estimate_parameters(self, only_current, binned): + self.m.signal.axes_manager[-1].is_binned = binned + s = self.m.as_signal() + assert s.axes_manager[-1].is_binned == binned + g = hs.model.components1D.PowerLaw() + g.estimate_parameters(s, None, None, only_current=only_current) + assert g._axes_manager[-1].is_binned == binned + A_value = 1008.4913 if binned else 1006.4378 + r_value = 4.001768 if binned else 4.001752 + np.testing.assert_allclose(g.A.value, A_value) + np.testing.assert_allclose(g.r.value, r_value) + + if only_current: + A_value, r_value = 0, 0 + # Test that it all works when calling it with a different signal + s2 = hs.stack((s, s)) + g.estimate_parameters(s2, None, None, only_current=only_current) + assert g._axes_manager[-1].is_binned == binned + np.testing.assert_allclose(g.A.map["values"][1], A_value) + np.testing.assert_allclose(g.r.map["values"][1], r_value) + + def test_missing_data(self): + g = hs.model.components1D.PowerLaw() + s = self.m.as_signal() + s2 = hs.signals.Signal1D(s.data) + g.estimate_parameters(s2, None, None) + + def test_function_grad_cutoff(self): + pl = self.m[0] + pl.left_cutoff.value = 105.0 + axis = self.s.axes_manager[0].axis + for attr in ["function", "grad_A", "grad_r", "grad_origin"]: + values = getattr(pl, attr)((axis)) + np.testing.assert_allclose(values[:501], np.zeros((501))) + assert getattr(pl, attr)((axis))[500] == 0 + getattr(pl, attr)((axis))[502] > 0 + + def test_exception_gradient_calculation(self): + # if this doesn't warn, it means that sympy can compute the gradients + # and the power law component can be updated. + with pytest.warns(UserWarning): + hs.model.components1D.PowerLaw(compute_gradients=True) diff --git a/hyperspy/tests/component/test_rc.py b/hyperspy/tests/component/test_rc.py index 45d3a97ae1..6ead589670 100644 --- a/hyperspy/tests/component/test_rc.py +++ b/hyperspy/tests/component/test_rc.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np @@ -29,4 +29,4 @@ def test_function(): g.tau.value = 3 assert g.function(0) == 1 np.testing.assert_allclose(g.function(50), 3) - np.testing.assert_allclose(g.function(-3), 3-2*np.e) + np.testing.assert_allclose(g.function(-3), 3 - 2 * np.e) diff --git a/hyperspy/tests/component/test_scalablefixedpattern.py b/hyperspy/tests/component/test_scalablefixedpattern.py new file mode 100644 index 0000000000..812671ca9d --- /dev/null +++ b/hyperspy/tests/component/test_scalablefixedpattern.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import numpy as np +import pytest + +import hyperspy.api as hs + + +class TestScalableFixedPattern: + def setup_method(self, method): + s = hs.signals.Signal1D(np.linspace(0.0, 100.0, 10)) + s1 = hs.signals.Signal1D(np.linspace(0.0, 1.0, 10)) + s.axes_manager[0].scale = 0.1 + s1.axes_manager[0].scale = 0.1 + self.s = s + self.pattern = s1 + + def test_position(self): + s1 = self.pattern + fp = hs.model.components1D.ScalableFixedPattern(s1) + assert fp._position is fp.shift + + def test_both_unbinned(self): + s = self.s + s1 = self.pattern + s.axes_manager[-1].is_binned = False + s1.axes_manager[-1].is_binned = False + m = s.create_model() + fp = hs.model.components1D.ScalableFixedPattern(s1) + m.append(fp) + fp.xscale.free = False + fp.shift.free = False + m.fit() + np.testing.assert_allclose(fp.yscale.value, 100) + + @pytest.mark.parametrize(("uniform"), (True, False)) + def test_both_binned(self, uniform): + s = self.s + s1 = self.pattern + s.axes_manager[-1].is_binned = True + s1.axes_manager[-1].is_binned = True + if not uniform: + s.axes_manager[0].convert_to_non_uniform_axis() + s1.axes_manager[0].convert_to_non_uniform_axis() + m = s.create_model() + fp = hs.model.components1D.ScalableFixedPattern(s1) + m.append(fp) + fp.xscale.free = False + fp.shift.free = False + m.fit() + np.testing.assert_allclose(fp.yscale.value, 100) + + def test_pattern_unbinned_signal_binned(self): + s = self.s + s1 = self.pattern + s.axes_manager[-1].is_binned = True + s1.axes_manager[-1].is_binned = False + m = s.create_model() + fp = hs.model.components1D.ScalableFixedPattern(s1) + m.append(fp) + fp.xscale.free = False + fp.shift.free = False + m.fit() + np.testing.assert_allclose(fp.yscale.value, 1000) + + def test_pattern_binned_signal_unbinned(self): + s = self.s + s1 = self.pattern + s.axes_manager[-1].is_binned = False + s1.axes_manager[-1].is_binned = True + m = s.create_model() + fp = hs.model.components1D.ScalableFixedPattern(s1) + m.append(fp) + fp.xscale.free = False + fp.shift.free = False + m.fit() + np.testing.assert_allclose(fp.yscale.value, 10) + + def test_function(self): + s = self.s + s1 = self.pattern + fp = hs.model.components1D.ScalableFixedPattern(s1, interpolate=False) + m = s.create_model() + m.append(fp) + m.fit(grad="analytical") + x = s.axes_manager[0].axis + np.testing.assert_allclose(s.data, fp.function(x)) + np.testing.assert_allclose(fp.function(x), fp.function_nd(x)) + + def test_function_nd(self): + s = self.s + s1 = self.pattern + fp = hs.model.components1D.ScalableFixedPattern(s1) + s_multi = hs.stack([s] * 3) + m = s_multi.create_model() + m.append(fp) + fp.yscale.map["values"] = [1.0, 0.5, 1.0] + fp.xscale.map["values"] = [1.0, 1.0, 0.75] + results = fp.function_nd(s.axes_manager[0].axis) + expected = np.array([s1.data * v for v in [1, 0.5, 0.75]]) + np.testing.assert_allclose(results, expected) + + @pytest.mark.parametrize("interpolate", [True, False]) + def test_recreate_component(self, interpolate): + s = self.s + s1 = self.pattern + fp = hs.model.components1D.ScalableFixedPattern(s1, interpolate=interpolate) + assert fp.yscale._linear + assert not fp.xscale._linear + assert not fp.shift._linear + + m = s.create_model() + m.append(fp) + model_dict = m.as_dictionary() + + m2 = s.create_model() + m2._load_dictionary(model_dict) + assert m2[0].interpolate == interpolate + np.testing.assert_allclose(m2[0].signal.data, s1.data) + assert m2[0].yscale._linear + assert not m2[0].xscale._linear + assert not m2[0].shift._linear diff --git a/hyperspy/tests/component/test_skewnormal.py b/hyperspy/tests/component/test_skewnormal.py index 93cf4527a6..de9c3bebff 100644 --- a/hyperspy/tests/component/test_skewnormal.py +++ b/hyperspy/tests/component/test_skewnormal.py @@ -1,28 +1,25 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - +# along with HyperSpy. If not, see . import itertools -from distutils.version import LooseVersion import numpy as np import pytest -import sympy from hyperspy.components1d import SkewNormal from hyperspy.signals import Signal1D @@ -30,10 +27,6 @@ TRUE_FALSE_2_TUPLE = [p for p in itertools.product((True, False), repeat=2)] -pytestmark = pytest.mark.skipif(LooseVersion(sympy.__version__) < - LooseVersion("1.3"), - reason="This test requires SymPy >= 1.3") - def test_function(): g = SkewNormal() @@ -73,8 +66,9 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g1.A.value, g2.A.value * factor) assert abs(g2.x0.value - g1.x0.value) <= 0.002 diff --git a/hyperspy/tests/component/test_splitvoigt.py b/hyperspy/tests/component/test_splitvoigt.py index 90e2b76c8f..41f4df2829 100644 --- a/hyperspy/tests/component/test_splitvoigt.py +++ b/hyperspy/tests/component/test_splitvoigt.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2016 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import itertools @@ -26,7 +26,6 @@ from hyperspy.signals import Signal1D from hyperspy.utils import stack - TRUE_FALSE_2_TUPLE = [p for p in itertools.product((True, False), repeat=2)] @@ -77,8 +76,9 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g1.A.value, g2.A.value * factor, rtol=0.2) assert abs(g2.centre.value - g1.centre.value) <= 0.1 @@ -92,21 +92,20 @@ def test_function_nd(binned, lazy): s = Signal1D(np.empty((200,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] - axis.scale = .05 + axis.scale = 0.05 axis.offset = -5 A, sigma1, sigma2, fraction, centre = 5, 0.3, 0.75, 0.5, 1 - g1 = SplitVoigt(A=A, sigma1=sigma1, sigma2=sigma2, fraction=fraction, - centre=centre) + g1 = SplitVoigt(A=A, sigma1=sigma1, sigma2=sigma2, fraction=fraction, centre=centre) s.data = g1.function(axis.axis) s2 = stack([s] * 2) if lazy: s2 = s2.as_lazy() g2 = SplitVoigt() assert g2.estimate_parameters(s2, axis.low_value, axis.high_value, False) - g2.A.map['values'] = [A] * 2 - g2.sigma1.map['values'] = [sigma1] * 2 - g2.sigma2.map['values'] = [sigma2] * 2 - g2.fraction.map['values'] = [fraction] * 2 - g2.centre.map['values'] = [centre] * 2 + g2.A.map["values"] = [A] * 2 + g2.sigma1.map["values"] = [sigma1] * 2 + g2.sigma2.map["values"] = [sigma2] * 2 + g2.fraction.map["values"] = [fraction] * 2 + g2.centre.map["values"] = [centre] * 2 assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g2.function_nd(axis.axis), s2.data) diff --git a/hyperspy/tests/component/test_voigt.py b/hyperspy/tests/component/test_voigt.py index 5814d2889e..0fc2a82cc4 100644 --- a/hyperspy/tests/component/test_voigt.py +++ b/hyperspy/tests/component/test_voigt.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2016 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details.from hyperspy.utils import stack # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import itertools @@ -30,7 +30,7 @@ def test_function(): - g = Voigt(legacy=False) + g = Voigt() g.area.value = 5 g.sigma.value = 0.5 g.gamma.value = 0.2 @@ -41,18 +41,19 @@ def test_function(): @pytest.mark.parametrize(("lazy"), (True, False)) @pytest.mark.parametrize(("uniform"), (True, False)) +@pytest.mark.parametrize(("mapnone"), (True, False)) @pytest.mark.parametrize(("only_current", "binned"), TRUE_FALSE_2_TUPLE) -def test_estimate_parameters_binned(only_current, binned, lazy, uniform): +def test_estimate_parameters_binned(only_current, binned, lazy, uniform, mapnone): s = Signal1D(np.empty((200,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] - axis.scale = .05 + axis.scale = 0.05 axis.offset = -5 - g1 = Voigt(centre=1, area=5, gamma=0.001, sigma=0.5, legacy=False) + g1 = Voigt(centre=1, area=5, gamma=0.001, sigma=0.5) s.data = g1.function(axis.axis) if lazy: s = s.as_lazy() - g2 = Voigt(legacy=False) + g2 = Voigt() if not uniform: axis.convert_to_non_uniform_axis() if binned and uniform: @@ -61,8 +62,11 @@ def test_estimate_parameters_binned(only_current, binned, lazy, uniform): factor = np.gradient(axis.axis) else: factor = 1 - assert g2.estimate_parameters(s, axis.low_value, axis.high_value, - only_current=only_current) + if mapnone: + g2.area.map = None + assert g2.estimate_parameters( + s, axis.low_value, axis.high_value, only_current=only_current + ) assert g2._axes_manager[-1].is_binned == binned np.testing.assert_allclose(g2.sigma.value, 0.5, 0.01) np.testing.assert_allclose(g1.area.value, g2.area.value * factor, 0.01) @@ -75,14 +79,14 @@ def test_function_nd(binned, lazy): s = Signal1D(np.empty((200,))) s.axes_manager.signal_axes[0].is_binned = binned axis = s.axes_manager.signal_axes[0] - axis.scale = .05 + axis.scale = 0.05 axis.offset = -5 - g1 = Voigt(centre=1, area=5, gamma=0, sigma=0.5, legacy=False) + g1 = Voigt(centre=1, area=5, gamma=0, sigma=0.5) s.data = g1.function(axis.axis) s2 = stack([s] * 2) if lazy: s2 = s2.as_lazy() - g2 = Voigt(legacy=False) + g2 = Voigt() factor = axis.scale if binned else 1 g2.estimate_parameters(s2, axis.low_value, axis.high_value, False) assert g2._axes_manager[-1].is_binned == binned @@ -90,46 +94,54 @@ def test_function_nd(binned, lazy): def test_util_lwidth_set(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.lwidth = 3.0 np.testing.assert_allclose(g1.lwidth / 2, g1.gamma.value) + def test_util_lwidth_get(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.gamma.value = 3.0 np.testing.assert_allclose(g1.lwidth / 2, g1.gamma.value) + def test_util_lwidth_getset(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.lwidth = 3.0 np.testing.assert_allclose(g1.lwidth, 3.0) + def test_util_gwidth_set(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.gwidth = 1.0 np.testing.assert_allclose(g1.sigma.value, 1.0 / (2 * np.sqrt(2 * np.log(2)))) + def test_util_gwidth_get(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.sigma.value = 1.0 np.testing.assert_allclose(g1.gwidth, 1.0 * (2 * np.sqrt(2 * np.log(2)))) + def test_util_gwidth_getset(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.gwidth = 1.0 np.testing.assert_allclose(g1.gwidth, 1.0) + def test_util_FWHM_set(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.FWHM = 1.0 np.testing.assert_allclose(g1.sigma.value, 1.0 / (2 * np.sqrt(2 * np.log(2)))) + def test_util_FWHM_get(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.sigma.value = 1.0 np.testing.assert_allclose(g1.FWHM, 1.0 * (2 * np.sqrt(2 * np.log(2)))) + def test_util_FWHM_getset(): - g1 = Voigt(legacy=False) + g1 = Voigt() g1.FWHM = 1.0 np.testing.assert_allclose(g1.FWHM, 1.0) diff --git a/hyperspy/tests/component/test_volume_plasmon_drude.py b/hyperspy/tests/component/test_volume_plasmon_drude.py deleted file mode 100644 index d302a139b1..0000000000 --- a/hyperspy/tests/component/test_volume_plasmon_drude.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np - -from hyperspy.components1d import VolumePlasmonDrude - - -def test_function(): - g = VolumePlasmonDrude() - g.intensity.value = 3.0 - g.plasmon_energy.value = 8.0 - g.fwhm.value = 2.0 - assert g.function(0) == 0 - np.testing.assert_allclose(g.function(8), 12) - np.testing.assert_allclose(g.function(1), 9.66524e-2, rtol=1e-6) - np.testing.assert_allclose(g.function(30), 1.639867e-2, rtol=1e-6) diff --git a/hyperspy/tests/datasets/__init__.py b/hyperspy/tests/datasets/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hyperspy/tests/datasets/test_artificial_data.py b/hyperspy/tests/datasets/test_artificial_data.py deleted file mode 100644 index 6b40462a26..0000000000 --- a/hyperspy/tests/datasets/test_artificial_data.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import numpy as np -import pytest - -import hyperspy.datasets.artificial_data as ad - - -@pytest.mark.parametrize("add_noise", (True, False)) -def test_get_low_loss_eels_signal(add_noise): - s = ad.get_low_loss_eels_signal(add_noise=add_noise) - assert s.metadata.Signal.signal_type == 'EELS' - - -def test_get_core_loss_eels_signal(): - s = ad.get_core_loss_eels_signal(add_powerlaw=False) - assert s.metadata.Signal.signal_type == 'EELS' - s1 = ad.get_core_loss_eels_signal(add_powerlaw=True) - assert s1.metadata.Signal.signal_type == 'EELS' - assert s1.data.sum() > s.data.sum() - - np.random.seed(seed=10) - s2 = ad.get_core_loss_eels_signal(add_noise=True) - np.random.seed(seed=10) - s3 = ad.get_core_loss_eels_signal(add_noise=True) - assert (s2.data == s3.data).all() - -@pytest.mark.parametrize("add_noise", (True, False)) -def test_get_core_loss_eels_model(add_noise): - m = ad.get_core_loss_eels_model(add_powerlaw=False, add_noise=add_noise) - assert m.signal.metadata.Signal.signal_type == 'EELS' - m1 = ad.get_core_loss_eels_model(add_powerlaw=True, add_noise=add_noise) - assert m1.signal.metadata.Signal.signal_type == 'EELS' - assert m1.signal.data.sum() > m.signal.data.sum() - - -@pytest.mark.parametrize("add_noise", (True, False)) -def test_get_low_loss_eels_line_scan_signal(add_noise): - s = ad.get_low_loss_eels_line_scan_signal(add_noise=add_noise) - assert s.metadata.Signal.signal_type == 'EELS' - - -@pytest.mark.parametrize("add_powerlaw", (True, False)) -@pytest.mark.parametrize("add_noise", (True, False)) -def test_get_core_loss_eels_line_scan_signal(add_powerlaw, add_noise): - s = ad.get_core_loss_eels_line_scan_signal(add_powerlaw, add_noise) - assert s.metadata.Signal.signal_type == 'EELS' - - -def test_get_atomic_resolution_tem_signal2d(): - s = ad.get_atomic_resolution_tem_signal2d() - assert s.axes_manager.signal_dimension == 2 - -@pytest.mark.parametrize("navigation_dimension",(0,1,2,3)) -@pytest.mark.parametrize("uniform",(True,False)) -@pytest.mark.parametrize("add_baseline",(True,False)) -@pytest.mark.parametrize("add_noise",(True,False)) -def test_get_luminescence_signal(navigation_dimension, uniform, add_baseline, add_noise): - #Creating signal - s = ad.get_luminescence_signal(navigation_dimension, - uniform, - add_baseline, - add_noise) - #Checking that dimension initialisation works - assert tuple([10 for i in range(navigation_dimension)]+[1024]) == s.data.shape - #Verifying that both functional and uniform data axis work - sax = s.axes_manager.signal_axes[0] - assert sax.is_uniform == uniform - #Verifying that baseline works - if add_baseline: - assert s.data.min()>340 - #Verification that noise works - #Case of baseline + energy axis is discarded because of - #jacobian transformation - if not(add_baseline and not uniform): - #Verify that adding noise works - noisedat = s.isig[:100].data - assert (noisedat.std()>0.1)==add_noise diff --git a/hyperspy/tests/datasets/test_eelsdb.py b/hyperspy/tests/datasets/test_eelsdb.py deleted file mode 100644 index 34fb8d0826..0000000000 --- a/hyperspy/tests/datasets/test_eelsdb.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import warnings - -import pytest -import requests -from requests.exceptions import SSLError - -from hyperspy.misc.eels.eelsdb import eelsdb - - -def eelsdb_down(): - try: - _ = requests.get('http://api.eelsdb.eu', verify=True) - return False - except SSLError: - _ = requests.get('http://api.eelsdb.eu', verify=False) - return False - except requests.exceptions.ConnectionError: - return True - - -@pytest.mark.skipif(eelsdb_down(), reason="Unable to connect to EELSdb") -def test_eelsdb_eels(): - try: - ss = eelsdb( - title="Boron Nitride Multiwall Nanotube", - formula="BN", - spectrum_type="coreloss", - edge="K", - min_energy=370, - max_energy=1000, - min_energy_compare="gt", - max_energy_compare="lt", - resolution="0.7 eV", - resolution_compare="lt", - max_n=2, - order="spectrumMin", - order_direction='DESC', - monochromated=False, ) - except SSLError: - warnings.warn( - "The https://eelsdb.eu certificate seems to be invalid. " - "Consider notifying the issue to the EELSdb webmaster.") - ss = eelsdb( - title="Boron Nitride Multiwall Nanotube", - formula="BN", - spectrum_type="coreloss", - edge="K", - min_energy=370, - max_energy=1000, - min_energy_compare="gt", - max_energy_compare="lt", - resolution="0.7 eV", - resolution_compare="lt", - max_n=2, - order="spectrumMin", - order_direction='DESC', - monochromated=False, - verify_certificate=False) - except Exception as e: - # e.g. failures such as ConnectionError or MaxRetryError - pytest.skip(f"Skipping eelsdb test due to {e}") - - assert len(ss) == 2 - md = ss[0].metadata - assert md.General.author == "Odile Stephan" - assert ( - md.Acquisition_instrument.TEM.Detector.EELS.collection_angle == 24) - assert md.Acquisition_instrument.TEM.convergence_angle == 15 - assert md.Acquisition_instrument.TEM.beam_energy == 100 - assert md.Signal.signal_type == "EELS" - assert "perpendicular" in md.Sample.description - assert "parallel" in ss[1].metadata.Sample.description - assert md.Sample.chemical_formula == "BN" - assert md.Acquisition_instrument.TEM.microscope == "STEM-VG" - - -@pytest.mark.skipif(eelsdb_down(), reason="Unable to connect to EELSdb") -def test_eelsdb_xas(): - try: - ss = eelsdb( - spectrum_type="xrayabs", max_n=1,) - except SSLError: - ss = eelsdb( - spectrum_type="xrayabs", max_n=1, verify_certificate=False) - except Exception as e: - # e.g. failures such as ConnectionError or MaxRetryError - pytest.skip(f"Skipping eelsdb test due to {e}") - - assert len(ss) == 1 - md = ss[0].metadata - assert md.Signal.signal_type == "XAS" diff --git a/hyperspy/tests/doc_docstr_examples/doc_examples_EDS.ipynb b/hyperspy/tests/doc_docstr_examples/doc_examples_EDS.ipynb deleted file mode 100644 index efd6cf0e6b..0000000000 --- a/hyperspy/tests/doc_docstr_examples/doc_examples_EDS.ipynb +++ /dev/null @@ -1,1414 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# EDS doc" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Examples in the EDS chapter" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "%matplotlib qt4\n", - "import numpy as np\n", - "import hyperspy.api as hs\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Signal1D loading and parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "('Ni_superalloy_011.raw', )" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from urllib.request import urlretrieve\n", - "url = 'http://cook.msm.cam.ac.uk//~hyperspy//EDS_tutorial//'\n", - "urlretrieve(url + 'Ni_superalloy_1pix.msa', 'Ni_superalloy_1pix.msa')\n", - "urlretrieve(url + 'Ni_superalloy_010.rpl', 'Ni_superalloy_010.rpl')\n", - "urlretrieve(url + 'Ni_superalloy_010.raw', 'Ni_superalloy_010.raw')\n", - "urlretrieve(url + 'Ni_superalloy_011.rpl', 'Ni_superalloy_011.rpl')\n", - "urlretrieve(url + 'Ni_superalloy_011.raw', 'Ni_superalloy_011.raw')" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.load(\"Ni_superalloy_1pix.msa\")\n", - "s" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading as Signal2D\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:83: VisibleDeprecationWarning: The Signal2D class will be deprecated from version 1.0.0 and replaced with Signal2D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signal.py:3413: VisibleDeprecationWarning: The as_signal1D method will be deprecated from version 1.0.0 and replaced with as_signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "si = hs.load(\"Ni_superalloy_010.rpl\").as_signal1D(0)\n", - "si" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:83: VisibleDeprecationWarning: The Signal2D class will be deprecated from version 1.0.0 and replaced with Signal2D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Loading individual files\n", - "Loading as Signal2D\n", - "Loading as Signal2D\n", - "\n", - "Individual files loaded correctly\n", - "\n", - "\tTitle: doc_docstr_examples\n", - "\tSignal type: \n", - "\tData dimensions: (1024, 2, 256, 224)\n", - "\tData representation: image\n", - "\tData type: uint8\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signal.py:3413: VisibleDeprecationWarning: The as_signal1D method will be deprecated from version 1.0.0 and replaced with as_signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "si4D = hs.load(\"Ni_superalloy_0*.rpl\", stack=True)\n", - "si4D = si4D.as_signal1D(0)\n", - "si4D" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s.set_signal_type(\"EDS_SEM\")\n", - "s" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.load(\"Ni_superalloy_1pix.msa\", signal_type=\"EDS_SEM\")\n", - "s" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "├── Detector\n", - "│ └── EDS\n", - "│ ├── azimuth_angle = 63.0\n", - "│ ├── elevation_angle = 35.0\n", - "│ ├── energy_resolution_MnKa = 130.0\n", - "│ ├── live_time = 0.006855\n", - "│ └── real_time = 0.0\n", - "├── beam_current = 0.0\n", - "├── beam_energy = 15.0\n", - "└── tilt_stage = 38.0" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.load(\"Ni_superalloy_1pix.msa\", signal_type=\"EDS_SEM\")\n", - "s.metadata.Acquisition_instrument.SEM" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.load(\"Ni_superalloy_1pix.msa\", signal_type=\"EDS_SEM\")\n", - "s.metadata.Acquisition_instrument.SEM.beam_energy = 30" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.load(\"Ni_superalloy_1pix.msa\", signal_type=\"EDS_SEM\")\n", - "s.set_microscope_parameters(beam_energy = 30)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.load(\"Ni_superalloy_1pix.msa\", signal_type=\"EDS_SEM\")\n", - "s.set_microscope_parameters()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading as Signal2D\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:83: VisibleDeprecationWarning: The Signal2D class will be deprecated from version 1.0.0 and replaced with Signal2D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signal.py:3413: VisibleDeprecationWarning: The as_signal1D method will be deprecated from version 1.0.0 and replaced with as_signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "si = hs.load(\"Ni_superalloy_010.rpl\").as_signal1D(0)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:83: VisibleDeprecationWarning: The Signal2D class will be deprecated from version 1.0.0 and replaced with Signal2D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signal.py:3413: VisibleDeprecationWarning: The as_signal1D method will be deprecated from version 1.0.0 and replaced with as_signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading as Signal2D\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "si = hs.load(\"Ni_superalloy_010.rpl\", signal_type=\"EDS_SEM\").as_signal1D(0)\n", - "si.axes_manager[-1].name = 'E'\n", - "si.axes_manager['E'].units = 'keV'\n", - "si.axes_manager['E'].scale = 0.01\n", - "si.axes_manager['E'].offset = -0.1" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "si" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:83: VisibleDeprecationWarning: The Signal2D class will be deprecated from version 1.0.0 and replaced with Signal2D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signal.py:3413: VisibleDeprecationWarning: The as_signal1D method will be deprecated from version 1.0.0 and replaced with as_signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading as Signal2D\n" - ] - } - ], - "source": [ - "# Load spectrum.msa which contains the parameters\n", - "s1pixel = hs.load(\"Ni_superalloy_1pix.msa\", signal_type=\"EDS_TEM\")\n", - "# Load spectrum_image.rpl which contains no parameters\n", - "si = hs.load(\"Ni_superalloy_010.rpl\", signal_type=\"EDS_TEM\").as_signal1D(0)\n", - "# Set all the properties of s1pixel to si\n", - "si.get_calibration_from(s1pixel)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Describing the sample" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "├── description = 'FePt bimetallic nanoparticles'\n", - "├── elements = ['Fe', 'Pt']\n", - "├── thickness = 100\n", - "└── xray_lines = ['Fe_Ka', 'Pt_La']" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.add_lines()\n", - "s.metadata.Sample.thickness = 100\n", - "s.metadata.Sample" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "├── description = 'FePt bimetallic nanoparticles'\n", - "└── elements = ['Cu', 'Fe', 'Pt']" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.set_elements(['Fe', 'Pt'])\n", - "s.add_elements(['Cu'])\n", - "s.metadata.Sample" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n", - "Fe_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "├── description = 'FePt bimetallic nanoparticles'\n", - "├── elements = ['Fe', 'Pt']\n", - "└── xray_lines = ['Fe_Ka', 'Fe_La', 'Pt_La']" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.set_elements(['Fe', 'Pt'])\n", - "s.set_lines(['Fe_Ka', 'Pt_La'])\n", - "s.add_lines(['Fe_La'])\n", - "s.metadata.Sample" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "├── description = 'EDS-TM002 from BAM (www.webshop.bam.de)'\n", - "└── elements = array(['Al', 'C', 'Cu', 'Mn', 'Zr'], \n", - " dtype='|S2')" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.metadata.Sample" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Al_Ka line added,\n", - "Cu_Ka line added,\n", - "Mn_Ka line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "├── description = 'EDS-TM002 from BAM (www.webshop.bam.de)'\n", - "├── elements = ['Al', 'Cu', 'Mn']\n", - "└── xray_lines = ['Al_Ka', 'Cu_Ka', 'Mn_Ka']" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.set_elements(['Al', 'Cu', 'Mn'])\n", - "s.set_microscope_parameters(beam_energy=30)\n", - "s.add_lines()\n", - "s.metadata.Sample" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Al_Ka line added,\n", - "Cu_La line added,\n", - "Mn_La line added,\n" - ] - }, - { - "data": { - "text/plain": [ - "├── description = 'EDS-TM002 from BAM (www.webshop.bam.de)'\n", - "├── elements = ['Al', 'Cu', 'Mn']\n", - "└── xray_lines = ['Al_Ka', 'Cu_La', 'Mn_La']" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s.set_microscope_parameters(beam_energy=10)\n", - "s.set_lines([])\n", - "s.metadata.Sample" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mn_Ka line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/_signals/eds.py:466: UserWarning: Mn_Ka is not in the data energy range.\n", - " warnings.warn(\"%s is not in the data energy range.\" % xray)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.set_elements(['Mn'])\n", - "s.set_microscope_parameters(beam_energy=5)\n", - "s.add_lines([\"Mn_Ka\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mn_Ka line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/_signals/eds.py:466: UserWarning: Mn_Ka is not in the data energy range.\n", - " warnings.warn(\"%s is not in the data energy range.\" % xray)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.set_elements(['Mn'])\n", - "s.set_microscope_parameters(beam_energy=5)\n", - "s.add_lines(['Mn_Ka'])" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Warning: Mn_Ka is not in the data energy range.\n" - ] - } - ], - "source": [ - "s.plot(True)" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "├── Z = 26\n", - "├── atomic_weight = 55.845\n", - "└── name = iron" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "hs.material.elements.Fe.General_properties" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "└── density (g/cm^3) = 7.874" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "hs.material.elements.Fe.Physical_properties" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "├── Ka\n", - "│ ├── energy (keV) = 6.4039\n", - "│ └── weight = 1.0\n", - "├── Kb\n", - "│ ├── energy (keV) = 7.058\n", - "│ └── weight = 0.1272\n", - "├── La\n", - "│ ├── energy (keV) = 0.7045\n", - "│ └── weight = 1.0\n", - "├── Lb3\n", - "│ ├── energy (keV) = 0.7921\n", - "│ └── weight = 0.02448\n", - "├── Ll\n", - "│ ├── energy (keV) = 0.6152\n", - "│ └── weight = 0.3086\n", - "└── Ln\n", - " ├── energy (keV) = 0.6282\n", - " └── weight = 0.12525" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "hs.material.elements.Fe.Atomic_properties.Xray_lines" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.add_elements(['C','Mn','Cu','Al','Zr'])\n", - "s.plot(True)" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.add_elements(['C','Mn','Cu','Al','Zr'])\n", - "s.plot(True, only_lines=['Ka','b'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Get lines intensity" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "('core_shell.hdf5', )" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from urllib.request import urlretrieve\n", - "url = 'http://cook.msm.cam.ac.uk//~hyperspy//EDS_tutorial//'\n", - "urlretrieve(url + 'core_shell.hdf5', 'core_shell.hdf5')" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:126: VisibleDeprecationWarning: The Signal class will be deprecated from version 1.0.0 and replaced with BaseSignal\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.load('core_shell.hdf5')\n", - "s.get_lines_intensity(['Fe_Ka'],plot_result=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:126: VisibleDeprecationWarning: The Signal class will be deprecated from version 1.0.0 and replaced with BaseSignal\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "s = hs.load('core_shell.hdf5')\n", - "s.set_lines(['Fe_Ka', 'Pt_La'])\n", - "s.get_lines_intensity()" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signal.py:1331: VisibleDeprecationWarning: Indexing the `Signal` class is deprecated and will be removed in Hyperspy 1.0. Please use `.isig` and/or `.inav` instead.\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()[5.:13.]\n", - "s.add_lines()\n", - "s.plot(integration_windows='auto')" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signal.py:1331: VisibleDeprecationWarning: Indexing the `Signal` class is deprecated and will be removed in Hyperspy 1.0. Please use `.isig` and/or `.inav` instead.\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - }, - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#estimate_background_windows()\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()[5.:13.]\n", - "s.change_dtype('float')\n", - "s.add_lines()\n", - "bw = s.estimate_background_windows(line_width=[5.0, 2.0])\n", - "s.plot(background_windows=bw)\n", - "s.get_lines_intensity(background_windows=bw, plot_result=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Quant" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.change_dtype('float')\n", - "s.add_lines()\n", - "kfactors = [1.450226, 5.075602] #For Fe Ka and Pt La\n", - "bw = s.estimate_background_windows(line_width=[5.0, 2.0])\n", - "intensities = s.get_lines_intensity(background_windows=bw)" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "intensities" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - ")>" - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "intensities[0].axes_manager" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "intensities = hs.stack(intensities)" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "intensities.split(axis=0)" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "weight_percent = s.quantification(intensities, kfactors, plot_result=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "hs.material.weight_to_atomic(weight_percent)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cleanup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "%%bash\n", - "\n", - "rm Ni_superalloy_1pix.msa Ni_superalloy_010.rpl Ni_superalloy_010.raw \n", - "rm Ni_superalloy_011.rpl Ni_superalloy_011.raw \n", - "rm core_shell.hdf5" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Conda Python3", - "language": "python", - "name": "conda_py35" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3.0 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.1" - }, - "widgets": { - "state": {}, - "version": "1.1.1" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/hyperspy/tests/doc_docstr_examples/doc_examples_Visualisation.ipynb b/hyperspy/tests/doc_docstr_examples/doc_examples_Visualisation.ipynb index 4222e1a632..9ffd9053f4 100644 --- a/hyperspy/tests/doc_docstr_examples/doc_examples_Visualisation.ipynb +++ b/hyperspy/tests/doc_docstr_examples/doc_examples_Visualisation.ipynb @@ -356,8 +356,8 @@ } ], "source": [ - "import scipy.misc\n", - "s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10])\n", + "import scipy\n", + "s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10])\n", "cascade_plot = hs.plot.plot_spectra(s, style='cascade')\n", "cascade_plot.figure.savefig(\"cascade_plot.png\")" ] @@ -389,12 +389,12 @@ } ], "source": [ - "import scipy.misc\n", - "s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10])\n", + "import scipy\n", + "s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10])\n", "color_list = ['red', 'red', 'blue', 'blue', 'red', 'red']\n", - "line_style_list = ['-','--','steps','-.',':','-']\n", + "linestyle_list = ['-', '--', '-.', ':', '-']\n", "hs.plot.plot_spectra(s, style='cascade', color=color_list,\n", - "line_style=line_style_list,legend='auto')" + "linestyle=linestyle_list, legend='auto')" ] }, { @@ -424,8 +424,8 @@ } ], "source": [ - "import scipy.misc\n", - "s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10])\n", + "import scipy\n", + "s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10])\n", "hs.plot.plot_spectra(s, style='heatmap')" ] }, @@ -457,8 +457,8 @@ } ], "source": [ - "import scipy.misc\n", - "s = hs.signals.Signal1D(scipy.misc.ascent()[100:120:10])\n", + "import scipy\n", + "s = hs.signals.Signal1D(scipy.datasets.ascent()[100:120:10])\n", "hs.plot.plot_spectra(s, style='mosaic')" ] }, @@ -480,8 +480,8 @@ ], "source": [ "import matplotlib.cm\n", - "import scipy.misc\n", - "s = hs.signals.Signal1D(scipy.misc.ascent()[100:120:10])\n", + "import scipy\n", + "s = hs.signals.Signal1D(scipy.datasets.ascent()[100:120:10])\n", "ax = hs.plot.plot_spectra(s, style=\"heatmap\")\n", "ax.images[0].set_cmap(matplotlib.cm.jet)" ] @@ -505,8 +505,8 @@ } ], "source": [ - "import scipy.misc\n", - "s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10])\n", + "import scipy\n", + "s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10])\n", "legendtext = ['Plot 0', 'Plot 1', 'Plot 2', 'Plot 3', 'Plot 4', 'Plot 5']\n", "cascade_plot = hs.plot.plot_spectra(\n", "s, style='cascade', legend=legendtext, dpi=60,\n", @@ -536,8 +536,8 @@ } ], "source": [ - "import scipy.misc\n", - "s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10])\n", + "import scipy\n", + "s = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10])\n", "cascade_plot = hs.plot.plot_spectra(s)\n", "cascade_plot.set_xlabel(\"An axis\")\n", "cascade_plot.set_ylabel(\"Another axis\")\n", @@ -562,10 +562,10 @@ } ], "source": [ - "import scipy.misc\n", + "import scipy\n", "fig, axarr = plt.subplots(1,2)\n", - "s1 = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10])\n", - "s2 = hs.signals.Signal1D(scipy.misc.ascent()[200:260:10])\n", + "s1 = hs.signals.Signal1D(scipy.datasets.ascent()[100:160:10])\n", + "s2 = hs.signals.Signal1D(scipy.datasets.ascent()[200:260:10])\n", "hs.plot.plot_spectra(s1, style='cascade',color='blue',ax=axarr[0],fig=fig)\n", "hs.plot.plot_spectra(s2, style='cascade',color='red',ax=axarr[1],fig=fig)\n", "fig.canvas.draw()" @@ -605,8 +605,8 @@ } ], "source": [ - "import scipy.misc\n", - "s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0)[:,:3]\n", + "import scipy\n", + "s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0)[:,:3]\n", "s2 = s1.deepcopy()*-1\n", "hs.plot.plot_signals([s1, s2])" ] @@ -632,8 +632,8 @@ } ], "source": [ - "import scipy.misc\n", - "s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0)[:,:3]\n", + "import scipy\n", + "s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0)[:,:3]\n", "s2 = s1.deepcopy()*-1\n", "hs.plot.plot_signals([s1, s2], navigator=\"slider\")" ] @@ -659,8 +659,8 @@ } ], "source": [ - "import scipy.misc\n", - "s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0)[:,:3]\n", + "import scipy\n", + "s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0)[:,:3]\n", "s2 = s1.deepcopy()*-1\n", "s3 = hs.signals.Signal1D(np.linspace(0,9,9).reshape([3,3]))\n", "hs.plot.plot_signals([s1, s2], navigator_list=[\"slider\", s3])" @@ -687,8 +687,8 @@ } ], "source": [ - "import scipy.misc\n", - "s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0)[:,:3]\n", + "import scipy\n", + "s1 = hs.signals.Signal1D(scipy.datasets.face()).as_signal1D(0)[:,:3]\n", "s2 = s1.deepcopy()*-1\n", "hs.plot.plot_signals([s1, s2], sync=False, navigator_list=[\"slider\", \"slider\"])" ] @@ -786,9 +786,8 @@ ], "source": [ "import scipy\n", - "image = hs.signals.Signal2D([scipy.misc.ascent()]*6)\n", - "angles = hs.signals.Signal(range(10,70,10))\n", - "angles.axes_manager.set_signal_dimension(0)\n", + "image = hs.signals.Signal2D([scipy.datasets.ascent()]*6)\n", + "angles = hs.signals.BaseSignal(range(10, 70, 10)).T\n", "image.map(scipy.ndimage.rotate, angle=angles, reshape=False)\n", "hs.plot.plot_images(image, tight_layout=True)" ] @@ -841,9 +840,8 @@ ], "source": [ "import scipy\n", - "image = hs.signals.Signal2D([scipy.misc.ascent()]*6)\n", - "angles = hs.signals.Signal(range(10,70,10))\n", - "angles.axes_manager.set_signal_dimension(0)\n", + "image = hs.signals.Signal2D([scipy.datasets.ascent()]*6)\n", + "angles = hs.signals.BaseSignal(range(10, 70, 10)).T\n", "image.map(scipy.ndimage.rotate, angle=angles, reshape=False)\n", "hs.plot.plot_images(\n", "image, suptitle='Turning Ascent', axes_decor='off',\n", @@ -915,24 +913,23 @@ "\n", "\n", "# load red channel of raccoon as an image\n", - "image0 = hs.signals.Signal2D(scipy.misc.face()[:,:,0])\n", + "image0 = hs.signals.Signal2D(scipy.datasets.face()[:,:,0])\n", "image0.metadata.General.title = 'Rocky Raccoon - R'\n", "\n", "\n", "# load ascent into 6 hyperimage\n", - "image1 = hs.signals.Signal2D([scipy.misc.ascent()]*6)\n", - "angles = hs.signals.Signal(range(10,70,10))\n", - "angles.axes_manager.set_signal_dimension(0)\n", + "image1 = hs.signals.Signal2D([scipy.datasets.ascent()]*6)\n", + "angles = hs.signals.BaseSignal(range(10, 70, 10)).T\n", "image1.map(scipy.ndimage.rotate, angle=angles, reshape=False)\n", "\n", "\n", "# load green channel of raccoon as an image\n", - "image2 = hs.signals.Signal2D(scipy.misc.face()[:,:,1])\n", + "image2 = hs.signals.Signal2D(scipy.datasets.face()[:,:,1])\n", "image2.metadata.General.title = 'Rocky Raccoon - G'\n", "\n", "\n", "# load rgb image of the raccoon\n", - "rgb = hs.signals.Signal1D(scipy.misc.face())\n", + "rgb = hs.signals.Signal1D(scipy.datasets.face())\n", "rgb.change_dtype(\"rgb8\")\n", "rgb.metadata.General.title = 'Raccoon - RGB'\n", " \n", @@ -1006,7 +1003,7 @@ "im = si_EDS.get_lines_intensity()\n", "hs.plot.plot_images(\n", "im, tight_layout=True, cmap='RdYlBu_r', axes_decor='off',\n", - "colorbar='single', saturated_pixels=0.0, scalebar='all',\n", + "colorbar='single', scalebar='all',\n", "scalebar_color='black', suptitle_fontsize=16,\n", "padding={'top':0.8, 'bottom':0.10, 'left':0.05,\n", "'right':0.85, 'wspace':0.20, 'hspace':0.10})" diff --git a/hyperspy/tests/doc_docstr_examples/docstr_examples_EDS.ipynb b/hyperspy/tests/doc_docstr_examples/docstr_examples_EDS.ipynb deleted file mode 100644 index 52947f8840..0000000000 --- a/hyperspy/tests/doc_docstr_examples/docstr_examples_EDS.ipynb +++ /dev/null @@ -1,1115 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "%matplotlib qt4\n", - "import numpy as np\n", - "import hyperspy.api as hs\n", - "import matplotlib.pyplot as plt\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Doc-string examples for EDS" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Loading signal examples" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.plot(True)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.plot(True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## eds" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "array([1000279])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#Sum()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.sum(0).data" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#rebin()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "print(s)\n", - "print(s.rebin([512]))" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Al' 'C' 'Cu' 'Mn' 'Zr']\n", - "['Al']\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#set_elements()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "print(s.metadata.Sample.elements)\n", - "s.set_elements(['Al'])\n", - "print(s.metadata.Sample.elements)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Al' 'C' 'Cu' 'Mn' 'Zr']\n", - "['Al', 'Ar', 'C', 'Cu', 'Mn', 'Zr']\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#add_elements()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "print(s.metadata.Sample.elements)\n", - "s.add_elements(['Ar'])\n", - "print(s.metadata.Sample.elements)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Al_Ka line added,\n", - "C_Ka line added,\n", - "Cu_La line added,\n", - "Mn_La line added,\n", - "Zr_La line added,\n", - "['Al_Ka', 'C_Ka', 'Cu_La', 'Mn_La', 'Zr_La']\n", - "Cu_Ka line added,\n", - "Al_Ka line added,\n", - "C_Ka line added,\n", - "Mn_La line added,\n", - "Zr_La line added,\n", - "Cu_Ka line added,\n", - "['Al_Ka', 'C_Ka', 'Cu_Ka', 'Mn_La', 'Zr_La']\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#set_lines()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.add_lines()\n", - "print(s.metadata.Sample.xray_lines)\n", - "s.set_lines(['Cu_Ka'])\n", - "print(s.metadata.Sample.xray_lines)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Al_Ka line added,\n", - "C_Ka line added,\n", - "Cu_La line added,\n", - "Mn_La line added,\n", - "Zr_La line added,\n", - "['Al_Ka', 'C_Ka', 'Cu_La', 'Mn_La', 'Zr_La']\n", - "Al_Ka line added,\n", - "C_Ka line added,\n", - "Cu_Ka line added,\n", - "Mn_Ka line added,\n", - "Zr_La line added,\n", - "['Al_Ka', 'C_Ka', 'Cu_Ka', 'Mn_Ka', 'Zr_La']\n", - "Al_Ka line added,\n", - "C_Ka line added,\n", - "Cu_La line added,\n", - "Mn_La line added,\n", - "Zr_La line added,\n", - "['Al_Ka', 'C_Ka', 'Cu_La', 'Mn_La', 'Zr_La']\n", - "Cu_Ka line added,\n", - "['Al_Ka', 'C_Ka', 'Cu_Ka', 'Cu_La', 'Mn_La', 'Zr_La']\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#add_lines()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.add_lines()\n", - "print(s.metadata.Sample.xray_lines)\n", - " \n", - "\n", - "\n", - "\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.set_microscope_parameters(beam_energy=30)\n", - "s.add_lines()\n", - "print(s.metadata.Sample.xray_lines)\n", - " \n", - "\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.add_lines()\n", - "print(s.metadata.Sample.xray_lines)\n", - "s.add_lines(['Cu_Ka'])\n", - "print(s.metadata.Sample.xray_lines)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mn_Ka at 5.8987 keV : Intensity = 52773.00\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#get_lines_intensity()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.get_lines_intensity(['Mn_Ka'], plot_result=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mn_Ka at 5.8987 keV : Intensity = 53597.00\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - }, - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#get_lines_intensity()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.plot(['Mn_Ka'], integration_windows=2.1)\n", - "s.get_lines_intensity(['Mn_Ka'],\n", - "integration_windows=2.1, plot_result=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mn_Ka line added,\n", - "Mn_Ka at 5.8987 keV : Intensity = 46716.00\n" - ] - }, - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#get_lines_intensity()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.change_dtype('float')\n", - "s.set_elements(['Mn'])\n", - "s.set_lines(['Mn_Ka'])\n", - "bw = s.estimate_background_windows()\n", - "s.plot(background_windows=bw)\n", - "s.get_lines_intensity(background_windows=bw, plot_result=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n", - "Fe_Ka at 6.4039 keV : Intensity = 3710.00\n", - "Pt_La at 9.4421 keV : Intensity = 15872.00\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - }, - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#estimate_integration_windows()\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.add_lines()\n", - "iw = s.estimate_integration_windows()\n", - "s.plot(integration_windows=iw)\n", - "s.get_lines_intensity(integration_windows=iw, plot_result=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka at 6.4039 keV : Intensity = 2754.00\n", - "Pt_La at 9.4421 keV : Intensity = 15090.00\n" - ] - }, - { - "data": { - "text/plain": [ - "[,\n", - " ]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#estimate_background_windows()\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.change_dtype('float')\n", - "s.add_lines()\n", - "bw = s.estimate_background_windows(line_width=[5.0, 2.0])\n", - "s.plot(background_windows=bw)\n", - "s.get_lines_intensity(background_windows=bw, plot_result=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "37.0" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#get_take_off()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.get_take_off_angle()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "57.00000000000001" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#get_take_off()\n", - "s.set_microscope_parameters(tilt_stage=20.)\n", - "s.get_take_off_angle()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - } - ], - "source": [ - "#plot()\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.plot()\n", - "\n", - "\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.plot(True)\n", - "\n", - "\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.add_lines()\n", - "bw = s.estimate_background_windows()\n", - "s.plot(background_windows=bw)\n", - " \n", - "\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s.plot(['Mn_Ka'], integration_windows='auto')\n", - "\n", - "\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.add_lines()\n", - "bw = s.estimate_background_windows()\n", - "s.plot(background_windows=bw, integration_windows=2.1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## eds_sem" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.0\n", - "0.01\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#get_calibration_from\n", - "ref = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "s = hs.signals.EDSSEMSpectrum(\n", - "hs.datasets.example_signals.EDS_SEM_Spectrum().data)\n", - "print(s.axes_manager[0].scale)\n", - "s.get_calibration_from(ref)\n", - "print(s.axes_manager[0].scale)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Default value 130.0 eV\n", - "Now set to 135.0 eV\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#set_micro\n", - "s = hs.datasets.example_signals.EDS_SEM_Spectrum()\n", - "print('Default value %s eV' %\n", - "s.metadata.Acquisition_instrument.\n", - "SEM.Detector.EDS.energy_resolution_MnKa)\n", - "s.set_microscope_parameters(energy_resolution_MnKa=135.)\n", - "print('Now set to %s eV' %\n", - "s.metadata.Acquisition_instrument.\n", - "SEM.Detector.EDS.energy_resolution_MnKa)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "133.312296\n", - "135.0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#set_micro\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "print(s.metadata.Acquisition_instrument.\n", - "TEM.Detector.EDS.energy_resolution_MnKa)\n", - "s.set_microscope_parameters(energy_resolution_MnKa=135.)\n", - "print(s.metadata.Acquisition_instrument.\n", - "TEM.Detector.EDS.energy_resolution_MnKa)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## eds_tem" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.0\n", - "0.020028\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "#get_calibration_from\n", - "ref = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s = hs.signals.EDSTEMSpectrum(\n", - "hs.datasets.example_signals.EDS_TEM_Spectrum().data)\n", - "print(s.axes_manager[0].scale)\n", - "s.get_calibration_from(ref)\n", - "print(s.axes_manager[0].scale)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe_Ka line added,\n", - "Pt_La line added,\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/anaconda3/lib/python3.5/site-packages/matplotlib/__init__.py:892: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.\n", - " warnings.warn(self.msg_depr % (key, alt_key))\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fe (Fe_Ka): Composition = 15.41 atomic percent\n", - "Pt (Pt_La): Composition = 84.59 atomic percent\n" - ] - } - ], - "source": [ - "#quant\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s.change_dtype('float')\n", - "s.add_lines()\n", - "kfactors = [1.450226, 5.075602] #For Fe Ka and Pt La\n", - "bw = s.estimate_background_windows(line_width=[5.0, 2.0])\n", - "s.plot(background_windows=bw)\n", - "intensities = s.get_lines_intensity(background_windows=bw)\n", - "res = s.quantification(intensities, kfactors, plot_result=True,\n", - "composition_units='atomic')" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "1" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "res[0].axes_manager.signal_size" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:126: VisibleDeprecationWarning: The Signal class will be deprecated from version 1.0.0 and replaced with BaseSignal\n", - " VisibleDeprecationWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "array([False, False, False, True], dtype=bool)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Simulate a spectrum image with vacuum region\n", - "import numpy as np\n", - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "s_vac = hs.signals.BaseSignal(np.ones_like(s.data, dtype=float))*0.005\n", - "s_vac.add_poissonian_noise()\n", - "si = hs.stack([s]*3 + [s_vac])\n", - "si.vacuum_mask().data" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Scaling the data to normalize the (presumably) Poissonian noise\n", - "\n", - "Performing decomposition analysis\n", - "Auto transposing the data\n", - "Undoing data pre-treatments\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/to266/dev/hyperspy/hyperspy/signals.py:54: VisibleDeprecationWarning: The Signal1D class will be deprecated from version 1.0.0 and replaced with Signal1D\n", - " VisibleDeprecationWarning)\n", - "/home/to266/dev/hyperspy/hyperspy/signals.py:126: VisibleDeprecationWarning: The Signal class will be deprecated from version 1.0.0 and replaced with BaseSignal\n", - " VisibleDeprecationWarning)\n" - ] - } - ], - "source": [ - "s = hs.datasets.example_signals.EDS_TEM_Spectrum()\n", - "si = hs.stack([s]*3)\n", - "si.change_dtype(float)\n", - "si.decomposition()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## utils.eds" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "2.8766744984001607" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Electron range in pure Copper at 30 kV in micron\n", - "hs.eds.electron_range('Cu', 30.)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "1.9361716759499248" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# X-ray range of Cu Ka in pure Copper at 30 kV in micron\n", - "hs.eds.xray_range('Cu_Ka', 30.)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "7.6418811280855454" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# X-ray range of Cu Ka in pure Carbon at 30kV in micron\n", - "hs.eds.xray_range('Cu_Ka', 30., hs.material.elements.C.\n", - "Physical_properties.density_gcm3)\n", - " " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Conda Python3", - "language": "python", - "name": "conda_py35" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.1" - }, - "widgets": { - "state": {}, - "version": "1.1.1" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/hyperspy/tests/drawing/data/Cr_L_ll.hspy b/hyperspy/tests/drawing/data/Cr_L_ll.hspy deleted file mode 100644 index 7371e4e0f8..0000000000 Binary files a/hyperspy/tests/drawing/data/Cr_L_ll.hspy and /dev/null differ diff --git a/hyperspy/tests/drawing/data/signal_markers_hs1_7_5.hspy b/hyperspy/tests/drawing/data/signal_markers_hs1_7_5.hspy new file mode 100644 index 0000000000..57d74302f0 Binary files /dev/null and b/hyperspy/tests/drawing/data/signal_markers_hs1_7_5.hspy differ diff --git a/hyperspy/tests/drawing/markers/test_colorbar_collection.png b/hyperspy/tests/drawing/markers/test_colorbar_collection.png new file mode 100644 index 0000000000..81768b7ffe Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_colorbar_collection.png differ diff --git a/hyperspy/tests/drawing/markers/test_iterating_markers.png b/hyperspy/tests/drawing/markers/test_iterating_markers.png new file mode 100644 index 0000000000..577c6c45aa Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_iterating_markers.png differ diff --git a/hyperspy/tests/drawing/markers/test_load_old_markers.png b/hyperspy/tests/drawing/markers/test_load_old_markers.png new file mode 100644 index 0000000000..5ce9e6455c Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_load_old_markers.png differ diff --git a/hyperspy/tests/drawing/markers/test_multi_collections_navigator.png b/hyperspy/tests/drawing/markers/test_multi_collections_navigator.png new file mode 100644 index 0000000000..9bb074bb72 Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_multi_collections_navigator.png differ diff --git a/hyperspy/tests/drawing/markers/test_multi_collections_signal.png b/hyperspy/tests/drawing/markers/test_multi_collections_signal.png new file mode 100644 index 0000000000..5092f766d9 Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_multi_collections_signal.png differ diff --git a/hyperspy/tests/drawing/markers/test_position_texts_with_mathtext.png b/hyperspy/tests/drawing/markers/test_position_texts_with_mathtext.png new file mode 100644 index 0000000000..6675a584d6 Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_position_texts_with_mathtext.png differ diff --git a/hyperspy/tests/drawing/markers/test_relative_text_collection_with_reference.png b/hyperspy/tests/drawing/markers/test_relative_text_collection_with_reference.png new file mode 100644 index 0000000000..89cf0c1df0 Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_relative_text_collection_with_reference.png differ diff --git a/hyperspy/tests/drawing/markers/test_text_collection_close_render.png b/hyperspy/tests/drawing/markers/test_text_collection_close_render.png new file mode 100644 index 0000000000..cf840e31b1 Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_text_collection_close_render.png differ diff --git a/hyperspy/tests/drawing/markers/test_text_marker_plot.png b/hyperspy/tests/drawing/markers/test_text_marker_plot.png new file mode 100644 index 0000000000..f832afe645 Binary files /dev/null and b/hyperspy/tests/drawing/markers/test_text_marker_plot.png differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_eds_lines.png b/hyperspy/tests/drawing/plot_markers/test_plot_eds_lines.png deleted file mode 100644 index 92c70ee575..0000000000 Binary files a/hyperspy/tests/drawing/plot_markers/test_plot_eds_lines.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_eels_labels.png b/hyperspy/tests/drawing/plot_markers/test_plot_eels_labels.png deleted file mode 100644 index 0a9ce849e4..0000000000 Binary files a/hyperspy/tests/drawing/plot_markers/test_plot_eels_labels.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_line_markers.png b/hyperspy/tests/drawing/plot_markers/test_plot_line_markers.png index 6ac6381939..6e4a96e253 100644 Binary files a/hyperspy/tests/drawing/plot_markers/test_plot_line_markers.png and b/hyperspy/tests/drawing/plot_markers/test_plot_line_markers.png differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_markers_zorder.png b/hyperspy/tests/drawing/plot_markers/test_plot_markers_zorder.png new file mode 100644 index 0000000000..599046cc67 Binary files /dev/null and b/hyperspy/tests/drawing/plot_markers/test_plot_markers_zorder.png differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_point_markers.png b/hyperspy/tests/drawing/plot_markers/test_plot_point_markers.png index 8fb15a4baa..5b18f87d78 100644 Binary files a/hyperspy/tests/drawing/plot_markers/test_plot_point_markers.png and b/hyperspy/tests/drawing/plot_markers/test_plot_point_markers.png differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_rectange_markers.png b/hyperspy/tests/drawing/plot_markers/test_plot_rectange_markers.png deleted file mode 100644 index 5f90dfc99a..0000000000 Binary files a/hyperspy/tests/drawing/plot_markers/test_plot_rectange_markers.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_rectangle_markers.png b/hyperspy/tests/drawing/plot_markers/test_plot_rectangle_markers.png new file mode 100644 index 0000000000..51bc7babc2 Binary files /dev/null and b/hyperspy/tests/drawing/plot_markers/test_plot_rectangle_markers.png differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_nav.png b/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_nav.png index a62c41cd29..0e60a3b97d 100644 Binary files a/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_nav.png and b/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_nav.png differ diff --git a/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_sig.png b/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_sig.png index ec58d9772b..25517c3dcc 100644 Binary files a/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_sig.png and b/hyperspy/tests/drawing/plot_markers/test_plot_text_markers_sig.png differ diff --git a/hyperspy/tests/drawing/plot_model/test_fit_EELS_convolved_False.png b/hyperspy/tests/drawing/plot_model/test_fit_EELS_convolved_False.png deleted file mode 100644 index 44f91be042..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_fit_EELS_convolved_False.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_fit_EELS_convolved_True.png b/hyperspy/tests/drawing/plot_model/test_fit_EELS_convolved_True.png deleted file mode 100644 index ccbe466e17..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_fit_EELS_convolved_True.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-False-False.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-False-False.png deleted file mode 100644 index 3ab2008dc8..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-False-False.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-False-True.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-False-True.png deleted file mode 100644 index a509ab5f82..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-False-True.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-True-False.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-True-False.png deleted file mode 100644 index c82711153b..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-True-False.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-True-True.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-True-True.png deleted file mode 100644 index f8a2cd9639..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_False-True-True.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-False-False.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-False-False.png deleted file mode 100644 index 9252708bb0..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-False-False.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-False-True.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-False-True.png deleted file mode 100644 index 79290b696f..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-False-True.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-True-False.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-True-False.png deleted file mode 100644 index 4bb8a3a0aa..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-True-False.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-True-True.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-True-True.png deleted file mode 100644 index 4347d587df..0000000000 Binary files a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_eelsmodel_True-True-True.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_False-False.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_False-False.png new file mode 100644 index 0000000000..1382316120 Binary files /dev/null and b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_False-False.png differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_False-True.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_False-True.png new file mode 100644 index 0000000000..dbc66cc161 Binary files /dev/null and b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_False-True.png differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_True-False.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_True-False.png new file mode 100644 index 0000000000..ded8979ad4 Binary files /dev/null and b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_True-False.png differ diff --git a/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_True-True.png b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_True-True.png new file mode 100644 index 0000000000..15dd2d94b6 Binary files /dev/null and b/hyperspy/tests/drawing/plot_model/test_plot_gaussian_signal1D_True-True.png differ diff --git a/hyperspy/tests/drawing/plot_model1d/test_default_navigator_plot.png b/hyperspy/tests/drawing/plot_model1d/test_default_navigator_plot.png index c0a7dfa07f..1a113ce0eb 100644 Binary files a/hyperspy/tests/drawing/plot_model1d/test_default_navigator_plot.png and b/hyperspy/tests/drawing/plot_model1d/test_default_navigator_plot.png differ diff --git a/hyperspy/tests/drawing/plot_model1d/test_default_signal_plot.png b/hyperspy/tests/drawing/plot_model1d/test_default_signal_plot.png index 1c53daf758..b71eaa9b1d 100644 Binary files a/hyperspy/tests/drawing/plot_model1d/test_default_signal_plot.png and b/hyperspy/tests/drawing/plot_model1d/test_default_signal_plot.png differ diff --git a/hyperspy/tests/drawing/plot_model1d/test_disable_plot_components.png b/hyperspy/tests/drawing/plot_model1d/test_disable_plot_components.png index 1c53daf758..b71eaa9b1d 100644 Binary files a/hyperspy/tests/drawing/plot_model1d/test_disable_plot_components.png and b/hyperspy/tests/drawing/plot_model1d/test_disable_plot_components.png differ diff --git a/hyperspy/tests/drawing/plot_model1d/test_plot_components.png b/hyperspy/tests/drawing/plot_model1d/test_plot_components.png index 396693fbac..13374dd308 100644 Binary files a/hyperspy/tests/drawing/plot_model1d/test_plot_components.png and b/hyperspy/tests/drawing/plot_model1d/test_plot_components.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav1_sig1.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav1_sig1.png index 8d93e57851..48736a1839 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav1_sig1.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav1_sig1.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav2_sig2.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav2_sig2.png index bd1abce461..71b5e1065d 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav2_sig2.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_distances_nav2_sig2.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav1_sig1.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav1_sig1.png index 0bcd1d4d9b..bc3c43330f 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav1_sig1.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav1_sig1.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig1.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig1.png index f620fed9a4..09ddf1457f 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig1.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig1.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig2.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig2.png index e3fd99e390..49254d9bf0 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig2.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_labels_nav2_sig2.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_metric.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_metric.png deleted file mode 100644 index c7eaea2949..0000000000 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_metric.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav1_sig1.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav1_sig1.png index 85d5e6c870..2675cd0fb5 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav1_sig1.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav1_sig1.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav2_sig1.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav2_sig1.png index 85d5e6c870..2675cd0fb5 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav2_sig1.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav2_sig1.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav2_sig2.png b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav2_sig2.png new file mode 100644 index 0000000000..fe2dcb095d Binary files /dev/null and b/hyperspy/tests/drawing/plot_mva/test_plot_cluster_signals_nav2_sig2.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_cumulative_explained_variance_ratio.png b/hyperspy/tests/drawing/plot_mva/test_plot_cumulative_explained_variance_ratio.png index 09d10d5c37..eb3cb83621 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_cumulative_explained_variance_ratio.png and b/hyperspy/tests/drawing/plot_mva/test_plot_cumulative_explained_variance_ratio.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_3.png b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_3.png index f4722b0a0c..c710c55e3f 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_3.png and b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_3.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_n1.png b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_n1.png index 10ad925552..ddedff0e2b 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_n1.png and b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_factors_nav1_n1.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_3.png b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_3.png index fef9165e29..98c3095bcf 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_3.png and b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_3.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_n1.png b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_n1.png index 2fad36d6f9..6b40112bd4 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_n1.png and b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav1_n1.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_6-3-all.png b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_6-3-all.png index 54698a2b59..43f605dd22 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_6-3-all.png and b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_6-3-all.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_8-4-None.png b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_8-4-None.png index afb46ef2f5..8cef6e5d31 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_8-4-None.png and b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_8-4-None.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_n2-2-ticks.png b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_n2-2-ticks.png index 319320727a..1cf6d2a91f 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_n2-2-ticks.png and b/hyperspy/tests/drawing/plot_mva/test_plot_decomposition_loadings_nav2_n2-2-ticks.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-cardinal.png index 06d5c97014..e2673b8693 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-ordinal.png index 19f7b8b927..3c5571c619 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-index-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-cardinal.png index 29c7a17caf..4e6adededc 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-ordinal.png index 44eca58312..ce5af30497 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0-number-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-cardinal.png index 201f321b3a..a4863aecd0 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-ordinal.png index 00250426db..663e41ab1c 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-index-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-cardinal.png index 13b4cf1133..8274b7b1f9 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-ordinal.png index f41c3749f9..3b6b4d9cc6 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_10-0.001-number-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-cardinal.png index add45394a1..8b9f10a55b 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-ordinal.png index 77583812c6..c07aae93e6 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-index-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-cardinal.png index 50d96fa215..86d0e1ba5e 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-ordinal.png index 7fd0f152b6..ea084308d0 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0-number-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-cardinal.png index 49b76b7be3..8467b7b695 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-ordinal.png index 0aba53a4ef..2a54865009 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-index-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-cardinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-cardinal.png index e6cf76c09c..f77d448d83 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-cardinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-cardinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-ordinal.png b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-ordinal.png index 3ad7154c25..814039521c 100644 Binary files a/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-ordinal.png and b/hyperspy/tests/drawing/plot_mva/test_plot_explained_variance_ratio_50-0.001-number-ordinal.png differ diff --git a/hyperspy/tests/drawing/plot_mva/test_plotting_test_working.png b/hyperspy/tests/drawing/plot_mva/test_plotting_test_working.png deleted file mode 100644 index ad2f550e72..0000000000 Binary files a/hyperspy/tests/drawing/plot_mva/test_plotting_test_working.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_navigation.png b/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_navigation.png index ecbbc8b9cc..df8f1e1f68 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_navigation.png and b/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_navigation.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_signal.png b/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_signal.png index 683a8112ca..054bbf0691 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_signal.png and b/hyperspy/tests/drawing/plot_roi/test_plot_circle_roi_signal.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_linewidth.png b/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_linewidth.png index 154a29d2f5..51eaf56758 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_linewidth.png and b/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_linewidth.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_navigation.png b/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_navigation.png index 04c50ea97a..c25adf5518 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_navigation.png and b/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_navigation.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_signal.png b/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_signal.png index 5d1cd27555..26bb847ed0 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_signal.png and b/hyperspy/tests/drawing/plot_roi/test_plot_line2d_roi_signal.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_0.png b/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_0.png index a4af7d6c60..d36aa66b59 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_0.png and b/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_0.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_1.png b/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_1.png index d4128c9ef3..30f8cec87c 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_1.png and b/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_1.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_2.png b/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_2.png index 2e84910044..beeee788ce 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_2.png and b/hyperspy/tests/drawing/plot_roi/test_plot_point1D_axis_2.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_point2D_navigation.png b/hyperspy/tests/drawing/plot_roi/test_plot_point2D_navigation.png index 5645f987cf..51947a8906 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_point2D_navigation.png and b/hyperspy/tests/drawing/plot_roi/test_plot_point2D_navigation.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_point2D_signal.png b/hyperspy/tests/drawing/plot_roi/test_plot_point2D_signal.png index 7f7dda9d5c..ea66624c87 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_point2D_signal.png and b/hyperspy/tests/drawing/plot_roi/test_plot_point2D_signal.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_navigation.png b/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_navigation.png index 5e168256e4..b0708e2c25 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_navigation.png and b/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_navigation.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_signal.png b/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_signal.png index 8cb6b91655..20b3cd9c42 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_signal.png and b/hyperspy/tests/drawing/plot_roi/test_plot_rectangular_roi_signal.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_0.png b/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_0.png index b1cee2bc3d..3d060f88ce 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_0.png and b/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_0.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_1.png b/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_1.png index dcf467592b..fa29d9ea94 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_1.png and b/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_1.png differ diff --git a/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_2.png b/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_2.png index 4faed44858..4885497997 100644 Binary files a/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_2.png and b/hyperspy/tests/drawing/plot_roi/test_plot_spanroi_axis_2.png differ diff --git a/hyperspy/tests/drawing/plot_roi_map/test_cmap_image_None.png b/hyperspy/tests/drawing/plot_roi_map/test_cmap_image_None.png new file mode 100644 index 0000000000..4bdfa66741 Binary files /dev/null and b/hyperspy/tests/drawing/plot_roi_map/test_cmap_image_None.png differ diff --git a/hyperspy/tests/drawing/plot_roi_map/test_cmap_image_gray.png b/hyperspy/tests/drawing/plot_roi_map/test_cmap_image_gray.png new file mode 100644 index 0000000000..446c5bd36e Binary files /dev/null and b/hyperspy/tests/drawing/plot_roi_map/test_cmap_image_gray.png differ diff --git a/hyperspy/tests/drawing/plot_roi_map/test_color_image_None.png b/hyperspy/tests/drawing/plot_roi_map/test_color_image_None.png new file mode 100644 index 0000000000..4bdfa66741 Binary files /dev/null and b/hyperspy/tests/drawing/plot_roi_map/test_color_image_None.png differ diff --git a/hyperspy/tests/drawing/plot_roi_map/test_color_image_color1.png b/hyperspy/tests/drawing/plot_roi_map/test_color_image_color1.png new file mode 100644 index 0000000000..e8783bf3a0 Binary files /dev/null and b/hyperspy/tests/drawing/plot_roi_map/test_color_image_color1.png differ diff --git a/hyperspy/tests/drawing/plot_roi_map/test_single_figure_image_None.png b/hyperspy/tests/drawing/plot_roi_map/test_single_figure_image_None.png new file mode 100644 index 0000000000..e75df59484 Binary files /dev/null and b/hyperspy/tests/drawing/plot_roi_map/test_single_figure_image_None.png differ diff --git a/hyperspy/tests/drawing/plot_roi_map/test_single_figure_image_gray.png b/hyperspy/tests/drawing/plot_roi_map/test_single_figure_image_gray.png new file mode 100644 index 0000000000..a66e9ea055 Binary files /dev/null and b/hyperspy/tests/drawing/plot_roi_map/test_single_figure_image_gray.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_1.png b/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_1.png index afd13026d6..7a0b021317 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_1.png and b/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_1.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_2.png b/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_2.png index d222b8d38e..4af32f14a3 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_2.png and b/hyperspy/tests/drawing/plot_signal/test_plot_data_changed_event_2.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_imag.png index e493917c24..6aafa97687 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_real.png index e493917c24..6aafa97687 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-real.png index 23c8ebe3ae..ea6bf396fb 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-1-sig-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_imag.png index f3cda1ba7f..aaa465c316 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_real.png index f3cda1ba7f..aaa465c316 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-real.png index a4cc6a812b..e04fd5b821 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_0-2-sig-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_imag.png index 3e12fe53ce..625c861605 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_real.png index 3e12fe53ce..625c861605 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-real.png index bffd667fdd..beda538233 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-nav-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_imag.png index 5f266b18b2..6d5c031087 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_real.png index 5f266b18b2..6d5c031087 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-real.png index 742274ed41..d130e54c31 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-1-sig-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_imag.png index 9f8667dd53..8fd9d2ca77 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_real.png index 2612af9ffd..8fd9d2ca77 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-real.png index 8e4b01482c..46eb5c7d25 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-nav-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_imag.png index 72db1226bd..1d22cee0eb 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_real.png index 72db1226bd..1d22cee0eb 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-real.png index 6dd8a30d34..b9e895fe6b 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_1-2-sig-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_imag.png index 206699426e..d9233c7783 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_real.png index 206699426e..d9233c7783 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-real.png index 748fde6647..7ed3993186 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-nav-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_imag.png index 846fc4fd16..77e89ac907 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_real.png index 846fc4fd16..77e89ac907 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-real.png index 445acfd75d..969446fcdb 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-1-sig-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_imag.png index b53dc33ca9..2b18f1c195 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_real.png index b53dc33ca9..2b18f1c195 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-real.png index 00bff3777b..0636ea6b68 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-nav-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_imag.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_imag.png index b4f0c06815..5c4b7ffc3d 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_imag.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_imag.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_real.png index b4f0c06815..5c4b7ffc3d 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-complex_real.png differ diff --git a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-real.png b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-real.png index a6ce425078..bdf729be83 100644 Binary files a/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-real.png and b/hyperspy/tests/drawing/plot_signal/test_plot_sig_nav_2-2-sig-real.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_.png index 8004282e8e..f769553787 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_v.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_v.png index a98bb66ac0..d7897a4909 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_v.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_v.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_x.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_x.png index 789a5d5c2a..f48a7fda2c 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_x.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_x.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_xv.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_xv.png index 248d860348..3e8f1615d4 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_xv.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_autoscale_xv.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_log_scale.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_log_scale.png index 6f67854688..db9e013a13 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_log_scale.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_log_scale.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_nav.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_nav.png index d198d95ef3..6270f956a2 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_nav.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_sig.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_sig.png index 0b041870c6..7f1f69f969 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_sig.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_sig_update.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_sig_update.png new file mode 100644 index 0000000000..54516e8b89 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_non_uniform_sig_update.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_auto_update.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_auto_update.png index f53eb27687..b3bc3d35a7 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_auto_update.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_auto_update.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_ax.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_ax.png new file mode 100644 index 0000000000..2b6be9bba7 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_ax.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_cascade.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_cascade.png index 3796f5e381..8cb6229f34 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_cascade.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_cascade.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_default.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_default.png index 4294eb1e4a..ac90a471e9 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_default.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_default.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_heatmap.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_heatmap.png index 501fb53b55..202f0835aa 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_heatmap.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_heatmap.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_-.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_-.png new file mode 100644 index 0000000000..0ad8b9adeb Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_-.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_None.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_None.png new file mode 100644 index 0000000000..0ad8b9adeb Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_None.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_linestyle2.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_linestyle2.png new file mode 100644 index 0000000000..3b09940968 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_linestyle_linestyle2.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_mosaic.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_mosaic.png index 68b39fde9e..d3fb9f580a 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_mosaic.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_mosaic.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_cascade.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_cascade.png new file mode 100644 index 0000000000..e25ccefb52 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_cascade.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_heatmap.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_heatmap.png new file mode 100644 index 0000000000..cda288bdb7 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_heatmap.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_mosaic.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_mosaic.png new file mode 100644 index 0000000000..8c11a6a72a Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_mosaic.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_overlap.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_overlap.png new file mode 100644 index 0000000000..e2fe1ce868 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_normalise_overlap.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_overlap.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_overlap.png index 4294eb1e4a..ac90a471e9 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_overlap.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_overlap.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_cascade.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_cascade.png index a039a21ee7..7163d8b81f 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_cascade.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_cascade.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_default.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_default.png index 71387f5f9a..22d03c4289 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_default.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_default.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_heatmap.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_heatmap.png index a173f21385..ad657a43da 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_heatmap.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_heatmap.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_mosaic.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_mosaic.png index 90e9ed38bc..8a4f3b3bf3 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_mosaic.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_mosaic.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_overlap.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_overlap.png index 71387f5f9a..22d03c4289 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_overlap.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_rev_overlap.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1nav.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1nav.png index f1f0571134..1c88a42f46 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1nav.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1sig.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1sig.png index 26724204fc..f8170e7db2 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1sig.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_1sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2nav.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2nav.png index 7768203141..1cddd24bd4 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2nav.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2sig.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2sig.png index 7768203141..1cddd24bd4 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2sig.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_spectra_sync_2sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-nav.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-nav.png index 846b37b681..aa0916ac71 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-nav.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-sig.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-sig.png index 52e2106054..54de92874e 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-sig.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_1-sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-nav.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-nav.png index 10dc3e7a06..5bea118705 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-nav.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-sig.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-sig.png index 6f79c98f9a..55b70bdce2 100644 Binary files a/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-sig.png and b/hyperspy/tests/drawing/plot_signal1d/test_plot_two_cursors_2-sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_nav.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_nav.png new file mode 100644 index 0000000000..d0ad89ec11 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_nav_update.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_nav_update.png new file mode 100644 index 0000000000..c1302ab70f Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_nav_update.png differ diff --git a/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_sig.png b/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_sig.png new file mode 100644 index 0000000000..6d0cb1024f Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal1d/test_plot_uniform_sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_False.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_False.png index e6560ef8c5..7faa72d2cb 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_False.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_False.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_True.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_True.png index 17ed3a0201..168b29c0bd 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_True.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_FFT_True.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.2.png index 2ac6a04eca..29b66baff2 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.7.png index 3d26556250..c296a5eec8 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.2.png index 99b9a9e7c3..aad3914031 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.7.png index 450da58362..4d08738c84 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-False-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.2.png index 88e8821009..d371607cab 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.7.png index 8b64300e27..a157669832 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.2.png index cc16c68059..0a8289b961 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.7.png index ce4668e587..54f9768a24 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-False-True-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.2.png index 1e61724d73..677011d68a 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.7.png index b77dd6e268..dc48b470b4 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.2.png index f3053802d2..3b89e9cb96 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.7.png index 0de5597514..0dfa1d2891 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-False-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.2.png index 0025620bc9..62d6855e10 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.7.png index 5fdce8d78c..0772591e90 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.2.png index 2cfbaec348..f409e7474a 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.7.png index b21a00a262..40dddf56ac 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_False-True-True-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.2.png index 5559efd2a4..3173e2ebee 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.7.png index 3d26556250..c296a5eec8 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.2.png index 7780ac951a..04af6c7960 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.7.png index 450da58362..4d08738c84 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-False-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.2.png index df808c539e..64e895100b 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.7.png index 8b64300e27..a157669832 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.2.png index 457bb6fd93..f58685288e 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.7.png index ce4668e587..54f9768a24 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-False-True-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.2.png index eadf1ffd3b..e04fd5b821 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.7.png index b77dd6e268..dc48b470b4 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.2.png index d3119ca2bd..56e120a60c 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.7.png index 0de5597514..0dfa1d2891 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-False-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.2.png index 630ba9711c..aaec78e0b3 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.7.png index 5fdce8d78c..0772591e90 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-False-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.2.png index a69057eacd..a34647720c 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.7.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.7.png index b21a00a262..40dddf56ac 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.7.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_True-True-True-True-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xy.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xy.png index bd640afca4..0ca9ec2551 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xy.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xy.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xyv.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xyv.png index 319fe5f4f3..780d327eba 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xyv.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_autoscale_xyv.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_image_ax.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_image_ax.png new file mode 100644 index 0000000000..16a492a35c Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal2d/test_plot_image_ax.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list.png index e06a3dd443..eead627b4f 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list_w_diverging.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list_w_diverging.png index 5dd0784d01..ee8f7ed5bf 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list_w_diverging.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_list_w_diverging.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bitfalse.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bitfalse.png index e357c8bdaa..9773d6886d 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bitfalse.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bitfalse.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bittrue.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bittrue.png index aaa508fcb0..a17bef1078 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bittrue.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_make_cmap_bittrue.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_mpl_colors.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_mpl_colors.png index ab6a338c82..69e6d47313 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_mpl_colors.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_mpl_colors.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_multi_w_rgb.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_multi_w_rgb.png index 7041d849f5..26c462ceff 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_multi_w_rgb.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_multi_w_rgb.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_one_string.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_one_string.png index 0fdc2df516..4e6a6d54a5 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_one_string.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_cmap_one_string.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax0.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax0.png index adea436cae..189a160687 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax0.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax0.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax1.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax1.png index fdf06c50d4..5d5c406941 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax1.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_None-vmin_vmax1.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax0.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax0.png index 157f89a221..caa943000d 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax0.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax0.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax1.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax1.png index 83a66446d1..0aac6e71d5 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax1.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_multi-vmin_vmax1.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax0.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax0.png index 0a469b3a9e..6d3f5568fa 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax0.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax0.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax1.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax1.png index a80617a732..4e8d3e87db 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax1.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_colorbar_single-vmin_vmax1.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_default.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_default.png index 4bd965e994..61355baa22 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_default.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_default.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image.png index a88959dee7..0b909afd51 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image_stack.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image_stack.png index 7ee60572b0..6e7b3e9408 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image_stack.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_single_image_stack.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile0.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile0.png index 9baa591fd4..d82471bf60 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile0.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile0.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile1.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile1.png index 8b87269609..449a7349cd 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile1.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile1.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile2.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile2.png index f01ffa90ad..25abae9e90 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile2.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile2.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile3.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile3.png index 0902bcdea3..11c650a72b 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile3.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_images_vmin_vmax_percentile_percentile3.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile0.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile0.png index 420d4edd92..06ffc1616d 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile0.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile0.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile1.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile1.png index 945308a8d0..ab9d329c17 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile1.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_log_scale_percentile1.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_None-None.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_None-None.png index 000bd5bb0b..91930d1f65 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_None-None.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_None-None.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_vmin0-vmax0.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_vmin0-vmax0.png index b5cb6e240c..b226865f1b 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_vmin0-vmax0.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_multiple_images_list_vmin0-vmax0.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_None.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_None.png index 4597aa36bd..dbac1860f1 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_None.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_None.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_gray.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_gray.png index 08be6ad1c5..b8d44a4415 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_gray.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_gray.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_preference.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_preference.png index b07210cc68..ec12b90190 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_preference.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_navigator_colormap_preference.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_sig.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_sig.png new file mode 100644 index 0000000000..9e754877a5 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_update_nav.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_update_nav.png new file mode 100644 index 0000000000..8e07b0bf11 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_update_nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_update_sig.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_update_sig.png new file mode 100644 index 0000000000..0e6ef91864 Binary files /dev/null and b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_2s1n_update_sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_nav.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_nav.png index 1c11e9ae4b..99c769baca 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_nav.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_nav.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_sig.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_sig.png index cce2a56f22..8b1d187166 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_sig.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_non_uniform_sig.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-auto-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-auto-all.png index 0dabb9a8e4..06ae5d8f30 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-auto-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-auto-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-label1-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-label1-all.png index 79c21c3ac1..b590acd5bf 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-label1-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-auto-label1-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-auto-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-auto-all.png index 0dabb9a8e4..06ae5d8f30 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-auto-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-auto-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-label1-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-label1-all.png index 79c21c3ac1..b590acd5bf 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-label1-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_1.0-colors1-label1-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-auto-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-auto-all.png index 0055ba9947..21c57ff9f2 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-auto-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-auto-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-label1-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-label1-all.png index d0273f9c9d..d44554defb 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-label1-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-auto-label1-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-auto-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-auto-all.png index 0055ba9947..21c57ff9f2 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-auto-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-auto-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-label1-all.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-label1-all.png index d0273f9c9d..d44554defb 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-label1-all.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_overlay_alphas1-colors1-label1-all.png differ diff --git a/hyperspy/tests/drawing/plot_signal2d/test_plot_rgb_image.png b/hyperspy/tests/drawing/plot_signal2d/test_plot_rgb_image.png index 63d2f9723d..350afcc572 100644 Binary files a/hyperspy/tests/drawing/plot_signal2d/test_plot_rgb_image.png and b/hyperspy/tests/drawing/plot_signal2d/test_plot_rgb_image.png differ diff --git a/hyperspy/tests/drawing/plot_signal_tools/test_plot_BackgroundRemoval.png b/hyperspy/tests/drawing/plot_signal_tools/test_plot_BackgroundRemoval.png index 2b61a97091..6c95b387c9 100644 Binary files a/hyperspy/tests/drawing/plot_signal_tools/test_plot_BackgroundRemoval.png and b/hyperspy/tests/drawing/plot_signal_tools/test_plot_BackgroundRemoval.png differ diff --git a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-0.7.png b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-0.7.png index d2234e7983..817d574289 100644 Binary files a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-0.7.png and b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-1.2.png b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-1.2.png index c1ccb110c6..817d574289 100644 Binary files a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-1.2.png and b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile0-1.2.png differ diff --git a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-0.7.png b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-0.7.png index 319348c1d7..03993b5a8a 100644 Binary files a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-0.7.png and b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-0.7.png differ diff --git a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-1.2.png b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-1.2.png index ce49a50ee5..03993b5a8a 100644 Binary files a/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-1.2.png and b/hyperspy/tests/drawing/plot_signal_tools/test_plot_contrast_editor_percentile1-1.2.png differ diff --git a/hyperspy/tests/drawing/plot_widgets/test_plot_ModifiableSpanSelector.png b/hyperspy/tests/drawing/plot_widgets/test_plot_ModifiableSpanSelector.png deleted file mode 100644 index 74896118a7..0000000000 Binary files a/hyperspy/tests/drawing/plot_widgets/test_plot_ModifiableSpanSelector.png and /dev/null differ diff --git a/hyperspy/tests/drawing/plot_widgets/test_plot_line2d.png b/hyperspy/tests/drawing/plot_widgets/test_plot_line2d.png index 975aea59a1..c7ecdf01d5 100644 Binary files a/hyperspy/tests/drawing/plot_widgets/test_plot_line2d.png and b/hyperspy/tests/drawing/plot_widgets/test_plot_line2d.png differ diff --git a/hyperspy/tests/drawing/plot_widgets/test_plot_range.png b/hyperspy/tests/drawing/plot_widgets/test_plot_range.png index 2f4c755448..36ddb31cee 100644 Binary files a/hyperspy/tests/drawing/plot_widgets/test_plot_range.png and b/hyperspy/tests/drawing/plot_widgets/test_plot_range.png differ diff --git a/hyperspy/tests/drawing/plot_widgets/test_plot_range_Signal2D.png b/hyperspy/tests/drawing/plot_widgets/test_plot_range_Signal2D.png index fc643c7d99..8a76174a83 100644 Binary files a/hyperspy/tests/drawing/plot_widgets/test_plot_range_Signal2D.png and b/hyperspy/tests/drawing/plot_widgets/test_plot_range_Signal2D.png differ diff --git a/hyperspy/tests/drawing/test_figure.py b/hyperspy/tests/drawing/test_figure.py index 89d478654e..3f4cda1b19 100644 --- a/hyperspy/tests/drawing/test_figure.py +++ b/hyperspy/tests/drawing/test_figure.py @@ -1,30 +1,44 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import matplotlib +import matplotlib.pyplot as plt import numpy as np import pytest +from matplotlib.backend_bases import CloseEvent +from packaging.version import Version +import hyperspy.api as hs from hyperspy._components.polynomial import Polynomial -from hyperspy.datasets.example_signals import EDS_TEM_Spectrum +from hyperspy.drawing._markers.points import Points from hyperspy.drawing.figure import BlittedFigure from hyperspy.misc.test_utils import check_closing_plot from hyperspy.signals import Signal1D, Signal2D +def _close_figure_matplotlib_event(figure): + try: + # Introduced in matplotlib 3.6 and `clost_event` deprecated + event = CloseEvent("close_event", figure) + figure.canvas.callbacks.process("close_event", event) + except Exception: # Deprecated in matplotlib 3.6 + figure.canvas.close_event() + + def test_figure_title_length(): f = BlittedFigure() f.title = "Test" * 50 @@ -53,28 +67,35 @@ def test_close_figure_using_matplotlib(): fig.create_figure() assert fig.figure is not None # Close using matplotlib, similar to using gui - fig.figure.canvas.close_event() + _close_figure_matplotlib_event(fig.figure) _assert_figure_state_after_close(fig) def test_close_figure_with_plotted_marker(): - s = EDS_TEM_Spectrum() + s = Signal1D(np.arange(10)) + m = Points( + offsets=[ + [0, 0], + ], + color="red", + sizes=100, + ) + s.add_marker(m) s.plot(True) s._plot.close() check_closing_plot(s) -@pytest.mark.filterwarnings("ignore:The API of the `Polynomial`") -@pytest.mark.parametrize('navigator', ["auto", "slider", "spectrum"]) -@pytest.mark.parametrize('nav_dim', [1, 2]) -@pytest.mark.parametrize('sig_dim', [1, 2]) +@pytest.mark.parametrize("navigator", ["auto", "slider", "spectrum"]) +@pytest.mark.parametrize("nav_dim", [1, 2]) +@pytest.mark.parametrize("sig_dim", [1, 2]) def test_close_figure(navigator, nav_dim, sig_dim): - total_dim = nav_dim*sig_dim + total_dim = nav_dim * sig_dim if sig_dim == 1: Signal = Signal1D elif sig_dim == 2: Signal = Signal2D - s = Signal(np.arange(pow(10, total_dim)).reshape([10]*total_dim)) + s = Signal(np.arange(pow(10, total_dim)).reshape([10] * total_dim)) s.plot(navigator=navigator) s._plot.close() check_closing_plot(s, check_data_changed_close=False) @@ -82,6 +103,104 @@ def test_close_figure(navigator, nav_dim, sig_dim): if sig_dim == 1: m = s.create_model() m.plot() - # Close with matplotlib event - m._plot.signal_plot.figure.canvas.close_event() + # Close using matplotlib, similar to using gui + _close_figure_matplotlib_event(m._plot.signal_plot.figure) m.extend([Polynomial(1)]) + + +def test_remove_markers(): + s = Signal2D(np.arange(pow(10, 3)).reshape([10] * 3)) + s.plot() + m = Points( + offsets=[ + [0, 0], + ], + color="red", + sizes=100, + ) + s.add_marker(m) + s._plot.signal_plot.remove_markers() + assert len(s._plot.signal_plot.ax_markers) == 0 + assert m._collection is None # Check that the collection is set to None + + +@pytest.mark.skipif( + Version(matplotlib.__version__) < Version("3.9.0"), + reason="Subfigures plotting requires matplotlib >= 3.9.0", +) +def test_close_figure_with_subfigure(): + rng = np.random.default_rng() + s = Signal1D(rng.random((10, 10, 10))) + + fig = plt.figure() + subfig_nav, subfig_sig = fig.subfigures(1, 2) + + s.plot(navigator_kwds=dict(fig=subfig_nav), fig=subfig_sig) + s._plot.close() + + # This shows an empty axis... + # s.plot( + # navigator_kwds=dict(fig=subfig_nav), + # fig=subfig_sig + # ) + + +@pytest.mark.skipif( + Version(matplotlib.__version__) >= Version("3.9.0"), + reason="Error raised for matplotlib < 3.9.0", +) +def test_subfigure_preferences_setting(): + rng = np.random.default_rng() + s = Signal1D(rng.random((10, 10, 10))) + + hs.preferences.Plot.use_subfigure = True + if Version(matplotlib.__version__) < Version("3.9.0"): + with pytest.raises(ValueError): + s.plot() + else: + s.plot() + s._plot.close() + # Set default setting back + hs.preferences.Plot.use_subfigure = False + + +@pytest.mark.skipif( + Version(matplotlib.__version__) < Version("3.9.0"), + reason="Subfigures plotting requires matplotlib >= 3.9.0", +) +def test_close_figure_with_subfigure_matplotlib_event(): + rng = np.random.default_rng() + s = Signal1D(rng.random((10, 10, 10))) + + fig = plt.figure() + subfig_nav, subfig_sig = fig.subfigures(1, 2) + + s.plot(navigator_kwds=dict(fig=subfig_nav), fig=subfig_sig) + plt.close(fig) + + +@pytest.mark.skipif( + Version(matplotlib.__version__) < Version("3.9.0"), + reason="Subfigures plotting requires matplotlib >= 3.9.0", +) +def test_subfigure_get_mpl_figure(): + rng = np.random.default_rng() + s = Signal1D(rng.random((10, 10, 10))) + + hs.preferences.Plot.use_subfigure = True + s.plot() + assert isinstance(s._plot.signal_plot.get_mpl_figure(), matplotlib.figure.Figure) + assert isinstance(s._plot.signal_plot.figure, matplotlib.figure.SubFigure) + s._plot.signal_plot.close() + # Set default setting back + hs.preferences.Plot.use_subfigure = False + + +def test_separate_figure_get_mpl_figure(): + rng = np.random.default_rng() + s = Signal1D(rng.random((10, 10, 10))) + + s.plot() + assert isinstance(s._plot.signal_plot.get_mpl_figure(), matplotlib.figure.Figure) + assert isinstance(s._plot.signal_plot.figure, matplotlib.figure.Figure) + s._plot.signal_plot.close() diff --git a/hyperspy/tests/drawing/test_horizontal_plotting.py b/hyperspy/tests/drawing/test_horizontal_plotting.py new file mode 100644 index 0000000000..49c7cf3cee --- /dev/null +++ b/hyperspy/tests/drawing/test_horizontal_plotting.py @@ -0,0 +1,75 @@ +# Copyright 2007-2022 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . +import matplotlib +import numpy as np +import pytest + +import hyperspy.api as hs + +ipympl = pytest.importorskip("ipympl") +ipywidgets = pytest.importorskip("ipywidgets") + + +# ipympl issue: https://github.com/matplotlib/ipympl/issues/236 +# DeprecationWarning: Passing unrecognized arguments to +# super(Toolbar).__init__(). NavigationToolbar2WebAgg.__init__( +# missing 1 required positional argument: 'canvas' +@pytest.mark.filterwarnings("ignore:Passing unrecognized arguments to") +class TestIPYMPL: + def test_horizontal(self, capsys): + matplotlib.use("module://ipympl.backend_nbagg") + s = hs.signals.Signal2D(np.random.random((4, 4, 2, 2))) + s.plot(plot_style="horizontal") + captured = capsys.readouterr() + assert "HBox(children=(Canvas(" in captured.out + + def test_vertical(self, capsys): + matplotlib.use("module://ipympl.backend_nbagg") + s = hs.signals.Signal2D(np.random.random((4, 4, 2, 2))) + s.plot(plot_style="vertical") + captured = capsys.readouterr() + + assert "VBox(children=(Canvas(" in captured.out + + def test_only_signal(self, capsys): + matplotlib.use("module://ipympl.backend_nbagg") + s = hs.signals.Signal2D(np.random.random((2, 2))) + s.plot() + captured = capsys.readouterr() + assert "Canvas(toolbar=Toolbar(" in captured.out + + def test_only_navigation(self, capsys): + matplotlib.use("module://ipympl.backend_nbagg") + s = hs.signals.Signal2D(np.random.random((2, 2))).T + s.plot() + captured = capsys.readouterr() + assert "Canvas(toolbar=Toolbar(" in captured.out + + def test_warnings( + self, + ): + with pytest.warns(UserWarning): + s = hs.signals.Signal2D(np.random.random((4, 2, 2))) + s.plot(plot_style="vertical") + + def test_misspelling( + self, + ): + matplotlib.use("module://ipympl.backend_nbagg") + with pytest.raises(ValueError): + s = hs.signals.Signal2D(np.random.random((4, 2, 2))) + s.plot(plot_style="Vert") diff --git a/hyperspy/tests/drawing/test_markers.py b/hyperspy/tests/drawing/test_markers.py new file mode 100644 index 0000000000..69d2ebc3dc --- /dev/null +++ b/hyperspy/tests/drawing/test_markers.py @@ -0,0 +1,1242 @@ +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . +from copy import deepcopy +from pathlib import Path + +import dask.array as da +import matplotlib.pyplot as plt +import numpy as np +import pytest +from matplotlib.collections import ( + LineCollection, + PolyCollection, + StarPolygonCollection, +) +from matplotlib.transforms import ( + CompositeGenericTransform, + IdentityTransform, +) + +import hyperspy.api as hs +from hyperspy._signals.signal2d import BaseSignal, Signal1D, Signal2D +from hyperspy.axes import UniformDataAxis +from hyperspy.drawing.markers import markers_dict_to_markers +from hyperspy.external.matplotlib.collections import ( + CircleCollection, + EllipseCollection, + RectangleCollection, + SquareCollection, + TextCollection, +) +from hyperspy.external.matplotlib.quiver import Quiver +from hyperspy.misc.test_utils import update_close_figure +from hyperspy.utils.markers import ( + Arrows, + Circles, + Ellipses, + HorizontalLines, + Lines, + Markers, + Points, + Polygons, + Rectangles, + Squares, + Texts, + VerticalLines, +) + +BASELINE_DIR = "markers" +DEFAULT_TOL = 2.0 +STYLE_PYTEST_MPL = "default" +FILE_PATH = Path(__file__).resolve().parent + + +plt.style.use(STYLE_PYTEST_MPL) + + +class TestMarkers: + @pytest.fixture + def data(self): + d = np.empty((3,), dtype=object) + for i in np.ndindex(d.shape): + d[i] = np.stack([np.arange(3), np.ones(3) * i], axis=1) + return d + + @pytest.fixture + def lazy_data(self): + d = np.empty((3,), dtype=object) + for i in np.ndindex(d.shape): + d[i] = np.stack([np.arange(3), np.ones(3) * i], axis=1) + d = da.from_array(d, chunks=(1, 1, 1)) + + return d + + @pytest.fixture + def signal(self, data): + sig = BaseSignal(data, ragged=True) + sig.metadata.set_item( + "Peaks.signal_axes", + ( + UniformDataAxis(scale=0.5, offset=-1), + UniformDataAxis(scale=2.0, offset=-2), + ), + ) + return sig + + @pytest.fixture + def collections(self): + collections = [ + Circles, + Ellipses, + Rectangles, + Squares, + Points, + ] + num_col = len(collections) + offsets = [ + np.stack([np.ones(num_col) * i, np.arange(num_col)], axis=1) + for i in range(len(collections)) + ] + kwargs = [ + {"sizes": (0.4,)}, + {"widths": (0.2,), "heights": (0.7,), "angles": (60,), "units": "xy"}, + {"widths": (0.2,), "heights": (0.7,), "angles": (60,), "units": "xy"}, + {"widths": (0.4,)}, + {"sizes": (20,)}, + ] + for k, o in zip(kwargs, offsets): + k["offsets"] = o + collections = [c(**k) for k, c in zip(kwargs, collections)] + return collections + + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) + def test_multi_collections_signal(self, collections): + num_col = len(collections) + s = Signal2D(np.zeros((2, num_col, num_col))) + s.axes_manager.signal_axes[0].offset = 0 + s.axes_manager.signal_axes[1].offset = 0 + s.plot(interpolation=None) + [s.add_marker(c) for c in collections] + return s._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) + def test_multi_collections_navigator(self, collections): + num_col = len(collections) + s = Signal2D(np.zeros((num_col, num_col, 1, 1))) + s.axes_manager.signal_axes[0].offset = 0 + s.axes_manager.signal_axes[1].offset = 0 + s.plot(interpolation=None) + [s.add_marker(c, plot_on_signal=False) for c in collections] + return s._plot.navigator_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, + tolerance=DEFAULT_TOL, + style=STYLE_PYTEST_MPL, + filename="test_iterating_markers.png", + ) + @pytest.mark.parametrize("iter_data", ("lazy_data", "data")) + def test_iterating_markers(self, request, iter_data): + data = request.getfixturevalue(iter_data) + s = Signal2D(np.ones((3, 5, 6))) + markers = Points(offsets=data, sizes=(50,)) + s.add_marker(markers) + s.axes_manager.navigation_axes[0].index = 2 + return s._plot.signal_plot.figure + + def test_parameters_2_scatter(self, data): + m = Points( + offsets=np.array([[100, 70], [70, 100]]), + color="g", + sizes=(3,), + ) + s = Signal2D(np.zeros((100, 100))) + s.add_marker(m) + + def test_parameters_singletons(self, signal, data): + m = Points(offsets=np.array([[100, 70], [70, 100]]), color="b", sizes=3) + s = Signal2D(np.zeros((2, 100, 100))) + s.add_marker(m) + + def test_parameters_singletons_iterating(self): + data = np.empty(2, dtype=object) + data[0] = np.array([[100, 70], [70, 100]]) + data[1] = np.array([[100, 70], [70, 100]]) + sizes = np.empty(2, dtype=object) + sizes[0] = 3 + sizes[1] = 4 + m = Points(offsets=np.array([[100, 70], [70, 100]]), color="b", sizes=sizes) + s = Signal2D(np.zeros((2, 100, 100))) + s.add_marker(m) + + @pytest.mark.parametrize( + "signal_axes", + ( + "metadata", + ( + UniformDataAxis(scale=0.5, offset=-1), + UniformDataAxis(scale=2.0, offset=-2), + ), + None, + ), + ) + def test_from_signal(self, signal, data, signal_axes): + col = Points.from_signal(signal, sizes=(10,), signal_axes=signal_axes) + + s = Signal2D(np.ones((3, 5, 6))) + s.add_marker(col) + s.axes_manager.navigation_axes[0].index = 1 + if isinstance(signal_axes, (tuple, str)): + ans = np.zeros_like(data[1]) + ans[:, 1] = data[1][:, 0] * 2 - 2 + ans[:, 0] = data[1][:, 1] * 0.5 - 1 + np.testing.assert_array_equal(col.get_current_kwargs()["offsets"], ans) + else: + np.testing.assert_array_equal(col.get_current_kwargs()["offsets"], data[1]) + + def test_from_signal_lines(self, signal, data): + data = np.empty((3,), dtype=object) + for i in np.ndindex(data.shape): + data[i] = np.ones((10, 2, 2)) * i + + signal = BaseSignal(data, ragged=True) + lines = Lines.from_signal(signal, key="segments", signal_axes=None) + s = Signal2D(np.ones((3, 5, 6))) + s.add_marker(lines) + + def test_from_signal_not_ragged(self): + s = hs.signals.Signal2D(np.ones((2, 3, 5, 6, 7))) + + # not a ragged signal, navigation_dim must be zero + pos = hs.signals.BaseSignal(np.arange(10).reshape((5, 2))) + col = hs.plot.markers.Points.from_signal(pos) + + s.add_marker(col) + + def test_from_signal_fail(self, signal): + with pytest.raises(ValueError): + _ = Points.from_signal(signal, sizes=(10,), signal_axes="test") + + def test_find_peaks(self): + from skimage.draw import disk + from skimage.morphology import disk as disk2 + + import hyperspy.api as hs + + rr, cc = disk( + center=(10, 8), + radius=4, + ) + img = np.zeros((2, 3, 4, 20, 20)) + img[:, :, :, rr, cc] = 1 + img[:, 1, 0] = 0 + img[:, 0, 1] = 0 + s = hs.signals.Signal2D(img) + s.axes_manager.signal_axes[0].scale = 1.5 + s.axes_manager.signal_axes[1].scale = 2 + s.axes_manager.signal_axes[0].offset = -1 + s.axes_manager.signal_axes[1].offset = -1 + pks = s.find_peaks( + interactive=False, + method="template_matching", + template=disk2(4), + ) + col = hs.plot.markers.Points.from_signal( + pks, sizes=(10,), signal_axes=s.axes_manager.signal_axes + ) + s.add_marker(col) + + marker_pos = [11, 19] + np.testing.assert_array_equal(col.get_current_kwargs()["offsets"], [marker_pos]) + s.axes_manager.indices = (0, 1, 0) + np.testing.assert_array_equal( + col.get_current_kwargs()["offsets"], [[np.nan, np.nan]] + ) + + s.axes_manager.indices = (1, 0, 0) + np.testing.assert_array_equal( + col.get_current_kwargs()["offsets"], [[np.nan, np.nan]] + ) + + # Go to end with last navigation axis at 0 + s.axes_manager.indices = (3, 2, 0) + np.testing.assert_array_equal(col.get_current_kwargs()["offsets"], [marker_pos]) + + # Go to end with last navigation axis at 1 + s.axes_manager.indices = (3, 2, 1) + np.testing.assert_array_equal(col.get_current_kwargs()["offsets"], [marker_pos]) + + def test_find_peaks0d(self): + from skimage.draw import disk + from skimage.morphology import disk as disk2 + + rr, cc = disk( + center=(10, 8), + radius=4, + ) + img = np.zeros((1, 20, 20)) + img[:, rr, cc] = 1 + s = Signal2D(img) + s.axes_manager.signal_axes[0].scale = 1.5 + s.axes_manager.signal_axes[1].scale = 2 + s.axes_manager.signal_axes[0].offset = -1 + s.axes_manager.signal_axes[1].offset = -1 + pks = s.find_peaks( + interactive=False, + method="template_matching", + template=disk2(4), + ) + col = Points.from_signal( + pks, sizes=(0.3,), signal_axes=s.axes_manager.signal_axes + ) + s.add_marker(col) + np.testing.assert_array_equal(col.get_current_kwargs()["offsets"], [[11, 19]]) + + def test_deepcopy_signal_with_markers(self, collections): + num_col = len(collections) + s = Signal2D(np.zeros((2, num_col, num_col))) + s.plot() + [s.add_marker(c, permanent=True) for c in collections] + new_s = deepcopy(s) + assert len(new_s.metadata["Markers"]) == num_col + + def test_deepcopy_signal_with_muultiple_markers_same_class(self): + markers_list = [Points(offsets=np.array([1, 2]) * i) for i in range(3)] + num_markers = len(markers_list) + s = Signal2D(np.zeros((2, 10, 10))) + s.plot() + [s.add_marker(m, permanent=True) for m in markers_list] + s2 = deepcopy(s) + assert len(s2.metadata["Markers"]) == num_markers + ref_name = ["Points", "Points1", "Points2"] + assert s.metadata.Markers.keys() == s2.metadata.Markers.keys() == ref_name + + def test_get_current_signal(self, collections): + num_col = len(collections) + s = Signal2D(np.zeros((2, num_col, num_col))) + s.plot() + [s.add_marker(c, permanent=True) for c in collections] + cs = s.get_current_signal() + assert len(cs.metadata["Markers"]) == num_col + assert isinstance(cs.metadata.Markers.Circles, Circles) + + def test_plot_and_render(self): + markers = Points(offsets=[[1, 1], [4, 4]]) + s = Signal1D(np.arange(100).reshape((10, 10))) + s.add_marker(markers) + markers.plot(render_figure=True) + + +class TestInitMarkers: + @pytest.fixture + def signal(self): + signal = Signal2D(np.zeros((3, 10, 10))) + return signal + + @pytest.fixture + def static_line_collection(self, signal): + segments = np.ones((10, 2, 2)) + markers = Lines(segments=segments) + markers._signal = signal + return markers + + @pytest.fixture + def iterating_line_collection(self, signal): + data = np.empty((3,), dtype=object) + for i in np.ndindex(data.shape): + data[i] = np.ones((10, 2, 2)) * i + markers = Lines(segments=data) + markers._signal = signal + return markers + + def test_multiple_collections( + self, static_line_collection, iterating_line_collection, signal + ): + signal.add_marker(static_line_collection, permanent=True) + signal.add_marker(iterating_line_collection, permanent=True) + assert len(signal.metadata.Markers) == 2 + + @pytest.mark.parametrize( + "collection", ("iterating_line_collection", "static_line_collection") + ) + def test_init(self, collection, request): + col = request.getfixturevalue(collection) + assert isinstance(col, Markers) + + @pytest.mark.parametrize( + "collection", ("iterating_line_collection", "static_line_collection") + ) + def test_get_data(self, collection, request): + col = request.getfixturevalue(collection) + kwds = col.get_current_kwargs() + assert isinstance(kwds, dict) + assert kwds["segments"].shape == (10, 2, 2) + + @pytest.mark.parametrize( + "collection", ("iterating_line_collection", "static_line_collection") + ) + def test_to_dictionary(self, collection, request): + col = request.getfixturevalue(collection) + dict = col._to_dictionary() + assert dict["class"] == "Lines" + assert dict["name"] == "" + assert dict["plot_on_signal"] is True + + @pytest.mark.parametrize( + "collection", ("iterating_line_collection", "static_line_collection") + ) + def test_update(self, collection, request, signal): + col = request.getfixturevalue(collection) + signal.plot() + signal.add_marker(col) + signal.axes_manager.navigation_axes[0].index = 2 + if collection == "iterating_line_collection": + col.get_current_kwargs()["segments"] + np.testing.assert_array_equal( + col.get_current_kwargs()["segments"], np.ones((10, 2, 2)) * 2 + ) + else: + np.testing.assert_array_equal( + col.get_current_kwargs()["segments"], np.ones((10, 2, 2)) + ) + + @pytest.mark.parametrize( + "collection", ("iterating_line_collection", "static_line_collection") + ) + def test_fail_plot(self, collection, request): + col = request.getfixturevalue(collection) + with pytest.raises(AttributeError): + col.plot() + + def test_deepcopy_iterating_line_collection(self, iterating_line_collection): + it_2 = deepcopy(iterating_line_collection) + assert it_2 is not iterating_line_collection + + def test_wrong_navigation_size(self): + s = Signal2D(np.zeros((2, 3, 3))) + offsets = np.empty((3, 2), dtype=object) + for i in np.ndindex(offsets.shape): + offsets[i] = np.ones((3, 2)) + m = Points(offsets=offsets) + with pytest.raises(ValueError): + s.add_marker(m) + + def test_add_markers_to_multiple_signals(self): + s = Signal2D(np.zeros((2, 3, 3))) + s2 = Signal2D(np.zeros((2, 3, 3))) + m = Points(offsets=[[1, 1], [2, 2]]) + s.add_marker(m, permanent=True) + with pytest.raises(ValueError): + s2.add_marker(m, permanent=True) + + def test_add_markers_to_same_signal(self): + s = Signal2D(np.zeros((2, 3, 3))) + m = Points(offsets=[[1, 1], [2, 2]]) + s.add_marker(m, permanent=True) + with pytest.raises(ValueError): + s.add_marker(m, permanent=True) + + def test_add_markers_to_navigator_without_nav(self): + s = Signal2D(np.zeros((3, 3))) + m = Points(offsets=[[1, 1], [2, 2]]) + with pytest.raises(ValueError): + s.add_marker(m, plot_on_signal=False) + + def test_marker_collection_lazy_nonragged(self): + m = Points(offsets=da.array([[1, 1], [2, 2]])) + assert not isinstance(m.kwargs["offsets"], da.Array) + + def test_rep(self): + offsets = np.array([[1, 1], [2, 2]]) + m = Markers(offsets=offsets, verts=3, sizes=3, collection=PolyCollection) + assert m.__repr__() == "" + + m = Points(offsets=offsets) + assert m.__repr__() == "" + + def test_rep_iterating(self, signal): + offsets = np.empty(3, dtype=object) + for i in range(3): + offsets[i] = np.array([[1, 1], [2, 2]]) + m = Points(offsets=offsets) + assert m.__repr__() == "" + + signal.plot() + signal.add_marker(m) + assert m.__repr__() == "" + + def test_update_static(self): + m = Points(offsets=([[1, 1], [2, 2]])) + s = Signal1D(np.ones((10, 10))) + s.plot() + s.add_marker(m) + s.axes_manager.navigation_axes[0].index = 2 + + @pytest.mark.parametrize( + "subclass", + ( + (Arrows, Quiver, {"offsets": [[1, 1]], "U": [1], "V": [1]}), + (Circles, CircleCollection, {"offsets": [[1, 1]], "sizes": [1]}), + ( + Ellipses, + EllipseCollection, + {"offsets": [1, 2], "widths": [1], "heights": [1]}, + ), + (HorizontalLines, LineCollection, {"offsets": [1, 2]}), + (Points, CircleCollection, {"offsets": [[1, 1]], "sizes": [1]}), + (VerticalLines, LineCollection, {"offsets": [1, 2]}), + ( + Rectangles, + RectangleCollection, + {"offsets": [[1, 1]], "widths": [1], "heights": [1]}, + ), + (Squares, SquareCollection, {"offsets": [[1, 1]], "widths": [1]}), + (Texts, TextCollection, {"offsets": [[1, 1]], "texts": ["a"]}), + (Lines, LineCollection, {"segments": [[0, 0], [1, 1]]}), + ( + Markers, + StarPolygonCollection, + {"collection": "StarPolygonCollection", "offsets": [[1, 1]]}, + ), + ), + ) + def test_initialize_subclasses(self, subclass): + m = subclass[0](**subclass[2]) + assert m._collection_class is subclass[1] + + @pytest.mark.parametrize( + "subclass", + ( + (Arrows, {"offsets": [[1, 1]], "U": [1], "V": [1]}), + (Circles, {"offsets": [[1, 1]], "sizes": [1]}), + (Ellipses, {"offsets": [1, 2], "widths": [1], "heights": [1]}), + (HorizontalLines, {"offsets": [1, 2]}), + (Points, {"offsets": [[1, 1]], "sizes": [1]}), + (VerticalLines, {"offsets": [1, 2]}), + (Rectangles, {"offsets": [[1, 1]], "widths": [1], "heights": [1]}), + (Squares, {"offsets": [[1, 1]], "widths": [1]}), + (Texts, {"offsets": [[1, 1]], "texts": ["a"]}), + (Lines, {"segments": [[0, 0], [1, 1]]}), + (Markers, {"collection": "StarPolygonCollection", "offsets": [[1, 1]]}), + ), + ) + def test_deepcopy(self, subclass): + transforms_kwargs = {} + # only add transform for compatible mMrkers + if subclass[0] not in [HorizontalLines, VerticalLines]: + transforms_kwargs["transform"] = "display" + transforms_kwargs["offset_transform"] = "display" + + m = subclass[0](**subclass[1], **transforms_kwargs) + m2 = deepcopy(m) + + assert m2 is not m + print(m.kwargs.keys()) + for key, value in m.kwargs.items(): + print(key, value, m2.kwargs[key]) + assert np.all(m2.kwargs[key] == value) + + assert m2.offset_transform == m.offset_transform == "display" + if subclass[0] not in [HorizontalLines, VerticalLines]: + assert m2.transform == "display" + assert m2.transform == m.transform == "display" + assert "transform" not in m.kwargs + assert "transform" not in m2.kwargs + + def test_markers_errors_incorrect_transform(self): + with pytest.raises(ValueError): + Circles(offsets=[[1, 1]], sizes=1, transform="data") + with pytest.raises(ValueError): + Lines(segments=[[0, 0], [1, 1]], offset_transform="data") + with pytest.raises(ValueError): + Polygons(verts=[[0, 0], [1, 1]], offset_transform="data") + with pytest.raises(ValueError): + Rectangles(offsets=[[1, 1]], widths=1, heights=1, transform="data") + with pytest.raises(ValueError): + Squares(offsets=[[1, 1]], widths=1, transform="data") + with pytest.raises(ValueError): + Ellipses(offsets=[[1, 1]], widths=1, heights=1, transform="data") + + +class TestsMarkersAddRemove: + def test_add_items_variable(self): + offsets = np.array([[1, 1], [2, 2]]) + m = Points(offsets=offsets) + assert len(m) == 2 + m.add_items(offsets=np.array([[0, 1]])) + assert len(m) == 3 + assert not m._is_iterating + + def test_add_items_variable_length(self): + offsets = np.empty(2, dtype=object) + for i in range(2): + offsets[i] = np.array([[1, 1], [2, 2]]) + m = Points(offsets=offsets) + assert m._is_iterating + for offset in m.kwargs["offsets"].flat: + assert offset.shape == (2, 2) + + m.add_items(offsets=np.array([[0, 1]])) + for offset in m.kwargs["offsets"].flat: + assert offset.shape == (3, 2) + np.testing.assert_allclose(offset[-1], [0, 1]) + assert len(m.kwargs["offsets"][0]) == 3 + assert m._is_iterating + + m.add_items(offsets=np.array([[0, 2]])) + for offset in m.kwargs["offsets"].flat: + assert offset.shape == (4, 2) + np.testing.assert_allclose(offset[-1], [0, 2]) + + def test_remove_items_iterable_navigation_indices(self): + offsets = np.empty(4, dtype=object) + texts = np.empty(4, dtype=object) + for i in range(len(offsets)): + offsets[i] = np.array([[1, 1], [2, 2]]) + texts[i] = ["a" * (i + 1)] * 2 + m = Texts(offsets=offsets, texts=texts) + + assert m._is_iterating + for nav_position in range(4): + assert len(m.kwargs["offsets"][nav_position]) == 2 + assert len(m.kwargs["texts"][nav_position]) == 2 + assert m.kwargs["texts"][nav_position] == ["a" * (nav_position + 1)] * 2 + m.add_items( + offsets=np.array([[0, 1]]), + texts=["new_text"], + navigation_indices=(1,), + ) + + # marker added only in nav coordinates (1, ) + for nav_position in [0, 2, 3]: + assert len(m.kwargs["offsets"][nav_position]) == 2 + assert len(m.kwargs["texts"][nav_position]) == 2 + assert m.kwargs["texts"][nav_position] == ["a" * (nav_position + 1)] * 2 + + assert len(m.kwargs["offsets"][1]) == 3 + assert len(m.kwargs["texts"][1]) == 3 + assert m.kwargs["texts"][1][2] == "new_text" + assert m.kwargs["texts"][1][2] == "new_text" + + def test_remove_items_iterable_navigation_indices2(self): + offsets = np.empty(4, dtype=object) + texts = np.empty(4, dtype=object) + for i in range(len(offsets)): + offsets[i] = np.array([[1, 1], [2, 2]]) + texts[i] = ["a" * (i + 1)] * 2 + m = Texts(offsets=offsets, texts=texts) + + assert m._is_iterating + for nav_position in range(4): + assert len(m.kwargs["offsets"][nav_position]) == 2 + assert len(m.kwargs["texts"][nav_position]) == 2 + m.remove_items(1, navigation_indices=(1,)) + + # marker removed only in nav coordinates (1, ) + for nav_position in [0, 2, 3]: + assert len(m.kwargs["offsets"][nav_position]) == 2 + assert len(m.kwargs["texts"][nav_position]) == 2 + assert len(m.kwargs["offsets"][1]) == 1 + assert len(m.kwargs["texts"][1]) == 1 + + def test_remove_items(self): + offsets = np.empty(2, dtype=object) + texts = np.empty(2, dtype=object) + for i in range(2): + offsets[i] = np.array([[1, 1], [2, 2]]) + texts[i] = ["a" * i] * 2 + m = Texts(offsets=offsets, texts=texts) + assert len(m.kwargs["offsets"][0]) == 2 + assert len(m.kwargs["texts"][0]) == 2 + m.remove_items(indices=1, keys="offsets") + assert len(m.kwargs["offsets"][0]) == 1 + assert len(m.kwargs["texts"][0]) == 2 + + def test_remove_items_None(self): + offsets = np.empty(2, dtype=object) + texts = ["a"] + for i in range(2): + offsets[i] = np.array([[1, 1], [2, 2]]) + m = Texts(offsets=offsets, texts=texts) + assert len(m.kwargs["offsets"][0]) == 2 + assert len(m.kwargs["texts"]) == 1 + m.remove_items(indices=1) + assert len(m.kwargs["offsets"][0]) == 1 + assert len(m.kwargs["texts"]) == 1 + + def test_remove_items_None_iterable(self): + nav_length = 4 + offsets = np.empty(nav_length, dtype=object) + texts = np.empty(nav_length, dtype=object) + for i in range(len(offsets)): + # 3 markers + offsets[i] = np.array([[1, 1], [2, 2], [3, 3]]) + texts[i] = ["a" * (i + 1)] * 3 + m = Texts(offsets=offsets, texts=texts) + for nav_indices in range(nav_length): + assert len(m.kwargs["offsets"][nav_indices]) == 3 + assert len(m.kwargs["texts"][nav_indices]) == 3 + np.testing.assert_allclose(m.kwargs["offsets"][nav_indices][0], [1, 1]) + np.testing.assert_allclose(m.kwargs["offsets"][nav_indices][1], [2, 2]) + np.testing.assert_allclose(m.kwargs["offsets"][nav_indices][2], [3, 3]) + m.remove_items(indices=1) + for nav_indices in range(nav_length): + assert len(m.kwargs["offsets"][nav_indices]) == 2 + assert len(m.kwargs["texts"][nav_indices]) == 2 + np.testing.assert_allclose(m.kwargs["offsets"][nav_indices][0], [1, 1]) + np.testing.assert_allclose(m.kwargs["offsets"][nav_indices][1], [3, 3]) + + def test_remove_items_slice(self): + offsets = np.stack([np.arange(0, 100, 10)] * 2).T + np.array( + [ + 5, + ] + * 2 + ) + texts = np.array(["a", "b", "c", "d", "e", "f", "g", "f", "h", "i"]) + m = Texts(offsets=offsets, texts=texts) + + assert len(m.kwargs["offsets"]) == 10 + assert len(m.kwargs["texts"]) == 10 + m.remove_items(indices=[1, 2]) + assert len(m.kwargs["offsets"]) == 8 + assert len(m.kwargs["texts"]) == 8 + + np.testing.assert_allclose(m.kwargs["offsets"][0], [5, 5]) + np.testing.assert_allclose(m.kwargs["offsets"][1], [35, 35]) + assert m.kwargs["texts"][0] == "a" + assert m.kwargs["texts"][1] == "d" + + def test_remove_items_navigation_indices(self): + offsets = np.array([[1, 1], [2, 2]]) + m = Points(offsets=offsets) + assert not m._is_iterating + assert len(m) == 2 + with pytest.raises(ValueError): + m.remove_items(1, navigation_indices=(1,)) + + +class TestMarkersDictToMarkers: + @pytest.fixture + def iter_data(self): + data = { + "x1": np.arange(10), + "y1": np.arange(10), + "x2": np.arange(10), + "y2": np.arange(10), + "size": np.arange(10), + "text": np.array(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]), + } + return data + + @pytest.fixture + def static_data(self): + data = { + "x1": 1, + "y1": 2, + "x2": 3, + "y2": 4, + "text": "a", + "size": 5, + } + return data + + @pytest.fixture + def static_and_iter_data(self): + data = { + "x1": 1, + "y1": np.arange(10), + "x2": np.arange(10), + "y2": 4, + "text": np.array(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]), + "size": np.arange(10), + } + return data + + @pytest.fixture + def signal(self): + return Signal1D(np.ones((10, 20))) + + @pytest.mark.parametrize( + "data", ("iter_data", "static_data", "static_and_iter_data") + ) + @pytest.mark.parametrize( + "marker_type", + ( + "Point", + "HorizontalLineSegment", + "LineSegment", + "Ellipse", + "HorizontalLine", + "VerticalLine", + "Arrow", + "Rectangle", + "VerticalLineSegment", + "Text", + ), + ) + def test_marker_hs17_API(self, request, marker_type, data, signal): + d = request.getfixturevalue(data) + test_dict = {} + test_dict["data"] = d + test_dict["marker_type"] = marker_type + test_dict["marker_properties"] = {"color": "black"} + test_dict["plot_on_signal"] = True + if marker_type in ["Ellipse", "Rectangle"]: + test_dict["marker_properties"]["fill"] = None + markers = markers_dict_to_markers(test_dict) + + signal.add_marker( + markers, + ) + signal.plot() + + @pytest.mark.parametrize( + "data", ("iter_data", "static_data", "static_and_iter_data") + ) + @pytest.mark.parametrize("marker_type", ("NotAValidMarker",)) + def test_marker_hs17_API_fail(self, request, marker_type, data): + d = request.getfixturevalue(data) + test_dict = {} + test_dict["data"] = d + test_dict["marker_type"] = marker_type + test_dict["marker_properties"] = {"color": "black"} + test_dict["plot_on_signal"] = True + with pytest.raises(AttributeError): + markers_dict_to_markers(test_dict) + + def test_marker2collection_empty(self): + with pytest.raises(ValueError): + markers_dict_to_markers({}) + + +def _test_marker_collection_close(): + signal = Signal2D(np.ones((10, 10))) + segments = np.ones((10, 2, 2)) + markers = Lines(segments=segments) + signal.add_marker(markers) + return signal + + +@update_close_figure() +def test_marker_collection_close(): + return _test_marker_collection_close() + + +class TestMarkersTransform: + @pytest.mark.parametrize( + "offset_transform", + ( + "data", + "display", + "xaxis", + "yaxis", + "axes", + "relative", + ), + ) + def test_set_offset_transform(self, offset_transform): + m = Points( + offsets=[[1, 1], [4, 4]], + sizes=(10,), + color=("black",), + offset_transform=offset_transform, + ) + assert m.offset_transform == offset_transform + signal = Signal1D((np.arange(100) + 1).reshape(10, 10)) + + signal.plot() + signal.add_marker(m) + mapping = { + "data": m.ax.transData.__class__, + "display": IdentityTransform, + "xaxis": m.ax.get_yaxis_transform().__class__, + "yaxis": m.ax.get_xaxis_transform().__class__, + "axes": m.ax.transAxes.__class__, + "relative": CompositeGenericTransform, + } + + assert isinstance(m.offset_transform, mapping[offset_transform]) + + def test_set_plotted_transform( + self, + ): + markers = Points( + offsets=[[1, 1], [4, 4]], + sizes=(10,), + color=("black",), + offset_transform="display", + ) + assert markers.offset_transform == "display" + signal = Signal1D((np.arange(100) + 1).reshape(10, 10)) + signal.plot() + signal.add_marker(markers) + assert isinstance(markers.transform, IdentityTransform) + assert isinstance(markers.offset_transform, IdentityTransform) + markers.offset_transform = "data" + assert isinstance(markers.offset_transform, CompositeGenericTransform) + assert markers._collection.get_transform() == markers.transform + + def test_unknown_tranform(self): + with pytest.raises(ValueError): + _ = Points( + offsets=[[1, 1], [4, 4]], + sizes=(10,), + color=("black",), + transform="test", + offset_transform="test", + ) + + +class TestRelativeMarkers: + def test_relative_marker_collection(self): + signal = Signal1D((np.arange(100) + 1).reshape(10, 10)) + segments = np.zeros((10, 2, 2)) + segments[:, 1, 1] = 1 # set y values end + segments[:, 0, 0] = np.arange(10).reshape(10) # set x values + segments[:, 1, 0] = np.arange(10).reshape(10) # set x values + + markers = Lines(segments=segments, transform="relative") + texts = Texts(offsets=segments[:, 1], texts="a", offset_transform="relative") + signal.plot() + signal.add_marker(markers) + signal.add_marker(texts) + signal.axes_manager.navigation_axes[0].index = 1 + segs = markers._collection.get_segments() + offs = texts._collection.get_offsets() + assert segs[0][0][0] == 0 + assert segs[0][1][1] == 11 + assert offs[0][1] == 11 + + def test_relative_marker_collection_with_shifts(self): + signal = Signal1D((np.arange(100) + 1).reshape(10, 10)) + segments = np.zeros((10, 2, 2)) + segments[:, 1, 1] = 1 # set y values end + segments[:, 0, 0] = np.arange(10).reshape(10) # set x values + segments[:, 1, 0] = np.arange(10).reshape(10) # set x values + + markers = Lines(segments=segments, shift=1 / 9, transform="relative") + texts = Texts( + offsets=segments[:, 1], shift=1 / 9, texts="a", offset_transform="relative" + ) + signal.plot() + signal.add_marker(markers) + signal.add_marker(texts) + signal.axes_manager.navigation_axes[0].index = 1 + segs = markers._collection.get_segments() + offs = texts._collection.get_offsets() + assert segs[0][0][0] == 0 + assert segs[0][1][1] == 12 + assert offs[0][1] == 12 + + +class TestLines: + @pytest.fixture + def offsets(self): + d = np.empty((3,), dtype=object) + for i in np.ndindex(d.shape): + d[i] = np.arange(i[0] + 1) + return d + + def test_vertical_line_collection(self, offsets): + vert = VerticalLines(offsets=offsets) + s = Signal2D(np.zeros((3, 3, 3))) + # s.axes_manager.signal_axes[0].offset = 0 + # s.axes_manager.signal_axes[1].offset = 0 + s.plot() + s.add_marker(vert) + segments = vert.get_current_kwargs()["segments"] + # Offsets --> segments for vertical lines + assert segments.shape == (1, 2, 2) # one line + np.testing.assert_array_equal(segments, np.array([[[0, 0], [0, 1]]])) + + # change position to navigation coordinate (1, ) + s.axes_manager.indices = (1,) + segments = vert.get_current_kwargs()["segments"] + assert segments.shape == (2, 2, 2) # two lines + np.testing.assert_array_equal( + segments, np.array([[[0, 0], [0, 1]], [[1, 0], [1, 1]]]) + ) + + def test_horizontal_line_collection(self, offsets): + hor = HorizontalLines(offsets=offsets) + s = Signal2D(np.zeros((3, 3, 3))) + s.axes_manager.signal_axes[0].offset = 0 + s.axes_manager.signal_axes[1].offset = 0 + s.plot(interpolation=None) + s.add_marker(hor) + kwargs = hor.get_current_kwargs() + np.testing.assert_array_equal(kwargs["segments"], [[[0.0, 0], [1, 0]]]) + + def test_horizontal_vertical_line_error(self, offsets): + with pytest.raises(ValueError): + _ = HorizontalLines(offsets=offsets, transform="data") + with pytest.raises(ValueError): + _ = VerticalLines(offsets=offsets, transform="data") + + +def test_marker_collection_close_render(): + signal = Signal2D(np.ones((2, 10, 10))) + markers = Points(offsets=[[1, 1], [4, 4]], sizes=(10,), color=("black",)) + signal.plot() + signal.add_marker(markers, render_figure=True) + markers.close(render_figure=True) + + +class TestMarkers2: + @pytest.fixture + def offsets(self): + d = np.empty((3,), dtype=object) + for i in np.ndindex(d.shape): + d[i] = np.stack([np.arange(3), np.ones(3) * i], axis=1) + return d + + @pytest.fixture + def extra_kwargs(self): + widths = np.empty((3,), dtype=object) + for i in np.ndindex(widths.shape): + widths[i] = np.ones(3) + + heights = np.empty((3,), dtype=object) + for i in np.ndindex(heights.shape): + heights[i] = np.ones(3) + angles = np.empty((3,), dtype=object) + for i in np.ndindex(angles.shape): + angles[i] = np.ones(3) + + kwds = { + Points: {}, + Circles: {"sizes": (1,)}, + Arrows: {"U": 1, "V": 1}, + Ellipses: {"widths": widths, "heights": heights, "angles": angles}, + } + return kwds + + @pytest.fixture + def signal(self): + return Signal2D(np.ones((3, 10, 10))) + + @pytest.mark.parametrize("MarkerClass", [Points, Circles, Ellipses, Arrows]) + def test_offsest_markers(self, extra_kwargs, MarkerClass, offsets, signal): + markers = MarkerClass(offsets=offsets, **extra_kwargs[MarkerClass]) + signal.plot() + signal.add_marker(markers) + signal.axes_manager.navigation_axes[0].index = 1 + + def test_arrows(self, signal): + arrows = Arrows(offsets=[[1, 1], [4, 4]], U=1, V=1, C=(2, 2)) + signal.plot() + signal.add_marker(arrows) + signal.axes_manager.navigation_axes[0].index = 1 + + @pytest.mark.parametrize("MarkerClass", [Points, Circles, Ellipses, Arrows]) + def test_markers_length_offsets(self, extra_kwargs, MarkerClass, offsets, signal): + m = MarkerClass(offsets=offsets, **extra_kwargs[MarkerClass]) + # variable length markers: needs axes_manager, etc. + with pytest.raises(RuntimeError): + len(m) + + signal.add_marker(m) + + assert len(m) == 3 + + +def test_polygons(): + s = Signal2D(np.zeros((100, 100))) + poylgon1 = [[1, 1], [20, 20], [1, 20], [25, 5]] + poylgon2 = [[50, 60], [90, 40], [60, 40], [23, 60]] + verts = [poylgon1, poylgon2] + m = Polygons(verts=verts) + s.plot() + s.add_marker(m) + + +def test_warning_logger(): + s = Signal2D(np.ones((10, 10))) + m = Points( + offsets=[ + [1, 1], + ], + sizes=10, + ) + s.plot() + with pytest.warns(UserWarning): + s.add_marker(m, plot_marker=False, permanent=False) + + +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=5.0, style=STYLE_PYTEST_MPL +) +def test_load_old_markers(): + """ + File generated using + + import hyperspy.api as hs + import numpy as np + + s = hs.signals.Signal2D(np.ones((14, 14))) + + m_point = hs.plot.markers.point(x=2, y=2, color='C0') + m_line = hs.plot.markers.line_segment(x1=4, x2=6, y1=2, y2=4, color='C1') + m_vline = hs.plot.markers.vertical_line(x=12, color='C1') + m_vline_segment = hs.plot.markers.vertical_line_segment(x=8, y1=0, y2=4, color='C1') + m_hline = hs.plot.markers.horizontal_line(y=5, color='C1') + m_hline_segment = hs.plot.markers.horizontal_line_segment(x1=1, x2=9, y=6, color='C1') + m_arrow = hs.plot.markers.arrow(x1=4, y1=7, x2=6, y2=8, arrowstyle='<->') + m_text = hs.plot.markers.text(x=1, y=4, text="test", color='C2') + m_rect = hs.plot.markers.rectangle(x1=1, x2=3, y1=7, y2=12, edgecolor='C3', fill=True, facecolor='C4') + m_ellipse = hs.plot.markers.ellipse(x=8, y=10, width=4, height=5, edgecolor='C5') + + marker_list = [ + m_point, + m_line, + m_vline, + m_vline_segment, + m_hline, + m_hline_segment, + m_rect, + m_text, + m_arrow, + m_ellipse + ] + + s.add_marker(marker_list, permanent=True) + s.plot(axes_ticks=True) + + import matplotlib.pyplot as plt + plt.savefig('test.png', dpi=300) + s.save("signal_markers_hs1_7_5.hspy") + """ + s = hs.load(FILE_PATH / "data" / "signal_markers_hs1_7_5.hspy") + s.metadata.General.original_filename = "" + s.tmp_parameters.filename = "" + s.plot(axes_ticks=True) + return s._plot.signal_plot.figure + + +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=5.0, style=STYLE_PYTEST_MPL +) +def test_colorbar_collection(): + s = Signal2D(np.ones((100, 100))) + rng = np.random.default_rng(0) + sizes = rng.random((10,)) * 20 + 5 + offsets = rng.random((10, 2)) * 100 + m = hs.plot.markers.Circles( + sizes=sizes, + offsets=offsets, + linewidth=2, + ) + + with pytest.raises(RuntimeError): + m.plot_colorbar() + + s.plot() + s.add_marker(m) + m.set_ScalarMappable_array(sizes.ravel() / 2) + cbar = m.plot_colorbar() + cbar.set_label("Circle radius") + return s._plot.signal_plot.figure + + +def test_collection_error(): + with pytest.raises(ValueError): + Markers(offsets=[[1, 1], [2, 2]], collection="NotACollection") + + with pytest.raises(ValueError): + Markers(offsets=[[1, 1], [2, 2]], collection=object) + + m = Points(offsets=[[1, 1], [2, 2]]) + with pytest.raises(ValueError): + m._set_transform(value="test") + + +def test_permanent_markers_close_open_cycle(): + s = Signal2D(np.ones((100, 100))) + rng = np.random.default_rng(0) + offsets = rng.random((10, 2)) * 100 + m = hs.plot.markers.Points(offsets=offsets) + assert m._signal is None + assert m._axes_manager is None + + s.add_marker(m, permanent=True) + assert m._signal is s + assert m._axes_manager is s.axes_manager + + s._plot.close() + assert m._signal is None + assert m._axes_manager is None + + s.plot() + assert m._signal is s + assert m._axes_manager is s.axes_manager + + +def test_variable_length_markers_navigation_shape(): + nav_dim = 2 + rng = np.random.default_rng(0) + + nav_shape = np.arange(10, 10 * (nav_dim + 1), step=10) + data = np.ones(tuple(nav_shape) + (100, 100)) + s = hs.signals.Signal2D(data) + + offsets = np.empty(s.axes_manager.navigation_shape, dtype=object) + for ind in np.ndindex(offsets.shape): + num = rng.integers(3, 10) + offsets[ind] = rng.random((num, 2)) * 100 + + m = hs.plot.markers.Points( + offsets=offsets, + color="orange", + ) + + s.plot() + s.add_marker(m, permanent=True) + # go to last indices to check that the shape of `offsets` and + # navigation are aligned and plotting/getting currnet kwargs works fine + s.axes_manager.indices = np.array(s.axes_manager.navigation_shape) - 1 + + +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=5.0, style=STYLE_PYTEST_MPL +) +def test_position_texts_with_mathtext(): + s = hs.signals.Signal2D(np.arange(25).reshape((5, 5))) + + s.plot() + + offset = [ + [3, 3], + ] + raw_text = "$\\bar{1}$" + + point_marker = hs.plot.markers.Points(offset) + text_marker = hs.plot.markers.Texts( + offset, + texts=[ + raw_text, + ], + color="red", + ) + + s.add_marker([point_marker, text_marker]) + + return s._plot.signal_plot.figure diff --git a/hyperspy/tests/drawing/test_mpl_testing_setup.py b/hyperspy/tests/drawing/test_mpl_testing_setup.py index dea5483fd9..fd6efc442c 100644 --- a/hyperspy/tests/drawing/test_mpl_testing_setup.py +++ b/hyperspy/tests/drawing/test_mpl_testing_setup.py @@ -1,21 +1,20 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . -from distutils.version import LooseVersion import matplotlib import pytest @@ -25,22 +24,21 @@ def test_mlp_agg_for_CI_testing(): if check_running_tests_in_CI(): - assert matplotlib.get_backend() == 'agg' + assert matplotlib.get_backend().lower() == "agg" + @pytest.fixture def mpl_cmdopt(request): return request.config.getoption("--mpl") -def test_mpl_version(): - # for simplicity, only matplotlib 2.x is supported for testing - assert LooseVersion(matplotlib.__version__) >= LooseVersion('2.0.0') - -@pytest.mark.xfail(reason="Check if plotting tests are working: if this test passes," - " it means that the image comparison of the plotting test are" - " not working.", - strict=True) -@pytest.mark.mpl_image_compare(baseline_dir='', tolerance=2) +@pytest.mark.xfail( + reason="Check if plotting tests are working: if this test passes," + " it means that the image comparison of the plotting test are" + " not working.", + strict=True, +) +@pytest.mark.mpl_image_compare(baseline_dir="", tolerance=2) def test_plotting_test_working(mpl_cmdopt): # Skip if --mpl command line option is not present, because it will always # pass the image comparison test and, therefore, this test will always fail. diff --git a/hyperspy/tests/drawing/test_plot_histograms.py b/hyperspy/tests/drawing/test_plot_histograms.py new file mode 100644 index 0000000000..30b5094959 --- /dev/null +++ b/hyperspy/tests/drawing/test_plot_histograms.py @@ -0,0 +1,29 @@ +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import numpy as np + +import hyperspy.api as hs + + +def test_plot_histograms(): + img = hs.signals.Signal2D(np.random.chisquare(1, [10, 10, 100])) + img2 = hs.signals.Signal2D(np.random.chisquare(2, [10, 10, 100])) + ax = hs.plot.plot_histograms([img, img2], legend=["hist1", "hist2"]) + assert len(ax.lines) == 2 + l1 = ax.lines[0] + assert l1.get_drawstyle() == "steps-mid" diff --git a/hyperspy/tests/drawing/test_plot_lazy.py b/hyperspy/tests/drawing/test_plot_lazy.py index 702350452f..fe22ca1eb1 100644 --- a/hyperspy/tests/drawing/test_plot_lazy.py +++ b/hyperspy/tests/drawing/test_plot_lazy.py @@ -1,19 +1,19 @@ -# Copyright 2007-2020 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import dask.array as da import numpy as np @@ -22,47 +22,44 @@ import hyperspy.api as hs -@pytest.mark.parametrize('ndim', [0, 1, 2, 3]) +@pytest.mark.parametrize("ndim", [0, 1, 2, 3]) def test_plot_lazy(ndim): N = 10 dim = ndim + 1 - s = hs.signals.Signal1D(da.arange(N**dim).reshape([N]*dim)).as_lazy() + s = hs.signals.Signal1D(da.arange(N**dim).reshape([N] * dim)).as_lazy() s.plot() if ndim == 0: - assert s._plot.navigator_data_function == None - elif ndim in [1, 2]: - assert s.navigator.data.shape == tuple([N]*ndim) - assert isinstance(s.navigator, hs.signals.BaseSignal) + assert s._plot.navigator_data_function is None else: - assert s._plot.navigator_data_function == 'slider' + assert s.navigator.data.shape == tuple([N] * ndim) + assert isinstance(s.navigator, hs.signals.BaseSignal) -@pytest.mark.parametrize('plot_kwargs', [{}, - {'navigator':'auto'} , - {'navigator':'spectrum'}]) +@pytest.mark.parametrize( + "plot_kwargs", [{}, {"navigator": "auto"}, {"navigator": "spectrum"}] +) def test_plot_lazy_chunks(plot_kwargs): N = 15 dim = 3 - s = hs.signals.Signal1D(da.arange(N**dim).reshape([N]*dim)).as_lazy() + s = hs.signals.Signal1D(da.arange(N**dim).reshape([N] * dim)).as_lazy() s.data = s.data.rechunk(("auto", "auto", 5)) s.plot(**plot_kwargs) - assert s.navigator.data.shape == tuple([N]*(dim-1)) - assert s.navigator.original_metadata.sum_from == '[slice(5, 10, None)]' + assert s.navigator.data.shape == tuple([N] * (dim - 1)) + assert s.navigator.original_metadata.sum_from == "[slice(5, 10, None)]" def test_compute_navigator(): N = 15 dim = 3 - s = hs.signals.Signal1D(da.arange(N**dim).reshape([N]*dim)).as_lazy() + s = hs.signals.Signal1D(da.arange(N**dim).reshape([N] * dim)).as_lazy() s.compute_navigator(chunks_number=3) - assert s.navigator.original_metadata.sum_from == '[slice(5, 10, None)]' + assert s.navigator.original_metadata.sum_from == "[slice(5, 10, None)]" # change the navigator and check it is used when plotting s.navigator = s.navigator / s.navigator.mean() s.plot() - np.testing.assert_allclose(s._plot.navigator_data_function(), - s.navigator.data) + np.testing.assert_allclose(s._plot.navigator_data_function(), s.navigator.data) def test_navigator_deepcopy_with_new_data(): @@ -72,12 +69,12 @@ def test_navigator_deepcopy_with_new_data(): s1 = s._deepcopy_with_new_data() # After transpose, the navigator should be removed - assert not s1.metadata.has_item('_HyperSpy.navigator') + assert not s1.metadata.has_item("_HyperSpy.navigator") assert s1.navigator is None s2 = s._deepcopy_with_new_data(copy_navigator=True) # After transpose, the navigator should be removed - assert s2.metadata.has_item('_HyperSpy.navigator') + assert s2.metadata.has_item("_HyperSpy.navigator") assert s2.navigator == s.navigator @@ -87,13 +84,13 @@ def test_remove_navigator_operation(): s.compute_navigator(chunks_number=3) s1 = s.T # After transpose, the navigator should be removed - assert not s1.metadata.has_item('_HyperSpy.navigator') + assert not s1.metadata.has_item("_HyperSpy.navigator") assert s1.navigator is None s1.plot() s2 = s1.sum(-1) # After transpose, the navigator should be removed - assert not s2.metadata.has_item('_HyperSpy.navigator') + assert not s2.metadata.has_item("_HyperSpy.navigator") assert s2.navigator is None s2.plot() @@ -101,34 +98,52 @@ def test_remove_navigator_operation(): def test_compute_navigator_index(): N = 15 dim = 4 - s = hs.signals.Signal2D(da.arange(N**dim).reshape([N]*dim)).as_lazy() + s = hs.signals.Signal2D(da.arange(N**dim).reshape([N] * dim)).as_lazy() for ax in s.axes_manager.signal_axes: ax.scale = 0.1 ax.offset = -0.75 s.compute_navigator(index=0.0, chunks_number=3) - assert s.navigator.original_metadata.sum_from == '[slice(5, 10, None), slice(5, 10, None)]' + assert ( + s.navigator.original_metadata.sum_from + == "[slice(5, 10, None), slice(5, 10, None)]" + ) s.compute_navigator(index=0, chunks_number=3) - assert s.navigator.original_metadata.sum_from == '[slice(0, 5, None), slice(0, 5, None)]' + assert ( + s.navigator.original_metadata.sum_from + == "[slice(0, 5, None), slice(0, 5, None)]" + ) s.compute_navigator(index=-0.7, chunks_number=3) - assert s.navigator.original_metadata.sum_from == '[slice(0, 5, None), slice(0, 5, None)]' + assert ( + s.navigator.original_metadata.sum_from + == "[slice(0, 5, None), slice(0, 5, None)]" + ) s.compute_navigator(index=[-0.7, 0.0], chunks_number=3) - assert s.navigator.original_metadata.sum_from == '[slice(0, 5, None), slice(5, 10, None)]' + assert ( + s.navigator.original_metadata.sum_from + == "[slice(0, 5, None), slice(5, 10, None)]" + ) s.compute_navigator(index=0.0, chunks_number=[3, 5]) - assert s.navigator.original_metadata.sum_from == '[slice(5, 10, None), slice(6, 9, None)]' + assert ( + s.navigator.original_metadata.sum_from + == "[slice(5, 10, None), slice(6, 9, None)]" + ) s.compute_navigator(index=[0.7, -0.7], chunks_number=[3, 5]) - assert s.navigator.original_metadata.sum_from == '[slice(10, 15, None), slice(0, 3, None)]' + assert ( + s.navigator.original_metadata.sum_from + == "[slice(10, 15, None), slice(0, 3, None)]" + ) def test_plot_navigator_signal(): N = 15 dim = 4 - s = hs.signals.Signal2D(da.arange(N**dim).reshape([N]*dim)).as_lazy() + s = hs.signals.Signal2D(da.arange(N**dim).reshape([N] * dim)).as_lazy() nav = s.inav[10, 10] nav.compute() nav *= -1 diff --git a/hyperspy/tests/drawing/test_plot_markers.py b/hyperspy/tests/drawing/test_plot_markers.py deleted file mode 100644 index 702b705a29..0000000000 --- a/hyperspy/tests/drawing/test_plot_markers.py +++ /dev/null @@ -1,603 +0,0 @@ -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . - -import logging - -import numpy as np -import pytest - -from hyperspy.datasets.artificial_data import get_core_loss_eels_line_scan_signal -from hyperspy.datasets.example_signals import EDS_TEM_Spectrum -from hyperspy.drawing.marker import dict2marker -from hyperspy.misc.test_utils import sanitize_dict, update_close_figure -from hyperspy.signals import BaseSignal, Signal1D, Signal2D -from hyperspy.utils import markers, stack - -default_tol = 2.0 -baseline_dir = 'plot_markers' -style_pytest_mpl = 'default' - - -class TestMarkers: - - def test_get_data(self): - s = Signal2D(np.zeros([3, 2, 2])) - m = markers.line_segment(x1=list(range(3)), - x2=list(range(3)), - y1=1.3, - y2=1.5) - m.axes_manager = s.axes_manager - assert m.get_data_position('x1') == 0 - assert m.get_data_position('y1') == 1.3 - s.axes_manager[0].index = 2 - assert m.get_data_position('x1') == 2 - assert m.get_data_position('y1') == 1.3 - - def test_iterate_strings(self): - s = Signal2D(np.zeros([3, 2, 2])) - m = markers.text(x=list(range(3)), - y=list(range(3)), - text=['one', 'two', 'three']) - m.axes_manager = s.axes_manager - assert m.get_data_position('text') == 'one' - s.axes_manager[0].index = 2 - assert m.get_data_position('text') == 'three' - - def test_get_one_string(self): - s = Signal2D(np.zeros([3, 2, 2])) - m = markers.text(x=list(range(3)), - y=list(range(3)), - text='one') - m.axes_manager = s.axes_manager - assert m.get_data_position('text') == 'one' - s.axes_manager[0].index = 2 - assert m.get_data_position('text') == 'one' - - def test_get_data_array(self): - s = Signal2D(np.zeros([2, 2, 2, 2])) - m = markers.line_segment(x1=[[1.1, 1.2], [1.3, 1.4]], x2=1.1, y1=1.3, - y2=1.5) - m.axes_manager = s.axes_manager - assert m.get_data_position('x1') == 1.1 - s.axes_manager[0].index = 1 - assert m.get_data_position('x1') == 1.2 - s.axes_manager[1].index = 1 - assert m.get_data_position('x1') == 1.4 - - def test_set_get_data(self): - m = markers.point(x=0, y=1.3) - assert m.data['x1'] == 0 - assert m.data['y1'] == 1.3 - m.add_data(y1=0.3) - assert m.data['x1'] == 0 - assert m.data['y1'] == 0.3 - m.set_data(y1=1.3) - assert m.data['x1'][()][()] is None - assert m.data['y1'] == 1.3 - assert m.data['x1'].dtype == np.dtype('O') - m.add_data(y1=[1, 2]) - assert m.data['y1'][()].shape == (2,) - - def test_markers_properties(self): - m = markers.text(x=1, y=2, text='a') - m.set_marker_properties(fontsize=30, color='red') - assert m.marker_properties == {'color': 'red', 'fontsize': 30} - - def test_auto_update(self): - m = markers.text(y=1, x=2, text='a') - assert m.auto_update is False - m = markers.text(y=[1, 2], x=2, text='a') - assert m.auto_update is True - m.add_data(y1=1) - assert m.auto_update is False - m.add_data(y1=[1, 2]) - assert m.auto_update is True - - def test_get_data_shape_point(self): - m0 = markers.point(5, 5) - m1 = markers.point((5, 10), (5, 10)) - m2 = markers.point(((12, 2, 9), (1, 2, 3)), ((2, 5, 1), (3, 9, 2))) - m3 = markers.vertical_line(((12, 2), (2, 5), (9, 2))) - m4 = markers.point(5, 5) - m4.data['x1'][()] = np.array(None, dtype=object) - m4.data['y1'][()] = np.array(None, dtype=object) - m5 = markers.vertical_line(9) - m6 = markers.rectangle(1, 5, 6, 8) - m7 = markers.rectangle((1, 2), (5, 6), (6, 7), (8, 9)) - m8 = markers.point( - np.arange(256).reshape(2, 2, 2, 2, 2, 2, 2, 2), - np.arange(256).reshape(2, 2, 2, 2, 2, 2, 2, 2)) - assert m0._get_data_shape() == () - assert m1._get_data_shape() == (2,) - assert m2._get_data_shape() == (2, 3) - assert m3._get_data_shape() == (3, 2) - with pytest.raises(ValueError): - assert m4._get_data_shape() == () - assert m5._get_data_shape() == () - assert m6._get_data_shape() == () - assert m7._get_data_shape() == (2,) - assert m8._get_data_shape() == (2, 2, 2, 2, 2, 2, 2, 2) - - def test_add_marker_not_plot(self): - # This will do nothing, since plot_marker=False and permanent=False - # So this test will return a _logger warning - s = Signal1D(np.arange(10)) - m = markers.point(x=5, y=5) - s.add_marker(m, plot_marker=False) - - def test_add_marker_signal1d_navigation_dim(self): - s = Signal1D(np.zeros((3, 50, 50))) - m0 = markers.point(5, 5) - m1 = markers.point((5, 10), (10, 15)) - m2 = markers.point(np.zeros((3, 50)), np.zeros((3, 50))) - s.add_marker(m0) - with pytest.raises(ValueError): - s.add_marker(m1) - s.add_marker(m2) - - def test_add_marker_signal2d_navigation_dim_vertical_line(self): - s = Signal2D(np.arange(2 * 3 * 8 * 9).reshape(2, 3, 8, 9)) - marker_pos_list = [[1, 3, 5], [2, 4, 6]] - m = markers.vertical_line(marker_pos_list) - s.add_marker(m) - s.axes_manager.indices = (0, 1) - for iy, temp_marker_list in enumerate(marker_pos_list): - for ix, value in enumerate(temp_marker_list): - s.axes_manager.indices = (ix, iy) - vertical_line = s._plot.signal_plot.ax.lines[1] - assert value == vertical_line.get_data()[0] - - def test_add_marker_signal2d_navigation_dim(self): - s = Signal2D(np.zeros((3, 50, 50))) - m0 = markers.point(5, 5) - m1 = markers.point((5, 10), (10, 15)) - m2 = markers.point(np.zeros((3, )), np.zeros((3, ))) - s.add_marker(m0) - with pytest.raises(ValueError): - s.add_marker(m1) - s.add_marker(m2) - - def test_add_markers_as_list(self): - s = Signal1D(np.arange(10)) - marker_list = [] - for i in range(12): - marker_list.append(markers.point(4, 8)) - s.add_marker(marker_list) - - def test_check_if_plot_is_not_active(self): - s = Signal1D(np.arange(100).reshape([10,10])) - m = markers.vertical_line(np.arange(10)) - s.add_marker(m) - s._plot.close() - s.add_marker(m) - - -class Test_permanent_markers: - - def test_add_permanent_marker(self): - s = Signal1D(np.arange(10)) - m = markers.point(x=5, y=5) - s.add_marker(m, permanent=True) - assert list(s.metadata.Markers)[0][1] == m - - def test_add_permanent_marker_not_plot(self): - s = Signal1D(np.arange(10)) - m = markers.point(x=5, y=5) - s.add_marker(m, permanent=True, plot_marker=False) - assert list(s.metadata.Markers)[0][1] == m - - def test_remove_permanent_marker_name(self): - s = Signal1D(np.arange(10)) - m = markers.point(x=5, y=5) - m.name = 'test' - s.add_marker(m, permanent=True) - assert list(s.metadata.Markers)[0][1] == m - del s.metadata.Markers.test - assert len(list(s.metadata.Markers)) == 0 - - def test_permanent_marker_names(self): - s = Signal1D(np.arange(10)) - m0 = markers.point(x=5, y=5) - m1 = markers.point(x=5, y=5) - m0.name = 'test' - m1.name = 'test' - s.add_marker(m0, permanent=True) - s.add_marker(m1, permanent=True) - assert s.metadata.Markers.test == m0 - assert m0.name == 'test' - assert s.metadata.Markers.test1 == m1 - assert m1.name == 'test1' - - def test_add_permanent_marker_twice(self): - s = Signal1D(np.arange(10)) - m = markers.point(x=5, y=5) - s.add_marker(m, permanent=True) - with pytest.raises(ValueError): - s.add_marker(m, permanent=True) - - def test_add_permanent_marker_twice_different_signal(self): - s0 = Signal1D(np.arange(10)) - s1 = Signal1D(np.arange(10)) - m = markers.point(x=5, y=5) - s0.add_marker(m, permanent=True) - with pytest.raises(ValueError): - s1.add_marker(m, permanent=True) - - def test_add_several_permanent_markers(self): - s = Signal1D(np.arange(10)) - m_point = markers.point(x=5, y=5) - m_line = markers.line_segment(x1=5, x2=10, y1=5, y2=10) - m_vline = markers.vertical_line(x=5) - m_vline_segment = markers.vertical_line_segment(x=4, y1=3, y2=6) - m_hline = markers.horizontal_line(y=5) - m_hline_segment = markers.horizontal_line_segment(x1=1, x2=9, y=5) - m_rect = markers.rectangle(x1=1, x2=3, y1=5, y2=10) - m_text = markers.text(x=1, y=5, text="test") - s.add_marker(m_point, permanent=True) - s.add_marker(m_line, permanent=True) - s.add_marker(m_vline, permanent=True) - s.add_marker(m_vline_segment, permanent=True) - s.add_marker(m_hline, permanent=True) - s.add_marker(m_hline_segment, permanent=True) - s.add_marker(m_rect, permanent=True) - s.add_marker(m_text, permanent=True) - assert len(list(s.metadata.Markers)) == 8 - with pytest.raises(ValueError): - s.add_marker(m_rect, permanent=True) - - def test_add_markers_as_list(self): - s = Signal1D(np.arange(10)) - marker_list = [] - for i in range(10): - marker_list.append(markers.point(1, 2)) - s.add_marker(marker_list, permanent=True) - assert len(s.metadata.Markers) == 10 - - def test_add_markers_as_list_add_same_twice(self): - s = Signal1D(np.arange(10)) - marker_list = [] - for i in range(10): - marker_list.append(markers.point(1, 2)) - s.add_marker(marker_list, permanent=True) - with pytest.raises(ValueError): - s.add_marker(marker_list, permanent=True) - - def test_add_markers_as_list_add_different_twice(self): - s = Signal1D(np.arange(10)) - marker_list0 = [] - for i in range(10): - marker_list0.append(markers.point(1, 2)) - s.add_marker(marker_list0, permanent=True) - assert len(s.metadata.Markers) == 10 - marker_list1 = [] - for i in range(10): - marker_list1.append(markers.point(4, 8)) - s.add_marker(marker_list1, permanent=True) - assert len(s.metadata.Markers) == 20 - - def test_add_permanent_marker_signal2d(self): - s = Signal2D(np.arange(100).reshape(10, 10)) - m = markers.point(x=5, y=5) - s.add_marker(m, permanent=True) - assert list(s.metadata.Markers)[0][1] == m - - def test_deepcopy_permanent_marker(self): - x, y, color, name = 2, 9, 'blue', 'test_point' - s = Signal2D(np.arange(100).reshape(10, 10)) - m = markers.point(x=x, y=y, color=color) - m.name = name - s.add_marker(m, permanent=True) - s1 = s.deepcopy() - m1 = s1.metadata.Markers.get_item(name) - assert m.get_data_position('x1') == m1.get_data_position('x1') - assert m.get_data_position('y1') == m1.get_data_position('y1') - assert m.name == m1.name - assert m.marker_properties['color'] == m1.marker_properties['color'] - - def test_dict2marker(self): - m_point0 = markers.point(x=5, y=5) - m_point1 = markers.point(x=(5, 10), y=(1, 5)) - m_line = markers.line_segment(x1=5, x2=10, y1=5, y2=10) - m_vline = markers.vertical_line(x=5) - m_vline_segment = markers.vertical_line_segment(x=4, y1=3, y2=6) - m_hline = markers.horizontal_line(y=5) - m_hline_segment = markers.horizontal_line_segment(x1=1, x2=9, y=5) - m_rect = markers.rectangle(x1=1, x2=3, y1=5, y2=10) - m_text = markers.text(x=1, y=5, text="test") - - m_point0_new = dict2marker(m_point0._to_dictionary(), m_point0.name) - m_point1_new = dict2marker(m_point1._to_dictionary(), m_point1.name) - m_line_new = dict2marker(m_line._to_dictionary(), m_line.name) - m_vline_new = dict2marker(m_vline._to_dictionary(), m_vline.name) - m_vline_segment_new = dict2marker( - m_vline_segment._to_dictionary(), m_vline_segment.name) - m_hline_new = dict2marker(m_hline._to_dictionary(), m_hline.name) - m_hline_segment_new = dict2marker( - m_hline_segment._to_dictionary(), m_hline_segment.name) - m_rect_new = dict2marker(m_rect._to_dictionary(), m_rect.name) - m_text_new = dict2marker(m_text._to_dictionary(), m_text.name) - - m_point0_dict = sanitize_dict(m_point0._to_dictionary()) - m_point1_dict = sanitize_dict(m_point1._to_dictionary()) - m_line_dict = sanitize_dict(m_line._to_dictionary()) - m_vline_dict = sanitize_dict(m_vline._to_dictionary()) - m_vline_segment_dict = sanitize_dict(m_vline_segment._to_dictionary()) - m_hline_dict = sanitize_dict(m_hline._to_dictionary()) - m_hline_segment_dict = sanitize_dict(m_hline_segment._to_dictionary()) - m_rect_dict = sanitize_dict(m_rect._to_dictionary()) - m_text_dict = sanitize_dict(m_text._to_dictionary()) - - m_point0_new_dict = sanitize_dict(m_point0_new._to_dictionary()) - m_point1_new_dict = sanitize_dict(m_point1_new._to_dictionary()) - m_line_new_dict = sanitize_dict(m_line_new._to_dictionary()) - m_vline_new_dict = sanitize_dict(m_vline_new._to_dictionary()) - m_vline_segment_new_dict = sanitize_dict( - m_vline_segment_new._to_dictionary()) - m_hline_new_dict = sanitize_dict(m_hline_new._to_dictionary()) - m_hline_segment_new_dict = sanitize_dict( - m_hline_segment_new._to_dictionary()) - m_rect_new_dict = sanitize_dict(m_rect_new._to_dictionary()) - m_text_new_dict = sanitize_dict(m_text_new._to_dictionary()) - assert m_point0_dict == m_point0_new_dict - assert m_point1_dict == m_point1_new_dict - assert m_line_dict == m_line_new_dict - assert m_vline_dict == m_vline_new_dict - assert m_vline_segment_dict == m_vline_segment_new_dict - assert m_hline_dict == m_hline_new_dict - assert m_hline_segment_dict == m_hline_segment_new_dict - assert m_rect_dict == m_rect_new_dict - assert m_text_dict == m_text_new_dict - - -def _test_plot_rectange_markers(): - # Create test image 100x100 pixels: - im = Signal2D(np.arange(100).reshape([10, 10])) - - # Add four line markers: - m1 = markers.line_segment( - x1=2, y1=2, x2=7, y2=2, color='red', linewidth=3) - m2 = markers.line_segment( - x1=2, y1=2, x2=2, y2=7, color='red', linewidth=3) - m3 = markers.line_segment( - x1=2, y1=7, x2=7, y2=7, color='red', linewidth=3) - m4 = markers.line_segment( - x1=7, y1=2, x2=7, y2=7, color='red', linewidth=3) - - # Add rectangle marker at same position: - m = markers.rectangle(x1=2, x2=7, y1=2, y2=7, - linewidth=4, color='blue', ls='dotted') - - # Plot image and add markers to img: - im.plot() - im.add_marker(m) - im.add_marker(m1) - im.add_marker(m2) - im.add_marker(m3) - im.add_marker(m4) - return im - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) -def test_plot_rectange_markers(): - im = _test_plot_rectange_markers() - return im._plot.signal_plot.figure - - -@update_close_figure() -def test_plot_rectange_markers_close(): - return _test_plot_rectange_markers() # return for @update_close_figure - - -def _test_plot_point_markers(): - width = 100 - data = np.arange(width * width).reshape((width, width)) - s = Signal2D(data) - - x, y = 10 * np.arange(4), 15 * np.arange(4) - color = ['yellow', 'green', 'red', 'blue'] - for xi, yi, c in zip(x, y, color): - m = markers.point(x=xi, y=yi, color=c) - s.add_marker(m) - return s - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) -def test_plot_point_markers(): - s = _test_plot_point_markers() - return s._plot.signal_plot.figure - - -@update_close_figure() -def test_plot_point_markers_close(): - return _test_plot_point_markers() - - -def _test_plot_text_markers(): - s = Signal1D(np.arange(100).reshape([10, 10])) - s.plot(navigator='spectrum') - for i in range(s.axes_manager.shape[0]): - m = markers.text(y=s.sum(-1).data[i] + 5, x=i, text='abcdefghij'[i]) - s.add_marker(m, plot_on_signal=False) - x = s.axes_manager.shape[-1] / 2 # middle of signal plot - m = markers.text(x=x, y=s.inav[x].data + 2, text=[i for i in 'abcdefghij']) - s.add_marker(m) - return s - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) -def test_plot_text_markers_nav(): - s = _test_plot_text_markers() - return s._plot.navigator_plot.figure - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) -def test_plot_text_markers_sig(): - s = _test_plot_text_markers() - return s._plot.signal_plot.figure - - -@update_close_figure() -def test_plot_text_markers_close(): - return _test_plot_text_markers() - - -def _test_plot_line_markers(): - im = Signal2D(np.arange(100 * 100).reshape((100, 100))) - m0 = markers.vertical_line_segment(x=20, y1=30, y2=70, linewidth=4, - color='red', linestyle='dotted') - im.add_marker(m0) - m1 = markers.horizontal_line_segment(x1=30, x2=20, y=80, linewidth=8, - color='blue', linestyle='-') - im.add_marker(m1) - m2 = markers.vertical_line(50, linewidth=12, color='green') - im.add_marker(m2) - m3 = markers.horizontal_line(50, linewidth=10, color='yellow') - im.add_marker(m3) - return im - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) -def test_plot_line_markers(): - im = _test_plot_line_markers() - return im._plot.signal_plot.figure - - -@update_close_figure() -def test_plot_line_markers_close(): - return _test_plot_line_markers() - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) -def test_plot_eds_lines(): - a = EDS_TEM_Spectrum() - s = stack([a, a * 5]) - s.plot(True) - s.axes_manager.navigation_axes[0].index = 1 - return s._plot.signal_plot.figure - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl, - filename='test_plot_eds_lines.png') -def test_plot_xray_lines(): - # It should be the same image as with previous test (test_plot_eds_lines) - a = EDS_TEM_Spectrum() - s = stack([a, a * 5]) - s.plot() - s._plot_xray_lines(xray_lines=True) - s.axes_manager.navigation_axes[0].index = 1 - return s._plot.signal_plot.figure - - -def test_plot_eds_lines_not_in_range(caplog): - s = EDS_TEM_Spectrum().isig[5.0:8.0] - s.plot() - with caplog.at_level(logging.WARNING): - s._plot_xray_lines(xray_lines=['Pt_Ka']) - - assert "Pt_Ka is not in the data energy range." in caplog.text - - -def test_plot_eds_lines_background(): - s = EDS_TEM_Spectrum().isig[5.0:8.0] - s.plot() - bw = s.estimate_background_windows() - s._plot_xray_lines(background_windows=bw) - - -def test_plot_add_background_windows(): - s = EDS_TEM_Spectrum().isig[5.0:8.0] - s.plot() - bw = s.estimate_background_windows() - s._add_background_windows_markers(bw) - # Add integration windows - iw = s.estimate_integration_windows(windows_width=2.0, xray_lines=['Fe_Ka']) - s._add_vertical_lines_groups(iw, linestyle='--') - - -def test_iterate_markers(): - from skimage.feature import peak_local_max - import scipy.misc - ims = BaseSignal(scipy.misc.face()).as_signal2D([1, 2]) - index = np.array([peak_local_max(im.data, min_distance=100, - num_peaks=4) for im in ims]) - # Add multiple markers - for i in range(4): - xs = index[:, i, 1] - ys = index[:, i, 0] - m = markers.point(x=xs, y=ys, color='red') - ims.add_marker(m, plot_marker=True, permanent=True) - m = markers.text(x=10 + xs, y=10 + ys, text=str(i), color='k') - ims.add_marker(m, plot_marker=True, permanent=True) - xs = index[:, :, 1] - ys = index[:, :, 0] - m = markers.rectangle(np.min(xs, 1), - np.min(ys, 1), - np.max(xs, 1), - np.max(ys, 1), - color='green') - ims.add_marker(m, plot_marker=True, permanent=True) - - for im in ims: - m_original = ims.metadata.Markers - m_iterated = im.metadata.Markers - for key in m_original.keys(): - mo = m_original[key] - mi = m_iterated[key] - assert mo.__class__.__name__ == mi.__class__.__name__ - assert mo.name == mi.name - assert mo.get_data_position('x1') == mi.get_data_position('x1') - assert mo.get_data_position('y1') == mi.get_data_position('y1') - assert mo.get_data_position('text') == mi.get_data_position('text') - assert mo.marker_properties['color'] == \ - mi.marker_properties['color'] - - -@update_close_figure() -def test_plot_eds_markers_close(): - s = EDS_TEM_Spectrum() - s.plot(True) - return s - - -def test_plot_eds_markers_no_energy(): - s = EDS_TEM_Spectrum() - del s.metadata.Acquisition_instrument.TEM.beam_energy - s.plot(True) - - -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) -def test_plot_eels_labels(): - s = get_core_loss_eels_line_scan_signal(True, random_state=10) - s.add_elements(['Cr']) - s.plot(plot_edges=True) - return s._plot.signal_plot.figure - - -def test_plot_eels_labels_nav(): - s = get_core_loss_eels_line_scan_signal(True) - s.add_elements(['Cr', 'Fe']) - s.plot(plot_edges=True) - s.axes_manager.indices = (10, ) - s._plot.close() diff --git a/hyperspy/tests/drawing/test_plot_model.py b/hyperspy/tests/drawing/test_plot_model.py index 908778eed9..30bcb27355 100644 --- a/hyperspy/tests/drawing/test_plot_model.py +++ b/hyperspy/tests/drawing/test_plot_model.py @@ -1,44 +1,44 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . + +from pathlib import Path import numpy as np import pytest -from pathlib import Path import hyperspy.api as hs from hyperspy.components1d import Gaussian -from hyperspy.exceptions import VisibleDeprecationWarning -from hyperspy.signals import EELSSpectrum, Signal1D +from hyperspy.signals import Signal1D my_path = Path(__file__).resolve().parent -baseline_dir = 'plot_model' +baseline_dir = "plot_model" default_tol = 2.0 def create_ll_signal(signal_shape=1000): offset = 0 - zlp_param = {'A': 10000.0, 'centre': 0.0 + offset, 'sigma': 15.0} + zlp_param = {"A": 10000.0, "centre": 0.0 + offset, "sigma": 15.0} zlp = Gaussian(**zlp_param) - plasmon_param = {'A': 2000.0, 'centre': 200.0 + offset, 'sigma': 75.0} + plasmon_param = {"A": 2000.0, "centre": 200.0 + offset, "sigma": 75.0} plasmon = Gaussian(**plasmon_param) axis = np.arange(signal_shape) data = zlp.function(axis) + plasmon.function(axis) - ll = EELSSpectrum(data) + ll = Signal1D(data) ll.axes_manager[-1].offset = -offset ll.axes_manager[-1].scale = 0.1 return ll @@ -50,27 +50,29 @@ def create_ll_signal(signal_shape=1000): scale = 0.1 -def create_sum_of_gaussians(convolved=False): - param1 = {'A': A_value_gaussian[0], - 'centre': centre_value_gaussian[0] / scale, - 'sigma': sigma_value_gaussian[0] / scale} +def create_sum_of_gaussians(): + param1 = { + "A": A_value_gaussian[0], + "centre": centre_value_gaussian[0] / scale, + "sigma": sigma_value_gaussian[0] / scale, + } gs1 = Gaussian(**param1) - param2 = {'A': A_value_gaussian[1], - 'centre': centre_value_gaussian[1] / scale, - 'sigma': sigma_value_gaussian[1] / scale} + param2 = { + "A": A_value_gaussian[1], + "centre": centre_value_gaussian[1] / scale, + "sigma": sigma_value_gaussian[1] / scale, + } gs2 = Gaussian(**param2) - param3 = {'A': A_value_gaussian[2], - 'centre': centre_value_gaussian[2] / scale, - 'sigma': sigma_value_gaussian[2] / scale} + param3 = { + "A": A_value_gaussian[2], + "centre": centre_value_gaussian[2] / scale, + "sigma": sigma_value_gaussian[2] / scale, + } gs3 = Gaussian(**param3) axis = np.arange(1000) data = gs1.function(axis) + gs2.function(axis) + gs3.function(axis) - if convolved: - to_convolved = create_ll_signal(data.shape[0]).data - data = np.convolve(data, to_convolved) / sum(to_convolved) - s = Signal1D(data[:1000]) s.axes_manager[-1].scale = scale return s @@ -78,21 +80,16 @@ def create_sum_of_gaussians(convolved=False): @pytest.mark.parametrize("binned", [True, False]) @pytest.mark.parametrize("plot_component", [True, False]) -@pytest.mark.parametrize("convolved", [True, False]) -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) -def test_plot_gaussian_eelsmodel(convolved, plot_component, binned): - s = create_sum_of_gaussians(convolved) - s.set_signal_type('EELS') +@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) +def test_plot_gaussian_signal1D(plot_component, binned): + s = create_sum_of_gaussians() s.axes_manager[-1].is_binned == binned - s.metadata.General.title = 'Convolved: {}, plot_component: {}, binned: {}'.format( - convolved, plot_component, binned) + s.metadata.General.title = "plot_component: {}, binned: {}".format( + plot_component, binned + ) - ll = create_ll_signal(1000) if convolved else None - - s.set_microscope_parameters(200, 20, 50) s.axes_manager[-1].is_binned = binned - m = s.create_model(auto_background=False, ll=ll) + m = s.create_model() m.extend([Gaussian(), Gaussian(), Gaussian()]) @@ -102,8 +99,7 @@ def set_gaussian(gaussian, centre, sigma): gaussian.sigma.value = sigma gaussian.sigma.free = False - for gaussian, centre, sigma in zip(m, centre_value_gaussian, - sigma_value_gaussian): + for gaussian, centre, sigma in zip(m, centre_value_gaussian, sigma_value_gaussian): set_gaussian(gaussian, centre, sigma) m.fit() @@ -115,43 +111,19 @@ def A_value(s, component, binned): else: return component.A.value - if convolved: - np.testing.assert_almost_equal(A_value(s, m[0], binned), 0.014034, decimal=5) - np.testing.assert_almost_equal(A_value(s, m[1], binned), 0.008420, decimal=5) - np.testing.assert_almost_equal(A_value(s, m[2], binned), 0.028068, decimal=5) - else: - np.testing.assert_almost_equal(A_value(s, m[0], binned), 100.0, decimal=5) - np.testing.assert_almost_equal(A_value(s, m[1], binned), 60.0, decimal=5) - np.testing.assert_almost_equal(A_value(s, m[2], binned), 200.0, decimal=5) + np.testing.assert_almost_equal(A_value(s, m[0], binned), 100.0, decimal=5) + np.testing.assert_almost_equal(A_value(s, m[1], binned), 60.0, decimal=5) + np.testing.assert_almost_equal(A_value(s, m[2], binned), 200.0, decimal=5) return m._plot.signal_plot.figure -@pytest.mark.parametrize(("convolved"), [False, True]) -@pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) -def test_fit_EELS_convolved(convolved): - dname = my_path.joinpath('data') - with pytest.warns(VisibleDeprecationWarning): - cl = hs.load(dname.joinpath('Cr_L_cl.hspy')) - cl.axes_manager[-1].is_binned = False - cl.metadata.General.title = 'Convolved: {}'.format(convolved) - ll = None - if convolved: - with pytest.warns(VisibleDeprecationWarning): - ll = hs.load(dname.joinpath('Cr_L_ll.hspy')) - m = cl.create_model(auto_background=False, ll=ll, GOS='hydrogenic') - m.fit(kind='smart') - m.plot(plot_components=True) - return m._plot.signal_plot.figure - - def test_plot_component(): m = hs.signals.Signal1D(np.arange(100).reshape(2, 50)).create_model() m.append(hs.model.components1D.Gaussian(A=250, sigma=5, centre=20)) m.plot(plot_components=True) ax = m.signal._plot.signal_plot.ax - p = hs.model.components1D.Polynomial(order=1, legacy=False, a0=-10, a1=0) + p = hs.model.components1D.Polynomial(order=1, a0=-10, a1=0) m.append(p) assert ax.get_ylim() == (-10.1, 49.0) m.remove(0) @@ -161,3 +133,11 @@ def test_plot_component(): assert ax.get_ylim() == (-0.1, 49.0) m.append(p) m.signal._plot.close() + + +@pytest.mark.parametrize(("only_free"), [False, True]) +@pytest.mark.parametrize(("only_active"), [False, True]) +def test_plot_results(only_free, only_active): + m = hs.signals.Signal1D(np.arange(100).reshape(2, 50)).create_model() + m.append(hs.model.components1D.Gaussian(A=250, sigma=5, centre=20)) + m.plot_results(only_free=only_free, only_active=only_active) diff --git a/hyperspy/tests/drawing/test_plot_model1d.py b/hyperspy/tests/drawing/test_plot_model1d.py index c329a4d0fd..819b07b15c 100644 --- a/hyperspy/tests/drawing/test_plot_model1d.py +++ b/hyperspy/tests/drawing/test_plot_model1d.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np import pytest @@ -23,41 +23,44 @@ from hyperspy.signals import Signal1D DEFAULT_TOL = 2.0 -BASELINE_DIR = 'plot_model1d' -STYLE_PYTEST_MPL = 'default' +BASELINE_DIR = "plot_model1d" +STYLE_PYTEST_MPL = "default" class TestModelPlot: def setup_method(self, method): - s = Signal1D(np.arange(1000).reshape((10, 100))) - np.random.seed(0) - s.add_poissonian_noise() + s = Signal1D(np.arange(1000, dtype=np.int64).reshape((10, 100))) + s.add_poissonian_noise(random_state=0) m = s.create_model() line = Expression("a * x", name="line", a=1) m.append(line) self.m = m @pytest.mark.mpl_image_compare( - baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_default_signal_plot(self): self.m.plot() return self.m._plot.signal_plot.figure @pytest.mark.mpl_image_compare( - baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_components(self): self.m.plot(plot_components=True) return self.m._plot.signal_plot.figure @pytest.mark.mpl_image_compare( - baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_disable_plot_components(self): self.m.plot(plot_components=True) self.m.disable_plot_components() return self.m._plot.signal_plot.figure @pytest.mark.mpl_image_compare( - baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_default_navigator_plot(self): self.m.plot() return self.m._plot.navigator_plot.figure @@ -65,3 +68,32 @@ def test_default_navigator_plot(self): def test_no_navigator(self): self.m.plot(navigator=None) assert self.m.signal._plot.navigator_plot is None + + @pytest.mark.parametrize("plot_residual", (False, True)) + def test_plot_events_connection(self, plot_residual): + # Check that the events connection and + # the plotting line are reset when closing + expected_connection = 1 + if plot_residual: + expected_connection += 1 + c = self.m[0] + + assert self.m._model_line is None + assert self.m._residual_line is None + assert len(c.events.active_changed.connected) == 0 + for p in c.parameters: + assert len(p.events.value_changed.connected) == 0 + + self.m.plot(plot_residual=plot_residual) + assert self.m._model_line is not None + assert (self.m._residual_line is not None) is plot_residual + assert len(c.events.active_changed.connected) == expected_connection + for p in c.parameters: + assert len(p.events.value_changed.connected) == expected_connection + + self.m._plot.close() + assert len(c.events.active_changed.connected) == 0 + for p in c.parameters: + assert len(p.events.value_changed.connected) == 0 + assert self.m._model_line is None + assert self.m._residual_line is None diff --git a/hyperspy/tests/drawing/test_plot_mva.py b/hyperspy/tests/drawing/test_plot_mva.py index 7ccdf419ed..bd0ac312cf 100644 --- a/hyperspy/tests/drawing/test_plot_mva.py +++ b/hyperspy/tests/drawing/test_plot_mva.py @@ -1,41 +1,39 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np import pytest +from packaging.version import Version from hyperspy import signals from hyperspy.misc.machine_learning.import_sklearn import sklearn_installed -baseline_dir = 'plot_mva' +baseline_dir = "plot_mva" default_tol = 2.0 class TestPlotDecomposition: - def setup_method(self, method): - np.random.seed(1) - sources = np.random.random(size=(5, 100)) - np.random.seed(1) - mixmat = np.random.random((100, 5)) + rng = np.random.default_rng(1) + sources = rng.random(size=(5, 100)) + mixmat = rng.random((100, 5)) self.s = signals.Signal1D(np.dot(mixmat, sources)) - np.random.seed(1) - self.s.add_gaussian_noise(.1) + self.s.add_gaussian_noise(0.1, random_state=rng) self.s.decomposition() self.s2 = signals.Signal1D(self.s.data.reshape(10, 10, 100)) self.s2.decomposition() @@ -43,66 +41,62 @@ def setup_method(self, method): def _generate_parameters(): parameters = [] for n in [10, 50]: - for xaxis_type in ['index', 'number']: + for xaxis_type in ["index", "number"]: for threshold in [0, 0.001]: - for xaxis_labeling in ['ordinal', 'cardinal']: - parameters.append([n, threshold, xaxis_type, - xaxis_labeling]) + for xaxis_labeling in ["ordinal", "cardinal"]: + parameters.append([n, threshold, xaxis_type, xaxis_labeling]) return parameters - @pytest.mark.parametrize(("n", "threshold", "xaxis_type", "xaxis_labeling"), - _generate_parameters()) - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) - def test_plot_explained_variance_ratio(self, n, threshold, xaxis_type, - xaxis_labeling): - ax = self.s.plot_explained_variance_ratio(n, threshold=threshold, - xaxis_type=xaxis_type, - xaxis_labeling=xaxis_labeling) + @pytest.mark.parametrize( + ("n", "threshold", "xaxis_type", "xaxis_labeling"), _generate_parameters() + ) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) + def test_plot_explained_variance_ratio( + self, n, threshold, xaxis_type, xaxis_labeling + ): + ax = self.s.plot_explained_variance_ratio( + n, threshold=threshold, xaxis_type=xaxis_type, xaxis_labeling=xaxis_labeling + ) return ax.get_figure() - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_cumulative_explained_variance_ratio(self): ax = self.s.plot_cumulative_explained_variance_ratio() return ax.get_figure() @pytest.mark.parametrize("n", [3, [3, 4]]) - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_decomposition_loadings_nav1(self, n): return self.s.plot_decomposition_loadings(n) @pytest.mark.parametrize("n", (3, [3, 4])) - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_decomposition_factors_nav1(self, n): return self.s.plot_decomposition_factors(n) - @pytest.mark.parametrize(("n", "per_row", "axes_decor"), - ((6, 3, 'all'), (8, 4, None), - ([3, 4, 5, 6], 2, 'ticks'))) - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.parametrize( + ("n", "per_row", "axes_decor"), + ((6, 3, "all"), (8, 4, None), ([3, 4, 5, 6], 2, "ticks")), + ) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_decomposition_loadings_nav2(self, n, per_row, axes_decor): - return self.s2.plot_decomposition_loadings(n, per_row=per_row, - title='Loading', - axes_decor=axes_decor) + return self.s2.plot_decomposition_loadings( + n, per_row=per_row, title="Loading", axes_decor=axes_decor + ) + @pytest.mark.skipif(not sklearn_installed, reason="sklearn not installed") class TestPlotClusterAnalysis: - def setup_method(self, method): - np.random.seed(1) + rng = np.random.default_rng(1) # Use prime numbers to avoid fluke equivalences # create 3 random clusters - n_samples=[250,100,50] - std = [1.0,2.0,0.5] + n_samples = [250, 100, 50] + std = [1.0, 2.0, 0.5] X = [] - centers = np.array([[-15.0, -15.0,-15.0], [1.0, 1.0,1.0], - [15.0, 15.0, 15.0]]) + centers = np.array([[-15.0, -15.0, -15.0], [1.0, 1.0, 1.0], [15.0, 15.0, 15.0]]) for i, (n, std) in enumerate(zip(n_samples, std)): - X.append(centers[i] + np.random.normal(scale=std, size=(n, 3))) + X.append(centers[i] + rng.normal(scale=std, size=(n, 3))) data = np.concatenate(X) @@ -111,88 +105,118 @@ def setup_method(self, method): # nav2, sig1 s2 = signals.Signal1D(data.reshape(40, 10, 3)) + import sklearn + + n_init = "auto" if Version(sklearn.__version__) >= Version("1.3") else 10 + # Run decomposition and cluster analysis s.decomposition() - s.cluster_analysis("decomposition",n_clusters=3, algorithm='kmeans', - preprocessing="minmax", random_state=0) - s.estimate_number_of_clusters("decomposition",metric="elbow") - + s.cluster_analysis( + "decomposition", + n_clusters=3, + algorithm="kmeans", + preprocessing="minmax", + random_state=0, + n_init=n_init, + ) + s.estimate_number_of_clusters( + "decomposition", + metric="elbow", + n_init=n_init, + ) + s2.decomposition() - s2.cluster_analysis("decomposition",n_clusters=3, algorithm='kmeans', - preprocessing="minmax", random_state=0) - + s2.cluster_analysis( + "decomposition", + n_clusters=3, + algorithm="kmeans", + preprocessing="minmax", + random_state=0, + n_init=n_init, + ) + data = np.zeros((2000, 5)) - data[:250*5:5, :] = 10 - data[2 + 250*5:350*5:5, :] = 2 - data[350*5:400*5, 4] = 20 + data[: 250 * 5 : 5, :] = 10 + data[2 + 250 * 5 : 350 * 5 : 5, :] = 2 + data[350 * 5 : 400 * 5, 4] = 20 # nav2, sig2 s3 = signals.Signal2D(data.reshape(20, 20, 5, 5)) s3.decomposition() - s3.cluster_analysis("decomposition",n_clusters=3, algorithm='kmeans', - preprocessing="minmax", random_state=0) - + s3.cluster_analysis( + "decomposition", + n_clusters=3, + algorithm="kmeans", + preprocessing="minmax", + random_state=0, + n_init=n_init, + ) + self.s = s self.s2 = s2 self.s3 = s3 - - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_cluster_labels_nav1_sig1(self): return self.s.plot_cluster_labels() - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_cluster_signals_nav1_sig1(self): return self.s.plot_cluster_signals() - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_cluster_distances_nav1_sig1(self): return self.s.plot_cluster_distances() - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_cluster_labels_nav2_sig1(self): return self.s2.plot_cluster_labels() - - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_cluster_signals_nav2_sig1(self): return self.s2.plot_cluster_signals() - - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) def test_plot_cluster_labels_nav2_sig2(self): return self.s3.plot_cluster_labels() -# @pytest.mark.mpl_image_compare( -# baseline_dir=baseline_dir, tolerance=default_tol) -# def test_plot_cluster_distances_nav2_sig2(self): -# return self.s3.plot_cluster_distances() - - # @pytest.mark.skipif(sys.platform == "win32", - # reason="does not run on windows 32") - # @pytest.mark.mpl_image_compare( - # baseline_dir=baseline_dir, tolerance=default_tol) - # def test_plot_cluster_signals_nav2_sig2(self): - # return self.s3.plot_cluster_signals() - # @pytest.mark.skipif(sys.platform == "win32", - # reason="does not run on windows 32") - @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol) + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol * 5) + def test_plot_cluster_distances_nav2_sig2(self): + return self.s3.plot_cluster_distances() + + @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=default_tol) + def test_plot_cluster_signals_nav2_sig2(self): + return self.s3.plot_cluster_signals() + def test_plot_cluster_metric(self): - ax = self.s.plot_cluster_metric() - return ax.get_figure() + self.s.plot_cluster_metric() def test_except_nocluster_metric(self): with pytest.raises(ValueError): self.s2.plot_cluster_metric() +def test_plot_signal_dimension3(): + rng = np.random.default_rng(1) + sources = rng.random(size=(5, 100)) + mixmat = rng.random((100, 5)) + s = signals.Signal1D(np.dot(mixmat, sources)) + s.add_gaussian_noise(0.1, random_state=rng) + s2 = signals.Signal1D(s.data.reshape(2, 5, 10, 100)) + + s3 = s2.transpose(signal_axes=3) + s3.decomposition() + s3.plot_decomposition_results() + + s4 = s2.transpose(signal_axes=1) + s4.decomposition() + s4.plot_decomposition_results() + + def test_plot_without_decomposition(): - sources = np.random.random(size=(5, 100)) - mixmat = np.random.random((100, 5)) + rng = np.random.default_rng(1) + sources = rng.random(size=(5, 100)) + mixmat = rng.random((100, 5)) s = signals.Signal1D(np.dot(mixmat, sources)) with pytest.raises(RuntimeError): s.plot_decomposition_factors() diff --git a/hyperspy/tests/drawing/test_plot_roi_map.py b/hyperspy/tests/drawing/test_plot_roi_map.py new file mode 100644 index 0000000000..47c04fbf3f --- /dev/null +++ b/hyperspy/tests/drawing/test_plot_roi_map.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- +# Copyright 2007-2022 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import matplotlib.pyplot as plt +import numpy as np +import pytest + +import hyperspy.api as hs +from hyperspy.utils.plot import plot_roi_map + +BASELINE_DIR = "plot_roi_map" +DEFAULT_TOL = 2.0 +STYLE_PYTEST_MPL = "default" + + +# params different shapes of data, sig, nav dims +@pytest.fixture( + params=[(1, 1), (1, 2), (2, 1), (2, 2)], ids=lambda sn: f"s{sn[0]}n{sn[1]}" +) +def test_signal(request): + sig_dims, nav_dims = request.param + + sig_size = 13 + nav_size = 3 + + sig_shape = (sig_size,) * sig_dims + nav_shape = (nav_size,) * nav_dims + shape = (*nav_shape, *sig_shape) + test_data = np.zeros(shape) + + axes = [] + + for _, name in zip(range(nav_dims), "xyz"): + axes.append( + {"name": name, "size": nav_size, "offset": 0, "scale": 1, "units": "um"} + ) + + for _, name in zip(range(sig_dims), ["Ix", "Iy", "Iz"]): + axes.append( + { + "name": name, + "size": sig_size, + "offset": 0, + "scale": 1, + "units": "nm", + } + ) + sig = hs.signals.BaseSignal( + test_data, + axes=axes, + ) + + sig = sig.transpose(sig_dims) + + sig.inav[0, ...].isig[0, ...] = 1 + sig.inav[1, ...].isig[2, ...] = 2 + sig.inav[2, ...].isig[4, ...] = 3 + + return sig + + +def test_args_wrong_shape(): + rng = np.random.default_rng() + + sig2 = hs.signals.BaseSignal(rng.random(size=(2, 2))) + + no_sig = sig2.transpose(0) + no_nav = sig2.transpose(2) + + sig5 = hs.signals.BaseSignal(rng.random(size=(2, 2, 2, 2, 2))) + three_sigs = sig5.transpose(3) + three_navs = sig5.transpose(2) + + with pytest.raises(ValueError): + plot_roi_map(no_sig, 1) + + # navigation needed + with pytest.raises(ValueError): + plot_roi_map(no_nav, [hs.roi.Point1DROI(0)]) + + # unsupported signal + for sig in [no_nav, three_sigs]: + with pytest.raises(ValueError): + plot_roi_map(sig, [hs.roi.Point1DROI(0)]) + + # value error also raised because 1D ROI not right shape + with pytest.raises(ValueError): + plot_roi_map(sig, [hs.roi.Point1DROI(0)]) + + # 3 navigation works fine + plot_roi_map(three_navs, [hs.roi.CircleROI()]) + + # 3 navigation works fine + plot_roi_map(three_navs) + + +def test_passing_rois(): + s = hs.signals.Signal1D(np.arange(100).reshape(10, 10)) + int_rois, int_roi_sums = plot_roi_map(s, 3) + + rois, roi_sums = plot_roi_map(s, int_rois) + + assert rois is int_rois + + # passing the rois rather than generating own should yield same results + assert int_roi_sums is not roi_sums + assert int_roi_sums == roi_sums + + +def test_roi_positioning(): + s = hs.signals.Signal1D(np.arange(100).reshape(10, 10)) + rois, _ = plot_roi_map(s, 1) + + assert len(rois) == 1 + + assert rois[0].left == 0 + assert rois[0].right == 4.5 + + rois, _ = plot_roi_map(s, 2) + + assert len(rois) == 2 + assert rois[0].left == 0 + assert rois[0].right == 2.25 + assert rois[1].left == 2.25 + assert rois[1].right == 4.5 + + # no overlap + assert rois[0].right <= rois[1].left + + rois, _ = plot_roi_map(s, 3) + + assert len(rois) == 3 + assert rois[0].left == 0 + assert rois[0].right == 1.5 + assert rois[1].left == 1.5 + assert rois[1].right == 3 + assert rois[2].left == 3 + assert rois[2].right == 4.5 + + # no overlap + assert rois[0].right <= rois[1].left and rois[1].right <= rois[2].left + + +@pytest.mark.parametrize("nrois", [1, 2, 3]) +def test_navigator(test_signal, nrois): + rois, roi_sums = plot_roi_map(test_signal, nrois) + assert len(rois) == nrois + + +def test_roi_sums(): + # check that the sum is correct + s = hs.signals.Signal1D(np.arange(100).reshape(10, 10)) + + rois, roi_sums = hs.plot.plot_roi_map(s, 2) + + for roi, roi_sum in zip(rois, roi_sums): + np.testing.assert_allclose(roi(s).sum(-1), roi_sum.data) + + +def test_circle_roi(): + data = np.zeros((2, 2, 7, 7)) + data[-1, -1, 0, 0] = 10000 + s = hs.signals.Signal2D(data) + roi = hs.roi.CircleROI(cx=3, cy=3, r=4, r_inner=0) + + rois, roi_sums = hs.plot.plot_roi_map(s, rois=[roi]) + roi_sum = roi_sums[0] + + assert not np.any(np.isin(10000, roi_sum)) + assert np.allclose(roi_sum, np.zeros((2, 2))) + + roi.cx = 4 # force update + roi.cx = 3 + + # no change expected + assert not np.any(np.isin(10000, roi_sum)) + assert np.allclose(roi_sum, np.zeros((2, 2))) + + roi.cx = 0 + roi.cy = 0 + + # check can actually find 10000 + assert np.any(np.isin(10000, roi_sum)) + + +def test_pass_ROI(): + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal2D(data) + + roi = hs.roi.CircleROI() + hs.plot.plot_roi_map(s, rois=roi) + + +def test_color(): + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal2D(data) + + # same number + hs.plot.plot_roi_map(s, rois=3, color=["C0", "C1", "C2"]) + + with pytest.raises(ValueError): + hs.plot.plot_roi_map(s, rois=3, color=["C0", "C1"]) + + with pytest.raises(ValueError): + hs.plot.plot_roi_map(s, rois=3, color=["C0", "C1", "C2", "C3"]) + + with pytest.raises(ValueError): + hs.plot.plot_roi_map(s, rois=1, color=["unvalid_cmap"]) + + +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL +) +@pytest.mark.parametrize("cmap", (None, "gray")) +def test_cmap_image(cmap): + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal1D(data) + + rois, roi_sums = hs.plot.plot_roi_map(s, rois=2, cmap=cmap) + + return roi_sums[0]._plot.signal_plot.figure + + +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL +) +@pytest.mark.parametrize("color", (None, ["r", "b"])) +def test_color_image(color): + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal1D(data) + + rois, roi_sums = hs.plot.plot_roi_map(s, rois=2, color=color) + + return roi_sums[0]._plot.signal_plot.figure + + +def test_close(): + # We can't test with `single_figure=True`, because `plt.close()` + # doesn't call on close callback. + # https://github.com/matplotlib/matplotlib/issues/18609 + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal1D(data) + + rois, roi_sums = hs.plot.plot_roi_map(s, rois=3) + # check that it closes and remove the roi from the figure + for roi, roi_sum in zip(rois, roi_sums): + assert len(roi.signal_map) == 1 + roi_sum._plot.close() + assert len(roi.signal_map) == 0 + + +def test_cmap_error(): + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal2D(data) + + # same number + hs.plot.plot_roi_map(s, rois=3, cmap=["C0", "C1", "C2"]) + + with pytest.raises(ValueError): + hs.plot.plot_roi_map(s, rois=3, cmap=["C0", "C1"]) + + with pytest.raises(ValueError): + hs.plot.plot_roi_map(s, rois=3, cmap=["C0", "C1", "C2", "C3"]) + + +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL +) +@pytest.mark.parametrize("cmap", (None, "gray")) +def test_single_figure_image(cmap): + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal1D(data) + + hs.plot.plot_roi_map(s, rois=3, cmap=cmap, single_figure=True, scalebar=False) + + return plt.gcf() + + +def test_single_figure_kwargs(): + pytest.importorskip("matplotlib", minversion="3.8") + + rng = np.random.default_rng(0) + data = rng.random(size=(10, 10, 50)) + s = hs.signals.Signal1D(data) + + title = "A title" + hs.plot.plot_roi_map( + s, rois=3, single_figure=True, single_figure_kwargs={"suptitle": title} + ) + fig = plt.gcf() + assert fig.get_suptitle() == title + + s2 = s.T + legend = ["Custom text 0", "Custom text 1"] + hs.plot.plot_roi_map( + s2, rois=2, single_figure=True, single_figure_kwargs={"legend": legend} + ) + + ax = plt.gca() + texts = ax.get_legend().get_texts() + for text, expected_text in zip(texts, legend[::-1]): + assert text.get_text() == expected_text + + +@pytest.mark.parametrize("color", (None, ["C0", "C1"], ["r", "b"])) +def test_single_figure_spectra(color): + rng = np.random.default_rng(0) + data = rng.random(size=(50, 10, 10)) + s = hs.signals.Signal2D(data) + + hs.plot.plot_roi_map(s, rois=2, color=color, single_figure=True) + if color is None: + color = ["b", "g"] + + ax = plt.gca() + for line, color_ in zip(ax.lines, color): + assert line.get_color() == color_ diff --git a/hyperspy/tests/drawing/test_plot_roi_widgets.py b/hyperspy/tests/drawing/test_plot_roi_widgets.py index c37f414948..8edb5b97e5 100644 --- a/hyperspy/tests/drawing/test_plot_roi_widgets.py +++ b/hyperspy/tests/drawing/test_plot_roi_widgets.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np import pytest @@ -22,9 +22,9 @@ from hyperspy.signals import Signal1D, Signal2D from hyperspy.utils import roi -BASELINE_DIR = 'plot_roi' +BASELINE_DIR = "plot_roi" DEFAULT_TOL = 2.0 -STYLE_PYTEST_MPL = 'default' +STYLE_PYTEST_MPL = "default" def _transpose_space(space, im): @@ -45,8 +45,7 @@ def _transpose_space(space, im): } -class TestPlotROI(): - +class TestPlotROI: def setup_method(self, method): # Create test image 100x100 pixels: im = Signal2D(np.arange(50000).reshape([10, 50, 100])) @@ -55,64 +54,125 @@ def setup_method(self, method): im.axes_manager[2].scale = 1e-3 self.im = im - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_point1D_axis_0(self): + im = self.im + im.plot() + p = roi.Point1DROI(0.5) + p.add_widget( + signal=im, + axes=[ + 0, + ], + color="cyan", + ) + return im._plot.navigator_plot.figure + + def test_plot_point1D_axis_0_non_iterable(self): self.im.plot() p = roi.Point1DROI(0.5) - p.add_widget(signal=self.im, axes=[0, ], color="cyan") - return self.im._plot.navigator_plot.figure + p.add_widget(signal=self.im, axes=0, color="cyan") - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_point1D_axis_1(self): - self.im.plot() + im = self.im + im.plot() p = roi.Point1DROI(0.05) - p.add_widget(signal=self.im, axes=[1, ], color="cyan") - return self.im._plot.signal_plot.figure - - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + p.add_widget( + signal=im, + axes=[ + 1, + ], + color="cyan", + ) + return im._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_point1D_axis_2(self): - self.im.plot() + im = self.im + im.plot() p = roi.Point1DROI(0.005) - p.add_widget(signal=self.im, axes=[2, ], color="cyan") - return self.im._plot.signal_plot.figure - - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + p.add_widget( + signal=im, + axes=[ + 2, + ], + color="cyan", + ) + return im._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_spanroi_axis_0(self): - self.im.plot() + im = self.im + im.plot() p = roi.SpanROI(0.5, 0.7) - p.add_widget(signal=self.im, axes=[0, ], color="cyan") - return self.im._plot.navigator_plot.figure + p.add_widget( + signal=im, + axes=[ + 0, + ], + color="cyan", + ) + return im._plot.navigator_plot.figure def test_plot_spanroi_close(self): - self.im.plot() + im = self.im + im.plot() p = roi.SpanROI(0.5, 0.7) - p.add_widget(signal=self.im, axes=[0, ], color="cyan") + p.add_widget( + signal=im, + axes=[ + 0, + ], + color="cyan", + ) for widget in p.widgets: widget.close() - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_spanroi_axis_1(self): - self.im.plot() + im = self.im + im.plot() p = roi.SpanROI(0.05, 0.07) - p.add_widget(signal=self.im, axes=[1, ], color="cyan") - return self.im._plot.signal_plot.figure - - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + p.add_widget( + signal=im, + axes=[ + 1, + ], + color="cyan", + ) + return im._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_spanroi_axis_2(self): - self.im.plot() + im = self.im + im.plot() p = roi.SpanROI(0.005, 0.007) - p.add_widget(signal=self.im, axes=[2, ], color="cyan") - return self.im._plot.signal_plot.figure + p.add_widget( + signal=im, + axes=[ + 2, + ], + color="cyan", + ) + return im._plot.signal_plot.figure @pytest.mark.parametrize("space", ("signal", "navigation")) - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_point2D(self, space): objs = _transpose_space(im=self.im, space=space) p = roi.Point2DROI(0.05, 0.01) @@ -120,8 +180,9 @@ def test_plot_point2D(self, space): return objs["figure"] @pytest.mark.parametrize("space", ("signal", "navigation")) - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_circle_roi(self, space): self.im.axes_manager[2].scale = 0.01 objs = _transpose_space(im=self.im, space=space) @@ -132,8 +193,9 @@ def test_plot_circle_roi(self, space): return objs["figure"] @pytest.mark.parametrize("space", ("signal", "navigation")) - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_rectangular_roi(self, space): objs = _transpose_space(im=self.im, space=space) p = roi.RectangularROI(left=0.01, top=0.01, right=0.1, bottom=0.03) @@ -149,8 +211,9 @@ def test_plot_rectangular_roi_remove(self, render_figure): p.remove_widget(im, render_figure=render_figure) @pytest.mark.parametrize("space", ("signal", "navigation")) - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_line2d_roi(self, space): im = self.im objs = _transpose_space(im=im, space=space) @@ -161,13 +224,14 @@ def test_plot_line2d_roi(self, space): p2.add_widget(signal=objs["im"], axes=objs["axes"]) return objs["figure"] - @pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) def test_plot_line2d_roi_linewidth(self): im = self.im for axis in im.axes_manager.signal_axes: axis.scale = 0.1 - objs = _transpose_space(im=im, space='signal') + objs = _transpose_space(im=im, space="signal") p = roi.Line2DROI(x1=0.3, y1=0.5, x2=6.0, y2=3.0, linewidth=0.5) p.add_widget(signal=objs["im"], axes=objs["axes"]) @@ -175,15 +239,15 @@ def test_plot_line2d_roi_linewidth(self): p2.add_widget(signal=objs["im"], axes=objs["axes"]) widget2 = list(p2.widgets)[0] widget2.decrease_size() - assert widget2.size == (0.0, ) + assert widget2.size == (0.0,) widget2.increase_size() - assert widget2.size == (0.1, ) + assert widget2.size == (0.1,) p3 = roi.Line2DROI(x1=3.5, y1=0.5, x2=9.5, y2=3.0, linewidth=0.1) p3.add_widget(signal=objs["im"], axes=objs["axes"]) widget3 = list(p3.widgets)[0] widget3.decrease_size() - assert widget3.size == (0.0, ) + assert widget3.size == (0.0,) return objs["figure"] @@ -193,8 +257,14 @@ def test_error_message(): im.plot() im._plot.close() p = roi.Point1DROI(0.5) - with pytest.raises(Exception, match='does not have an active plot.'): - p.add_widget(signal=im, axes=[0, ], color="cyan") + with pytest.raises(Exception, match="does not have an active plot."): + p.add_widget( + signal=im, + axes=[ + 0, + ], + color="cyan", + ) def test_remove_rois(): @@ -204,13 +274,13 @@ def test_remove_rois(): s.plot() s2.plot() - s_roi = r.interactive(s) - s2_roi = r.interactive(s2) + _ = r.interactive(s) + _ = r.interactive(s2) r.remove_widget(s) -@pytest.mark.parametrize('snap', [True, False]) +@pytest.mark.parametrize("snap", [True, False]) def test_snapping_axis_values(snap): s = Signal2D(np.arange(100).reshape(10, 10)) s.axes_manager[0].offset = 5 @@ -218,3 +288,17 @@ def test_snapping_axis_values(snap): r = roi.Line2DROI(x1=6, y1=0, x2=12, y2=4, linewidth=0) s.plot() _ = r.interactive(s, snap=snap) + + +def test_plot_span_roi_changed_event(): + s = Signal1D(np.arange(100)) + s.plot() + r = roi.SpanROI() + s_span = r.interactive(s) + np.testing.assert_allclose(s_span.data, np.arange(25, 74)) + + w = list(r.widgets)[0] + assert w._pos == (24.5,) + assert w._size == (50.0,) + w._set_span_extents(10, 20) + np.testing.assert_allclose(s_span.data, np.arange(9, 19)) diff --git a/hyperspy/tests/drawing/test_plot_signal.py b/hyperspy/tests/drawing/test_plot_signal.py index 722ea9ae24..4fc475880b 100644 --- a/hyperspy/tests/drawing/test_plot_signal.py +++ b/hyperspy/tests/drawing/test_plot_signal.py @@ -1,19 +1,19 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import matplotlib.pyplot as plt import numpy as np @@ -21,46 +21,50 @@ import traits.api as t import hyperspy.api as hs -from hyperspy.drawing.signal1d import Signal1DFigure, Signal1DLine from hyperspy.drawing.image import ImagePlot -from hyperspy.misc.test_utils import update_close_figure, check_closing_plot - +from hyperspy.drawing.signal1d import Signal1DFigure, Signal1DLine +from hyperspy.misc.test_utils import check_closing_plot, update_close_figure -scalebar_color = 'blue' +scalebar_color = "blue" default_tol = 2.0 -baseline_dir = 'plot_signal' -style_pytest_mpl = 'default' +baseline_dir = "plot_signal" +style_pytest_mpl = "default" class _TestPlot: - - def __init__(self, ndim, sdim, data_type='real'): + def __init__(self, ndim, sdim, data_type="real"): shape = np.arange(1, ndim + sdim + 1) * 5 n = 1 for i in shape: n *= i data = np.arange(n).reshape(shape) - title = 'Signal: %i, Navigator: %i' % (sdim, ndim) - dtype = '' - if 'complex' in data_type: + title = "Signal: %i, Navigator: %i" % (sdim, ndim) + dtype = "" + if "complex" in data_type: data = data + 1j * (data + 9) - title += ', complex' - dtype = 'Complex' - s = hs.signals.__dict__['%sSignal%iD' % (dtype, sdim)](data) + title += ", complex" + dtype = "Complex" + s = getattr(hs.signals, f"{dtype}Signal{sdim}D")(data) if sdim == 1: - s.axes_manager = self._set_signal_axes(s.axes_manager, name='Energy', - units='keV', scale=.5, offset=0.3) + s.axes_manager = self._set_signal_axes( + s.axes_manager, name="Energy", units="keV", scale=0.5, offset=0.3 + ) elif sdim == 2: - s.axes_manager = self._set_signal_axes(s.axes_manager, name='Reciprocal distance', - units='1/nm', scale=1, offset=0.0) + s.axes_manager = self._set_signal_axes( + s.axes_manager, + name="Reciprocal distance", + units="1/nm", + scale=1, + offset=0.0, + ) if ndim > 0: - s.axes_manager = self._set_navigation_axes(s.axes_manager, name='', - units='nm', scale=1.0, - offset=5.0) + s.axes_manager = self._set_navigation_axes( + s.axes_manager, name="", units="nm", scale=1.0, offset=5.0 + ) s.metadata.General.title = title # workaround to be able to access the figure in case of complex 2d # signals - if 'complex' in data_type and sdim == 2: + if "complex" in data_type and sdim == 2: real = s.real real.plot() self.real_plot = real._plot @@ -70,16 +74,18 @@ def __init__(self, ndim, sdim, data_type='real'): self.signal = s self.sdim = sdim - def _set_navigation_axes(self, axes_manager, name=t.Undefined, - units=t.Undefined, scale=1.0, offset=0.0): + def _set_navigation_axes( + self, axes_manager, name=t.Undefined, units=t.Undefined, scale=1.0, offset=0.0 + ): for nav_axis in axes_manager.navigation_axes: nav_axis.units = units nav_axis.scale = scale nav_axis.offset = offset return axes_manager - def _set_signal_axes(self, axes_manager, name=t.Undefined, - units=t.Undefined, scale=1.0, offset=0.0): + def _set_signal_axes( + self, axes_manager, name=t.Undefined, units=t.Undefined, scale=1.0, offset=0.0 + ): for sig_axis in axes_manager.signal_axes: sig_axis.name = name sig_axis.units = units @@ -92,10 +98,10 @@ def _generate_parameter(): parameters = [] for ndim in [0, 1, 2]: for sdim in [1, 2]: - for plot_type in ['nav', 'sig']: + for plot_type in ["nav", "sig"]: # For complex 2D, there are 4 figures generated, some of these # tests are redondants - for data_type in ['real', 'complex_real', 'complex_imag']: + for data_type in ["real", "complex_real", "complex_imag"]: if ndim == 0 and plot_type == "nav": # in this case, no nav figure pass else: @@ -103,10 +109,12 @@ def _generate_parameter(): return parameters -@pytest.mark.parametrize(("ndim", "sdim", "plot_type", "data_type"), - _generate_parameter()) +@pytest.mark.parametrize( + ("ndim", "sdim", "plot_type", "data_type"), _generate_parameter() +) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_sig_nav(ndim, sdim, plot_type, data_type): test_plot = _TestPlot(ndim, sdim, data_type) test_plot.signal.plot() @@ -115,7 +123,8 @@ def test_plot_sig_nav(ndim, sdim, plot_type, data_type): @pytest.mark.parametrize("sdim", [1, 2]) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_data_changed_event(sdim): if sdim == 2: s = hs.signals.Signal2D(np.arange(25).reshape((5, 5))) @@ -127,6 +136,24 @@ def test_plot_data_changed_event(sdim): return plt.gcf() +@pytest.mark.parametrize("ndim", [0, 1, 2]) +@pytest.mark.parametrize("sdim", [1, 2]) +def test_plot_event_close(sdim, ndim): + # check that the events are correctly disconnected after closing + dim = sdim + ndim + nav_event = 1 if sdim + ndim >= 3 else 0 + data = np.arange(5**dim).reshape((5,) * dim) + s = hs.signals.Signal1D(data) if sdim == 1 else hs.signals.Signal2D(data) + assert len(s.events.data_changed.connected) == 0 + assert len(s.axes_manager.events.any_axis_changed.connected) == 0 + s.plot() + assert len(s.events.data_changed.connected) == 1 + nav_event + assert len(s.axes_manager.events.any_axis_changed.connected) == nav_event + s._plot.close() + assert len(s.events.data_changed.connected) == 0 + assert len(s.axes_manager.events.any_axis_changed.connected) == 0 + + def _get_figure(test_plot, data_type, plot_type): if plot_type == "sig": plot = "signal_plot" @@ -135,9 +162,9 @@ def _get_figure(test_plot, data_type, plot_type): if "complex" in data_type and test_plot.sdim == 2: if data_type == "complex_real": - plot_part = 'real_plot' + plot_part = "real_plot" elif data_type == "complex_imag": - plot_part = 'real_plot' + plot_part = "real_plot" fig = getattr(getattr(test_plot, plot_part), plot).figure else: fig = getattr(test_plot.signal._plot, plot).figure @@ -200,12 +227,12 @@ def test_plot_close_cycle(sdim): s._plot.close() -@pytest.mark.parametrize('autoscale', ['', 'x', 'xv', 'v']) +@pytest.mark.parametrize("autoscale", ["", "x", "xv", "v"]) @pytest.mark.parametrize("ndim", [1, 2]) def test_plot_navigator_kwds(ndim, autoscale): test_plot_nav1d = _TestPlot(ndim=ndim, sdim=2, data_type="real") s = test_plot_nav1d.signal - s.plot(navigator_kwds={'norm':'log', 'autoscale':autoscale}) + s.plot(navigator_kwds={"norm": "log", "autoscale": autoscale}) if ndim == 1: assert isinstance(s._plot.navigator_plot, Signal1DFigure) plot = s._plot.navigator_plot.ax_lines[0] @@ -214,7 +241,7 @@ def test_plot_navigator_kwds(ndim, autoscale): plot = s._plot.navigator_plot assert isinstance(plot, ImagePlot) - assert plot.norm == 'log' + assert plot.norm == "log" assert plot.autoscale == autoscale s._plot.close() @@ -228,17 +255,17 @@ def test_plot_signal_dim0(): check_closing_plot(s) -@pytest.mark.parametrize('bool_value', [True, False]) +@pytest.mark.parametrize("bool_value", [True, False]) @pytest.mark.parametrize("sdim", [1, 2]) def test_data_function_kwargs(sdim, bool_value): test_plot_nav1d = _TestPlot(ndim=1, sdim=sdim, data_type="complex") s = test_plot_nav1d.signal s.plot(power_spectrum=bool_value, fft_shift=bool_value) if sdim == 1: - for key in ['power_spectrum', 'fft_shift']: + for key in ["power_spectrum", "fft_shift"]: assert s._plot.signal_data_function_kwargs[key] is bool_value else: - for key in ['power_spectrum', 'fft_shift']: + for key in ["power_spectrum", "fft_shift"]: assert s._plot_kwargs[key] is bool_value @@ -249,7 +276,7 @@ def test_plot_power_spectrum(): s = hs.signals.ComplexSignal1D(np.arange(100)) s.plot(power_spectrum=True) - assert s._plot.signal_data_function_kwargs['power_spectrum'] is True + assert s._plot.signal_data_function_kwargs["power_spectrum"] is True @pytest.mark.parametrize("sdim", [1, 2]) @@ -259,14 +286,14 @@ def test_plot_slider(ndim, sdim): s = test_plot_nav1d.signal # Plot twice to check that the args of the second call are used. s.plot() - s.plot(navigator='slider') + s.plot(navigator="slider") assert s._plot.signal_plot is not None assert s._plot.navigator_plot is None s._plot.close() check_closing_plot(s, check_data_changed_close=False) if ndim > 1: - s.plot(navigator='spectrum') + s.plot(navigator="spectrum") assert s._plot.signal_plot is not None assert s._plot.navigator_plot is not None assert isinstance(s._plot.navigator_plot, Signal1DFigure) @@ -310,7 +337,7 @@ def test_plot_autoscale(sdim): test_plot_nav1d = _TestPlot(ndim=1, sdim=sdim, data_type="real") s = test_plot_nav1d.signal with pytest.raises(ValueError): - s.plot(autoscale='xa') + s.plot(autoscale="xa") s.change_dtype(bool) s.plot() @@ -321,8 +348,28 @@ def test_plot_complex_representation(): imag_ref = np.arange(9).reshape((3, 3)) + 9 comp_ref = real_ref + 1j * imag_ref s = hs.signals.ComplexSignal1D(comp_ref) - s.plot(representation='polar', same_axes=True) - s.plot(representation='polar', same_axes=False) + s.plot() + # change indices to trigger update + s.axes_manager.indices = (1,) + s.plot(representation="polar", same_axes=True) + s.plot(representation="polar", same_axes=False) with pytest.raises(ValueError): - s.plot(representation='unsupported_argument') - \ No newline at end of file + s.plot(representation="unsupported_argument") + + +def test_plot_signal_scalar(): + s = hs.signals.BaseSignal([1.0]) + s.plot() + assert s._plot is None + + +@pytest.mark.parametrize("lazy", [True, False]) +def test_plot_ragged_array(lazy): + data = np.empty((2, 5), dtype=object) + data.fill(np.array([10, 20])) + + s = hs.signals.BaseSignal(data, ragged=True) + if lazy: + s = s.as_lazy() + with pytest.raises(RuntimeError): + s.plot() diff --git a/hyperspy/tests/drawing/test_plot_signal1d.py b/hyperspy/tests/drawing/test_plot_signal1d.py index d77dbf97a5..664bd36a75 100644 --- a/hyperspy/tests/drawing/test_plot_signal1d.py +++ b/hyperspy/tests/drawing/test_plot_signal1d.py @@ -1,185 +1,209 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import copy +import importlib import os -from shutil import copyfile from pathlib import Path +from shutil import copyfile import matplotlib.pyplot as plt import numpy as np import pytest -import scipy.misc +from matplotlib.backend_bases import MouseEvent, PickEvent + +try: + # scipy >=1.10 + from scipy.datasets import ascent, face +except ImportError: + # scipy <1.10 + from scipy.misc import ascent, face import hyperspy.api as hs +from hyperspy.drawing.signal1d import Signal1DLine from hyperspy.misc.test_utils import update_close_figure from hyperspy.signals import Signal1D -from hyperspy.drawing.signal1d import Signal1DLine from hyperspy.tests.drawing.test_plot_signal import _TestPlot -scalebar_color = 'blue' +scalebar_color = "blue" default_tol = 2.0 -baseline_dir = 'plot_signal1d' -style_pytest_mpl = 'default' +baseline_dir = "plot_signal1d" +style_pytest_mpl = "default" -style = ['default', 'overlap', 'cascade', 'mosaic', 'heatmap'] +style = ["default", "overlap", "cascade", "mosaic", "heatmap"] def _generate_filename_list(style): path = Path(__file__).resolve().parent baseline_path = path.joinpath(baseline_dir) - filename_list = [f'test_plot_spectra_{s}' for s in style] + \ - [f'test_plot_spectra_rev_{s}' for s in style] + filename_list = [f"test_plot_spectra_{s}" for s in style] + [ + f"test_plot_spectra_rev_{s}" for s in style + ] filename_list2 = [] for filename in filename_list: - for i in range(0, 4): - filename_list2.append( - baseline_path.joinpath(f'{filename}{i}.png') - ) + for suffix in ["True", "None"]: + filename_list2.append(baseline_path.joinpath(f"{filename}-{suffix}.png")) return filename_list2 +def _matplotlib_pick_event(figure, click, artist): + try: + # Introduced in matplotlib 3.6 and `pick_event` deprecated + event = PickEvent("pick_event", figure, click, artist) + figure.canvas.callbacks.process("pick_event", event) + except Exception: # Deprecated in matplotlib 3.6 + figure.canvas.pick_event(figure.canvas, click, artist) + + @pytest.fixture def setup_teardown(request, scope="class"): - try: - import pytest_mpl + plot_testing = request.config.getoption("--mpl") + pytest_mpl_spec = importlib.util.find_spec("pytest_mpl") + + if pytest_mpl_spec is None: + mpl_generate_path_cmdopt = None + else: # This option is available only when pytest-mpl is installed mpl_generate_path_cmdopt = request.config.getoption("--mpl-generate-path") - except ImportError: - mpl_generate_path_cmdopt = None # SETUP # duplicate baseline images to match the test_name when the # parametrized 'test_plot_spectra' are run. For a same 'style', the # expected images are the same. - if mpl_generate_path_cmdopt is None: + if mpl_generate_path_cmdopt is None and plot_testing: for filename in _generate_filename_list(style): - copyfile(f"{str(filename)[:-5]}.png", filename) + copyfile(f"{str(filename)[:-9]}.png", filename) yield # TEARDOWN # Create the baseline images: copy one baseline image for each test # and remove the other ones. - if mpl_generate_path_cmdopt: + if mpl_generate_path_cmdopt and plot_testing: for filename in _generate_filename_list(style): - copyfile(filename, f"{str(filename)[:-5]}.png") - # Delete the images that have been created in 'setup_class' - for filename in _generate_filename_list(style): - os.remove(filename) + copyfile(filename, f"{str(filename)[:-9]}.png") + if plot_testing: + # Delete the images that have been created in 'setup_class' + for filename in _generate_filename_list(style): + os.remove(filename) @pytest.mark.usefixtures("setup_teardown") -class TestPlotSpectra(): - - s = hs.signals.Signal1D(scipy.misc.ascent()[100:160:10]) - - # Add a test signal with decreasing axis - s_reverse = s.deepcopy() - s_reverse.axes_manager[1].offset = 512 - s_reverse.axes_manager[1].scale = -1 +class TestPlotSpectra: + def setup_method(self, method): + s = hs.signals.Signal1D(ascent()[100:160:10]) + + # Add a test signal with decreasing axis + s_reverse = s.deepcopy() + s_reverse.axes_manager[1].offset = 512 + s_reverse.axes_manager[1].scale = -1 + self.s = s + self.s_reverse = s_reverse def _generate_parameters(style): parameters = [] for s in style: for fig in [True, None]: - for ax in [True, None]: - parameters.append([s, fig, ax]) + parameters.append([s, fig]) return parameters - def _generate_ids(style, duplicate=4): - ids = [] - for s in style: - ids.extend([s] * duplicate) - return ids - - @pytest.mark.parametrize(("style", "fig", "ax"), - _generate_parameters(style), - ids=_generate_ids(style)) - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) - def test_plot_spectra(self, style, fig, ax): + @pytest.mark.parametrize(("style", "fig"), _generate_parameters(style)) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_spectra(self, style, fig): + kwargs = {} + # Set figsize to get the same figure between default and passed figure + if style == "mosaic": + kwargs["figsize"] = [3.0, 12.0] + elif style != "heatmap": + kwargs["figsize"] = [6.0, 4.0] if fig: - fig = plt.figure() - if ax: - fig = plt.figure() - ax = fig.add_subplot(111) - - ax = hs.plot.plot_spectra(self.s, style=style, legend='auto', - fig=fig, ax=ax) - if style == 'mosaic': + fig = plt.figure(**kwargs) + if style == "heatmap": + # Not supported with this style, the error is tested elsewhere + fig = None + + ax = hs.plot.plot_spectra(self.s, style=style, legend="auto", fig=fig, **kwargs) + if style == "mosaic": ax = ax[0] return ax.figure - @pytest.mark.parametrize(("style", "fig", "ax"), - _generate_parameters(style), - ids=_generate_ids(style)) - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) - def test_plot_spectra_rev(self, style, fig, ax): + @pytest.mark.parametrize(("style", "fig"), _generate_parameters(style)) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_spectra_rev(self, style, fig): + kwargs = {} + if style == "mosaic": + kwargs["figsize"] = [3.0, 12.0] + elif style != "heatmap": + kwargs["figsize"] = [6.0, 4.0] if fig: - fig = plt.figure() - if ax: - fig = plt.figure() - ax = fig.add_subplot(111) - - ax = hs.plot.plot_spectra(self.s_reverse, style=style, legend='auto', - fig=fig, ax=ax) - if style == 'mosaic': + fig = plt.figure(**kwargs) + if style == "heatmap": + # Not supported with this style, the error is tested elsewhere + fig = None + + ax = hs.plot.plot_spectra( + self.s_reverse, style=style, legend="auto", fig=fig, **kwargs + ) + if style == "mosaic": ax = ax[0] return ax.figure - @pytest.mark.parametrize("figure", ['1nav', '1sig', '2nav', '2sig']) - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.parametrize("figure", ["1nav", "1sig", "2nav", "2sig"]) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_spectra_sync(self, figure): - s1 = hs.signals.Signal1D(scipy.misc.face()).as_signal1D(0).inav[:, :3] + s1 = hs.signals.Signal1D(face().astype(float)).as_signal1D(0).inav[:, :3] s2 = s1.deepcopy() * -1 hs.plot.plot_signals([s1, s2]) - if figure == '1nav': + if figure == "1nav": return s1._plot.signal_plot.figure - if figure == '1sig': + if figure == "1sig": return s1._plot.navigator_plot.figure - if figure == '2nav': + if figure == "2nav": return s2._plot.navigator_plot.figure - if figure == '2sig': + if figure == "2sig": return s2._plot.navigator_plot.figure def test_plot_spectra_legend_pick(self): - x = np.linspace(0., 2., 512) + x = np.linspace(0.0, 2.0, 512) n = np.arange(1, 5) - x_pow_n = x[None, :]**n[:, None] + x_pow_n = x[None, :] ** n[:, None] s = hs.signals.Signal1D(x_pow_n) - my_legend = [r'x^' + str(io) for io in n] + my_legend = [r"x^" + str(io) for io in n] f = plt.figure() ax = hs.plot.plot_spectra(s, legend=my_legend, fig=f) leg = ax.get_legend() leg_artists = leg.get_lines() - click = plt.matplotlib.backend_bases.MouseEvent( - 'button_press_event', f.canvas, 0, 0, 'left') + click = MouseEvent("button_press_event", f.canvas, 0, 0, "left") for artist, li in zip(leg_artists, ax.lines[::-1]): - plt.matplotlib.backends.backend_agg.FigureCanvasBase.pick_event( - f.canvas, click, artist) + _matplotlib_pick_event(f, click, artist) assert not li.get_visible() - plt.matplotlib.backends.backend_agg.FigureCanvasBase.pick_event( - f.canvas, click, artist) + _matplotlib_pick_event(f, click, artist) + assert li.get_visible() - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_spectra_auto_update(self): s = hs.signals.Signal1D(np.arange(100)) s2 = s / 2 @@ -192,26 +216,68 @@ def test_plot_spectra_auto_update(self): return ax.get_figure() -class TestPlotNonLinearAxis: - - def setup_method(self): - dict0 = {'size': 10, 'name': 'Axis0', 'units': 'A', 'scale': 0.2, - 'offset': 1, 'navigate': True} - dict1 = {'axis': np.arange(100)**3, 'name': 'Axis1', 'units': 'O', - 'navigate': False} +class TestPlotNonUniformAxis: + def setup_method(self, method): + dict0 = { + "size": 10, + "name": "Axis0", + "units": "A", + "scale": 0.2, + "offset": 1, + "navigate": True, + } + dict1 = { + "axis": np.arange(100) ** 3, + "name": "Axis1", + "units": "O", + "navigate": False, + } np.random.seed(1) - s = hs.signals.Signal1D(np.random.random((10, 100)), - axes=[dict0, dict1]) + s = hs.signals.Signal1D(np.random.random((10, 100)), axes=[dict0, dict1]) self.s = s - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_non_uniform_sig(self): self.s.plot() return self.s._plot.signal_plot.figure - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_non_uniform_sig_update(self): + s2 = self.s + s2.plot() + s2.axes_manager[0].index += 1 + return s2._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_uniform_nav(self): + self.s.plot() + return self.s._plot.navigator_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_uniform_nav_update(self): + s2 = self.s + s2.plot() + s2.axes_manager[0].index += 1 + return self.s._plot.navigator_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_uniform_sig(self): + self.s.plot() + return self.s._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_non_uniform_nav(self): s2 = self.s.T s2.plot() @@ -242,7 +308,7 @@ def test_plot_nav2_close(): def _test_plot_two_cursors(ndim): test_plot = _TestPlot(ndim=ndim, sdim=1) # sdim=2 not supported s = test_plot.signal - s.metadata.General.title = 'Nav %i, Sig 1, two cursor' % ndim + s.metadata.General.title = "Nav %i, Sig 1, two cursor" % ndim s.axes_manager[0].index = 4 s.plot() s._plot.add_right_pointer() @@ -255,10 +321,10 @@ def _test_plot_two_cursors(ndim): return s -@pytest.mark.parametrize('autoscale', ['', 'x', 'xv', 'v']) -@pytest.mark.parametrize('norm', ['log', 'auto']) +@pytest.mark.parametrize("autoscale", ["", "x", "xv", "v"]) +@pytest.mark.parametrize("norm", ["log", "auto"]) def test_plot_two_cursos_parameters(autoscale, norm): - kwargs = {'autoscale':autoscale, 'norm':norm} + kwargs = {"autoscale": autoscale, "norm": norm} test_plot = _TestPlot(ndim=2, sdim=1) # sdim=2 not supported s = test_plot.signal s.plot(**kwargs) @@ -270,29 +336,31 @@ def test_plot_two_cursos_parameters(autoscale, norm): def _generate_parameter(): parameters = [] for ndim in [1, 2]: - for plot_type in ['nav', 'sig']: + for plot_type in ["nav", "sig"]: parameters.append([ndim, plot_type]) return parameters -@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_log_scale(): s = Signal1D(np.exp(-np.arange(100) / 5.0)) - s.plot(norm='log') + s.plot(norm="log") return s._plot.signal_plot.figure @pytest.mark.parametrize(("ndim", "plot_type"), _generate_parameter()) -@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_two_cursors(ndim, plot_type): s = _test_plot_two_cursors(ndim=ndim) if plot_type == "sig": f = s._plot.signal_plot.figure else: - f= s._plot.navigator_plot.figure + f = s._plot.navigator_plot.figure return f @@ -319,43 +387,166 @@ def test_plot_with_non_finite_value(): s.axes_manager.events.indices_changed.trigger(s.axes_manager) -def test_plot_add_line_events(): +@pytest.mark.parametrize("ax", ["left", "right"]) +def test_plot_add_line_events(ax): s = hs.signals.Signal1D(np.arange(100)) s.plot() assert len(s.axes_manager.events.indices_changed.connected) == 1 - figure = s._plot.signal_plot + plot = s._plot.signal_plot + assert len(s._plot.signal_plot.figure.get_axes()) == 1 def line_function(axes_manager=None): return 100 - np.arange(100) line = Signal1DLine() line.data_function = line_function - line.set_line_properties(color='blue', type='line', scaley=False) - figure.add_line(line, connect_navigation=True) + line.set_line_properties(color="blue", type="line", scaley=False) + + if ax == "right": + plot.create_right_axis() + plot.right_axes_manager = copy.deepcopy(s.axes_manager) + expected_axis_number = 2 + expected_indices_changed_connected = 1 + else: + expected_axis_number = 1 + expected_indices_changed_connected = 2 + plot.add_line(line, ax=ax, connect_navigation=True) + + assert len(s._plot.signal_plot.figure.get_axes()) == expected_axis_number line.plot() assert len(line.events.closed.connected) == 1 - assert len(s.axes_manager.events.indices_changed.connected) == 2 + # expected_indices_changed_connected is 2 only when adding line on the left + # because for the right ax, we have a deepcopy of the axes_manager + assert ( + len(s.axes_manager.events.indices_changed.connected) + == expected_indices_changed_connected + ) line.close() - figure.close_right_axis() + plot.close_right_axis() + assert len(s._plot.signal_plot.figure.get_axes()) == 1 assert len(line.events.closed.connected) == 0 assert len(s.axes_manager.events.indices_changed.connected) == 1 - figure.close() + s._plot.close() assert len(s.axes_manager.events.indices_changed.connected) == 0 + assert len(s._plot.events.closed.connected) == 0 + assert s._plot.signal_plot is None -@pytest.mark.parametrize("autoscale", ['', 'x', 'xv', 'v']) -@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) +@pytest.mark.parametrize("autoscale", ["", "x", "xv", "v"]) +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_autoscale(autoscale): - s = hs.datasets.artificial_data.get_core_loss_eels_line_scan_signal( - add_powerlaw=True, add_noise=False) + s = hs.data.two_gaussians().inav[0, 0] s.plot(autoscale=autoscale) ax = s._plot.signal_plot.ax - ax.set_xlim(500.0, 700.0) - ax.set_ylim(-10.0, 20.0) + ax.set_xlim(50.0, 70.0) + ax.set_ylim(-50.0, 200.0) s.axes_manager.events.indices_changed.trigger(s.axes_manager) return s._plot.signal_plot.figure + + +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) +@pytest.mark.parametrize("linestyle", [None, "-", ["-", "--"]]) +def test_plot_spectra_linestyle(linestyle): + s = hs.signals.Signal1D(np.arange(100).reshape(2, 50)) + ax = hs.plot.plot_spectra(s, linestyle=linestyle) + + return ax.get_figure() + + +def test_plot_spectra_linestyle_error(): + s = hs.signals.Signal1D(np.arange(100).reshape(2, 50)) + + with pytest.raises(ValueError): + hs.plot.plot_spectra(s, linestyle="invalid") + + +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) +@pytest.mark.parametrize("style", ("overlap", "cascade", "mosaic", "heatmap")) +def test_plot_spectra_normalise(style): + s = hs.signals.Signal1D(np.arange(100)) + 100 + s2 = s * 1000 + + ax = hs.plot.plot_spectra([s, s2], style=style, normalise=True) + if style == "mosaic": + ax = ax[0] + + return ax.get_figure() + + +def test_plot_spectra_normalise_interactive(): + s = hs.signals.Signal1D(np.arange(100)) + 100 + s2 = np.sqrt(s) + + ax = hs.plot.plot_spectra([s, s2], normalise=True) + lines = ax.get_lines() + assert lines[0].get_data()[1][0] == 0 + assert lines[0].get_data()[1][-1] == 1 + assert lines[1].get_data()[1][0] == 0 + assert lines[1].get_data()[1][-1] == 1 + + # Simulate data changed + s.data = s.data * -1 + s.events.data_changed.trigger(s) + + # check the values + assert lines[0].get_data()[1][0] == 1 + assert lines[0].get_data()[1][-1] == 0 + assert lines[1].get_data()[1][0] == 0 + assert lines[1].get_data()[1][-1] == 1 + + +def test_plot_empty_slice_autoscale(): + s = hs.signals.Signal1D(np.arange(100)) + s.plot() + r = hs.roi.SpanROI() + s_span = r.interactive(s) + s_span.plot(autoscale="x") + # change span selector to an "empty" slice and trigger update + r.left = 24 + r.right = 24.1 + + s_span.plot(autoscale="v") + # change span selector to an "empty" slice and trigger update + r.left = 23 + r.right = 23.1 + + +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) +def test_plot_spectra_ax(): + s = hs.signals.Signal1D(np.arange(10)) + s.axes_manager[-1].name = "" + s.axes_manager[-1].units = "" + s2 = -s + s_stack = hs.stack([s, s2]) + + fig, ax = plt.subplots(ncols=5, nrows=1) + with pytest.raises(ValueError): + hs.plot.plot_spectra(s_stack, style="heatmap", ax=ax) + with pytest.raises(ValueError): + hs.plot.plot_spectra(s_stack, style="heatmap", fig=fig) + + with pytest.raises(ValueError): + hs.plot.plot_spectra(s_stack, ax=ax) + + hs.plot.plot_spectra(s_stack, ax=ax[1], style="overlap") + hs.plot.plot_spectra(s_stack, ax=ax[2], style="cascade", padding=1.5) + hs.plot.plot_spectra(s_stack, ax=ax[3:], style="mosaic") + + ax[1].set_title("overlap") + ax[2].set_title("cascade") + ax[3].set_title("mosaic 0") + ax[4].set_title("mosaic 1") + + return fig diff --git a/hyperspy/tests/drawing/test_plot_signal2d.py b/hyperspy/tests/drawing/test_plot_signal2d.py index ad8e230a0b..7ea4ebd8f1 100644 --- a/hyperspy/tests/drawing/test_plot_signal2d.py +++ b/hyperspy/tests/drawing/test_plot_signal2d.py @@ -1,41 +1,54 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . +import logging + +import matplotlib import matplotlib.pyplot as plt import numpy as np import pytest import scipy.ndimage +from matplotlib.backend_bases import MouseButton, MouseEvent +from packaging.version import Version + +try: + # scipy >=1.10 + from scipy.datasets import ascent, face +except ImportError: + # scipy <1.10 + from scipy.misc import ascent, face import traits.api as t import hyperspy.api as hs +from hyperspy.decorators import lazifyTestClass from hyperspy.drawing.utils import make_cmap, plot_RGB_map from hyperspy.tests.drawing.test_plot_signal import _TestPlot -scalebar_color = 'blue' +scalebar_color = "blue" default_tol = 2.0 -baseline_dir = 'plot_signal2d' -style_pytest_mpl = 'default' +baseline_dir = "plot_signal2d" +style_pytest_mpl = "default" def _generate_image_stack_signal(): image = hs.signals.Signal2D(np.random.random((2, 3, 512, 512))) for i in range(2): for j in range(3): - image.data[i, j, :] = scipy.misc.ascent() * (i + 0.5 + j) + image.data[i, j, :] = ascent() * (i + 0.5 + j) axes = image.axes_manager axes[2].name = "x" axes[3].name = "y" @@ -45,8 +58,9 @@ def _generate_image_stack_signal(): return image -def _set_navigation_axes(axes_manager, name=t.Undefined, units=t.Undefined, - scale=1.0, offset=0.0): +def _set_navigation_axes( + axes_manager, name=t.Undefined, units=t.Undefined, scale=1.0, offset=0.0 +): for nav_axis in axes_manager.navigation_axes: nav_axis.units = units nav_axis.scale = scale @@ -54,8 +68,9 @@ def _set_navigation_axes(axes_manager, name=t.Undefined, units=t.Undefined, return axes_manager -def _set_signal_axes(axes_manager, name=t.Undefined, units=t.Undefined, - scale=1.0, offset=0.0): +def _set_signal_axes( + axes_manager, name=t.Undefined, units=t.Undefined, scale=1.0, offset=0.0 +): for sig_axis in axes_manager.signal_axes: sig_axis.name = name sig_axis.units = units @@ -64,9 +79,10 @@ def _set_signal_axes(axes_manager, name=t.Undefined, units=t.Undefined, return axes_manager -@pytest.mark.parametrize("normalization", ['single', 'global']) +@pytest.mark.parametrize("normalization", ["single", "global"]) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_rgb_image(normalization): w = 20 data = np.arange(1, w * w + 1).reshape(w, w) @@ -85,23 +101,34 @@ def _generate_parameter(): for axes_ticks in [True, False]: for centre_colormap in [True, False]: for min_aspect in [0.2, 0.7]: - parameters.append([scalebar, colorbar, axes_ticks, - centre_colormap, min_aspect]) + parameters.append( + [ + scalebar, + colorbar, + axes_ticks, + centre_colormap, + min_aspect, + ] + ) return parameters -@pytest.mark.parametrize(("scalebar", "colorbar", "axes_ticks", - "centre_colormap", "min_aspect"), - _generate_parameter()) +@pytest.mark.parametrize( + ("scalebar", "colorbar", "axes_ticks", "centre_colormap", "min_aspect"), + _generate_parameter(), +) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot(scalebar, colorbar, axes_ticks, centre_colormap, min_aspect): test_plot = _TestPlot(ndim=0, sdim=2) - test_plot.signal.plot(scalebar=scalebar, - colorbar=colorbar, - axes_ticks=axes_ticks, - centre_colormap=centre_colormap, - min_aspect=min_aspect) + test_plot.signal.plot( + scalebar=scalebar, + colorbar=colorbar, + axes_ticks=axes_ticks, + centre_colormap=centre_colormap, + min_aspect=min_aspect, + ) return test_plot.signal._plot.signal_plot.figure @@ -114,32 +141,37 @@ def _generate_parameter_plot_images(): @pytest.mark.parametrize("percentile", [(None, None), ("1th", "99th")]) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_log_scale(percentile): test_plot = _TestPlot(ndim=0, sdim=2) test_plot.signal += 1 # need to avoid zeros in log - test_plot.signal.plot(norm='log', vmin=percentile[0], vmax=percentile[1]) + test_plot.signal.plot(norm="log", vmin=percentile[0], vmax=percentile[1]) return test_plot.signal._plot.signal_plot.figure @pytest.mark.parametrize("fft_shift", [True, False]) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_FFT(fft_shift): - s = hs.datasets.example_signals.object_hologram() - s2 = s.isig[:128, :128].fft() - s2.plot(fft_shift=fft_shift, axes_ticks=True, power_spectrum=True) - return s2._plot.signal_plot.figure + s = hs.data.wave_image(random_state=0) + + s_fft = s.fft() + s_fft.plot(fft_shift=fft_shift, axes_ticks=True, power_spectrum=True) + return s_fft._plot.signal_plot.figure -@pytest.mark.parametrize(("vmin", "vmax"), (_generate_parameter_plot_images(), - (None, None))) +@pytest.mark.parametrize( + ("vmin", "vmax"), (_generate_parameter_plot_images(), (None, None)) +) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_multiple_images_list(vmin, vmax): # load red channel of raccoon as an image - image0 = hs.signals.Signal2D(scipy.misc.face()[:, :, 0]) - image0.metadata.General.title = 'Rocky Raccoon - R' + image0 = hs.signals.Signal2D(face()[:, :, 0]) + image0.metadata.General.title = "Rocky Raccoon - R" axes0 = image0.axes_manager axes0[0].name = "x" axes0[1].name = "y" @@ -150,8 +182,8 @@ def test_plot_multiple_images_list(vmin, vmax): image1 = _generate_image_stack_signal() # load green channel of raccoon as an image - image2 = hs.signals.Signal2D(scipy.misc.face()[:, :, 1]) - image2.metadata.General.title = 'Rocky Raccoon - G' + image2 = hs.signals.Signal2D(face()[:, :, 1]) + image2.metadata.General.title = "Rocky Raccoon - G" axes2 = image2.axes_manager axes2[0].name = "x" axes2[1].name = "y" @@ -159,26 +191,33 @@ def test_plot_multiple_images_list(vmin, vmax): axes2[1].units = "mm" # load rgb imimagesage - rgb = hs.signals.Signal1D(scipy.misc.face()) + rgb = hs.signals.Signal1D(face()) rgb.change_dtype("rgb8") - rgb.metadata.General.title = 'RGB' + rgb.metadata.General.title = "RGB" axesRGB = rgb.axes_manager axesRGB[0].name = "x" axesRGB[1].name = "y" axesRGB[0].units = "nm" axesRGB[1].units = "nm" - hs.plot.plot_images([image0, image1, image2, rgb], tight_layout=True, - labelwrap=20, vmin=vmin, vmax=vmax) + hs.plot.plot_images( + [image0, image1, image2, rgb], + tight_layout=True, + labelwrap=20, + vmin=vmin, + vmax=vmax, + ) return plt.gcf() + @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_rgb_image(): # load rgb imimagesage - rgb = hs.signals.Signal1D(scipy.misc.face()) + rgb = hs.signals.Signal1D(face()) rgb.change_dtype("rgb8") - rgb.metadata.General.title = 'RGB' + rgb.metadata.General.title = "RGB" axesRGB = rgb.axes_manager axesRGB[0].name = "x" axesRGB[1].name = "y" @@ -187,38 +226,38 @@ def test_plot_rgb_image(): rgb.plot() return plt.gcf() -class _TestIteratedSignal: +class _TestIteratedSignal: def __init__(self): - s = hs.signals.Signal2D([scipy.misc.ascent()] * 6) + s = hs.signals.Signal2D([ascent()] * 6) angles = hs.signals.BaseSignal(range(00, 60, 10)) s.map(scipy.ndimage.rotate, angle=angles.T, reshape=False) # prevent values outside of integer range s.data = np.clip(s.data, 0, 255) - title = 'Ascent' - - s.axes_manager = self._set_signal_axes(s.axes_manager, - name='spatial', - units='nm', scale=1, - offset=0.0) - s.axes_manager = self._set_navigation_axes(s.axes_manager, - name='index', - units='images', - scale=1, offset=0) + title = "Ascent" + + s.axes_manager = self._set_signal_axes( + s.axes_manager, name="spatial", units="nm", scale=1, offset=0.0 + ) + s.axes_manager = self._set_navigation_axes( + s.axes_manager, name="index", units="images", scale=1, offset=0 + ) s.metadata.General.title = title self.signal = s - def _set_navigation_axes(self, axes_manager, name=t.Undefined, - units=t.Undefined, scale=1.0, offset=0.0): + def _set_navigation_axes( + self, axes_manager, name=t.Undefined, units=t.Undefined, scale=1.0, offset=0.0 + ): for nav_axis in axes_manager.navigation_axes: nav_axis.units = units nav_axis.scale = scale nav_axis.offset = offset return axes_manager - def _set_signal_axes(self, axes_manager, name=t.Undefined, - units=t.Undefined, scale=1.0, offset=0.0): + def _set_signal_axes( + self, axes_manager, name=t.Undefined, units=t.Undefined, scale=1.0, offset=0.0 + ): for sig_axis in axes_manager.signal_axes: sig_axis.name = name sig_axis.units = units @@ -227,36 +266,81 @@ def _set_signal_axes(self, axes_manager, name=t.Undefined, return axes_manager -class TestPlotNonLinearAxis: - +class TestPlotNonUniformAxis: def setup_method(self): - dict0 = {'axis': np.arange(10)**0.5, 'name':'Non uniform 0', 'units':'A', - 'navigate':True} - dict1 = {'axis': np.arange(10)**0.5, 'name':'Non uniform 1', 'units':'A', - 'navigate':False} - dict2 = {'size': 100, 'name':'Linear 2', 'units':'A', 'scale':0.2, - 'offset':1, 'navigate':False} + dict0 = { + "axis": np.arange(10) ** 0.5, + "name": "Non uniform 0", + "units": "A", + "navigate": True, + } + dict1 = { + "axis": np.arange(10) ** 0.5, + "name": "Non uniform 1", + "units": "A", + "navigate": False, + } + dict2 = { + "size": 100, + "name": "Linear 2", + "units": "A", + "scale": 0.2, + "offset": 1, + "navigate": False, + } np.random.seed(1) - s = hs.signals.Signal2D(np.random.random((10, 10, 100)), - axes=[dict0, dict1, dict2]) + s = hs.signals.Signal2D( + np.random.random((10, 10, 100)), axes=[dict0, dict1, dict2] + ) self.s = s - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_non_uniform_nav(self): self.s.plot() return self.s._plot.navigator_plot.figure - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_non_uniform_2s1n_sig(self): + self.s.plot() + return self.s._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_non_uniform_2s1n_update_sig(self): + s2 = self.s + s2.axes_manager[0].index += 1 + s2.plot() + return s2._plot.signal_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) + def test_plot_non_uniform_2s1n_update_nav(self): + s2 = self.s + s2.axes_manager[0].index += 1 + s2.plot() + return s2._plot.navigator_plot.figure + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_non_uniform_sig(self): s2 = self.s.T s2.plot(navigator=None) return s2._plot.signal_plot.figure + def test_plot_images(self): + hs.plot.plot_images(self.s.inav[0]) + @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_default(): test_im_plot = _TestIteratedSignal() hs.plot.plot_images(test_im_plot.signal) @@ -264,32 +348,31 @@ def test_plot_images_default(): @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_list(): test_im_plot = _TestIteratedSignal() - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap=['viridis', 'gray']) + hs.plot.plot_images(test_im_plot.signal, axes_decor="off", cmap=["viridis", "gray"]) return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_list_w_diverging(): test_im_plot = _TestIteratedSignal() - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap=['viridis', 'gray', 'RdBu_r']) + hs.plot.plot_images( + test_im_plot.signal, axes_decor="off", cmap=["viridis", "gray", "RdBu_r"] + ) return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_mpl_colors(): test_im_plot = _TestIteratedSignal() - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap='mpl_colors') + hs.plot.plot_images(test_im_plot.signal, axes_decor="off", cmap="mpl_colors") return plt.gcf() @@ -297,131 +380,146 @@ def test_plot_images_cmap_mpl_colors_w_single_cbar(): # This should give an error, so test for that test_im_plot = _TestIteratedSignal() with pytest.raises(ValueError): - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap='mpl_colors', - colorbar='single') + hs.plot.plot_images( + test_im_plot.signal, axes_decor="off", cmap="mpl_colors", colorbar="single" + ) def test_plot_images_bogus_cmap(): # This should give an error, so test for that test_im_plot = _TestIteratedSignal() with pytest.raises(ValueError) as val_error: - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap=3.14159265359, - colorbar=None) - assert str(val_error.value) == 'The provided cmap value was not ' \ - 'understood. Please check input values.' + hs.plot.plot_images( + test_im_plot.signal, axes_decor="off", cmap=3.14159265359, colorbar=None + ) + assert ( + str(val_error.value) == "The provided cmap value was not " + "understood. Please check input values." + ) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_one_string(): test_im_plot = _TestIteratedSignal() - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap='RdBu_r', - colorbar='single') + hs.plot.plot_images( + test_im_plot.signal, axes_decor="off", cmap="RdBu_r", colorbar="single" + ) return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_make_cmap_bittrue(): test_im_plot = _TestIteratedSignal() - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap=make_cmap([(255, 255, 255), - '#F5B0CB', - (220, 106, 207), - '#745C97', - (57, 55, 91)], - bit=True, - name='test_cmap', - register=True)) + hs.plot.plot_images( + test_im_plot.signal, + axes_decor="off", + cmap=make_cmap( + [(255, 255, 255), "#F5B0CB", (220, 106, 207), "#745C97", (57, 55, 91)], + bit=True, + name="test_cmap", + register=True, + ), + ) return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_make_cmap_bitfalse(): test_im_plot = _TestIteratedSignal() - hs.plot.plot_images(test_im_plot.signal, - axes_decor='off', - cmap=make_cmap([(1, 1, 1), - '#F5B0CB', - (0.86, 0.42, 0.81), - '#745C97', - (0.22, 0.22, 0.36)], - bit=False, - name='test_cmap2', - register=True)) + hs.plot.plot_images( + test_im_plot.signal, + axes_decor="off", + cmap=make_cmap( + [(1, 1, 1), "#F5B0CB", (0.86, 0.42, 0.81), "#745C97", (0.22, 0.22, 0.36)], + bit=False, + name="test_cmap2", + register=True, + ), + ) return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_multi_signal(): test_plot1 = _TestIteratedSignal() test_plot2 = _TestIteratedSignal() + test_plot2.signal.change_dtype(float) test_plot2.signal *= 2 # change scale of second signal test_plot2.signal = test_plot2.signal.inav[::-1] - test_plot2.signal.metadata.General.title = 'Descent' - - hs.plot.plot_images([test_plot1.signal, test_plot2.signal], - axes_decor='off', - per_row=4, - cmap='mpl_colors') + test_plot2.signal.metadata.General.title = "Descent" + + hs.plot.plot_images( + [test_plot1.signal, test_plot2.signal], + axes_decor="off", + per_row=4, + cmap="mpl_colors", + ) return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_cmap_multi_w_rgb(): test_plot1 = _TestIteratedSignal() test_plot2 = _TestIteratedSignal() + test_plot2.signal.change_dtype(float) test_plot2.signal *= 2 # change scale of second signal - test_plot2.signal.metadata.General.title = 'Ascent-2' - - rgb_sig = hs.signals.Signal1D(scipy.misc.face()) - rgb_sig.change_dtype('rgb8') - rgb_sig.metadata.General.title = 'Racoon!' - - hs.plot.plot_images([test_plot1.signal, test_plot2.signal, rgb_sig], - axes_decor='off', - per_row=4, - cmap='mpl_colors') + test_plot2.signal.metadata.General.title = "Ascent-2" + + rgb_sig = hs.signals.Signal1D(face()) + rgb_sig.change_dtype("rgb8") + rgb_sig.metadata.General.title = "Racoon!" + + hs.plot.plot_images( + [test_plot1.signal, test_plot2.signal, rgb_sig], + axes_decor="off", + per_row=4, + cmap="mpl_colors", + ) return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_single_image(): image0 = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) image0.isig[5, 5] = 200 - image0.metadata.General.title = 'This is the title from the metadata' + image0.metadata.General.title = "This is the title from the metadata" hs.plot.plot_images(image0, vmin="0.05th", vmax="99.95th") return plt.gcf() @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_single_image_stack(): image0 = hs.signals.Signal2D(np.arange(200).reshape(2, 10, 10)) image0.isig[5, 5] = 200 - image0.metadata.General.title = 'This is the title from the metadata' + image0.metadata.General.title = "This is the title from the metadata" hs.plot.plot_images(image0, vmin="0.05th", vmax="99.95th") return plt.gcf() +@pytest.mark.skipif( + Version(matplotlib.__version__) < Version("3.6.0"), + reason="This test requires matplotlib >= 3.6.0", +) def test_plot_images_multi_signal_w_axes_replot(): - imdata = np.random.rand(3, 5, 5) + imdata = np.random.rand(6, 5, 5) imgs = hs.signals.Signal2D(imdata) - img_list = [imgs, imgs.inav[:2], imgs.inav[0]] - subplots = hs.plot.plot_images(img_list, axes_decor=None) + subplots = hs.plot.plot_images(imgs, axes_decor=None) f = plt.gcf() f.canvas.draw() f.canvas.flush_events() @@ -430,49 +528,57 @@ def test_plot_images_multi_signal_w_axes_replot(): for axi in subplots: imi = axi.images[0].get_array() x, y = axi.transData.transform((2, 2)) - # Calling base class method because of backends - plt.matplotlib.backends.backend_agg.FigureCanvasBase.button_press_event( - f.canvas, x, y, 'left', True) + MouseEvent( + "button_press_event", f.canvas, x, y, MouseButton.LEFT, dblclick=True + )._process() fn = plt.gcf() tests.append(np.allclose(imi, plt.gca().images[0].get_array().data)) plt.close(fn) - assert np.alltrue(tests) - return f - - -@pytest.mark.parametrize("percentile", [("2.5th", "97.5th"), - [["0th", "10th", "20th"], ["100th", "90th", "80th"]], - [["5th", "10th"], ["95th", "90th"]], - [["5th", None, "10th"], ["95th", None, "90th"]], - ]) + assert np.all(tests) + + +@pytest.mark.parametrize( + "percentile", + [ + ("2.5th", "97.5th"), + [["0th", "10th", "20th"], ["100th", "90th", "80th"]], + [["5th", "10th"], ["95th", "90th"]], + [["5th", None, "10th"], ["95th", None, "90th"]], + ], +) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_vmin_vmax_percentile(percentile): image0 = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) image0.isig[5, 5] = 200 - image0.metadata.General.title = 'This is the title from the metadata' - ax = hs.plot.plot_images([image0, image0, image0], - vmin=percentile[0], - vmax=percentile[1], - axes_decor='off') + image0.metadata.General.title = "This is the title from the metadata" + ax = hs.plot.plot_images( + [image0, image0, image0], + vmin=percentile[0], + vmax=percentile[1], + axes_decor="off", + ) return ax[0].figure -@pytest.mark.parametrize("vmin_vmax", [(50, 150), - ([0, 10], [120, None])]) -@pytest.mark.parametrize("colorbar", ['single', 'multi', None]) +@pytest.mark.parametrize("vmin_vmax", [(50, 150), ([0, 10], [120, None])]) +@pytest.mark.parametrize("colorbar", ["single", "multi", None]) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_images_colorbar(colorbar, vmin_vmax): print("vmin_vmax:", vmin_vmax) image0 = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) image0.isig[5, 5] = 200 - image0.metadata.General.title = 'This is the title from the metadata' - ax = hs.plot.plot_images([image0, image0], - colorbar=colorbar, - vmin=vmin_vmax[0], - vmax=vmin_vmax[1], - axes_decor='ticks') + image0.metadata.General.title = "This is the title from the metadata" + ax = hs.plot.plot_images( + [image0, image0], + colorbar=colorbar, + vmin=vmin_vmax[0], + vmax=vmin_vmax[1], + axes_decor="ticks", + ) return ax[0].figure @@ -491,7 +597,7 @@ def test_plot_images_not_signal(): hs.plot.plot_images(data) with pytest.raises(ValueError): - hs.plot.plot_images('not a list of signal') + hs.plot.plot_images("not a list of signal") def test_plot_images_tranpose(): @@ -502,6 +608,18 @@ def test_plot_images_tranpose(): hs.plot.plot_images([a, b]) +def test_plot_images_update(): + s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) + s2 = s / 2 + axs = hs.plot.plot_images([s, s2]) + + s.data = -s.data - 10 + s.events.data_changed.trigger(s) + + np.testing.assert_allclose(axs[0].images[0].get_array()[0, :2], [-10, -11]) + np.testing.assert_allclose(axs[1].images[0].get_array()[0, :2], [0, 0.5]) + + # Ignore numpy warning about clipping np.nan values @pytest.mark.filterwarnings("ignore:Passing `np.nan` to mean no clipping in np.clip") def test_plot_with_non_finite_value(): @@ -522,43 +640,46 @@ def test_plot_with_non_finite_value(): s.axes_manager.events.indices_changed.trigger(s.axes_manager) -@pytest.mark.parametrize("cmap", ['gray', None]) +@pytest.mark.parametrize("cmap", ["gray", None]) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_log_negative_value(cmap): - s = hs.signals.Signal2D(np.arange(10*10, dtype=float).reshape(10, 10)) + s = hs.signals.Signal2D(np.arange(10 * 10, dtype=float).reshape(10, 10)) s -= 49.5 if cmap: - s.plot(norm='log', cmap=cmap) + s.plot(norm="log", cmap=cmap) else: - s.plot(norm='log') + s.plot(norm="log") return plt.gcf() -@pytest.mark.parametrize("cmap", ['gray', None, 'preference']) +@pytest.mark.parametrize("cmap", ["gray", None, "preference"]) @pytest.mark.mpl_image_compare( - baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl) + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_navigator_colormap(cmap): - if cmap == 'preference': - hs.preferences.Plot.cmap_navigator = 'hot' + if cmap == "preference": + hs.preferences.Plot.cmap_navigator = "hot" cmap = None - s = hs.signals.Signal1D(np.arange(10*10*10).reshape(10, 10, 10)) - s.plot(navigator_kwds={'cmap':cmap}) + s = hs.signals.Signal1D(np.arange(10 * 10 * 10).reshape(10, 10, 10)) + s.plot(navigator_kwds={"cmap": cmap}) return s._plot.navigator_plot.figure -@pytest.mark.parametrize("autoscale", ['', 'xy', 'xv', 'xyv', 'v']) -@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) +@pytest.mark.parametrize("autoscale", ["", "xy", "xv", "xyv", "v"]) +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) def test_plot_autoscale(autoscale): s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) s.plot(autoscale=autoscale, axes_ticks=True) imf = s._plot.signal_plot ax = imf.ax - extend = [5.0, 10.0, 3., 10.0] + extend = [5.0, 10.0, 3.0, 10.0] ax.images[0].set_extent(extend) ax.set_xlim(5.0, 10.0) - ax.set_ylim(3., 10.0) + ax.set_ylim(3.0, 10.0) ax.images[0].norm.vmin = imf._vmin = 10 ax.images[0].norm.vmax = imf._vmax = 50 @@ -566,12 +687,16 @@ def test_plot_autoscale(autoscale): s.axes_manager.events.indices_changed.trigger(s.axes_manager) # Because we are hacking the vmin, vmax with matplotlib, we need to update # colorbar too - imf._colorbar.draw_all() + if Version(matplotlib.__version__) <= Version("3.6.0"): + # `draw_all` is deprecated in matplotlib 3.6.0 + imf._colorbar.draw_all() + else: + imf.figure.draw_without_rendering() return s._plot.signal_plot.figure -@pytest.mark.parametrize("autoscale", ['', 'v']) +@pytest.mark.parametrize("autoscale", ["", "v"]) def test_plot_autoscale_data_changed(autoscale): s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) s.plot(autoscale=autoscale, axes_ticks=True) @@ -582,33 +707,60 @@ def test_plot_autoscale_data_changed(autoscale): s.data = s.data / 2 s.events.data_changed.trigger(s) - if 'v' in autoscale: + if "v" in autoscale: np.testing.assert_allclose(imf._vmin, s.data.min()) np.testing.assert_allclose(imf._vmax, s.data.max()) else: np.testing.assert_allclose(imf._vmin, _vmin) np.testing.assert_allclose(imf._vmax, _vmax) -@pytest.mark.parametrize("axes_decor", ['all', 'off']) -@pytest.mark.parametrize("label", ['auto', ['b','g']]) -@pytest.mark.parametrize("colors", ['auto', ['b','g']]) -@pytest.mark.parametrize("alphas", [1.0, [0.9,0.9]]) -@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) -def test_plot_overlay(axes_decor,label,colors,alphas): + +def test_plot_vmin_vmax_error(): + s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) + with pytest.raises(TypeError): + s.plot(vmin=[0]) + + with pytest.raises(TypeError): + s.plot(vmin=np.array([0])) + + with pytest.raises(TypeError): + s.plot(vmin=(0,)) + + with pytest.raises(TypeError): + s.plot(vmax=[100]) + + with pytest.raises(TypeError): + s.plot(vmin=[0], vmax=[100]) + + +@pytest.mark.parametrize("axes_decor", ["all", "off"]) +@pytest.mark.parametrize("label", ["auto", ["b", "g"]]) +@pytest.mark.parametrize("colors", ["auto", ["b", "g"]]) +@pytest.mark.parametrize("alphas", [1.0, [0.9, 0.9]]) +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) +def test_plot_overlay(axes_decor, label, colors, alphas): s1 = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) - s2 = hs.signals.Signal2D(np.arange(99,-1,-1).reshape(10, 10)) - ax = hs.plot.plot_images([s1,s2], overlay=True, scalebar='all', - label=label, suptitle=False, - axes_decor=axes_decor, colors=colors, - alphas=alphas, pixel_size_factor=10) + s2 = hs.signals.Signal2D(np.arange(99, -1, -1).reshape(10, 10)) + ax = hs.plot.plot_images( + [s1, s2], + overlay=True, + scalebar="all", + label=label, + suptitle=False, + axes_decor=axes_decor, + colors=colors, + alphas=alphas, + pixel_size_factor=10, + ) return ax[0].figure def test_plot_scale_different_sign(): N = 10 - s = hs.signals.Signal2D(np.arange(N**2).reshape([10]*2)) + s = hs.signals.Signal2D(np.arange(N**2).reshape([10] * 2)) s2 = s.isig[:, ::-1] s2.axes_manager[0].scale = 1.0 s2.axes_manager[1].scale = -1.0 @@ -616,3 +768,300 @@ def test_plot_scale_different_sign(): s2.plot() assert s2._plot.signal_plot.pixel_units is not None assert s2._plot.signal_plot.scalebar is True + + +def test_plot_images_overlay_colorbar(): + s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) + hs.plot.plot_images([s, s], overlay=True, colorbar="single", axes_decor="off") + + +def test_plot_images_overlay_aspect_ratio(): + s = hs.signals.Signal2D(np.arange(100).reshape(2, 50)) + hs.plot.plot_images([s, s], overlay=True, axes_decor="off") + f = plt.gcf() + np.testing.assert_allclose((f.get_figwidth(), f.get_figheight()), (25.0, 1.0)) + + s = hs.signals.Signal2D(np.arange(100).reshape(20, 5)) + hs.plot.plot_images([s, s], overlay=True, axes_decor="off", scalebar="all") + f = plt.gcf() + np.testing.assert_allclose((f.get_figwidth(), f.get_figheight()), (2.0, 8.0)) + + +def test_plot_images_overlay_figsize(): + """Test figure size for different aspect ratio of image.""" + # Set reference figure size + plt.rcParams["figure.figsize"] = [6.4, 4.8] + + # aspect_ratio is 1 + s = hs.signals.Signal2D(np.random.random((10, 10))) + hs.plot.plot_images([s, s], overlay=True, scalebar="all", axes_decor="off") + f = plt.gcf() + np.testing.assert_allclose((f.get_figwidth(), f.get_figheight()), (4.8, 4.8)) + + # aspect_ratio is 64 / 48 + s = hs.signals.Signal2D(np.random.random((48, 64))) + hs.plot.plot_images([s, s], overlay=True, scalebar="all", axes_decor="off") + f = plt.gcf() + np.testing.assert_allclose((f.get_figwidth(), f.get_figheight()), (6.4, 4.8)) + + # aspect_ratio is 2 + s = hs.signals.Signal2D(np.random.random((10, 20))) + hs.plot.plot_images([s, s], overlay=True, scalebar="all", axes_decor="off") + f = plt.gcf() + np.testing.assert_allclose((f.get_figwidth(), f.get_figheight()), (6.4, 3.2)) + + # aspect_ratio is 0.5 + s = hs.signals.Signal2D(np.random.random((20, 10))) + hs.plot.plot_images([s, s], overlay=True, scalebar="all", axes_decor="off") + f = plt.gcf() + np.testing.assert_allclose((f.get_figwidth(), f.get_figheight()), (2.4, 4.8)) + + +def test_plot_images_overlay_vmin_warning(caplog): + s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) + with caplog.at_level(logging.WARNING): + hs.plot.plot_images([s, s], overlay=True, vmin=0) + + assert "`vmin` is ignored when overlaying images." in caplog.text + + +def test_plot_images_overlay_signals(): + s = hs.signals.Signal2D(np.arange(10 * 10).reshape(10, 10)) + + hs.plot.plot_images(s, overlay=True) + + s = hs.stack([s, -s]) + hs.plot.plot_images(s, overlay=True) + + +def test_plot_scalebar_error(): + s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) + with pytest.raises(ValueError): + hs.plot.plot_images([s, s], scalebar="unsupported_argument") + + +def test_plot_scalebar_list(): + s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) + ax0, ax1 = hs.plot.plot_images([s, s], scalebar=[0, 1]) + assert hasattr(ax0, "scalebar") + assert hasattr(ax1, "scalebar") + + ax0, ax1 = hs.plot.plot_images([s, s], scalebar=[0]) + assert hasattr(ax0, "scalebar") + assert not hasattr(ax1, "scalebar") + + +@pytest.mark.skipif( + Version(matplotlib.__version__) < Version("3.9.0"), + reason="Subfigures plotting requires matplotlib >= 3.9.0", +) +def test_plot_subfigures(): + rng = np.random.default_rng() + s = hs.signals.Signal2D(rng.random((10, 10, 10, 10, 10))) + fig = plt.figure(figsize=(10, 10)) + subfigs = fig.subfigures(1, 2, wspace=0.07) + s.plot(fig=subfigs[0]) + + +@pytest.mark.skipif( + Version(matplotlib.__version__) < Version("3.9.0"), + reason="Subfigures plotting requires matplotlib >= 3.9.0", +) +def test_plot_subfigures_change_navigation_indices(): + rng = np.random.default_rng() + s = hs.signals.Signal2D(rng.random((10, 10, 10, 10, 10))) + hs.preferences.Plot.use_subfigure = True + s.plot() + s.axes_manager.indices = (1, 2, 3) + # Set default setting back + hs.preferences.Plot.use_subfigure = False + + +def test_plot_images_bool(): + data = np.arange(100).reshape((10, 10)) > 50 + s = hs.signals.Signal2D(data) + + hs.plot.plot_images(s) + + +def test_plot_static_signal_nav(): + s = hs.signals.Signal2D(np.ones((20, 20, 10, 10))) + nav = hs.signals.Signal2D(np.ones((20, 20))) + s.plot(navigator=nav) + + +@lazifyTestClass +class TestDynamicNavigatorPlot: + def setup_method(self, method): + self.signal5d2d = hs.signals.Signal2D( + np.arange((10**5)).reshape( + ( + 10, + 10, + 10, + 10, + 10, + ) + ) + ) + self.signal6d2d = hs.signals.Signal2D( + np.arange((10**6)).reshape((10, 10, 10, 10, 10, 10)) + ) + self.signal4d1d = hs.signals.Signal1D( + np.arange((10**4)).reshape((10, 10, 10, 10)) + ) + self.signal5d1d = hs.signals.Signal1D( + np.arange((10**5)).reshape( + ( + 10, + 10, + 10, + 10, + 10, + ) + ) + ) + + def test_plot_5d(self): + import numpy as np + + import hyperspy.api as hs + + s = self.signal5d2d + nav = hs.signals.BaseSignal(np.arange((10 * 10 * 10)).reshape(10, 10, 10)) + s.plot(navigator=nav) + data1 = s._plot.navigator_plot._current_data + s.axes_manager.indices = (0, 0, 1) + data2 = s._plot.navigator_plot._current_data + assert not np.array_equal(data1, data2) + s.axes_manager.indices = (0, 2, 1) + data3 = s._plot.navigator_plot._current_data + assert np.array_equal(data2, data3) + + def test_plot_5d_2(self): + import numpy as np + + import hyperspy.api as hs + + s = self.signal5d2d + nav = hs.signals.Signal2D(np.arange((10 * 10 * 10)).reshape(10, 10, 10)) + s.plot(navigator=nav) + data1 = s._plot.navigator_plot._current_data + s.axes_manager.indices = (0, 0, 1) + data2 = s._plot.navigator_plot._current_data + assert not np.array_equal(data1, data2) + s.axes_manager.indices = (0, 2, 1) + data3 = s._plot.navigator_plot._current_data + assert np.array_equal(data2, data3) + + def test_plot_6d(self): + import numpy as np + + import hyperspy.api as hs + + s = self.signal6d2d + nav = hs.signals.BaseSignal( + np.arange((10 * 10 * 10 * 10)).reshape(10, 10, 10, 10) + ) + s.plot(navigator=nav) + data1 = s._plot.navigator_plot._current_data + s.axes_manager.indices = (0, 0, 0, 1) + data2 = s._plot.navigator_plot._current_data + assert not np.array_equal(data1, data2) + s.axes_manager.indices = (0, 2, 0, 1) + data3 = s._plot.navigator_plot._current_data + assert np.array_equal(data2, data3) + + def test_plot_4d_1dSignal(self): + import numpy as np + + import hyperspy.api as hs + + s = self.signal4d1d + nav = hs.signals.BaseSignal(np.arange((10 * 10 * 10)).reshape(10, 10, 10)) + s.plot(navigator=nav) + data1 = s._plot.navigator_plot._current_data + s.axes_manager.indices = (0, 0, 1) + data2 = s._plot.navigator_plot._current_data + assert not np.array_equal(data1, data2) + s.axes_manager.indices = (0, 2, 1) + data3 = s._plot.navigator_plot._current_data + assert np.array_equal(data2, data3) + + def test_plot_5d_1dsignal(self): + import numpy as np + + import hyperspy.api as hs + + s = self.signal5d1d + nav = hs.signals.BaseSignal( + np.arange((10 * 10 * 10 * 10)).reshape(10, 10, 10, 10) + ) + s.plot(navigator=nav) + data1 = s._plot.navigator_plot._current_data + s.axes_manager.indices = (0, 0, 0, 1) + data2 = s._plot.navigator_plot._current_data + assert not np.array_equal(data1, data2) + s.axes_manager.indices = (0, 2, 0, 1) + data3 = s._plot.navigator_plot._current_data + assert np.array_equal(data2, data3) + + +@pytest.mark.parametrize("axes_decor", ["all", "ticks", "off", None]) +def test_plot_images_axes_ticks(axes_decor): + # Axes ticks should be the same with `plot_images` and `Signal2D.plot` + + data = np.arange(100).reshape(10, 10) + + positive_axis = {"scale": 1, "size": 10, "name": "positive", "units": "px"} + negative_axis = {"scale": -1, "size": 10, "name": "negative", "units": "px"} + + s = hs.signals.Signal2D(data, axes=[positive_axis, negative_axis]) + + # No colorbar to ensure we can get the correct axis with plt.gca + s.plot(axes_ticks=True, colorbar=False, title="") + plot_ax = plt.gca() + + (plot_images_ax,) = hs.plot.plot_images( + s, colorbar=False, label="", axes_decor=axes_decor + ) + + assert np.allclose(plot_ax.get_xticks(), plot_images_ax.get_xticks()) + assert np.allclose(plot_ax.get_yticks(), plot_images_ax.get_yticks()) + assert np.allclose(plot_ax.get_xlim(), plot_images_ax.get_xlim()) + assert np.allclose(plot_ax.get_ylim(), plot_images_ax.get_ylim()) + + +@pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl +) +def test_plot_image_ax(): + s = hs.signals.Signal2D(np.arange(100).reshape(10, 10)) + s2 = -s + + # check that passing a non-iterable is working + fig, ax = plt.subplots() + hs.plot.plot_images(s, ax=ax, axes_decor="off") + + s_ = hs.stack([s, s2]) + fig, ax = plt.subplots() + with pytest.raises(ValueError): + # length of ax is not compatible + hs.plot.plot_images(s_, ax=ax, axes_decor="off") + + fig, ax = plt.subplots(ncols=2, nrows=1) + # axes_decor="off" to avoid ticks warning + hs.plot.plot_images(s_, ax=ax, axes_decor="off") + + with pytest.raises(ValueError): + # ax can't be iterable with overlay=True + hs.plot.plot_images([s, s2], ax=ax, overlay=True) + + fig, ax = plt.subplots(ncols=1, nrows=1) + hs.plot.plot_images([s, s2], ax=ax, overlay=True) + + fig, ax = plt.subplots(ncols=3, nrows=1) + # axes_decor="off" to avoid ticks warning + hs.plot.plot_images([s, s2], ax=ax[1:], axes_decor="off") + + return fig diff --git a/hyperspy/tests/drawing/test_plot_signal_tools.py b/hyperspy/tests/drawing/test_plot_signal_tools.py index 4c03bae6d1..075cc4b7e1 100644 --- a/hyperspy/tests/drawing/test_plot_signal_tools.py +++ b/hyperspy/tests/drawing/test_plot_signal_tools.py @@ -1,35 +1,41 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import matplotlib.pyplot as plt import numpy as np import pytest -from hyperspy import signals, components1d, datasets -from hyperspy._signals.signal1d import BackgroundRemoval -from hyperspy.signal_tools import ImageContrastEditor +import hyperspy.api as hs +from hyperspy import components1d, signals +from hyperspy.signal_tools import ( + BackgroundRemoval, + ImageContrastEditor, + Signal1DCalibration, + SpanSelectorInSignal1D, +) BASELINE_DIR = "plot_signal_tools" DEFAULT_TOL = 2.0 -STYLE_PYTEST_MPL = 'default' +STYLE_PYTEST_MPL = "default" -@pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL +) def test_plot_BackgroundRemoval(): pl = components1d.PowerLaw() pl.A.value = 1e10 @@ -38,55 +44,89 @@ def test_plot_BackgroundRemoval(): s.axes_manager[0].offset = 100 s.add_poissonian_noise(random_state=1) - br = BackgroundRemoval(s, - background_type='Power Law', - polynomial_order=2, - fast=False, - plot_remainder=True) + br = BackgroundRemoval( + s, + background_type="Power Law", + polynomial_order=2, + fast=False, + plot_remainder=True, + ) - br.span_selector.set_initial((105, 150)) - br.span_selector.onmove_callback() + br.span_selector.extents = (105, 150) + # will draw the line + br.span_selector_changed() + # will update the right axis br.span_selector_changed() return br.signal._plot.signal_plot.figure +def test_plot_BackgroundRemoval_change_background(): + pl = components1d.PowerLaw() + pl.A.value = 1e10 + pl.r.value = 3 + s = signals.Signal1D(pl.function(np.arange(100, 200))) + s.axes_manager[0].offset = 100 + s.add_gaussian_noise(100) + + br = BackgroundRemoval( + s, + background_type="Power Law", + polynomial_order=2, + fast=False, + plot_remainder=True, + ) + + br.span_selector.extents = (105, 150) + # will draw the line + br.span_selector_changed() + # will update the right axis + br.span_selector_changed() + assert isinstance(br.background_estimator, components1d.PowerLaw) + br.background_type = "Polynomial" + assert isinstance(br.background_estimator, type(components1d.Polynomial())) + + def test_plot_BackgroundRemoval_close_figure(): s = signals.Signal1D(np.arange(1000).reshape(10, 100)) - br = BackgroundRemoval(s, background_type='Gaussian') + br = BackgroundRemoval(s, background_type="Gaussian") signal_plot = s._plot.signal_plot - assert len(signal_plot.events.closed.connected) == 5 + assert len(signal_plot.events.closed.connected) == 3 + assert len(s._plot.events.closed.connected) == 1 assert len(s.axes_manager.events.indices_changed.connected) == 4 s._plot.close() - assert not br._fit in s.axes_manager.events.indices_changed.connected - assert not br.disconnect in signal_plot.events.closed.connected + assert br._fit not in s.axes_manager.events.indices_changed.connected + assert len(s._plot.events.closed.connected) == 0 + assert len(signal_plot.events.closed.connected) == 0 def test_plot_BackgroundRemoval_close_tool(): s = signals.Signal1D(np.arange(1000).reshape(10, 100)) - br = BackgroundRemoval(s, background_type='Gaussian') - br.span_selector.set_initial((20, 40)) - br.span_selector.onmove_callback() + br = BackgroundRemoval(s, background_type="Gaussian") + br.span_selector.extents = (20, 40) br.span_selector_changed() signal_plot = s._plot.signal_plot - assert len(signal_plot.events.closed.connected) == 5 + assert len(signal_plot.events.closed.connected) == 3 + assert len(s._plot.events.closed.connected) == 1 assert len(s.axes_manager.events.indices_changed.connected) == 4 br.on_disabling_span_selector() - assert not br._fit in s.axes_manager.events.indices_changed.connected + assert br._fit not in s.axes_manager.events.indices_changed.connected s._plot.close() - assert not br.disconnect in signal_plot.events.closed.connected + assert len(s._plot.events.closed.connected) == 0 + assert len(signal_plot.events.closed.connected) == 0 -@pytest.mark.mpl_image_compare(baseline_dir=BASELINE_DIR, - tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL) +@pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL +) @pytest.mark.parametrize("gamma", (0.7, 1.2)) @pytest.mark.parametrize("percentile", (["0.15th", "99.85th"], ["0.25th", "99.75th"])) def test_plot_contrast_editor(gamma, percentile): - np.random.seed(1) - data = np.random.random(size=(10, 10, 100, 100))*1000 - data += np.arange(10*10*100*100).reshape((10, 10, 100, 100)) + rng = np.random.default_rng(1) + data = rng.random(size=(10, 10, 100, 100)) * 1000 + data += np.arange(10 * 10 * 100 * 100).reshape((10, 10, 100, 100)) s = signals.Signal2D(data) s.plot(gamma=gamma, vmin=percentile[0], vmax=percentile[1]) ceditor = ImageContrastEditor(s._plot.signal_plot) @@ -98,27 +138,171 @@ def test_plot_contrast_editor(gamma, percentile): @pytest.mark.parametrize("norm", ("linear", "log", "power", "symlog")) def test_plot_contrast_editor_norm(norm): - np.random.seed(1) - data = np.random.random(size=(100, 100))*1000 - data += np.arange(100*100).reshape((100, 100)) + rng = np.random.default_rng(1) + data = rng.random(size=(100, 100)) * 1000 + data += np.arange(100 * 100).reshape((100, 100)) s = signals.Signal2D(data) s.plot(norm=norm) ceditor = ImageContrastEditor(s._plot.signal_plot) if norm == "log": # test log with negative numbers - s2 = s - 5E3 + s2 = s - 5e3 s2.plot(norm=norm) _ = ImageContrastEditor(s._plot.signal_plot) assert ceditor.norm == norm.capitalize() def test_plot_contrast_editor_complex(): - s = datasets.example_signals.object_hologram() + s = hs.data.wave_image(random_state=0) + fft = s.fft(True) fft.plot(True, vmin=None, vmax=None) ceditor = ImageContrastEditor(fft._plot.signal_plot) assert ceditor.bins == 250 np.testing.assert_allclose(ceditor._vmin, fft._plot.signal_plot._vmin) np.testing.assert_allclose(ceditor._vmax, fft._plot.signal_plot._vmax) - np.testing.assert_allclose(ceditor._vmin, 1.495977361e+3) - np.testing.assert_allclose(ceditor._vmax, 3.568838458887e+17) + np.testing.assert_allclose(ceditor._vmin, 0.2002909426101699) + np.testing.assert_allclose(ceditor._vmax, 1074314272.3907123) + + +def test_plot_constrast_editor_setting_changed(): + # Test that changing setting works + rng = np.random.default_rng(1) + data = rng.random(size=(100, 100)) * 1000 + data += np.arange(100 * 100).reshape((100, 100)) + s = signals.Signal2D(data) + s.plot() + ceditor = ImageContrastEditor(s._plot.signal_plot) + ceditor.span_selector.extents = (3e3, 5e3) + ceditor.update_span_selector_traits() + np.testing.assert_allclose(ceditor.ss_left_value, 3e3) + np.testing.assert_allclose(ceditor.ss_right_value, 5e3) + assert ceditor.auto + # Do a cycle to trigger traits changed + ceditor.auto = False + assert not ceditor.auto + ceditor.auto = True # reset and clear span selector + assert ceditor.auto + assert not ceditor.span_selector.get_visible() + assert not ceditor._is_selector_visible + assert not ceditor.line.line.get_visible() + ceditor.span_selector.extents = (3e3, 5e3) + ceditor.span_selector.set_visible(True) + ceditor.update_line() + assert ceditor._is_selector_visible + assert ceditor.line.line.get_visible() + + assert ceditor.bins == 24 + assert ceditor.line.axis.shape == (ceditor.bins,) + ceditor.bins = 50 + assert ceditor.bins == 50 + assert ceditor.line.axis.shape == (ceditor.bins,) + + # test other parameters + ceditor.linthresh = 0.1 + assert ceditor.image.linthresh == 0.1 + + ceditor.linscale = 0.5 + assert ceditor.image.linscale == 0.5 + + +def test_plot_constrast_editor_auto_indices_changed(): + rng = np.random.default_rng(1) + data = rng.random(size=(10, 10, 100, 100)) * 1000 + data += np.arange(10 * 10 * 100 * 100).reshape((10, 10, 100, 100)) + s = signals.Signal2D(data) + s.plot() + ceditor = ImageContrastEditor(s._plot.signal_plot) + ceditor.span_selector.extents = (3e3, 5e3) + ceditor.update_span_selector_traits() + s.axes_manager.indices = (0, 1) + # auto is None by default, the span selector need to be removed: + assert not ceditor.span_selector.get_visible() + assert not ceditor._is_selector_visible + ref_value = (100020.046452, 110953.450532) + np.testing.assert_allclose(ceditor._get_current_range(), ref_value) + + # Change auto to False + ceditor.auto = False + s.axes_manager.indices = (0, 2) + # vmin, vmax shouldn't have changed + np.testing.assert_allclose(ceditor._get_current_range(), ref_value) + + +def test_plot_constrast_editor_reset(): + rng = np.random.default_rng(1) + data = rng.random(size=(100, 100)) * 1000 + data += np.arange(100 * 100).reshape((100, 100)) + s = signals.Signal2D(data) + s.plot() + ceditor = ImageContrastEditor(s._plot.signal_plot) + ceditor.span_selector.extents = (3e3, 5e3) + ceditor._update_image_contrast() + vmin, vmax = 36.559113, 10960.787649 + np.testing.assert_allclose(ceditor._vmin, vmin) + np.testing.assert_allclose(ceditor._vmax, vmax) + np.testing.assert_allclose(ceditor._get_current_range(), (3e3, 5e3)) + + ceditor.reset() + assert not ceditor.span_selector.get_visible() + assert not ceditor._is_selector_visible + np.testing.assert_allclose(ceditor._get_current_range(), (vmin, vmax)) + np.testing.assert_allclose(ceditor.image._vmin, vmin) + np.testing.assert_allclose(ceditor.image._vmax, vmax) + + +def test_plot_constrast_editor_apply(): + rng = np.random.default_rng(1) + data = rng.random(size=(100, 100)) * 1000 + data += np.arange(100 * 100).reshape((100, 100)) + s = signals.Signal2D(data) + s.plot() + ceditor = ImageContrastEditor(s._plot.signal_plot) + ceditor.span_selector.extents = (3e3, 5e3) + ceditor._update_image_contrast() + image_vmin_vmax = ceditor.image._vmin, ceditor.image._vmax + ceditor.apply() + assert not ceditor.span_selector.get_visible() + assert not ceditor._is_selector_visible + np.testing.assert_allclose( + (ceditor.image._vmin, ceditor.image._vmax), + image_vmin_vmax, + ) + + +def test_span_selector_in_signal1d(): + s = signals.Signal1D(np.arange(1000).reshape(10, 100)) + calibration_tool = SpanSelectorInSignal1D(s) + calibration_tool.span_selector.extents = (20, 40) + calibration_tool.span_selector_changed() + calibration_tool.span_selector.extents = (10.1, 10.2) + calibration_tool.span_selector_changed() + + +def test_span_selector_in_signal1d_model(): + m = hs.data.two_gaussians().create_model() + calibration_tool = SpanSelectorInSignal1D(m) + assert len(m.signal._plot.signal_plot.ax_lines) == 2 + assert m.signal is calibration_tool.signal + calibration_tool.span_selector.extents = (40, 60) + calibration_tool.span_selector_changed() + calibration_tool.span_selector_switch(False) + assert calibration_tool.span_selector is None + + +def test_signal1d_calibration(): + s = signals.Signal1D(np.arange(1000).reshape(10, 100)) + s.axes_manager[-1].scale = 0.1 + calibration_tool = Signal1DCalibration(s) + np.testing.assert_allclose( + calibration_tool.span_selector.snap_values, s.axes_manager.signal_axes[0].axis + ) + calibration_tool.span_selector.extents = (2.0, 4.0) + calibration_tool.span_selector_changed() + assert calibration_tool.ss_left_value == 2.0 + assert calibration_tool.ss_right_value == 4.0 + calibration_tool.span_selector.extents = (3.02, 5.09) + np.testing.assert_allclose(calibration_tool.span_selector.extents, (3.0, 5.1)) + calibration_tool.span_selector_changed() + np.testing.assert_allclose(calibration_tool.ss_left_value, 3.0) + np.testing.assert_allclose(calibration_tool.ss_right_value, 5.1) diff --git a/hyperspy/tests/drawing/test_plot_widgets.py b/hyperspy/tests/drawing/test_plot_widgets.py index 6c18cdf8ca..ab9b25b9ca 100644 --- a/hyperspy/tests/drawing/test_plot_widgets.py +++ b/hyperspy/tests/drawing/test_plot_widgets.py @@ -1,35 +1,36 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import matplotlib import numpy as np import pytest +import hyperspy.api as hs from hyperspy.drawing import widgets +from hyperspy.misc.test_utils import mock_event from hyperspy.signals import Signal1D, Signal2D -baseline_dir = 'plot_widgets' +baseline_dir = "plot_widgets" default_tol = 2.0 -style_pytest_mpl = 'default' +style_pytest_mpl = "default" -class TestPlotLine2DWidget(): - +class TestPlotLine2DWidget: def setup_method(self, method): # Create test image 100x100 pixels: self.im = Signal2D(np.arange(10000).reshape([100, 100])) @@ -40,21 +41,21 @@ def setup_method(self, method): def test_init(self): assert self.line2d.axes_manager == self.im.axes_manager assert self.line2d.linewidth == 1 - assert self.line2d.color == 'red' + assert self.line2d.color == "red" assert self.line2d._size == np.array([0]) np.testing.assert_allclose(self.line2d._pos, np.array([[0, 0], [1.2, 0]])) assert self.line2d.position == ([0.0, 0.0], [1.2, 0.0]) np.testing.assert_allclose(self.line2d.indices[0], np.array([0, 0])) np.testing.assert_allclose(self.line2d.indices[1], np.array([1, 0])) - np.testing.assert_allclose(self.line2d.get_centre(), np.array([0.6, 0.])) + np.testing.assert_allclose(self.line2d.get_centre(), np.array([0.6, 0.0])) def test_position(self): self.line2d.position = ([12.0, 60.0], [36.0, 96.0]) assert self.line2d.position == ([12.0, 60.0], [36.0, 96.0]) np.testing.assert_allclose(self.line2d.indices[0], np.array([10, 50])) np.testing.assert_allclose(self.line2d.indices[1], np.array([30, 80])) - np.testing.assert_allclose(self.line2d.get_centre(), np.array([24., 78.])) + np.testing.assert_allclose(self.line2d.get_centre(), np.array([24.0, 78.0])) def test_position_snap_position(self): self.line2d.snap_position = True @@ -62,14 +63,14 @@ def test_position_snap_position(self): np.testing.assert_allclose(self.line2d.position, ([12.0, 61.2], [36.0, 96.0])) np.testing.assert_allclose(self.line2d.indices[0], np.array([10, 51])) np.testing.assert_allclose(self.line2d.indices[1], np.array([30, 80])) - np.testing.assert_allclose(self.line2d.get_centre(), np.array([24., 78.6])) + np.testing.assert_allclose(self.line2d.get_centre(), np.array([24.0, 78.6])) def test_indices(self): self.line2d.indices = ([10, 50], [30, 80]) np.testing.assert_allclose(self.line2d.indices[0], np.array([10, 50])) np.testing.assert_allclose(self.line2d.indices[1], np.array([30, 80])) assert self.line2d.position == ([12.0, 60.0], [36.0, 96.0]) - np.testing.assert_allclose(self.line2d.get_centre(), np.array([24., 78.])) + np.testing.assert_allclose(self.line2d.get_centre(), np.array([24.0, 78.0])) def test_length(self): x = 10 @@ -78,8 +79,9 @@ def test_length(self): y = 20 self.line2d.position = ([20.0, 10.0], [20.0 + x, 10 + y]) - np.testing.assert_almost_equal(self.line2d.get_line_length(), - np.sqrt(x**2 + y**2)) + np.testing.assert_almost_equal( + self.line2d.get_line_length(), np.sqrt(x**2 + y**2) + ) def test_change_size(self): # Need to plot the signal to set the mpl axis to the widget @@ -87,16 +89,16 @@ def test_change_size(self): self.line2d.set_mpl_ax(self.im._plot.signal_plot.ax) self.line2d.position = ([0.0, 0.0], [50.0, 50.0]) - assert self.line2d.size == (0, ) + assert self.line2d.size == (0,) self.line2d.increase_size() - assert self.line2d.size == (1.2, ) + assert self.line2d.size == (1.2,) self.line2d.increase_size() - assert self.line2d.size == (2.4, ) + assert self.line2d.size == (2.4,) self.line2d.decrease_size() - assert self.line2d.size == (1.2, ) + assert self.line2d.size == (1.2,) - self.line2d.size = (4.0, ) - assert self.line2d.size == (4.0, ) + self.line2d.size = (4.0,) + assert self.line2d.size == (4.0,) def test_change_size_snap_size(self): # Need to plot the signal to set the mpl axis to the widget @@ -108,12 +110,12 @@ def test_change_size_snap_size(self): assert self.line2d.position == ([12.0, 60.0], [36.0, 96.0]) np.testing.assert_allclose(self.line2d.indices[0], np.array([10, 50])) np.testing.assert_allclose(self.line2d.indices[1], np.array([30, 80])) - np.testing.assert_allclose(self.line2d.get_centre(), np.array([24., 78.])) + np.testing.assert_allclose(self.line2d.get_centre(), np.array([24.0, 78.0])) assert self.line2d.size == np.array([0]) self.line2d.size = [3] np.testing.assert_allclose(self.line2d.size, np.array([2.4])) - self.line2d.size = (5, ) + self.line2d.size = (5,) np.testing.assert_allclose(self.line2d.size, np.array([4.8])) self.line2d.size = np.array([7.4]) np.testing.assert_allclose(self.line2d.size, np.array([7.2])) @@ -126,13 +128,14 @@ def test_change_size_snap_size_different_scale(self): assert self.line2d.axes[1].scale == 1.2 self.line2d.snap_size = True # snapping size with the different axes scale is not supported - assert self.line2d.snap_size == False + assert self.line2d.snap_size is False - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_line2d(self): self.im.plot() - self.line2d.color = 'green' + self.line2d.color = "green" self.line2d.position = ([12.0, 60.0], [36.0, 96.0]) self.line2d.set_mpl_ax(self.im._plot.signal_plot.ax) assert self.line2d.ax == self.im._plot.signal_plot.ax @@ -142,8 +145,8 @@ def test_plot_line2d(self): line2d.set_mpl_ax(self.im._plot.signal_plot.ax) line2d.position = ([40.0, 20.0], [96.0, 36.0]) line2d.linewidth = 4 - line2d.size = (15.0, ) - assert line2d.size == (15.0, ) + line2d.size = (15.0,) + assert line2d.size == (15.0,) line2d_snap_all = widgets.Line2DWidget(self.im.axes_manager) line2d_snap_all.snap_all = True @@ -152,19 +155,18 @@ def test_plot_line2d(self): np.testing.assert_allclose(line2d_snap_all.position[0], [50.4, 60.0]) np.testing.assert_allclose(line2d_snap_all.position[1], [96.0, 54.0]) - line2d_snap_all.size = (15.0, ) + line2d_snap_all.size = (15.0,) np.testing.assert_allclose(line2d_snap_all.size[0], 14.4) np.testing.assert_allclose(line2d_snap_all.size[0], 14.4) return self.im._plot.signal_plot.figure -class TestPlotCircleWidget(): - +class TestPlotCircleWidget: def setup_method(self, method): # Create test image 100x100 pixels: N = 100 - im = Signal2D(np.arange(N**2).reshape([N]*2)) + im = Signal2D(np.arange(N**2).reshape([N] * 2)) im.axes_manager[0].scale = 1.2 im.axes_manager[1].scale = 1.2 circle = widgets.CircleWidget(im.axes_manager) @@ -210,8 +212,7 @@ def test_change_size(self): assert circle.size == size -class TestPlotRangeWidget(): - +class TestPlotRangeWidget: def setup_method(self, method): s = Signal1D(np.arange(50)) s.axes_manager[0].scale = 1.2 @@ -219,26 +220,33 @@ def setup_method(self, method): self.s = s self.range_widget = range_widget - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + def test_snap_position_span_None(self): + # When span is None, there shouldn't an error + assert self.range_widget.span is None + self.range_widget.snap_position = True + assert self.range_widget.snap_position + + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_range(self): s = self.s range_widget = self.range_widget s.plot() range_widget.set_mpl_ax(s._plot.signal_plot.ax) assert range_widget.ax == s._plot.signal_plot.ax - assert range_widget.color == 'red' # default color - assert range_widget.position == (0.0, ) - assert range_widget.size == (1.2, ) - assert range_widget.span.rect.get_alpha() == 0.5 + assert range_widget.color == "r" # default color + assert range_widget.position == (0.0,) + assert range_widget.size == (1.2,) + assert range_widget.span.artists[0].get_alpha() == 0.25 - w = widgets.RangeWidget(s.axes_manager, color='blue') + w = widgets.RangeWidget(s.axes_manager, color="blue") w.set_mpl_ax(s._plot.signal_plot.ax) w.set_ibounds(left=4, width=3) - assert w.color == 'blue' - color_rgba = matplotlib.colors.to_rgba('blue', alpha=0.5) - assert w.span.rect.get_fc() == color_rgba - assert w.span.rect.get_ec() == color_rgba + assert w.color == "blue" + color_rgba = matplotlib.colors.to_rgba("blue", alpha=0.25) + assert w.span.artists[0].get_fc() == color_rgba + assert w.span.artists[0].get_ec() == color_rgba np.testing.assert_allclose(w.position[0], 4.8) np.testing.assert_allclose(w.size[0], 3.6) @@ -247,14 +255,16 @@ def test_plot_range(self): assert w2.ax == s._plot.signal_plot.ax w2.set_bounds(left=24.0, width=12.0) - w2.color = 'green' - assert w2.color == 'green' + assert w2.position[0] == 24.0 + assert w2.size[0] == 12.0 + w2.color = "green" + assert w2.color == "green" w2.alpha = 0.25 assert w2.alpha == 0.25 return s._plot.signal_plot.figure - @pytest.mark.parametrize('render_figure', [True, False]) + @pytest.mark.parametrize("render_figure", [True, False]) def test_set_on(self, render_figure): s = self.s range_widget = self.range_widget @@ -263,7 +273,7 @@ def test_set_on(self, render_figure): range_widget._is_on = False range_widget.set_on(True, render_figure=render_figure) - assert range_widget.span.visible + assert range_widget.span.get_visible() range_widget.set_on(False, render_figure=render_figure) assert range_widget.span is None @@ -276,55 +286,265 @@ def test_update(self): range_widget.set_mpl_ax(s._plot.signal_plot.ax) range_widget.span.update() - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) + @pytest.mark.mpl_image_compare( + baseline_dir=baseline_dir, tolerance=default_tol, style=style_pytest_mpl + ) def test_plot_range_Signal2D(self): im = Signal2D(np.arange(10 * 10).reshape((10, 10))) im.axes_manager[0].scale = 0.1 im.axes_manager[1].scale = 5 im.plot() - range_h = widgets.RangeWidget(im.axes_manager, direction='horizontal') + range_h = widgets.RangeWidget(im.axes_manager, direction="horizontal") range_h.set_mpl_ax(im._plot.signal_plot.ax) - range_v = widgets.RangeWidget(im.axes_manager, direction='vertical', - color='blue') + range_v = widgets.RangeWidget( + im.axes_manager, direction="vertical", color="blue" + ) range_v.axes = (im.axes_manager[1],) range_v.set_mpl_ax(im._plot.signal_plot.ax) - assert range_v.position == (0.0, ) - assert range_v.size == (5.0, ) + assert range_v.position == (0.0,) + assert range_v.size == (5.0,) range_v.set_bounds(left=20.0, width=15.0) - assert range_v.position == (20.0, ) - assert range_v.size == (15.0, ) + assert range_v.position == (20.0,) + assert range_v.size == (15.0,) return im._plot.signal_plot.figure - @pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, - tolerance=default_tol, style=style_pytest_mpl) - def test_plot_ModifiableSpanSelector(self): - s = self.s - s.plot() - from hyperspy.drawing._widgets.range import ModifiableSpanSelector - ax = s._plot.signal_plot.ax - span_v = ModifiableSpanSelector(ax, direction='vertical') - span_v.set_initial((15, 20)) - assert span_v.range == (15, 20) - - span_v.range = (25, 30) - assert span_v.range == (25, 30) - - span_h = ModifiableSpanSelector(ax, direction='horizontal', - rectprops={'color': 'g', 'alpha': 0.2}) - color_rgba = matplotlib.colors.to_rgba('g', alpha=0.2) - assert span_h.rect.get_fc() == color_rgba - assert span_h.rect.get_ec() == color_rgba - span_h.set_initial((50.4, 55.2)) - np.testing.assert_allclose(span_h.range[0], 50.4) - np.testing.assert_allclose(span_h.range[1], 55.2) - - span_h.range = (40, 45) - assert span_h.range == (40, 45) - ax.figure.canvas.draw_idle() - return s._plot.signal_plot.figure +class TestSquareWidget: + @pytest.mark.parametrize("button", ("right-click", "left-click")) + def test_jump_click(self, button): + im = Signal2D(np.arange(10 * 10 * 10 * 10).reshape((10, 10, 10, 10))) + im.axes_manager[0].scale = 0.1 + im.axes_manager[1].scale = 5 + im.plot() + + jump = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key="shift", + button=button, + xdata=0.5, + ydata=10, + ) + im._plot.pointer._onjumpclick(event=jump) + current_index = [el.index for el in im.axes_manager.navigation_axes] + assert current_index == [5, 2] + + def test_jump_click_single_trigger(self): + im = Signal2D(np.arange(10 * 10 * 10 * 10).reshape((10, 10, 10, 10))) + im.axes_manager[0].scale = 0.1 + im.axes_manager[1].scale = 5 + im.plot() + + def count_calls(obj): + count_calls.counter += 1 + + count_calls.counter = 0 + + im.axes_manager.events.indices_changed.connect(count_calls) + + jump = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key="shift", + button="left-click", + xdata=0.5, + ydata=10, + ) + im._plot.pointer._onjumpclick(event=jump) + assert count_calls.counter == 1 + current_index = [el.index for el in im.axes_manager.navigation_axes] + assert current_index == [5, 2] + + def test_jump_click_1d(self): + im = Signal1D(np.arange(10 * 10).reshape((10, 10))) + im.axes_manager[0].scale = 0.1 + im.plot() + jump = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key="shift", + button="left-click", + xdata=1, + ydata=0.2, + ) + im._plot.pointer._onjumpclick(event=jump) + current_index = [el.index for el in im.axes_manager.navigation_axes] + assert current_index == [2] + + def test_jump_click_1d_vertical(self): + im = Signal2D(np.arange(10 * 10 * 10).reshape((10, 10, 10))) + im.axes_manager[0].scale = 0.1 + im.plot() + jump = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key="shift", + button="left-click", + xdata=0.2, + ) + im._plot.pointer._onjumpclick(event=jump) + current_index = [el.index for el in im.axes_manager.navigation_axes] + assert current_index == [2] + + def test_jump_click_out_of_bounds(self): + im = Signal2D(np.arange(10 * 10 * 10 * 10).reshape((10, 10, 10, 10))) + im.axes_manager[0].scale = 0.1 + im.axes_manager[1].scale = 5 + im.plot() + + jump = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key="shift", + button="left-click", + xdata=-5, + ydata=100, + ) + im._plot.pointer._onjumpclick(event=jump) + current_index = [el.index for el in im.axes_manager.navigation_axes] + assert current_index == [0, 9] # maybe this should fail and return [0,0] + + def test_drag_continuous_update(self): + im = Signal2D(np.arange(10 * 10 * 10 * 10).reshape((10, 10, 10, 10))) + im.axes_manager[0].scale = 1 + im.axes_manager[1].scale = 1 + im.plot() + + def count_calls(obj): + count_calls.counter += 1 + + count_calls.counter = 0 + im.axes_manager.events.indices_changed.connect(count_calls) + + widget = im._plot.pointer + pick = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key=None, + button="left-click", + xdata=0.5, + ydata=0.5, + artist=widget.patch[0], + ) + widget.onpick(pick) + assert widget.picked + drag_events = [] + for i in np.linspace(0.5, 5, 40): + drag = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key=None, + button="left-click", + xdata=i, + ydata=0.5, + artist=widget.patch[0], + ) + drag_events.append(drag) + for d in drag_events: + widget._onmousemove(d) + assert count_calls.counter == 5 + assert im.axes_manager.navigation_axes[0].index == 5 + assert im.axes_manager.navigation_axes[1].index == 0 + + def test_drag_continuous_update1d(self): + im = Signal2D(np.arange(10 * 10 * 10).reshape((10, 10, 10))) + im.axes_manager[0].scale = 1 + im.plot() + + def count_calls(obj): + count_calls.counter += 1 + + count_calls.counter = 0 + im.axes_manager.events.indices_changed.connect(count_calls) + + widget = im._plot.pointer + pick = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key=None, + button="left-click", + xdata=0.5, + ydata=0.5, + artist=widget.patch[0], + ) + widget.onpick(pick) + assert widget.picked + drag_events = [] + for i in np.linspace(0.5, 5, 40): + drag = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key=None, + button="left-click", + xdata=i, + ydata=0.5, + artist=widget.patch[0], + ) + drag_events.append(drag) + for d in drag_events: + widget._onmousemove(d) + assert count_calls.counter == 5 + assert im.axes_manager.navigation_axes[0].index == 5 + + def test_drag_continuous_update1d_no_change(self): + # drag down and check that it doesn't change the index + im = Signal2D(np.arange(10 * 10 * 10).reshape((10, 10, 10))) + im.axes_manager[0].scale = 1 + im.plot() + + def count_calls(obj): + count_calls.counter += 1 + + count_calls.counter = 0 + im.axes_manager.events.indices_changed.connect(count_calls) + + widget = im._plot.pointer + pick = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key=None, + button="left-click", + xdata=0.5, + ydata=0.5, + artist=widget.patch[0], + ) + widget.onpick(pick) + assert widget.picked + drag_events = [] + for i, j in zip(np.linspace(0.5, 5, 40), np.linspace(0.0, 0.49, 40)): + drag = mock_event( + im._plot.navigator_plot.figure, + im._plot.navigator_plot.figure.canvas, + key=None, + button="left-click", + xdata=j, + ydata=i, + artist=widget.patch[0], + ) + drag_events.append(drag) + for d in drag_events: + widget._onmousemove(d) + assert count_calls.counter == 0 + assert im.axes_manager.navigation_axes[0].index == 0 + # drag down and check that it doesn't change + + +def test_widgets_non_uniform_axis(): + s = hs.data.luminescence_signal(uniform=False).isig[10:20] + roi = hs.roi.SpanROI() + s.plot() + roi.add_widget(s) + np.testing.assert_allclose(roi.left, 1.61375935) + np.testing.assert_allclose(roi.right, 1.61887959) + + roi2 = hs.roi.SpanROI() + with pytest.raises(ValueError): + roi2.add_widget(s, snap=True) + + w = list(roi.widgets)[0] + with pytest.raises(ValueError): + w.snap_size = True diff --git a/hyperspy/tests/drawing/test_texts.py b/hyperspy/tests/drawing/test_texts.py new file mode 100644 index 0000000000..a0783c1998 --- /dev/null +++ b/hyperspy/tests/drawing/test_texts.py @@ -0,0 +1,112 @@ +# Copyright 2007-2024 The HyperSpy developers +# +# This file is part of HyperSpy. +# +# HyperSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# HyperSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with HyperSpy. If not, see . + +import dask.array as da +import matplotlib.pyplot as plt +import numpy as np +import pytest + +from hyperspy._signals.signal2d import Signal2D +from hyperspy.drawing._markers.texts import Texts +from hyperspy.misc.test_utils import update_close_figure + +BASELINE_DIR = "markers" +DEFAULT_TOL = 2.0 +STYLE_PYTEST_MPL = "default" +plt.style.use(STYLE_PYTEST_MPL) + + +class TestTextCollection: + @pytest.fixture + def data(self): + d = np.empty((3,), dtype=object) + for i in np.ndindex(d.shape): + d[i] = np.stack([np.arange(3), np.ones(3) * i], axis=1) + return d + + @pytest.fixture + def lazy_data(self): + d = np.empty((3,), dtype=object) + for i in np.ndindex(d.shape): + d[i] = np.stack([np.arange(3), np.ones(3) * i], axis=1) + d = da.from_array(d, chunks=(1, 1, 1)) + + return d + + @pytest.mark.parametrize( + "texts", + ( + ("test",), + "test", + ("test", "test"), + "ragged_text", + ), + ) + @pytest.mark.parametrize("iter_data", ("lazy_data", "data")) + def test_iterating_marker(self, texts, request, iter_data): + data = request.getfixturevalue(iter_data) + s = Signal2D(np.ones((3, 5, 6))) + s.plot() + ragged_texts = texts == "ragged_text" + if ragged_texts: + t = np.empty((3,), dtype=object) + for i in np.ndindex(t.shape): + t[i] = ("test" + str(i),) + texts = t + markers = Texts(offsets=data, texts=texts) + children_before = s._plot.signal_plot.ax.get_children() + s.add_marker(markers) + s.axes_manager.navigation_axes[0].index = 1 + children_after = s._plot.signal_plot.ax.get_children() + assert len(children_after) - len(children_before) == 1 + + @pytest.mark.mpl_image_compare( + baseline_dir=BASELINE_DIR, tolerance=DEFAULT_TOL, style=STYLE_PYTEST_MPL + ) + def test_text_marker_plot(self): + s = Signal2D(np.ones((3, 5, 6))) + s.data[:, :, ::2] = np.nan + markers = Texts(offsets=[[2.0, 3.0]], texts=("test",), sizes=(20,)) + s.add_marker(markers, render_figure=True) + return s._plot.signal_plot.figure + + +def _test_text_collection_close(): + signal = Signal2D(np.ones((10, 10))) + markers = Texts(offsets=[[1, 1], [4, 4]], texts=("test",)) + signal.add_marker(markers) + return signal + + +@update_close_figure() +def test_text_collection_close(): + return _test_text_collection_close() + + +def test_text_collection_close_render(): + s = Signal2D(np.ones((2, 10, 10))) + markers = Texts( + offsets=[[1, 1], [4, 4]], texts=("test",), sizes=(10,), color=("black",) + ) + s.plot() + children_before = s._plot.signal_plot.ax.get_children() + s.add_marker(markers, render_figure=True) + children_after = s._plot.signal_plot.ax.get_children() + assert len(children_after) - len(children_before) == 1 + + markers.close(render_figure=True) + assert len(s._plot.signal_plot.ax.get_children()) == len(children_before) diff --git a/hyperspy/tests/drawing/test_utils.py b/hyperspy/tests/drawing/test_utils.py index 77779c7430..6c41af352a 100644 --- a/hyperspy/tests/drawing/test_utils.py +++ b/hyperspy/tests/drawing/test_utils.py @@ -1,38 +1,40 @@ -# Copyright 2007-2021 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from unittest.mock import Mock -import numpy as np import matplotlib +import numpy as np import pytest -from hyperspy.drawing.utils import create_figure, contrast_stretching +from hyperspy.drawing.utils import contrast_stretching, create_figure def test_create_figure(): - if matplotlib.get_backend() == "agg": - pytest.xfail("{} backend does not support on_close event.".format( - matplotlib.get_backend())) + # needs to run inside the function to make sure the correct backend is used + # not possible to pytest.mark.skipif decorator + if matplotlib.get_backend().lower() == "agg": + pytest.skip("'agg' backend does not support on_close event.") dummy_function = Mock() - fig = create_figure(window_title="test title", - _on_figure_window_close=dummy_function) - assert isinstance(fig, matplotlib.figure.Figure) == True + fig = create_figure( + window_title="test title", _on_figure_window_close=dummy_function + ) + assert isinstance(fig, matplotlib.figure.Figure) is True matplotlib.pyplot.close(fig) dummy_function.assert_called_once_with() @@ -41,7 +43,7 @@ def test_contrast_stretching(): data = np.arange(100) assert contrast_stretching(data, 1, 99) == (1, 99) assert contrast_stretching(data, 1.0, 99.0) == (1, 99) - assert contrast_stretching(data, '1th', '99th') == (0.99, 98.01) - assert contrast_stretching(data, '0.05th', '99.95th') == (0.0495, 98.9505) + assert contrast_stretching(data, "1th", "99th") == (0.99, 98.01) + assert contrast_stretching(data, "0.05th", "99.95th") == (0.0495, 98.9505) # vmin, vmax are to set in conftest.py assert contrast_stretching(data, None, None) == (0.0, 99.0) diff --git a/hyperspy/tests/drawing/test_widget.py b/hyperspy/tests/drawing/test_widget.py index 3e1d325b9d..187538e699 100644 --- a/hyperspy/tests/drawing/test_widget.py +++ b/hyperspy/tests/drawing/test_widget.py @@ -1,32 +1,161 @@ -# Copyright 2007-2020 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . import numpy as np -import pytest -from hyperspy.drawing import widget -from hyperspy.signals import Signal1D + +from hyperspy import roi, signals +from hyperspy.drawing import widget, widgets +from hyperspy.misc.test_utils import mock_event + def test_get_step(): - s = Signal1D(np.zeros((4, 4))) + s = signals.Signal1D(np.zeros((4, 4))) axis = s.axes_manager.navigation_axes[0] - step = widget.ResizableDraggableWidgetBase._get_step(s,s.axes_manager.navigation_axes[0]) - assert(step == 1) + step = widget.ResizableDraggableWidgetBase._get_step( + s, s.axes_manager.navigation_axes[0] + ) + assert step == 1 axis.index = 3 - step = widget.ResizableDraggableWidgetBase._get_step(s,s.axes_manager.navigation_axes[0]) - assert(step == 1) - - + step = widget.ResizableDraggableWidgetBase._get_step( + s, s.axes_manager.navigation_axes[0] + ) + assert step == 1 + + +def test_scalebar_remove(): + im = signals.Signal2D(-np.arange(10000).reshape([100, 100])) + for ax in im.axes_manager.signal_axes: + ax.scale = 1.2 + ax.units = "nm" + im.plot() + assert im._plot.signal_plot.ax.scalebar is not None + im._plot.signal_plot.ax.scalebar.remove() + + +def test_remove_widget_line(): + s = signals.Signal1D(np.arange(10 * 25).reshape(10, 25)) + s.plot() + + ax = s._plot.navigator_plot.ax + assert len(ax.get_lines()) == 2 + assert isinstance(s._plot.pointer, widgets.HorizontalLineWidget) + assert len(s._plot.pointer.patch) == 1 + + # Remove pointer + s._plot.pointer.close(render_figure=True) + assert len(ax.lines) == 1 + assert len(s._plot.pointer.patch) == 1 + + im = signals.Signal2D(np.arange(10 * 25 * 25).reshape(10, 25, 25)) + im.plot() + + ax = im._plot.navigator_plot.ax + assert len(ax.get_lines()) == 2 + assert isinstance(im._plot.pointer, widgets.VerticalLineWidget) + assert len(im._plot.pointer.patch) == 1 + + # Remove pointer + im._plot.pointer.close(render_figure=True) + assert len(ax.lines) == 1 + assert len(im._plot.pointer.patch) == 1 + + +def test_calculate_size(): + s = signals.Signal2D(np.arange(10000).reshape(10, 10, 10, 10)) + + # Test that scalebar.calculate_size passes only positive value to closest_nice_number + s.axes_manager[0].scale = -1 + s.plot() + + +def test_adding_removing_resizers_on_pick_event(): + """ + Test adding and removing resizers on pick events + """ + s = signals.Signal2D(np.random.random((10, 10))) + + xx2, yy2, xx1, yy1 = 0, 0, 2, 2 + + shiftx = 5 + shifty = 3 + + rect_roi0 = roi.RectangularROI(xx2, yy2, xx1, yy1) + rect_roi1 = roi.RectangularROI( + xx2 + shiftx, yy2 + shifty, xx1 + shiftx, yy1 + shifty + ) + s.plot() + + _ = rect_roi0.interactive(s) + _ = rect_roi1.interactive(s) + widget0 = list(rect_roi0.widgets)[0] + widget1 = list(rect_roi1.widgets)[0] + + assert not widget0.picked + assert not widget1.picked + + fig = s._plot.signal_plot.figure + + # PickEvent on widget0 + mouseevent0 = mock_event(fig, fig.canvas, xdata=1, ydata=1, artist=widget0.patch[0]) + pickevent0 = mock_event( + fig, fig.canvas, artist=widget0.patch[0], mouseevent=mouseevent0 + ) + + # PickEvent on widget1 + mouseevent1 = mock_event(fig, fig.canvas, xdata=6, ydata=4, artist=widget1.patch[0]) + pickevent1 = mock_event( + fig, fig.canvas, artist=widget1.patch[0], mouseevent=mouseevent1 + ) + + # PickEvent outside widget0 and widget1 + mouseevent2 = mock_event(fig, fig.canvas, xdata=8, ydata=8) + pickevent2 = mock_event(fig, fig.canvas, mouseevent=mouseevent2) + + widget0.onpick(pickevent0) + widget1.onpick(pickevent0) + assert widget0.picked + assert widget0._resizers_on + assert not widget1.picked + assert not widget1._resizers_on + + widget0.onpick(pickevent2) + widget1.onpick(pickevent2) + assert not widget0.picked + assert not widget0._resizers_on + assert not widget1.picked + assert not widget1._resizers_on + + widget0.onpick(pickevent1) + widget1.onpick(pickevent1) + assert not widget0.picked + assert not widget0._resizers_on + assert widget1.picked + assert widget1._resizers_on + + widget0.onpick(pickevent0) + widget1.onpick(pickevent0) + assert widget0.picked + assert widget0._resizers_on + assert not widget1.picked + assert not widget1._resizers_on + + widget0.onpick(pickevent2) + widget1.onpick(pickevent2) + assert not widget0.picked + assert not widget0._resizers_on + assert not widget1.picked + assert not widget1._resizers_on diff --git a/hyperspy/tests/external/test_mpfit.py b/hyperspy/tests/external/test_mpfit.py index bdc6a460cf..78aeca9cde 100644 --- a/hyperspy/tests/external/test_mpfit.py +++ b/hyperspy/tests/external/test_mpfit.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2007-2020 The HyperSpy developers +# Copyright 2007-2024 The HyperSpy developers # -# This file is part of HyperSpy. +# This file is part of HyperSpy. # -# HyperSpy is free software: you can redistribute it and/or modify +# HyperSpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# HyperSpy is distributed in the hope that it will be useful, +# HyperSpy is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . +# along with HyperSpy. If not, see . from copy import deepcopy @@ -179,4 +179,3 @@ def test_rosenbrock_error(): m = mpfit(myfunctrosenbrock, p0, maxiter=1) assert m.status == 5 np.testing.assert_allclose(m.params, p0, rtol=5e-7) - diff --git a/hyperspy/tests/io/FEI_new/128x128-TEM_search.emi b/hyperspy/tests/io/FEI_new/128x128-TEM_search.emi deleted file mode 100644 index 371dd262dc..0000000000 Binary files a/hyperspy/tests/io/FEI_new/128x128-TEM_search.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/128x128-TEM_search_1.ser b/hyperspy/tests/io/FEI_new/128x128-TEM_search_1.ser deleted file mode 100644 index 998e2b9ae1..0000000000 Binary files a/hyperspy/tests/io/FEI_new/128x128-TEM_search_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/128x128_TEM_acquire-sum1.emi b/hyperspy/tests/io/FEI_new/128x128_TEM_acquire-sum1.emi deleted file mode 100644 index 908b55d4e1..0000000000 Binary files a/hyperspy/tests/io/FEI_new/128x128_TEM_acquire-sum1.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/128x128_TEM_acquire-sum1_1.ser b/hyperspy/tests/io/FEI_new/128x128_TEM_acquire-sum1_1.ser deleted file mode 100644 index 9dab89824c..0000000000 Binary files a/hyperspy/tests/io/FEI_new/128x128_TEM_acquire-sum1_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/128x128x5-diffraction_preview.emi b/hyperspy/tests/io/FEI_new/128x128x5-diffraction_preview.emi deleted file mode 100644 index 1e16710385..0000000000 Binary files a/hyperspy/tests/io/FEI_new/128x128x5-diffraction_preview.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/128x128x5-diffraction_preview_1.ser b/hyperspy/tests/io/FEI_new/128x128x5-diffraction_preview_1.ser deleted file mode 100644 index 1a6df44004..0000000000 Binary files a/hyperspy/tests/io/FEI_new/128x128x5-diffraction_preview_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS.emi b/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS.emi deleted file mode 100644 index 91f6380ad0..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_1.ser b/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_1.ser deleted file mode 100644 index dc5d29d74a..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_2.ser b/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_2.ser deleted file mode 100644 index 8e35acbb24..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_2.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_copy.emi b/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_copy.emi deleted file mode 100644 index 91f6380ad0..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_copy.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_copy_1.ser b/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_copy_1.ser deleted file mode 100644 index dc5d29d74a..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-diffraction_imagel_5x5x256x256_EDS_copy_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS.emi b/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS.emi deleted file mode 100644 index e950b2dc2e..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS_1.ser b/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS_1.ser deleted file mode 100644 index 0c1cbddbf1..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS_2.ser b/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS_2.ser deleted file mode 100644 index a8e07e133b..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-line_profile_horizontal_5x128x128_EDS_2.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-spectrum_image_5x5x4000-not_square.emi b/hyperspy/tests/io/FEI_new/16x16-spectrum_image_5x5x4000-not_square.emi deleted file mode 100644 index 5abe2b31d7..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-spectrum_image_5x5x4000-not_square.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_new/16x16-spectrum_image_5x5x4000-not_square_1.ser b/hyperspy/tests/io/FEI_new/16x16-spectrum_image_5x5x4000-not_square_1.ser deleted file mode 100644 index c0394c4ce2..0000000000 Binary files a/hyperspy/tests/io/FEI_new/16x16-spectrum_image_5x5x4000-not_square_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/03_Scanning Preview.emi b/hyperspy/tests/io/FEI_old/03_Scanning Preview.emi deleted file mode 100644 index 73652c6a8f..0000000000 Binary files a/hyperspy/tests/io/FEI_old/03_Scanning Preview.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/03_Scanning Preview_1.ser b/hyperspy/tests/io/FEI_old/03_Scanning Preview_1.ser deleted file mode 100644 index 2acac674c1..0000000000 Binary files a/hyperspy/tests/io/FEI_old/03_Scanning Preview_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-2_point-spectra-2x1024.emi b/hyperspy/tests/io/FEI_old/16x16-2_point-spectra-2x1024.emi deleted file mode 100644 index 59813a9dd5..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-2_point-spectra-2x1024.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-2_point-spectra-2x1024_1.ser b/hyperspy/tests/io/FEI_old/16x16-2_point-spectra-2x1024_1.ser deleted file mode 100644 index f309e30f55..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-2_point-spectra-2x1024_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-line_profile_diagonal_10x1024.emi b/hyperspy/tests/io/FEI_old/16x16-line_profile_diagonal_10x1024.emi deleted file mode 100644 index dde00d3d3c..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-line_profile_diagonal_10x1024.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-line_profile_diagonal_10x1024_1.ser b/hyperspy/tests/io/FEI_old/16x16-line_profile_diagonal_10x1024_1.ser deleted file mode 100644 index 358aff7848..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-line_profile_diagonal_10x1024_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-line_profile_horizontal_10x1024.emi b/hyperspy/tests/io/FEI_old/16x16-line_profile_horizontal_10x1024.emi deleted file mode 100644 index e96f65e289..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-line_profile_horizontal_10x1024.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-line_profile_horizontal_10x1024_1.ser b/hyperspy/tests/io/FEI_old/16x16-line_profile_horizontal_10x1024_1.ser deleted file mode 100644 index ae2d1dcb2b..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-line_profile_horizontal_10x1024_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-point_spectrum-1x1024.emi b/hyperspy/tests/io/FEI_old/16x16-point_spectrum-1x1024.emi deleted file mode 100644 index 8770df6c91..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-point_spectrum-1x1024.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-point_spectrum-1x1024_1.ser b/hyperspy/tests/io/FEI_old/16x16-point_spectrum-1x1024_1.ser deleted file mode 100644 index c8a65c3e15..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-point_spectrum-1x1024_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-spectrum_image-5x5x1024.emi b/hyperspy/tests/io/FEI_old/16x16-spectrum_image-5x5x1024.emi deleted file mode 100644 index 592329ca96..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-spectrum_image-5x5x1024.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16-spectrum_image-5x5x1024_1.ser b/hyperspy/tests/io/FEI_old/16x16-spectrum_image-5x5x1024_1.ser deleted file mode 100644 index 76f44afaa3..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16-spectrum_image-5x5x1024_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire.emi b/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire.emi deleted file mode 100644 index 1ef8852726..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire_1.ser b/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire_1.ser deleted file mode 100644 index 3bd3a482a4..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire_2.ser b/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire_2.ser deleted file mode 100644 index 7957e77862..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_acquire_2.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search.emi b/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search.emi deleted file mode 100644 index 847e30dcf3..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search_1.ser b/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search_1.ser deleted file mode 100644 index 44056c3a4f..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search_2.ser b/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search_2.ser deleted file mode 100644 index ed1bf00368..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16_STEM_BF_DF_search_2.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview.emi b/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview.emi deleted file mode 100644 index d8c4310e2b..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview_1.ser b/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview_1.ser deleted file mode 100644 index 7e05272e84..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview_2.ser b/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview_2.ser deleted file mode 100644 index 7ec8235745..0000000000 Binary files a/hyperspy/tests/io/FEI_old/16x16x5_STEM_BF_DF_preview_2.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire.emi b/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire.emi deleted file mode 100644 index b56d7ba4cb..0000000000 Binary files a/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire.npy b/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire.npy deleted file mode 100644 index d101948d12..0000000000 Binary files a/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire.npy and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire_1.ser b/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire_1.ser deleted file mode 100644 index a1b3ea5677..0000000000 Binary files a/hyperspy/tests/io/FEI_old/64x64_TEM_images_acquire_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/64x64_diffraction_acquire.emi b/hyperspy/tests/io/FEI_old/64x64_diffraction_acquire.emi deleted file mode 100644 index d6bfd41817..0000000000 Binary files a/hyperspy/tests/io/FEI_old/64x64_diffraction_acquire.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/64x64_diffraction_acquire_1.ser b/hyperspy/tests/io/FEI_old/64x64_diffraction_acquire_1.ser deleted file mode 100644 index 586008d351..0000000000 Binary files a/hyperspy/tests/io/FEI_old/64x64_diffraction_acquire_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/64x64x5_TEM_preview.emi b/hyperspy/tests/io/FEI_old/64x64x5_TEM_preview.emi deleted file mode 100644 index 751140e283..0000000000 Binary files a/hyperspy/tests/io/FEI_old/64x64x5_TEM_preview.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/64x64x5_TEM_preview_1.ser b/hyperspy/tests/io/FEI_old/64x64x5_TEM_preview_1.ser deleted file mode 100644 index 1b26bc0d50..0000000000 Binary files a/hyperspy/tests/io/FEI_old/64x64x5_TEM_preview_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/X - Au NP EELS_2.ser b/hyperspy/tests/io/FEI_old/X - Au NP EELS_2.ser deleted file mode 100644 index 13d32879c1..0000000000 Binary files a/hyperspy/tests/io/FEI_old/X - Au NP EELS_2.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata.emi b/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata.emi deleted file mode 100644 index 9641bed217..0000000000 Binary files a/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata_1.ser b/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata_1.ser deleted file mode 100644 index 55244f24e6..0000000000 Binary files a/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata_2.ser b/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata_2.ser deleted file mode 100644 index 0425e527fb..0000000000 Binary files a/hyperspy/tests/io/FEI_old/more_ser_then_emi_metadata_2.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/no_AcquireDate.emi b/hyperspy/tests/io/FEI_old/no_AcquireDate.emi deleted file mode 100644 index 66d05a7558..0000000000 Binary files a/hyperspy/tests/io/FEI_old/no_AcquireDate.emi and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/no_AcquireDate_1.ser b/hyperspy/tests/io/FEI_old/no_AcquireDate_1.ser deleted file mode 100644 index da3f4e7933..0000000000 Binary files a/hyperspy/tests/io/FEI_old/no_AcquireDate_1.ser and /dev/null differ diff --git a/hyperspy/tests/io/FEI_old/non_float_meta_value_zeroed.tar.gz b/hyperspy/tests/io/FEI_old/non_float_meta_value_zeroed.tar.gz deleted file mode 100644 index 9fb1da47d3..0000000000 Binary files a/hyperspy/tests/io/FEI_old/non_float_meta_value_zeroed.tar.gz and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000000.img b/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000000.img deleted file mode 100644 index d7e995401d..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000000.img and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000001.map b/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000001.map deleted file mode 100644 index 2a6a99ebed..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000001.map and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000002.map b/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000002.map deleted file mode 100644 index 240830e310..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000002.map and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000003.map b/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000003.map deleted file mode 100644 index 1109c84692..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000003.map and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000004.map b/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000004.map deleted file mode 100644 index e032d5a5de..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000004.map and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000005.map b/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000005.map deleted file mode 100644 index 45d6d73fba..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000005.map and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000006.pts b/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000006.pts deleted file mode 100644 index 66f6fa78d1..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/Sample/00_View000/View000_0000006.pts and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/met03.EDS b/hyperspy/tests/io/JEOL_files/met03.EDS deleted file mode 100644 index eec0f04fdf..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/met03.EDS and /dev/null differ diff --git a/hyperspy/tests/io/JEOL_files/rawdata.ASW b/hyperspy/tests/io/JEOL_files/rawdata.ASW deleted file mode 100644 index a58661fef1..0000000000 Binary files a/hyperspy/tests/io/JEOL_files/rawdata.ASW and /dev/null differ diff --git a/hyperspy/tests/io/__init__.py b/hyperspy/tests/io/__init__.py deleted file mode 100644 index c41a8ae211..0000000000 --- a/hyperspy/tests/io/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2007-2021 The HyperSpy developers -# -# This file is part of HyperSpy. -# -# HyperSpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# HyperSpy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with HyperSpy. If not, see . diff --git a/hyperspy/tests/io/blockfile_data/test1.blo b/hyperspy/tests/io/blockfile_data/test1.blo deleted file mode 100644 index 8e3b7d4528..0000000000 Binary files a/hyperspy/tests/io/blockfile_data/test1.blo and /dev/null differ diff --git a/hyperspy/tests/io/blockfile_data/test2.blo b/hyperspy/tests/io/blockfile_data/test2.blo deleted file mode 100644 index 6f3cf7b775..0000000000 Binary files a/hyperspy/tests/io/blockfile_data/test2.blo and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/16x16_12bit_packed_8bit.bcf b/hyperspy/tests/io/bruker_data/16x16_12bit_packed_8bit.bcf deleted file mode 100755 index fa7d7e68e2..0000000000 Binary files a/hyperspy/tests/io/bruker_data/16x16_12bit_packed_8bit.bcf and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/30x30_16bit.npy b/hyperspy/tests/io/bruker_data/30x30_16bit.npy deleted file mode 100644 index 206274b1e7..0000000000 Binary files a/hyperspy/tests/io/bruker_data/30x30_16bit.npy and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/30x30_16bit_ds.npy b/hyperspy/tests/io/bruker_data/30x30_16bit_ds.npy deleted file mode 100644 index 845f617fd2..0000000000 Binary files a/hyperspy/tests/io/bruker_data/30x30_16bit_ds.npy and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/30x30_instructively_packed_16bit_compressed.bcf b/hyperspy/tests/io/bruker_data/30x30_instructively_packed_16bit_compressed.bcf deleted file mode 100644 index 751e7cc6d2..0000000000 Binary files a/hyperspy/tests/io/bruker_data/30x30_instructively_packed_16bit_compressed.bcf and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/30x30_original_metadata.json b/hyperspy/tests/io/bruker_data/30x30_original_metadata.json deleted file mode 100644 index 13c3a7bd5a..0000000000 --- a/hyperspy/tests/io/bruker_data/30x30_original_metadata.json +++ /dev/null @@ -1,642 +0,0 @@ -{ - "Hardware": { - "TRTKnownHeader": { - "Type": "RTHardware", - "Size": 137 - }, - "ZeroPeakPosition": 95, - "ZeroPeakFrequency": 66666, - "Amplification": 20000.0, - "ShapingTime": 275000, - "XmlClassType": "TRTSpectrumHardwareHeader" - }, - "Detector": { - "TRTKnownHeader": { - "Type": "RTDetector", - "Version": 5, - "Size": 9932 - }, - "Technology": "SD3pr", - "Serial": "8273/74", - "Type": "XFlash 6|10", - "DetectorThickness": 0.45, - "SiDeadLayerThickness": 0.029, - "DetLayers": { - "Layer0": { - "Atom": "13", - "Thickness": "5E-3" - }, - "Layer1": { - "Atom": "7", - "Thickness": "8.5E-2" - }, - "Layer2": { - "Atom": "5", - "Thickness": "6E-3" - }, - "Layer3": { - "Atom": "6", - "Thickness": "6E-3" - }, - "Layer4": { - "Atom": "8", - "Thickness": "6.5E-2" - } - }, - "WindowType": "slew AP3.3", - "WindowLayers": { - "Layer0": { - "Atom": 5, - "Thickness": 0.013 - }, - "Layer1": { - "Atom": 6, - "Thickness": 0.145 - }, - "Layer2": { - "Atom": 7, - "Thickness": 0.045 - }, - "Layer3": { - "Atom": 8, - "Thickness": 0.085 - }, - "Layer4": { - "Atom": 13, - "Thickness": 0.035 - }, - "Layer5": { - "Atom": 14, - "Thickness": 380.0, - "RelativeArea": 0.23 - } - }, - "Corrections": { - "Escape": null, - "Tail": { - "FormulaType": "Internal", - "MainCorrection": 1 - }, - "Shelf": { - "FormulaType": "Internal", - "RangeStart": 0.08, - "RangeEnd": 10.0, - "MainCorrection": 1, - "Coefficient0": 1 - }, - "Shift": { - "FormulaType": "Internal", - "RangeStart": 0.08, - "RangeEnd": 0.555, - "MainCorrection": 1 - }, - "FWHMShift": null - }, - "CorrectionType": 3, - "ResponseFunctionCount": 21, - "SampleCount": 5, - "SampleOffset": -3, - "PulsePairResTimeCount": 0, - "PileUpMinEnergy": 1, - "PileUpWithBG": false, - "TailFactor": 1, - "ShelfFactor": 1, - "ShiftFactor": 0.3975, - "ShiftFactor2": -0.982, - "ShiftData": [ - 0.07, - 0.0058, - 0.183, - 0.0078, - 0.277, - 0.0058, - 0.555, - 0, - 1.1, - 0, - 3.293, - 0.0064, - 5.89, - 0, - 0, - 0, - 0, - 0 - ], - "ResponseFunction": [ - [ - 0, - 0.01, - 0.000801, - 0.01, - 0.00298, - 0.01, - 0.008902, - 0.01, - 0.025, - 0.010046, - 0.041098, - 0.013475, - 0.04702, - 0.017302, - 0.049199, - 0.019237, - 0.05, - 0.02 - ], - [ - 0, - 0.026417, - 0.003686, - 0.026417, - 0.013709, - 0.026417, - 0.04095, - 0.026417, - 0.115, - 0.026585, - 0.18905, - 0.039072, - 0.216292, - 0.053009, - 0.226314, - 0.060056, - 0.23, - 0.062835 - ], - [ - 0, - 0.03, - 0.005362, - 0.03, - 0.019937, - 0.03, - 0.059556, - 0.03, - 0.16725, - 0.030229, - 0.274944, - 0.047376, - 0.314563, - 0.066511, - 0.329139, - 0.076186, - 0.3345, - 0.08 - ], - [ - 0, - 0.025, - 0.007349, - 0.025, - 0.027328, - 0.025, - 0.081633, - 0.025, - 0.22925, - 0.025114, - 0.376867, - 0.033706, - 0.431172, - 0.043293, - 0.451151, - 0.048139, - 0.4585, - 0.05005 - ], - [ - 0, - 0.025, - 0.009626, - 0.025, - 0.035791, - 0.025, - 0.106915, - 0.025, - 0.30025, - 0.025, - 0.493585, - 0.025035, - 0.564709, - 0.025073, - 0.590874, - 0.025092, - 0.6005, - 0.0251 - ], - [ - 0, - 0.025, - 0.013761, - 0.025, - 0.051168, - 0.025, - 0.15285, - 0.025, - 0.42925, - 0.025, - 0.70565, - 0.025035, - 0.807332, - 0.025073, - 0.844738, - 0.025092, - 0.8585, - 0.0251 - ], - [ - 0, - 0.025, - 0.018394, - 0.025, - 0.068393, - 0.025, - 0.204305, - 0.025, - 0.57375, - 0.025, - 0.943195, - 0.025035, - 1.079108, - 0.025073, - 1.129106, - 0.025092, - 1.1475, - 0.0251 - ], - [ - 0, - 0.025, - 0.021968, - 0.025, - 0.081684, - 0.025, - 0.244008, - 0.025, - 0.68525, - 0.025, - 1.126492, - 0.025035, - 1.288816, - 0.025073, - 1.348531, - 0.025092, - 1.3705, - 0.0251 - ], - [ - 0, - 0.025, - 0.025864, - 0.025, - 0.096167, - 0.025, - 0.287273, - 0.025, - 0.80675, - 0.025, - 1.326227, - 0.025035, - 1.517333, - 0.025073, - 1.587636, - 0.025092, - 1.6135, - 0.0251 - ], - [ - 0, - 0.015, - 0.029334, - 0.015, - 0.109071, - 0.015, - 0.325818, - 0.015, - 0.915, - 0.015, - 1.504182, - 0.015035, - 1.720929, - 0.015073, - 1.800667, - 0.015092, - 1.83, - 0.0151 - ], - [ - 0, - 0.015, - 0.03153, - 0.015, - 0.117236, - 0.015, - 0.35021, - 0.015, - 0.9835, - 0.015, - 1.61679, - 0.015035, - 1.849764, - 0.015073, - 1.935471, - 0.015092, - 1.967, - 0.0151 - ], - [ - 0, - 0.015, - 0.03464, - 0.015, - 0.128798, - 0.015, - 0.38475, - 0.015, - 1.0805, - 0.015, - 1.776249, - 0.015035, - 2.032202, - 0.015073, - 2.12636, - 0.015092, - 2.161, - 0.0151 - ], - [ - 0, - 0, - 0.048088, - 0, - 0.178804, - 0, - 0.534129, - 0, - 1.5, - 0, - 2.465871, - 3.5e-05, - 2.821196, - 7.3e-05, - 2.951912, - 9.2e-05, - 3, - 0.0001 - ], - [ - 0, - 0, - 0.069279, - 0, - 0.257598, - 0, - 0.769501, - 0, - 2.161, - 0, - 3.552499, - 3.5e-05, - 4.064403, - 7.3e-05, - 4.252721, - 9.2e-05, - 4.322, - 0.0001 - ], - [ - 0, - 0, - 0.099623, - 0, - 0.370422, - 0, - 1.106536, - 0, - 3.1075, - 0, - 5.108463, - 3.5e-05, - 5.844577, - 7.3e-05, - 6.115378, - 9.2e-05, - 6.215, - 0.0001 - ], - [ - 0, - 0, - 0.134085, - 0, - 0.498566, - 0, - 1.489329, - 0, - 4.1825, - 0, - 6.875671, - 3.5e-05, - 7.866434, - 7.3e-05, - 8.230915, - 9.2e-05, - 8.365, - 0.0001 - ], - [ - 0, - 0, - 0.162313, - 0, - 0.603525, - 0, - 1.802863, - 0, - 5.063, - 0, - 8.323137, - 3.5e-05, - 9.522476, - 7.3e-05, - 9.963688, - 9.2e-05, - 10.125999, - 0.0001 - ], - [ - 0, - 0, - 0.192351, - 0, - 0.715217, - 0, - 2.136515, - 0, - 6, - 0, - 9.863485, - 3.5e-05, - 11.284782, - 7.3e-05, - 11.807649, - 9.2e-05, - 12, - 0.0001 - ], - [ - 0, - 0, - 0.24044, - 0, - 0.894022, - 0, - 2.670643, - 0, - 7.5, - 0, - 12.329357, - 0, - 14.105978, - 0, - 14.759561, - 0, - 15, - 0 - ], - [ - 0, - 0, - 0.320586, - 0, - 1.192029, - 0, - 3.560857, - 0, - 10, - 0, - 16.439142, - 0, - 18.80797, - 0, - 19.679415, - 0, - 20, - 0 - ], - [ - 0, - 0, - 1.60293, - 0, - 5.960146, - 0, - 17.804287, - 0, - 50, - 0, - 82.195709, - 0, - 94.039856, - 0, - 98.397072, - 0, - 100, - 0 - ] - ], - "XmlClassType": "TRTDetectorHeader" - }, - "Analysis": { - "TRTKnownHeader": { - "Type": "RTESMA", - "Size": 662 - }, - "PrimaryEnergy": 20.0, - "ReferenceFactor": -1, - "ReferenceStdDev": -1, - "BaseRefStdDev": 0.0007742422249, - "ElevationAngle": 35.0, - "AzimutAngle": 90.0, - "DoCoatCorrection": 2, - "CoatCorrection": null, - "XmlClassType": "TRTESMAHeader" - }, - "Spectrum": { - "Size": 82, - "Date": "4.10.2018", - "Time": "13:2:6", - "ChannelCount": 2048, - "CalibAbs": -0.47225277, - "CalibLin": 0.009999, - "SigmaAbs": 0.0003067640955, - "SigmaLin": 0.0004582905597, - "XmlClassType": "TRTSpectrumHeader" - }, - "DSP Configuration": { - "ImageWidth": 100, - "ImageHeight": 75, - "PixelAverage": 2, - "LineAverage": 1, - "SEBitCount": 12, - "ChannelCount": 2, - "Channel0": 1, - "ChannelName0": "AsB", - "ChannelName1": "SEI", - "CounterIndex": 1, - "CounterChannelUsed": 0, - "TiltAngle": 0, - "CounterMode": 0, - "PixelTime": 1, - "XmlClassType": "TRTDSPConfiguration" - }, - "Line counter": [ - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38930, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929, - 38929 - ], - "Stage": { - "X": 66940.81, - "Y": 54233.16, - "Z": 39194.77, - "Rotation": 326.10089, - "State": 7936, - "XmlClassType": "TRTSEMStageData" - }, - "Microscope": { - "HV": 20, - "WD": 8.45761, - "Mag": 1819.22595, - "DX": 1.66740910949363, - "DY": 1.66740910949362, - "Flags": 16776960, - "XmlClassType": "TRTSEMData" - } -} \ No newline at end of file diff --git a/hyperspy/tests/io/bruker_data/Hitachi_TM3030Plus.bcf b/hyperspy/tests/io/bruker_data/Hitachi_TM3030Plus.bcf deleted file mode 100644 index c8b1b9538d..0000000000 Binary files a/hyperspy/tests/io/bruker_data/Hitachi_TM3030Plus.bcf and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/Nope.bcf b/hyperspy/tests/io/bruker_data/Nope.bcf deleted file mode 100644 index ab6f4251b9..0000000000 Binary files a/hyperspy/tests/io/bruker_data/Nope.bcf and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/P45_the_default_job.bcf b/hyperspy/tests/io/bruker_data/P45_the_default_job.bcf deleted file mode 100644 index 83b67694e8..0000000000 Binary files a/hyperspy/tests/io/bruker_data/P45_the_default_job.bcf and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/bcf-edx-ebsd.bcf b/hyperspy/tests/io/bruker_data/bcf-edx-ebsd.bcf deleted file mode 100644 index b41f4796d3..0000000000 Binary files a/hyperspy/tests/io/bruker_data/bcf-edx-ebsd.bcf and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/bcf_v2_50x50px.bcf b/hyperspy/tests/io/bruker_data/bcf_v2_50x50px.bcf deleted file mode 100644 index c0fd0ed9af..0000000000 Binary files a/hyperspy/tests/io/bruker_data/bcf_v2_50x50px.bcf and /dev/null differ diff --git a/hyperspy/tests/io/bruker_data/bruker_m6_jetstream_file_example.spx b/hyperspy/tests/io/bruker_data/bruker_m6_jetstream_file_example.spx deleted file mode 100644 index 59f819782b..0000000000 --- a/hyperspy/tests/io/bruker_data/bruker_m6_jetstream_file_example.spx +++ /dev/null @@ -1,1467 +0,0 @@ - - - - - - - - RTHardware - 1 - 167 - - 30000 - 28046 - 7 - 96 - 10000 - 28913 - 4E4 - 130000 - 1 - 1 - - - -1 - BVJUUkVNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAACiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL6fGi/dGGFAN4lBYOWsY0C0yHa+n3owQAAAAAAAAAAAAAAAAAAAAAAA - 162 - - - - RTDetector - 3 - 9932 - - SDDpr - 13859M6_36 - XFlash 430 - 0.45 - 0.029 - eJyzcUkt8UmsTC0qtrMB0wYKjiX5ubZKhsZKCiEZmcnZeanFxbZKpq66xkr6UDWGUDXmKEos9ICKjOCKjKCKTFEUmSGbYwxVYoZbiQlUiQWqErhV+gj3AwCpRT07 - Beryllium - - - - - - - Internal - 1 - - - Internal - 8E-2 - 1E1 - 1 - 1 - - - Internal - 8E-2 - 5.55E-1 - 1 - - - - 3 - 21 - 5 - -3 - 6 - 1 - False - 0 - 0 - 1 - -1 - 1,0,1.74,0.007,4.512,0.0075,6.405,0.0085,8.637,0.009,15.775,0.006,25.271,-0.001,32.189999,-0.01,100,0, - 6.45,0.879382,7.15,1.043408,7.55,0.893671,8.35,0.919676,8.65,0.864889,9.65,1.077339, - 0,0.08287,0.001603,0.08287,0.00596,0.08287,0.017804,0.08287,0.05,0.08287,0.082196,0.082905,0.09404,0.082943,0.098397,0.082962,0.1,0.08297, - 0,0.08287,0.003206,0.08287,0.011921,0.08287,0.035609,0.08287,0.1,0.08287,0.164391,0.082905,0.188079,0.082943,0.196794,0.082962,0.2,0.08297, - 0,0.08287,0.004808,0.08287,0.017881,0.08287,0.053413,0.08287,0.15,0.08287,0.246587,0.082905,0.282119,0.082943,0.295192,0.082962,0.3,0.08297, - 0,0.08287,0.006411,0.08287,0.023841,0.08287,0.071218,0.08287,0.2,0.08287,0.328782,0.082905,0.376159,0.082943,0.393589,0.082962,0.4,0.08297, - 0,0.08287,0.008015,0.08287,0.029801,0.08287,0.089021,0.08287,0.25,0.08287,0.410979,0.082905,0.470199,0.082943,0.491985,0.082962,0.5,0.08297, - 0,0.08287,0.009618,0.08287,0.035761,0.08287,0.106825,0.08287,0.3,0.08287,0.493175,0.082905,0.564239,0.082943,0.590382,0.082962,0.6,0.08297, - 0,0.08287,0.011221,0.08287,0.041721,0.08287,0.12463,0.08287,0.35,0.08287,0.57537,0.082905,0.658279,0.082943,0.688779,0.082962,0.7,0.08297, - 0,0.08287,0.012824,0.08287,0.047681,0.08287,0.142434,0.08287,0.4,0.08287,0.657566,0.082905,0.752319,0.082943,0.787176,0.082962,0.8,0.08297, - 0,0.0001,0.014427,0.0001,0.053641,0.0001,0.160239,0.0001,0.45,0.0001,0.739761,0.000135,0.846359,0.000173,0.885573,0.000192,0.9,0.0002, - 0,0.0001,0.019235,0.0001,0.071522,0.0001,0.213652,0.0001,0.6,0.0001,0.986348,0.000135,1.128478,0.000173,1.180765,0.000192,1.2,0.0002, - 0,0.0001,0.024044,0.0001,0.089403,0.0001,0.267064,0.0001,0.75,0.0001,1.232936,0.000135,1.410597,0.000173,1.475956,0.000192,1.5,0.0002, - 0,0.0001,0.036867,0.0001,0.137084,0.0001,0.409498,0.0001,1.15,0.0001,1.890502,0.000135,2.162918,0.000173,2.263133,0.000192,2.3,0.0002, - 0,0.0001,0.072132,0.0001,0.268207,0.0001,0.801194,0.0001,2.25,0.000101,3.698806,0.000135,4.231793,0.000173,4.427869,0.000192,4.5,0.0002, - 0,0.0001,0.102587,0.0001,0.381449,0.0001,1.139474,0.0001,3.2,0.0001,5.260526,0.000135,6.018551,0.000173,6.297412,0.000192,6.4,0.0002, - 0,0.0001,0.118617,0.0001,0.441049,0.0001,1.317519,0.0001,3.7,0.000101,6.08248,0.000135,6.958951,0.000173,7.281383,0.000192,7.4,0.0002, - 0,0.0001,0.137851,0.0001,0.512571,0.0001,1.531165,0.0001,4.3,0.000101,7.068826,0.000135,8.087421,0.000173,8.462149,0.000192,8.6,0.0002, - 0,0.0001,0.179528,0.0001,0.667536,0.0001,1.994083,0.0001,5.6,0.000101,9.205923,0.000135,10.532468,0.000173,11.020473,0.000192,11.2,0.0002, - 0,0.0001,0.251661,0.0001,0.935744,0.0001,2.795272,0.0001,7.85,0.000101,12.904728,0.000135,14.764259,0.000173,15.448339,0.000192,15.7,0.0002, - 0,0.0001,0.323792,0.0001,1.203949,0.0001,3.596467,0.0001,10.1,0.0001,16.603535,0.000135,18.996048,0.000173,19.876209,0.000192,20.200001,0.0002, - 0,0.0001,0.403937,0.0001,1.501957,0.0001,4.486683,0.0001,12.6,0.000101,20.713322,0.000135,23.698042,0.000173,24.796062,0.000192,25.200001,0.0002, - 0,0.0001,1.554843,0.0001,5.781342,0.0001,17.270157,0.0001,48.5,0.000105,79.729843,0.000447,91.218658,0.00083,95.44516,0.001024,97,0.0011, - - - - RTXrfHeader - 2 - 760 - - XS-51 - 0032 - 19.9.2018 - ixtube-Rh-30 - 2001932 - 19.9.2018 - 50 - 199 - 45 - 7.8E1 - 1.2E1 - - Tube window - 1 - 4 - 1E2 - 1 - - Lens - 9.651050594E-3,7.533430458,3.075298988,7.193812162E-2,1.727155646,7.270352118E-2,1.139159233E-1,0,0,0 - 5E1 - - Empty - - 9E1 - 6E1 - 1.3E1 - 1.6E1 - 6.5E-3 - 0 - 0 - 1E3 - 0 - 100 - 8E2 - 0 - Helium - - - - RTXrfFPModelHeader - 1 - 3286 - - Bulk_Lens_SDD - 1 - 7E-1,5E1 - All - 1 - 1E-1 - 1E-1 - 0 - 1000 - 0 - 2E-2 - 6E1,1,0,0,0,0,0,0,0,0, - 5 - - K-1 - 11,21 - K - 4 - 1.302438831,-2.681697817,1.875104894,-4.766993738E-1,4.129461525E-2 - - - K-1 - 22,42 - K - 6 - -5.370375406,3.830190536,-9.099801935E-1,1.074233039E-1,-6.559829896E-3,1.976122515E-4,-2.319053879E-6 - - - K-1 - 43,56 - K - 4 - -1.782778086E2,2.640126219E1,-1.432188955,3.39073981E-2,-2.946009224E-4 - - - L-2 - 57,92 - L2 - 3 - -5.079474231E-1,4.77858695E-1,-3.745662063E-2,9.465406752E-4 - - - L-3 - 57,92 - L3 - 3 - 1.463849562,-1.15763212E-1,1.439767784E-2,-3.876626096E-4 - - TDetectorSDDConfig,TDetectorSDDConfig,TDetectorSDDConfig,TDetectorSDDConfig,TDetectorSDDConfig,TDetectorSDDConfig,DoModelPileUp - 1.016440958E-7,-1.189818041E-9,-6.991636318E-2,-3.706944848E-4,-1.664973031E-10,-1.142439589E-10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - - - - RTQuantitatorConfig - 1 - 66 - - 30 - 0 - 1E-3 - 1E-3 - - - - 82 - 23.3.2021 - - 4096 - -9.5550444E-1 - 9.999E-3 - 9.892997828E-4 - 4.466391048E-4 - - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,1,0,2,0,3,0,1,0,1,2,2,1,0,1,0,1,1,1,0,2,0,1,3,3,2,2,0,0,0,1,3,2,1,2,3,1,9,61,144,408,953,2146,4229,7679,12670,19028,25470,31398,34826,34748,31515,26028,19347,12916,7951,4442,2140,1009,381,169,48,16,4,5,2,1,2,0,0,0,0,0,0,0,0,0,1,0,0,7,59,44,37,24,30,28,29,24,21,21,23,35,26,26,25,18,26,22,15,21,21,22,28,23,23,18,24,13,22,26,22,19,13,23,25,26,23,12,25,22,15,23,22,11,25,24,17,17,20,27,23,19,27,20,16,13,19,25,14,19,17,24,20,17,13,18,23,14,20,15,10,18,17,20,21,23,15,20,20,17,22,21,27,13,21,16,23,19,21,22,19,20,19,32,25,27,21,34,25,17,24,21,11,22,11,17,15,15,24,20,21,18,21,24,21,15,25,15,25,25,25,18,20,13,18,13,15,11,15,16,13,23,11,12,16,18,12,16,18,31,30,29,50,25,31,30,27,33,30,43,31,35,38,38,72,77,103,149,186,264,327,420,555,678,792,905,1044,1221,1332,1393,1487,1365,1348,1173,987,835,670,461,365,273,223,197,148,141,131,102,102,74,80,82,77,84,77,79,84,78,110,99,121,106,110,103,90,112,98,88,72,67,56,55,53,61,46,49,56,39,49,35,33,39,34,31,28,30,28,44,37,40,59,54,67,61,74,68,89,77,80,77,84,61,70,70,58,57,58,69,67,72,60,68,65,93,90,104,91,115,116,123,153,177,196,201,253,295,274,303,278,280,242,218,195,159,126,91,72,60,56,57,41,44,53,67,61,35,60,54,70,75,95,65,87,85,93,107,113,120,167,173,154,171,201,177,156,158,134,140,106,93,85,79,67,63,54,53,75,72,89,71,119,122,162,226,349,504,787,1404,2063,2965,4510,6311,8844,11619,14878,18619,22111,25479,28145,29802,30838,30873,29279,27042,23663,20173,16662,13236,10124,7327,5323,3693,2496,1573,1040,696,502,402,335,429,495,680,851,1141,1534,1906,2307,2826,3162,3690,4053,4352,4603,4427,4494,4116,3661,3211,2770,2176,1650,1272,927,668,463,301,207,118,77,56,36,30,23,19,9,12,14,15,15,6,12,22,20,15,16,19,21,13,16,15,23,21,22,12,29,21,24,21,36,37,33,28,30,26,26,24,11,19,14,15,16,21,20,17,25,20,17,13,20,18,8,16,10,17,10,21,16,26,15,25,31,27,27,25,18,33,33,19,31,29,39,34,26,21,26,29,24,26,26,22,30,38,27,33,33,38,26,22,28,27,39,33,33,35,37,35,30,46,46,43,41,35,38,31,29,31,26,34,38,35,36,39,35,35,45,37,39,41,47,33,27,32,38,37,45,35,36,42,51,45,41,38,45,52,47,55,47,46,42,55,41,53,52,48,63,38,54,45,49,52,61,63,47,45,57,44,64,68,67,70,70,47,55,75,63,65,79,74,55,61,54,74,77,66,53,55,62,66,47,55,58,71,57,57,54,78,62,68,67,65,67,59,65,59,67,64,60,81,76,60,59,79,92,69,85,73,76,87,77,109,89,106,106,111,95,135,106,121,119,148,122,132,161,134,110,142,118,120,128,111,106,97,87,91,104,80,94,117,98,119,99,105,91,105,86,85,98,94,93,99,81,95,118,103,106,105,111,98,105,73,97,101,86,102,104,92,106,89,99,94,95,118,109,100,95,94,99,112,100,116,103,104,124,106,109,115,104,115,101,113,113,132,129,116,108,108,108,102,119,109,110,130,107,110,118,134,111,129,130,121,110,127,130,134,125,139,135,138,141,119,136,144,155,147,153,167,133,122,177,139,119,125,137,139,141,129,128,145,129,110,160,123,153,141,147,152,149,151,147,155,150,148,147,148,146,162,153,142,154,177,144,150,170,164,163,152,150,189,176,152,168,161,162,179,162,153,156,164,172,155,164,185,179,187,175,182,177,172,187,178,197,201,184,182,196,179,206,195,185,171,191,177,213,181,185,190,200,190,169,186,209,218,189,215,179,194,202,176,209,215,197,204,222,203,229,244,172,222,244,231,236,241,243,227,271,275,263,245,277,287,263,287,289,247,313,277,283,309,263,268,254,273,264,276,263,245,249,256,259,234,227,231,250,244,234,231,242,205,261,282,225,262,228,236,244,252,245,258,217,243,253,232,251,253,233,264,255,235,268,262,259,241,268,248,257,251,260,242,267,248,266,280,270,237,239,256,278,261,298,268,281,252,271,283,278,261,293,267,253,271,278,265,261,284,253,264,291,297,272,260,269,304,304,274,275,281,329,299,314,284,283,271,335,338,320,287,284,273,266,261,301,265,287,292,259,313,287,297,286,286,292,286,289,294,288,291,272,320,300,290,331,322,270,307,327,329,312,314,319,304,296,315,326,308,348,301,323,324,297,327,327,319,319,309,299,292,348,298,297,299,309,321,294,310,301,271,307,303,292,339,327,311,286,332,330,319,333,330,295,328,312,296,316,329,334,367,371,329,374,421,405,372,367,403,366,395,399,401,377,378,383,375,374,390,352,341,331,317,353,307,322,294,326,323,342,322,340,306,337,335,331,304,343,335,311,307,312,333,287,328,331,323,363,338,341,353,315,304,331,296,325,331,326,330,347,317,315,376,316,332,341,321,318,323,316,291,322,314,318,353,336,323,335,296,315,324,289,299,318,325,315,316,321,288,333,323,314,324,338,368,322,328,326,311,299,326,334,370,341,300,320,276,323,335,300,316,321,300,384,342,305,328,351,301,321,296,289,328,306,347,347,325,330,326,348,332,327,303,352,298,344,365,361,301,337,324,341,340,337,348,340,322,312,348,338,324,340,318,301,339,299,337,302,295,299,327,310,312,330,305,300,335,345,292,309,326,310,338,297,323,306,276,324,308,276,321,313,333,327,272,305,308,334,320,279,299,302,294,286,286,314,288,296,331,324,309,300,281,321,318,296,328,285,314,303,325,318,313,298,312,324,351,321,324,312,310,304,335,318,319,303,315,346,278,304,290,312,301,265,283,293,308,289,296,287,278,323,302,252,261,261,303,283,267,274,275,319,274,260,287,288,300,264,278,272,277,259,300,284,279,285,276,268,283,247,273,273,275,305,304,266,281,238,288,243,275,273,255,254,290,244,262,279,237,273,243,270,266,278,266,244,244,267,269,263,262,251,254,257,258,239,242,243,275,242,229,239,256,266,239,241,253,208,247,285,235,253,239,251,229,246,208,235,246,261,254,253,208,218,259,235,246,225,247,267,259,252,215,250,232,203,244,250,237,222,248,261,234,247,275,278,261,279,275,299,310,315,303,318,301,304,340,349,366,334,356,334,345,358,316,317,338,307,291,264,283,259,238,230,228,251,211,218,225,215,239,200,227,207,201,210,196,188,208,190,196,195,206,208,212,186,203,201,194,180,167,208,184,198,204,203,179,192,186,197,203,204,195,178,192,201,197,155,171,197,195,198,158,192,189,197,178,152,176,171,159,180,164,175,172,171,169,149,142,176,153,174,190,189,187,164,173,148,155,183,162,180,159,187,193,140,152,154,145,162,168,170,151,162,169,162,166,185,158,166,165,166,160,162,152,136,152,162,163,139,131,163,165,139,130,156,157,158,155,152,138,138,148,137,149,131,128,139,145,142,159,155,144,156,155,133,122,155,135,176,146,165,164,157,138,158,130,148,150,136,117,149,154,142,140,144,140,167,136,167,154,134,141,151,151,126,152,147,129,138,135,141,118,112,139,110,155,116,135,122,115,137,108,118,125,134,135,122,96,134,123,131,111,108,109,121,107,135,112,124,107,112,136,128,106,113,126,134,115,100,88,123,132,106,119,101,97,110,104,127,102,118,104,110,110,125,110,93,120,113,108,93,117,114,86,116,99,94,110,109,122,95,98,112,105,85,103,96,88,98,102,116,108,91,84,84,111,83,97,99,85,102,83,107,86,122,94,102,92,78,115,108,89,102,109,86,87,93,105,107,99,99,99,97,80,105,94,91,76,85,91,89,80,92,91,90,99,93,96,79,80,92,104,100,84,103,95,100,83,93,96,84,83,119,96,92,82,93,103,88,83,86,96,93,97,83,88,83,89,104,93,92,104,83,108,98,74,85,103,85,90,89,86,104,101,92,89,78,102,88,93,88,91,82,74,74,96,94,68,74,90,102,98,95,100,107,108,89,91,104,95,85,91,94,103,100,103,88,96,108,107,84,102,106,118,116,98,101,108,101,125,113,112,108,118,121,117,104,114,126,131,118,130,140,117,142,133,158,150,145,175,200,180,160,174,182,188,206,205,217,237,235,250,244,248,267,283,273,291,289,284,314,342,339,307,337,373,339,407,365,374,378,348,435,394,401,440,373,440,413,380,429,428,436,417,370,386,374,385,402,418,369,398,371,366,338,342,373,363,329,336,326,289,309,298,324,289,283,244,231,237,232,212,233,217,187,203,198,183,190,178,157,150,151,162,138,155,130,134,123,131,129,123,110,114,94,99,114,96,102,91,84,89,88,95,99,78,88,91,78,73,92,80,72,68,74,59,71,71,67,57,62,78,67,69,55,76,78,58,53,69,66,63,57,54,69,48,60,74,49,58,54,59,49,48,51,55,37,57,48,52,56,59,57,51,60,51,71,75,73,56,63,67,81,71,88,65,81,94,100,109,117,90,102,98,119,119,131,136,116,118,126,112,119,141,112,106,114,118,122,122,115,89,99,79,76,65,77,68,61,52,48,56,62,42,42,36,43,35,37,36,33,35,51,31,26,35,36,40,42,39,36,29,35,57,38,54,47,30,44,40,48,48,53,56,54,42,47,38,53,47,40,54,56,58,49,48,50,40,66,56,49,64,54,57,55,70,67,59,62,63,60,44,60,61,51,53,62,57,52,50,49,56,66,58,59,53,45,37,54,60,48,35,43,55,39,47,45,51,43,33,32,34,48,31,36,39,36,39,34,28,26,29,35,33,34,29,35,39,38,27,25,30,30,25,36,24,26,23,28,30,24,19,20,24,29,25,33,20,23,14,29,17,14,19,14,22,23,17,16,19,15,18,24,24,18,17,20,22,15,15,11,11,16,13,14,14,13,12,15,12,22,13,9,13,13,12,19,17,15,14,13,8,10,16,7,11,17,20,18,12,12,8,22,11,12,13,17,17,8,11,7,11,12,15,15,14,11,15,19,10,9,12,6,14,13,11,21,15,12,10,5,6,11,17,12,6,6,21,14,15,21,15,12,9,12,10,8,13,11,15,20,17,18,7,16,20,14,18,13,15,10,8,21,13,16,14,14,11,14,12,12,14,7,10,9,7,8,10,12,10,9,9,9,8,10,11,11,10,11,4,7,7,8,7,5,10,10,6,9,3,10,10,8,9,12,9,11,3,6,8,7,7,9,10,4,2,6,7,5,7,6,7,4,4,7,12,6,7,4,7,3,10,8,2,8,9,6,8,11,5,8,7,6,5,4,11,7,7,9,8,7,5,5,6,6,5,8,5,6,10,6,4,7,7,4,7,6,6,6,6,6,8,11,2,6,2,10,9,4,7,6,12,5,4,7,5,5,1,2,4,2,8,5,5,3,2,3,3,5,7,4,3,5,7,4,5,1,11,5,4,3,3,5,2,5,2,5,9,1,15,2,5,3,6,6,6,4,7,5,2,2,4,5,5,3,1,3,4,4,3,5,5,5,4,7,7,7,7,2,2,2,4,6,0,10,8,1,3,8,8,6,3,2,3,6,7,10,7,6,7,1,2,1,9,3,1,3,10,5,2,6,4,4,6,5,6,5,5,4,2,5,7,5,5,1,5,2,6,5,4,4,2,2,7,3,3,3,1,4,10,3,5,5,1,4,1,1,4,0,1,4,6,8,7,3,1,7,4,1,1,3,3,5,4,3,2,3,2,5,3,1,3,2,4,5,4,1,2,7,0,2,2,2,3,7,7,2,7,7,6,6,3,2,1,4,1,3,1,5,8,1,4,3,2,3,3,2,2,4,2,7,3,3,1,1,3,0,2,5,1,1,1,4,5,5,4,2,2,1,4,2,3,3,3,4,1,4,3,3,2,3,3,2,4,1,3,5,3,6,3,4,1,2,3,2,7,3,2,2,2,0,2,4,4,2,3,1,2,2,4,4,2,3,2,1,3,1,1,1,3,4,2,3,3,3,0,1,4,3,0,2,3,1,1,0,4,2,1,1,5,5,4,2,4,0,5,1,4,2,1,2,2,3,3,2,1,1,3,2,2,2,0,3,1,3,2,3,5,1,3,4,2,1,1,3,3,1,0,2,3,1,1,0,1,5,3,2,0,4,1,4,3,6,2,2,0,2,5,2,0,0,0,5,2,2,3,1,0,2,1,4,2,3,5,4,1,3,2,1,2,5,3,2,2,4,5,2,3,2,3,1,1,0,3,2,1,1,1,2,0,2,0,1,2,1,1,0,1,1,2,1,2,1,1,0,1,1,1,0,1,2,1,1,1,2,2,1,3,0,1,1,1,2,1,3,1,1,3,2,2,2,0,2,3,0,0,3,2,4,1,0,2,2,2,1,1,2,4,2,0,0,0,1,1,3,3,3,3,3,2,1,2,1,2,2,2,1,0,2,2,2,1,1,2,1,1,2,0,0,1,0,2,1,0,2,1,4,0,0,1,3,0,2,3,1,0,0,2,1,3,2,4,2,1,2,2,0,0,1,0,1,2,3,0,2,0,1,1,1,3,0,1,1,2,0,0,1,1,0,2,2,0,1,0,1,0,1,1,1,1,1,0,2,1,1,5,1,2,1,0,1,0,0,1,3,0,1,2,3,1,1,0,1,2,1,1,2,2,0,2,2,0,0,2,2,0,1,5,0,1,2,1,1,0,1,0,1,1,2,0,1,2,1,1,0,1,1,1,0,0,0,3,2,2,2,1,1,0,1,1,1,0,2,0,0,1,0,0,2,2,2,0,0,0,1,0,2,1,0,0,0,2,0,0,0,1,0,0,0,0,1,1,0,2,3,0,0,1,2,0,1,4,0,0,0,0,1,1,1,0,1,1,1,2,0,1,0,1,0,1,1,0,0,0,0,1,1,0,0,2,0,1,0,0,0,0,2,1,0,1,1,1,2,1,0,1,0,0,0,1,0,1,0,2,0,1,2,1,1,2,0,0,0,0,1,1,1,2,1,2,0,1,0,1,0,3,0,1,1,3,1,1,0,1,0,2,1,2,0,2,0,3,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,1,0,1,1,0,0,0,0,2,0,1,2,2,0,0,1,0,0,0,1,1,0,0,0,1,0,0,1,0,0,1,1,0,1,0,0,1,0,0,2,3,0,1,1,1,2,0,1,3,2,2,1,0,0,1,0,1,1,1,1,0,2,0,1,0,1,1,0,0,1,0,0,0,0,1,1,3,1,1,1,0,0,1,1,0,1,1,2,0,1,0,1,0,1,2,1,0,0,0,1,1,0,2,1,1,1,1,1,0,2,0,0,0,0,1,0,0,1,0,0,1,0,0,1,1,1,1,2,1,0,0,3,3,2,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,1,1,1,1,0,0,0,0,0,0,2,1,1,2,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,1,0,1,0,0,0,0,0,1,1,0,1,0,0,1,0,0,1,0,1,0,0,0,3,0,0,0,1,0,1,1,1,1,0,2,0,1,1,0,1,1,2,0,0,0,1,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,0,0,1,1,1,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,1,1,0,0,2,0,1,0,0,0,2,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,1,0,0,2,1,1,0,0,0,1,1,0,1,0,1,1,0,1,0,1,0,1,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,2,1,0,1,0,0,0,0,0,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,1,1,0,1,1,2,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,2,0,1,0,1,0,0,0,1,0,0,0,2,0,0,0,0,0,0,1,0,1,0,0,0,0,1,0,0,2,0,0,1,0,0,0,0,0,0,2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1,1,0,1,0,0,0,1,0,0,0,1,1,1,0,0,0,1,0,0,0,1,0,1,0,0,0,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,1,2,1,1,1,1,1,0,1,0,0,0,1,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,1,0,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,2,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,1,0,0 - - - 45 - K-Serie - 2080 - - - 18 - K-Serie - 2484 - - - 16 - K-Serie - 5.02277459E-4 - 2.567430107E-4 - 106 - - - 14 - K-Serie - 4.823244885E-4 - 2.159392682E-4 - 13 - - - 13 - K-Serie - 4.846837318E-3 - 2.084660463E-3 - 27 - - - 12 - K-Serie - 3.29544125E-2 - 1.276789645E-2 - 29 - - - 15 - K-Serie - 1.597694593E-1 - 7.888562072E-2 - 14956 - - - 17 - K-Serie - 4.866464538E-4 - 2.750255435E-4 - 183 - - - 19 - K-Serie - 2.374483884E-3 - 1.479916641E-3 - 1322 - - - 20 - K-Serie - 7.968463339E-1 - 5.090855531E-1 - 459971 - - - 22 - K-Serie - - - 24 - K-Serie - - - 25 - K-Serie - 2.590534039E-6 - 2.268675083E-6 - 6 - - - 26 - K-Serie - 3.158197279E-4 - 2.811572304E-4 - 918 - - - 27 - K-Serie - - - 28 - K-Serie - - - 29 - K-Serie - - - 30 - K-Serie - 2.031831253E-4 - 2.117917927E-4 - 1210 - - - 33 - K-Serie - 1.566180175E-4 - 1.870505194E-4 - 1169 - - - 34 - K-Serie - - - 35 - K-Serie - 3.481986604E-6 - 4.435124029E-6 - 27 - - - 51 - K-Serie - - - 50 - K-Serie - - - 82 - L-Serie - 6.637913027E-5 - 2.192458905E-4 - 812 - - - 83 - L-Serie - - - 80 - L-Serie - 4.257596272E-5 - 1.361394238E-4 - 502 - - - 79 - L-Serie - - - 47 - K-Serie - - - 48 - K-Serie - - - 40 - K-Serie - - - 38 - K-Serie - 4.191792274E-4 - 5.8548147E-4 - 2893 - - - 56 - K-Serie - 5.273970211E-4 - 1.154551328E-3 - 16 - - - 45 - Dec,Fix - - - 18 - Dec,Fix - - - 16 - - - 14 - - - 13 - - - 12 - - - 15 - - - 17 - - - 19 - - - 20 - - - 22 - - - 24 - - - 25 - - - 26 - - - 27 - - - 28 - - - 29 - - - 30 - - - 33 - - - 34 - - - 35 - - - 51 - - - 50 - - - 82 - - - 83 - - - 80 - - - 79 - - - 47 - - - 48 - - - 40 - - - 38 - - - 56 - - - 45 - Rh - Rh-KA - KA - 2.01638949E1 - 3.568196172E-1 - 3727 - 2366 - - - 18 - Ar - Ar-KA - KA - 2.956387566 - 1.715270136E-1 - 3680 - 3206 - - - 16 - S - S-KA - KA - 2.3067 - 1.603909193E-1 - 1578 - 1186 - - - 14 - Si - Si-K - K - 1.742213593 - 1.500457023E-1 - 454 - 218 - - - 13 - Al - Al-K - K - 1.487120909 - 1.451289538E-1 - 317 - 123 - - - 12 - Mg - Mg-K - K - 1.253817007 - 1.404815883E-1 - 346 - 190 - - - 15 - P - P-KA - KA - 2.013207983 - 1.550982978E-1 - 16611 - 16284 - - - 17 - Cl - Cl-KA - KA - 2.6209938 - 1.658715155E-1 - 1058 - 597 - - - 19 - K - K-KA - KA - 3.312437057 - 1.773335553E-1 - 2512 - 2044 - - - 20 - Ca - Ca-KA - KA - 3.690544536 - 1.832985484E-1 - 379535 - 379127 - - - 22 - Ti - Ti-KA - KA - 4.508992387 - 1.955882298E-1 - 465 - 261 - - - 24 - Cr - Cr-KA - KA - 5.410129107 - 2.082824896E-1 - 863 - 165 - - - 25 - Mn - Mn-KA - KA - 5.893121513 - 2.147777587E-1 - 1452 - 324 - - - 26 - Fe - Fe-KA - KA - 6.397264868 - 2.213542634E-1 - 2726 - 1173 - - - 27 - Co - Co-KA - KA - 6.922758913 - 2.280074665E-1 - 2356 - 335 - - - 28 - Ni - Ni-KA - KA - 7.469455497 - 2.347290507E-1 - 3308 - 601 - - - 29 - Cu - Cu-KA - KA - 8.0377615 - 2.415180993E-1 - 4434 - 791 - - - 30 - Zn - Zn-KA - KA - 8.627328596 - 2.483650904E-1 - 6952 - 2293 - - - 33 - As - As-KA - KA - 1.052585941E1 - 2.692332357E-1 - 10175 - 3398 - - - 34 - Se - Se-KA - KA - 1.12042784E1 - 2.763082814E-1 - 9297 - 2110 - - - 35 - Br - Br-KA - KA - 1.190163911E1 - 2.833968291E-1 - 9492 - 2398 - - - 51 - Sb - Sb-KA - KA - 2.630347919E1 - 4.028029227E-1 - 117 - 109 - - - 50 - Sn - Sn-KA - KA - 2.521425707E1 - 3.950357957E-1 - 161 - 136 - - - 82 - Pb - Pb-LA - LA - 1.054075704E1 - 2.693905943E-1 - 10216 - 3434 - - - 83 - Bi - Bi-LA - LA - 1.082814293E1 - 2.724083657E-1 - 9106 - 2226 - - - 80 - Hg - Hg-LA - LA - 9.979964752 - 2.634023065E-1 - 8738 - 2271 - - - 79 - Au - Au-LA - LA - 9.704460229 - 2.604099583E-1 - 7838 - 1810 - - - 47 - Ag - Ag-KA - KA - 2.21071804E1 - 3.719894881E-1 - 509 - 144 - - - 48 - Cd - Cd-KA - KA - 2.311615936E1 - 3.796268777E-1 - 321 - 158 - - - 40 - Zr - Zr-KA - KA - 1.573758015E1 - 3.195897269E-1 - 4837 - 921 - - - 38 - Sr - Sr-KA - KA - 1.413382829E1 - 3.049809229E-1 - 9371 - 3699 - - - 56 - Ba - Ba-LA - LA - 4.464977951 - 1.949470292E-1 - 487 - 275 - - - - - - 45 - 255 - 1 - - - 18 - 65280 - 1 - - - 16 - 16711680 - 1 - - - 14 - 16776960 - 1 - - - 13 - 16711935 - 1 - - - 12 - 65535 - 1 - - - 15 - 32767 - 1 - - - 17 - 8388352 - 1 - - - 19 - 16711807 - 1 - - - 20 - 8323327 - 1 - - - 22 - 65407 - 1 - - - 24 - 255 - 1 - - - 25 - 65280 - 1 - - - 26 - 16711680 - 1 - - - 27 - 16776960 - 1 - - - 28 - 16711935 - 1 - - - 29 - 65535 - 1 - - - 30 - 32767 - 1 - - - 33 - 8388352 - 1 - - - 34 - 16711807 - 1 - - - 35 - 8323327 - 1 - - - 51 - 65407 - 1 - - - 50 - 255 - 1 - - - 82 - 65280 - 1 - - - 83 - 16711680 - 1 - - - 80 - 16776960 - 1 - - - 79 - 16711935 - 1 - - - 47 - 65535 - 1 - - - 48 - 32767 - 1 - - - 40 - 8388352 - 1 - - - 38 - 16711807 - 1 - - - 56 - 8323327 - 1 - - - - - - - - - - - - - - 82 - 23.3.2021 - - 4096 - -9.5550444E-1 - 9.999E-3 - 9.892997828E-4 - 4.466391048E-4 - - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22.76,22.71,22.62,22.55,22.48,22.39,22.27,22.19,22.09,21.99,21.85,21.75,21.63,21.52,21.27,21.17,21.04,20.93,20.72,20.62,20.49,20.4,20.23,20.15,20.04,19.96,19.84,19.78,19.69,19.62,19.54,19.5,19.42,19.37,19.3,19.28,19.22,19.18,19.12,19.11,19.07,19.04,18.99,19,18.97,18.95,18.91,18.9,18.87,18.84,18.79,18.78,18.74,18.7,18.64,18.62,18.57,18.52,18.46,18.43,18.37,18.31,18.25,18.2,18.13,18.06,17.98,17.94,17.87,17.79,17.7,17.62,17.57,17.49,17.4,17.3,17.2,17.16,17.12,17.1,17.09,17.12,17.13,17.12,17.17,17.34,17.57,17.76,17.96,18.24,18.62,19.02,19.4,19.81,20.31,20.88,21.47,22.01,22.64,23.35,24.09,24.86,25.57,26.34,27.15,27.97,28.76,29.59,30.49,31.4,32.2,32.96,33.74,34.57,35.4,36.14,36.85,37.57,38.33,39.07,39.75,40.39,41.04,41.68,42.3,42.88,43.43,43.98,44.51,45.02,45.49,45.94,46.38,46.77,47.12,47.45,47.77,48.08,48.34,48.55,48.75,48.95,49.13,49.24,49.29,49.33,49.37,49.4,49.4,49.35,49.26,49.19,49.08,48.94,48.77,48.6,48.4,48.19,47.92,47.63,47.39,47.15,46.85,46.52,46.12,45.75,45.44,45.13,44.75,44.35,43.88,43.46,43.11,42.84,42.63,42.41,42.15,41.93,41.77,41.67,41.64,41.63,41.59,41.61,41.65,41.74,41.9,42.1,42.3,42.56,42.81,43.09,43.44,43.84,44.27,44.77,45.23,45.7,46.15,46.53,46.95,47.42,47.84,48.29,48.71,49.06,49.46,49.9,50.27,50.69,51.09,51.4,51.78,52.18,52.51,52.89,53.26,53.54,53.89,54.26,54.55,54.91,55.25,55.49,55.82,56.17,56.41,56.75,57.06,57.27,57.59,57.92,58.19,58.59,58.99,59.24,59.56,59.87,60.17,60.63,61.12,61.42,61.75,62.06,62.39,62.92,63.51,63.86,64.21,64.51,64.89,65.49,66.18,66.58,66.95,67.25,67.66,68.29,69,69.35,69.67,69.98,70.44,71.08,72.08,73.43,75.53,77.46,79.46,81.67,84.38,87.69,91.43,94.93,98.43,102.17,106.49,111.56,116.76,121.47,126.08,130.89,136.42,142.95,149.3,155.08,160.72,166.57,173.1,180.34,186.44,190.97,195.39,200.08,205.44,211.54,215.98,219.17,222.36,225.87,229.83,234.23,236.73,238.08,239.6,241.5,243.75,246.32,246.62,246.22,246.15,246.43,246.75,247.06,245.16,242.77,240.95,239.55,238.04,236.29,232.21,228.1,224.69,221.63,218.16,214.15,208.17,202.39,197.55,193.11,188.13,182.34,174.71,167.68,161.58,155.79,149.27,141.76,132.97,124.99,118.05,111.41,104.54,97.39,89.54,82.73,76.94,71.46,65.81,60.2,54.33,49.32,45.18,41.34,37.42,33.73,30.13,27.22,24.88,22.77,20.65,19.03,18.01,17.45,17.17,17.08,17.07,17.1,17.14,17.2,17.19,17.17,17.16,17.2,17.24,17.29,17.27,17.25,17.26,17.3,17.34,17.37,17.33,17.33,17.34,17.39,17.43,17.45,17.4,17.41,17.43,17.55,17.66,17.76,17.79,17.91,18.02,18.22,18.39,18.57,18.7,18.92,19.12,19.39,19.63,19.88,20.1,20.42,20.71,21.05,21.36,21.68,22,22.41,22.79,23.2,23.5,23.83,24.14,24.55,24.92,25.31,25.61,25.94,26.26,26.65,27.02,27.39,27.69,28.02,28.34,28.72,29.08,29.44,29.74,30.07,30.39,30.75,31.11,31.46,31.8,32.1,32.44,32.78,33.11,33.45,33.79,34.16,34.49,34.87,35.16,35.47,35.8,36.17,36.57,36.95,37.31,37.56,37.85,38.21,38.6,39.03,39.43,39.8,40.06,40.36,40.77,41.17,41.63,42.05,42.43,42.71,43.02,43.42,43.83,44.34,44.78,45.16,45.46,45.78,46.18,46.61,47.14,47.6,48,48.32,48.64,49.04,49.45,49.97,50.43,50.83,51.15,51.49,51.89,52.29,52.79,53.23,53.63,53.98,54.32,54.71,55.1,55.59,56.03,56.43,56.79,57.14,57.53,57.9,58.38,58.8,59.21,59.59,59.94,60.33,60.69,61.15,61.57,62,62.4,62.77,63.15,63.51,63.97,64.41,64.86,65.29,65.68,66.06,66.41,66.88,67.33,67.8,68.27,68.7,69.12,69.52,69.98,70.46,70.95,71.45,71.91,72.37,72.8,73.25,73.73,74.24,74.78,75.24,75.73,76.18,76.66,77.14,77.67,78.22,78.71,79.17,79.64,80.13,80.63,81.13,81.66,82.15,82.63,83.06,83.56,84.06,84.56,85.05,85.56,86.04,86.5,86.91,87.43,87.92,88.41,88.89,89.39,89.86,90.3,90.7,91.22,91.7,92.18,92.65,93.13,93.58,94,94.38,94.93,95.39,95.85,96.31,96.78,97.22,97.63,98,98.55,99,99.45,99.89,100.35,100.77,101.19,101.59,102.16,102.59,103.03,103.47,103.91,104.34,104.77,105.21,105.78,106.2,106.62,107.05,107.48,107.93,108.38,108.86,109.44,109.84,110.26,110.68,111.11,111.58,112.05,112.55,113.09,113.48,113.9,114.31,114.74,115.22,115.71,116.23,116.73,117.11,117.52,117.96,118.43,118.94,119.43,119.95,120.41,120.79,121.2,121.64,122.17,122.7,123.19,123.72,124.14,124.51,124.92,125.39,125.97,126.53,127.02,127.56,128.06,128.55,129.09,129.59,130.2,130.74,131.23,131.78,132.36,132.98,133.64,134.18,134.87,135.49,136.12,136.79,137.45,138.19,138.97,139.65,140.44,141.15,141.91,142.69,143.42,144.16,144.94,145.65,146.51,147.31,148.2,149.11,149.93,150.7,151.48,152.2,153.14,153.98,154.9,155.79,156.61,157.4,158.19,158.93,159.84,160.71,161.64,162.52,163.34,164.15,164.95,165.71,166.59,167.49,168.43,169.3,170.11,170.91,171.7,172.47,173.37,174.3,175.25,176.09,176.89,177.68,178.46,179.26,180.17,181.12,182.07,182.9,183.69,184.47,185.24,186.05,186.97,187.94,188.89,189.7,190.47,191.24,192,192.82,193.73,194.69,195.63,196.42,197.18,197.95,198.69,199.52,200.42,201.35,202.29,203.05,203.79,204.54,205.27,206.11,207,207.91,208.83,209.57,210.3,211.03,211.75,212.58,213.45,214.34,215.23,215.95,216.66,217.37,218.08,218.9,219.75,220.61,221.48,222.18,222.88,223.57,224.26,225.07,225.91,226.73,227.57,228.24,228.91,229.58,230.25,231.05,231.87,232.66,233.47,234.11,234.77,235.43,236.09,236.86,237.66,238.41,239.19,239.8,240.43,241.06,241.7,242.45,243.24,243.96,244.71,245.29,245.91,246.53,247.15,247.88,248.64,249.33,250.04,250.58,251.17,251.77,252.38,253.08,253.83,254.48,255.16,255.68,256.26,256.85,257.44,258.12,258.86,259.46,260.1,260.59,261.15,261.72,262.29,262.95,263.67,264.24,264.86,265.32,265.86,266.43,266.99,267.63,268.33,268.86,269.44,269.88,270.4,270.95,271.49,272.11,272.8,273.3,273.85,274.27,274.78,275.33,275.86,276.46,277.12,277.58,278.11,278.5,278.99,279.53,280.04,280.62,281.26,281.7,282.2,282.57,283.06,283.59,284.09,284.65,285.27,285.67,286.14,286.5,286.97,287.49,287.97,288.51,289.09,289.48,289.92,290.27,290.73,291.24,291.7,292.22,292.77,293.13,293.55,293.89,294.33,294.82,295.26,295.75,296.27,296.61,297.01,297.34,297.77,298.24,298.66,299.12,299.6,299.93,300.3,300.63,301.04,301.48,301.87,302.3,302.74,303.06,303.41,303.74,304.13,304.53,304.91,305.3,305.69,306,306.33,306.66,307.03,307.4,307.74,308.09,308.44,308.74,309.06,309.4,309.73,310.06,310.37,310.69,311,311.29,311.61,311.93,312.28,312.55,312.82,313.1,313.37,313.64,313.95,314.28,314.59,314.91,315.12,315.34,315.57,315.8,316.08,316.4,316.71,317.01,317.31,317.45,317.61,317.81,318.01,318.27,318.61,318.88,319.17,319.44,319.52,319.64,319.79,319.94,320.2,320.53,320.77,321.04,321.29,321.32,321.39,321.5,321.61,321.85,322.17,322.36,322.63,322.85,322.82,322.85,322.92,322.99,323.21,323.5,323.66,323.91,324.1,324.04,324.03,324.06,324.09,324.29,324.55,324.66,324.9,325.07,324.96,324.92,324.92,324.91,325.08,325.29,325.37,325.59,325.73,325.61,325.55,325.51,325.46,325.6,325.77,325.82,326.01,326.13,325.99,325.91,325.83,325.76,325.86,325.98,326,326.17,326.27,326.12,326.02,325.92,325.82,325.88,325.96,325.94,326.09,326.16,326.02,325.91,325.78,325.66,325.68,325.71,325.66,325.77,325.82,325.69,325.57,325.42,325.29,325.27,325.26,325.18,325.25,325.27,325.16,325.03,324.86,324.71,324.65,324.59,324.5,324.52,324.51,324.41,324.28,324.09,323.93,323.83,323.73,323.62,323.6,323.56,323.48,323.36,323.18,322.99,322.87,322.75,322.6,322.53,322.46,322.37,322.24,322.1,321.91,321.74,321.62,321.49,321.34,321.23,321.13,321,320.84,320.69,320.5,320.32,320.19,320.05,319.86,319.71,319.58,319.4,319.21,319.06,318.86,318.67,318.54,318.39,318.16,317.98,317.83,317.6,317.36,317.2,316.99,316.78,316.64,316.48,316.22,316.01,315.82,315.55,315.27,315.09,314.88,314.65,314.51,314.35,314.06,313.81,313.59,313.27,312.94,312.75,312.51,312.26,312.1,311.94,311.62,311.35,311.1,310.74,310.37,310.14,309.9,309.63,309.46,309.29,308.96,308.66,308.38,307.98,307.55,307.3,307.02,306.73,306.54,306.37,306.02,305.7,305.39,304.96,304.49,304.2,303.91,303.6,303.39,303.21,302.86,302.51,302.19,301.72,301.2,300.86,300.54,300.2,299.97,299.78,299.42,299.07,298.72,298.22,297.67,297.29,296.95,296.59,296.33,296.15,295.78,295.41,295.04,294.52,293.93,293.5,293.12,292.74,292.45,292.25,291.89,291.51,291.12,290.59,289.97,289.5,289.1,288.69,288.38,288.17,287.82,287.43,287.02,286.47,285.81,285.3,284.86,284.43,284.09,283.87,283.53,283.13,282.71,282.15,281.48,280.92,280.47,280,279.64,279.42,279.08,278.66,278.23,277.67,276.97,276.37,275.89,275.4,275.01,274.77,274.44,274.02,273.59,273.03,272.32,271.69,271.17,270.66,270.25,270,269.67,269.24,268.8,268.24,267.51,266.85,266.31,265.77,265.34,265.07,264.74,264.32,263.87,263.31,262.59,261.9,261.33,260.77,260.31,260.02,259.69,259.25,258.8,258.25,257.52,256.81,256.22,255.63,255.15,254.83,254.5,254.06,253.62,253.07,252.35,251.62,251,250.38,249.86,249.52,249.17,248.73,248.28,247.74,247.02,246.29,245.64,244.99,244.45,244.07,243.7,243.26,242.81,242.28,241.58,240.84,240.16,239.48,238.89,238.47,238.07,237.62,237.18,236.66,235.97,235.23,234.52,233.81,233.18,232.73,232.3,231.85,231.41,230.89,230.22,229.48,228.74,227.98,227.31,226.8,226.34,225.88,225.44,224.95,224.3,223.56,222.79,222,221.28,220.73,220.23,219.77,219.34,218.86,218.22,217.49,216.69,215.85,215.08,214.47,213.95,213.47,213.06,212.6,211.98,211.25,210.43,209.55,208.73,208.09,207.53,207.06,206.65,206.21,205.61,204.88,204.03,203.11,202.24,201.56,201.02,200.54,200.15,199.73,199.16,198.43,197.56,196.6,195.68,194.98,194.45,193.97,193.6,193.21,192.66,191.93,191.04,190.04,189.06,188.35,187.84,187.37,187.01,186.66,186.11,185.35,184.45,183.41,182.39,181.72,181.29,180.89,180.62,180.34,179.83,179.13,178.32,177.41,176.52,175.94,175.6,175.29,175.09,174.88,174.42,173.78,173.08,172.29,171.53,171.05,170.79,170.57,170.46,170.32,169.86,169.22,168.55,167.85,167.17,166.76,166.5,166.25,166.06,165.82,165.3,164.67,164.03,163.4,162.82,162.47,162.21,161.93,161.66,161.33,160.74,160.11,159.51,158.96,158.47,158.18,157.91,157.63,157.28,156.85,156.19,155.57,155.01,154.53,154.12,153.89,153.64,153.37,153,152.55,151.9,151.32,150.82,150.41,150.09,149.92,149.69,149.42,149.05,148.56,147.93,147.39,146.92,146.57,146.33,146.22,146.01,145.75,145.36,144.86,144.24,143.71,143.25,142.91,142.68,142.55,142.33,142.05,141.66,141.13,140.52,140.01,139.56,139.22,139,138.85,138.61,138.32,137.92,137.38,136.78,136.28,135.84,135.5,135.28,135.12,134.86,134.56,134.15,133.6,133.02,132.52,132.09,131.74,131.53,131.34,131.08,130.77,130.36,129.8,129.23,128.75,128.32,127.97,127.76,127.55,127.28,126.97,126.55,126.02,125.48,124.99,124.57,124.22,124,123.77,123.5,123.18,122.77,122.28,121.74,121.27,120.84,120.48,120.26,120.03,119.79,119.47,119.06,118.61,118.09,117.63,117.2,116.83,116.56,116.34,116.13,115.87,115.47,115,114.55,114.06,113.63,113.25,112.95,112.69,112.51,112.3,111.98,111.54,111.01,110.56,110.08,109.68,109.33,109.08,108.83,108.67,108.44,108.11,107.6,107.01,106.57,106.18,105.9,105.57,105.36,105.12,104.98,104.79,104.51,104.06,103.56,103.16,102.86,102.7,102.46,102.37,102.26,102.21,102.05,101.82,101.44,101.02,100.68,100.47,100.42,100.28,100.22,100.16,100.2,100.07,99.9,99.58,99.2,98.83,98.59,98.52,98.43,98.39,98.38,98.44,98.36,98.17,97.86,97.55,97.26,97.04,96.99,96.99,97.03,97.07,97.15,97.11,96.91,96.6,96.37,96.15,95.96,95.93,96.02,96.12,96.22,96.32,96.32,96.11,95.79,95.57,95.38,95.2,95.16,95.24,95.35,95.46,95.58,95.56,95.35,95.03,94.82,94.64,94.48,94.42,94.49,94.61,94.73,94.87,94.85,94.62,94.31,94.1,93.95,93.8,93.72,93.79,93.92,94.07,94.23,94.21,93.97,93.62,93.41,93.29,93.14,93.06,93.11,93.25,93.44,93.71,93.9,93.91,93.81,93.87,94.04,94.21,94.39,94.69,95.08,95.5,95.87,96.27,96.51,96.65,96.97,97.41,97.88,98.33,98.86,99.5,100.14,100.71,101.3,101.73,102.01,102.33,102.76,103.21,103.64,104.12,104.66,105.21,105.71,106.22,106.64,107.02,107.34,107.73,108.16,108.54,108.93,109.37,109.8,110.23,110.64,111,111.34,111.62,111.93,112.28,112.62,112.95,113.27,113.59,113.91,114.23,114.51,114.79,115.02,115.27,115.52,115.76,116.01,116.23,116.45,116.68,116.89,117.09,117.27,117.42,117.54,117.67,117.81,117.94,118.09,118.2,118.3,118.4,118.48,118.56,118.62,118.65,118.66,118.69,118.7,118.7,118.72,118.7,118.67,118.65,118.59,118.53,118.45,118.34,118.2,118.09,117.98,117.86,117.76,117.61,117.44,117.28,117.07,116.87,116.66,116.41,116.15,115.91,115.66,115.4,115.15,114.87,114.58,114.3,113.94,113.59,113.24,112.85,112.43,112.07,111.7,111.32,110.96,110.56,110.14,109.73,109.23,108.76,108.29,107.78,107.24,106.76,106.27,105.77,105.28,104.77,104.26,103.74,103.11,102.52,101.94,101.32,100.67,100.1,99.52,98.92,98.34,97.74,97.15,96.55,95.81,95.14,94.48,93.78,93.04,92.39,91.73,91.05,90.38,89.72,89.07,88.41,87.58,86.85,86.13,85.38,84.59,83.9,83.21,82.48,81.77,81.07,80.38,79.69,78.91,78.3,77.7,77,76.29,75.66,74.98,74.22,73.5,72.79,72.07,71.38,70.67,70.2,69.72,69.09,68.49,67.94,67.31,66.53,65.82,65.11,64.38,63.7,63.16,62.76,62.33,61.81,61.37,60.96,60.49,59.97,59.55,59.09,58.62,58.18,57.82,57.53,57.16,56.75,56.47,56.19,55.9,55.62,55.49,55.3,55.09,54.9,54.72,54.44,54.12,53.72,53.45,53.17,52.88,52.64,52.51,52.3,52.09,51.87,51.66,51.4,51.07,50.69,50.42,50.15,49.86,49.64,49.51,49.3,49.07,48.84,48.59,48.35,48.01,47.64,47.39,47.12,46.82,46.64,46.51,46.29,46.05,45.79,45.52,45.29,44.95,44.59,44.34,44.07,43.78,43.64,43.52,43.32,43.11,42.91,42.71,42.57,42.3,42.04,41.85,41.65,41.43,41.34,41.24,41.06,40.89,40.73,40.6,40.55,40.36,40.18,40.07,39.93,39.77,39.74,39.65,39.47,39.29,39.13,39,38.94,38.75,38.57,38.44,38.31,38.15,38.1,38,37.82,37.64,37.48,37.34,37.25,37.06,36.88,36.74,36.6,36.45,36.38,36.26,36.07,35.88,35.71,35.56,35.46,35.26,35.08,34.93,34.79,34.63,34.54,34.41,34.21,34.01,33.83,33.67,33.55,33.34,33.16,32.99,32.84,32.68,32.57,32.42,32.2,31.99,31.81,31.64,31.5,31.28,31.09,30.91,30.76,30.59,30.45,30.28,30.06,29.84,29.64,29.46,29.3,29.08,28.88,28.68,28.53,28.35,28.2,28.01,27.77,27.53,27.33,27.14,26.96,26.73,26.53,26.32,26.17,25.99,25.81,25.6,25.34,25.1,24.88,24.69,24.5,24.26,24.05,23.83,23.68,23.49,23.29,23.06,22.8,22.55,22.33,22.13,21.93,21.68,21.48,21.26,21.1,20.91,20.69,20.45,20.17,19.92,19.69,19.48,19.27,19.02,18.82,18.61,18.45,18.25,18.01,17.78,17.56,17.38,17.22,17.08,16.91,16.73,16.6,16.46,16.3,16.1,15.84,15.61,15.46,15.36,15.28,15.2,15.07,14.96,14.91,14.85,14.75,14.63,14.46,14.31,14.21,14.12,14.07,13.96,13.82,13.74,13.69,13.64,13.54,13.5,13.41,13.36,13.3,13.23,13.18,13.08,12.97,12.94,12.91,12.86,12.77,12.73,12.65,12.58,12.5,12.44,12.39,12.3,12.24,12.26,12.24,12.19,12.1,12.07,11.99,11.92,11.85,11.83,11.78,11.7,11.65,11.67,11.64,11.59,11.5,11.47,11.4,11.33,11.28,11.28,11.24,11.17,11.13,11.14,11.11,11.05,10.97,10.94,10.87,10.8,10.75,10.74,10.69,10.63,10.6,10.6,10.56,10.5,10.42,10.4,10.33,10.26,10.21,10.19,10.14,10.09,10.06,10.05,10,9.94,9.87,9.84,9.78,9.71,9.66,9.64,9.57,9.53,9.5,9.49,9.44,9.37,9.3,9.28,9.22,9.15,9.11,9.07,9,8.96,8.94,8.92,8.86,8.79,8.73,8.71,8.65,8.59,8.54,8.5,8.42,8.39,8.37,8.35,8.28,8.21,8.15,8.13,8.08,8.01,7.97,7.93,7.85,7.83,7.82,7.79,7.73,7.68,7.64,7.62,7.57,7.51,7.46,7.41,7.34,7.33,7.32,7.3,7.25,7.22,7.19,7.17,7.12,7.06,7.02,6.97,6.89,6.88,6.87,6.85,6.8,6.77,6.74,6.71,6.66,6.61,6.57,6.51,6.44,6.43,6.41,6.39,6.35,6.32,6.28,6.25,6.19,6.14,6.11,6.05,5.98,5.96,5.95,5.92,5.88,5.86,5.81,5.77,5.71,5.67,5.63,5.57,5.5,5.49,5.47,5.44,5.41,5.39,5.33,5.28,5.23,5.19,5.16,5.13,5.1,5.08,5.06,5.04,5.02,4.99,4.97,4.95,4.92,4.89,4.86,4.83,4.84,4.86,4.84,4.82,4.8,4.77,4.75,4.77,4.78,4.77,4.75,4.73,4.7,4.7,4.71,4.69,4.67,4.65,4.63,4.61,4.62,4.63,4.62,4.61,4.59,4.56,4.56,4.56,4.54,4.51,4.5,4.48,4.46,4.47,4.48,4.48,4.47,4.45,4.43,4.43,4.42,4.39,4.37,4.36,4.35,4.33,4.34,4.35,4.34,4.33,4.33,4.31,4.31,4.3,4.27,4.24,4.23,4.22,4.2,4.21,4.22,4.2,4.2,4.2,4.18,4.18,4.17,4.13,4.11,4.1,4.09,4.07,4.07,4.08,4.07,4.06,4.06,4.05,4.05,4.03,3.99,3.97,3.95,3.94,3.92,3.92,3.93,3.92,3.91,3.91,3.9,3.91,3.88,3.84,3.82,3.8,3.79,3.76,3.76,3.77,3.76,3.75,3.75,3.74,3.75,3.71,3.68,3.65,3.63,3.62,3.6,3.59,3.6,3.59,3.58,3.58,3.58,3.6,3.57,3.53,3.51,3.49,3.47,3.45,3.45,3.46,3.44,3.43,3.43,3.43,3.46,3.44,3.41,3.4,3.37,3.35,3.33,3.32,3.33,3.31,3.3,3.3,3.3,3.32,3.31,3.29,3.27,3.24,3.22,3.2,3.19,3.19,3.18,3.16,3.16,3.16,3.18,3.17,3.15,3.14,3.11,3.08,3.06,3.05,3.06,3.05,3.03,3.03,3.03,3.04,3.03,3.02,3.01,2.98,2.95,2.93,2.91,2.91,2.91,2.9,2.89,2.89,2.9,2.89,2.89,2.89,2.88,2.85,2.83,2.81,2.81,2.81,2.8,2.79,2.79,2.79,2.79,2.79,2.81,2.81,2.8,2.78,2.76,2.75,2.75,2.75,2.74,2.73,2.72,2.72,2.72,2.73,2.74,2.74,2.74,2.71,2.7,2.69,2.69,2.69,2.68,2.67,2.66,2.66,2.66,2.66,2.67,2.67,2.67,2.64,2.63,2.63,2.63,2.63,2.61,2.6,2.59,2.59,2.58,2.58,2.6,2.59,2.59,2.57,2.56,2.56,2.56,2.56,2.54,2.53,2.52,2.51,2.51,2.51,2.52,2.52,2.51,2.49,2.49,2.49,2.49,2.49,2.47,2.46,2.45,2.44,2.44,2.43,2.44,2.44,2.43,2.42,2.41,2.42,2.42,2.42,2.4,2.39,2.37,2.37,2.36,2.35,2.36,2.35,2.35,2.33,2.33,2.34,2.34,2.34,2.32,2.3,2.29,2.28,2.28,2.27,2.27,2.26,2.26,2.25,2.25,2.25,2.25,2.25,2.23,2.22,2.2,2.2,2.19,2.17,2.18,2.17,2.17,2.16,2.16,2.16,2.16,2.16,2.14,2.12,2.11,2.1,2.09,2.08,2.08,2.07,2.07,2.06,2.06,2.07,2.07,2.06,2.04,2.03,2.02,2.01,1.99,1.98,1.97,1.97,1.97,1.96,1.97,1.97,1.97,1.96,1.94,1.93,1.92,1.91,1.89,1.87,1.86,1.86,1.86,1.86,1.86,1.87,1.87,1.86,1.84,1.82,1.82,1.8,1.78,1.76,1.75,1.75,1.75,1.75,1.76,1.77,1.77,1.75,1.73,1.72,1.71,1.7,1.68,1.65,1.64,1.64,1.65,1.66,1.67,1.68,1.69,1.68,1.66,1.65,1.65,1.64,1.61,1.59,1.58,1.58,1.59,1.6,1.62,1.63,1.64,1.64,1.63,1.62,1.62,1.6,1.58,1.55,1.55,1.54,1.55,1.56,1.57,1.59,1.6,1.6,1.6,1.59,1.59,1.56,1.54,1.52,1.51,1.51,1.51,1.51,1.53,1.54,1.55,1.55,1.56,1.55,1.55,1.53,1.5,1.48,1.48,1.47,1.47,1.47,1.48,1.49,1.5,1.51,1.52,1.52,1.51,1.49,1.47,1.45,1.44,1.44,1.43,1.43,1.44,1.45,1.46,1.47,1.48,1.48,1.47,1.46,1.43,1.41,1.41,1.4,1.39,1.39,1.39,1.4,1.41,1.42,1.43,1.43,1.43,1.41,1.39,1.37,1.37,1.36,1.35,1.34,1.34,1.35,1.36,1.37,1.38,1.38,1.37,1.36,1.34,1.32,1.32,1.31,1.3,1.28,1.28,1.29,1.3,1.31,1.32,1.32,1.32,1.31,1.29,1.27,1.27,1.26,1.25,1.23,1.23,1.24,1.24,1.26,1.26,1.26,1.26,1.25,1.24,1.22,1.22,1.21,1.2,1.18,1.18,1.18,1.19,1.2,1.2,1.21,1.2,1.2,1.19,1.17,1.17,1.16,1.15,1.13,1.12,1.13,1.13,1.15,1.14,1.14,1.14,1.14,1.13,1.12,1.12,1.11,1.09,1.07,1.07,1.07,1.07,1.08,1.08,1.08,1.08,1.08,1.07,1.06,1.06,1.05,1.03,1.01,1.01,1.01,1.01,1.02,1.01,1.01,1.01,1.01,1.01,1,1,0.98,0.97,0.95,0.94,0.94,0.94,0.95,0.94,0.93,0.94,0.94,0.95,0.94,0.93,0.91,0.9,0.88,0.87,0.87,0.88,0.88,0.87,0.86,0.86,0.87,0.88,0.87,0.86,0.84,0.83,0.81,0.8,0.8,0.8,0.81,0.8,0.78,0.79,0.8,0.81,0.81,0.8,0.79,0.78,0.76,0.75,0.75,0.75,0.75,0.74,0.72,0.73,0.74,0.76,0.76,0.76,0.75,0.74,0.72,0.72,0.71,0.71,0.71,0.7,0.68,0.68,0.69,0.71,0.71,0.72,0.71,0.7,0.69,0.68,0.68,0.68,0.68,0.66,0.64,0.64,0.65,0.66,0.67,0.68,0.67,0.67,0.66,0.65,0.64,0.64,0.64,0.62,0.61,0.61,0.61,0.62,0.63,0.64,0.64,0.64,0.63,0.62,0.61,0.61,0.6,0.59,0.57,0.57,0.57,0.57,0.59,0.61,0.61,0.6,0.59,0.59,0.58,0.57,0.56,0.54,0.53,0.53,0.52,0.53,0.55,0.56,0.57,0.57,0.56,0.55,0.54,0.53,0.52,0.5,0.49,0.48,0.47,0.47,0.5,0.52,0.53,0.53,0.52,0.51,0.5,0.49,0.47,0.45,0.44,0.43,0.42,0.42,0.44,0.47,0.49,0.48,0.48,0.48,0.48,0.46,0.44,0.43,0.42,0.41,0.39,0.39,0.42,0.45,0.47,0.47,0.48,0.48,0.48,0.47,0.45,0.44,0.43,0.42,0.41,0.4,0.43,0.46,0.48,0.48,0.48,0.48,0.48,0.47,0.46,0.45,0.44,0.43,0.41,0.41,0.44,0.46,0.48,0.48,0.48,0.48,0.48,0.47,0.46,0.45,0.45,0.43,0.42,0.42,0.44,0.46,0.48,0.48,0.48,0.48,0.48,0.47,0.46,0.45,0.45,0.43,0.42,0.41,0.43,0.45,0.47,0.47,0.47,0.47,0.47,0.46,0.45,0.44,0.44,0.42,0.41,0.4,0.42,0.44,0.45,0.46,0.46,0.45,0.45,0.45,0.44,0.43,0.42,0.41,0.39,0.39,0.4,0.42,0.43,0.44,0.43,0.43,0.43,0.42,0.42,0.41,0.4,0.38,0.37,0.36,0.37,0.38,0.4,0.41,0.4,0.4,0.4,0.39,0.39,0.38,0.37,0.35,0.33,0.33,0.33,0.34,0.36,0.37,0.36,0.36,0.36,0.35,0.35,0.34,0.33,0.31,0.29,0.28,0.28,0.29,0.31,0.32,0.32,0.31,0.31,0.31,0.3,0.29,0.28,0.26,0.24,0.23,0.22,0.23,0.24,0.26,0.27,0.27,0.27,0.27,0.26,0.26,0.25,0.24,0.22,0.2,0.18,0.19,0.2,0.22,0.24,0.25,0.25,0.25,0.25,0.25,0.24,0.23,0.22,0.21,0.19,0.17,0.18,0.19,0.2,0.22,0.23,0.23,0.23,0.23,0.23,0.22,0.21,0.21,0.19,0.17,0.16,0.17,0.17,0.19,0.2,0.21,0.21,0.21,0.21,0.21,0.2,0.19,0.19,0.17,0.16,0.14,0.15,0.16,0.16,0.17,0.18,0.18,0.18,0.18,0.18,0.17,0.16,0.16,0.15,0.14,0.13,0.13,0.13,0.14,0.14,0.15,0.15,0.14,0.14,0.14,0.14,0.13,0.13,0.12,0.11,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.09,0.09,0.09,0.09,0.08,0.09,0.09,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.07,0.07,0.07,0.07,0.07,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.1,0.11,0.09,0.09,0.09,0.09,0.09,0.11,0.11,0.11,0.11,0.11,0.11,0.11,0.11,0.11,0.12,0.11,0.11,0.11,0.11,0.11,0.12,0.12,0.12,0.12,0.11,0.11,0.11,0.11,0.12,0.13,0.12,0.12,0.12,0.12,0.12,0.13,0.13,0.13,0.12,0.12,0.12,0.12,0.12,0.13,0.14,0.13,0.13,0.13,0.13,0.12,0.13,0.14,0.13,0.13,0.12,0.12,0.12,0.13,0.14,0.15,0.14,0.14,0.14,0.14,0.14,0.14,0.14,0.14,0.13,0.12,0.13,0.13,0.13,0.14,0.15,0.15,0.15,0.15,0.15,0.15,0.15,0.15,0.15,0.14,0.13,0.13,0.13,0.13,0.14,0.15,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.15,0.14,0.13,0.13,0.13,0.13,0.14,0.15,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.15,0.14,0.13,0.12,0.12,0.12,0.13,0.14,0.15,0.16,0.16,0.16,0.16,0.16,0.16,0.15,0.14,0.13,0.12,0.11,0.11,0.12,0.13,0.14,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.15,0.14,0.12,0.11,0.12,0.12,0.13,0.14,0.15,0.16,0.17,0.17,0.17,0.17,0.16,0.16,0.15,0.14,0.12,0.12,0.12,0.12,0.13,0.14,0.15,0.17,0.17,0.17,0.17,0.17,0.17,0.16,0.15,0.14,0.12,0.12,0.12,0.12,0.13,0.14,0.16,0.17,0.17,0.17,0.17,0.17,0.17,0.17,0.16,0.14,0.13,0.13,0.13,0.13,0.14,0.15,0.16,0.17,0.17,0.17,0.17,0.17,0.17,0.17,0.16,0.15,0.13,0.13,0.14,0.14,0.15,0.15,0.16,0.17,0.17,0.17,0.17,0.17,0.17,0.17,0.16,0.15,0.14,0.14,0.14,0.14,0.15,0.15,0.15,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.15,0.14,0.14,0.14,0.14,0.14,0.14,0.14,0.15,0.14,0.14,0.14,0.14,0.15,0.16,0.15,0.14,0.14,0.14,0.14,0.14,0.14,0.13,0.12,0.13,0.13,0.12,0.12,0.12,0.13,0.14,0.14,0.13,0.13,0.13,0.13,0.13,0.13,0.12,0.1,0.1,0.1,0.1,0.1,0.1,0.11,0.12,0.12,0.12,0.12,0.11,0.11,0.11,0.11,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.11,0.12,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.12,0.12,0.13,0.13,0.13,0.13,0.13,0.14,0.15,0.15,0.14,0.14,0.14,0.14,0.14,0.14,0.13,0.14,0.16,0.16,0.15,0.15,0.16,0.16,0.17,0.17,0.16,0.14,0.14,0.14,0.14,0.14,0.14,0.15,0.18,0.18,0.17,0.17,0.18,0.18,0.19,0.18,0.17,0.15,0.15,0.15,0.15,0.15,0.15,0.17,0.19,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.19,0.17,0.17,0.17,0.17,0.19,0.2,0.22,0.25,0.27,0.27,0.27,0.27,0.27,0.28,0.3,0.3,0.31,0.32,0.34,0.34,0.36,0.38,0.4,0.44,0.48,0.5,0.53,0.57,0.61,0.65,0.69,0.72,0.77,0.8,0.83,0.86,0.87,0.87,0.87,0.85,0.84,0.82,0.81,0.8,0.81,0.81,0.81,0.82,0.82,0.81,0.81,0.79,0.77,0.74,0.69,0.64,0.59,0.52,0.47,0.43,0.39,0.37,0.37,0.37,0.37,0.37,0.37,0.37,0.37,0.34,0.32,0.3,0.26,0.2,0.16,0.12,0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.04,0.11,0.11,0.11,0.15,0.18,0.21,0.24,0.27,0.29,0.32,0.34,0.36,0.39,0.43,0.48,0.52,0.56,0.56,0.56,0.64,0.64,0.64,0.64,0.64,0.69,1.02,1.33,1.03,1.03,1,0 - - - - - - - - 45 - L3-M1,2.376,9 - L2-M1,2.518,0 - L3-M4,2.692,25 - L3-M5,2.697,226 - L2-M4,2.834,0 - L1-M2,2.891,0 - L1-M3,2.916,0 - L3-N1,2.923,1 - L3-N5,3.002,24 - L2-N1,3.065,0 - L2-N4,3.144,0 - L1-N2,3.361,0 - L1-N3,3.365,0 - K-L2,20.074,636 - K-L3,20.216,1183 - K-M2,22.699,75 - K-M3,22.724,145 - K-M4,22.908,2 - K-N2,3,23.169,38 - K-N4,23.218,0 - - - 18 - K-L2,2.955,752 - K-L3,2.958,1486 - K-M2,3.19,83 - K-M3,3.19,163 - - - 16 - K-L3,2.307,66 - K-L2,2.307,33 - K-M3,2.464,5 - K-M2,2.47,2 - - - 14 - K-L2,1.739,4 - K-L3,1.74,8 - K-M2,1.837,0 - K-M3,1.837,0 - - - 13 - K-L2,1.486,9 - K-L3,1.486,18 - K-M2,1.557,0 - K-M3,1.557,0 - - - 12 - K-L2,1.253,10 - K-L3,1.254,20 - K-M2,1.302,0 - K-M3,1.302,0 - - - 15 - K-L2,2.013,4595 - K-L3,2.014,9348 - K-M3,2.134,673 - K-M2,2.144,340 - - - 17 - K-L2,2.62,56 - K-L3,2.622,111 - K-M2,2.812,6 - K-M3,2.812,11 - - - 19 - K-L2,3.311,390 - K-L3,3.314,774 - K-M2,3.59,53 - K-M3,3.59,105 - - - 20 - K-L2,3.689,133910 - K-L3,3.692,267206 - K-M3,4.013,38771 - K-M2,4.013,20084 - - - 22 - K-L2,4.506,0 - K-L3,4.512,0 - K-M3,4.933,0 - K-M2,4.933,0 - K-M4,4.964,0 - - - 24 - K-L2,5.405,0 - K-L3,5.415,0 - K-M3,5.947,0 - K-M2,5.947,0 - K-M4,5.987,0 - - - 25 - K-L2,5.888,2 - K-L3,5.899,3 - K-M2,6.49,0 - K-M3,6.49,1 - K-M4,6.535,0 - - - 26 - K-L2,6.391,265 - K-L3,6.404,524 - K-M3,7.058,84 - K-M2,7.058,44 - K-M4,7.108,1 - - - 27 - K-L2,6.915,0 - K-L3,6.93,0 - K-M3,7.649,0 - K-M2,7.649,0 - K-M4,7.706,0 - - - 28 - K-L2,7.461,0 - K-L3,7.478,0 - K-M2,8.265,0 - K-M3,8.265,0 - K-M4,8.329,0 - - - 29 - K-L2,8.028,0 - K-L3,8.048,0 - K-M2,8.903,0 - K-M3,8.905,0 - K-M4,8.977,0 - - - 30 - K-L2,8.616,350 - K-L3,8.639,689 - K-M2,9.567,58 - K-M3,9.57,112 - K-M4,9.65,1 - - - 33 - K-L2,10.508,336 - K-L3,10.543,656 - K-M2,11.721,60 - K-M3,11.726,115 - K-M4,11.825,1 - K-N2,3,11.864,2 - - - 34 - K-L2,11.184,0 - K-L3,11.224,0 - K-M2,12.492,0 - K-M3,12.497,0 - K-M4,12.602,0 - K-N2,3,12.655,0 - - - 35 - K-L2,11.878,8 - K-L3,11.924,15 - K-M2,13.285,1 - K-M3,13.292,3 - K-M4,13.404,0 - K-N2,3,13.471,0 - - - 51 - L3-M1,3.192,59 - L2-M1,3.44,0 - L3-M4,3.595,165 - L3-M5,3.604,1480 - L2-M4,3.842,0 - L1-M2,3.885,4 - L1-M3,3.932,7 - L3-N1,3.979,9 - L3-N5,4.099,260 - L2-N1,4.229,0 - L2-N4,4.347,0 - L1-N2,4.602,1 - L1-N3,4.602,2 - L1-O3,4.697,0 - K-L2,26.111,0 - K-L3,26.359,0 - K-M2,29.678,0 - K-M3,29.725,0 - K-M4,29.953,0 - K-N2,3,30.395,0 - K-N4,30.458,0 - - - 50 - L3-M1,3.044,0 - L2-M1,3.271,0 - L3-M4,3.436,0 - L3-M5,3.444,0 - L2-M4,3.663,0 - L1-M2,3.708,0 - L1-M3,3.75,0 - L3-N1,3.792,0 - L3-N5,3.904,0 - L2-N1,4.019,0 - L2-N4,4.131,0 - L1-N3,4.381,0 - L1-N2,4.381,0 - L1-O3,4.464,0 - K-L2,25.044,0 - K-L3,25.271,0 - K-M2,28.444,0 - K-M3,28.485,0 - K-M4,28.707,0 - K-N2,3,29.116,0 - K-N4,29.175,0 - - - 82 - M4-N2,1.823,0 - M5-N3,1.84,3 - M3-N1,2.174,0 - M5-N6,2.34,7 - M5-N7,2.345,292 - M5-O3,2.399,42 - M4-N6,2.444,0 - M4-O2,2.477,0 - M3-N5,2.654,0 - M3-O4,5,3.045,0 - M2-N4,3.124,0 - M1-N3,3.202,0 - L3-M1,9.184,15 - L3-M4,10.449,39 - L3-M5,10.551,357 - L2-M1,11.349,4 - L3-N1,12.143,7 - L1-M2,12.307,0 - L3-N5,12.601,114 - L2-M4,12.614,203 - L1-M3,12.795,0 - L3-O4,13.014,15 - L2-N1,14.308,1 - L2-N4,14.766,48 - L1-N2,15.099,0 - L2-O4,15.179,9 - L1-N3,15.217,0 - L1-O3,15.777,0 - - - 83 - M4-N2,1.883,0 - M5-N3,1.901,0 - M3-N1,2.239,0 - M5-N6,2.417,0 - M5-N7,2.423,0 - M4-N6,2.526,0 - M4-O2,2.571,0 - M3-N5,2.737,0 - M3-O4,5,3.153,0 - M2-N4,3.234,0 - M1-N3,3.315,0 - L3-M1,9.42,0 - L3-M4,10.731,0 - L3-M5,10.839,0 - L2-M1,11.712,0 - L3-N1,12.48,0 - L1-M2,12.692,0 - L3-N5,12.955,0 - L2-M4,13.023,0 - L1-M3,13.211,0 - L3-O4,13.392,0 - L2-N1,14.773,0 - L2-N4,15.247,0 - L1-N2,15.583,0 - L2-O4,15.684,0 - L1-N3,15.709,0 - L1-O3,16.295,0 - - - 80 - M4-N2,1.712,0 - M5-N3,1.724,0 - M3-N1,2.036,0 - M5-N6,2.19,0 - M5-N7,2.195,0 - M4-N6,2.281,0 - M3-N5,2.488,0 - M3-O4,5,2.838,0 - M2-N4,2.9,493 - M1-N3,2.986,102 - L3-M1,8.722,9 - L3-M4,9.899,24 - L3-M5,9.989,222 - L2-M1,10.647,2 - L3-N1,11.482,4 - L1-M2,11.56,0 - L2-M4,11.824,125 - L3-N5,11.906,71 - L1-M3,11.992,0 - L3-O4,12.274,8 - L2-N1,13.41,1 - L2-N4,13.831,30 - L1-N2,14.159,0 - L2-O4,14.199,5 - L1-N3,14.262,0 - L1-O3,14.778,0 - - - 79 - M4-N2,1.648,0 - M5-N3,1.661,0 - M4-N3,1.746,0 - M3-N1,1.981,0 - M5-N6,2.118,0 - M5-N7,2.123,0 - M4-N6,2.203,0 - M3-N5,2.408,0 - M3-O4,5,2.742,0 - M2-N4,2.797,135 - M1-N3,2.883,0 - L3-M1,8.494,0 - L3-M4,9.628,0 - L3-M5,9.713,0 - L2-M1,10.309,0 - L3-N1,11.157,0 - L1-M2,11.205,0 - L2-M4,11.443,0 - L3-N5,11.566,0 - L1-M3,11.61,0 - L3-O4,11.914,0 - L2-N1,12.974,0 - L2-N4,13.381,0 - L1-N2,13.71,0 - L2-O4,13.729,0 - L1-N3,13.807,0 - L1-O3,14.3,0 - - - 47 - L3-M1,2.632,0 - L2-M1,2.805,0 - L3-M4,2.977,0 - L3-M5,2.983,0 - L2-M4,3.15,0 - L1-M2,3.202,0 - L1-M3,3.233,0 - L3-N1,3.254,0 - L3-N5,3.347,0 - L2-N1,3.428,0 - L2-N4,3.52,0 - L1-N2,3.742,0 - L1-N3,3.748,0 - K-L2,21.99,0 - K-L3,22.163,0 - K-M2,24.91,0 - K-M3,24.941,0 - K-M4,25.14,0 - K-N2,3,25.45,0 - K-N4,25.51,0 - - - 48 - L3-M1,2.766,0 - L2-M1,2.955,0 - L3-M4,3.126,0 - L3-M5,3.133,0 - L2-M4,3.315,0 - L1-M2,3.365,0 - L1-M3,3.4,0 - L3-N1,3.428,0 - L3-N5,3.526,0 - L2-N1,3.619,0 - L2-N4,3.715,0 - L1-N3,3.954,0 - L1-N2,3.954,0 - K-L2,22.984,0 - K-L3,23.173,0 - K-M2,26.058,0 - K-M3,26.093,0 - K-M4,26.299,0 - K-N2,3,26.647,0 - K-N4,26.699,0 - - - 40 - L3-M1,1.792,0 - L2-M1,1.876,0 - L3-M4,2.04,0 - L3-M5,2.042,0 - L2-M4,2.124,0 - L3-N1,2.171,0 - L1-M2,2.187,0 - L1-M3,2.201,0 - L3-N5,2.219,0 - L2-N1,2.255,0 - L2-N4,2.303,0 - L1-N2,2.503,0 - L1-N3,2.503,0 - K-L2,15.691,0 - K-L3,15.775,0 - K-M2,17.654,0 - K-M3,17.668,0 - K-M4,17.817,0 - K-N2,3,17.97,0 - - - 38 - K-L2,14.098,833 - K-L3,14.165,1607 - K-M2,15.825,137 - K-M3,15.835,264 - K-M4,15.969,3 - K-N2,3,16.083,50 - - - 56 - L3-M1,3.954,5 - L2-M1,4.331,1 - L3-M4,4.451,14 - L3-M5,4.466,129 - L2-M4,4.828,40 - L1-M2,4.852,16 - L1-M3,4.926,28 - L3-N1,4.994,1 - L3-N5,5.154,26 - L2-N1,5.371,0 - L2-N4,5.531,6 - L1-N2,5.797,5 - L1-N3,5.81,7 - L1-O3,5.937,0 - K-L2,31.817,5 - K-L3,32.194,9 - K-M2,36.304,1 - K-M3,36.378,1 - K-M4,36.645,0 - K-N2,3,37.249,1 - K-N4,37.348,0 - - - - - - - - diff --git a/hyperspy/tests/io/bruker_data/bruker_nano.spx b/hyperspy/tests/io/bruker_data/bruker_nano.spx deleted file mode 100755 index 185e91f9cb..0000000000 --- a/hyperspy/tests/io/bruker_data/bruker_nano.spx +++ /dev/null @@ -1,285 +0,0 @@ - - - - - - - this is spectrum decription. What does it mean? - - - - RTHardware - 191 - - 7575 - 7385 - 3 - 96 - 1408 - 8927 - 1E4 - 130000 - -20 - 1 - - - - RTDetector - 3 - 9936 - - SD3pr - 9990 - XFlash 5010 - 4.5E-1 - 2.9E-2 - eJyzcUkt8UmsTC0qtrMB0wYKjiX5ubZKhsZKCiEZmcnZeanFxbZKpq66xkr6UDWGUDXmKEos9ExddY3gioygikxRFJkhm2MMVWKGW4kJVIkFqhK4VfoI9wMAqUU9Ow== - slew AP3.3 - - - - - - - - - 3 - 21 - 5 - -3 - 0 - 1 - 0 - 1 - 1 - 8.5E-1 - -1 - 0.07,0.0058,0.183,0.0078,0.277,0.0058,0.555,0,1.1,0,3.293,0.0064,5.89,0,0,0,0,0, - 0,0.01,0.000801,0.01,0.00298,0.01,0.008902,0.01,0.025,0.010046,0.041098,0.013475,0.04702,0.017302,0.049199,0.019237,0.05,0.02, - 0,0.03,0.00444,0.03,0.01651,0.03,0.049318,0.03,0.1385,0.03023,0.227682,0.047375,0.26049,0.06651,0.27256,0.076185,0.277,0.08, - 0,0.03,0.006283,0.03,0.023364,0.03,0.069793,0.03,0.196,0.030228,0.322207,0.047377,0.368636,0.066512,0.385717,0.076186,0.392,0.08, - 0,0,0.008415,0,0.031291,0,0.093473,0,0.2625,0,0.431527,0.000035,0.493709,0.000073,0.516585,0.000092,0.525,0.0001, - 0,0,0.010836,0,0.040291,0,0.120357,0,0.338,0,0.555643,0.000035,0.635709,0.000073,0.665164,0.000092,0.676,0.0001, - 0,0,0.016687,0,0.062045,0,0.185343,0,0.5205,0,0.855657,0.000035,0.978955,0.000073,1.024313,0.000092,1.041,0.0001, - 0,0,0.020101,0,0.07474,0,0.223266,0,0.627,0,1.030734,0.000035,1.17926,0.000073,1.233899,0.000092,1.254,0.0001, - 0,0,0.023836,0,0.088627,0,0.26475,0,0.7435,0,1.22225,0.000035,1.398373,0.000073,1.463164,0.000092,1.487,0.0001, - 0,0,0.027891,0,0.103707,0,0.309795,0,0.87,0,1.430205,0.000035,1.636293,0.000073,1.712109,0.000092,1.74,0.0001, - 0,0,0.030776,0,0.114435,0,0.341842,0,0.96,0,1.578158,0.000035,1.805565,0.000073,1.889224,0.000092,1.92,0.0001, - 0,0,0.032283,0,0.120037,0,0.358578,0,1.007,0,1.655422,0.000035,1.893963,0.000073,1.981717,0.000092,2.014,0.0001, - 0,0,0.036996,0,0.13756,0,0.410923,0,1.154,0,1.897077,0.000035,2.17044,0.000073,2.271004,0.000092,2.308,0.0001, - 0,0,0.05918,0,0.220049,0,0.657334,0,1.846,0,3.034665,0.000035,3.471952,0.000073,3.63282,0.000092,3.692,0.0001, - 0,0,0.079378,0,0.295146,0,0.881668,0,2.476,0,4.070332,0.000035,4.656854,0.000073,4.872623,0.000092,4.952,0.0001, - 0,0,0.119867,0,0.445699,0,1.331404,0,3.739,0,6.146595,0.000035,7.0323,0.000073,7.358133,0.000092,7.478,0.0001, - 0,0,0.148303,0,0.551433,0,1.647253,0,4.626,0,7.604747,0.000035,8.700567,0.000073,9.103698,0.000092,9.252,0.0001, - 0,0,0.176322,0,0.655616,0,1.958472,0,5.5,0,9.041529,0.000035,10.344384,0.000073,10.823678,0.000092,11,0.0001, - 0,0,0.208381,0,0.774819,0,2.314557,0,6.5,0,10.685443,0.000035,12.225181,0.000073,12.791619,0.000092,13,0.0001, - 0,0,0.24044,0,0.894022,0,2.670643,0,7.5,0,12.329357,0,14.105978,0,14.759561,0,15,0, - 0,0,0.320586,0,1.192029,0,3.560857,0,10,0,16.439142,0,18.80797,0,19.679415,0,20,0, - 0,0,1.60293,0,5.960146,0,17.804287,0,50,0,82.195709,0,94.039856,0,98.397072,0,100,0, - - - - RTESMA - 706 - - 1.5E1 - -1 - -1 - 2.000100008E-3 - 3.5E1 - 4.5E1 - 2 - - 6.666666667E2 - 9 - - - - 82 - 2.3.2018 - - 4096 - -2.38925E-1 - 2.5E-3 - 2.473828662E-4 - 4.349876499E-4 - - Acquired - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,2,10,9,14,12,29,59,52,83,130,160,178,269,322,380,433,478,557,665,640,620,644,609,641,617,464,434,402,307,270,223,169,134,106,78,51,44,20,20,15,8,3,2,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,6,5,5,6,5,8,8,2,4,6,3,10,2,6,4,4,8,2,2,3,2,4,2,2,9,6,1,6,7,5,3,6,8,3,6,9,18,13,9,11,12,15,11,13,20,20,17,21,20,23,22,19,29,19,18,18,21,25,20,25,26,27,32,19,18,10,13,14,12,15,11,8,7,4,9,7,5,5,5,5,8,7,3,1,2,4,5,3,4,8,6,4,3,4,2,6,5,7,6,4,3,3,8,4,6,1,6,4,4,5,5,7,7,5,6,3,5,4,4,5,5,5,0,8,5,8,10,9,5,7,8,9,8,6,9,10,8,9,11,8,12,6,5,7,8,11,8,13,15,12,8,10,21,21,9,16,15,21,17,22,22,19,15,20,12,15,20,29,25,15,30,27,24,26,25,33,34,26,32,36,33,27,39,44,26,34,29,35,40,35,41,33,34,47,45,46,45,53,51,43,35,42,38,39,38,45,36,35,29,40,28,52,41,26,36,43,41,44,33,53,52,57,69,69,79,83,84,86,99,95,94,99,105,108,120,118,108,122,97,103,99,87,80,82,87,75,86,71,54,50,53,53,41,34,31,46,38,31,29,37,28,34,24,27,21,36,28,31,28,39,36,34,40,43,30,41,40,39,30,41,36,46,31,32,33,40,27,42,23,45,40,37,28,32,23,44,31,31,39,36,36,45,40,48,50,68,57,96,80,86,97,119,121,134,152,168,177,203,212,227,211,232,260,268,306,270,295,322,294,281,265,266,237,278,241,213,190,177,166,163,151,138,115,86,75,82,69,54,48,49,58,44,31,35,26,23,24,22,26,22,20,17,19,16,8,11,15,14,11,15,14,14,22,9,9,11,21,12,11,10,15,9,10,8,11,8,14,12,11,12,7,10,13,11,8,6,13,14,6,10,11,17,10,13,11,15,11,17,14,14,7,13,8,10,6,7,10,10,13,13,13,11,5,12,17,16,4,13,14,15,9,11,11,11,14,11,6,12,20,15,15,21,15,9,12,11,14,14,14,22,19,12,17,19,14,11,17,8,16,15,22,11,11,14,26,11,18,13,12,7,14,17,14,19,11,14,19,9,13,17,9,12,17,19,18,16,18,13,10,8,8,10,10,7,12,19,13,13,16,14,13,14,12,18,12,13,12,16,21,12,11,14,14,11,15,9,15,11,13,15,13,16,9,19,14,10,12,20,7,18,14,12,12,16,13,14,12,12,11,16,11,12,10,12,11,10,13,18,20,9,10,13,9,14,22,7,16,14,18,16,18,18,15,13,8,14,11,18,14,10,10,15,14,12,12,18,11,14,15,14,15,8,15,8,15,11,11,20,12,16,16,12,11,19,14,6,13,19,9,15,12,16,15,17,14,14,13,10,13,16,11,14,11,21,18,9,18,9,12,9,13,8,14,16,11,11,20,15,19,18,14,15,16,10,9,15,22,13,14,20,13,15,15,18,17,17,12,19,11,21,26,17,20,9,9,17,15,17,18,17,18,17,14,19,10,15,23,14,15,9,14,15,16,15,15,14,16,20,17,14,12,14,13,10,9,19,14,9,14,19,7,13,18,21,14,16,17,15,13,28,19,11,14,21,13,18,10,17,12,13,12,18,13,9,11,7,9,26,13,17,17,11,11,26,12,17,15,19,11,16,16,12,16,11,13,19,14,17,21,14,10,10,22,13,17,7,19,18,18,11,17,20,9,16,15,14,14,17,18,15,15,15,15,11,18,20,20,22,19,22,20,20,23,13,13,19,18,17,16,16,20,21,21,28,21,19,26,19,23,16,12,29,18,20,14,18,24,26,28,16,24,21,20,35,18,28,30,16,15,26,23,24,30,25,22,32,21,30,24,27,29,22,32,33,24,35,25,25,23,21,31,28,28,35,27,35,36,40,31,45,67,48,55,58,60,77,59,63,98,106,116,119,135,143,156,173,195,211,245,256,275,297,316,356,350,375,368,421,426,487,489,482,513,492,567,563,572,608,567,558,541,556,496,483,511,504,454,436,441,389,366,361,305,339,308,297,246,241,192,179,166,137,148,124,96,102,79,81,82,62,68,38,54,53,45,49,41,40,45,33,29,41,36,38,32,46,41,32,42,33,47,47,35,39,48,47,53,55,49,53,49,44,46,63,43,36,39,55,43,64,37,39,46,36,35,40,35,24,38,23,31,22,24,33,19,30,11,19,20,14,17,13,18,17,13,19,12,14,15,20,12,10,9,8,11,13,19,9,8,15,9,9,6,8,8,14,8,6,6,10,9,12,7,9,14,15,6,11,6,11,10,11,10,5,20,8,10,9,9,9,6,14,6,9,9,9,15,5,11,5,5,16,5,9,17,11,4,8,8,9,10,7,11,5,8,7,10,11,5,5,5,10,6,10,3,11,6,9,5,10,15,13,9,13,9,10,11,7,8,11,9,11,2,7,12,10,5,7,8,9,7,9,9,9,4,7,6,2,7,11,4,12,10,9,9,16,10,12,12,5,9,8,9,10,8,4,7,7,17,11,8,11,5,9,10,10,4,14,9,5,16,10,7,11,8,9,10,6,8,7,9,8,7,13,9,10,12,7,5,11,6,5,12,11,12,6,8,7,8,12,10,9,9,12,10,9,5,8,7,12,7,11,5,5,7,10,8,11,9,3,11,11,10,12,6,14,7,9,9,7,7,7,16,7,10,8,8,7,3,5,9,10,9,10,8,15,8,9,7,9,9,7,5,9,4,8,4,10,7,7,11,8,6,6,5,12,8,3,11,5,10,7,14,6,10,8,11,7,9,12,13,14,11,10,8,6,7,11,8,4,5,12,6,8,7,8,13,7,9,7,7,7,7,8,12,8,8,5,12,19,6,6,7,4,17,6,10,7,7,14,5,11,2,10,11,12,8,4,9,10,8,6,12,3,5,4,7,9,7,10,7,9,10,5,12,14,7,7,7,14,11,5,7,11,9,5,8,6,9,10,11,6,8,13,5,9,4,8,12,3,11,4,6,8,7,6,7,11,6,4,2,5,4,6,6,3,9,6,9,9,8,8,12,6,11,4,7,7,8,10,8,8,10,9,6,7,11,5,7,8,9,10,6,7,5,8,6,6,8,14,7,20,14,12,8,5,5,9,12,6,3,4,9,6,8,6,2,8,7,11,4,6,7,8,9,4,8,6,10,6,6,7,4,3,8,8,9,3,5,6,4,8,11,14,10,5,13,12,8,10,7,9,15,6,8,6,6,6,4,6,7,13,3,5,5,5,13,2,3,8,3,12,9,5,6,4,4,6,8,7,8,11,5,6,6,6,8,5,2,6,6,6,7,5,3,12,5,5,4,9,4,7,4,7,6,8,10,10,6,14,5,7,3,8,2,7,5,6,6,6,11,5,3,6,3,9,8,12,9,7,9,6,3,9,4,5,8,5,10,7,6,7,4,10,9,2,7,9,7,8,5,6,5,15,8,9,6,5,4,7,6,8,6,7,5,2,6,5,8,4,4,4,8,3,7,8,5,7,6,8,7,7,4,7,6,9,11,9,5,9,9,12,5,9,9,7,3,7,5,8,4,5,13,11,6,4,7,7,8,6,6,7,8,2,4,6,2,6,5,4,5,3,12,6,6,5,3,6,1,5,12,5,4,3,7,5,10,5,10,4,8,6,9,6,5,7,10,6,5,9,10,4,8,1,11,7,3,7,2,8,4,4,6,6,6,3,6,7,6,5,8,7,9,10,6,3,6,8,3,2,8,7,6,6,5,8,3,4,9,6,6,11,5,5,5,6,4,7,7,7,11,8,11,5,5,3,4,7,6,4,5,7,6,6,11,7,8,8,10,6,7,6,6,6,6,5,10,4,5,4,5,10,2,10,4,3,6,9,7,2,3,1,6,9,11,7,2,4,8,4,4,9,8,5,6,11,7,2,9,4,9,4,5,8,6,1,6,8,2,3,7,5,4,11,7,6,6,8,7,7,6,5,4,7,5,5,7,5,8,3,8,6,8,1,5,8,4,4,0,8,7,5,8,8,5,6,4,5,3,9,5,5,8,9,10,7,5,6,4,4,6,7,2,5,6,14,11,5,7,9,5,3,4,8,8,4,6,4,4,6,6,8,6,10,1,6,10,5,4,6,4,4,5,12,4,2,11,7,3,4,4,6,4,7,7,6,5,7,4,3,9,4,6,8,2,4,4,5,2,6,7,3,8,1,7,5,2,7,5,3,5,2,7,8,3,5,6,2,6,7,4,6,6,9,5,4,4,5,9,5,8,8,5,6,7,7,4,4,1,4,4,3,9,5,11,10,2,4,11,4,9,6,6,4,7,4,6,1,4,5,2,7,4,3,8,4,4,5,5,7,8,4,4,4,11,10,2,2,2,7,2,7,4,4,7,5,4,5,4,4,3,7,5,4,3,8,2,7,4,4,5,4,9,5,9,7,4,4,8,6,1,5,5,2,4,3,2,4,7,6,5,4,7,6,4,5,6,6,5,1,5,1,1,3,3,9,4,5,8,5,7,7,7,4,3,9,3,2,8,7,0,5,3,2,2,5,7,4,5,3,5,3,5,4,7,4,4,6,1,1,1,5,3,5,4,8,5,6,4,5,3,7,4,8,5,4,9,5,4,6,7,6,6,5,5,6,7,4,4,4,4,1,5,10,3,7,2,5,6,6,3,1,2,7,4,4,4,4,10,8,7,1,4,12,8,6,8,9,2,3,8,6,2,4,5,3,5,6,6,7,5,2,4,1,11,7,2,4,5,5,6,6,5,7,6,5,2,7,10,5,5,8,1,3,4,3,3,3,3,5,5,6,5,3,4,0,3,3,4,3,9,1,5,5,6,6,5,8,5,8,3,3,4,6,4,3,5,6,3,6,1,5,4,2,1,1,9,4,7,6,3,4,3,3,4,2,3,3,2,5,4,6,3,4,5,5,2,2,3,6,4,4,7,1,6,5,4,7,4,5,3,4,2,8,0,2,7,3,5,5,5,3,7,9,3,6,6,2,5,0,4,2,4,4,2,3,7,5,5,3,3,4,6,1,6,1,8,6,5,3,3,2,8,0,3,2,3,1,4,4,2,3,6,2,0,1,2,2,5,7,3,4,2,5,4,6,5,2,6,4,5,3,6,3,7,6,2,2,5,6,7,2,5,4,2,3,3,3,0,2,1,7,2,3,7,3,3,5,3,4,7,2,3,3,4,5,6,4,1,2,7,5,6,8,3,7,8,2,4,3,5,4,5,0,3,3,5,3,7,2,4,3,4,3,4,3,6,4,4,3,1,4,5,6,1,5,3,5,2,3,5,4,3,4,3,2,7,4,2,8,5,3,4,1,3,3,2,5,4,5,2,4,1,1,4,1,8,1,4,3,1,1,1,5,3,6,4,5,1,1,4,2,8,1,4,2,3,5,6,5,4,3,3,6,1,3,2,6,3,3,2,5,5,6,6,1,6,4,9,4,3,1,4,2,3,5,4,6,5,8,3,2,1,5,6,1,2,4,7,4,2,2,1,4,3,9,7,2,5,1,4,3,4,5,5,2,4,5,6,6,4,3,8,1,11,7,8,9,7,4,7,12,9,11,11,13,9,5,16,19,17,20,19,14,22,20,28,29,33,28,28,31,35,34,37,31,41,39,36,43,61,66,58,61,50,68,63,66,63,61,71,60,79,74,67,75,80,64,61,93,74,94,76,63,74,81,82,81,64,77,69,88,97,55,80,69,62,67,74,61,51,67,68,62,63,49,45,45,44,51,44,41,37,28,40,24,27,29,30,26,19,15,15,21,24,16,11,13,15,12,12,11,7,8,6,15,4,9,4,4,3,5,4,5,8,3,4,5,4,2,6,5,3,3,4,2,1,4,6,1,3,4,4,2,2,4,3,1,3,2,2,1,5,2,4,1,1,3,3,9,2,2,1,3,5,4,9,0,4,4,5,3,6,4,5,6,3,3,1,5,4,2,3,2,2,3,0,6,0,4,3,3,2,2,5,4,6,3,3,3,0,3,5,4,5,2,1,7,3,1,3,7,2,1,1,3,4,4,3,3,4,5,2,1,2,6,0,5,3,2,6,5,3,4,5,4,4,1,0,3,4,4,4,0,4,4,1,3,2,2,4,3,2,3,2,2,2,1,1,0,3,1,6,4,7,2,4,2,4,2,5,2,3,5,3,6,3,3,6,8,2,7,3,4,2,3,6,5,7,7,4,4,8,7,4,3,7,14,11,11,9,9,8,11,15,7,11,11,13,11,14,12,11,7,11,11,10,10,9,9,9,16,12,10,14,17,11,11,10,13,14,12,11,11,11,16,8,13,7,12,10,8,7,6,12,8,7,11,5,6,5,10,8,12,1,6,8,6,5,6,5,8,8,4,6,6,1,4,6,4,1,3,4,5,2,4,4,5,4,5,1,2,6,0,3,3,3,3,1,3,3,2,1,1,3,1,4,1,0,3,4,2,1,4,2,4,4,5,1,4,1,4,1,6,3,4,4,1,4,1,2,0,1,5,3,3,1,3,3,2,1,1,1,0,2,4,3,2,2,2,3,3,5,3,2,2,2,3,1,1,1,2,5,2,3,2,2,2,2,2,2,3,4,2,2,2,3,4,2,3,3,1,2,3,2,4,0,3,4,3,6,0,3,3,5,5,3,3,2,1,5,2,4,1,1,2,2,3,3,2,2,1,1,5,3,1,1,2,5,2,0,0,1,4,6,2,2,1,5,4,1,3,1,1,4,2,3,2,3,5,1,4,2,5,4,3,2,1,2,0,1,0,2,2,5,3,1,3,3,1,1,2,0,3,0,6,2,2,0,2,1,2,3,2,2,3,0,4,1,6,1,1,3,5,0,2,1,0,1,1,1,3,2,2,1,2,2,0,1,2,1,7,2,1,5,3,0,6,3,2,3,2,0,2,0,2,2,6,3,1,2,3,1,2,3,2,0,2,3,3,3,2,1,8,3,2,3,3,1,1,4,2,0,4,0,2,3,3,0,1,2,2,0,0,1,2,4,5,4,2,2,3,4,1,5,6,12,5,3,5,1,5,8,7,8,3,5,8,7,11,7,10,12,11,8,4,10,11,16,11,13,13,12,21,19,17,8,14,17,21,23,23,29,17,22,22,37,25,19,22,37,26,33,40,42,23,26,36,25,30,51,41,30,37,29,32,42,35,40,36,38,29,40,40,34,29,40,31,33,35,48,23,32,31,28,26,44,30,28,25,25,26,26,19,30,29,18,14,25,16,24,22,14,17,16,12,5,10,7,7,14,12,13,9,8,8,8,6,9,4,3,5,10,4,2,4,5,2,3,4,0,4,5,6,3,1,1,4,6,1,7,1,3,1,4,3,3,2,3,1,3,1,3,3,3,2,1,4,1,1,1,3,2,0,7,3,3,3,2,5,1,2,1,0,2,5,6,4,2,1,0,0,1,0,0,1,1,3,0,1,0,1,5,1,1,2,0,0,0,2,1,2,3,2,0,3,3,1,2,0,0,3,7,5,2,1,2,4,2,2,0,0,0,2,1,0,3,0,3,2,1,0,4,0,2,3,2,0,3,3,3,2,2,2,1,1,1,3,2,4,5,3,1,1,1,1,1,1,3,2,1,1,2,1,4,2,4,3,0,2,1,1,0,2,3,3,2,0,0,3,1,2,2,1,1,0,2,3,0,1,0,1,2,2,1,3,0,0,4,1,0,3,2,2,3,2,4,1,3,1,1,3,3,3,1,0,2,1,0,2,5,2,2,1,0,0,1,2,2,1,0,3,0,1,2,0,4,2,1,3,2,0,2,1,5,4,0,2,1,2,1,0,2,3,3,1,0,2,1,0,0,2,3,2,0,3,2,2,5,5,1,4,4,3,4,5,3,4,3,8,6,8,5,3,2,6,3,5,5,7,3,6,4,2,6,3,6,2,2,4,3,4,3,7,7,7,8,5,5,6,8,10,6,5,6,2,5,5,7,5,6,2,8,14,7,9,3,8,2,4,8,5,5,6,2,5,8,4,8,3,6,1,7,2,3,1,1,3,0,2,2,5,7,4,4,6,7,2,3,3,4,0,3,2,3,0,3,6,2,1,0,2,3,1,1,2,1,3,3,3,2,1,1,4,2,1,1,1,3,2,1,1,1,2,1,0,1,0,1,2,4,1,0,0,2,1,3,6,0,2,2,2,1,1,2,1,0,3,2,2,2,2,4,1,1,2,3,2,2,1,0,1,0,1,0,2,1,4,2,0,1,1,1,3,0,3,1,3,1,0,1,1,2,3,1,1,3,0,3,3,1,4,0,1,1,3,2,2,1,0,2,0,0,0,1,0,1,0,2,2,0,1,1,0,4,3,1,0,1,2,2,2,2,2,1,0,2,2,0,1,2,2,1,0,1,2,0,1,1,3,2,2,3,1,1,3,0,1,0,0,1,2,1,0,1,1,2,0,2,1,0,0,2,2,1,2,0,4,4,1,2,1,2,1,0,0,2,0,1,0,1,0,0,1,0,1,0,1,0,0,4,1,0,4,0,3,1,1,1,2,1,0,1,1,2,0,5,3,4,0,0,1,1,2,1,2,1,2,2,0,2,1,0,0,1,0,1,0,0,1,0,1,1,2,0,2,2,2,1,1,1,0,2,2,1,2,1,0,1,2,1,2,1,2,1,2,0,1,0,1,0,1,1,0,1,0,1,4,3,1,1,0,4,1,0,1,1,1,3,0,2,2,0,1,2,3,0,0,1,2,0,2,0,0,1,2,1,3,0,0,4,3,0,1,1,1,0,2,0,1,1,2,1,1,1,0,0,2,3,1,1,1,1,3,1,0,1,2,3,0,0,1,0,1,2,2,3,3,2,1,2,1,1,2,0,0,1,0,2,0,0,0,1,0,1,1,1,1,3,0,3,0,1,0,2,0,1,1,0,3,1,0,0,0,1,2,1,4,1,2,1,1,0 - - - 16 - K-Serie - 4.176372041E-1 - 2.637948976E-1 - 1.898079836E4 - 1.1E2 - 3.743534268E-2 - - - 26 - K-Serie - 2.787831594E-1 - 3.066823675E-1 - 4.510390669E3 - 2.5E1 - 3.490403022E-2 - - - 29 - K-Serie - 3.035796365E-1 - 3.799997213E-1 - 2.290628769E3 - 1.4E1 - 4.096377034E-2 - - - 26 - Fe - Fe-Ka - KA - 6.39862973 - 1.964821903E-1 - 4263 - 4252 - - - 16 - S - S-Ka - KA - 2.3067 - 1.262233472E-1 - 18642 - 18457 - - - 29 - Cu - Cu-Ka - KA - 8.036489146 - 2.183585218E-1 - 2208 - 2207 - - 0 - - - - - 26 - 16744192 - 1 - - - 16 - 16744192 - 1 - - - 29 - 65535 - 1 - - - - - - - - Analysis made on Cameca SXFiveFE with Bruker Nano detector - - - - 2 - 1 - 1 - 1 - - - 0 - 0, - 0,Name - - - 0 - 0, - 0,Value - - - - - - - 2.4825E-2 - 9.962325 - 0 - 5.782313565E1 - 0 - 5.782313565E1 - 0 - 0 - 0 - 1 - Short - Lines - 55,54,50 - 0 - 5 - 5 - 0 - - Calibri - 11 - 150,150,150 - - - Calibri - 16 - 150,150,150 - - 0 - - - bruker_nano.spx - bruker_nano.spx - 0 - 1 - 195,0,0 - 195,0,0 - 1 - Solid - 1 - 0 - 1 - 0 - - - - 1 - Hertz - 0 - None - 1 - 1 - 0 - 1 - 1 - 1 - 0 - 0 - 0 - 1 - 6 - 1 - - Verdana - 16 - 8,0,0 - - - - - diff --git a/hyperspy/tests/io/bruker_data/extracted_from_bcf.spx b/hyperspy/tests/io/bruker_data/extracted_from_bcf.spx deleted file mode 100755 index 9b3a6f8150..0000000000 --- a/hyperspy/tests/io/bruker_data/extracted_from_bcf.spx +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - - RTHardware - 137 - - 40 - 25 - 3.8E1 - 95 - 33332 - 143400 - 2E4 - 60000 - - - - RTDetector - 5 - 9932 - - SDDpr - 8279_80 - XFlash 6|30 - 0.45 - 0.029 - eJyzcUkt8UmsTC0qtrMB0wYKjiX5ubZKhsZKCiEZmcnZeanFxbZKpq66xkr6UDWGUDXmKEos9ICKjOCKjKCKTFEUmSGbYwxVYoZbiQlUiQWqErhV+gj3AwCpRT07 - slew AP3.3 - - - - - - - - - - - - Internal - 1 - - - Internal - 8E-2 - 1E1 - 1 - 1 - - - Internal - 8E-2 - 5.55E-1 - 1 - - - - 3 - 21 - 5 - -3 - 0 - 1 - False - 1 - 1 - 5.115E-1 - -9.315E-1 - 0.07,0.0058,0.183,0.0078,0.277,0.0058,0.555,0,1.1,0,3.293,0.0064,5.89,0,0,0,0,0, - 0,0.01,0.000801,0.01,0.00298,0.01,0.008902,0.01,0.025,0.010046,0.041098,0.013475,0.04702,0.017302,0.049199,0.019237,0.05,0.02, - 0,0.026417,0.003686,0.026417,0.013709,0.026417,0.04095,0.026417,0.115,0.026585,0.18905,0.039072,0.216292,0.053009,0.226314,0.060056,0.23,0.062835, - 0,0.03,0.005362,0.03,0.019937,0.03,0.059556,0.03,0.16725,0.030229,0.274944,0.047376,0.314563,0.066511,0.329139,0.076186,0.3345,0.08, - 0,0.015,0.007349,0.015,0.027328,0.015,0.081633,0.015,0.22925,0.015114,0.376867,0.023706,0.431172,0.033293,0.451151,0.038139,0.4585,0.04005, - 0,0,0.009626,0,0.035791,0,0.106915,0,0.30025,0,0.493585,0.000035,0.564709,0.000073,0.590874,0.000092,0.6005,0.0001, - 0,0,0.013761,0,0.051168,0,0.15285,0,0.42925,0,0.70565,0.000035,0.807332,0.000073,0.844738,0.000092,0.8585,0.0001, - 0,0,0.018394,0,0.068393,0,0.204305,0,0.57375,0,0.943195,0.000035,1.079108,0.000073,1.129106,0.000092,1.1475,0.0001, - 0,0,0.021968,0,0.081684,0,0.244008,0,0.68525,0,1.126492,0.000035,1.288816,0.000073,1.348531,0.000092,1.3705,0.0001, - 0,0,0.025864,0,0.096167,0,0.287273,0,0.80675,0,1.326227,0.000035,1.517333,0.000073,1.587636,0.000092,1.6135,0.0001, - 0,0,0.029334,0,0.109071,0,0.325818,0,0.915,0,1.504182,0.000035,1.720929,0.000073,1.800667,0.000092,1.83,0.0001, - 0,0,0.03153,0,0.117236,0,0.35021,0,0.9835,0,1.61679,0.000035,1.849764,0.000073,1.935471,0.000092,1.967,0.0001, - 0,0,0.03464,0,0.128798,0,0.38475,0,1.0805,0,1.776249,0.000035,2.032202,0.000073,2.12636,0.000092,2.161,0.0001, - 0,0,0.048088,0,0.178804,0,0.534129,0,1.5,0,2.465871,0.000035,2.821196,0.000073,2.951912,0.000092,3,0.0001, - 0,0,0.069279,0,0.257598,0,0.769501,0,2.161,0,3.552499,0.000035,4.064403,0.000073,4.252721,0.000092,4.322,0.0001, - 0,0,0.099623,0,0.370422,0,1.106536,0,3.1075,0,5.108463,0.000035,5.844577,0.000073,6.115378,0.000092,6.215,0.0001, - 0,0,0.134085,0,0.498566,0,1.489329,0,4.1825,0,6.875671,0.000035,7.866434,0.000073,8.230915,0.000092,8.365,0.0001, - 0,0,0.162313,0,0.603525,0,1.802863,0,5.063,0,8.323137,0.000035,9.522476,0.000073,9.963688,0.000092,10.125999,0.0001, - 0,0,0.192351,0,0.715217,0,2.136515,0,6,0,9.863485,0.000035,11.284782,0.000073,11.807649,0.000092,12,0.0001, - 0,0,0.24044,0,0.894022,0,2.670643,0,7.5,0,12.329357,0,14.105978,0,14.759561,0,15,0, - 0,0,0.320586,0,1.192029,0,3.560857,0,10,0,16.439142,0,18.80797,0,19.679415,0,20,0, - 0,0,1.60293,0,5.960146,0,17.804287,0,50,0,82.195709,0,94.039856,0,98.397072,0,100,0, - - - - RTESMA - 662 - - 1.5E1 - -1 - -1 - 2.000100008E-3 - 3.5E1 - 4.5E1 - 2 - - - - - 82 - 4.3.2018 - - 1548 - -4.7225E-1 - 1E-2 - 3.524514617E-4 - 4.311038316E-4 - - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,8,25,68,118,163,178,129,87,31,13,2,1,0,0,0,0,0,1,1,0,0,0,1,1,2,0,4,2,1,7,8,14,17,8,7,4,8,0,2,0,0,4,5,3,0,3,1,4,3,4,3,7,6,8,10,14,4,10,11,11,11,16,16,15,11,19,21,10,20,19,19,24,20,31,48,45,38,44,41,21,17,12,5,5,3,3,2,13,7,13,10,10,14,8,5,8,1,3,4,5,5,3,6,2,4,4,1,1,0,2,2,0,2,1,2,2,2,1,1,2,4,5,3,3,0,1,2,4,4,3,2,2,6,1,3,4,2,8,1,2,1,3,6,1,7,4,4,5,6,2,3,4,3,7,3,7,6,5,4,7,3,2,5,4,5,5,3,1,4,3,1,6,2,2,2,7,1,3,4,3,7,7,6,9,3,6,1,1,3,6,8,5,5,5,3,3,5,4,5,3,3,6,3,4,5,4,3,4,1,2,1,7,5,4,4,2,3,4,3,3,2,2,6,4,5,2,3,4,1,3,3,4,3,5,5,2,6,12,3,4,3,3,5,3,4,4,7,4,0,1,5,3,3,6,3,2,3,4,3,2,4,2,2,1,1,1,5,5,4,4,5,2,9,3,5,6,3,2,3,5,4,3,5,3,4,2,1,3,6,4,3,4,4,3,1,5,1,2,4,6,4,5,3,2,3,1,7,5,7,1,3,3,6,4,2,2,2,6,2,3,3,6,0,4,2,5,8,5,2,5,2,4,3,2,2,1,2,3,2,2,0,4,0,4,1,0,3,1,1,3,1,3,2,1,3,3,3,2,2,4,3,1,1,2,5,1,3,1,1,2,0,3,3,1,0,4,2,2,2,2,2,2,3,1,5,3,2,1,5,3,1,2,1,1,2,2,2,1,6,1,2,0,3,1,1,2,2,2,2,1,2,2,1,5,3,3,2,2,2,1,1,3,0,1,0,1,2,1,6,1,3,2,0,0,2,4,7,3,2,2,1,3,1,2,2,2,0,1,3,1,1,2,0,3,2,1,2,1,2,4,1,3,1,0,2,3,0,0,2,1,2,0,2,4,1,1,1,0,2,1,2,2,0,3,1,1,1,0,0,2,1,0,3,5,1,2,3,1,2,2,1,1,1,0,1,2,2,2,1,1,3,1,0,0,0,2,2,0,0,1,1,1,1,2,0,0,0,4,0,2,2,3,5,2,3,5,6,5,8,10,8,24,16,22,24,19,17,13,22,26,13,17,11,9,6,5,6,2,1,5,2,1,1,1,0,1,2,2,0,0,1,2,0,3,2,3,3,1,1,1,1,2,2,1,2,0,0,2,4,4,5,2,1,6,6,3,3,3,5,8,5,4,4,6,2,2,1,4,3,2,1,0,0,0,1,3,3,1,0,1,2,1,1,2,0,1,1,0,0,3,1,4,2,10,6,7,14,16,11,30,38,38,43,44,42,46,63,39,44,32,21,18,17,17,11,12,15,7,4,5,3,3,0,1,1,1,1,1,5,3,1,0,1,2,2,0,2,0,0,1,2,1,1,0,1,1,1,0,1,0,0,2,1,2,0,3,1,1,1,2,1,5,3,3,4,6,6,5,9,6,4,3,6,1,5,5,4,4,1,1,5,3,2,1,2,2,0,2,0,1,0,1,0,1,1,0,0,2,0,2,1,1,3,0,3,5,3,2,7,1,7,6,4,4,4,4,4,3,3,0,1,4,0,2,1,3,0,0,0,0,3,1,2,1,1,0,3,0,0,1,0,2,1,1,0,0,1,0,0,0,0,0,1,1,0,0,2,1,1,1,1,1,2,1,0,0,1,1,1,0,3,0,1,0,0,0,1,1,0,0,1,2,0,2,0,0,0,0,3,0,2,0,1,3,1,1,0,0,1,0,0,1,1,0,0,0,1,3,1,1,0,0,0,1,2,0,0,0,1,1,1,0,2,0,0,2,0,0,0,0,1,2,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1,0,0,0,0,1,0,1,0,1,0,0,0,0,2,1,2,0,0,0,1,0,1,0,1,1,2,2,1,0,1,0,0,0,1,0,1,0,1,0,0,0,2,0,0,1,1,1,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,2,2,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,2,1,0,1,1,1,0,2,2,2,0,0,0,0,0,0,0,0,1,2,1,1,0,2,1,1,1,0,0,1,0,1,0,1,1,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,1,0,0,1,1,0,0,1,2,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,1,0,1,1,0,0,1,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,2,1,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 - - - - -0.48665783 - 0.81256217 - 0 - 39.2928022966004 - 0 - 0 - 0 - 0 - Short - Lines - 223,223,223 - 5 - 5 - 0 - - Verdana - 11 - 8,0,0 -