diff --git a/.gitignore b/.gitignore index eeadad245..3da2895e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Byte-compiled / optimized / DLL files +data __pycache__/ *.py[cod] *$py.class @@ -41,6 +42,10 @@ pip-delete-this-directory.txt docs/api docs/_build +# Results and plots +*.fits.gz +*.png + # Unit test / coverage reports htmlcov/ .tox/ diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..91626e0fb --- /dev/null +++ b/.mailmap @@ -0,0 +1,4 @@ +Michele Peresano Michele Peresano + +Thomas Vuillaume vuillaut +Thomas Vuillaume Thomas Vuillaume diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..87b6bf2c3 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,11 @@ +version: 2 + +python: + version: 3.7 + install: + - method: pip + path: . + extra_requirements: + - docs + - tests + system_packages: false diff --git a/.travis.yml b/.travis.yml index e3a951283..bbd7e43ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: generic env: - global: - - PYTHONIOENCODING=UTF8 - - MPLBACKEND=Agg + global: + - PYTHONIOENCODING=UTF8 + - MPLBACKEND=Agg matrix: include: @@ -20,28 +20,35 @@ matrix: - CONDA=true before_install: - - - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - - bash miniconda.sh -b -p $HOME/miniconda - - . $HOME/miniconda/etc/profile.d/conda.sh - - hash -r - - conda config --set always_yes yes --set changeps1 no - - conda update -q conda # get latest conda version - - conda info -a # Useful for debugging any issues with conda + - travis_wait 15 ./download_test_data.sh + - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; + - bash miniconda.sh -b -p $HOME/miniconda + - . $HOME/miniconda/etc/profile.d/conda.sh + - hash -r + - conda config --set always_yes yes --set changeps1 no + - conda update -q conda # get latest conda version + - conda info -a # Useful for debugging any issues with conda install: - - conda env create -f environment.yml - - conda activate pyirf - - pip install travis-sphinx codecov pytest-cov - - python setup.py install + - sed -i -e "s/- python=.*/- python=$PYTHON_VERSION/g" environment.yml + - conda env create -f environment.yml + - conda activate pyirf-dev + # cython needed for building gammapy + - pip install travis-sphinx codecov pytest-cov Cython + - pip install .[all] + - python --version script: - - pytest --cov=pyirf + - pytest --cov=pyirf --cov-report=xml + - python examples/calculate_eventdisplay_irfs.py + - travis-sphinx -v --outdir=docbuild build --source=docs/ after_script: - - if [[ "$CONDA" == "true" ]];then - conda deactivate - fi + - if [[ "$CONDA" == "true" ]];then + conda deactivate + fi + - python setup.py build_sphinx after_success: - - codecov + - codecov + - travis-sphinx -v --outdir=docbuild deploy diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 529e80ba6..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -include README.rst - -include pyirf/resources/*.yml - -recursive-include pyirf/resources -recursive-include pyirf/scripts/*.py - -global-exclude *.pyc diff --git a/README.rst b/README.rst index 2080443f7..d2e15a73f 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,15 @@ -================================================== -pyirf |travis| |codacy| |coverage| |documentation| -================================================== +================================== +pyirf |travis| |codacy| |coverage| +================================== .. |travis| image:: https://travis-ci.com/cta-observatory/pyirf.svg?branch=master :target: https://travis-ci.com/cta-observatory/pyirf -.. |codacy| image:: https://app.codacy.com/project/badge/Grade/669fef80d3d54070960e66351477e383 - :target: https://www.codacy.com/gh/cta-observatory/pyirf?utm_source=github.com&utm_medium=referral&utm_content=cta-observatory/pyirf&utm_campaign=Badge_Grade +.. |codacy| image:: https://app.codacy.com/project/badge/Grade/669fef80d3d54070960e66351477e383 + :target: https://www.codacy.com/gh/cta-observatory/pyirf/dashboard?utm_source=github.com&utm_medium=referral&utm_content=cta-observatory/pyirf&utm_campaign=Badge_Grade .. |coverage| image:: https://codecov.io/gh/cta-observatory/pyirf/branch/master/graph/badge.svg :target: https://codecov.io/gh/cta-observatory/pyirf -.. |documentation| image:: https://readthedocs.org/projects/pyirf/badge/?version=latest - :target: https://pyirf.readthedocs.io/en/latest/?badge=latest -Python IRF builder +Python library to calculate IACT IRFs and Sensitivities. -=== Under construction ==== - -The current version works only for point sources. - -=== Documentation ==== - -https://pyirf.readthedocs.io/ +**Documentation:** https://cta-observatory.github.io/pyirf/ diff --git a/docs/AUTHORS.rst b/docs/AUTHORS.rst index 67cbfa798..39c519851 100644 --- a/docs/AUTHORS.rst +++ b/docs/AUTHORS.rst @@ -3,14 +3,16 @@ Authors ======= -The following is a complete list of all contributors in alphabetical order by last name: +To see who contributed to ``pyirf``, please visit the +`GitHub contributors page `__ +or run -- Lea Jouvin -- Julien Lefacheur (left) -- Maximilian Nöthe -- Michele Peresano -- Thomas Vuillaume +.. code-block:: bash -*pyirf* has been developed starting from part a previous project authored by Julien Lefacheur. + git shortlog -sne -For more details go to the `GitHub contributors page `__. + +``pyirf`` started as part of `protopipe `__ by Julien Lefaucher, +but was largely rewritten in September 2020, making use of code from the +previous version, the `pyfact `__ module and the +`FACT irf `__ package. diff --git a/docs/benchmarks/index.rst b/docs/benchmarks/index.rst new file mode 100644 index 000000000..53badad58 --- /dev/null +++ b/docs/benchmarks/index.rst @@ -0,0 +1,11 @@ +.. _benchmarks: + +Benchmarks +========== + +Functions to calculate benchmarks. + +------------- + +.. automodapi:: pyirf.benchmarks + :no-inheritance-diagram: diff --git a/docs/binning.rst b/docs/binning.rst new file mode 100644 index 000000000..f91b493e8 --- /dev/null +++ b/docs/binning.rst @@ -0,0 +1,11 @@ +.. _binning: + +Binning and Histogram Utilities +=============================== + + +Reference/API +------------- + +.. automodapi:: pyirf.binning + :no-inheritance-diagram: diff --git a/docs/changelog.rst b/docs/changelog.rst index ebd8f0f16..73b23b375 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,11 +3,7 @@ Changelog ========= -This is the changelog for *pyirf*. - -We use a one-line description of every pull request, roughly in chronological order. -We don't list every pull request. -Maintenance and cleanup changes are not of interest to users and are not listed here. +We use a one-line description of every pull request. .. RELEASE TEMPLATE .. @@ -37,22 +33,24 @@ Maintenance and cleanup changes are not of interest to users and are not listed .. _pyirf_0p3_release: -0.1.0 (Unreleased) ------------------- +`0.1.0 `__ (2020-09-16) +------------------------------------------------------------------------------------- + +This is a pre-release. + +- Released September 16th, 2020 -. . . .. _pyirf_0p1p0alpha_prerelease: -`0.1.0-alpha `__ (May 27th, 2020) ------------------------------------------------------------------------------------------------------ +`0.1.0-alpha `__ (2020-05-27) +------------------------------------------------------------------------------------------------- Summary +++++++ This is a pre-release. -- This is a pre-release - Released May 27th, 2020 - 3 contributors diff --git a/docs/conf.py b/docs/conf.py index f7dbbe2aa..5f4904e53 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,8 +19,8 @@ # -- Project information ----------------------------------------------------- project = "pyirf" -copyright = "2020, Julien Lefaucheur, Michele Peresano, Thomas Vuillaume" -author = "Julien Lefaucheur, Michele Peresano, Thomas Vuillaume" +copyright = "2020, Maximilian Nöthe, Michele Peresano, Thomas Vuillaume" +author = "Maximilian Nöthe, Michele Peresano, Thomas Vuillaume" # The full version, including alpha/beta/rc tags version = __version__ @@ -40,10 +40,9 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "rinoh.frontend.sphinx", - "sphinx_automodapi.automodapi", - "sphinx.ext.autodoc", "numpydoc", + "nbsphinx", + "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", @@ -55,12 +54,14 @@ "sphinx.ext.napoleon", "sphinx_automodapi.automodapi", "sphinx_automodapi.smart_resolver", - "nbsphinx", - "IPython.sphinxext.ipython_console_highlighting", ] # nbsphinx -nbsphinx_execute = "never" +# nbsphinx_execute = "never" +nbsphinx_execute_arguments = [ + "--InlineBackend.figure_formats={'svg', }", + "--InlineBackend.rc={'figure.dpi': 96}", +] numpydoc_show_class_members = False autosummary_generate = True @@ -79,17 +80,11 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "alabaster" +html_theme = "sphinx_rtd_theme" html_theme_options = { - "github_user": "cta-observatory", - "github_repo": "pyirf", - "badge_branch": "master", - "codecov_button": "true", - "github_button": "true", - "travis_button": "true", - "sidebar_collapse": "false", - "sidebar_includehidden": "true", + 'canonical_url': 'https://cta-observatory.github.io/pyirf', + 'display_version': True, } # Add any paths that contain custom static files (such as style sheets) here, diff --git a/docs/contribute.rst b/docs/contribute.rst new file mode 100644 index 000000000..fd10dc3d0 --- /dev/null +++ b/docs/contribute.rst @@ -0,0 +1,68 @@ +.. _contribute: + +How to contribute +================= + + +Issue Tracker +------------- + +We use the `GitHub issue tracker `__ +for individual issues and the `GitHub Projects page `_ can give you a quick overview. + +If you found a bug or you are missing a feature, please check the existing +issues and then open a new one or contribute to the existing issue. + +Development procedure +--------------------- + + +We use the standard `GitHub workflow `__. + +If you are not part of the ``cta-observatory`` organization, +you need to fork the repository to contribute. +See the `GitHub tutorial on forks `__ if you are unsure how to do this. + +#. When you find something that is wrong or missing + + - Go to the issue tracker and check if an issue already exists for your bug or feature + - In general it is always better to anticipate a PR with a new issue and link the two + +#. To work on a bug fix or new feature, create a new branch, add commits and open your pull request + + - If you think your pull request is good to go and ready to be reviewed, + you can directly open it as normal pull request. + + - You can also open it as a “Draft Pull Request”, if you are not yet finished + but want to get early feedback on your ideas. + + - Especially when working on a bug, it makes sense to first add a new + test that fails due to the bug and in a later commit add the fix showing + that the test is then passing. + This helps understanding the bug and will prevent it from reappearing later. + +#. Wait for review comments and then implement or discuss requested changes. + + +We use `Travis CI `__ to +run the unit tests and documentation building automatically for every pull request. +Passing unit tests and coverage of the changed code are required for all pull requests. + +Further details +--------------- + +Please also have a look at the + +- ``ctapipe`` `development guidelines `__ +- The `Open Gamma-Ray Astronomy data formats `__ + which also describe the IRF formats and their definitions. +- ``ctools`` `documentation page on IRFs `__ +- `CTA IRF working group wiki (internal) `__ + +- `CTA IRF Description Document for Prod3b (internal) `__ + + +Benchmarks +---------- + +- `Comparison with EventDisplay <../notebooks/comparison_with_EventDisplay.ipynb>`__ | *comparison_with_EventDisplay.ipynb* diff --git a/docs/contribute/comparison_with_EventDisplay.ipynb b/docs/contribute/comparison_with_EventDisplay.ipynb deleted file mode 100644 index 7ec9b696b..000000000 --- a/docs/contribute/comparison_with_EventDisplay.ipynb +++ /dev/null @@ -1,739 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Remove input cells at runtime (nbsphinx)\n", - "import IPython.core.display as d\n", - "d.display_html('', raw=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Comparison with EventDisplay" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Purpose of this notebook:**\n", - "\n", - "- Read DL2 files from _EventDisplay_ in FITS format\n", - "\n", - "- Read _pyirf_ output\n", - "\n", - "- Compare the outputs\n", - "\n", - "**Notes:**\n", - "\n", - "The following results correspond to:\n", - "\n", - "- Paranal site\n", - "- Zd 20 deg, Az 180 deg\n", - "- 50 h observation time\n", - "\n", - "**Resources:**\n", - "\n", - "_EventDisplay_ DL2 data, https://forge.in2p3.fr/projects/cta_analysis-and-simulations/wiki/Eventdisplay_Prod3b_DL2_Lists\n", - "\n", - "**TO-DOs:**\n", - "\n", - "- ..." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Table of contents\n", - "\n", - "* [IRFs](#IRFs)\n", - " - [Effective area](#Effective-area)\n", - " - [Angular resolution](#Angular-resolution)\n", - " - [Energy resolution](#Energy-resolution)\n", - " - [Background rate](#Background-rate)\n", - "* [Differential sensitivity](#Differential-sensitivity)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Imports" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import uproot\n", - "from astropy.io import fits\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import astropy.units as u\n", - "from astropy.coordinates import Angle\n", - "from gammapy.maps import MapAxis\n", - "from gammapy.irf import EffectiveAreaTable2D, EnergyDispersion2D, BgRateTable" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Definitions of classes and functions" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "If judged useful, these should be moved to pyirf!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Input data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "### _EventDisplay_" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "The input data provided by _EventDisplay_ is stored in _ROOT_ format, so _uproot_ is used to transform it into _numpy_ objects. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "tags": [ - "parameters" - ] - }, - "outputs": [], - "source": [ - "# Path of EventDisplay IRF data in the user's local setup\n", - "# Please, empty the indir_EventDisplay variable before pushing to the repo\n", - "indir_EventDisplay = \"\"\n", - "infile_EventDisplay = \"DESY.d20180113.V3.ID0_180degNIM2LST4MST4SST4SCMST4.prod3b-paranal20degs05b-NN.S.3HB9-FD.180000s.root\"\n", - "\n", - "input_EventDisplay = uproot.open(f'{indir_EventDisplay}/{infile_EventDisplay}')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Contents of the ROOT file\n", - "# input_EventDisplay.keys()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "### Setup of output data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## _pyirf_" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following is the current IRF + sensititivy output FITS format provided by this software." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "tags": [ - "parameters" - ] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Filename: /Users/michele/Applications/ctasoft/tests/pyirf/EventDisplay/from_pyirf/irf_EventDisplay_Time50h//irf.fits.gz\n", - "No. Name Ver Type Cards Dimensions Format\n", - " 0 PRIMARY 1 PrimaryHDU 5 (100,) float64 \n", - " 1 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 2 POINT SPREAD FUNCTION 1 BinTableHDU 18 21R x 3C [E, E, E] \n", - " 3 ENERGY DISPERSION 1 BinTableHDU 37 1R x 7C [60D, 60D, 300D, 300D, 2D, 2D, 36000D] \n", - " 4 BACKGROUND 1 BinTableHDU 18 21R x 3C [E, E, E] \n", - " 5 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 6 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 7 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 8 SENSITIVITY 1 BinTableHDU 22 21R x 5C [E, E, E, E, E] \n" - ] - } - ], - "source": [ - "# Path of the pyirf output data in the user's local setup\n", - "# Please, empty the indir_pyirf variable before pushing to the repo\n", - "indir_pyirf = \"\"\n", - "infile_pyirf = \"irf.fits.gz\"\n", - "\n", - "hdul_pyirf = fits.open(f'{indir_pyirf}/{infile_pyirf}') # will be closed at the end of the notebook\n", - "\n", - "# Contents of the FITS file\n", - "hdul_pyirf.info()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "### Setup of output data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "It is possible to extract data from pyirf FITS file with different approaches, e.g.\n", - "\n", - "- using *gammapy*, by reading the file HDUs into the appropriate IRF class like `EffectiveAreaTable2D` or `EnergyDispersion2D`\n", - "- opening the FITS file manually and reading data through the *astropy.fits* module as e.g. `BinTableHDU`\n", - "\n", - "The *gammapy* solution seems to be cleaner, but it means that we depend on this specific science tool for plotting. This is not bad per-se, but we could need a more elastic approach for now, given that e.g. we do not yet work on full-enclosure IRFs and the offset handling in *gammapy* is hard-coded in its plotting methods.\n", - "\n", - "To produce the following plots I use a mix of these two approaches as example, but it is possible to open and plot data by using consistently each of the two." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# Effective area\n", - "\n", - "# Approach using gammapy\n", - "\n", - "#aeff2D = EffectiveAreaTable2D.read(f'{indir_pyirf}/{infile_pyirf}', hdu=1)\n", - "#print(aeff2D)\n", - "#aeff=aeff2D.to_effective_area_table(offset=Angle('1d'), energy=energy * u.TeV)\n", - "#aeff.plot()\n", - "#plt.grid(which=\"both\")\n", - "#plt.yscale(\"log\")\n", - "\n", - "# Manual approach\n", - "aeff_pyirf = hdul_pyirf[1]\n", - "\n", - "aeff_pyirf_ENERG_LO = aeff_pyirf.data[0][0]\n", - "aeff_pyirf_EFFAREA = aeff_pyirf.data[0][4][1]*1.e4 # there seems to be a 10**4 missing...maybe a bug in pyirf?" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "# Angular resolution\n", - "\n", - "# At the moment the format provided by pyirf is not compatible with GADF\n", - "# https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/psf/index.html\n", - "\n", - "psf_pyirf = hdul_pyirf[2]" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/michele/Applications/anaconda3/envs/pyirf/lib/python3.7/site-packages/astropy/units/quantity.py:464: RuntimeWarning: invalid value encountered in true_divide\n", - " result = super().__array_ufunc__(function, method, *arrays, **kwargs)\n" - ] - } - ], - "source": [ - "# Energy dispersion\n", - "\n", - "# here I open manually, but I use gammapy.irf.EnergyDispersion2D to get the energy resolution\n", - "\n", - "eDisp_pyirf = hdul_pyirf[3]\n", - "\n", - "eDisp_pyirf_ENERG_LO = eDisp_pyirf.data[0][0]\n", - "eDisp_pyirf_ENERG_HI = eDisp_pyirf.data[0][1]\n", - "\n", - "edisp2d_pyirf = EnergyDispersion2D.read(f'{indir_pyirf}/{infile_pyirf}', hdu=\"ENERGY DISPERSION\")\n", - "edisp_pyirf = edisp2d_pyirf.to_energy_dispersion(offset=Angle('1d'), e_reco=eDisp_pyirf_ENERG_LO * u.TeV, e_true=eDisp_pyirf_ENERG_LO * u.TeV)\n", - "\n", - "edisp_true_pyirf = np.asarray([(eDisp_pyirf_ENERG_HI[i]-eDisp_pyirf_ENERG_LO[i])/2. for i in range(len(eDisp_pyirf_ENERG_LO))])\n", - "\n", - "resolution_pyirf = []\n", - "for e_true in edisp_true_pyirf:\n", - " resolution_pyirf.append(edisp_pyirf.get_resolution(e_true * u.TeV))\n", - "resolution_pyirf = np.asarray(resolution_pyirf)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# Background rate\n", - "\n", - "background_pyirf = hdul_pyirf[4]" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "# Differential sensitivity\n", - "\n", - "sensitivity_pyirf = hdul_pyirf[8]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Comparison" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For the moment we do not require to replicate perfectly the EventDisplay output, because this depends also on:\n", - "\n", - "- the configuration in config.yaml,\n", - "- the specific cuts optiization performed by EventDisplay (which has not yet been replicated in pyirf)\n", - "\n", - "This comparison is here to make sure that we can produce a reliable and stable output and use it to proceed with the development.\n", - "\n", - "A more detailed and complete version of this notebook will be provided with an official DL3 benchmarking." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### IRFs\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Effective area\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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LiIikgIK6CIiIimggC4iIpICCugiIiIpoIAuIiKSAgroIiIiKaCALiIikgJaKa5MWiku2asppWGlq7A8qp90tJ2gdK0Ul5z6SVrbKbZSXEMG9JyxY8f62rVr63LvtrY2Wltb63atqOeE5St2vNCxoPSoabVSrXvXs27C8qh+0tF2gtLrUTdbt25l3bp1bN68mc2bNzNkyJCKr1nOdaKeE5av2PFCx4LSo6ZV25AhQxg5ciSDBw/ulW5mBQO6tk8VERHWrVvHTjvtxKhRo9i0aRM77bRTxdd84403Sr5O1HPC8hU7XuhYUHrUtGpydzZs2MC6desYPXp05PPUhy4iImzevJlhw4ZhZvUuyoBnZgwbNozNmzeXdJ4CuoiIACiYJ0g5daGALiIiDem4446js7Mz8Ng999zD/vvvz5QpU2pcqvpRH7qIiDSk+++/v19abivR73//+3z3u98dUAFdT+giIpIIzz//PIceeiif+MQnOOiggzj99NO57777+NjHPtaT5+c//zmnnnoqAKNGjeKVV17h+eefZ//99+fTn/40hxxyCNdccw2PPvooF198MTNnzqzXr1NzCugiIpIYzzzzDBdeeCGrVq1i55135qmnnuJ3v/sdf/3rXwG45ZZbOO+88/qdt3btWv7pn/6J9vZ2rrrqKiZMmMBtt93G9ddfX+tfoW70yl1ERHrZ4aGrYEPla3w0d78NTdkw8+5xcOyc0HNGjhzJ4YcfDsA555zDN7/5Tc4991z+93//l/POO49ly5bx/e9/n66url7n7bPPPnzoQx+quMyNTAFdREQSo+/objPjvPPO48QTT2TIkCGcccYZbLdd/9C144471qqIiaWALiIivfxtylfYvgoLp3SVsQDLiy++yLJly5g0aRJ33HEHRxxxBHvuuSd77rkn1157LT//+c8rLldaqQ9dREQSY+zYsdx6660cdNBBvPrqq1xyySUAnH322ey1114ccMABdS5hcukJXUREEmPQoEHMnTu3X/rDDz/MBRdc0Cvt+eefB2C33XZjzZo1vY7df//9sS7PmkQNuTmLdlvTjkRh0rBbVFge1U862k5Qej3qZpddduF973sfAN3d3TQ1NVV8zVKv86c//YkzzjiDxx9/vFf6hz/8Yd71rnfxox/9iB122CHStYsdL3QsKD1qWhyeffZZNm7c2Cut2G5rPZPwG/EzZswYr5eHHnqorteKek5YvmLHCx0LSo+aVivVunc96yYsj+onHW0nKL0edfPUU0/1/Pz6669X5ZrlXCfqOWH5ih0vdCwoPWpaHPLrJAd4wgvERPWhi4iIpIACuoiISAoooIuIiKSAArqIiEgKKKCLiEgiNDU1cfjhhzN+/HjGjx/PnDnhS8WWoq2tjaVLl/Z8v/rqqxkxYgTjx49n33335eyzz+app57qOX7++efz+9//vuT7LFiwgM9+9rNVKXMpNA9dREQSobm5mUceeSS2+eNtbW0MHTqUcePG9aR94Qtf4F//9V+BTCD+yEc+wurVq9l999256aabeOONN2IpSxz0hC4iIiVb1N7B4XOWMPry+zh8zhIWtXfEcp/Fixczbdq0nu9tbW2ceOKJAPzsZz9j0qRJHHLIIZxxxhls2rQJyGyret1113HIIYcwbtw4fv/73/P8888zd+5cvvGNb3D44Yfz61//ut+9TjvtND760Y+SW9+ktbWVFStW0N3dzYwZMzjwwAMZN24c3/72t3uOf/7zn2fy5MkceOCB/ebPA/zkJz9h4sSJTJgwgaOOOoqXX36Zbdu2se+++/bsILdt2zbe97738corr1T0v5UCuoiIlGRRewez7l5NR2cXDnR0djHr7tUVB/Wurq5er9wXLlzI0UcfzaOPPsqbb74JwMKFC5k+fTobNmzg2muv5Re/+AUrVqzgsMMO44Ybbui51rBhw1ixYgWXXHIJX//61xk1ahQXX3wxX/jCF3jkkUc48sgjA8twyCGH9HvNvnLlSjo6OlizZg2rV6/mnHPO6Tn25ptvsnTpUr773e/yyU9+st/1jjjiCB599FHa29s588wz+drXvsagQYM455xzuO222wD4xS9+wcEHH8xuu+1W0f9+euUuIiKRTJ+3DID2FzrZ0r2t17Gurd186a5V3PH4Cyy8aFJZ1y/0yv2YY47hJz/5Caeffjr33XcfX/va11i8eDFPPfVUz1arW7ZsYdKkd+570kknAXDooYdy9913Ry6DB6ye+p73vIc//vGPXHrppRx//PG97nPWWWcBmdXsXn/9dTo7O3udu27dOqZPn85LL73Eli1bGD16NACf/OQnOfnkk/n85z/P/PnzA/d4L5We0EVEpCR9g3lYeqWmT5/OnXfeyZIlS/jABz7QE/CPPvpoVq5cycqVK3nqqae4+eabe87JLRHb1NTE22+/Hfle7e3t7L///r3Sdt11V5588klaW1v5zne+02vAW9B2r/kuvfRSPvvZz7J69WrmzZvH5s2bAdhrr73YY489WLJkCY899hjHHnts5DIWooAuIiKRLLxoEgsvmsSIlubA4yNamst+Oi8m15f93//930yfPh2AD3zgAzzyyCM8++yzALz11ls8/fTTRa+z0047FR3k9qMf/Yif/exnPU/dOa+88grbtm3jtNNO45prruHJJ5/sObZw4UIgs3nMLrvswi677NLr3I0bNzJixAgAbr311l7Hzj//fM455xymTZtWlbXhFdBFRKQkM6eOpXlw7wDUPLiJmVPHVnTdvn3ol19+OZB5yj7hhBNYvHgxJ5xwApDZYW3BggWcddZZHHTQQXzoQx8KnWJ24okncs899/QaFPeNb3yjZ9rawoULWbJkCbvvvnuv8zo6OmhtbWX8+PHMmDGDq666qufYrrvuyuTJk7n44ot7vSHIufrqqznjjDM48sgj+/WRn3TSSWzatKkqr9tBfegiIlKiUyZknjivf2At6zu72LOlmZlTx/akl6u7u5s33ngjcNrat7/97Z7R5Tkf+chHWL58eb+8zz//fM+T+GGHHUZbWxsAY8aMYdWqVT33OPLII7n66qt7zut777a2tp60FStW9MqXc9pppzF79uxe958xYwYzZswA4OSTT+bkk08O/H2ffPJJDj74YPbbb7/A46VSQBcRkZKdMmFExQF8IJszZw433nhjz0j3alBAFxERKUPuyb8cl19+eU+XQrWoD11ERCQFFNBFRAQInoMt9VFOXSigi4gIQ4YMYcOGDQrqCeDubNiwgSFDhpR0nvrQRUSEkSNHsm7dOv7617+yefPmkoNJkHKuE/WcsHzFjhc6FpQeNa3ahgwZwsiRI0s6JzEB3cz2Bz4H7AY86O431rlIIiIDxuDBg3uWJW1ra2PChAkVX7Oc60Q9JyxfseOFjgWlR01LglhfuZvZfDP7i5mt6ZN+jJmtNbNnzexyAHf/nbtfDEwDDouzXCIiImkTdx/6AuCY/AQzawK+AxwLHACcZWYHZI+dBDwMPBhzuURERFIl1oDu7r8CXu2T/EHgWXf/o7tvAX4AnJzN/2N3nwycHWe5RERE0qYefegjgBfzvq8DJppZK3AqsANwf6GTzexC4MLs17/1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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"EffectiveAreaEtrue\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.allbins[3:-1]])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.allbins[3:-1]])\n", - "y = h.allvalues[3:-1]\n", - "yerr = h.allvariances[3:-1]\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(1.e3, 1.e7)\n", - "plt.xscale(\"log\")\n", - "plt.yscale(\"log\")\n", - "plt.xlabel(\"True energy [TeV]\")\n", - "plt.ylabel(\"Effective collection area [cm^2]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=None, fmt=\"o\", label=\"EventDisplay\")\n", - "plt.loglog(aeff_pyirf_ENERG_LO, aeff_pyirf_EFFAREA, drawstyle='steps-post', label=\"pyirf\")\n", - "\n", - "plt.legend(loc=4)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Angular resolution\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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1FNPVXNzsw4++GDdddddFfU8Pit6AEAolje1aM6yZu1u3ytJaom3ac6yZi1vagn9sx555BFdeOGFne9jsZimTp0qSfr973+vCRMmaPz48fr617+u1tZWSdLIkSN12223afz48aqrq9PLL7+srVu36pe//KX+6Z/+SZMmTdIf//jHXp91wQUX6IwzzlBHfpbGxkatX79e7e3tuuyyy3T88cerrq5OP//5zzvrr7vuOk2cOFHHH3+81q5d26vPBx98UCeddJLq6+t12mmn6S9/+Yv27t2r0aNH6+2335Yk7d27V0cddZTeeeedPn1XBHoAQJ90XJO/YelGte1p71bXtqddNyzd2Kdr9m1tbd1O3S9ZskSnn366nn32We3cuVOStGTJEs2YMUPvvvuufvjDH+rxxx/X+vXr1dDQoNtvv72zr2HDhmn9+vW66qqr9JOf/EQjR47UlVdeqeuvv15PP/20vvzlL6ccw/jx43udrt+wYYNaWlq0adMmNTc369JLL+2s27lzp5555hndcccduvzyy3v1d/LJJ+vZZ59VU1OTLrroIv3oRz/SgAEDdOmll+q3v/2tJOnxxx/X2LFjddBBB+X93UmcugcAhKRjJR+0PKh0p+7PPPNMPfjgg5o+fbpWrFihH/3oR3rkkUe0efNmTZo0KfHZu3drwoQJnW3OPfdcSdKJJ56oZcuWBR6Dp8gi+7nPfU6vvfaarr32Wp1zzjndPufiiy+WlMjg9+GHHyoej3dru337ds2YMUN//vOftXv3bo0aNUqSdPnll+u8887TddddpwULFmjmzJmBx5gOK3oAQJ8smTVBS2ZNUM3QqpT1NUOrtGTWhJR1fTFjxgzde++9Wrlypb7whS90/kPg9NNP14YNG7RhwwZt3rxZd999d2ebjnS5AwcO1CeffBL4s5qamnTMMcd0K/vsZz+rF154QY2NjfrFL37R7Ua7VFvrdnXttdfqmmuuUXNzs+bPn69du3ZJkg4//HAdcsghWrlypdasWaOzzjor8BjTidSKvkuue8VisZKMobW1NZTPzqefoG2yHZepPl1d0PKwvp98MT+Zy5mf7G2KNT9BywrtwAMP1I4dO9Te3p71BrRrTzlCt6x4Rbs++XQFP3ifAbr2lCO6tQ3SV0+p2px44ol6/vnndeedd2ratGnasWOHxo8fr7//+7/Xhg0b9PnP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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"AngRes\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.bins])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.bins])\n", - "y = h.values\n", - "yerr = h.variances\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(2.e-2, 1)\n", - "plt.xscale(\"log\")\n", - "plt.xlabel(\"True energy [TeV]\")\n", - "plt.ylabel(\"Angular resolution [deg]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", - "\n", - "plt.semilogy(psf_pyirf.columns[\"ENERG_LO\"].array,\n", - " psf_pyirf.columns[\"PSF68\"].array,\n", - " drawstyle='steps-post',\n", - " label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Energy resolution\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"ERes\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.bins[1:]])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.bins[1:]])\n", - "y = h.values[1:]\n", - "yerr = h.variances[1:]\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(0., 0.3)\n", - "plt.xscale(\"log\")\n", - "#plt.yscale(\"log\")\n", - "plt.xlabel(\"Reconstructed energy [TeV]\")\n", - "plt.ylabel(\"Energy resolution\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", - "plt.semilogx(edisp_true_pyirf, resolution_pyirf, label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Background rate\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"BGRate\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.bins])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.bins])\n", - "y = h.values\n", - "yerr = h.variances\n", - "\n", - "# Style settings\n", - "#plt.xlim(1.e-2, 2.e2)\n", - "#plt.ylim(1.e-7, 1.1)\n", - "plt.xscale(\"log\")\n", - "plt.xlabel(\"Reconstructed energy [TeV]\")\n", - "plt.ylabel(\"Background rate [s^-1]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", - "plt.loglog(background_pyirf.columns['ENERG_LO'].array,\n", - " background_pyirf.columns['BGD'].array,\n", - " drawstyle='steps-post',\n", - " label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Differential sensitivity\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAt4AAAHkCAYAAAAJnSgJAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOzde7xVVb338e+PrSd8BMUL+aBgaImKN7ykoqfc5CU0iBOh5K3yxtHSDp3qpI+90lIPpmU+lqmlpGaoPGapqFkqWzOUQNwmoHgILwf05KUQtmnZ5vf8sdamxXbPteZa87bG2p/367Veseac47Ldv5iDOX9jDHN3AQAAAMjWgKI7AAAAAPQHDLwBAACAHDDwBgAAAHLAwBsAAADIAQNvAAAAIAcMvAEAAIAcbFR0B7JkZhMlTdxkk01OGzFiRNHdaci6des0YEB+/z5Ks70kdTVSNm6ZONfVuqba+bx/Z2nKs+9pt5VnvNVzPfEWLdR4S1pXVvFGrEXjXpp+GeIt2rPPPvuauw/t86S7t/xn1KhRHqq5c+cG216SuhopG7dMnOtqXVPtfN6/szTl2fe028oz3uq5nniLFmq8Ja0rq3gj1qJxL02/DPEWTdJCjxiThvlPCQAAACAwDLwBAACAHJi38JbxPTnew4YNO23WrFlFd6chXV1dGjRoUJDtJamrkbJxy8S5rtY11c7n/TtLU559T7utPOOtnuuJt2ihxlvSurKKN2ItGvfS9MsQb9HGjRv3uLvv19e5lh5499h555192bJlRXejIR0dHWpvbw+yvSR1NVI2bpk419W6ptr5vH9nacqz72m3lWe81XM98RYt1HhLWldW8UasRWuFe+k777yjlStX6u23345d9u2339bAgQPrai9umTjX1bqm2vlG+p63gQMHavjw4dp44403OG5mkQPvll7VBAAAoBWsXLlSgwcP1siRI2VmscqsXbtWgwcPrquduGXiXFfrmmrnG+l7ntxdr7/+ulauXKkddtghdjlyvAEAAJrc22+/ra222ir2oBvZMjNttdVWdb2BkBh4AwAABIFBd3Np5PfR0jneTK4stj0mhIQn1MluSetjcmUxQo03JleGpxXupZtvvrk+8IEP1FW2u7tbbW1tmZSJc93kyZM1c+ZMDRky5F3nfv7zn+uiiy7SNttso7vvvrvhfhRt+fLleuONNzY4Vm1yZeGb2+TxYQOdYtpj0f/whLqhSdL62ECnGKHGGxvohKcV7qVLly6tu+yaNWsyKxPnur6uWbdunXd3d/tHP/pRnzNnTuJ+FK2v34vYQAcAAABJPP/889pll130mc98RmPHjtWUKVN099136xOf+MT6a379619r8uTJkqTdd99dr732mp5//nntuuuu+tznPqd99tlHF1xwgR555BFNnz5dX/nKV4r6cQrBwBsAAACxLFu2TNOmTdOjjz6qzTbbTEuXLtXTTz+tV199VZL04x//WCeddFKf5T796U/riSee0Hnnnaf99ttP1157rS699NK8f4RCsZwgAABAQL5x1xItfWlNzevqyZMeve1mOm/ibjWvGzFihA4++GCtXbtWJ5xwgq644gqdeOKJuummm3TSSSfp0Ucf1Y033viucu973/t04IEHxupLK2NyZZNrhQkheZVlAlJyoU52S1ofkyuLEWq8MbkyPK1wL62cXPmtX/1Bz/yxq2ZZd4+98sYu2wzSV494f9XB+gsvvKCjjjpKS5YsUXd3tx555BFdc801+s53vqOpU6fq05/+tF544QVdcMEFkqTddttNDz/8sLq6unTMMcdo/vz56+s66qij9M1vflP77df3HEQmVwb8YXJlMe0xuTI8oU52S1ofkyuLEWq8MbkyPK1wL22GyZXPPfecS/J58+b5mjVr/NRTT/Vvf/vb7u4+YcIE33bbbX3JkiXrr99+++391Vdf9eeee8532223Deo65JBDvKOjI9W+F4HJlQAAAMjErrvuqhtuuEFjx47Vn/70J51xxhmSpOOPP14jRozQ6NGjC+5hcyPHGwAAALEMGDBAV1999bu2dH/kkUd02mmnbXDt4sWLNXjwYG299dZavHjxBuc6Ojq0du3aXPrcTBh4AwAAoGH77ruvNt10U33nO98puitNj4E3AAAAaho5cuS7nlxL0uOPP15Ab8JEjjcAAACQAwbeAAAAQA5Yx7vJtcLao3mVZa3b5EJdVzlpfazjXYxQ4411vMPTCvfSynW842pkLey4ZeJcV+uaaudZxzvgD+t4F9Me63iHJ9R1lZPWxzrexQg13ljHOzytcC9thnW8672u1jXVzrOONwAAAPqttrY2jRkzRmPGjNHBBx+siy++ONX6Ozo6NG/evPXfzz//fG233XYaM2aMdtppJ02ePFlLly5df/7UU0/d4Htc119/vc4888xU+lwvVjUBAABATZtssok6Ozsl6V3reKeho6NDgwYN0kEHHbT+2Be/+EV9+ctfliTdeuut+shHPqKnnnpKQ4cO1bXXXptq+3ngiTcAAAAacu+99+qYY45Z/72jo0MTJ06UJD3wwAMaO3as9tlnHx199NHq6uqSVFqW8LzzztOHPvQh7bHHHnrmmWf0/PPP6+qrr9Z3v/tdjRkzZoMn3z2mTp2qI444Qj3z9trb27Vw4UJ1d3frs5/9rHbffXftscce+u53v7v+/PTp03XQQQdp99131+9+97t31XnXXXfpgAMO0N57763DDjtMf/zjH7Vu3TrttNNOevXVVyVJ69at0wc+8AG99tprif97MfAGAABATW+99dYGqSa33nqrDj/8cD322GN68803JZWeSk+dOlWvvfaaLr30Ut1///1atGiR9ttvP1122WXr69p66631m9/8RmeccYa+/e1va+TIkTr99NP1xS9+UZ2dnRs89a60zz776JlnntngWGdnp1atWqXFixfrqaee0kknnbT+3Jtvvql58+bpBz/4gU4++eR31ffP//zPeuyxx/TEE0/oU5/6lC655BINGDBAJ5xwgn76059Kku6//37ttdde2nrrrRP/NyTVBAAAADVFpZqMHz9ed911l6ZMmaK7775bl1xyiR566CE988wzOvjggyVJf/vb3zR27Nj1dU2ePFlSadfL22+/PXYfvI/V+HbccUetWLFCZ511lj72sY/piCOOWH/u2GOPlSR9+MMf1po1a7R69eoNyq5cuVJTp07Vyy+/rL/97W/aYYcdJEknn3yyJk2apOnTp2vmzJkbDOaT4Ik3AABAQb5x1xJ9464lRXcjkalTp2r27Nl68MEH9cEPflCDBw+Wu2vcuHHq7OxUZ2enli5dquuuu259mfe85z2SShM2//73v8du64knntCuu+66wbEttthCTz75pNrb23XllVfq1FNPXX/OzDa4tvf3s846S2eeeaaeeuopXXPNNXr77bclSSNGjNA222yjBx98UPPnz9eRRx4Zu4/VMPAGAAAoyNKX1mjpS2uK7kYi7e3tWrRokX70ox9p6tSpkqQDDzxQ8+fP1/LlyyVJf/nLX/Tss89WrWfw4MFau3Zt5Pmf/exn+tWvfrX+KXaP1157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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(12,8))\n", - "\n", - "# Data\n", - "h = input_EventDisplay[\"DiffSens\"]\n", - "\n", - "#x = np.asarray([(x_bin[1]+x_bin[0])/2. for x_bin in h.allbins[2:-1]])\n", - "x = 10**h.edges[1:-1]\n", - "\n", - "y = h.values[1:]\n", - "#yerr = h.allvariances[2:-1]\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(3.e-16, 7.e-9)\n", - "\n", - "plt.xscale(\"log\")\n", - "plt.yscale(\"log\")\n", - "plt.xlabel(\"Reconstructed energy [TeV]\")\n", - "plt.ylabel(\"E^2 x Flux Sensitivity [erg cm^-2 s^-2]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "\n", - "# Plot function\n", - "\n", - "errdict=dict(fmt=\"o\")\n", - "\n", - "plt.bar(x,\n", - " height=y, \n", - " width=np.diff(10**h.edges[1:]), \n", - " align='edge', \n", - " xerr=np.diff(10**h.edges[1:])/2,\n", - " yerr=None,\n", - " fill=False,\n", - " linewidth=0,\n", - " label=\"EventDisplay\",\n", - " ecolor = \"blue\",\n", - " )\n", - "\n", - "plt.loglog(sensitivity_pyirf.columns['ENERG_LO'].array,\n", - " sensitivity_pyirf.columns['SENSITIVITY'].array,\n", - " drawstyle='steps-post',\n", - " label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Close FITS files" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "input_EventDisplay.close()\n", - "hdul_pyirf.close()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Edit Metadata", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "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.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/contribute/index.rst b/docs/contribute/index.rst deleted file mode 100644 index 7b2fe59b2..000000000 --- a/docs/contribute/index.rst +++ /dev/null @@ -1,61 +0,0 @@ -.. _contribute: - -How to contribute -================= - -.. toctree:: - :hidden: - - repo.rst - ./comparison_with_EventDisplay.ipynb - -Development procedure ---------------------- - -A common way to add your contribution would look like this: - -1. install *pyirf* in developer mode (:ref:`developer`) -2. start using it! -3. when you find something that is wrong or missing - - - go to the Projects or Issues tab of the GitHub repository (:ref:`repo`) and check if - it is already popped out - - in general it is always better to anticipate a PR with a new issue and link the two - -4. branch from the master branch and start working on the code -5. create a PR from your branch to the project's official repository - -This will trigger a number of operations on the Continuous Integration (CI) -pipeline, which are related to the quality of pushed modifications: - -- documentation -- unit-testing -- benchmarks - -So your PR should always come with: - - a unit-test for each (at least) new method, function or class you introduce, - - same for docstrings - - execution (locally on your machine, for the moment) of the benchmarks - -Please, at the time of your first contribution, add your first and last -name together with your contact email in the `AUTHORS.rst` file that you find -in the documentation folder of the project. - -Further details ---------------- - -- Unit-tests are supposed to cover the whole code and all its possibilities - in terms of cases, arguments, ecc.. This is ensured by a check on their - *coverage* which we should always aim to maximize and keep stable (ideally to 100%) -- Benchmarks instead check for the quality and performance of the results, - they come as notebooks stored for the moment under the *notebooks* folder -- These guidelines are necessarely quite general in terms of code quality, - please have a look also to the - `ctapipe development guidelines `_ -- for what concerns CTA IRFs have a look to the - `CTA IRF working group (internal) `_ - -Benchmarks ------------ - -- `Comparison with EventDisplay <./comparison_with_EventDisplay.ipynb>`__ | *comparison_with_EventDisplay.ipynb* diff --git a/docs/contribute/repo.rst b/docs/contribute/repo.rst deleted file mode 100644 index e6d8182ca..000000000 --- a/docs/contribute/repo.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. _repo: - -The repository -============== - -Is useful for both basic users and developers to monitor the status of the -development of *pyirf*. - -Start from the **projects** tab, which currently consists of - -- *Next release*, -- *Things to fix*. - -They don't come with specific deadlines, because they are meant to -give a continuous overview regardless of versioning. - -Please, if what you have in mind is not already covered open a new issue. - -Next release ------------- - -It collects all open issues and pull-requests related to the -work needed for releasing a new version. - -It has 4 sections: - -- *Summary issues*, lists of issues all related to a particular subject, -- *To Do*, open issues that should trigger pull-requests (some can be as simple as a question!), -- *In progress*, pull-requests pushed by a user to the repository, -- *Review in progress*, one or some of the maintainers started reviewing - the pull-request(s) and/or discussing with the authors, -- *Reviewer approved*, the pull-request has been approved, - but not yet merged into the master branch, -- *Done*, the pull-request has been accepted and merged; any linked issue - will automatically disappear. - -At any point, if an issue or pull-request gets re-opened it will automatically -reappear in the corresponding section of this project. - -Things to fix -------------- - -A tracker for bugs, but also for when the code works, but there is either -a limitation or degradation in performance. - -The project is divided in the following sections: - -- *Needs triage*, collects all open issues labelled either ``bug`` or ``wrong behaviour`` - that have not been classified by priority, -- *High priority*, open issues that previously needed triage, but that have been - recognized to be fatal or urgent, -- *Low prioriy*, same but for issues related to non-urgent performance / non-fatal bugs, -- *In progress*, pull-requests opened to solve either of the prioritized issues - of this project (could be under review or stale), -- *Closed*, are closed issues or approved and merged pull-requests. diff --git a/docs/cut_optimization.rst b/docs/cut_optimization.rst new file mode 100644 index 000000000..b35363b37 --- /dev/null +++ b/docs/cut_optimization.rst @@ -0,0 +1,11 @@ +.. _cut_optimization: + +Cut Optimization +================ + + +Reference/API +------------- + +.. automodapi:: pyirf.cut_optimization + :no-inheritance-diagram: diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 000000000..36c4f9775 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,39 @@ +.. _examples: + +Examples +======== + +Calculating Sensitivity and IRFs for EventDisplay DL2 data +---------------------------------------------------------- + +The ``examples/calculate_eventdisplay_irfs.py`` file is +using ``pyirf`` to optimize cuts, calculate sensitivity and IRFs +and then store these to FITS files for DL2 event lists from EventDisplay. + +The ROOT files were provided by Gernot Maier and converted to FITS format +using `the EventDisplay DL2 converter script `_. +The resulting FITS files are the input to the example and can be downloaded using: + +.. code:: bash + + ./download_test_data.sh + +This requires ``curl`` and ``unzip`` to be installed. + +The example can then be run from the root of the repository after installing pyirf +by running: + +.. code:: bash + + python examples/calculate_eventdisplay_irfs.py + + +A jupyter notebook plotting the results and comparing them to the EventDisplay output +is available in :doc:`notebooks/comparison_with_EventDisplay`. + + +Visualization of the included Flux Models +----------------------------------------- + +The ``examples/plot_spectra.py`` visualizes the Flux models included +in ``pyirf`` for Crab Nebula, proton and electron flux. diff --git a/docs/index.rst b/docs/index.rst index fe708db40..eafebd5a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,6 +3,9 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +.. meta:: + :github_url: https://github.com/cta-observatory/pyirf + Welcome to pyirf's documentation! ================================= @@ -13,15 +16,15 @@ The package is being developed and tested by members of the CTA consortium and is a spin-off of the analog sub-process of the `pipeline protopype `_. -Its main features are currently to, +Its main features are currently to * find the best cutoff in gammaness/score, to discriminate between signal and background, as well as the angular cut to obtain the best sensitivity for a given amount of observation time and a given template for the - source of interest (:ref:`perf`) + source of interest (:ref:`cut_optimization`) * compute the instrument response functions, effective area, - point spread function and energy resolution (:ref:`perf`) - * estimate the sensitivity of the array (:ref:`perf`), + point spread function and energy resolution (:ref:`irf`) + * estimate the sensitivity of the array (:ref:`sensitivity`), with plans to extend its capabilities to reach the requirements of the future observatory. @@ -36,26 +39,34 @@ which this documentation is linked. .. warning:: This is not yet stable code, so expect large and rapid changes. -.. _pyirf_intro: .. toctree:: - :caption: Overview :maxdepth: 1 + :caption: Overview + :name: _pyirf_intro - install/index - usage/index - contribute/index + install + introduction + examples + notebooks/index + contribute changelog AUTHORS -.. _pyirf_structure: + .. toctree:: - :caption: Structure :maxdepth: 1 + :caption: API Documentation + :name: _pyirf_api_docs + irf/index + sensitivity + benchmarks/index + cut_optimization + spectral + binning io/index - resources/index - perf/index - scripts/index + utils + Indices and tables ================== diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 000000000..0069fc02b --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,42 @@ +.. _install: + +Installation +============ + + +``pyirf`` requires Python ≥3.6 and the packages as defined in the ``setup.py``. +Core dependencies are + +* ``numpy`` +* ``astropy`` +* ``scipy`` + +Dependencies for development, like unit tests and building the documentation +are defined in ``extras``. + +Installing a released version +----------------------------- + + +To install a release version, just install the ``pyirf`` package using + +.. code-block:: bash + + pip install pyirf + +or add it to the dependencies of your project. + + +Installing for development +-------------------------- + +If you want to work on pyirf itself, clone the repository or your fork of +the repository and install the local copy of pyirf in development mode. + +Make sure you add the ``tests`` and ``docs`` extra to also install the dependencies +for unit tests and building the documentation. +You can also simply install the ``all`` extra: + +.. code-block:: bash + + pip install -e '.[all]' # or [docs,tests] diff --git a/docs/install/basic.rst b/docs/install/basic.rst deleted file mode 100644 index c7f8e9d62..000000000 --- a/docs/install/basic.rst +++ /dev/null @@ -1,30 +0,0 @@ -.. _basic: - -Installation for basic users -============================ - -.. warning:: - Given that *pyirf* is undergoing fast development, it is likely that you - will benefit more from a more recent version of the code for now. - - The development version could disrupt functionalities that were working for - you, but the latest released version could lack some of those you need. - - To install the latest development version go to :ref:`developer`. - -If you are a user with no interest in developing *pyirf*, you can start by -downloading the `latest released version `__ - -Steps for installation: - - 1. uncompress the file which is always called *pyirf-X.Y.Z* depending on version, - 2. enter the folder ``cd pyirf-X.Y.Z`` - 3. create a dedicated environment with ``conda env create -f environment.yml`` - 4. activate it with ``conda activate pyirf`` - 5. install *pyirf* itself with ``pip install .``. - -Next steps: - - * get accustomed to the basics (:ref:`perf`), - * start using *pyirf* (:ref:`usage`), - * for bugs and new features, please contribute to the project (:ref:`contribute`). diff --git a/docs/install/developer.rst b/docs/install/developer.rst deleted file mode 100644 index ccd9ddce7..000000000 --- a/docs/install/developer.rst +++ /dev/null @@ -1,21 +0,0 @@ -.. _developer: - -Installation for developers -=========================== - -If you want to use *pyirf* and also contribute to its development, follow these steps: - - 1. Fork the official `repository `_ has explained `here `__ (follow all the instructions) - 2. now your local copy is linked to your remote repository (**origin**) and the official one (**upstream**) - 3. create a dedicated environment with ``conda env create -f environment.yml`` - 4. activate it with ``conda activate pyirf`` - 5. install *pyirf* itself in developer mode with ``pip install -e .`` - -In this way, you will always use the version of the source code on which you -are working. - -Next steps: - - * get accustomed to the basics (:ref:`perf`), - * start using *pyirf* (:ref:`usage`), - * for bugs and new features, please contribute to the project (:ref:`contribute`). diff --git a/docs/install/index.rst b/docs/install/index.rst deleted file mode 100644 index 8c17c9707..000000000 --- a/docs/install/index.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. _install: - -Installation -============ - -The only requirement is a Python3.x installation with a compatible package -manager (e.g. pip). - -A virtual environment manager such as the one provided by an Anaconda -(or Miniconda) installation supporting such python version is recommended. - -These instructions we will assume that this is the case. - -There are two different ways to install `pyirf`, - -* if you just want to use it as it is (:ref:`basic`), -* or if you also want to develop it (:ref:`developer`). - -.. warning:: - We are in the early phases of development: even if a pre-release already - exists, it is likely that you will benefit more from the development version. - -After installing `pyirf`, you can start using it (:ref:`usage`). - -.. Note:: - - For a faster use, edit your preferred login script (e.g. ``.bashrc`` on Linux or - ``.profile`` on macos) with a function that initializes the environment. - The following is a minimal example using Bash. - - .. code-block:: bash - - alias pyirf_init="pyirf_init" - - function pyirf_init() { - - conda activate pyirf # Then activate the pyirf environment - export PYIRF=$WHEREISPYIRF/pyirf/scripts # A shortcut to the scripts folder - - } - -.. toctree:: - :hidden: - :maxdepth: 2 - - basic - developer diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 100644 index 000000000..80b68b7e8 --- /dev/null +++ b/docs/introduction.rst @@ -0,0 +1,91 @@ +.. _introduction: + +Introduction to ``pyirf`` +========================= + + +``pyirf`` aims to provide functions to calculate the Instrument Response Functions (IRFs) +and sensitivity for Imaging Air Cherenkov Telescopes. + +To support a wide range of use cases, ``pyirf`` opts for a library approach of +composable building blocks with well-defined inputs and outputs. + +For more information on IRFs, have a look at the `Specification of the Data Formats for Gamma-Ray Astronomy`_ +or the `ctools documentation on IRFs `_. + + +Currently, ``pyirf`` allows calculation of the usual factorization of the IRFs into: + +* Effective area +* Energy migration +* Point spread function + +Additionally, functions for calculating point-source flux sensitivity are provided. +Flux sensitivity is defined as the smallest flux an IACT can detect with a certain significance, +usually 5 σ according to the Li&Ma likelihood ratio test, in a specified amount of time. + +``pyirf`` also provides functions to calculate event weights, that are needed +to translate a set of simulations to a physical flux for calculating sensitivity +and expected event counts. + +Event selection with energy dependent cuts is also supported, +but at the moment, only rudimentary functions to find optimal cuts are provided. + + +Input formats +------------- + +``pyirf`` does not rely on specific input file formats. +All functions take ``numpy`` arrays, astropy quantities or astropy tables for the +required data and also return the results as these objects. + +``~pyirf.io`` provides functions to export the internal IRF representation +to FITS files following the `Specification of the Data Formats for Gamma-Ray Astronomy`_ + + +DL2 event lists +^^^^^^^^^^^^^^^ + +Most functions for calculating IRFs need DL2 event lists as input. +We use ``~astropy.table.QTable`` instances for this. +``QTable`` are very similar to the standard ``~astropy.table.Table``, +but offer better interoperability with ``astropy.units.Quantity``. + +We expect certain columns to be present in the tables with the appropriate units. +To learn which functions need which columns to be present, have a look at the :ref:`_pyirf_api_docs` + +Most functions only need a small subgroup of these columns. + +.. table:: Column definitions for DL2 event lists + + +-------------------+--------+----------------------------------------------------+ + | Column | Unit | Explanation | + +===================+========+====================================================+ + | true_energy | TeV | True energy of the simulated shower | + +-------------------+--------+----------------------------------------------------+ + | weight | | Event weight | + +-------------------+--------+----------------------------------------------------+ + | source_fov_offset | deg | Distance of the true origin to the FOV center | + +-------------------+--------+----------------------------------------------------+ + | true_alt | deg | True altitude of the shower origin | + | true_alt | deg | True altitude of the shower origin | + +-------------------+--------+----------------------------------------------------+ + | true_az | deg | True azimuth of the shower origin | + +-------------------+--------+----------------------------------------------------+ + | pointing_alt | deg | Altitude of the field of view center | + +-------------------+--------+----------------------------------------------------+ + | pointing_az | deg | Azimuth of the field of view center | + +-------------------+--------+----------------------------------------------------+ + | reco_energy | TeV | Reconstructed energy of the simulated shower | + +-------------------+--------+----------------------------------------------------+ + | reco_alt | deg | Reconstructed altitude of shower origin | + +-------------------+--------+----------------------------------------------------+ + | reco_az | deg | Reconstructed azimuth of shower origin | + +-------------------+--------+----------------------------------------------------+ + | gh_score | | Gamma/Hadron classification output | + +-------------------+--------+----------------------------------------------------+ + | multiplicity | | Number of telescopes used in the reconstruction | + +-------------------+--------+----------------------------------------------------+ + + +.. _Specification of the Data Formats for Gamma-Ray Astronomy: https://gamma-astro-data-formats.readthedocs.io diff --git a/docs/io/index.rst b/docs/io/index.rst index ee2506057..3094fd16d 100644 --- a/docs/io/index.rst +++ b/docs/io/index.rst @@ -6,28 +6,11 @@ Input / Output Introduction ------------ -This module contains a set of classes and functions for +This module contains functions to read input data and write IRFs in GADF format. -- configuration input, -- DL2 input, -- DL3 output. +Currently there is only support for reading EventDisplay DL2 FITS files, +which were converted from the ROOT files by using `EventDisplay DL2 conversion scripts `_. -The precise structure is currently under development, but we expect that: - -- there should be a reader for each data format (we expect to support only FITS and HDF5 for the moment), -- each reader should read a ``pipeline`` argument, depending on different DL2 file format and internal structure, -- every reader calls a mapper that reads user-defined DL2 column names from the configuration file (see :ref:`resources` for an example) into an internal data format, -- the output format for the IRFs is based on the latest version of the GADF plus any integration we think is necessary. - -Most of the required column names for the interanl data format are defined in the configuration file under the -section ``column_definition``. - -The current output is composed by: - -- IRFs in FITS format, -- a table also in FITS format containing information about the cuts applied to generate the IRFs, -- an HDF5 table for each particle type containing the DL2 events selected with those cuts -- a diagnostic folder containing intermediate plots in form of PDF and pickle files Reference/API ------------- diff --git a/docs/irf/index.rst b/docs/irf/index.rst new file mode 100644 index 000000000..d0c9de164 --- /dev/null +++ b/docs/irf/index.rst @@ -0,0 +1,34 @@ +.. _irf: + +Instrument Response Functions +============================= + + +Effective Area +-------------- + +The collection area, which is proportional to the gamma-ray efficiency +of detection, is computed as a function of the true energy. The events which +are considered are the ones passing the threshold of the best cutoff plus +the angular cuts. + +Energy Dispersion Matrix +------------------------ + +The migration matrix, ratio of the reconstructed energy over the true energy +as a function of the true energy, is computed with the events passing the +threshold of the best cutoff plus the angular cuts. + + +Point Spread Function +--------------------- + +The PSF describes the probability of measuring a gamma ray +of a given true energy and true position at a reconstructed position. + + +Reference/API +------------- + +.. automodapi:: pyirf.irf + :no-inheritance-diagram: diff --git a/docs/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb new file mode 100644 index 000000000..5df5c1a8d --- /dev/null +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -0,0 +1,638 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comparison with EventDisplay" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Purpose of this notebook:**\n", + "\n", + "Compare IRF and Sensitivity as computed by pyirf and EventDisplay on the same DL2 results\n", + "\n", + "**Notes:**\n", + "\n", + "The following results correspond to:\n", + "\n", + "- Paranal site\n", + "- Zd 20 deg, Az 180 deg\n", + "- 50 h observation time\n", + "\n", + "**Resources:**\n", + "\n", + "_EventDisplay_ DL2 data, https://forge.in2p3.fr/projects/cta_analysis-and-simulations/wiki/Eventdisplay_Prod3b_DL2_Lists\n", + "\n", + "\n", + "Download and unpack the data using \n", + "\n", + "```bash\n", + "$ curl -fL -o data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download\n", + "$ unzip data.zip\n", + "$ mv eventdisplay_dl2 data\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Table of contents\n", + "\n", + "* [Optimized cuts](#Optimized-cuts)\n", + " - [Direction cut](#Direction-cut)\n", + "* [Differential sensitivity from cuts optimization](#Differential-sensitivity-from-cuts-optimization)\n", + "* [IRFs](#IRFs)\n", + " - [Effective area](#Effective-area)\n", + " - [Point Spread Function](#Point-Spread-Function)\n", + " + [Angular resolution](#Angular-resolution)\n", + " - [Energy dispersion](#Energy-dispersion)\n", + " + [Energy resolution](#Energy-resolution)\n", + " - [Background rate](#Background-rate)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import numpy as np\n", + "import uproot\n", + "from astropy.io import fits\n", + "import astropy.units as u\n", + "import matplotlib.pyplot as plt\n", + "from astropy.table import QTable\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.rcParams['figure.figsize'] = (9, 6)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "## Input data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "### _EventDisplay_" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "The input data provided by _EventDisplay_ is stored in _ROOT_ format, so _uproot_ is used to transform it into _numpy_ objects. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "# Path of EventDisplay IRF data in the user's local setup\n", + "# Please, empty the indir_EventDisplay variable before pushing to the repo\n", + "indir = \"../../data/\"\n", + "irf_file_event_display = \"DESY.d20180113.V3.ID0_180degNIM2LST4MST4SST4SCMST4.prod3b-paranal20degs05b-NN.S.3HB9-FD.180000s.root\"\n", + "\n", + "irf_eventdisplay = uproot.open(os.path.join(indir, irf_file_event_display))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "## _pyirf_" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following is the current IRF + sensititivy output FITS format provided by this software.\n", + "\n", + "Run `python examples/calculate_eventdisplay_irfs.py` after downloading the data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pyirf_file = '../../pyirf_eventdisplay.fits.gz'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimized cuts\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Direction cut\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "from astropy.table import QTable\n", + "\n", + "\n", + "rad_max = QTable.read(pyirf_file, hdu='RAD_MAX')[0]\n", + "\n", + "\n", + "theta_cut_ed = irf_eventdisplay['ThetaCut;1']\n", + "plt.errorbar(\n", + " 10**theta_cut_ed.edges[:-1],\n", + " theta_cut_ed.values**2,\n", + " xerr=np.diff(10**theta_cut_ed.edges),\n", + " ls='',\n", + " label='EventDisplay',\n", + ")\n", + "\n", + "plt.errorbar(\n", + " 0.5 * (rad_max['ENERG_LO'] + rad_max['ENERG_HI'])[1:-1].to_value(u.TeV),\n", + " rad_max['RAD_MAX'].T[1:-1, 0].to_value(u.deg)**2,\n", + " xerr=0.5 * (rad_max['ENERG_HI'] - rad_max['ENERG_LO'])[1:-1].to_value(u.TeV),\n", + " ls='',\n", + " label='pyirf',\n", + ")\n", + "\n", + "plt.legend()\n", + "plt.ylabel('θ²-cut / deg²')\n", + "plt.xlabel(r'$E_\\mathrm{reco} / \\mathrm{TeV}$')\n", + "plt.xscale('log')\n", + "plt.yscale('log')\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from astropy.table import QTable\n", + "\n", + "\n", + "gh_cut = QTable.read(pyirf_file, hdu='GH_CUTS')[1:-1]\n", + "\n", + "\n", + "plt.errorbar(\n", + " 0.5 * (gh_cut['low'] + gh_cut['high']).to_value(u.TeV),\n", + " gh_cut['cut'],\n", + " xerr=0.5 * (gh_cut['high'] - gh_cut['low']).to_value(u.TeV),\n", + " ls='',\n", + " label='pyirf',\n", + ")\n", + "\n", + "plt.legend()\n", + "plt.ylabel('G/H-cut')\n", + "plt.xlabel(r'$E_\\mathrm{reco} / \\mathrm{TeV}$')\n", + "plt.xscale('log')\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Differential sensitivity from cuts optimization\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# [1:-1] removes under/overflow bin\n", + "sensitivity = QTable.read(pyirf_file, hdu='SENSITIVITY')[1:-1]\n", + "sensitivity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(12,8))\n", + "\n", + "# Get data from event display file\n", + "h = irf_eventdisplay[\"DiffSens\"]\n", + "bins = 10**h.edges\n", + "x = 0.5 * (bins[:-1] + bins[1:])\n", + "width = np.diff(bins)\n", + "y = h.values\n", + "\n", + "plt.errorbar(\n", + " x,\n", + " y, \n", + " xerr=width/2,\n", + " yerr=None,\n", + " label=\"EventDisplay\",\n", + " ls=''\n", + ")\n", + "\n", + "unit = u.Unit('erg cm-2 s-1')\n", + "\n", + "\n", + "e = sensitivity['reco_energy_center']\n", + "s = (e**2 * sensitivity['flux_sensitivity'])\n", + "\n", + "plt.errorbar(\n", + " e.to_value(u.TeV),\n", + " s.to_value(unit),\n", + " xerr=(sensitivity['reco_energy_high'] - sensitivity['reco_energy_low']).to_value(u.TeV) / 2,\n", + " ls='',\n", + " label='pyirf'\n", + ")\n", + "\n", + "\n", + "\n", + "# Style settings\n", + "plt.title('Minimal Flux Needed for 5σ Detection in 50 hours')\n", + "plt.xscale(\"log\")\n", + "plt.yscale(\"log\")\n", + "plt.xlabel(\"Reconstructed energy [TeV]\")\n", + "plt.ylabel(rf\"$(E^2 \\cdot \\mathrm{{Flux Sensitivity}}) /$ ({unit.to_string('latex')})\")\n", + "plt.grid(which=\"both\")\n", + "plt.legend()\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## IRFs\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Effective area\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Data from EventDisplay\n", + "h = irf_eventdisplay[\"EffectiveAreaEtrue\"]\n", + "\n", + "x = 0.5 * (10**h.edges[:-1] + 10**h.edges[1:])\n", + "xerr = 0.5 * np.diff(10**h.edges)\n", + "y = h.values\n", + "yerr = np.sqrt(h.variances)\n", + "\n", + "plt.errorbar(x, y, xerr=xerr, yerr=yerr, ls='', label=\"EventDisplay\")\n", + "\n", + "for name in ('', '_NO_CUTS', '_ONLY_GH', '_ONLY_THETA'):\n", + "\n", + " area = QTable.read(pyirf_file, hdu='EFFECTIVE_AREA' + name)[0]\n", + "\n", + " \n", + " plt.errorbar(\n", + " 0.5 * (area['ENERG_LO'] + area['ENERG_HI']).to_value(u.TeV)[1:-1],\n", + " area['EFFAREA'].to_value(u.m**2).T[1:-1, 0],\n", + " xerr=0.5 * (area['ENERG_LO'] - area['ENERG_HI']).to_value(u.TeV)[1:-1],\n", + " ls='',\n", + " label='pyirf ' + name,\n", + " )\n", + "\n", + "# Style settings\n", + "plt.xscale(\"log\")\n", + "plt.yscale(\"log\")\n", + "plt.xlabel(\"True energy / TeV\")\n", + "plt.ylabel(\"Effective collection area / m²\")\n", + "plt.grid(which=\"both\")\n", + "plt.legend()\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Point Spread Function\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psf_table = QTable.read(pyirf_file, hdu='PSF')[0]\n", + "# select the only fov offset bin\n", + "psf = psf_table['RPSF'].T[:, 0, :].to_value(1 / u.sr)\n", + "\n", + "offset_bins = np.append(psf_table['RAD_LO'], psf_table['RAD_HI'][-1])\n", + "phi_bins = np.linspace(0, 2 * np.pi, 100)\n", + "\n", + "\n", + "\n", + "# Let's make a nice 2d representation of the radially symmetric PSF\n", + "r, phi = np.meshgrid(offset_bins.to_value(u.deg), phi_bins)\n", + "\n", + "# look at a single energy bin\n", + "# repeat values for each phi bin\n", + "center = 0.5 * (psf_table['ENERG_LO'] + psf_table['ENERG_HI'])\n", + "\n", + "\n", + "fig = plt.figure(figsize=(15, 5))\n", + "axs = [fig.add_subplot(1, 3, i, projection='polar') for i in range(1, 4)]\n", + "\n", + "\n", + "for bin_id, ax in zip([10, 20, 30], axs):\n", + " image = np.tile(psf[bin_id], (len(phi_bins) - 1, 1))\n", + " \n", + " ax.set_title(f'PSF @ {center[bin_id]:.2f} TeV')\n", + " ax.pcolormesh(phi, r, image)\n", + " ax.set_ylim(0, 0.25)\n", + " ax.set_aspect(1)\n", + " \n", + "fig.tight_layout()\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "# Profile\n", + "center = 0.5 * (offset_bins[1:] + offset_bins[:-1])\n", + "xerr = 0.5 * (offset_bins[1:] - offset_bins[:-1])\n", + "\n", + "for bin_id in [10, 20, 30]:\n", + " plt.errorbar(\n", + " center.to_value(u.deg),\n", + " psf[bin_id],\n", + " xerr=xerr.to_value(u.deg),\n", + " ls='',\n", + " label=f'Energy Bin {bin_id}'\n", + " )\n", + " \n", + "#plt.yscale('log')\n", + "plt.legend()\n", + "plt.xlim(0, 0.25)\n", + "plt.ylabel('PSF PDF / sr⁻¹')\n", + "plt.xlabel('Distance from True Source / deg')\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Angular resolution\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Data from EventDisplay\n", + "h = irf_eventdisplay[\"AngRes\"]\n", + "x = 0.5 * (10**h.edges[:-1] + 10**h.edges[1:])\n", + "xerr = 0.5 * np.diff(10**h.edges)\n", + "y = h.values\n", + "yerr = np.sqrt(h.variances)\n", + "plt.errorbar(x, y, xerr=xerr, yerr=yerr, ls='', label=\"EventDisplay\")\n", + "\n", + "# pyirf\n", + "\n", + "ang_res = QTable.read(pyirf_file, hdu='ANGULAR_RESOLUTION')[1:-1]\n", + "\n", + "plt.errorbar(\n", + " 0.5 * (ang_res['true_energy_low'] + ang_res['true_energy_high']).to_value(u.TeV),\n", + " ang_res['angular_resolution'].to_value(u.deg),\n", + " xerr=0.5 * (ang_res['true_energy_high'] - ang_res['true_energy_low']).to_value(u.TeV),\n", + " ls='',\n", + " label='pyirf'\n", + ")\n", + "\n", + "\n", + "# Style settings\n", + "plt.xlim(1.e-2, 2.e2)\n", + "plt.ylim(2.e-2, 1)\n", + "plt.xscale(\"log\")\n", + "plt.yscale(\"log\")\n", + "plt.xlabel(\"True energy / TeV\")\n", + "plt.ylabel(\"Angular Resolution / deg\")\n", + "plt.grid(which=\"both\")\n", + "plt.legend(loc=\"best\")\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Energy dispersion\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "edisp = QTable.read(pyirf_file, hdu='ENERGY_DISPERSION')[0]\n", + "\n", + "e_bins = edisp['ENERG_LO'][1:]\n", + "migra_bins = edisp['MIGRA_LO'][1:]\n", + "\n", + "plt.title('pyirf')\n", + "plt.pcolormesh(e_bins.to_value(u.TeV), migra_bins, edisp['MATRIX'].T[1:-1, 1:-1, 0].T, cmap='inferno')\n", + "\n", + "plt.xscale('log')\n", + "plt.yscale('log')\n", + "plt.colorbar(label='PDF Value')\n", + "\n", + "plt.xlabel(r'$E_\\mathrm{True} / \\mathrm{TeV}$')\n", + "plt.ylabel(r'$E_\\mathrm{Reco} / E_\\mathrm{True}$')\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Energy resolution\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Data from EventDisplay\n", + "h = irf_eventdisplay[\"ERes\"]\n", + "x = 0.5 * (10**h.edges[:-1] + 10**h.edges[1:])\n", + "xerr = np.diff(10**h.edges) / 2\n", + "y = h.values\n", + "yerr = np.sqrt(h.variances)\n", + "\n", + "# Data from pyirf\n", + "bias_resolution = QTable.read(pyirf_file, hdu='ENERGY_BIAS_RESOLUTION')[1:-1]\n", + "\n", + "# Plot function\n", + "plt.errorbar(x, y, xerr=xerr, yerr=yerr, ls='', label=\"EventDisplay\")\n", + "plt.errorbar(\n", + " 0.5 * (bias_resolution['true_energy_low'] + bias_resolution['true_energy_high']).to_value(u.TeV),\n", + " bias_resolution['resolution'],\n", + " xerr=0.5 * (bias_resolution['true_energy_high'] - bias_resolution['true_energy_low']).to_value(u.TeV),\n", + " ls='',\n", + " label='pyirf'\n", + ")\n", + "plt.xscale('log')\n", + "\n", + "# Style settings\n", + "plt.xlabel(r\"$E_\\mathrm{True} / \\mathrm{TeV}$\")\n", + "plt.ylabel(\"Energy resolution\")\n", + "plt.grid(which=\"both\")\n", + "plt.legend(loc=\"best\")\n", + "\n", + "None # to remove clutter by mpl objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Background rate\n", + "[back to top](#Table-of-contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Data from EventDisplay\n", + "h = irf_eventdisplay[\"BGRate\"]\n", + "x = 0.5 * (10**h.edges[:-1] + 10**h.edges[1:])\n", + "xerr = np.diff(10**h.edges) / 2\n", + "y = h.values\n", + "yerr = np.sqrt(h.variances)\n", + "\n", + "# Style settings\n", + "plt.xscale(\"log\")\n", + "plt.xlabel(r\"$E_\\mathrm{Reco} / \\mathrm{TeV}$\")\n", + "plt.ylabel(\"Background rate / s⁻¹\")\n", + "plt.grid(which=\"both\")\n", + "\n", + "# Plot function\n", + "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", + "plt.legend(loc=\"best\")\n", + "\n", + "None # to remove clutter by mpl objects" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "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.7.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/notebooks/index.rst b/docs/notebooks/index.rst new file mode 100644 index 000000000..8da9140c5 --- /dev/null +++ b/docs/notebooks/index.rst @@ -0,0 +1,10 @@ +.. _notebooks: + +================= +Example Notebooks +================= + +.. toctree:: + :maxdepth: 1 + + comparison_with_EventDisplay diff --git a/docs/perf/index.rst b/docs/perf/index.rst deleted file mode 100644 index 6d38a3771..000000000 --- a/docs/perf/index.rst +++ /dev/null @@ -1,246 +0,0 @@ -.. _perf: - -**** -perf -**** - -Introduction -============ - -The perf module contains classes that are used to estimate the performance of the -instrument. There are tools to handle the determination of the best-cutoffs -to separate gamma and the background (protons + electrons), to produce the -instrument response functions (IRFs) and to estimate the sensitivity. - -The following responses are computed: - - * Effective area as a function of true energy - * Migration matrix as a function of true and reconstructed energy - * Point spread function computed with a 68 % radius containment as a function of reconstructed energy - * Background rate as a function of reconstructed energy - -The point-like source sensitivity is estimated with the gammapy_ -library. We describe below how to estimate the performance of the instruments -and we describe in details how it is done. - -Operations performed on the input DL2 data -========================================== - -Best cutoffs determination --------------------------- - -The criteria to determine the best cut off which has been use up to now -is to obtain the minimal flux with a :math:`5\sigma` detection -in a given observation time for a Crab-like source template. -In order to find the cuts, a preliminary step is to weight the events according -to what has been measured in real life. -Both the weighting of the events and the determination of best cutoffs -are done with the `CutsOptimisation` class. -The application of the cuts and the generation of diagnostic plots -are handled respectively by the `CutsApplicator` and the `CutsDiagnostic` -classes. - -Weighting of events -------------------- - -The simulations are generated with a given spectral index, typically of 2 to get -high statistics at high energy. We thus need to flatten the spectral distribution -of the particle and then correct it to match reality. This is done -by computing a weight :math:`w(E)`, which is a function of true energy, for each particle. -It can be expressed as the multiplication of the ratios of fluxes and -the ratios of observation time, each ratio being defined as the division between -the 'observation quantity' and the Monte-Carlo quantity: - -.. math:: - - w(E) &= \frac{\phi_{\text{Obs}}}{\phi_{\text{MC}}} \times \frac{T_{\text{Obs}}}{T_{\text{MC}}} \\ - &= A_\text{MC} \times I_\theta \times E^\Gamma \times I_E \times T_\text{Obs} \times \phi_\text{Obs}(E)/ N_\text{MC} - -where the different quantities are defined as follow: - - * :math:`A_\mathrm{MC}`: MC generator area - * :math:`I_\theta = 2 \pi (1-\cos\theta)`: angular phase space factor for diffuse flux (:math:`I_\theta = 1` for point-sources) - * :math:`E^\Gamma`: accounts for the fact that the MC events have been drawn with an :math:`E^{-\Gamma}` spectrum - * :math:`\Gamma`: spectral index of the MC generator - * :math:`I_E = \int_{E_\text{min}}^{E_\text{max}} E^{-\Gamma} dE = (E_\text{max}^{(1-\Gamma)} - E_\text{min}^{(1-\Gamma)}) / (1-\Gamma)`: energy phase space factor - * :math:`T_\text{Obs}`: assumed observation time - * :math:`N_\text{MC}`: number of generated MC events - * :math:`\phi_\text{Obs}(E)`: expected differential flux to be matched - -The differential diffuse spectrum of the cosmic-rays comes from -`K. Bernlöhr et al. (2013) `_. -Concerning the gamma-rays, the Crab Nebula spectrum is usually took from `HEGRA -measurements `_. - -Best cutoffs ------------- - -Since the gamma/hadron separation power vary a lot with energy, the -best cutoffs to separate the gamma-rays and the background will be determined for -different bins in reconstructed energy. Those energy intervals are typically -chosen to get 5 bins per decade in energy. - -Since we are dealing with point-like sources, a cut on the angular distance -between the event position and the position of the source is done. Here the -user have the choice to optimise the angular cut as a function of energy (MARS-like), -to use the point-spread function with a 68 % containment radius (EvtDisplay-like), -or to use a fixed angular cut. Up to now, no optimisation is done for the minimal -event multiplicity for an event to be took into account in the analysis (MARS-like). -A fixed cut on the multiplicity is done. - -For each energy bin and for a given angular cut, the following procedure is done -to compute the minimal flux reachable: - - 1. Correct the number of protons and electrons to match the region of interest - define by the angular cut, e.g. the ON region - 2. Compute the gamma and the background (protons + electrons) efficiencies as - a function of the score/gammaness (fine binning) - 3. Compute the lowest flux reachable in a given observation time and with a - detection level of :math:`5\sigma` according to the `Li & Ma (1983) - `_ formula (17). Scale - the flux by the corresponding minimal number of photons if one of those - requirements is not met: - - * :math:`N_\text{excess} \geq N_\text{min}` - * :math:`N_\text{excess} \geq \text{syst}_\text{bkg} \times N_\text{bkg}` - 4. Select the cutoff and the angular cut which give the lowest flux - -To look for the minimal flux, the score/gammaness are sampled according to -fixed value in background efficiencies, 15 values between 0.05 and 0.5 by step -of 0.05, as in the MARS analysis for CTA. We do not go below 0.05 since we -want some robustness against fluctuations. - -In the two requirements, the number of excess is defined by -:math:`N_\text{excess}=N_\text{ON} - \alpha \times N_\text{OFF}`, :math:`\alpha` -is the normalisation between the ON and the OFF regions, :math:`N_\text{bkg}` -is the number of background in the ON regions and :math:`\text{syst}_\text{bkg}` -is the systematics on the number of background events. Typical values for -:math:`\alpha`, :math:`N_\text{min}` and :math:`\text{syst}_\text{bkg}` are 1/5, -10 and 5 %, respectively. - -The final results of the procedure is a FITS table containing the results of the -optimisation for each energy bin such as, the minimal and maximal energy range of -the bin, the best cutoff, the best angular cut, with the corresponding excess, -background, etc. - -Application of the cutoffs --------------------------- - -A dedicated class, called `CutsApplicator`, is in charge to apply the cuts -to the different event lists. Each event will be flagged according to the -different cuts it will pass, e.g. score/gammaness and angular cuts. -The output tables will be further processed when the user will generate IRFs. - -Diagnostics ------------ - -Several diagnostic plots are generated during the procedure. -For each energy bin both the efficiencies and the rates as a function -of the score/gammaness, as well as characteristics of the bin, are automatically -generated. -The efficiencies and the angular cuts are all also plotted against the -reconstructed energy in order to control the optimisation procedure -(e.g. background free regions, evolution of background efficiencies -with the angular cut, etc.). - -.. todo:: - - Move diagnostics to benchmarking. - -Description of the output -========================= - -The instrument response functions characterise the performance of the instrument. -In addition it is needed to estimate the sensitivity of the array. -A proposition for the CTA IRF data format is available -`here `_. - -Instrument Response Functions (IRFs) ------------------------------------- - -The IRF are stored as an HDU (Header Data Unit) list in a FITS -(Flexible Image Transport System) file. -Up to now we only considered analyses built with ON-axis gamma-ray simulations -and dedicated to the study of point-like sources. -We do not have offset dependency on the IRF for the moment and thus do not have -axes corresponding to offset bins. -Except for the migration matrix for which we hacked a bit the generation of the -EnergyDispersion object, since it expects offset axes, everything goes pretty -much smoothly. - -Effective area -^^^^^^^^^^^^^^ - -The collection area, which is proportional to the gamma-ray efficiency -of detection, is computed as a function of the true energy. The events which -are considered are the one passing the threshold of the best cutoff plus -the angular cuts. - -Energy migration matrix -^^^^^^^^^^^^^^^^^^^^^^^ - -The migration matrix, ratio of the reconstructed energy over the true energy -as a function of the true energy, is computed with the events passing the -threshold of the best cutoff plus the angular cuts. -In order to be able to use the energy dispersion with Gammapy_ to compute -the sensitvity we artificially created fake offset bins. -I guess that Gammapy_ should be able to reaf IRF with single offset. - -Background -^^^^^^^^^^ - -The question to consider whether the bakground is an IRF or not. Since here it -is needed to estimate the sensitivity of the instrument we consider it is included -in the IRFs. -Here a simple HDU containing the background (protons + electrons) rate as a -function of the reconstructed energy is generated. -The events which are considered are the one passing the threshold of -the best cutoff and the angular cuts. - -Point spread function -^^^^^^^^^^^^^^^^^^^^^ - -Here we do not really need the PSF to compute the sensitivity, since the angular -cuts are already applied to the effective area, the energy migration matrix -and the background. -I chose to represent the PSF with a containment radius of 68 % as a function -of reconstructed energy as a simple HDU. -The events which are considered are the one passing the threshold of -the best cutoff. - -We should generate the recommended IRF, e.g. parametrised as what? Apparently -there are multiple solutions -(see `here, `_). - -Angular cut values -^^^^^^^^^^^^^^^^^^ - -To be implemented: ``_ - -Sensitivity ------------ - -The sensitivity is computed with the Gammapy software. - -What could be improved? -======================= - -.. todo:: - - Move this to GitHub issues and update it - - * `Data format for IRFs `_ - * Propagation and reading SIMTEL informations (meta-data, histograms) - directly in the DL2 - * Implement optimisation on the number of telescopes to consider an event - * - -Reference/API -============= - -.. automodapi:: pyirf.perf - :no-inheritance-diagram: - -.. _HDF5: https://www.hdfgroup.org/solutions/hdf5/ -.. _Gammapy: https://gammapy.org/ -.. _data format: https://gamma-astro-data-formats.readthedocs.io/ diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 10169c834..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -astropy -ctapipe==0.7 -gammapy==0.8 -ipython -numpy -numpydoc -pandas -pygments >= 2.5.1 -pytest -pyyaml -tables -rinohtype -sphinx -nbsphinx -sphinx-automodapi -sphinx_rtd_theme diff --git a/docs/resources/index.rst b/docs/resources/index.rst deleted file mode 100644 index 5a11a0fb6..000000000 --- a/docs/resources/index.rst +++ /dev/null @@ -1,147 +0,0 @@ -.. _resources: - -========= -Resources -========= - -For the moment this folder hosts only one configuration file, ``config.yaml``. - -In the following we report an example: - -.. code-block:: yaml - - # Example configuration file for PYIRF - - # Based on protopipe one, it should progressively use GADF nomenclature. - - general: - # part of the DL2 filename(s) common between particle types - template_input_file: '{}_onSource.S.3HB9-FD_ID0.eff-0-CUT0' - # Output table name - output_table_name: 'table_best_cutoff' - # where is the DL2 data - indir: '/Users/michele/Applications/ctasoft/tests/pyirf/EventDisplay/DL2/Paranal_20deg/CUT0' - # where to store DL3 data - outdir: '/Users/michele/Applications/ctasoft/tests/pyirf/EventDisplay/from_pyirf' - - analysis: - - # Theta square cut optimisation (opti, fixed, r68) - thsq_opt: - type: 'r68' - value: 0.2 # In degree, necessary for type fixed - - # Normalisation between ON and OFF regions - alpha: 0.2 - - # Minimimal significance - min_sigma: 5 - - # Minimal number of gamma-ray-like - min_excess: 10 - - # Minimal fraction of background events for excess comparison - bkg_syst: 0.05 - - # Reco energy binning - ereco_binning: # TeV - emin: 0.05 - emax: 50 - nbin: 21 - - # Reco energy binning - etrue_binning: # TeV - emin: 0.05 - emax: 50 - nbin: 42 - - # ============================================================================= - # TO REVIEW - # ============================================================================= - - particle_information: - gamma: - n_events_per_file: 22500000 # 10**5 * 10 - n_files: 1 - e_min: 0.05 - e_max: 50 - gen_radius: 1000 - diff_cone: 0 - gen_gamma: 2 - - proton: - n_events_per_file: 3750000000 # 2 * 10**5 * 20 - n_files: 1 - e_min: 0.01 - e_max: 100 - gen_radius: 2500 - diff_cone: 1 - gen_gamma: 2 - offset_cut: 1. - - electron: - n_events_per_file: 450000000 # 10**5 * 20 - n_files: 1 - e_min: 0.005 - e_max: 5 - gen_radius: 1000 - diff_cone: 1 - gen_gamma: 2 - offset_cut: 1. - - # ============================================================================= - # PLEASE, COMPILE THE FOLLOWING PART DEPENDING ON THE CONTENT OF YOUR DL2 FILES - # - # some quantity are mandatory, some optional, some custom - # - # Definitions for mandatory and optional quantities come from - # the latest version of GADF. - # Custom are there only for legacy data. - # - # ============================================================================= - - column_definition: - - # MANDATORY COLUMNS - - # Event identification number - EVENT_ID: 'EVENT_ID' - # Event time - TIME: 'TIME' - # Reconstructed event Right Ascension - RA: 'RA' - # Reconstructed event Declination - DEC: 'DEC' - # Reconstructed event energy - ENERGY: 'ENERGY' - - # OPTIONAL COLUMNS - # Event quality partition - EVENT_TYPE: 'GH_MVA' - # Telescope multiplicity. Number of telescopes that have seen the event. - MULTIP: 'MULTIP' - # Reconstructed event Galactic longitude - GLON: 'GLON' - # Reconstructed event Galactic latitude - GLAT: 'GLAT' - # Reconstructed altitude - ALT: 'ALT' - # Reconstructed azimuth - AZ: 'AZ' - # ecc...to be integrated later - - - # COSTUM COLUMNS - # Observation identification number - OBS_ID: 'OBS_ID' - # True energy - TRUE_ENERGY: 'MC_ENERGY' - # True altitude - TRUE_ALT: 'MC_ALT' - # True azimuth - TRUE_AZ: 'MC_AZ' - # Column name for classification output (protopipe) - classification_output: - name: 'gammaness' # should be substituted by EVENT_TYPE - range: [0, 1] # technically always true (some algorithms could have different domains?) - angular_distance_to_the_src: 'THETA' # WARNING: for point-source simulations! diff --git a/docs/scripts/index.rst b/docs/scripts/index.rst deleted file mode 100644 index e51c5bfb3..000000000 --- a/docs/scripts/index.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. _scripts: - -======= -Scripts -======= - -Introduction -============ - -This module contains the scripts to produce DL3 data as explained in :ref:`usage`. - -At the moment there are 3 such scripts: - -- ``make_DL3.py``, the new version which is supposed to be final one at least for DL3 data based on simulations, -- ``lst_performance.py``, a script specific for LSTchain. - -Details -======= - -make_DL3 --------- - -The usage is the following, - -.. code-block:: bash - - >$ python $PYIRF/make_DL3.py -h - usage: make_DL3.py [-h] --config_file CONFIG_FILE --obs_time OBS_TIME - --pipeline PIPELINE [--debug] - - Produce DL3 data from DL2. - - optional arguments: - -h, --help show this help message and exit - --config_file CONFIG_FILE - A configuration file - --obs_time OBS_TIME An observation time written as (value.unit), e.g. - '50.h' - --pipeline PIPELINE Name of the pipeline that has produced the DL2 files. - --debug Print debugging information. - -Currently the only accepted pipeline is *EventDisplay*. -The configuration file to be used should be `config.yaml` (:ref:`resources`). - -lst_performance ---------------- - -The usage is the following, - -.. code-block:: bash - - >$ python $PYIRF/lst_performance.py -h - usage: lst_performance.py [-h] [--obs_time OBS_TIME] --dl2_gamma - DL2_GAMMA_FILENAME --dl2_proton DL2_PROTON_FILENAME - --dl2_electron DL2_ELECTRON_FILENAME - [--outdir OUTDIR] [--conf CONFIG_FILE] - - Make performance files - - optional arguments: - -h, --help show this help message and exit - --obs_time OBS_TIME Observation time in hours - --dl2_gamma DL2_GAMMA_FILENAME, -g DL2_GAMMA_FILENAME - path to the gamma dl2 file - --dl2_proton DL2_PROTON_FILENAME, -p DL2_PROTON_FILENAME - path to the proton dl2 file - --dl2_electron DL2_ELECTRON_FILENAME, -e DL2_ELECTRON_FILENAME - path to the electron dl2 file - --outdir OUTDIR, -o OUTDIR - Output directory - --conf CONFIG_FILE, -c CONFIG_FILE - Optional. Path to a config file. If none is given, the - standard performance config is used - -.. todo:: - - Add any other further information missing. - -Reference/API -------------- - -.. automodapi:: pyirf.scripts - :no-inheritance-diagram: - :include-all-objects: diff --git a/docs/sensitivity.rst b/docs/sensitivity.rst new file mode 100644 index 000000000..d1575ff57 --- /dev/null +++ b/docs/sensitivity.rst @@ -0,0 +1,11 @@ +.. _sensitivity: + +Sensitivity +=========== + + +Reference/API +------------- + +.. automodapi:: pyirf.sensitivity + :no-inheritance-diagram: diff --git a/docs/spectral.rst b/docs/spectral.rst new file mode 100644 index 000000000..1b8189382 --- /dev/null +++ b/docs/spectral.rst @@ -0,0 +1,13 @@ +.. _spectral: + +Event Weighting and Spectrum Definitions +======================================== + + +Reference/API +------------- + + +.. automodapi:: pyirf.spectral + :no-inheritance-diagram: + :include-all-objects: diff --git a/docs/usage/EventDisplay.rst b/docs/usage/EventDisplay.rst deleted file mode 100644 index eec161110..000000000 --- a/docs/usage/EventDisplay.rst +++ /dev/null @@ -1,74 +0,0 @@ -.. _EventDisplay: - -============================================= -How to build DL3 data from EventDisplay files -============================================= - -.. toctree:: - :hidden: - - ../contribute/comparison_with_EventDisplay.ipynb - -Retrieve EventDisplay data --------------------------- - -DL2 -+++ - -- hosted `here `__ -- La Palma and Paranal datasets, both 20 deg zenith 180 Azimuth -- all datasets are provided in 3 quality levels depending on direction and classification cuts - - + *cut 0*, neither cut applied - + *cut 1*, passing classification cut and not direction cut - + *cut 2*, passing both - -For details, see `documentation `__. - -IRFs -++++ - -- in ROOT format -- download `here `__ -- after unpacking the folder contains - - + 3 summary PDF performance plots for different azimuth directions - + IRFs stored under ``data/WPPhys201890925LongObs`` - -The ROOT files named -``DESY.d20180113.V3.ID0_180degNIM2LST4MST4SST4SCMST4.prod3b-paranal20degs05b-NN.S.3HB9-FD`` -are related to the DL2 data above and replicated for different observing times in seconds. - -Launch *pyirf* --------------- - -To create the DL3 data you will need to - -- copy the configuration file in your working directory, -- modify it according to your setup, -- launch the ``pyirf.scripts.make_DL3`` script. - -To produce e.g. DL3 data for 50 hours, - -``python $PYIRF/pyirf/scripts/make_DL3.py --config_file config.yml --pipeline EventDisplay --obs_time 50.h`` - -Results -------- - -Your directory tree structure containing the output data should look like this, - -:: - - . - ├── config.yml - └── irf_EventDisplay_Time50h - ├── diagnostic - ├── electron_processed.h5 - ├── gamma_processed.h5 - ├── irf.fits.gz - ├── proton_processed.h5 - └── table_best_cutoff.fits - -where the `diagnostic` folder contains some plots with further information. - -You can open the single files or check directly the results using the `Comparison with EventDisplay <../contribute/comparison_with_EventDisplay.ipynb>`__ notebook. diff --git a/docs/usage/index.rst b/docs/usage/index.rst deleted file mode 100644 index f05a05efa..000000000 --- a/docs/usage/index.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. _usage: - -Workflow and usage -================== - -.. toctree:: - :maxdepth: 2 - :hidden: - - EventDisplay - lstchain_irf - protopipe - -You should have a working installation of *pyirf* (:ref:`install`). - -For bugs and new features, please contribute to the project (:ref:`contribute`). - -How to? -------- - -In order to use *pyirf*, you need lists of events at the -DL2 level, e.g. events with a minimal number of information: - - * Direction - * True energy - * Reconstructed energy - * Score/gammaness - -In general a number of event lists are needed in order to estimate -the performance of the instruments: - - * Gamma-rays, considered as signal - * Protons, considered as a source of diffuse background - * Electrons, considered as a source of diffuse background - - At the moment we support the following pipelines: - - * LSTchain (:ref:`lstchain_irf`), - * EventDisplay (:ref:`EventDisplay`). - - .. * protopipe (:ref:`protopipe`). diff --git a/docs/usage/lstchain_irf.rst b/docs/usage/lstchain_irf.rst deleted file mode 100644 index 9f0f2feb0..000000000 --- a/docs/usage/lstchain_irf.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _lstchain_irf: - -===================================== -How to build IRFs from lstchain files -===================================== - - -Install the alpha release of pyirf (v0.1.0-alpha): -(creating a new env is optional) - -.. code-block:: bash - - PYIRF_VER=v0.1.0-alpha - wget https://raw.githubusercontent.com/cta-observatory/pyirf/$PYIRF_VER/environment.yml - conda env create -n pyirf -f environment.yml - conda activate pyirf - pip install https://github.com/cta-observatory/pyirf/archive/$PYIRF_VER.zip - - -Once you have generated DL2 files using lstchain v0.5.x for gammas, protons and electrons, you may use the script: - -.. code-block:: bash - - python pyirf/scripts/lst_performance.py -g dl2_gamma.h5 -p dl2_proton.h5 -e dl2_electron.h5 -o . - - -This will create a subdirectory with some control plots and the file irf.fits.gz containing the IRFs. - - -Authors are aware that there are numerous caveat at the moment. -If you are interested in generating IRFs, you contribution to improve pyirf is most welcome. diff --git a/docs/usage/protopipe.rst b/docs/usage/protopipe.rst deleted file mode 100644 index 084c08247..000000000 --- a/docs/usage/protopipe.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. _protopipe: - -====================================== -How to build IRFs from protopipe files -====================================== diff --git a/docs/utils.rst b/docs/utils.rst new file mode 100644 index 000000000..f0b6b6cb8 --- /dev/null +++ b/docs/utils.rst @@ -0,0 +1,11 @@ +.. _utils: + +Utility functions +================= + + +Reference/API +------------- + +.. automodapi:: pyirf.utils + :no-inheritance-diagram: diff --git a/download_test_data.sh b/download_test_data.sh new file mode 100755 index 000000000..08605c637 --- /dev/null +++ b/download_test_data.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +curl -sSfL -o data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download +unzip data.zip +rm data.zip +mv eventdisplay_dl2 data diff --git a/environment.yml b/environment.yml index 2066335e4..c6a5beba0 100644 --- a/environment.yml +++ b/environment.yml @@ -1,32 +1,26 @@ # A conda environment with all useful package for ctapipe developers -name: pyirf +name: pyirf-dev + channels: - default - - cta-observatory dependencies: - - ctapipe==0.7 + - python=3.7 - astropy - - conda-forge::nbsphinx - - cython + - numpy - ipython - - joblib - jupyter - - gammapy=0.8 - matplotlib - - nbsphinx - - numba - - numpy>=1.15.4 - - numpydoc - - pandas - - pytables - - pytest - - pyyaml - scipy - setuptools + # tests + - pytest + - pytest-cov + # docs + - numpydoc - sphinx - - sphinx-automodapi - sphinx_rtd_theme - pip - pip: - - rinohtype - - ctaplot + - nbsphinx + - sphinx_automodapi + - uproot~=3.0 diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py new file mode 100644 index 000000000..94208ce3f --- /dev/null +++ b/examples/calculate_eventdisplay_irfs.py @@ -0,0 +1,285 @@ +""" +Example for using pyirf to calculate IRFS and sensitivity from EventDisplay DL2 fits +files produced from the root output by this script: + +https://github.com/Eventdisplay/Converters/blob/master/DL2/generate_DL2_file.py +""" +import logging +import operator + +import numpy as np +from astropy import table +import astropy.units as u +from astropy.io import fits + +from pyirf.io.eventdisplay import read_eventdisplay_fits +from pyirf.binning import ( + create_bins_per_decade, + add_overflow_bins, + create_histogram_table, +) +from pyirf.cuts import calculate_percentile_cut, evaluate_binned_cut +from pyirf.sensitivity import calculate_sensitivity +from pyirf.utils import calculate_theta, calculate_source_fov_offset +from pyirf.benchmarks import energy_bias_resolution, angular_resolution + +from pyirf.spectral import ( + calculate_event_weights, + PowerLaw, + CRAB_HEGRA, + IRFDOC_PROTON_SPECTRUM, + IRFDOC_ELECTRON_SPECTRUM, +) +from pyirf.cut_optimization import optimize_gh_cut + +from pyirf.irf import ( + point_like_effective_area, + energy_dispersion, + psf_table, +) + +from pyirf.io import ( + create_aeff2d_hdu, + create_psf_table_hdu, + create_energy_dispersion_hdu, + create_rad_max_hdu, +) + + +log = logging.getLogger("pyirf") + + +T_OBS = 50 * u.hour + +# scaling between on and off region. +# Make off region 5 times larger than on region for better +# background statistics +ALPHA = 0.05 + +# gh cut used for first calculation of the binned theta cuts +INITIAL_GH_CUT = 0.0 + +particles = { + "gamma": { + "file": "data/gamma_onSource.S.3HB9-FD_ID0.eff-0.fits.gz", + "target_spectrum": CRAB_HEGRA, + }, + "proton": { + "file": "data/proton_onSource.S.3HB9-FD_ID0.eff-0.fits.gz", + "target_spectrum": IRFDOC_PROTON_SPECTRUM, + }, + "electron": { + "file": "data/electron_onSource.S.3HB9-FD_ID0.eff-0.fits.gz", + "target_spectrum": IRFDOC_ELECTRON_SPECTRUM, + }, +} + + +def get_bg_cuts(cuts, alpha): + """Rescale the cut values to enlarge the background region""" + cuts = cuts.copy() + cuts["cut"] /= np.sqrt(alpha) + return cuts + + +def main(): + logging.basicConfig(level=logging.INFO) + logging.getLogger("pyirf").setLevel(logging.DEBUG) + + for k, p in particles.items(): + log.info(f"Simulated {k.title()} Events:") + p["events"], p["simulation_info"] = read_eventdisplay_fits(p["file"]) + + p["simulated_spectrum"] = PowerLaw.from_simulation(p["simulation_info"], T_OBS) + p["events"]["weight"] = calculate_event_weights( + p["events"]["true_energy"], p["target_spectrum"], p["simulated_spectrum"] + ) + p["events"]["source_fov_offset"] = calculate_source_fov_offset(p["events"]) + # calculate theta / distance between reco and assuemd source positoin + # we handle only ON observations here, so the assumed source pos + # is the pointing position + p["events"]["theta"] = calculate_theta( + p["events"], + assumed_source_az=p["events"]["pointing_az"], + assumed_source_alt=p["events"]["pointing_alt"], + ) + log.info(p["simulation_info"]) + log.info("") + + gammas = particles["gamma"]["events"] + # background table composed of both electrons and protons + background = table.vstack( + [particles["proton"]["events"], particles["electron"]["events"]] + ) + + log.info(f"Using fixed G/H cut of {INITIAL_GH_CUT} to calculate theta cuts") + + # event display uses much finer bins for the theta cut than + # for the sensitivity + theta_bins = add_overflow_bins( + create_bins_per_decade(10 ** (-1.9) * u.TeV, 10 ** 2.3005 * u.TeV, 50,) + ) + + # theta cut is 68 percent containmente of the gammas + # for now with a fixed global, unoptimized score cut + mask_theta_cuts = gammas["gh_score"] >= INITIAL_GH_CUT + theta_cuts = calculate_percentile_cut( + gammas["theta"][mask_theta_cuts], + gammas["reco_energy"][mask_theta_cuts], + bins=theta_bins, + min_value=0.05 * u.deg, + fill_value=np.nan * u.deg, + percentile=68, + ) + + # evaluate the theta cut + gammas["selected_theta"] = evaluate_binned_cut( + gammas["theta"], gammas["reco_energy"], theta_cuts, operator.le + ) + # we make the background region larger by a factor of ALPHA, + # so the radius by sqrt(ALPHA) to get better statistics for the background + theta_cuts_bg = get_bg_cuts(theta_cuts, ALPHA) + background["selected_theta"] = evaluate_binned_cut( + background["theta"], background["reco_energy"], theta_cuts_bg, operator.le + ) + + # same bins as event display uses + sensitivity_bins = add_overflow_bins( + create_bins_per_decade( + 10 ** -1.9 * u.TeV, 10 ** 2.31 * u.TeV, bins_per_decade=5 + ) + ) + + log.info("Optimizing G/H separation cut for best sensitivity") + sensitivity_step_2, gh_cuts = optimize_gh_cut( + gammas[gammas["selected_theta"]], + background[background["selected_theta"]], + bins=sensitivity_bins, + cut_values=np.arange(-1.0, 1.005, 0.05), + op=operator.ge, + alpha=ALPHA, + ) + + # now that we have the optimized gh cuts, we recalculate the theta + # cut as 68 percent containment on the events surviving these cuts. + for tab in (gammas, background): + tab["selected_gh"] = evaluate_binned_cut( + tab["gh_score"], tab["reco_energy"], gh_cuts, operator.ge + ) + + theta_cuts_opt = calculate_percentile_cut( + gammas["theta"], + gammas["reco_energy"], + theta_bins, + fill_value=np.nan * u.deg, + percentile=68, + min_value=0.05 * u.deg, + ) + + theta_cuts_opt_bg = get_bg_cuts(theta_cuts_opt, ALPHA) + + for tab, cuts in zip([gammas, background], [theta_cuts_opt, theta_cuts_opt_bg]): + tab["selected_theta"] = evaluate_binned_cut( + tab["theta"], tab["reco_energy"], cuts, operator.le + ) + tab["selected"] = tab["selected_theta"] & tab["selected_gh"] + + signal_hist = create_histogram_table( + gammas[gammas["selected"]], bins=sensitivity_bins + ) + background_hist = create_histogram_table( + background[background["selected"]], bins=sensitivity_bins + ) + + sensitivity = calculate_sensitivity(signal_hist, background_hist, alpha=ALPHA) + + # scale relative sensitivity by Crab flux to get the flux sensitivity + for s in (sensitivity_step_2, sensitivity): + s["flux_sensitivity"] = s["relative_sensitivity"] * CRAB_HEGRA( + s["reco_energy_center"] + ) + + # write OGADF output file + hdus = [ + fits.PrimaryHDU(), + fits.BinTableHDU(sensitivity, name="SENSITIVITY"), + fits.BinTableHDU(sensitivity_step_2, name="SENSITIVITY_STEP_2"), + fits.BinTableHDU(theta_cuts, name="THETA_CUTS"), + fits.BinTableHDU(theta_cuts_opt, name="THETA_CUTS_OPT"), + fits.BinTableHDU(gh_cuts, name="GH_CUTS"), + ] + + masks = { + "": gammas["selected"], + "_NO_CUTS": slice(None), + "_ONLY_GH": gammas["selected_gh"], + "_ONLY_THETA": gammas["selected_theta"], + } + + # binnings for the irfs + true_energy_bins = add_overflow_bins( + create_bins_per_decade(10 ** -1.9 * u.TeV, 10 ** 2.31 * u.TeV, 10,) + ) + fov_offset_bins = [0, 0.5] * u.deg + source_offset_bins = np.arange(0, 1 + 1e-4, 1e-3) * u.deg + energy_migration_bins = np.geomspace(0.2, 5, 200) + + for label, mask in masks.items(): + effective_area = point_like_effective_area( + gammas[mask], + particles["gamma"]["simulation_info"], + true_energy_bins=true_energy_bins, + ) + hdus.append( + create_aeff2d_hdu( + effective_area[..., np.newaxis], # add one dimension for FOV offset + true_energy_bins, + fov_offset_bins, + extname="EFFECTIVE_AREA" + label, + ) + ) + edisp = energy_dispersion( + gammas[mask], + true_energy_bins=true_energy_bins, + fov_offset_bins=fov_offset_bins, + migration_bins=energy_migration_bins, + ) + hdus.append( + create_energy_dispersion_hdu( + edisp, + true_energy_bins=true_energy_bins, + migration_bins=energy_migration_bins, + fov_offset_bins=fov_offset_bins, + extname="ENERGY_DISPERSION" + label, + ) + ) + + bias_resolution = energy_bias_resolution( + gammas[gammas["selected"]], true_energy_bins, + ) + ang_res = angular_resolution(gammas[gammas["selected_gh"]], true_energy_bins,) + + psf = psf_table( + gammas[gammas["selected_gh"]], + true_energy_bins, + fov_offset_bins=fov_offset_bins, + source_offset_bins=source_offset_bins, + ) + + hdus.append( + create_psf_table_hdu( + psf, true_energy_bins, source_offset_bins, fov_offset_bins, + ) + ) + hdus.append( + create_rad_max_hdu( + theta_bins, fov_offset_bins, rad_max=theta_cuts_opt["cut"][:, np.newaxis] + ) + ) + hdus.append(fits.BinTableHDU(ang_res, name="ANGULAR_RESOLUTION")) + hdus.append(fits.BinTableHDU(bias_resolution, name="ENERGY_BIAS_RESOLUTION")) + fits.HDUList(hdus).writeto("pyirf_eventdisplay.fits.gz", overwrite=True) + + +if __name__ == "__main__": + main() diff --git a/examples/plot_spectra.py b/examples/plot_spectra.py new file mode 100644 index 000000000..b7fb25f2a --- /dev/null +++ b/examples/plot_spectra.py @@ -0,0 +1,82 @@ +import matplotlib.pyplot as plt +import numpy as np +import astropy.units as u +from scipy.stats import norm + +from pyirf.spectral import ( + IRFDOC_ELECTRON_SPECTRUM, + IRFDOC_PROTON_SPECTRUM, + PDG_ALL_PARTICLE, + CRAB_HEGRA, + CRAB_MAGIC_JHEAP2015, + POINT_SOURCE_FLUX_UNIT, + DIFFUSE_FLUX_UNIT, +) + + +cr_spectra = { + "PDG All Particle Spectrum": PDG_ALL_PARTICLE, + "ATIC Proton Fit (from IRF Document)": IRFDOC_PROTON_SPECTRUM, +} + + +if __name__ == "__main__": + + energy = np.geomspace(0.001, 300, 1000) * u.TeV + + plt.figure(constrained_layout=True) + plt.title("Crab Nebula Flux") + plt.plot( + energy.to_value(u.TeV), + CRAB_HEGRA(energy).to_value(POINT_SOURCE_FLUX_UNIT), + label="HEGRA", + ) + plt.plot( + energy.to_value(u.TeV), + CRAB_MAGIC_JHEAP2015(energy).to_value(POINT_SOURCE_FLUX_UNIT), + label="MAGIC JHEAP 2015", + ) + + plt.legend() + plt.xscale("log") + plt.yscale("log") + plt.xlabel("E / TeV") + plt.ylabel(f'Flux / ({POINT_SOURCE_FLUX_UNIT.to_string("latex")})') + + plt.figure(constrained_layout=True) + plt.title("Cosmic Ray Flux") + + for label, spectrum in cr_spectra.items(): + unit = energy.unit ** 2 * DIFFUSE_FLUX_UNIT + plt.plot( + energy.to_value(u.TeV), + (spectrum(energy) * energy ** 2).to_value(unit), + label=label, + ) + + plt.legend() + plt.xscale("log") + plt.yscale("log") + plt.xlabel(r"$E \,\,/\,\, \mathrm{TeV}$") + plt.ylabel(rf'$E^2 \cdot \Phi \,\,/\,\,$ ({unit.to_string("latex")})') + + energy = np.geomspace(0.006, 10, 1000) * u.TeV + plt.figure(constrained_layout=True) + plt.title("Electron Flux") + + unit = u.TeV ** 2 / u.m ** 2 / u.s / u.sr + plt.plot( + energy.to_value(u.TeV), + (energy ** 3 * IRFDOC_ELECTRON_SPECTRUM(energy)).to_value(unit), + label="IFAE 2013 (from IRF Document)", + ) + + plt.legend() + plt.xscale("log") + plt.xlim(5e-3, 10) + plt.ylim(1e-5, 0.25e-3) + plt.xlabel(r"$E \,\,/\,\, \mathrm{TeV}$") + plt.ylabel(rf'$E^3 \cdot \Phi \,\,/\,\,$ ({unit.to_string("latex")})') + plt.grid() + + plt.show() diff --git a/notebooks/comparison_with_EventDisplay.ipynb b/notebooks/comparison_with_EventDisplay.ipynb deleted file mode 100644 index 7ec9b696b..000000000 --- a/notebooks/comparison_with_EventDisplay.ipynb +++ /dev/null @@ -1,739 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Remove input cells at runtime (nbsphinx)\n", - "import IPython.core.display as d\n", - "d.display_html('', raw=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Comparison with EventDisplay" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Purpose of this notebook:**\n", - "\n", - "- Read DL2 files from _EventDisplay_ in FITS format\n", - "\n", - "- Read _pyirf_ output\n", - "\n", - "- Compare the outputs\n", - "\n", - "**Notes:**\n", - "\n", - "The following results correspond to:\n", - "\n", - "- Paranal site\n", - "- Zd 20 deg, Az 180 deg\n", - "- 50 h observation time\n", - "\n", - "**Resources:**\n", - "\n", - "_EventDisplay_ DL2 data, https://forge.in2p3.fr/projects/cta_analysis-and-simulations/wiki/Eventdisplay_Prod3b_DL2_Lists\n", - "\n", - "**TO-DOs:**\n", - "\n", - "- ..." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Table of contents\n", - "\n", - "* [IRFs](#IRFs)\n", - " - [Effective area](#Effective-area)\n", - " - [Angular resolution](#Angular-resolution)\n", - " - [Energy resolution](#Energy-resolution)\n", - " - [Background rate](#Background-rate)\n", - "* [Differential sensitivity](#Differential-sensitivity)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Imports" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import uproot\n", - "from astropy.io import fits\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import astropy.units as u\n", - "from astropy.coordinates import Angle\n", - "from gammapy.maps import MapAxis\n", - "from gammapy.irf import EffectiveAreaTable2D, EnergyDispersion2D, BgRateTable" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Definitions of classes and functions" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "If judged useful, these should be moved to pyirf!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Input data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "### _EventDisplay_" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "The input data provided by _EventDisplay_ is stored in _ROOT_ format, so _uproot_ is used to transform it into _numpy_ objects. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "tags": [ - "parameters" - ] - }, - "outputs": [], - "source": [ - "# Path of EventDisplay IRF data in the user's local setup\n", - "# Please, empty the indir_EventDisplay variable before pushing to the repo\n", - "indir_EventDisplay = \"\"\n", - "infile_EventDisplay = \"DESY.d20180113.V3.ID0_180degNIM2LST4MST4SST4SCMST4.prod3b-paranal20degs05b-NN.S.3HB9-FD.180000s.root\"\n", - "\n", - "input_EventDisplay = uproot.open(f'{indir_EventDisplay}/{infile_EventDisplay}')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Contents of the ROOT file\n", - "# input_EventDisplay.keys()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "### Setup of output data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## _pyirf_" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following is the current IRF + sensititivy output FITS format provided by this software." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "tags": [ - "parameters" - ] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Filename: /Users/michele/Applications/ctasoft/tests/pyirf/EventDisplay/from_pyirf/irf_EventDisplay_Time50h//irf.fits.gz\n", - "No. Name Ver Type Cards Dimensions Format\n", - " 0 PRIMARY 1 PrimaryHDU 5 (100,) float64 \n", - " 1 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 2 POINT SPREAD FUNCTION 1 BinTableHDU 18 21R x 3C [E, E, E] \n", - " 3 ENERGY DISPERSION 1 BinTableHDU 37 1R x 7C [60D, 60D, 300D, 300D, 2D, 2D, 36000D] \n", - " 4 BACKGROUND 1 BinTableHDU 18 21R x 3C [E, E, E] \n", - " 5 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 6 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 7 EFFECTIVE AREA 1 BinTableHDU 31 1R x 5C [42D, 42D, 2D, 2D, 84D] \n", - " 8 SENSITIVITY 1 BinTableHDU 22 21R x 5C [E, E, E, E, E] \n" - ] - } - ], - "source": [ - "# Path of the pyirf output data in the user's local setup\n", - "# Please, empty the indir_pyirf variable before pushing to the repo\n", - "indir_pyirf = \"\"\n", - "infile_pyirf = \"irf.fits.gz\"\n", - "\n", - "hdul_pyirf = fits.open(f'{indir_pyirf}/{infile_pyirf}') # will be closed at the end of the notebook\n", - "\n", - "# Contents of the FITS file\n", - "hdul_pyirf.info()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "### Setup of output data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "It is possible to extract data from pyirf FITS file with different approaches, e.g.\n", - "\n", - "- using *gammapy*, by reading the file HDUs into the appropriate IRF class like `EffectiveAreaTable2D` or `EnergyDispersion2D`\n", - "- opening the FITS file manually and reading data through the *astropy.fits* module as e.g. `BinTableHDU`\n", - "\n", - "The *gammapy* solution seems to be cleaner, but it means that we depend on this specific science tool for plotting. This is not bad per-se, but we could need a more elastic approach for now, given that e.g. we do not yet work on full-enclosure IRFs and the offset handling in *gammapy* is hard-coded in its plotting methods.\n", - "\n", - "To produce the following plots I use a mix of these two approaches as example, but it is possible to open and plot data by using consistently each of the two." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# Effective area\n", - "\n", - "# Approach using gammapy\n", - "\n", - "#aeff2D = EffectiveAreaTable2D.read(f'{indir_pyirf}/{infile_pyirf}', hdu=1)\n", - "#print(aeff2D)\n", - "#aeff=aeff2D.to_effective_area_table(offset=Angle('1d'), energy=energy * u.TeV)\n", - "#aeff.plot()\n", - "#plt.grid(which=\"both\")\n", - "#plt.yscale(\"log\")\n", - "\n", - "# Manual approach\n", - "aeff_pyirf = hdul_pyirf[1]\n", - "\n", - "aeff_pyirf_ENERG_LO = aeff_pyirf.data[0][0]\n", - "aeff_pyirf_EFFAREA = aeff_pyirf.data[0][4][1]*1.e4 # there seems to be a 10**4 missing...maybe a bug in pyirf?" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "# Angular resolution\n", - "\n", - "# At the moment the format provided by pyirf is not compatible with GADF\n", - "# https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/psf/index.html\n", - "\n", - "psf_pyirf = hdul_pyirf[2]" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/michele/Applications/anaconda3/envs/pyirf/lib/python3.7/site-packages/astropy/units/quantity.py:464: RuntimeWarning: invalid value encountered in true_divide\n", - " result = super().__array_ufunc__(function, method, *arrays, **kwargs)\n" - ] - } - ], - "source": [ - "# Energy dispersion\n", - "\n", - "# here I open manually, but I use gammapy.irf.EnergyDispersion2D to get the energy resolution\n", - "\n", - "eDisp_pyirf = hdul_pyirf[3]\n", - "\n", - "eDisp_pyirf_ENERG_LO = eDisp_pyirf.data[0][0]\n", - "eDisp_pyirf_ENERG_HI = eDisp_pyirf.data[0][1]\n", - "\n", - "edisp2d_pyirf = EnergyDispersion2D.read(f'{indir_pyirf}/{infile_pyirf}', hdu=\"ENERGY DISPERSION\")\n", - "edisp_pyirf = edisp2d_pyirf.to_energy_dispersion(offset=Angle('1d'), e_reco=eDisp_pyirf_ENERG_LO * u.TeV, e_true=eDisp_pyirf_ENERG_LO * u.TeV)\n", - "\n", - "edisp_true_pyirf = np.asarray([(eDisp_pyirf_ENERG_HI[i]-eDisp_pyirf_ENERG_LO[i])/2. for i in range(len(eDisp_pyirf_ENERG_LO))])\n", - "\n", - "resolution_pyirf = []\n", - "for e_true in edisp_true_pyirf:\n", - " resolution_pyirf.append(edisp_pyirf.get_resolution(e_true * u.TeV))\n", - "resolution_pyirf = np.asarray(resolution_pyirf)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# Background rate\n", - "\n", - "background_pyirf = hdul_pyirf[4]" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "# Differential sensitivity\n", - "\n", - "sensitivity_pyirf = hdul_pyirf[8]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Comparison" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For the moment we do not require to replicate perfectly the EventDisplay output, because this depends also on:\n", - "\n", - "- the configuration in config.yaml,\n", - "- the specific cuts optiization performed by EventDisplay (which has not yet been replicated in pyirf)\n", - "\n", - "This comparison is here to make sure that we can produce a reliable and stable output and use it to proceed with the development.\n", - "\n", - "A more detailed and complete version of this notebook will be provided with an official DL3 benchmarking." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### IRFs\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Effective area\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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LiIikgIK6CIiIimggC4iIpICCugiIiIpoIAuIiKSAgroIiIiKaCALiIikgJaKa5MWiku2asppWGlq7A8qp90tJ2gdK0Ul5z6SVrbKbZSXEMG9JyxY8f62rVr63LvtrY2Wltb63atqOeE5St2vNCxoPSoabVSrXvXs27C8qh+0tF2gtLrUTdbt25l3bp1bN68mc2bNzNkyJCKr1nOdaKeE5av2PFCx4LSo6ZV25AhQxg5ciSDBw/ulW5mBQO6tk8VERHWrVvHTjvtxKhRo9i0aRM77bRTxdd84403Sr5O1HPC8hU7XuhYUHrUtGpydzZs2MC6desYPXp05PPUhy4iImzevJlhw4ZhZvUuyoBnZgwbNozNmzeXdJ4CuoiIACiYJ0g5daGALiIiDem4446js7Mz8Ng999zD/vvvz5QpU2pcqvpRH7qIiDSk+++/v19abivR73//+3z3u98dUAFdT+giIpIIzz//PIceeiif+MQnOOiggzj99NO57777+NjHPtaT5+c//zmnnnoqAKNGjeKVV17h+eefZ//99+fTn/40hxxyCNdccw2PPvooF198MTNnzqzXr1NzCugiIpIYzzzzDBdeeCGrVq1i55135qmnnuJ3v/sdf/3rXwG45ZZbOO+88/qdt3btWv7pn/6J9vZ2rrrqKiZMmMBtt93G9ddfX+tfoW70yl1ERHrZ4aGrYEPla3w0d78NTdkw8+5xcOyc0HNGjhzJ4YcfDsA555zDN7/5Tc4991z+93//l/POO49ly5bx/e9/n66url7n7bPPPnzoQx+quMyNTAFdREQSo+/objPjvPPO48QTT2TIkCGcccYZbLdd/9C144471qqIiaWALiIivfxtylfYvgoLp3SVsQDLiy++yLJly5g0aRJ33HEHRxxxBHvuuSd77rkn1157LT//+c8rLldaqQ9dREQSY+zYsdx6660cdNBBvPrqq1xyySUAnH322ey1114ccMABdS5hcukJXUREEmPQoEHMnTu3X/rDDz/MBRdc0Cvt+eefB2C33XZjzZo1vY7df//9sS7PmkQNuTmLdlvTjkRh0rBbVFge1U862k5Qej3qZpddduF973sfAN3d3TQ1NVV8zVKv86c//YkzzjiDxx9/vFf6hz/8Yd71rnfxox/9iB122CHStYsdL3QsKD1qWhyeffZZNm7c2Cut2G5rPZPwG/EzZswYr5eHHnqorteKek5YvmLHCx0LSo+aVivVunc96yYsj+onHW0nKL0edfPUU0/1/Pz6669X5ZrlXCfqOWH5ih0vdCwoPWpaHPLrJAd4wgvERPWhi4iIpIACuoiISAoooIuIiKSAArqIiEgKKKCLiEgiNDU1cfjhhzN+/HjGjx/PnDnhS8WWoq2tjaVLl/Z8v/rqqxkxYgTjx49n33335eyzz+app57qOX7++efz+9//vuT7LFiwgM9+9rNVKXMpNA9dREQSobm5mUceeSS2+eNtbW0MHTqUcePG9aR94Qtf4F//9V+BTCD+yEc+wurVq9l999256aabeOONN2IpSxz0hC4iIiVb1N7B4XOWMPry+zh8zhIWtXfEcp/Fixczbdq0nu9tbW2ceOKJAPzsZz9j0qRJHHLIIZxxxhls2rQJyGyret1113HIIYcwbtw4fv/73/P8888zd+5cvvGNb3D44Yfz61//ut+9TjvtND760Y+SW9+ktbWVFStW0N3dzYwZMzjwwAMZN24c3/72t3uOf/7zn2fy5MkceOCB/ebPA/zkJz9h4sSJTJgwgaOOOoqXX36Zbdu2se+++/bsILdt2zbe97738corr1T0v5UCuoiIlGRRewez7l5NR2cXDnR0djHr7tUVB/Wurq5er9wXLlzI0UcfzaOPPsqbb74JwMKFC5k+fTobNmzg2muv5Re/+AUrVqzgsMMO44Ybbui51rBhw1ixYgWXXHIJX//61xk1ahQXX3wxX/jCF3jkkUc48sgjA8twyCGH9HvNvnLlSjo6OlizZg2rV6/mnHPO6Tn25ptvsnTpUr773e/yyU9+st/1jjjiCB599FHa29s588wz+drXvsagQYM455xzuO222wD4xS9+wcEHH8xuu+1W0f9+euUuIiKRTJ+3DID2FzrZ0r2t17Gurd186a5V3PH4Cyy8aFJZ1y/0yv2YY47hJz/5Caeffjr33XcfX/va11i8eDFPPfVUz1arW7ZsYdKkd+570kknAXDooYdy9913Ry6DB6ye+p73vIc//vGPXHrppRx//PG97nPWWWcBmdXsXn/9dTo7O3udu27dOqZPn85LL73Eli1bGD16NACf/OQnOfnkk/n85z/P/PnzA/d4L5We0EVEpCR9g3lYeqWmT5/OnXfeyZIlS/jABz7QE/CPPvpoVq5cycqVK3nqqae4+eabe87JLRHb1NTE22+/Hfle7e3t7L///r3Sdt11V5588klaW1v5zne+02vAW9B2r/kuvfRSPvvZz7J69WrmzZvH5s2bAdhrr73YY489WLJkCY899hjHHnts5DIWooAuIiKRLLxoEgsvmsSIlubA4yNamst+Oi8m15f93//930yfPh2AD3zgAzzyyCM8++yzALz11ls8/fTTRa+z0047FR3k9qMf/Yif/exnPU/dOa+88grbtm3jtNNO45prruHJJ5/sObZw4UIgs3nMLrvswi677NLr3I0bNzJixAgAbr311l7Hzj//fM455xymTZtWlbXhFdBFRKQkM6eOpXlw7wDUPLiJmVPHVnTdvn3ol19+OZB5yj7hhBNYvHgxJ5xwApDZYW3BggWcddZZHHTQQXzoQx8KnWJ24okncs899/QaFPeNb3yjZ9rawoULWbJkCbvvvnuv8zo6OmhtbWX8+PHMmDGDq666qufYrrvuyuTJk7n44ot7vSHIufrqqznjjDM48sgj+/WRn3TSSWzatKkqr9tBfegiIlKiUyZknjivf2At6zu72LOlmZlTx/akl6u7u5s33ngjcNrat7/97Z7R5Tkf+chHWL58eb+8zz//fM+T+GGHHUZbWxsAY8aMYdWqVT33OPLII7n66qt7zut777a2tp60FStW9MqXc9pppzF79uxe958xYwYzZswA4OSTT+bkk08O/H2ffPJJDj74YPbbb7/A46VSQBcRkZKdMmFExQF8IJszZw433nhjz0j3alBAFxERKUPuyb8cl19+eU+XQrWoD11ERCQFFNBFRAQInoMt9VFOXSigi4gIQ4YMYcOGDQrqCeDubNiwgSFDhpR0nvrQRUSEkSNHsm7dOv7617+yefPmkoNJkHKuE/WcsHzFjhc6FpQeNa3ahgwZwsiRI0s6JzEB3cz2Bz4H7AY86O431rlIIiIDxuDBg3uWJW1ra2PChAkVX7Oc60Q9JyxfseOFjgWlR01LglhfuZvZfDP7i5mt6ZN+jJmtNbNnzexyAHf/nbtfDEwDDouzXCIiImkTdx/6AuCY/AQzawK+AxwLHACcZWYHZI+dBDwMPBhzuURERFIl1oDu7r8CXu2T/EHgWXf/o7tvAX4AnJzN/2N3nwycHWe5RERE0qYefegjgBfzvq8DJppZK3AqsANwf6GTzexC4MLs17/1fZ1fQ7sAG+t4rajnhOUrdrzQsaD0oLTdgFcilDEO1aqfetZNWB7VTzraTlB6PesGVD9hafWsn30LHnH3WD/AKGBN3vczgJvyvp8LfKvMaz8Rd/mL3Pt79bxW1HPC8hU7XuhYUHqBtIavn3rWjeon2fVTrboJSq9n3ah+IqUlsu3UYx76OmCvvO8jgfV1KEelflLna0U9JyxfseOFjgWlV/N/j2qoVnnqWTdheVQ/6Wg7Ue5Va6qf6PeptYLlsWzEj42ZjQLudfcDs9+3A54G/hHoAJYDH3f335Zx7SfcXSPiE0r1k2yqn+RS3SRbUusn7mlrdwDLgLFmts7MPuXubwOfBR4AfgfcWU4wz/pelYoq8VD9JJvqJ7lUN8mWyPqJ/QldRERE4qe13EVERFJAAV1ERCQFFNBFRERSILUB3cxOMbP/NrMfmdlH610e6c3M3mNmN5vZXfUui4CZ7Whmt2bbjFZqTBi1l2RLSrxJZEAvZVOXQtx9kbtfAMwApsdY3AGnSvXzR3f/VLwlHdhKrKdTgbuybeakmhd2ACpx8yq1lxorsX4SEW8SGdApYVMXMxtnZvf2+fx93qlXZs+T6llA9epH4rOA6JsjjeSdJZm7a1jGgWwBJWxeJTW3gNLrp67xJjH7oedz919lF6TJ17OpC4CZ/QA42d1nAyf0vYaZGTAHWOzuK+It8cBSjfqR+JVST2RWcBwJrCS5f+inSon181RtSyel1I+Z/Y4ExJtGarhBm7qMKJL/UuAo4HQzuzjOgglQYv2Y2TAzmwtMMLNZcRdOehSqp7uB08zsRpK31OVAElg/ai+JUaj9JCLeJPIJvQALSCu4Ko67fxP4ZnzFkT5KrZ8NgP7Qqr3AenL3N4Hzal0Y6adQ/ai9JEOh+klEvGmkJ/S0bOqSVqqfxqB6SjbVT7Ilun4aKaAvB/Y1s9Fmtj1wJvDjOpdJ3qH6aQyqp2RT/SRbousnkQG9Bpu6SAVUP41B9ZRsqp9ka8T60eYsIiIiKZDIJ3QREREpjQK6iIhICiigi4iIpIACuoiISAoooIuIiKSAArqIiEgKKKCL1Fl2ne6V2c+fzawj7/v29S5f3MzsKDPbaGY/NrPxeb/7q2b2XPbnB4qc/6iZ/UOftMvN7Ibsjn9Pmtkr8f8mIvWleegiCWJmVwOb3P3rfdKNTHvdVpeCFWFm22UX3Cj3/KOAz7r7KX3S/5fMHu2LQs7/HLCfu1+Sl7YSuMDdl5vZEGCdu+9WbhlFGoGe0EUSyszeZ2ZrsrtsrQD2MrPOvONnmtlN2Z/3MLO7zewJM3vczD4UcL3tsk+tj5vZKjM7P5t+lJk9mD1/rZl9P++cD5jZL83sN2a22Mz2yKY/bGbXmdmvgM+a2b5m9lj22tfkymlmd5jZ8XnXW2hmx1Xwv8kVZrY8W/4vZ5MXAh8zs+2yecYCQ919ebn3EWlECugiyXYAcLO7TwA6iuT7JvA1dz8MmAbcFJDnQuAv7v5B4APAZ8xs7+yxQ4DPZO+3v5l9yMx2AP4LOM3dDwX+F7gm73o7u/uH3f0/gW8BX89e++W8PDeR3cXNzHbN3rfg6/NizOwk4N1k9qSeAEwxsw+6+5+B3wL/mM16FnBHOfcQaWSNtH2qyED0h4hPmkeRWXM6931XM2t29668PB8lE6zPzH7fBdg3+/Oj7v4S9LyuHgVsBt4P/CJ73SYyu03l/CDv54lA7sn7duDa7M9LgG+Z2TAygfZOd++O8PsE+Wj2Hkdmvw8FxgCPkwngZ5L5Y2E6cHqZ9xBpWAroIsn2Zt7P2+i9H/OQvJ8N+KC7bylyLQM+7e4P9krM9GH/LS+pm8x/GwxY5e5HEuzNAuk93N3N7Dbg48CM7L/lMuAr7n5rwLEfAteZ2URgS5I2zBCpFb1yF2kQ2QFxr2X7qwcBH8s7/Asyr8wBMLPxAZd4APh0fl+zmTUXueVTwAgz+2A2//Zm9v4CeR/PK8+ZfY7dAswENrv72iL3C/MAcL6ZvStbnr2zT/64+2vAY8A89LpdBigFdJHGchnwU+BBer/+/gxweHaw2FPABQHnzgOeAVaa2RrgRoq8pXP3v5F5dX2DmT0JtJN5tR7kn4HLzOxx4O+BjXnXWQ88TSawl83df0xm7+nHzGw1mcC9Y16WO4CD6d0VIDJgaNqaiFTMzHYE3sq+Yj8H+Ji7n5Z3bDVwsLu/EXBu4LS1KpZN09ZkQNATuohUwweAdjNbRebtwEwAM5sK/A74RlAwz/obMN7MflztQpnZAcCj9B55L5JKekIXERFJAT2hi4iIpIACuoiISAoooIuIiKSAArqIiEgKKKCLiIikgAK6iIhICvx/0pL2FNMx1SAAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"EffectiveAreaEtrue\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.allbins[3:-1]])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.allbins[3:-1]])\n", - "y = h.allvalues[3:-1]\n", - "yerr = h.allvariances[3:-1]\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(1.e3, 1.e7)\n", - "plt.xscale(\"log\")\n", - "plt.yscale(\"log\")\n", - "plt.xlabel(\"True energy [TeV]\")\n", - "plt.ylabel(\"Effective collection area [cm^2]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=None, fmt=\"o\", label=\"EventDisplay\")\n", - "plt.loglog(aeff_pyirf_ENERG_LO, aeff_pyirf_EFFAREA, drawstyle='steps-post', label=\"pyirf\")\n", - "\n", - "plt.legend(loc=4)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Angular resolution\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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1FNPVXNzsw4++GDdddddFfU8Pit6AEAolje1aM6yZu1u3ytJaom3ac6yZi1vagn9sx555BFdeOGFne9jsZimTp0qSfr973+vCRMmaPz48fr617+u1tZWSdLIkSN12223afz48aqrq9PLL7+srVu36pe//KX+6Z/+SZMmTdIf//jHXp91wQUX6IwzzlBHfpbGxkatX79e7e3tuuyyy3T88cerrq5OP//5zzvrr7vuOk2cOFHHH3+81q5d26vPBx98UCeddJLq6+t12mmn6S9/+Yv27t2r0aNH6+2335Yk7d27V0cddZTeeeedPn1XBHoAQJ90XJO/YelGte1p71bXtqddNyzd2Kdr9m1tbd1O3S9ZskSnn366nn32We3cuVOStGTJEs2YMUPvvvuufvjDH+rxxx/X+vXr1dDQoNtvv72zr2HDhmn9+vW66qqr9JOf/EQjR47UlVdeqeuvv15PP/20vvzlL6ccw/jx43udrt+wYYNaWlq0adMmNTc369JLL+2s27lzp5555hndcccduvzyy3v1d/LJJ+vZZ59VU1OTLrroIv3oRz/SgAEDdOmll+q3v/2tJOnxxx/X2LFjddBBB+X93UmcugcAhKRjJR+0PKh0p+7PPPNMPfjgg5o+fbpWrFihH/3oR3rkkUe0efNmTZo0KfHZu3drwoQJnW3OPfdcSdKJJ56oZcuWBR6Dp8gi+7nPfU6vvfaarr32Wp1zzjndPufiiy+WlMjg9+GHHyoej3dru337ds2YMUN//vOftXv3bo0aNUqSdPnll+u8887TddddpwULFmjmzJmBx5gOK3oAQJ8smTVBS2ZNUM3QqpT1NUOrtGTWhJR1fTFjxgzde++9Wrlypb7whS90/kPg9NNP14YNG7RhwwZt3rxZd999d2ebjnS5AwcO1CeffBL4s5qamnTMMcd0K/vsZz+rF154QY2NjfrFL37R7Ua7VFvrdnXttdfqmmuuUXNzs+bPn69du3ZJkg4//HAdcsghWrlypdasWaOzzjor8BjTidSKvkuue8VisZKMobW1NZTPzqefoG2yHZepPl1d0PKwvp98MT+Zy5mf7G2KNT9BywrtwAMP1I4dO9Te3p71BrRrTzlCt6x4Rbs++XQFP3ifAbr2lCO6tQ3SV0+p2px44ol6/vnndeedd2ratGnasWOHxo8fr7//+7/Xhg0b9PnP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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"AngRes\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.bins])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.bins])\n", - "y = h.values\n", - "yerr = h.variances\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(2.e-2, 1)\n", - "plt.xscale(\"log\")\n", - "plt.xlabel(\"True energy [TeV]\")\n", - "plt.ylabel(\"Angular resolution [deg]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", - "\n", - "plt.semilogy(psf_pyirf.columns[\"ENERG_LO\"].array,\n", - " psf_pyirf.columns[\"PSF68\"].array,\n", - " drawstyle='steps-post',\n", - " label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Energy resolution\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAfgAAAF7CAYAAAA+DJkJAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOzdeXgV1f3H8fdJCBA22YJIQDYJgrLJJpsEBQJ1owgCKhV3rGitioX+2rpWqLXaxQ13qxVBRYoCogJxYVGWsAmyikiggsEggUC28/tjbjAJN8kkuZO75PN6nvvk3jNnZr5hNN97Zs5irLWIiIhIZIkKdgAiIiISeErwIiIiEUgJXkREJAIpwYuIiEQgJXgREZEIpAQvIiISgTxN8MaYYcaYrcaYHcaYKX62TzTGbDTGrDPGfG6M6Vhg21TffluNMUlexikiIhJpjFfj4I0x0cA2YAiwF1gFjLPWbi5Qp5619iff+8uAX1trh/kS/UygF9AM+BhIsNbmehKsiIhIhPGyBd8L2GGt3WWtzQLeBC4vWCE/ufvUBvK/bVwOvGmtPWGt/QbY4TueiIiIuFDNw2PHA98V+LwX6F20kjHmNuAuoDpwYYF9VxbZN96bMEVERCKPlwne+Ck75XmAtfYp4CljzFXAH4Br3e5rjLkZuBmgZs2a3c8888xSg6p1bB+Qx7FazUutWxZ5eXlERVX8hkh5juN2n9LqlbS9uG3+yt2WVZZAnlvXJ/B0farG9QnmtSmtTjhfn23btv1grY3zu9Fa68kL6AMsKvB5KjC1hPpRwGF/dYFFQJ+SzpeQkGBdef9ua/8cb21enrv6Li1dujRox3G7T2n1Stpe3DZ/5W7LKksgz63rE3i6PuUrqyyR8LettDrhfH2A1baYvOjlV45VQDtjTGtjTHVgLDCvYAVjTLsCHy8GtvvezwPGGmNqGGNaA+2ALwMSVVx7yDoCR/YH5HAiIiKhyLNb9NbaHGPMJJzWdzTwkrX2K2PMgzjfOOYBk4wxg4Fs4Eec2/P46s0GNgM5wG02UD3oGyc4P3/YBvWaBeSQIiIiocbLZ/BYaxcAC4qU/anA+9+UsO+fgT8HPKj8BH9wG7RJDPjhRUREQoGnCT4k1W0KNerBD1uDHYmISEjKzs5m7969HD9+nNNOO40tW7ZU+JjlOY7bfdzUK6lOcdv8lbstC7SaNWvSvHlzYmJiXO9T9RK8MU4r/qASvIiIP3v37qVu3bq0atWKjIwM6tatW+FjHjlypMzHcbuPm3ol1Slum79yt2WBZK0lLS2NvXv30rp1a9f7Vc256M88H/asgEO7gh2JiEjIOX78OI0aNcIYfyOWpbIZY2jUqBHHjx8v035VM8H3vQOiq8OSh4MdiYhISFJyDy3luR5VM8HXPR363Aab3oF9KcGORkREKugXv/gF6enpfre99dZb9OjRg0GDBlVyVMFVNRM8OK342Ibw8QPBjkRERCpowYIF1K9fv1CZtZa8vDxefPFFHn/8cZYuXRqk6IKj6ib4mvXggsmwaynsXBLsaEREpIDdu3fTvXt3rr32Wjp37syoUaOYP38+v/zlL0/W+eijjxg5ciQArVq14ocffmD37t106NCBX//615x33nk89NBDfP7559x5551Mnjw5WL9OUFS9XvQF9bwBVj4DH98PrRMhSHMJi4iEqhpL74O0io86is3NgWhfymnaCYZPL3Wf7du38/LLL9OvXz+uv/56Nm/ezJYtWzh48CBxcXG8/PLLXHfddafst3XrVl5++WWefvppAJYuXcoDDzzAwIEDK/x7hJOqndGq1YAL/w/2r4fN7wY7GhERKaB58+b069cPgGuuuYZly5Yxfvx4Xn/9ddLT01mxYgXDhw8/Zb+WLVty/vnnV3a4Iadqt+ABOo2G5f+CxQ/B2ZdCterBjkhEJGScGPQA1QMwxjuzHGPFi/YcN8Zw3XXXcemll1KzZk1Gjx5NtWqnprHatWtXKNZIUbVb8ABR0TD4fvjxG1j7arCjERERn++++44VK1YAMHPmTPr370+zZs1o1qwZDz/8MBMmTAhugCFOCR7grMHQsj988hc4kRHsaEREBGjfvj2vvvoqnTt35tChQ9x6660AXH311bRo0YKOHTsGOcLQplv04ExfO+QBeOEiWPEUJP4u2BGJiFR5UVFRPPvss6eUf/7559x0002Fynbv3g1A48aN2bRpU6FtycnJHDlyxLM4Q5Va8Pma94AOl8Lyf0LGwWBHIyIifnTv3p0NGzZwzTXXBDuUkKcEX9BF90F2Jnz2WLAjERGp0lq1asUXX3xxSvmaNWv49NNPqVGjRhCiCi9K8AU1bgfnjYdVL8Khb4IdjYiISLkpwRc1cApEVYOlfw52JCIiIuWmBF9UvTPg/Fth41vOBDgiIiJhSAnen36/gdgGWohGRMSlMTNWMGbGimCHIQUowfsTWx8G3A07F8OuT4IdjYhIlRMdHU2/fv3o2rUrXbt2Zfr00ueuL4vk5GSWL19+8vP9999PfHw8Xbt2pV27dowcOZKvv/765PYbb7yRzZs3l/k8r7zyCpMmTQpIzGWlcfDF6XkTrHwWPr4PblrqjJUXEZFTzE1JJWVPOlm5efSbvoTJSe0Z0S2+QseMjY1l2bJlZZ7e1q3k5GTq1KlD3759T5b99re/5Z577gFg1qxZXHLJJWzatIm4uDheeOEFgLAaT68WfHFiasKg38O+FNjxcbCjEREJSXNTUpk6ZyNZuXkApKZnMnXORuampAb8XAsXLuTKK688+Tk5OZlLL70UgA8//JA+ffpw3nnnMXr0aDIynFlJW7VqxX333ceAAQPo1KkTX3/9Nbt37+bZZ5/liSeeoGvXroVa8vnGjBnDhRdeyBtvvAFAYmIiq1evJjc3lwkTJnDuuefSqVMnnnjiiZPb77zzTvr27cu5557Ll19+ecox33vvPXr37k23bt0YPHgw33//PXl5ebRr146DB535V/Ly8jjrrLP44YcfKvzvpQRfkk6joXYcrHkl2JGIiISU/Gfu9769gczs3ELbMrNzufftDRV6Jp+ZmVnoFv2sWbMYMmQIK1eu5OjRo4DTyh4zZgxpaWk8/PDDfPzxx6xdu5YePXrw+OOPnzxW48aN+eyzz7j11lt57LHHaNWqFRMnTuS3v/0t69atK9SKL6hLly6FbtMDbNiwgdTUVDZt2sTGjRsLLVd79OhRli9fztNPP831119/yvH69+/PypUrSUlJYezYsTz66KNERUVxzTXX8J///AeAjz/+mC5dutC4ceNy/9vl0y36klSrDl2vdlab+2m/08NeREROym+5uy13q7hb9MOGDeO9995j1KhRzJ8/n0cffZSFCxeyefPmk0vLZmVl0adPn5P7jBw5EnBmwZszZ47rGKy1p5S1atWKXbt2cfvtt3PxxRczdOjQk184xo0bB8AFF1zATz/9RHp6eqF99+7dy5gxY9i/fz9ZWVm0bt0agOuvv57LL7+cO++8k5deesnvGvfloRZ8ac77FdhcSHk92JGIiISMWbf0YdYtfYivH+t3e3z9WGbd0sfvtooYM2YMs2fPZsmSJfTs2fPkF4AhQ4awbt061q1bx+bNm3nxxRdP7pM/6110dDQ5OTmuz7VhwwY6dOhQqKxBgwasX7+exMREnnrqKW688caT2/wtb1vQ7bffzqRJk9i4cSMzZszg+PHjALRo0YLTTz+dJUuW8MUXX/hd4748lOBL06gttEmEtf+GvNzSaouIVCmTk9oTGxNdqCw2JprJSe09OV9iYiJr167l+eefZ8yYMQD07NmTZcuWsWPHDgCOHTvGtm3bSjxO3bp1S+ww984777BkyZKTrfJ8aWlp5OXlccUVV/DQQw+xdu3ak9tmzZoFOIvhnHbaaZx22mmF9j18+DDx8U7nw1dfLbw8+Y033sg111zDlVdeSXR04X/P8lKCd6P7BDi8B3YuCXYkIiIhZUS3eKaN7ET1aCedxNePZdrIThXuRV/0GfyUKVMApxV+ySWXsHDhQi655BLAecb+yiuvMG7cODp37sz5559/yrPzoi699FLefffdQp3s8jvdtWvXjtdff53333+fuLi4Qvvt27ePxMREunbtyoQJE5g2bdrJbQ0aNKBv375MnDix0B2EfPfffz+jR49mwIABpzxjv+yyy8jIyAjY7XnQM3h32l/8c2e7dkOCHY2ISEgZ0S2emV/uAQjYbfnc3FyOHDnid5jck08+yZNPPlmo7MILL2TVqlWn1M1fRvbIkSP06NGD5ORkABISEtiwYcPJbUlJSdx///2F9i3Yws/f78iRI4Va7QXrXXHFFYUSPsCECROYMGECAJdffjmXX3653993/fr1dOnShbPPPtvv9vJQgndDne1ERErkxfP2qmL69Ok888wzJ3vSB4pu0bulznYiIlKM5ORkevToUa59p0yZwrfffkv//v0DGpMSvFvqbCciImFECb4s1NlORKoIf2PAJXjKcz2U4MuiYGc7EZEIVbNmTdLS0pTkQ4S1lrS0NGrWrFmm/dTJrizU2U5EqoDmzZuzd+9eDh48yPHjx8ucWPwpz3Hc7uOmXkl1itvmr9xtWaDVrFmT5s2bl2kfJfiyOu9XsOzvTme7gZODHY2ISMDFxMScnEY1OTmZbt26VfiY5TmO233c1CupTnHb/JW7LQsFukVfVupsJyIiYUAJvjzU2U5EREKcEnx5qLOdiIiEOCX48sjvbLd1odPZTkREJMQowZeXZrYTEZEQpgRfXupsJyIiIUwJviLU2U5EREKUEnxFqLOdiIiEKCX4iijQ2a76ibRgRyMiInKSEnxFdb8WbC5n7F8c7EhERERO0lS1FdWwDbRJ5IzUj5zOdlHRxVadm5LKXxdtZV96Js3qxzI5qT0jusVXXqwiIlJleNqCN8YMM8ZsNcbsMMZM8bP9LmPMZmPMBmPMYmNMywLbco0x63yveV7GWWHdJ1DzxIESO9vNTUll6pyNpKZnYoHU9EymztnI3JTUyotTRESqDM9a8MaYaOApYAiwF1hljJlnrd1coFoK0MNae8wYcyvwKDDGty3TWtvVq/gCqv3FpNGA7Ddv5/5Gj/FjdKNTqqTsSScrN69QWWZ2Lve+vYGZX+45WZaenskzW1cw65Y+noctIiKRy8sWfC9gh7V2l7U2C3gTuLxgBWvtUmvtMd/HlUDZ1sILFdWq80CNuzgtL50/HJpK3dz0U6oUTe6llYuIiFSEsdZ6c2BjRgHDrLU3+j6PB3pbaycVU/9J4H/W2od9n3OAdUAOMN1aO9fPPjcDNwPExcV1nz17tie/ixsZGRnE53xL5w33c6xWPOu7PExOTJ2T2+9OPkba8VP/rRvVNPwtsVah49SpU+eUeqWd280+pdUraXtx2/yVuy2rLIE8t65P4On6VI3rE8xrU1qdcL4+gwYNWmOt7eF3o7XWkxcwGnihwOfxwL+KqXsNTgu+RoGyZr6fbYDdQNuSzpeQkGCDaenSpc6b7R9Z+2Bja5+70NrjP53c/u7avfbsPyy0LX/3/snX2X9YaN9du9f/ccpz7grWK2l7cdv8lbstqyyBPLeuT+Dp+pSvrLIE6tzBvDal1Qnn6wOstsXkRS9v0e8FWhT43BzYV7SSMWYw8H/AZdbaE/nl1tp9vp+7gGSgm4exBs5Zg2H0K7AvBd4YC1nOE4gR3eKZNrIT8fVjMUB8/VimjexU8V701nLGvg9hc2j3QxQRkcrl5TC5VUA7Y0xrIBUYC1xVsIIxphswA+dW/oEC5Q2AY9baE8aYxkA/nA544eHsi2Hkc/DOjTDrGhg3E6rVYES3+MAOi8v8EebeRvtt82Hn89AoGU4/J3DHFxGRsOVZC95amwNMAhYBW4DZ1tqvjDEPGmMu81X7K1AHeKvIcLgOwGpjzHpgKc4z+M2Ek06j4LJ/wc7F8Pb1kJsd2OOnroUZA2H7Ina1vgZq1IN3J0JOVmDPIyIiYcnTiW6stQuABUXK/lTg/eBi9lsOdPIytkpx3njIzoSFk2HurfDLGSVOhOOKtbDqBVj0e6jdBK77gD07j9Km13CYdTV89hgM+n1g4hcRkbClqWq91vtmGHw/bHwL3r/TSdDldeKIczdgwT3OUrUTP4MWPZ1tHS6BzmPh08ec1r2IiFRpSvCVof9v4YJ7nbXjP5hSviT/v03wXCJsngsX3QfjZkGthoXrDJ8OdU53btVnHw9I6CIiEp6U4CvLoN/D+bfBF886LfkdH8Phve6S/drX4IWL4EQGXPseDLgLovxcutgGcPm/4IetsPThwP8OIiISNrTYTGUxBpL+DHnZ8OVzP68hX6MexLX3vTrQMC0LDp8F9eIh+xjMvwfWvwGtB8IVL0CdJiWf56zB0P06WP6ks159S015KyJSFSnBVyZj4Bd/hYFT4ODXcHALHPjaeb9tEaS8TmeAjQ9A9bpQvRZkHHDqD7zXfQe9oQ85C9/MnQgTl0GN4MywJCIiwaMEHwy1G0HtftCqX+Hyo2mkfPQm3eJrwMGtcDgVet4AZ11UtuPXqAsjnoZXLoGP74OL/xa42EVEJCwowYeS2o04XP8c6JlY8WO16g/n/xpWPuVMvNP2woofU0REwoY62UWyi/4IjdrBfyfB8cPBjkZERCqREnwki4l1Jtc5sh8+mBrsaEREpBIpwUe65t2h/12w7j80+uGLYEcjIiKVRAm+Khj4Ozj9XNpvfRqOpgU7GhERqQRK8FVBterwy2eplpMB8+8KdjQiIlIJlOCriqad+LblaGeq2+/Da2E+EREpOyX4KmT/GUOcN9sXBTcQERHxnBJ8FZJVoxE07QTbPgx2KCIi4jEl+KqmXRJ89wVk/hjsSERExENK8FVNQhLYXNixONiRiIiIh5Tgq5r47lCrEWzXbXoRkUimBF/VREU7S8pu/wjycoMdjYiIeEQJvipqNxQyD0Hq2mBHIiIiHlGCr4raXggmSsPlREQimBJ8VVSrIbToDduU4EVEIpUSfFXVbij8bwP8tD/YkYiIiAeU4KuqhCTnp3rTi4hEJCX4qqpJR6jXXAleRCRCKcGHubkpqdydfIzWU+bTb/oS5qakutvRGEgYCruSIeeEpzGKiEjlU4IPY3NTUpk6ZyNpxy0WSE3PZOqcje6TfLuhkJUB3y73NE4REal81YIdgPg3ZsaKUuuk7EknKzevUFlmdi73vr2BmV/uOaX+re2LFLS+AKJrOLfp2w6qSLgiIhJi1IIPY0WTe2nlp6heG1oP0HA5EZEIpBZ8iJp1S59S6/SbvoTU9MxTyuPrx/rdPzk5+dSDtEuChZMhbWd5whQRkRClFnwYm5zUntiY6EJlsTHRTE4qei++BAlDnZ9qxYuIRBQl+DA2ols800Z2olFNg8FpuU8b2YkR3eLdH6RBK2jcXsPlREQijG7Rh7kR3eKpf3g7iYmJ5T9IuyHw5XNEN7slYHGJiEhwqQUvzqx2uVk0+HF9sCMREZEAUYIXOLMP1KhHw0Orgx2JiIgEiBK8QHQMtB1Eo7Q1YG2woxERkQBQghdHuyRqZB2C/20MdiQiIhIASvDiaDfE+bldw+VERCKBErw46jThp7pnwTYNlxMRiQRK8HLSoYY9YO8qOJoW7FBERKSClODlpLRGPQALOz4OdigiIlJBSvBy0pG6baF2Ez2HFxGJAErw8jMT5XS227EYcnOCHY2IiFSAErwU1m4oHE93nsWLiEjYUoKXwtoOgqhquk0vIhLmPE3wxphhxpitxpgdxpgpfrbfZYzZbIzZYIxZbIxpWWDbtcaY7b7XtV7GKQXUPM2ZulbD5UREwppnCd4YEw08BQwHOgLjjDEdi1RLAXpYazsDbwOP+vZtCNwH9AZ6AfcZYxp4FasU0W4oHPgK0r8LdiQiIlJOXrbgewE7rLW7rLVZwJvA5QUrWGuXWmuP+T6uBJr73icBH1lrD1lrfwQ+AoZ5GKsUlJDk/NQa8SIiYcvLBB8PFGwC7vWVFecGYGE595VAapwADVrBNj2HFxEJV8Z6tHqYMWY0kGStvdH3eTzQy1p7u5+61wCTgIHW2hPGmMlADWvtw77tfwSOWWv/VmS/m4GbAeLi4rrPnj3bk9/FjYyMDOrUqROU47jdp7R6Bbeftf15ztj/Icv6vU5edI1i9/VX7rassgTy3KFyfdxu0/UJ3D66PpV/nEBdm9LqhPP1GTRo0BprbQ+/G621nryAPsCiAp+nAlP91BsMbAGaFCgbB8wo8HkGMK6k8yUkJNhgWrp0adCO43af0uoV2r5jibX31bP26wUl7uuv3G1ZZQnkuUPm+rjcpusTuH10fSr/OIG6NqXVCefrA6y2xeRFL2/RrwLaGWNaG2OqA2OBeQUrGGO6+ZL3ZdbaAwU2LQKGGmMa+DrXDfWVSWVp2Q+q14WtC0uvKyIiIaeaVwe21uYYYybhJOZo4CVr7VfGmAdxvnHMA/4K1AHeMsYA7LHWXmatPWSMeQjnSwLAg9baQ17FKn5Uqw5nXeg8h8/LC3Y0IiJSRp4leABr7QJgQZGyPxV4P7iEfV8CXvIuOilVwnDY/F/43/pgRyIiImWkmeykeO2GAga2fhDsSEREpIyU4KV4tRtBi16wTc/hRUTCjRK8lCxhGOxfT/UTacGOREREykAJXkrWfjgAjdJWBzkQEREpCyV4KVnc2VC/JY3StHysiEg4UYKXkhkDCcNo8ON6yDpWen0REQkJrhK8MSbeGNPXGHNB/svrwCSEtB9GdF4WfPNpsCMRERGXSh0Hb4z5CzAG2Azk+ootoL/2VUXL/uRE16TatoXQXov6iYiEAzcT3YwA2ltrT3gdjISoatX5sUE34rYtAmud2/YiIhLS3Nyi3wXEeB2IhLYfGveCI/thv2a1ExEJB25a8MeAdcaYxcDJVry19g7PopKQc6hhd8DAtg+gWddghyMiIqVwk+DnUWQVOKl6squfBs17OqvLJU4JdjgiIlKKUhO8tfZV33KvCb6irdbabG/DkpDUfhgsfhB+2g/1zgh2NCIiUoJSn8EbYxKB7cBTwNPANg2Tq6ISnFnt2L4ouHGIiEip3HSy+xsw1Fo70Fp7AZAEPOFtWBKSmnSA+mdqdTkRkTDgJsHHWGu35n+w1m5DveqrJt+sduxKhuzMYEcjIiIlcJPgVxtjXjTGJPpezwNrvA5MQlTCMMjJ1Kx2IiIhzk2CvxX4CrgD+A3OjHYTvQxKQlir/lC9jtObXkREQpabXvQngMd9L6nqqtWAtoMgf1Y7EREJScUmeGPMbGvtlcaYjThzzxdire3saWQSuhKGw5b34H8bgh2JiIgUo6QW/G98Py+pjEAkjLQbChhfb/rewY5GRET8KPYZvLV2v+/tr6213xZ8Ab+unPAkJNWJg+Y9YJuew4uIhCo3neyG+CkbHuhAJMwkDIN9KVQ/cSjYkYiIiB8lPYO/Fael3tYYU/Bha11gmdeBSfDMTUnlr4u2si89k2b1Y5mc1J76RSu1Hw5LHqJR2mpgZBCiFBGRkpT0DP4NYCEwDSi4usgRa62abRFqbkoqU+dsJDM7F4DU9EymztnI+A7RJBas2KQjnHYmjdJWBSNMEREpRbEJ3lp7GDhsjPldkU11jDF1rLV7vA1NAm3aF5k8s3VFsdvT0zP55qcNZOXmFSrPzM7lpU25rJ9ReN/rsrsw5MiHzqx2MbGexCwiIuXj5hn8fOB938/FwC6clr1EoKLJPV+On+K1NXsTnXcCvvnM46hERKSs3Ex006ngZ2PMecAtnkUknpnaO5bExD7Fbk9OTub/VuaRmn7qPPONahpm3VJk35zzyH3kYaK3LYSEoYEOV0REKsBNC74Qa+1aoKcHsUgImJzUntiY6EJlsTHRXJHgZ32hajU41LAbbJoD6XpiIyISSkptwRtj7irwMQo4DzjoWUQSVCO6xQOc2ov+8Ha/9Xe1+RVx638Hs38F130AMTUrM1wRESlGqQkeZ1hcvhycZ/HveBOOhIIR3eJPJvp8ycn+E3xmrWbwy2fhzavgg9/Bpf+ojBBFRKQUbp7BP1AZgUgYO/ti6H8XfP44xPcAWgQ7IhGRKq+kiW7ew88iM/mstZd5EpGEpwv/APvWwvy7qdN1GhQeNS8iIpWspBb8Y5UWhYS/qGi44kWYMZBzvpoOF/0SajUMdlQiIlVWSYvNfJL/AlYAab7Xcl+ZSGG1G8OV/6bGiUPwzo2QlxvsiEREqqxSh8kZYxKB7cBTwNPANmPMBR7HJeGqeXe2t7sZdi6GT/4S7GhERKosN73o/wYMtdZuBTDGJAAzge5eBibha/8ZQ2lf6ycnwcd3h4SkYIckIlLluJnoJiY/uQNYa7cBfmY9EfExBi5+DJp2hjk3waFvgh2RiEiV4ybBrzbGvGiMSfS9XgDWeB2YhLmYWBjzGmBg1niick8EOyIRkSrFTYK/FfgKuAP4je/9RC+DkgjRoBVc8QJ8v4mEbc+ALXbUpYiIBFipCd5ae8Ja+7i1diRwA7DYWqvmmLjTbggkTqHp90th9UvBjkZEpMpw04s+2RhTzxjTEFgHvGyMedz70CRiXHAvaQ27w8LfwbzbYfcyyPO/LK2IiASGm1v0p1lrfwJGAi9ba7sDg70NSyJKVBRbOtwFna+Eje/AK7+Af3SBxQ/CwW3Bjk5EJCK5SfDVjDFnAFcC73scj0SonJg6MOJpmLwdRj4PcQnw+RPwVE+YMRBWPgMZB4IdpohIxHCT4B8EFgE7rbWrjDFtcCa+ESm76rWdlvw178BdX0PSI4CFD6bA386G10fBxrchOzPYkYqIhDU3nezestZ2ttbe6vu8y1p7hZuDG2OGGWO2GmN2GGOm+Nl+gTFmrTEmxxgzqsi2XGPMOt9rnttfSMJI3dOhz21wy6fw6y+g3x1wYAu8cwM8OwD2rw92hCIiYctNJ7sEY8xiY8wm3+fOxpg/uNgvGmd62+FAR2CcMaZjkWp7gAnAG34OkWmt7ep7aeW6SNfkbBh8P9y5Ea6aDVkZ8MJg59a9hteJiJSZm1v0zwNTgWwAa+0GYKyL/XoBO3wt/izgTeDyghWstbt9x1OXanFERTlT205cBm0vcm7dvzEGjv4Q7Gg3R/UAACAASURBVMhERMKKmwRfy1r7ZZGyHBf7xQPfFfi811fmVk1jzGpjzEpjzIgy7CeRoHYjGDcThj8Ku5bCM/1glxYxFBFxy9hSbn8aYxYCk4C3rLXn+Z6V32CtHV7KfqOBJGvtjb7P44Fe1trb/dR9BXjfWvt2gbJm1tp9vk59S4CLrLU7i+x3M3AzQFxcXPfZs2eX+gt7JSMjgzp16gTlOG73Ka1eSduL2+av3G2ZW7UzvqHj5seodSyVPWdewe5W47BRbtZJqvi5A3GsSL8+FaXrUzWuTzCvTWl1wvn6DBo0aI21toffjdbaEl9AG+Bj4BiQCnwOtHSxXx9gUYHPU4GpxdR9BRhVwrFK3G6tJSEhwQbT0qVLg3Yct/uUVq+k7cVt81futqxMTmRY+99J1t5Xz9rnL7L20Deudw3UtSnvsarE9akAXZ/ylVWWSPjbVlqdcL4+wGpbTF4s8Ra9MSYK6GGtHQzEAWdba/tba7918cViFdDOGNPaGFMd57m9q97wxpgGxpgavveNgX7AZjf7SoSqXhsu+xeMetmZHOfZAbDpnWBHJSISskpM8NbaPJzb81hrj1prj7g9sLU2x7fvImALMNta+5Ux5kFjzGUAxpiexpi9wGhghjHmK9/uHXBWsVsPLAWmW2uV4AXOHQkTP4O49vD29TDnZmdonYiIFOLmQeZHxph7gFnA0fxCa+2h0na01i4AFhQp+1OB96uA5n72Ww50chGbhIG5Kak8lHyMQx/Mp1n9WCYntWdEt7L0tyyiQUu4biEkT4fl/4QNs6BlP+hxPXS4FKrVCFzwIiJhyk2Cv97387YCZRbn2bxIieampDJ1zkYys53OnKnpmUydsxGgYkk+OgYu+iOcfyukvA5rXnYmyKnVGM4bD90nOMvViohUUaUmeGtt68oIRMLTtC8yeWbrikJl6ek/l6XsSScrt/A0B5nZudz79gZmfrnH7zFn3dLHfQC1G0P/O6HvHbBrCax6CZb9Az7/O5w1GHreALZ62X4pEZEI4H6skUg5FE3upZWXW1SUk9DPGgyHU2Htq7DmVZg5lvNrNIaoidD7FqhZL7DnFREJUUrwUiFTe8eSmFi4xZ2cnHyyrN/0JaSmn7pwTHz92LK11MvitHgY9Hu4YDJsXcixjx6j5tKHYf0bMPoVOKOLN+cVEQkhbmayEym3yUntiY2JLlQWGxPN5KT23p88OgY6XsaGLg86nfKyjzvz2696QfPbi0jEc7PYzDvGmIt9Y+JFymREt3imjexEo5oGg9NynzayU8U62JVHy74w8XNoPRDm3w1vTYDjhys3BhGRSuTmFv0zwHXAP40xbwGvWGu/9jYsiSQjusVT//B2EhMTgxtI7UbOSnXL/wmLH4T965xb9s26BTcuEREPuFkP/mNr7dXAecBunHHxy40x1xljYrwOUCSgoqKcXvfXLYDcbHhxKHzxnG7Zi0jEcXXb3RjTCGfd9huBFOAfOAn/I88iE/HSmec7t+zbDIKFk2H2eMhMD3ZUIiIB4+YZ/BzgM6AWcKm19jJr7SzrrAoXnOVzRAKhVkMY9yYMeQi2LoQZF0DqmmBHJSISEG5a8E9aaztaa6dZa/cX3GCLW6JOJFxERUG/O+C6D8DmwYtJsOaVYEclIlJhbjrZ1TfGjCxSdhjYaK094EFMIpWvRU+45VN450Z4706o2wwShgY7KhGRcnPTgr8BeAG42vd6HrgLWGaMGe9hbCKVq1ZDGPMaND3Xmdf+4LZgRyQiUm5uEnwe0MFae4W19gqgI3AC6A38zsvgRCpd9dowdiZEV4eZYyHzx2BHJCJSLm4SfCtr7fcFPh8AEnzLxWZ7E5ZIENVvAWNeh/Q98PYNkJcb7IhERMrMTYL/zBjzvjHmWmPMtcB/gU+NMbUBjSuSyNSyD1z8GOxcDB/9KdjRiIiUmZtOdrcBI4H+gAH+DbxjrbXAIA9jEwmu7hPg+69gxZNw+rnQdVywIxIRca3EBG+MiQYWWWsHA+9UTkgiISTpETiwBd77DTRuF+xoRERcK/EWvbU2FzhmjDmtkuIRCS3RMXDlv6FuU3jzaqqfSAt2RCIijmOHStzs5hn8cWCjMeZFY8w/818BCU4kHOTPeJeVwbmbpjnLzoqIBNPxw/DaL0us4ibBzwf+CHwKrCnwEqk6Tu8II5+j3pHtzu16LU4jIsFyIgP+Mxq+31RitVI72VlrXzXGxAJnWmu3Bio+kbBz9sV80+oqWm94A04/x5niVkSkMmVnOnN07F0Fo16G+4pvxbtZbOZSYB3wge9zV2PMvIAFKxJGvm15JXQcAR/fB9s/DnY4IlKV5JyAWdfA7s/hlzPgnBElVndzi/5+oBe+Me/W2nVA64rGKRKWjIERTzst+Levhx+2BzsiEakKcrPhretgx8dw2T+h85Wl7uImwedYaw8XKdMDSAmquSmp9Ju+hNZT5tNv+hLmpqRW3smr14axbzg97N8Yo+lsRcRbebkw52bYOh+G/xXO+5Wr3dwk+E3GmKuAaGNMO2PMv4DlFYlVpCLmpqQydc5GUtMzsUBqeiZT52ys3CRf/0wY+x9nOtu3roPcnMo7t4hUHXl58N9J8NUcGPIg9L7Z9a5uZrK7Hfg/nAVmZgKLgIfKFaiIC2NmrChxe8qedLJy8wqVZWbncu/bG5j55Z5T6t/aPqDh/ezM8+GSJ2DeJPjw/2D4Xzw6kYhUSdbCgrth/RuQ+Hvo95sy7e6mF/0xnAT/f+UMUSSgiib30so9dd54Z6a7lU9Bkw7O9LYiIhVlLSz6Pax+CfrdCQPvLfMhSk3wxpgE4B6gVcH61toLy3w2ERdm3dKnxO39pi8hNT3zlPL4+rF+901OTg5UaP4NeRAOfg3z74ZG7aBVP2/PJyKRK+cE/Lgb1v4bVj4NvSfC4PudDr5l5OYW/VvAs8ALgNbNlKCbnNSeqXM2kpn983+OsTHRTE7y6l58KaKrwaiX4IXBMHs83LQEGrQKTiwiEvpyc+DwHkjb6XvtgEO+94e/A+u7G3netTBsermSO7hL8DnW2mfKdXQRD4zoFg/AXxdtZV96Js3qxzI5qf3J8qCIrQ9XzYLnB8HMcXDDh1CjbvDiEZHQkZMFuz+DLe85Y9h/3A152T9vr1EPGraB5j2gy1ho2NZZ3KpZt3Ind3CX4N8zxvwaeBenox0A1tqSZ7kX8dCIbvHBTej+NGoLo1+B10c5Q1rG/Aei3AxUEZGIk50JOxY7SX3bQmfu+Op1oPUF0OESJ4k3Osv5u1E7rkKJvDhuEvy1vp+TC5RZoE3AoxEJd20vhGHTYOG9sOQhGHxfsCMSkcpy/CfY/iFsmQfbP4LsY1CzPpx9CXS4FNoMgpialRaOm170mrVOpCx63QzffwWfPw5NOgJxwY5IRLySl0uT7z+F/zwFu5IhNwvqnA5dxjlJvVV/Z1KsICg2wRtj7rXWPup7P9pa+1aBbY9Ya39fGQGKhB1j4BePOR1n/nsbdbs8DCQGOyoRCSRrnVvwH/2Jjge+cia/6nUzdLgMmvcMicdzJUUwtsD7qUW2DfMgFpHIUa06XPlvqHs65256BH7aF+yIRCRQ9qXAvy+D/1wB2Uf5quM9cMd6SPoznNk7JJI7lJzgTTHv/X0WkaJqN4ZxbxKdmwmvjYRj6pcqEtZ+3E2HzX+D5xKdx3DDH4XbVnGwyYCQSeoFlRSRLea9v88i4s/p57Dp3N/DoV3w+kinE46IhJejabBwCvyrB41/WAkD7oE71kHvW5y7dSGqpE52XYwxP+G01mN97/F9rrxugCJhLr1BF7jyVWcd55lj4eq3oXqtYIclIqWIyj0Bn/0NPv87ZGVAt/F8UWMgfS+6ItihuVJsC95aG22trWetrWutreZ7n/85OF0CRcJV++Hwyxnw7XJntrucrGBHJCIl2fExvb+4FRY/6PSEv3UFXPZPsmo0CnZkrrkZBy8igdBpFGQdhffugHdugFEvO9PcikjoyM2GpX+Gz58gu3ZLalz9H2hZ8voYoSr0egWIRLLu10LSI85EGPNud9Z6FpGKO7QLso5V7Bjp38ErF8PnT0D361h73l/DNrmDWvAila/PbXAiA5IfgRp1nJ64HkxTKVIlWEuLPXMg+d/OyJW+d0DPG6B67bId5+sFMPdWyMt1Fo869wryvF6J0mNK8CLBMPBeOPETrHjSmZ9aU9qKlF3WMZh3O213ve1MB5uVAR/9EZb9HfreDj1vLH3Rp5wTnLX9BUh+D87o6iT3Rm0rJ36PKcGL+MxNSa28FeqMgaEPO3+QPn/cacnT3ZtziUSi9O9g1tWwfwO7Wo+nzZh/Of9f7fkCPvkLfHw/LPsH9JnkzDDnz6Fd8NZ1NN+/DnrfCkMegGo1KvXX8JISvAhOci+4xnxqeiZT52wE8DbJX/y40/Fu8YM0a3czmtJWxIVvl8PsX0H2cRj3Jnv216RN/mOuM3vD+DmwdzV88qiz6NPyf9Ky6S8gs6uztDPApjkw7w6IimLTOVM5d/iU4P0+HvE0wRtjhgH/AKKBF6y104tsvwD4O9AZGGutfbvAtmuBP/g+PmytfdXLWCVyTfsik2e2riixTsqedLJyC3d4y8zO5d63NzDzyz0ny9LTnWPNuiVAHW+iomHEM5B1lIStz8G6btD1qsAcWyQSrX4JFkyGBq1gwgKIS4D9yafWa94Drp7tTCv7yaO03joT/r4Azp8IGQdgzcvOnPGjXuKHdbsq+7eoFJ71ojfGRANPAcOBjsA4Y0zHItX2ABOAN4rs2xC4D+gN9ALuM8Y08CpWkaLJvbTygIqOgVEv82P9zvDfSbBzqffnFAk3OVnw/m+dV5tBcONiJ7mXplk3GDeT1d2fgNYDnNv3a16Gfr+B6xY6i8REKC9b8L2AHdbaXQDGmDeBy4HN+RWstbt924r+FU0CPrLWHvJt/whngZuZHsYrEWpq71gSE0tucfebvoTU9MxTyuPrxxZqrScnJ5d6rHKJqcmmc6cyYNtDMPtauPFjd3+8RKqCjIPOBFF7VkC/O+GiPzl3v8pyiLpt4NLr4fvNTt+XFr08CjZ0GGu9mVbeGDMKGGatvdH3eTzQ21o7yU/dV4D382/RG2PuAWpaax/2ff4jkGmtfazIfjcDNwPExcV1nz17tie/ixsZGRnUqVMnKMdxu09p9UraXtw2f+VuyyqLm3Mv35fNK5uyyCrwVbN6FEw4tzp9m/08caPX16dRtWN0X3MPudGxrD3vr2RXr+fqOJF+fbw8lv7/KZknf9tsLtG5J8iLisGaaDD+byZnZGTQ1H7PuZseISb7J7a2v50Dp19Qrhgj9foMGjRojbW2h9+N1lpPXsBonOfu+Z/HA/8qpu4rwKgCnycDfyjw+Y/A3SWdLyEhwQbT0qVLg3Yct/uUVq+k7cVt81futqyyuD33u2v32r7TFttWv3vf9p222L67dm+5j1WefU7W2/OltQ/GWftikrXZx10dpypcH6+Opf9/ShbQv21pO6398E/WPtrW2vvq/fx6oKG1Dze1dloLZ9vfOlj798424y8drX2oibV/62htakqFYgyn6+Pmb1E+YLUtJi96eYt+L9CiwOfmgNtFsfdSuDtxcyA5IFGJFGNEt3jvesyXRYueMOJpZzrb937jdMLTRDgSrnKz4ev5dF7/OCSvBxMNCcPgzPMhL9vZnpvlexV+f3T/Xmp3GAyD/gB14oL9m1SKQI7o8TLBrwLaGWNaA6nAWMBt9+BFwCMFOtYNBaYGPkSRENVpFKTtdGa7a3QWXHBPsCMSKZsfd8OaVyHldTh6gFo1GsOg/4Nu10C9Zq4OsTk5mSaJiZ6GWZnGzCh5NA+4H9HjhmcJ3lqbY4yZhJOso4GXrLVfGWMexLmlMM8Y0xN4F2gAXGqMecBae4619pAx5iGcLwkAD1pfhzuRKmPgvZC23RnH26gtzv8mIiEsNxu2fQCrX4adS5w7T+2SoMd1rEytRuLAi4IdYcgL5IgeT8fBW2sXAAuKlP2pwPtVOLff/e37EvCSl/GJhDRj4LIn4cdv4d2J1O38MJoIR0LW1wtg/t1wZB/Ui4fEKdBtPJzmu628LzmY0YUEN/NnuB3Rk2/2xOKPpdXkREJZTE0Y+wbUacK5m/7sTM8pEkqO/wT/vQ3eHAe1GsG4N+E3G5wEf1oI9GkJM5OT2hMbU3gIYGxMNJOT2pf5WErwIqGuThxcNZvo3BMwcyycOBLsiEQcu5fBs/1g3Rsw4G64aQm0Hw7RmgW9vEZ0i2fayE7E14/F4LTcp43sVK4OwLoKIh6am5LKQ8nHOPTB/IotYNOkA1+dM5kuGx+Cd250WvVlnOhDJGCyjzt9Q1Y85UwZe90HzhzwEhCBGtGjFryIR/KHu6Qdt1h+Hu4yNyW1XMf7seF5ztrx2z6AD/8Y2GBF3Nq/Hp5LdJY67nE9TPxcyT1EqQUvUg5eDHe51c0jtl43wQ/bYeVTznSbSY/4lpoV8VhuDix7ApKnQ63GcPU70G5wsKOSEijBi3jEswVskh6BmFhnrevdn8HI5yt2PJHSpO2Ed2+BvavgnJFw8d+gVsNgRxUSAvYYzgNK8CLl4MVwl+TkZHcnj64GQx6AdkPg3Ynw4lBatrwScvurc5MElrU0S10Iy/7trHp4xYvOJEwCFJx1zlnTpSKzznlBfw1EPDI5qX2hKSeh/MNd/GrV33n+uWAyrTfOhJd3wMjnoGGbwBxfqraMA/DfSSRsXwRtL4TLn3I9A10kGDNjBenpmTyztfDjuIJlZX0M56ZhEEjqZCfikfzhLo1qmgoPdylWbH244nk2d7gbDm6DZwfA2tfAo1UipYr4egE83Qe++YTtZ93kPG+vQsndLc8ewwWIWvAiHhrRLZ76h7eT6PF82gdOv4COSdfB3Fth3iTYvggu+Yen55QIdCIDFv0e1r4KTTvByBdI3fw/2kVVvbbgrFv6kJycTGJi4VZ3wbKyPoarbFXvqolEqvot4FfzYMhDsPUDeKYvDQ6tDXZUEi6+WwUzBsDaf0O/O+HGJdDk7GBHFVBzU1LpN30JrafMp9/0JeUespovkLPOeUEteJFIEhUF/e6ANokw5ya6bHgAMldC20HQagA06RjsCCXU5ObAZ4/BJ486t+EnzIdW/YIdVcAt35fNa4v9L8Nav5zHzH/c9tB/13PouFUvehGpBGd0hpuT+fa1SbT8YY1zyx6gViM61m4PtXdA64HOUrRerTV/5H/OtLoNWjk9sCX0pO2EOTdD6mroPBZ+8SjUPC3YUZXZtC9O7QxX1Jpvs8gp8mg8v0Nc63qcsr/bW+yV9RiuPJTgRSJVTCzftBlPy8QXnUVqdn8G33xKva8/dFb9AqjTFFpfQNOsJpDWAhq0du4ClEf2cdizHHYsdpYKPbDZKY+qBg3bQlwCNG4Pce2hcQI0bgfVawfmd5WyyTrqrNP+8QPO0MpRL8G5VwQ7Kk8VTe75nA5xkfm0WglepCqo3wK6XgVdr2Ll0qUkdj7zZMJn11LOPnoQtj4JMbWd565NOjqv030/6zQ59ZjWwsGtsHOxk9S/XQY5xyG6Opx5Pgy+H+qcDj9sc3r4H9ji9M62Pw8b5LQz6RTdGE58BE07Q7Ouzl0FzbPvjf0bYM0rsGE2ZB1x7uKMeCbsV32b2jv2lM5wRXW/fwFpx08dXRJfP5apvaNK3T8cKcGLVDXGQKO2zqv7BLCWLxe8Rq+m1ml1H9gMWxdCyms/71OrMTTpAKef44yz/98G2LkUfvJ1UmrUzjlW2wud8fnFtcxzsuDQTueLwQ/b4OBWqu9eA6tecL4cgPMlo2knJ9mf0RXO6OK0+DWJT/mcyIBN7ziJfd9aqFYTzvmlc71a9PbuEU2IuSIhhte25Pqfl+Lw9iBG5h39HyNS1RnDsdpnQvfEwuUZB5xk//3mnxP/2tcg+yjUOA3aDISB9zpJvf6Z7s5VrbrzRaFJh5NFa5KTSRzQ30n4+9fBvnXOz7X/huxnffvFOkn/jC5OB8K2F0L1WoH47SPXvnVOUt/4lrNuQVwHZ7GizldCbIOAn25uSip/XbSVfemZAelsFugpYPs2i6Fjh45+Y0xOVoIXkSAL9B/REtVp4rzaJP5clpcHR/Y7t94D2aKOruY8Dji9o/MoASAv11lYZ/86ZwWzfetg/UxY9TzE1IKzBkOHyyBhaFh2DPPEsUOweS6sedX5d6sWC+eOdFrrzXt61lr/ecrWU3uol+e/T6+mgA3UMqzhQgleJEx4McynzKKiKu95bVS0rz/A2dBlrFOWm+0869/yHmx5H7bMg6gY50tIh0uJyaqCif7I9/C179/im8+cPg5NzoHhf/W11iv2X4ebHupupmz1N+1rRY5XkKuVGKsgJXiREFHaH9KyDvOBCPzDF+1L5m0SnQSWutpJbJvnwXt30JcoSH0OOlwKbS+CBi2hWo3gxuyF9D2+LznvwZ6VgHVGKvS7w7mr0axbpT5bD/SUraE+BWy4UIIXCRNVcZhPiaKioEUv5zXkIfh+E98u/Betjm2AD6b4Khmo29TpI1D/TDitxc/v67eE05oH9Vcokx92wJb/Okl9X4pTdvq5kDjV+ULTpIMnSd1ND3U3U7b6m/a1IscryPVKjFWMErxIiCjtD2l5hvlUmT98xkDTTuxufRWtEp9zkuHeL52Wbvp3kP4tfPclbJpTeJge0Kd6Q/i+P7S+wJntL6595bR+83KpnfGtM8Qw80fndewQZ23fAGn/8ZUdgmOH6HfkACQfdfaL7w6DH3CSeqO23sfpQqBXTvR8JcYqQgleJExUxWE+5db4LOdVVG6O00kwfQ8c/g7S9/Dj5mU03Zfi3OoHqN3EGerXegC0usBJooFI+McOwd5VzheNvV9C6lp6ZmXA6sLVmkbXgqNxUKshxDaEBq35/sdMmnceCGdfHJJ3HfI7rgWqA2ioTwEbLpTgRcJEVRzmE3DR1ZxJf+q3OFn0tU2m6cCB8ONu3+Q/nzk/v5rjVKh7BrQa4Mz2920NiKnp9E6PqemMKa9WE2JiCy/Rm5cLB792kvl3X9JrWzIk73O2mWhoei50GceWI3Xo0Cfp52QeW5/PP1t2yrSnO5KTad67cFmoCXQP9VCeAjZcKMGLhBEvhvkEerxxWDIGGrZ2Xuf9yknWaTth96ew+3PYlczZRw84s/0VYyAGlsc6nfpyspz5AgBqNeJYrbbU6nuT01+gWbeTEwF9n5xMh5aRN4OahAYleJEqzKvxxmHPmJ9v8/e43jfb3+v0OjvemXM/J/PnnzknIDuTb3dupVWzJs6MfFHVnETevCc0bMOmTz4hcUBisH8rqWKU4EUimL+hdwXHI5d1vLHbFbYijjEcq90C2iYWW2V3XjKtdDtZQkgVHFsjIvk03lgkcqkFLxLB/A29Kzgeuazjjd2q1Cl1pcx0faoGteBFqrDJSe2JjSm8NGtFxxvnP9dPTc/E8vNz/bkpqRWMVgJB16fqUAtepAor63jjMTNKn0s8XOYRz2/FpqZnEr9ySUS0YiPp+kjFKcGLVHGBHm8cDs/1A736WTgJh+sjgaEELyKuuXkuHwrziJfWki1rKxbCoyUbLtdHKoeewYtIQHnxXD/QvGrFzk1Jpd/0JbSeMp9+05eE5HPtcLg+EhhqwYtIQAV6XvLyKK0lW57RA6W1ZJfvy+a1xf5v+1dsRfbACoXrI5VDCV5EAs6LKXUDqTyrlfmbNKigNd9mnbKkb/5t/9b1OGXfYE4aFOrXRwJDCV5EwkIgx24XbMWmpmcSH4BWbNHkns+57a+noVL5lOBFJOR50es9vxXrTPyTWGp9f5MGFdT9/gWkHbenlMfXj2Vq76gS9y2JFgOS8lKCF5GgKu3WN7jv9Z4/z34wbn9fkRDDa1ty/d/2P1y+5Xy1GJBUhBK8iIS8cBi73bdZDB07dPT7GCE5+dQEP2bGikIL/+TTYkASKErwIhJUpd36Bve93gvOsx8Mge68Fg5fbCR0KcGLSMgrT6/3UDfrlj5+v5BUxmJAUjWoa6eIhLwR3eKZNrIT8fVjMTgJbtrIThH/HFqT0khFqAUvImGhKo7dLutiQCIFKcGLiISwQC8GJFWHp7fojTHDjDFbjTE7jDFT/GyvYYyZ5dv+hTGmla+8lTEm0xizzvd61ss4RUREIo1nLXhjTDTwFDAE2AusMsbMs9ZuLlDtBuBHa+1ZxpixwF+AMb5tO621Xb2KT0REJJJ52YLvBeyw1u6y1mYBbwKXF6lzOfCq7/3bwEXGGONhTCIiIlWClwk+HviuwOe9vjK/day1OcBhoJFvW2tjTIox5hNjzAAP4xQREYk4xtpT504OyIGNGQ0kWWtv9H0eD/Sy1t5eoM5Xvjp7fZ934rT8M4A61to0Y0x3YC5wjrX2pyLnuBm4GSAuLq777NmzPfld3MjIyKBOnTpBOY7bfUqrV9L24rb5K3dbVlkCeW5dn8DT9aka1yeY16a0OuF8fQYNGrTGWtvD70ZrrScvoA+wqMDnqcDUInUWAX1876sBP+D70lGkXjLQo6TzJSQk2GBaunRp0I7jdp/S6pW0vbht/srdllWWQJ5b1yfwdH3KV1ZZIuFvW2l1wvn6AKttMXnRy1v0q4B2xpjWxpjqwFhgXpE684Brfe9HAUustdYYE+frpIcxpg3QDtjlYawiIiIRxbNe9NbaHGPMJJxWejTwkrX2K2PMgzjfOOYBLwKvGWN2AIdwvgQAXAA8aIzJAXKBidbaQ17FKiIiEmk8nejGWrsAWFCk7E8F3h8HRvvZ7x3gHS9jExERiWSai15ERCQCKcGLiIhEICV4ERGRCKQELyIiEoGU4EVERCKQEryIiEgEUoIXERGJQErwIiIiEUgJXkREJAIpwYuIiEQgJXgREZEIpAQvIiISgZTgRUREIpASvIiISARSghcREYlASvAiIiIRSAleREQkAinBx0I0LgAACrdJREFUi4iIRCAleBERkQikBC8iIhKBlOBFREQikBK8iIhIBFKCFxERiUBK8CIiIhFICV5ERCQCKcGLiIhEICV4ERGRCKQELyIiEoGU4EVERCKQEryIiEgEUoIXERGJQErwIiIiEUgJXkREJAIpwYuIiEQgJXgREZEIpAQvIiISgZTgRUREIpASvIiISARSghcREYlASvAiIiIRSAleREQkAinBi4iIRCAleBERkQikBC8iIhKBlOBFREQikKcJ3hgzzBiz1Rizwxgzxc/2GsaYWb7tXxhjWhXYNtVXvtUYk+RlnCIiIpHGswRvjIkGngKGAx2BccaYjkWq3QD8aK09C3gC+Itv347AWOAcYBjwtO94IiIi4oKXLfhewA5r7S5rbRbwJnB5kTqXA6/63r8NXGSMMb7yN621J6y13wD/397dx8pRlXEc//4KSIstrwVCeMcQBDQWJdGqEMGmKopCaYCkCgKhQcFCYk2oEC0kBCJGkMQXipSLFetLC6UUsUCJVl6UAr0F2vJm1YAaKsU0FEuVy+Mf52zZXnbu3b3dvTu79/dJJj1zZs7MM/tkemZ25u55IW/PzMzM6tDKDn5/4MWq+ZdyXc11IuJNYCOwV51tzczMrMCOLdy2atRFnevU0xZJ04HpeXaLpKcbirC5diNdoLRjO/W2GWy9gZYXLatVX6tuPPBKHTG2QrNyM9RtOT8Dc34Gr+uG/LQzN4Ot08n5ObxwSUS0ZAImAkur5mcBs/qtsxSYmMs7kj4g9V+3er0B9vdYq46lzuOd067t1NtmsPUGWl60rFZ9QV3b8tOs3Dg/zo/z05m5Gan5aeVX9CuAwyUdKuldpJfmFvdbZzFwdi5PBR6IFPFi4Mz8lv2hpCuUR1sYazPc1cbt1NtmsPUGWl60rFZ9sz6LZmlmPM5P8zk/je1nuHXD/22DrdOV+VG+AmgJSScB1wM7AHMj4ipJV5KudhZLGg3MA44BXgXOjIh1ue1lwLnAm8AlEXHPIPt6LCKObdnB2HZxfsrN+Sk356fcypqflnbww0nS9IiY0+44rDbnp9ycn3JzfsqtrPnpmg7ezMzM3uafqjUzM+tC7uDNzMy6kDt4MzOzLjQiOnhJp0i6SdKdkia3Ox7blqTDJN0saUG7Y7FE0rsl3ZrPm2ntjse25XOmvMrU35S+g5c0V9L6/r9SN9hIddUiYlFEnA98GTijheGOOE3Kz7qIOK+1kVqDuZoCLMjnzeeHPdgRqJH8+JwZXg3mpjT9Tek7eKCHNKLcVkUj1Ul6v6Ql/aZ9qppenttZ8/TQvPxYa/VQZ66AA3h7PIi+YYxxJOuh/vzY8Oqh8dy0vb9p5W/RN0VELK8eJz7bOlIdgKRfAF+IiKuBz/XfRh6h7hrgnoh4orURjyzNyI8Nj0ZyRRrg6QCgl864Eeh4DeZnzfBGN7I1khtJaylJf9OpJ26jo819DZgETJV0QSsDM6DB/EjaS9KPgWMkzWp1cLaNolzdDpwm6UeU76c5R5Ka+fE5UwpF505p+pvS38EXqGu0ua0LIm4AbmhdONZPo/nZAPjCqz1q5ioiXgfOGe5g7B2K8uNzpv2KclOa/qZT7+BfAg6smj8A+EebYrF3cn46h3NVbs5PeZU+N53awdczUp21j/PTOZyrcnN+yqv0uSl9By9pPvAIcISklySdFxFvAheRxolfC/wqIla3M86RyvnpHM5VuTk/5dWpufFgM2ZmZl2o9HfwZmZm1jh38GZmZl3IHbyZmVkXcgdvZmbWhdzBm5mZdSF38GZmZl3IHbxZAyT1SeqV9LSkuyTt3uZ4vtnEbe0u6atDaDdb0sxmxdFqOd6/S7pS0jk5n72S/ivpqVy+pqDtOEkbJI3tV79E0hRJ0/LQoYuG52jMirmDN2vM5oiYEBHvA14FLmxzPDU7eCWNnt+7Aw138MMtD9O5va6LiG9FxC05nxNIPzN6Qp6/tFajiHgNeIA0olslnj2ADwO/iYjb8G/EW0m4gzcbukeoGiVP0jckrZD0pKQrqurPynWrJM3LdQdLWpbrl0k6KNf3SLpB0sOS1kmamuv3k7S86tuD4/Jd5phcd5ukQyStlfRD4AngQEmbquKYKqknl/eVdEeOaZWkj5KGuHxP3t61gxzTZZKelXQ/cEStD0fS3pIW5vYrJH0s18+WNFfS7/Ixzqhq80VJj+YYbqx05pI25TvuPwETJZ0k6RlJD+bPa4mkUZKel7R3bjMq302PH0pyJY3N+XhU0kpJJ+dF80k/S1pxGnB3RLwxlP2YtUxEePLkqc4J2JT/3QH4NfDpPD8ZmEMaYWoUsAQ4HjgaeBYYn9fbM/97F3B2Lp8LLMrlnrzdUcBRpPGmAb4OXFa173HV8eTyIcBbwEf6x5vLU4GeXP4lcEnV9nbL7Z+uWr/omD4EPAXsAuwKvADMrPFZ/Rz4eC4fBKzN5dnAw8DOwHhgA7ATcGT+XHbK6/0QOCuXAzg9l0eThuk8NM/PB5bk8rerjmsysLBGXLML4v1rJU95/jvAmbm8B/Bc3vfOwL+APfKy+4FPVbWbVMmnJ0/tnDp1uFizdhkjqZfUGT4O3JfrJ+dpZZ4fCxwOfABYEBGvAETEq3n5RGBKLs8jdSYViyLiLWCNpH1z3QpgrqSd8vLegvj+FhF/rOM4TgTOyjH1ARvzV83Vio5pHHBHRPwHQFLRABuTgKOkraNq7ippXC7fHRFbgC2S1gP7Ap8kXTysyG3GAOvz+n3Awlx+L7AuIv6S5+cD03N5LnAncD3pwumWQT+JYpOBz0iqfF0/GjgoIp6TdDcwRdIS0kXcsu3Yj1lLuIM3a8zmiJggaTfSHe2FpLGfBVwdETdWr5y/fq5nwIfqdbZUbwIgIpZLOh74LDBP0rUR8dMa23l9gO2OriOOakXHdAn1HdMoYGJEbO7XHrY9xj7S/0UCbo2IWTW29Ua+EKnEVVNEvCjpZUknkp6LT6sjziICTomIP9dYNh+YSboIuT3SwCNmpeJn8GZDEBEbgRnAzHxXvRQ4t/J2taT9Je1DurM7XdJeuX7PvImHefs57jTgwYH2J+lgYH1E3ATcDHwwL/pf3n+RlyUdmV+4O7WqfhnwlbztHSTtCrxGujuvKDqm5cCpksbkO/KTqe1e0mhblWOYMNAx5pim5n0gac983P09Axwm6ZA8f0a/5T8BfkYa3auPoVtKyjE5nmOqlt1PunO/gNTZm5WOO3izIYqIlcAq0nPae0nPnB+R9BSwgPScfDVwFfB7SauA7+XmM4BzJD0JfAm4eJDdfQLolbSS9FLX93P9HOBJSbcVtLuU9E3DA8A/q+ovBk7IsT4OHB0RG4CH8kt81w5wTE+QnuH3kr42/0PBvmcAx+YX9NYwyNvlEbEGuBy4N38u9wH71VhvM+lt/99KehB4GdhYtcpi0uOE7fl6HuAKYBelP51bTXp2X4mhD7iD9A7CQ9u5H7OW8HCxZtZxJI2NiE1K3/f/AHg+Iq7Ly44l/RnccQVtZ5NePvxui2KbBFwUEae0Yvtm9fIdvJl1ovPzy46rSX8BcCNAfiFuIVDrOX7FJmC6pCubHZSkaaR3Mv7d7G2bNcp38GZmZl3Id/BmZmZdyB28mZlZF3IHb2Zm1oXcwZuZmXUhd/BmZmZdyB28mZlZF/o/qD3vt5GNwNUAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"ERes\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.bins[1:]])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.bins[1:]])\n", - "y = h.values[1:]\n", - "yerr = h.variances[1:]\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(0., 0.3)\n", - "plt.xscale(\"log\")\n", - "#plt.yscale(\"log\")\n", - "plt.xlabel(\"Reconstructed energy [TeV]\")\n", - "plt.ylabel(\"Energy resolution\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", - "plt.semilogx(edisp_true_pyirf, resolution_pyirf, label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Background rate\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", - "\n", - "# Data from EventDisplay\n", - "h = input_EventDisplay[\"BGRate\"]\n", - "x = np.asarray([(10**x_bin[1]+10**x_bin[0])/2. for x_bin in h.bins])\n", - "xerr = np.asarray([(10**x_bin[1]-10**x_bin[0])/2 for x_bin in h.bins])\n", - "y = h.values\n", - "yerr = h.variances\n", - "\n", - "# Style settings\n", - "#plt.xlim(1.e-2, 2.e2)\n", - "#plt.ylim(1.e-7, 1.1)\n", - "plt.xscale(\"log\")\n", - "plt.xlabel(\"Reconstructed energy [TeV]\")\n", - "plt.ylabel(\"Background rate [s^-1]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "# Plot function\n", - "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", - "plt.loglog(background_pyirf.columns['ENERG_LO'].array,\n", - " background_pyirf.columns['BGD'].array,\n", - " drawstyle='steps-post',\n", - " label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Differential sensitivity\n", - "[back to top](#Table-of-contents)" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAt4AAAHkCAYAAAAJnSgJAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOzde7xVVb338e+PrSd8BMUL+aBgaImKN7ykoqfc5CU0iBOh5K3yxtHSDp3qpI+90lIPpmU+lqmlpGaoPGapqFkqWzOUQNwmoHgILwf05KUQtmnZ5vf8sdamxXbPteZa87bG2p/367Veseac47Ldv5iDOX9jDHN3AQAAAMjWgKI7AAAAAPQHDLwBAACAHDDwBgAAAHLAwBsAAADIAQNvAAAAIAcMvAEAAIAcbFR0B7JkZhMlTdxkk01OGzFiRNHdaci6des0YEB+/z5Ks70kdTVSNm6ZONfVuqba+bx/Z2nKs+9pt5VnvNVzPfEWLdR4S1pXVvFGrEXjXpp+GeIt2rPPPvuauw/t86S7t/xn1KhRHqq5c+cG216SuhopG7dMnOtqXVPtfN6/szTl2fe028oz3uq5nniLFmq8Ja0rq3gj1qJxL02/DPEWTdJCjxiThvlPCQAAACAwDLwBAACAHJi38JbxPTnew4YNO23WrFlFd6chXV1dGjRoUJDtJamrkbJxy8S5rtY11c7n/TtLU559T7utPOOtnuuJt2ihxlvSurKKN2ItGvfS9MsQb9HGjRv3uLvv19e5lh5499h555192bJlRXejIR0dHWpvbw+yvSR1NVI2bpk419W6ptr5vH9nacqz72m3lWe81XM98RYt1HhLWldW8UasRWuFe+k777yjlStX6u23345d9u2339bAgQPrai9umTjX1bqm2vlG+p63gQMHavjw4dp44403OG5mkQPvll7VBAAAoBWsXLlSgwcP1siRI2VmscqsXbtWgwcPrquduGXiXFfrmmrnG+l7ntxdr7/+ulauXKkddtghdjlyvAEAAJrc22+/ra222ir2oBvZMjNttdVWdb2BkBh4AwAABIFBd3Np5PfR0jneTK4stj0mhIQn1MluSetjcmUxQo03JleGpxXupZtvvrk+8IEP1FW2u7tbbW1tmZSJc93kyZM1c+ZMDRky5F3nfv7zn+uiiy7SNttso7vvvrvhfhRt+fLleuONNzY4Vm1yZeGb2+TxYQOdYtpj0f/whLqhSdL62ECnGKHGGxvohKcV7qVLly6tu+yaNWsyKxPnur6uWbdunXd3d/tHP/pRnzNnTuJ+FK2v34vYQAcAAABJPP/889pll130mc98RmPHjtWUKVN099136xOf+MT6a379619r8uTJkqTdd99dr732mp5//nntuuuu+tznPqd99tlHF1xwgR555BFNnz5dX/nKV4r6cQrBwBsAAACxLFu2TNOmTdOjjz6qzTbbTEuXLtXTTz+tV199VZL04x//WCeddFKf5T796U/riSee0Hnnnaf99ttP1157rS699NK8f4RCsZwgAABAQL5x1xItfWlNzevqyZMeve1mOm/ibjWvGzFihA4++GCtXbtWJ5xwgq644gqdeOKJuummm3TSSSfp0Ucf1Y033viucu973/t04IEHxupLK2NyZZNrhQkheZVlAlJyoU52S1ofkyuLEWq8MbkyPK1wL62cXPmtX/1Bz/yxq2ZZd4+98sYu2wzSV494f9XB+gsvvKCjjjpKS5YsUXd3tx555BFdc801+s53vqOpU6fq05/+tF544QVdcMEFkqTddttNDz/8sLq6unTMMcdo/vz56+s66qij9M1vflP77df3HEQmVwb8YXJlMe0xuTI8oU52S1ofkyuLEWq8MbkyPK1wL22GyZXPPfecS/J58+b5mjVr/NRTT/Vvf/vb7u4+YcIE33bbbX3JkiXrr99+++391Vdf9eeee8532223Deo65JBDvKOjI9W+F4HJlQAAAMjErrvuqhtuuEFjx47Vn/70J51xxhmSpOOPP14jRozQ6NGjC+5hcyPHGwAAALEMGDBAV1999bu2dH/kkUd02mmnbXDt4sWLNXjwYG299dZavHjxBuc6Ojq0du3aXPrcTBh4AwAAoGH77ruvNt10U33nO98puitNj4E3AAAAaho5cuS7nlxL0uOPP15Ab8JEjjcAAACQAwbeAAAAQA5Yx7vJtcLao3mVZa3b5EJdVzlpfazjXYxQ4411vMPTCvfSynW842pkLey4ZeJcV+uaaudZxzvgD+t4F9Me63iHJ9R1lZPWxzrexQg13ljHOzytcC9thnW8672u1jXVzrOONwAAAPqttrY2jRkzRmPGjNHBBx+siy++ONX6Ozo6NG/evPXfzz//fG233XYaM2aMdtppJ02ePFlLly5df/7UU0/d4Htc119/vc4888xU+lwvVjUBAABATZtssok6Ozsl6V3reKeho6NDgwYN0kEHHbT+2Be/+EV9+ctfliTdeuut+shHPqKnnnpKQ4cO1bXXXptq+3ngiTcAAAAacu+99+qYY45Z/72jo0MTJ06UJD3wwAMaO3as9tlnHx199NHq6uqSVFqW8LzzztOHPvQh7bHHHnrmmWf0/PPP6+qrr9Z3v/tdjRkzZoMn3z2mTp2qI444Qj3z9trb27Vw4UJ1d3frs5/9rHbffXftscce+u53v7v+/PTp03XQQQdp99131+9+97t31XnXXXfpgAMO0N57763DDjtMf/zjH7Vu3TrttNNOevXVVyVJ69at0wc+8AG99tprif97MfAGAABATW+99dYGqSa33nqrDj/8cD322GN68803JZWeSk+dOlWvvfaaLr30Ut1///1atGiR9ttvP1122WXr69p66631m9/8RmeccYa+/e1va+TIkTr99NP1xS9+UZ2dnRs89a60zz776JlnntngWGdnp1atWqXFixfrqaee0kknnbT+3Jtvvql58+bpBz/4gU4++eR31ffP//zPeuyxx/TEE0/oU5/6lC655BINGDBAJ5xwgn76059Kku6//37ttdde2nrrrRP/NyTVBAAAADVFpZqMHz9ed911l6ZMmaK7775bl1xyiR566CE988wzOvjggyVJf/vb3zR27Nj1dU2ePFlSadfL22+/PXYfvI/V+HbccUetWLFCZ511lj72sY/piCOOWH/u2GOPlSR9+MMf1po1a7R69eoNyq5cuVJTp07Vyy+/rL/97W/aYYcdJEknn3yyJk2apOnTp2vmzJkbDOaT4Ik3AABAQb5x1xJ9464lRXcjkalTp2r27Nl68MEH9cEPflCDBw+Wu2vcuHHq7OxUZ2enli5dquuuu259mfe85z2SShM2//73v8du64knntCuu+66wbEttthCTz75pNrb23XllVfq1FNPXX/OzDa4tvf3s846S2eeeaaeeuopXXPNNXr77bclSSNGjNA222yjBx98UPPnz9eRRx4Zu4/VMPAGAAAoyNKX1mjpS2uK7kYi7e3tWrRokX70ox9p6tSpkqQDDzxQ8+fP1/LlyyVJf/nLX/Tss89WrWfw4MFau3Zt5Pmf/exn+tWvfrX+KXaP1157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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(12,8))\n", - "\n", - "# Data\n", - "h = input_EventDisplay[\"DiffSens\"]\n", - "\n", - "#x = np.asarray([(x_bin[1]+x_bin[0])/2. for x_bin in h.allbins[2:-1]])\n", - "x = 10**h.edges[1:-1]\n", - "\n", - "y = h.values[1:]\n", - "#yerr = h.allvariances[2:-1]\n", - "\n", - "# Style settings\n", - "plt.xlim(1.e-2, 2.e2)\n", - "plt.ylim(3.e-16, 7.e-9)\n", - "\n", - "plt.xscale(\"log\")\n", - "plt.yscale(\"log\")\n", - "plt.xlabel(\"Reconstructed energy [TeV]\")\n", - "plt.ylabel(\"E^2 x Flux Sensitivity [erg cm^-2 s^-2]\")\n", - "plt.grid(which=\"both\")\n", - "\n", - "\n", - "# Plot function\n", - "\n", - "errdict=dict(fmt=\"o\")\n", - "\n", - "plt.bar(x,\n", - " height=y, \n", - " width=np.diff(10**h.edges[1:]), \n", - " align='edge', \n", - " xerr=np.diff(10**h.edges[1:])/2,\n", - " yerr=None,\n", - " fill=False,\n", - " linewidth=0,\n", - " label=\"EventDisplay\",\n", - " ecolor = \"blue\",\n", - " )\n", - "\n", - "plt.loglog(sensitivity_pyirf.columns['ENERG_LO'].array,\n", - " sensitivity_pyirf.columns['SENSITIVITY'].array,\n", - " drawstyle='steps-post',\n", - " label=\"pyirf\")\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "## Close FITS files" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "input_EventDisplay.close()\n", - "hdul_pyirf.close()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Edit Metadata", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "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.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/irf_maker.ipynb b/notebooks/irf_maker.ipynb deleted file mode 100644 index b0e18aa97..000000000 --- a/notebooks/irf_maker.ipynb +++ /dev/null @@ -1,1169 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# IRF Maker\n", - "Short example. \n", - "Likely to be removed or updated with the code soon." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.1.0-alpha'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import pyirf\n", - "pyirf.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import astropy.units as u" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from pyirf.perf.irf_maker import IrfMaker\n", - "from pyirf.io.io import load_config" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'general': {'output_table_name': 'table_best_cutoff'},\n", - " 'analysis': {'thsq_opt': {'type': 'r68', 'value': 0.2},\n", - " 'alpha': 0.2,\n", - " 'min_sigma': 5,\n", - " 'min_excess': 10,\n", - " 'bkg_syst': 0.05,\n", - " 'ereco_binning': {'emin': 0.05, 'emax': 50, 'nbin': 21},\n", - " 'etrue_binning': {'emin': 0.05, 'emax': 50, 'nbin': 42}},\n", - " 'particle_information': {'gamma': {'n_events_per_file': 22500000,\n", - " 'n_files': 1,\n", - " 'e_min': 0.05,\n", - " 'e_max': 50,\n", - " 'gen_radius': 1000,\n", - " 'diff_cone': 0,\n", - " 'gen_gamma': 2},\n", - " 'proton': {'n_events_per_file': 3750000000,\n", - " 'n_files': 1,\n", - " 'e_min': 0.01,\n", - " 'e_max': 100,\n", - " 'gen_radius': 2500,\n", - " 'diff_cone': 1,\n", - " 'gen_gamma': 2,\n", - " 'offset_cut': 1.0},\n", - " 'electron': {'n_events_per_file': 450000000,\n", - " 'n_files': 1,\n", - " 'e_min': 0.005,\n", - " 'e_max': 5,\n", - " 'gen_radius': 1000,\n", - " 'diff_cone': 1,\n", - " 'gen_gamma': 2,\n", - " 'offset_cut': 1.0}},\n", - " 'column_definition': {'mc_energy': 'mc_energy',\n", - " 'reco_energy': 'reco_energy',\n", - " 'classification_output': {'name': 'gammaness', 'range': [0, 1]},\n", - " 'angular_distance_to_the_src': 'xi'}}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "config = load_config('../pyirf/resources/performance.yml')\n", - "config" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### IrfMaker is taking as input the processed files from the first stage (cuts optimisation) named `particle_processed.h5`" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/irf_ThSq_r68_Time50.00h/electron_processed.h5\r\n", - "/Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/irf_ThSq_r68_Time50.00h/gamma_processed.h5\r\n", - "/Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/irf_ThSq_r68_Time50.00h/proton_processed.h5\r\n" - ] - } - ], - "source": [ - "ls /Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/irf_ThSq_r68_Time50.00h/*.h5" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "im = IrfMaker(config, {}, '/Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/irf_ThSq_r68_Time50.00h/')" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
indexevent_idgps_timeintensityinterceptkurtosisleakagelengthlog_intensitylog_mc_energy...reco_src_yreco_altreco_azreco_typegammanessxioffsetweightpass_best_cutoffpass_angular_cut
13141207.01.555673e+09581.4652419.8505493.3807180.0000000.1759362.7645241.857923...0.05357970.119559180.322399101.00.3375000.1624290.5312817.262738FalseTrue
21221504.01.555673e+09364.76442111.7653472.2711740.0000000.2042842.5620122.288086...-0.67256670.025331175.968404101.00.3446671.3780331.4545173.930064FalseFalse
41433902.01.555673e+096357.33932011.8118092.5142990.0004920.3083933.8032753.171297...-0.01698269.792865179.8994030.00.6078330.2100010.1959961.113803FalseTrue
46483904.01.555673e+091168.21906213.6136352.1464400.1893020.3336293.0675243.171297...-0.28846469.947862178.2782560.00.8264760.5918900.6894071.113803TrueFalse
73779201.01.555673e+09411.93614911.6970462.2137040.0000000.2745092.6148302.538885...-0.00855969.900853179.9490390.00.5650000.1006750.3013662.747286FalseTrue
..................................................................
167894117480932996702.01.555695e+092913.63467411.3916862.0990600.0166410.3802473.4644352.761204...-0.03274170.080291179.8033670.00.7880000.1046540.4850452.000167TrueTrue
167894517480972996706.01.555695e+09372.67494913.0688591.6340830.1475860.3055742.5713302.761204...0.11651170.285010180.706716101.00.3852380.3726320.7266112.000167FalseTrue
167894817481002996708.01.555695e+091423.23991212.2146072.0202850.0936570.4494933.1532782.761204...0.00343069.831259180.0203630.00.9045000.1688860.2313632.000167TrueTrue
167895217481052997309.01.555695e+092010.78771311.0085533.0276790.0000000.3344823.3033662.466273...-0.02270270.011168179.8641140.00.5491670.0477880.4138323.047359FalseTrue
167897817481312999109.01.555695e+09300.1716159.8929702.3175920.0000000.1561352.4773701.627574...-0.09043669.942744179.460416101.00.2008330.1934690.39022810.090605FalseTrue
\n", - "

90571 rows × 60 columns

\n", - "
" - ], - "text/plain": [ - " index event_id gps_time intensity intercept kurtosis \\\n", - "13 14 1207.0 1.555673e+09 581.465241 9.850549 3.380718 \n", - "21 22 1504.0 1.555673e+09 364.764421 11.765347 2.271174 \n", - "41 43 3902.0 1.555673e+09 6357.339320 11.811809 2.514299 \n", - "46 48 3904.0 1.555673e+09 1168.219062 13.613635 2.146440 \n", - "73 77 9201.0 1.555673e+09 411.936149 11.697046 2.213704 \n", - "... ... ... ... ... ... ... \n", - "1678941 1748093 2996702.0 1.555695e+09 2913.634674 11.391686 2.099060 \n", - "1678945 1748097 2996706.0 1.555695e+09 372.674949 13.068859 1.634083 \n", - "1678948 1748100 2996708.0 1.555695e+09 1423.239912 12.214607 2.020285 \n", - "1678952 1748105 2997309.0 1.555695e+09 2010.787713 11.008553 3.027679 \n", - "1678978 1748131 2999109.0 1.555695e+09 300.171615 9.892970 2.317592 \n", - "\n", - " leakage length log_intensity log_mc_energy ... reco_src_y \\\n", - "13 0.000000 0.175936 2.764524 1.857923 ... 0.053579 \n", - "21 0.000000 0.204284 2.562012 2.288086 ... -0.672566 \n", - "41 0.000492 0.308393 3.803275 3.171297 ... -0.016982 \n", - "46 0.189302 0.333629 3.067524 3.171297 ... -0.288464 \n", - "73 0.000000 0.274509 2.614830 2.538885 ... -0.008559 \n", - "... ... ... ... ... ... ... \n", - "1678941 0.016641 0.380247 3.464435 2.761204 ... -0.032741 \n", - "1678945 0.147586 0.305574 2.571330 2.761204 ... 0.116511 \n", - "1678948 0.093657 0.449493 3.153278 2.761204 ... 0.003430 \n", - "1678952 0.000000 0.334482 3.303366 2.466273 ... -0.022702 \n", - "1678978 0.000000 0.156135 2.477370 1.627574 ... -0.090436 \n", - "\n", - " reco_alt reco_az reco_type gammaness xi offset \\\n", - "13 70.119559 180.322399 101.0 0.337500 0.162429 0.531281 \n", - "21 70.025331 175.968404 101.0 0.344667 1.378033 1.454517 \n", - "41 69.792865 179.899403 0.0 0.607833 0.210001 0.195996 \n", - "46 69.947862 178.278256 0.0 0.826476 0.591890 0.689407 \n", - "73 69.900853 179.949039 0.0 0.565000 0.100675 0.301366 \n", - "... ... ... ... ... ... ... \n", - "1678941 70.080291 179.803367 0.0 0.788000 0.104654 0.485045 \n", - "1678945 70.285010 180.706716 101.0 0.385238 0.372632 0.726611 \n", - "1678948 69.831259 180.020363 0.0 0.904500 0.168886 0.231363 \n", - "1678952 70.011168 179.864114 0.0 0.549167 0.047788 0.413832 \n", - "1678978 69.942744 179.460416 101.0 0.200833 0.193469 0.390228 \n", - "\n", - " weight pass_best_cutoff pass_angular_cut \n", - "13 7.262738 False True \n", - "21 3.930064 False False \n", - "41 1.113803 False True \n", - "46 1.113803 True False \n", - "73 2.747286 False True \n", - "... ... ... ... \n", - "1678941 2.000167 True True \n", - "1678945 2.000167 False True \n", - "1678948 2.000167 True True \n", - "1678952 3.047359 False True \n", - "1678978 10.090605 False True \n", - "\n", - "[90571 rows x 60 columns]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "im.evt_dict['gamma']" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "FITS_rec([( 0.05 , 0.05893843, 412.04828),\n", - " ( 0.05893843, 0.06947477, 727.4802 ),\n", - " ( 0.06947477, 0.08189469, 1217.9238 ),\n", - " ( 0.08189469, 0.09653489, 1932.7802 ),\n", - " ( 0.09653489, 0.11379229, 2722.2407 ),\n", - " ( 0.11379229, 0.13413478, 3602.4155 ),\n", - " ( 0.13413478, 0.15811388, 4957.029 ),\n", - " ( 0.15811388, 0.18637969, 6320.1855 ),\n", - " ( 0.18637969, 0.21969853, 7422.6084 ),\n", - " ( 0.21969853, 0.25897375, 8935.44 ),\n", - " ( 0.25897375, 0.3052701 , 11075.894 ),\n", - " ( 0.3052701 , 0.35984284, 13185.071 ),\n", - " ( 0.35984284, 0.42417145, 15277.375 ),\n", - " ( 0.42417145, 0.5 , 17797.82 ),\n", - " ( 0.5 , 0.5893843 , 18036.31 ),\n", - " ( 0.5893843 , 0.69474775, 18409.26 ),\n", - " ( 0.69474775, 0.81894684, 18824.783 ),\n", - " ( 0.81894684, 0.96534884, 18996.383 ),\n", - " ( 0.96534884, 1.137923 , 19444.576 ),\n", - " ( 1.137923 , 1.3413479 , 18734.236 ),\n", - " ( 1.3413479 , 1.5811388 , 17839.383 ),\n", - " ( 1.5811388 , 1.8637968 , 18090.912 ),\n", - " ( 1.8637968 , 2.1969852 , 18239.389 ),\n", - " ( 2.1969852 , 2.5897374 , 17781.97 ),\n", - " ( 2.5897374 , 3.0527012 , 17245.047 ),\n", - " ( 3.0527012 , 3.5984282 , 15498.636 ),\n", - " ( 3.5984282 , 4.2417145 , 12973.8545 ),\n", - " ( 4.2417145 , 5. , 10689.615 ),\n", - " ( 5. , 5.893843 , 9749.356 ),\n", - " ( 5.893843 , 6.9474773 , 6721.8735 ),\n", - " ( 6.9474773 , 8.189468 , 4345.1636 ),\n", - " ( 8.189468 , 9.653489 , 4067.425 ),\n", - " ( 9.653489 , 11.37923 , 2486.0647 ),\n", - " (11.37923 , 13.413479 , 1465.2474 ),\n", - " (13.413479 , 15.811388 , 1233.7056 ),\n", - " (15.811388 , 18.637968 , 0. ),\n", - " (18.637968 , 21.969852 , 342.84567),\n", - " (21.969852 , 25.897373 , 404.1357 ),\n", - " (25.897373 , 30.527012 , 0. ),\n", - " (30.527012 , 35.984283 , 0. ),\n", - " (35.984283 , 42.417145 , 0. ),\n", - " (42.417145 , 50. , 0. )],\n", - " dtype=(numpy.record, [('ENERG_LO', ' 0']):\n", - " \"\"\"\n", - " read DL2 data from lstchain file and update it to be compliant with irf Maker\n", - " \"\"\"\n", - " dl2_params_lstcam_key = 'dl2/event/telescope/parameters/LST_LSTCam' # lstchain DL2 files\n", - " data = pd.read_hdf(filepath, key=dl2_params_lstcam_key)\n", - " data = deepcopy(data.query(f'tel_id == {tel_id}'))\n", - " for filter in filters:\n", - " data = deepcopy(data.query(filter))\n", - "\n", - " # angles are in degrees in protopipe\n", - " data['xi'] = pd.Series(angular_separation(data.reco_az.values * u.rad,\n", - " data.reco_alt.values * u.rad,\n", - " data.mc_az.values * u.rad,\n", - " data.mc_alt.values * u.rad,\n", - " ).to(u.deg).value,\n", - " index=data.index)\n", - "\n", - " data['offset'] = pd.Series(angular_separation(data.reco_az.values * u.rad,\n", - " data.reco_alt.values * u.rad,\n", - " data.mc_az_tel.values * u.rad,\n", - " data.mc_alt_tel.values * u.rad,\n", - " ).to(u.deg).value,\n", - " index=data.index)\n", - "\n", - " for key in ['mc_alt', 'mc_az', 'reco_alt', 'reco_az', 'mc_alt_tel', 'mc_az_tel']:\n", - " data[key] = np.rad2deg(data[key])\n", - "\n", - " return data" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "gamma = read_and_update_dl2('/Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/lstchain/dl2_dl1_gamma_south_pointing_20200229_v0.4.4_TV01_DL1_testing.h5')\n", - "\n", - "proton = read_and_update_dl2('/Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/lstchain/dl2_dl1_proton_south_pointing_20200229_v0.4.4_TV01_DL1_testing.h5')\n", - "\n", - "electron = read_and_update_dl2('/Users/thomasvuillaume/Work/CTA/Data/DL2/20200229_v0.4.4/lstchain/dl2_dl1_electron_south_pointing_20200229_v0.4.4_TV01_DL1_testing.h5')" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "im.evt_dict = dict(gamma=gamma,\n", - " proton=proton,\n", - " electron=electron\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
indexevent_idgps_timeintensityinterceptkurtosisleakagelengthlog_intensitylog_mc_energy...reco_disp_dxreco_disp_dyreco_src_xreco_src_yreco_altreco_azreco_typegammanessxioffset
00100.01.555673e+09191.06518110.4221481.4665710.2986120.2573862.2811822.604273...-0.3552830.5288900.334092-0.08663570.282887179.4745730.00.5457140.3344880.706256
33202.01.555673e+0953.9787677.5404962.8465340.0000000.0851961.7322231.194575...-0.081579-0.0320790.171339-0.26539769.943576178.416292101.00.3116110.5453060.646416
771100.01.555673e+0956.96060123.1553501.5946910.0000000.1351091.7555751.202576...-0.0327990.412595-0.5635600.97998668.357472185.443193101.00.1466672.5364002.313148
11111106.01.555673e+0977.4510778.6182422.4509590.0000000.0892801.8890271.202576...-0.026633-0.049027-0.180290-0.04375869.230895179.747499101.00.3975480.7741150.379634
13141207.01.555673e+09581.4652419.8505493.3807180.0000000.1759362.7645241.857923...-0.240113-0.1652550.2540440.05357970.119559180.322399101.00.3375000.1624290.531281
..................................................................
167897217481252999009.01.555695e+0985.0214757.3119961.7270300.0000000.1513221.9295291.258711...-0.0470850.1524470.397815-0.10938270.412822179.332403101.00.2880000.4706640.844249
167897417481272999108.01.555695e+09173.4392419.8202081.9749140.0000000.1622592.2391471.627574...0.1726340.0695750.6042400.06817670.835962180.424882101.00.4006670.8479991.244288
167897817481312999109.01.555695e+09300.1716159.8929702.3175920.0000000.1561352.4773701.627574...-0.322908-0.0162190.167894-0.09043669.942744179.460416101.00.2008330.1934690.390228
167898217481352999300.01.555695e+09105.76897910.2574362.3991090.0000000.2184102.0243582.055422...0.342928-0.410768-0.1544990.24758969.277922181.4319520.00.5703330.8772320.597181
167898617481392999608.01.555695e+09161.6280259.5757762.6099860.0000000.1429362.2085171.467662...0.0176250.0953660.2010160.20888070.006967181.250192101.00.2298330.4275670.593201
\n", - "

419324 rows × 57 columns

\n", - "
" - ], - "text/plain": [ - " index event_id gps_time intensity intercept kurtosis \\\n", - "0 0 100.0 1.555673e+09 191.065181 10.422148 1.466571 \n", - "3 3 202.0 1.555673e+09 53.978767 7.540496 2.846534 \n", - "7 7 1100.0 1.555673e+09 56.960601 23.155350 1.594691 \n", - "11 11 1106.0 1.555673e+09 77.451077 8.618242 2.450959 \n", - "13 14 1207.0 1.555673e+09 581.465241 9.850549 3.380718 \n", - "... ... ... ... ... ... ... \n", - "1678972 1748125 2999009.0 1.555695e+09 85.021475 7.311996 1.727030 \n", - "1678974 1748127 2999108.0 1.555695e+09 173.439241 9.820208 1.974914 \n", - "1678978 1748131 2999109.0 1.555695e+09 300.171615 9.892970 2.317592 \n", - "1678982 1748135 2999300.0 1.555695e+09 105.768979 10.257436 2.399109 \n", - "1678986 1748139 2999608.0 1.555695e+09 161.628025 9.575776 2.609986 \n", - "\n", - " leakage length log_intensity log_mc_energy ... reco_disp_dx \\\n", - "0 0.298612 0.257386 2.281182 2.604273 ... -0.355283 \n", - "3 0.000000 0.085196 1.732223 1.194575 ... -0.081579 \n", - "7 0.000000 0.135109 1.755575 1.202576 ... -0.032799 \n", - "11 0.000000 0.089280 1.889027 1.202576 ... -0.026633 \n", - "13 0.000000 0.175936 2.764524 1.857923 ... -0.240113 \n", - "... ... ... ... ... ... ... \n", - "1678972 0.000000 0.151322 1.929529 1.258711 ... -0.047085 \n", - "1678974 0.000000 0.162259 2.239147 1.627574 ... 0.172634 \n", - "1678978 0.000000 0.156135 2.477370 1.627574 ... -0.322908 \n", - "1678982 0.000000 0.218410 2.024358 2.055422 ... 0.342928 \n", - "1678986 0.000000 0.142936 2.208517 1.467662 ... 0.017625 \n", - "\n", - " reco_disp_dy reco_src_x reco_src_y reco_alt reco_az \\\n", - "0 0.528890 0.334092 -0.086635 70.282887 179.474573 \n", - "3 -0.032079 0.171339 -0.265397 69.943576 178.416292 \n", - "7 0.412595 -0.563560 0.979986 68.357472 185.443193 \n", - "11 -0.049027 -0.180290 -0.043758 69.230895 179.747499 \n", - "13 -0.165255 0.254044 0.053579 70.119559 180.322399 \n", - "... ... ... ... ... ... \n", - "1678972 0.152447 0.397815 -0.109382 70.412822 179.332403 \n", - "1678974 0.069575 0.604240 0.068176 70.835962 180.424882 \n", - "1678978 -0.016219 0.167894 -0.090436 69.942744 179.460416 \n", - "1678982 -0.410768 -0.154499 0.247589 69.277922 181.431952 \n", - "1678986 0.095366 0.201016 0.208880 70.006967 181.250192 \n", - "\n", - " reco_type gammaness xi offset \n", - "0 0.0 0.545714 0.334488 0.706256 \n", - "3 101.0 0.311611 0.545306 0.646416 \n", - "7 101.0 0.146667 2.536400 2.313148 \n", - "11 101.0 0.397548 0.774115 0.379634 \n", - "13 101.0 0.337500 0.162429 0.531281 \n", - "... ... ... ... ... \n", - "1678972 101.0 0.288000 0.470664 0.844249 \n", - "1678974 101.0 0.400667 0.847999 1.244288 \n", - "1678978 101.0 0.200833 0.193469 0.390228 \n", - "1678982 0.0 0.570333 0.877232 0.597181 \n", - "1678986 101.0 0.229833 0.427567 0.593201 \n", - "\n", - "[419324 rows x 57 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "im.evt_dict['gamma']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### apply your own cuts" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "for particle, data in im.evt_dict.items():\n", - " data['pass_best_cutoff'] = data['gammaness'] > 0.8\n", - " data['pass_angular_cut'] = data['xi'] < 0.3\n", - " data['weight'] = np.ones(len(data))" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "FITS_rec([( 0.05 , 0.05893843, 0.0000000e+00),\n", - " ( 0.05893843, 0.06947477, 2.1683464e+00),\n", - " ( 0.06947477, 0.08189469, 8.9459257e+00),\n", - " ( 0.08189469, 0.09653489, 2.5609715e+01),\n", - " ( 0.09653489, 0.11379229, 9.0563782e+01),\n", - " ( 0.11379229, 0.13413478, 2.1769391e+02),\n", - " ( 0.13413478, 0.15811388, 6.0204834e+02),\n", - " ( 0.15811388, 0.18637969, 1.2273899e+03),\n", - " ( 0.18637969, 0.21969853, 2.5644856e+03),\n", - " ( 0.21969853, 0.25897375, 3.9645713e+03),\n", - " ( 0.25897375, 0.3052701 , 6.0214751e+03),\n", - " ( 0.3052701 , 0.35984284, 8.1648608e+03),\n", - " ( 0.35984284, 0.42417145, 9.9885439e+03),\n", - " ( 0.42417145, 0.5 , 1.1836604e+04),\n", - " ( 0.5 , 0.5893843 , 1.2848916e+04),\n", - " ( 0.5893843 , 0.69474775, 1.3974992e+04),\n", - " ( 0.69474775, 0.81894684, 1.4735217e+04),\n", - " ( 0.81894684, 0.96534884, 1.6224507e+04),\n", - " ( 0.96534884, 1.137923 , 1.6692148e+04),\n", - " ( 1.137923 , 1.3413479 , 1.6850346e+04),\n", - " ( 1.3413479 , 1.5811388 , 1.5495342e+04),\n", - " ( 1.5811388 , 1.8637968 , 1.5298746e+04),\n", - " ( 1.8637968 , 2.1969852 , 1.4056672e+04),\n", - " ( 2.1969852 , 2.5897374 , 1.5114676e+04),\n", - " ( 2.5897374 , 3.0527012 , 1.3100519e+04),\n", - " ( 3.0527012 , 3.5984282 , 1.1680131e+04),\n", - " ( 3.5984282 , 4.2417145 , 1.0921867e+04),\n", - " ( 4.2417145 , 5. , 8.6609297e+03),\n", - " ( 5. , 5.893843 , 8.2777559e+03),\n", - " ( 5.893843 , 6.9474773 , 6.5050391e+03),\n", - " ( 6.9474773 , 8.189468 , 4.7285605e+03),\n", - " ( 8.189468 , 9.653489 , 4.2180703e+03),\n", - " ( 9.653489 , 11.37923 , 4.0842490e+03),\n", - " (11.37923 , 13.413479 , 3.1398162e+03),\n", - " (13.413479 , 15.811388 , 1.9739290e+03),\n", - " (15.811388 , 18.637968 , 8.7255206e+02),\n", - " (18.637968 , 21.969852 , 2.7427654e+03),\n", - " (21.969852 , 25.897373 , 1.2124071e+03),\n", - " (25.897373 , 30.527012 , 4.7638251e+02),\n", - " (30.527012 , 35.984283 , 5.6154474e+02),\n", - " (35.984283 , 42.417145 , 0.0000000e+00),\n", - " (42.417145 , 50. , 0.0000000e+00)],\n", - " dtype=(numpy.record, [('ENERG_LO', ' lower: + bins = np.append(lower, bins) + + if bins[-1] < upper: + bins = np.append(bins, upper) + + return bins + + +@u.quantity_input(e_min=u.TeV, e_max=u.TeV) +def create_bins_per_decade(e_min, e_max, bins_per_decade=5): + """ + Create a bin array with bins equally spaced in logarithmic energy + with ``bins_per_decade`` bins per decade. + + Parameters + ---------- + e_min: u.Quantity[energy] + Minimum energy, inclusive + e_max: u.Quantity[energy] + Maximum energy, exclusive + n_bins_per_decade: int + number of bins per decade + + Returns + ------- + bins: u.Quantity[energy] + The created bin array, will have units of e_min + + """ + unit = e_min.unit + log_lower = np.log10(e_min.to_value(unit)) + log_upper = np.log10(e_max.to_value(unit)) + + bins = 10 ** np.arange(log_lower, log_upper, 1 / bins_per_decade) + return u.Quantity(bins, e_min.unit, copy=False) + + +def calculate_bin_indices(data, bins): + """ + Calculate bin indices for given data array and bins. + Underflow will be -1 and overflow len(bins) - 1. + If the bis already include underflow / overflow bins, e.g. + `bins[0] = -np.inf` and `bins[-1] = np.inf`, using the result of this + function will always be a valid index into the resultung histogram. + + + Parameters + ---------- + data: ``~np.ndarray`` or ``~astropy.units.Quantity`` + Array with the data + + bins: ``~np.ndarray`` or ``~astropy.units.Quantity`` + Array or Quantity of bin edges. Must have the same unit as ``data`` if a Quantity. + + + Returns + ------- + bin_index: np.ndarray[int] + Indices of the histogram bin the values in data belong to + """ + + if hasattr(data, "unit"): + if not hasattr(bins, "unit"): + raise TypeError(f"If ``data`` is a Quantity, so must ``bin``, got {bins}") + unit = data.unit + data = data.to_value(unit) + bins = bins.to_value(unit) + + return np.digitize(data, bins) - 1 + + +def create_histogram_table(events, bins, key="reco_energy"): + """ + Histogram a variable from events data into an astropy table. + + Parameters + ---------- + events : ``astropy.QTable`` + Astropy Table object containing the reconstructed events information. + bins: ``~np.ndarray`` or ``~astropy.units.Quantity`` + Array or Quantity of bin edges. + It must have the same units as ``data`` if a Quantity. + key : ``string`` + Variable to histogram from the events table. + + Returns + ------- + hist: ``astropy.QTable`` + Astropy table containg the histogram. + """ + hist = QTable() + hist[key + "_low"] = bins[:-1] + hist[key + "_high"] = bins[1:] + hist[key + "_center"] = 0.5 * (hist[key + "_low"] + hist[key + "_high"]) + hist["n"], _ = np.histogram(events[key], bins) + + # also calculate weighted number of events + if "weight" in events.colnames: + hist["n_weighted"], _ = np.histogram( + events[key], bins, weights=events["weight"] + ) + return hist diff --git a/pyirf/cut_optimization.py b/pyirf/cut_optimization.py new file mode 100644 index 000000000..8c2a62168 --- /dev/null +++ b/pyirf/cut_optimization.py @@ -0,0 +1,74 @@ +import numpy as np +from astropy.table import Table +import astropy.units as u +from tqdm import tqdm + +from .cuts import evaluate_binned_cut +from .sensitivity import calculate_sensitivity +from .binning import create_histogram_table + + +__all__ = [ + "optimize_gh_cut", +] + + +def optimize_gh_cut(signal, background, bins, cut_values, op, alpha=1.0, progress=True): + """ + Optimize the gh-score in every energy bin. + Theta Squared Cut should already be applied on the input tables. + """ + + # we apply each cut for all bins globally, calculate the + # sensitivity and then lookup the best sensitivity for each + # bin independently + + sensitivities = [] + for cut_value in tqdm(cut_values, disable=not progress): + + # create appropriate table for ``evaluate_binned_cut`` + cut_table = Table() + cut_table["low"] = bins[0:-1] + cut_table["high"] = bins[1:] + cut_table["cut"] = cut_value + + # apply the current cut + signal_selected = evaluate_binned_cut( + signal["gh_score"], signal["reco_energy"], cut_table, op, + ) + + background_selected = evaluate_binned_cut( + background["gh_score"], background["reco_energy"], cut_table, op, + ) + + # create the histograms + signal_hist = create_histogram_table( + signal[signal_selected], bins, "reco_energy" + ) + background_hist = create_histogram_table( + background[background_selected], bins, "reco_energy" + ) + + sensitivity = calculate_sensitivity(signal_hist, background_hist, alpha=alpha,) + sensitivities.append(sensitivity) + + best_cut_table = Table() + best_cut_table["low"] = bins[0:-1] + best_cut_table["high"] = bins[1:] + best_cut_table["cut"] = np.nan + + best_sensitivity = sensitivities[0].copy() + for bin_id in range(len(bins) - 1): + sensitivities_bin = [s["relative_sensitivity"][bin_id] for s in sensitivities] + + if not np.all(np.isnan(sensitivities_bin)): + # nanargmin won't return the index of nan entries + best = np.nanargmin(sensitivities_bin) + else: + # if all are invalid, just use the first one + best = 0 + + best_sensitivity[bin_id] = sensitivities[best][bin_id] + best_cut_table["cut"][bin_id] = cut_values[best] + + return best_sensitivity, best_cut_table diff --git a/pyirf/cuts.py b/pyirf/cuts.py new file mode 100644 index 000000000..1a9b26098 --- /dev/null +++ b/pyirf/cuts.py @@ -0,0 +1,86 @@ +import numpy as np +from astropy.table import Table + +from .binning import calculate_bin_indices + + +def calculate_percentile_cut( + values, bin_values, bins, fill_value, percentile=68, min_value=None, max_value=None, +): + """ + Calculate cuts as the percentile of a given quantity in bins of another + quantity. + + Parameters + ---------- + values: ``~numpy.ndarray`` or ``~astropy.units.Quantity`` + The values for which the cut should be calculated + bin_values: ``~numpy.ndarray`` or ``~astropy.units.Quantity`` + The values used to sort the ``values`` into bins + bins: ``~numpy.ndarray`` or ``~astropy.units.Quantity`` + Bin edges + fill_value: float or quantity + Value inserted for empty bins + percentile: float + The percentile to calculate in each bin as a percentage, + i.e. 0 <= percentile <= 100. + min_value: float or quantity or None + If given, cuts smaller than this value are replaced with ``min_value`` + max_value: float or quantity or None + If given, cuts larger than this value are replaced with ``max_value`` + """ + # create a table to make use of groupby operations + table = Table({"values": values, "bin_values": bin_values}, copy=False) + + table["bin_index"] = calculate_bin_indices(table["bin_values"].quantity, bins) + + cut_table = Table() + cut_table["low"] = bins[:-1] + cut_table["high"] = bins[1:] + cut_table["cut"] = fill_value + + # use groupby operations to calculate the percentile in each bin + by_bin = table.group_by("bin_index") + + # fill only the non-empty bins + cut_table["cut"][by_bin.groups.keys["bin_index"]] = ( + by_bin["values"] + .groups.aggregate(lambda g: np.percentile(g, percentile)) + .quantity.to_value(cut_table["cut"].unit) + ) + + if min_value is not None: + invalid = cut_table["cut"] < min_value + cut_table["cut"] = np.where(invalid, min_value, cut_table["cut"]) + + if max_value is not None: + invalid = cut_table["cut"] > max_value + cut_table["cut"] = np.where(invalid, max_value, cut_table["cut"]) + + return cut_table + + +def evaluate_binned_cut(values, bin_values, cut_table, op): + """ + Evaluate a binned cut as defined in cut_table on given events + + Parameters + ---------- + values: ``~numpy.ndarray`` or ``~astropy.units.Quantity`` + The values on which the cut should be evaluated + bin_values: ``~numpy.ndarray`` or ``~astropy.units.Quantity`` + The values used to sort the ``values`` into bins + cut_table: ``~astropy.table.Table`` + A table describing the binned cuts, e.g. as created by + ``~pyirf.cuts.calculate_percentile_cut``. + Required columns: + `low`: lower edges of the bins + `high`: upper edges of the bins, + `cut`: cut value + op: binary operator function + A function taking two arguments, comparing element-wise and + returning an array of booleans. + """ + bins = np.append(cut_table["low"].quantity, cut_table["high"].quantity[-1]) + bin_index = calculate_bin_indices(bin_values, bins) + return op(values, cut_table["cut"][bin_index].quantity) diff --git a/pyirf/io/__init__.py b/pyirf/io/__init__.py index feb21ea05..48dc60941 100644 --- a/pyirf/io/__init__.py +++ b/pyirf/io/__init__.py @@ -1,19 +1,17 @@ -from .io import ( - load_config, - internal_dataformat_mapper, - read_simu_info_hdf5, - read_simu_info_merged_hdf5, - get_simu_info, - read_FITS, - write, +from .eventdisplay import read_eventdisplay_fits +from .gadf import ( + create_aeff2d_hdu, + create_energy_dispersion_hdu, + create_psf_table_hdu, + create_rad_max_hdu, ) + __all__ = [ - "load_config", - "internal_dataformat_mapper", - "read_simu_info_hdf5", - "read_simu_info_merged_hdf5", - "get_simu_info", - "read_FITS", - "write", + "read_eventdisplay_fits", + "create_psf_table_hdu", + "create_aeff2d_hdu", + "create_energy_dispersion_hdu", + "create_psf_table_hdu", + "create_rad_max_hdu", ] diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py new file mode 100644 index 000000000..4c356d1e0 --- /dev/null +++ b/pyirf/io/eventdisplay.py @@ -0,0 +1,70 @@ +from astropy.table import QTable +import astropy.units as u + +from ..simulations import SimulatedEventsInfo + +import logging +import numpy as np + + +log = logging.getLogger(__name__) + + +COLUMN_MAP = { + "obs_id": "OBS_ID", + "event_id": "EVENT_ID", + "true_energy": "MC_ENERGY", + "reco_energy": "ENERGY", + "true_alt": "MC_ALT", + "true_az": "MC_AZ", + "pointing_alt": "PNT_ALT", + "pointing_az": "PNT_AZ", + "reco_alt": "ALT", + "reco_az": "AZ", + "gh_score": "GH_MVA", + "multiplicity": "MULTIP", +} + + +def read_eventdisplay_fits(infile): + """ + Read a DL2 FITS file as produced by the EventDisplay DL2 converter + from ROOT files: + https://github.com/Eventdisplay/Converters/blob/master/DL2/generate_DL2_file.py + + Parameters + ---------- + infile: str or pathlib.Path + Path to the input fits file + + Returns + ------- + events: astropy.QTable + Astropy Table object containing the reconstructed events information. + simulated_events: ``~pyirf.simulations.SimulatedEventsInfo`` + + """ + log.debug(f"Reading {infile}") + events_table = QTable.read(infile, hdu="EVENTS") + sim_events = QTable.read(infile, hdu="SIMULATED EVENTS") + run_header = QTable.read(infile, hdu="RUNHEADER")[0] + + events = QTable({new: events_table[old] for new, old in COLUMN_MAP.items()}) + + n_runs = len(np.unique(events["obs_id"])) + log.info(f"Estimated number of runs from obs ids: {n_runs}") + + n_showers = run_header["num_showers"] * run_header["num_use"] * n_runs + log.debug(f"Number of events from n_runs and run header: {n_showers}") + log.debug(f'Number of events histogram: {sim_events["EVENTS"].sum()}') + + sim_info = SimulatedEventsInfo( + n_showers=n_showers, + energy_min=u.Quantity(run_header["E_range"][0], u.TeV), + energy_max=u.Quantity(run_header["E_range"][1], u.TeV), + max_impact=u.Quantity(run_header["core_range"][1], u.m), + spectral_index=run_header["spectral_index"], + viewcone=u.Quantity(run_header["viewcone"][1], u.deg), + ) + + return events, sim_info diff --git a/pyirf/io/gadf.py b/pyirf/io/gadf.py new file mode 100644 index 000000000..16c4a7870 --- /dev/null +++ b/pyirf/io/gadf.py @@ -0,0 +1,270 @@ +from astropy.table import QTable +import astropy.units as u +from astropy.io.fits import Header, BinTableHDU +import numpy as np +from astropy.time import Time + +from ..version import __version__ + + +__all__ = [ + "create_aeff2d_hdu", + "create_energy_dispersion_hdu", + "create_psf_table_hdu", + "create_rad_max_hdu", +] + + +DEFAULT_HEADER = Header() +DEFAULT_HEADER["CREATOR"] = f"pyirf v{__version__}" +DEFAULT_HEADER["HDUDOC"] = "https://github.com/open-gamma-ray-astro/gamma-astro-data-formats" +DEFAULT_HEADER["HDUVERS"] = "0.2" +DEFAULT_HEADER["HDUCLASS"] = "GADF" + + +def _add_header_cards(header, **header_cards): + for k, v in header_cards.items(): + header[k] = v + + +@u.quantity_input( + effective_area=u.m ** 2, true_energy_bins=u.TeV, fov_offset_bins=u.deg +) +def create_aeff2d_hdu( + effective_area, + true_energy_bins, + fov_offset_bins, + extname="EFFECTIVE AREA", + point_like=True, + **header_cards, +): + """ + Create a fits binary table HDU in GADF format for effective area. + See the specification at + https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/aeff/index.html + + Parameters + ---------- + effective_area: astropy.units.Quantity[area] + Effective area array, must have shape (n_energy_bins, n_fov_offset_bins) + true_energy_bins: astropy.units.Quantity[energy] + Bin edges in true energy + fov_offset_bins: astropy.units.Quantity[angle] + Bin edges in the field of view offset. + For Point-Like IRFs, only giving a single bin is appropriate. + point_like: bool + If the provided effective area was calculated after applying a direction cut, + pass ``True``, else ``False`` for a full-enclosure effective area. + extname: str + Name for BinTableHDU + **header_cards + Additional metadata to add to the header, use this to set e.g. TELESCOP or + INSTRUME. + """ + aeff = QTable() + aeff["ENERG_LO"] = u.Quantity(true_energy_bins[:-1], ndmin=2).to(u.TeV) + aeff["ENERG_HI"] = u.Quantity(true_energy_bins[1:], ndmin=2).to(u.TeV) + aeff["THETA_LO"] = u.Quantity(fov_offset_bins[:-1], ndmin=2).to(u.deg) + aeff["THETA_HI"] = u.Quantity(fov_offset_bins[1:], ndmin=2).to(u.deg) + # transpose because FITS uses opposite dimension order than numpy + aeff["EFFAREA"] = effective_area.T[np.newaxis, ...].to(u.m ** 2) + + # required header keywords + header = DEFAULT_HEADER.copy() + header["HDUCLAS1"] = "RESPONSE" + header["HDUCLAS2"] = "EFF_AREA" + header["HDUCLAS3"] = "POINT-LIKE" if point_like else "FULL-ENCLOSURE" + header["HDUCLAS4"] = "AEFF_2D" + header["DATE"] = Time.now().utc.iso + _add_header_cards(header, **header_cards) + + return BinTableHDU(aeff, header=header, name=extname) + + +@u.quantity_input( + psf=u.sr ** -1, + true_energy_bins=u.TeV, + fov_offset_bins=u.deg, + source_offset_bins=u.deg, +) +def create_psf_table_hdu( + psf, + true_energy_bins, + source_offset_bins, + fov_offset_bins, + point_like=True, + extname="PSF", + **header_cards, +): + """ + Create a fits binary table HDU in GADF format for the PSF table. + See the specification at + https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/psf/psf_table/index.html + + Parameters + ---------- + psf: astropy.units.Quantity[(solid angle)^-1] + Point spread function array, must have shape + (n_energy_bins, n_fov_offset_bins, n_source_offset_bins) + true_energy_bins: astropy.units.Quantity[energy] + Bin edges in true energy + source_offset_bins: astropy.units.Quantity[angle] + Bin edges in the source offset. + fov_offset_bins: astropy.units.Quantity[angle] + Bin edges in the field of view offset. + For Point-Like IRFs, only giving a single bin is appropriate. + point_like: bool + If the provided effective area was calculated after applying a direction cut, + pass ``True``, else ``False`` for a full-enclosure effective area. + extname: str + Name for BinTableHDU + **header_cards + Additional metadata to add to the header, use this to set e.g. TELESCOP or + INSTRUME. + """ + + psf = QTable( + { + "ENERG_LO": u.Quantity(true_energy_bins[:-1], ndmin=2).to(u.TeV), + "ENERG_HI": u.Quantity(true_energy_bins[1:], ndmin=2).to(u.TeV), + "THETA_LO": u.Quantity(fov_offset_bins[:-1], ndmin=2).to(u.deg), + "THETA_HI": u.Quantity(fov_offset_bins[1:], ndmin=2).to(u.deg), + "RAD_LO": u.Quantity(source_offset_bins[:-1], ndmin=2).to(u.deg), + "RAD_HI": u.Quantity(source_offset_bins[1:], ndmin=2).to(u.deg), + # transpose as FITS uses opposite dimension order + "RPSF": psf.T[np.newaxis, ...].to(1 / u.sr), + } + ) + + # required header keywords + header = DEFAULT_HEADER.copy() + header["HDUCLAS1"] = "RESPONSE" + header["HDUCLAS2"] = "PSF" + header["HDUCLAS3"] = "POINT-LIKE" if point_like else "FULL-ENCLOSURE" + header["HDUCLAS4"] = "PSF_TABLE" + header["DATE"] = Time.now().utc.iso + _add_header_cards(header, **header_cards) + + return BinTableHDU(psf, header=header, name=extname) + + +@u.quantity_input( + true_energy_bins=u.TeV, fov_offset_bins=u.deg, +) +def create_energy_dispersion_hdu( + energy_dispersion, + true_energy_bins, + migration_bins, + fov_offset_bins, + point_like=True, + extname="EDISP", + **header_cards, +): + """ + Create a fits binary table HDU in GADF format for the energy dispersion. + See the specification at + https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/aeff/index.html + + Parameters + ---------- + energy_dispersion: numpy.ndarray + Energy dispersion array, must have shape + (n_energy_bins, n_migra_bins, n_source_offset_bins) + true_energy_bins: astropy.units.Quantity[energy] + Bin edges in true energy + migration_bins: numpy.ndarray + Bin edges for the relative energy migration (``reco_energy / true_energy``) + fov_offset_bins: astropy.units.Quantity[angle] + Bin edges in the field of view offset. + For Point-Like IRFs, only giving a single bin is appropriate. + point_like: bool + If the provided effective area was calculated after applying a direction cut, + pass ``True``, else ``False`` for a full-enclosure effective area. + extname: str + Name for BinTableHDU + **header_cards + Additional metadata to add to the header, use this to set e.g. TELESCOP or + INSTRUME. + """ + + psf = QTable( + { + "ENERG_LO": u.Quantity(true_energy_bins[:-1], ndmin=2).to(u.TeV), + "ENERG_HI": u.Quantity(true_energy_bins[1:], ndmin=2).to(u.TeV), + "MIGRA_LO": u.Quantity(migration_bins[:-1], ndmin=2).to(u.one), + "MIGRA_HI": u.Quantity(migration_bins[1:], ndmin=2).to(u.one), + "THETA_LO": u.Quantity(fov_offset_bins[:-1], ndmin=2).to(u.deg), + "THETA_HI": u.Quantity(fov_offset_bins[1:], ndmin=2).to(u.deg), + # transpose as FITS uses opposite dimension order + "MATRIX": u.Quantity(energy_dispersion.T[np.newaxis, ...]).to(u.one), + } + ) + + # required header keywords + header = DEFAULT_HEADER.copy() + header["HDUCLAS1"] = "RESPONSE" + header["HDUCLAS2"] = "EDISP" + header["HDUCLAS3"] = "POINT-LIKE" if point_like else "FULL-ENCLOSURE" + header["HDUCLAS4"] = "EDISP_2D" + header["DATE"] = Time.now().utc.iso + _add_header_cards(header, **header_cards) + + return BinTableHDU(psf, header=header, name=extname) + + +@u.quantity_input( + psf=u.sr ** -1, + true_energy_bins=u.TeV, + fov_offset_bins=u.deg, + source_offset_bins=u.deg, +) +def create_rad_max_hdu( + reco_energy_bins, + fov_offset_bins, + rad_max, + point_like=True, + extname="RAD_MAX", + **header_cards, +): + """ + Create a fits binary table HDU in GADF format for the directional cut. + See the specification at + https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/aeff/index.html + + Parameters + ---------- + reco_energy_bins: astropy.units.Quantity[energy] + Bin edges in reconstructed energy + fov_offset_bins: astropy.units.Quantity[angle] + Bin edges in the field of view offset. + For Point-Like IRFs, only giving a single bin is appropriate. + rad_max: astropy.units.Quantity[angle] + Array of the directional (theta) cut. + Must have shape (n_reco_energy_bins, n_fov_offset_bins) + extname: str + Name for BinTableHDU + **header_cards + Additional metadata to add to the header, use this to set e.g. TELESCOP or + INSTRUME. + """ + rad_max_table = QTable( + { + "ENERG_LO": u.Quantity(reco_energy_bins[:-1], ndmin=2).to(u.TeV), + "ENERG_HI": u.Quantity(reco_energy_bins[1:], ndmin=2).to(u.TeV), + "THETA_LO": u.Quantity(fov_offset_bins[:-1], ndmin=2).to(u.deg), + "THETA_HI": u.Quantity(fov_offset_bins[1:], ndmin=2).to(u.deg), + # transpose as FITS uses opposite dimension order + "RAD_MAX": rad_max.T[np.newaxis, ...].to(u.deg), + } + ) + + # required header keywords + header = DEFAULT_HEADER.copy() + header["HDUCLAS1"] = "RESPONSE" + header["HDUCLAS2"] = "RAD_MAX" + header["HDUCLAS3"] = "POINT-LIKE" + header["HDUCLAS4"] = "RAD_MAX_2D" + header["DATE"] = Time.now().utc.iso + _add_header_cards(header, **header_cards) + + return BinTableHDU(rad_max_table, header=header, name=extname) diff --git a/pyirf/io/io.py b/pyirf/io/io.py deleted file mode 100644 index a90573cc3..000000000 --- a/pyirf/io/io.py +++ /dev/null @@ -1,257 +0,0 @@ -""" -Set of classes and functions of input and output. - -Proposal of general structure: -- a reader for each data format (in the end only FITS, but also HDF5 for now) -- a mapper that reads user-defined DL2 column names into GADF format -- there should be only one output format (we follow GADF). - -Currently some column names are defined in the configuration file under the -section 'column_definition'. - -""" - - -# PYTHON STANDARD LIBRARY -# import os - -# THIRD-PARTY MODULES - -from astropy.io import fits -import yaml - -# import pkg_resources -from tables import open_file -import numpy as np -import pandas as pd - -from ctapipe.io import HDF5TableReader -from ctapipe.io.containers import MCHeaderContainer - - -def load_config(name): - """ - Load YAML configuration file. - - Parameters - ---------- - name : str - Path of the configuration file. - - Returns - ------- - cfg : dict - Dictionary containing all the configuration information. - - """ - try: - with open(name) as stream: - cfg = yaml.load(stream, Loader=yaml.FullLoader) - except FileNotFoundError as e: - print(e) - raise - return cfg - - -def read_simu_info_hdf5(filename): - """ - Read simu info from an hdf5 file - - Returns - ------- - `ctapipe.containers.MCHeaderContainer` - """ - - with HDF5TableReader(filename) as reader: - mcheader = reader.read("/simulation/run_config", MCHeaderContainer()) - mc = next(mcheader) - - return mc - - -def read_simu_info_merged_hdf5(filename): - """ - Read simu info from a merged hdf5 file. - - Check that simu info are the same for all runs from merged file. - Combine relevant simu info such as num_showers (sum). - Note: works for a single run file as well. - - Parameters - ---------- - filename: path to an hdf5 merged file - - Returns - ------- - `ctapipe.containers.MCHeaderContainer` - - """ - with open_file(filename) as file: - simu_info = file.root["simulation/run_config"] - colnames = simu_info.colnames - not_to_check = [ - "num_showers", - "shower_prog_start", - "detector_prog_start", - "obs_id", - ] - for k in colnames: - if k not in not_to_check: - assert np.all(simu_info[:][k] == simu_info[0][k]) - num_showers = simu_info[:]["num_showers"].sum() - - combined_mcheader = read_simu_info_hdf5(filename) - combined_mcheader["num_showers"] = num_showers - return combined_mcheader - - -def get_simu_info(filepath, particle_name, config=None): - """ - read simu info from file and return config - """ - if config is None: - config = {} - - if "particle_information" not in config: - config["particle_information"] = {} - if particle_name not in config["particle_information"]: - config["particle_information"][particle_name] = {} - cfg = config["particle_information"][particle_name] - - simu = read_simu_info_merged_hdf5(filepath) - cfg["n_events_per_file"] = simu.num_showers * simu.shower_reuse - cfg["n_files"] = 1 - cfg["e_min"] = simu.energy_range_min - cfg["e_max"] = simu.energy_range_max - cfg["gen_radius"] = simu.max_scatter_range - cfg["diff_cone"] = simu.max_viewcone_radius - cfg["gen_gamma"] = -simu.spectral_index - - return config - - -def internal_dataformat_mapper(debug=False, config=None): - """Defines the format to be used internally after input. - - All readers should call this function to map input data from different - formats. - - Parameters - ---------- - config : dict - Dictionary obtained from pyirf.io.load_config. - debug : bool - If True, print some debugging information. - - Returns - ------- - - columns : dict - Dictionary that maps user-defined DL2 quantities to the internal equivalent. - - """ - - columns = {} - - for key in config["column_definition"]: - columns[key] = config["column_definition"][key] - - if debug: - print("Mapping to internal data format....") - print(columns) - - return columns - - -def read_FITS(config=None, infile=None, pipeline="EventDisplay", debug=False): - """ - Store contents of a FITS file into one or more astropy tables. - - Parameters - ---------- - config : str - Path of the DL2 file. - infile : str - Path of the DL2 file. - debug : bool - If True, print some debugging information. - - Returns - ------- - - table : astropy.Table - Astropy Table object containing the reconstructed events information. - - Notes - ----- - For the moment this reader is specific to EventDisplay. - - If DL2 files in FITS format are supposed to have all the same structure, - then this reader is fine; if not, this reader will become - read_EventDisplay_FITS and others will follow. - - In general, though, for the the final FITS reader or any other specific one: - - - if GADF mandatory columns names are missing, only a warning is raised, - - it is possible to add custom columns. - - """ - DL2data = dict() - - colnames = internal_dataformat_mapper(debug, config=config) - - # later differentiate between EVENTS, GTI & POINTING - - with fits.open(infile) as hdul: - - print(f"Found {len(hdul)} Header Data Units in {hdul.filename()}.") - - EVENTS = hdul[1] - - # map the keys - - for INTERNAL_key, USER_key in colnames.items(): - - print(f"Checking if {INTERNAL_key} equivalent is defined...") - - # check for mispellings in the config file - if USER_key in EVENTS.columns.names: - if pipeline == "EventDisplay": - # for Event Display EVENTS is HDU 1 - DL2data[INTERNAL_key] = EVENTS.data[USER_key] - else: - print("WARNING : we support only EventDisplay for now!") - else: - print(f"WARNING : {USER_key} missing from DL2 data!") - - # Convert to pandas dataframe - DL2data = pd.DataFrame.from_dict(DL2data) - - return DL2data - - -def write(cuts=None, irfs=None): - """ - DL3 data writer. - - This should be writer for the DL3 data. - For the moment it is just a dummy function for reference. - The final format is under development, but we try to follow the latest - version of GADF [1]_. - - Notes - ----- - - .. [1] https://gamma-astro-data-formats.readthedocs.io/en/latest/ - - """ - return None - - -# def get_resource(resource_name): -# """ get the filename for a resource """ -# resource_path = os.path.join('resources', resource_name) -# if not pkg_resources.resource_exists(__name__, resource_path): -# raise FileNotFoundError(f"Couldn't find resource: {resource_name}") -# else: -# return pkg_resources.resource_filename(__name__, resource_path) diff --git a/pyirf/io/tests/__init__.py b/pyirf/io/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyirf/io/tests/test_gadf.py b/pyirf/io/tests/test_gadf.py new file mode 100644 index 000000000..0ae43eb15 --- /dev/null +++ b/pyirf/io/tests/test_gadf.py @@ -0,0 +1,87 @@ +''' +Test export to GADF format +''' +import astropy.units as u +import numpy as np +from astropy.io import fits +import pytest +import tempfile + + +def test_effective_area2d(): + '''Test our effective area is readable by gammapy''' + pytest.importorskip('gammapy') + from pyirf.io import create_aeff2d_hdu + from gammapy.irf import EffectiveAreaTable2D + + e_bins = np.geomspace(0.1, 100, 31) * u.TeV + fov_bins = [0, 1, 2, 3] * u.deg + area = np.full((30, 3), 1e6) * u.m**2 + + for point_like in [True, False]: + with tempfile.NamedTemporaryFile(suffix='.fits') as f: + hdu = create_aeff2d_hdu(area, e_bins, fov_bins, point_like=point_like) + + fits.HDUList([fits.PrimaryHDU(), hdu]).writeto(f.name) + + # test reading with gammapy works + aeff2d = EffectiveAreaTable2D.read(f.name) + assert u.allclose(area, aeff2d.data.data, atol=1e-16 * u.m**2) + + +def test_energy_dispersion(): + '''Test our energy dispersion is readable by gammapy''' + pytest.importorskip('gammapy') + from pyirf.io import create_energy_dispersion_hdu + from gammapy.irf import EnergyDispersion2D + + e_bins = np.geomspace(0.1, 100, 31) * u.TeV + migra_bins = np.linspace(0.2, 5, 101) + fov_bins = [0, 1, 2, 3] * u.deg + edisp = np.zeros((30, 100, 3)) + edisp[:, 50, :] = 1.0 + + + for point_like in [True, False]: + with tempfile.NamedTemporaryFile(suffix='.fits') as f: + hdu = create_energy_dispersion_hdu( + edisp, e_bins, migra_bins, fov_bins, point_like=point_like + ) + + fits.HDUList([fits.PrimaryHDU(), hdu]).writeto(f.name) + + # test reading with gammapy works + edisp2d = EnergyDispersion2D.read(f.name, 'EDISP') + assert u.allclose(edisp, edisp2d.data.data, atol=1e-16) + + +def test_psf_table(): + '''Test our psf is readable by gammapy''' + pytest.importorskip('gammapy') + from pyirf.io import create_psf_table_hdu + from pyirf.utils import cone_solid_angle + from gammapy.irf import PSF3D + + e_bins = np.geomspace(0.1, 100, 31) * u.TeV + source_bins = np.linspace(0, 1, 101) * u.deg + fov_bins = [0, 1, 2, 3] * u.deg + psf = np.zeros((30, 100, 3)) + psf[:, 0, :] = 1 + psf = psf / cone_solid_angle(source_bins[1]) + + + for point_like in [True, False]: + with tempfile.NamedTemporaryFile(suffix='.fits') as f: + hdu = create_psf_table_hdu( + psf, e_bins, source_bins, fov_bins, point_like=point_like + ) + + fits.HDUList([fits.PrimaryHDU(), hdu]).writeto(f.name) + + # test reading with gammapy works + psf3d = PSF3D.read(f.name, 'PSF') + + # gammapy does not transpose psf when reading from fits, + # unlike how it handles effective area and edisp + # see https://github.com/gammapy/gammapy/issues/3025 + assert u.allclose(psf, psf3d.psf_value.T, atol=1e-16 / u.sr) diff --git a/pyirf/io/tests/test_io.py b/pyirf/io/tests/test_io.py deleted file mode 100644 index 38dee8b73..000000000 --- a/pyirf/io/tests/test_io.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Unit tests for input / output operations.""" - -from pkg_resources import resource_filename - -from pyirf.io import load_config - -# TO DO: test DL2 data in HDF5 and FITS format - - -def test_load_config(): - - config_file = resource_filename("pyirf", "resources/config.yml") - - assert load_config(config_file) is not None diff --git a/pyirf/irf/__init__.py b/pyirf/irf/__init__.py new file mode 100644 index 000000000..6a5e7dfba --- /dev/null +++ b/pyirf/irf/__init__.py @@ -0,0 +1,10 @@ +from .effective_area import effective_area, point_like_effective_area +from .energy_dispersion import energy_dispersion +from .psf import psf_table + +__all__ = [ + "effective_area", + "point_like_effective_area", + "energy_dispersion", + "psf_table", +] diff --git a/pyirf/irf/effective_area.py b/pyirf/irf/effective_area.py new file mode 100644 index 000000000..017240540 --- /dev/null +++ b/pyirf/irf/effective_area.py @@ -0,0 +1,50 @@ +import numpy as np +import astropy.units as u +from ..binning import create_histogram_table + + +__all__ = [ + "effective_area", + "point_like_effective_area", +] + + +@u.quantity_input(area=u.m ** 2) +def effective_area(n_selected, n_simulated, area): + """ + Calculate effective area for histograms of selected and total simulated events + + Parameters + ---------- + n_selected: int or numpy.ndarray[int] + The number of surviving (e.g. triggered, analysed, after cuts) + n_simulated: int or numpy.ndarray[int] + The total number of events simulated + area: astropy.units.Quantity[area] + Area in which particle's core position was simulated + """ + return (n_selected / n_simulated) * area + + +def point_like_effective_area(selected_events, simulation_info, true_energy_bins): + """ + Calculate effective area for the given set of DL2 events, simulation statistics + and true energy bins. + + Parameters + ---------- + selected_events: astropy.table.QTable + DL2 events table, required columns for this function: `true_energy`. + simulation_info: pyirf.simulations.SimulatedEventsInfo + The overall statistics of the simulated events + true_energy_bins: astropy.units.Quantity[energy] + The bin edges in which to calculate effective area. + """ + area = np.pi * simulation_info.max_impact ** 2 + + hist_selected = create_histogram_table( + selected_events, true_energy_bins, "true_energy" + ) + hist_simulated = simulation_info.calculate_n_showers(true_energy_bins) + + return effective_area(hist_selected["n"], hist_simulated, area) diff --git a/pyirf/irf/energy_dispersion.py b/pyirf/irf/energy_dispersion.py new file mode 100644 index 000000000..2ef4faaaf --- /dev/null +++ b/pyirf/irf/energy_dispersion.py @@ -0,0 +1,75 @@ +import numpy as np +import astropy.units as u + + +__all__ = [ + "energy_dispersion", +] + + +def _normalize_hist(hist): + # (N_E, N_MIGRA, N_FOV) + # (N_E, N_FOV) + + norm = hist.sum(axis=1) + h = np.swapaxes(hist, 0, 1) + + with np.errstate(invalid="ignore"): + h /= norm + + h = np.swapaxes(h, 0, 1) + return np.nan_to_num(h) + + +def energy_dispersion( + selected_events, true_energy_bins, fov_offset_bins, migration_bins, +): + """ + Calculate energy dispersion for the given DL2 event list. + Energy dispersion is defined as the probability of finding an event + at a given relative deviation ``(reco_energy / true_energy)`` for a given + true energy. + + Parameters + ---------- + selected_events: astropy.table.QTable + Table of the DL2 events. + Required columns: ``reco_energy``, ``true_energy``, ``source_fov_offset``. + true_energy_bins: astropy.units.Quantity[energy] + Bin edges in true energy + migration_bins: astropy.units.Quantity[energy] + Bin edges in relative deviation, recommended range: [0.2, 5] + fov_offset_bins: astropy.units.Quantity[angle] + Bin edges in the field of view offset. + For Point-Like IRFs, only giving a single bin is appropriate. + + Returns + ------- + energy_dispersion: numpy.ndarray + Energy dispersion matrix + with shape (n_true_energy_bins, n_migration_bins, n_fov_ofset_bins) + """ + mu = (selected_events["reco_energy"] / selected_events["true_energy"]).to_value( + u.one + ) + + energy_dispersion, _ = np.histogramdd( + np.column_stack( + [ + selected_events["true_energy"].to_value(u.TeV), + mu, + selected_events["source_fov_offset"].to_value(u.deg), + ] + ), + bins=[ + true_energy_bins.to_value(u.TeV), + migration_bins, + fov_offset_bins.to_value(u.deg), + ], + ) + + n_events_per_energy = energy_dispersion.sum(axis=1) + assert len(n_events_per_energy) == len(true_energy_bins) - 1 + energy_dispersion = _normalize_hist(energy_dispersion) + + return energy_dispersion diff --git a/pyirf/irf/psf.py b/pyirf/irf/psf.py new file mode 100644 index 000000000..290ed0358 --- /dev/null +++ b/pyirf/irf/psf.py @@ -0,0 +1,51 @@ +import numpy as np +import astropy.units as u + +from ..utils import cone_solid_angle + + +def psf_table(events, true_energy_bins, source_offset_bins, fov_offset_bins): + """ + Calculate the table based PSF (radially symmetrical bins around the true source) + """ + + array = np.column_stack( + [ + events["true_energy"].to_value(u.TeV), + events["source_fov_offset"].to_value(u.deg), + events["theta"].to_value(u.deg), + ] + ) + + hist, edges = np.histogramdd( + array, + [ + true_energy_bins.to_value(u.TeV), + fov_offset_bins.to_value(u.deg), + source_offset_bins.to_value(u.deg), + ], + ) + + psf = _normalize_psf(hist, source_offset_bins) + return psf + + +def _normalize_psf(hist, source_offset_bins): + """Normalize the psf histogram to a probability densitity over solid angle""" + solid_angle = np.diff(cone_solid_angle(source_offset_bins)) + + # ignore numpy zero division warning + with np.errstate(invalid="ignore"): + + # to correctly divide by using broadcasting here, + # we need to swap the axis order + n_events = hist.sum(axis=2).T + hist = np.swapaxes(hist, 0, 2) + + # normalize and replace nans with 0 + psf = np.nan_to_num(hist / n_events) + + # swap axes back to order required by GADF + psf = np.swapaxes(psf, 0, 2) + + return psf / solid_angle diff --git a/pyirf/irf/tests/test_effective_area.py b/pyirf/irf/tests/test_effective_area.py new file mode 100644 index 000000000..ce5495e98 --- /dev/null +++ b/pyirf/irf/tests/test_effective_area.py @@ -0,0 +1,42 @@ +import astropy.units as u +import numpy as np +from astropy.table import QTable + + +def test_effective_area(): + from pyirf.irf import effective_area + + n_selected = np.array([10, 20, 30]) + n_simulated = np.array([100, 2000, 15000]) + + area = 1e5 * u.m ** 2 + + assert u.allclose( + effective_area(n_selected, n_simulated, area), [1e4, 1e3, 200] * u.m ** 2 + ) + + +def test_pointlike_effective_area(): + from pyirf.irf import point_like_effective_area + from pyirf.simulations import SimulatedEventsInfo + + true_energy_bins = [0.1, 1.0, 10.0] * u.TeV + selected_events = QTable( + {"true_energy": np.append(np.full(1000, 0.5), np.full(10, 5)),} + ) + + # this should give 100000 events in the first bin and 10000 in the second + simulation_info = SimulatedEventsInfo( + n_showers=110000, + energy_min=true_energy_bins[0], + energy_max=true_energy_bins[-1], + max_impact=100 / np.sqrt(np.pi) * u.m, # this should give a nice round area + spectral_index=-2, + viewcone=0 * u.deg, + ) + + area = point_like_effective_area(selected_events, simulation_info, true_energy_bins) + + assert area.shape == (len(true_energy_bins) - 1,) + assert area.unit == u.m ** 2 + assert u.allclose(area, [100, 10] * u.m ** 2) diff --git a/pyirf/irf/tests/test_energy_dispersion.py b/pyirf/irf/tests/test_energy_dispersion.py new file mode 100644 index 000000000..ded38273e --- /dev/null +++ b/pyirf/irf/tests/test_energy_dispersion.py @@ -0,0 +1,83 @@ +import astropy.units as u +import numpy as np +from astropy.table import QTable + + +def test_energy_dispersion(): + from pyirf.irf import energy_dispersion + + np.random.seed(0) + + N = 10000 + TRUE_SIGMA_1 = 0.20 + TRUE_SIGMA_2 = 0.10 + TRUE_SIGMA_3 = 0.05 + + selected_events = QTable( + { + "reco_energy": np.concatenate( + [ + np.random.normal(1.0, TRUE_SIGMA_1, size=N) * 0.5, + np.random.normal(1.0, TRUE_SIGMA_2, size=N) * 5, + np.random.normal(1.0, TRUE_SIGMA_3, size=N) * 50, + ] + ) + * u.TeV, + "true_energy": np.concatenate( + [np.full(N, 0.5), np.full(N, 5.0), np.full(N, 50.0)] + ) + * u.TeV, + "source_fov_offset": np.concatenate( + [ + np.full(N // 2, 0.2), + np.full(N // 2, 1.5), + np.full(N // 2, 0.2), + np.full(N // 2, 1.5), + np.full(N // 2, 0.2), + np.full(N // 2, 1.5), + ] + ) + * u.deg, + } + ) + + true_energy_bins = np.array([0.1, 1.0, 10.0, 100]) * u.TeV + fov_offset_bins = np.array([0, 1, 2]) * u.deg + migration_bins = np.linspace(0, 2, 1001) + + result = energy_dispersion( + selected_events, true_energy_bins, fov_offset_bins, migration_bins + ) + + assert result.shape == (3, 1000, 2) + assert np.isclose(result.sum(), 6.0) + + cumulative_sum = np.cumsum(result, axis=1) + bin_centers = 0.5 * (migration_bins[1:] + migration_bins[:-1]) + assert np.isclose( + TRUE_SIGMA_1, + ( + bin_centers[np.where(cumulative_sum[0, :, :] >= 0.84)[0][0]] + - bin_centers[np.where(cumulative_sum[0, :, :] >= 0.16)[0][0]] + ) + / 2, + rtol=0.1, + ) + assert np.isclose( + TRUE_SIGMA_2, + ( + bin_centers[np.where(cumulative_sum[1, :, :] >= 0.84)[0][0]] + - bin_centers[np.where(cumulative_sum[1, :, :] >= 0.16)[0][0]] + ) + / 2, + rtol=0.1, + ) + assert np.isclose( + TRUE_SIGMA_3, + ( + bin_centers[np.where(cumulative_sum[2, :, :] >= 0.84)[0][0]] + - bin_centers[np.where(cumulative_sum[2, :, :] >= 0.16)[0][0]] + ) + / 2, + rtol=0.1, + ) diff --git a/pyirf/irf/tests/test_psf.py b/pyirf/irf/tests/test_psf.py new file mode 100644 index 000000000..0fd8810ae --- /dev/null +++ b/pyirf/irf/tests/test_psf.py @@ -0,0 +1,58 @@ +import astropy.units as u +from astropy.table import QTable +import numpy as np + + +def test_psf(): + from pyirf.irf import psf_table + from pyirf.utils import cone_solid_angle + + np.random.seed(0) + + N = 1000 + + TRUE_SIGMA_1 = 0.2 + TRUE_SIGMA_2 = 0.1 + TRUE_SIGMA = np.append(np.full(N, TRUE_SIGMA_1), np.full(N, TRUE_SIGMA_2)) + + # toy event data set with just two energies + # and a psf per energy bin, point-like + events = QTable( + { + "true_energy": np.append(np.full(N, 1), np.full(N, 2)) * u.TeV, + "source_fov_offset": np.zeros(2 * N) * u.deg, + "theta": np.random.normal(0, TRUE_SIGMA) * u.deg, + } + ) + + energy_bins = [0, 1.5, 3] * u.TeV + fov_bins = [0, 1] * u.deg + source_bins = np.linspace(0, 1, 201) * u.deg + + # We return a table with one row as needed for gadf + psf = psf_table(events, energy_bins, source_bins, fov_bins) + + # 2 energy bins, 1 fov bin, 200 source distance bins + assert psf.shape == (2, 1, 200) + assert psf.unit == u.Unit("sr-1") + + # check that psf is normalized + bin_solid_angle = np.diff(cone_solid_angle(source_bins)) + assert np.allclose(np.sum(psf * bin_solid_angle, axis=2), 1.0) + + cumulated = np.cumsum(psf * bin_solid_angle, axis=2) + + # first energy and only fov bin + bin_centers = 0.5 * (source_bins[1:] + source_bins[:-1]) + assert u.isclose( + bin_centers[np.where(cumulated[0, 0, :] >= 0.68)[0][0]], + TRUE_SIGMA_1 * u.deg, + rtol=0.1, + ) + + # second energy and only fov bin + assert u.isclose( + bin_centers[np.where(cumulated[1, 0, :] >= 0.68)[0][0]], + TRUE_SIGMA_2 * u.deg, + rtol=0.1, + ) diff --git a/pyirf/perf/__init__.py b/pyirf/perf/__init__.py deleted file mode 100644 index 60a421bca..000000000 --- a/pyirf/perf/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .irf_maker import IrfMaker, SensitivityMaker, BkgData, Irf -from .cut_optimisation import CutsOptimisation, CutsDiagnostic, CutsApplicator - -__all__ = [ - "IrfMaker", - "SensitivityMaker", - "BkgData", - "Irf", - "CutsOptimisation", - "CutsDiagnostic", - "CutsApplicator", -] diff --git a/pyirf/perf/cut_optimisation.py b/pyirf/perf/cut_optimisation.py deleted file mode 100644 index c48d13d78..000000000 --- a/pyirf/perf/cut_optimisation.py +++ /dev/null @@ -1,929 +0,0 @@ -import os -import matplotlib.pyplot as plt -import numpy as np -import astropy.units as u -from astropy.table import Table, Column -from gammapy.spectrum.models import PowerLaw -from gammapy.stats import significance_on_off - -from .utils import save_obj, load_obj, plot_hist - -__all__ = ["CutsOptimisation", "CutsDiagnostic", "CutsApplicator"] - - -class CutsApplicator: - """ - Apply best cut and angular cut to events. - - Apply cuts to gamma, proton and electrons that will be further used for - performance estimation (irf, sensitivity, etc.). - - Parameters - ---------- - config: `dict` - Configuration file - outdir: `str` - Output directory where analysis results is saved - evt_dict: `dict` - Dictionary of `pandas.DataFrame` - """ - - def __init__(self, config, evt_dict, outdir): - self.config = config - self.evt_dict = evt_dict - self.outdir = outdir - - # Read table with cuts - self.table = Table.read( - os.path.join( - outdir, "{}.fits".format(config["general"]["output_table_name"]) - ), - format="fits", - ) - - def apply_cuts(self, debug): - """ - Flag particle types passing either the angular cut or the best cutoff - and the save the data - """ - - for particle in self.evt_dict.keys(): - data = self.apply_cuts_on_data(self.evt_dict[particle].copy(), debug) - data.to_hdf( - os.path.join(self.outdir, f"{particle}_processed.h5"), - key="dl2", - mode="w", - ) - - # update the particle tables to make the IRFs - self.evt_dict[particle] = data - - def apply_cuts_on_data(self, data, debug): - """ - Flag particle passing angular cut and the best cutoff - - Parameters - ---------- - data: `pandas.DataFrame` - Data set corresponding to one type of particle - """ - - # in order not to throw away this part of code I now convert the - # astropy table "back" to a Pandas dataframe like in protopipe.perf - data = data.to_pandas() - - # Add columns with False initialisation - data["pass_best_cutoff"] = np.zeros(len(data), dtype=bool) - data["pass_angular_cut"] = np.zeros(len(data), dtype=bool) - - # colname_reco_energy = self.config["column_definition"]["reco_energy"] - colname_reco_energy = "ENERGY" - # colname_clf_output = self.config["column_definition"]["classification_output"][ - # "name" - # ] - # colname_angular_dist = self.config["column_definition"][ - # "angular_distance_to_the_src" - # ] - - # IN GADF THE LAST 2 SHOULD ALWAYS BE - colname_clf_output = "EVENT_TYPE" - colname_angular_dist = "THETA" - - # Loop over energy bins and apply cutoff for each slice - table = self.table[np.where(self.table["keep"].data)[0]] - for info in table: - - if debug: - print( - "Processing bin [{:.3f},{:.3f}]... (cut={:.3f}, theta={:.3f})".format( - info["emin"], - info["emax"], - info["best_cutoff"], - info["angular_cut"], - ) - ) - - # Best cutoff - data.loc[ - (data[colname_reco_energy] >= info["emin"]) - & (data[colname_reco_energy] < info["emax"]) - & (data[colname_clf_output] >= info["best_cutoff"]), - ["pass_best_cutoff"], - ] = True - # Angular cut - data.loc[ - (data[colname_reco_energy] >= info["emin"]) - & (data[colname_reco_energy] < info["emax"]) - & (data[colname_angular_dist] <= info["angular_cut"]), - ["pass_angular_cut"], - ] = True - - # Handle events which are not in energy range - # Best cutoff - data.loc[ - (data[colname_reco_energy] < table["emin"][0]) - & (data[colname_clf_output] >= table["best_cutoff"][0]), - ["pass_best_cutoff"], - ] = True - data.loc[ - (data[colname_reco_energy] >= table["emin"][-1]) - & (data[colname_clf_output] >= table["best_cutoff"][-1]), - ["pass_best_cutoff"], - ] = True - # Angular cut - data.loc[ - (data[colname_reco_energy] < table["emin"][0]) - & (data[colname_angular_dist] <= table["angular_cut"][0]), - ["pass_angular_cut"], - ] = True - data.loc[ - (data[colname_reco_energy] >= table["emin"][-1]) - & (data[colname_angular_dist] <= table["angular_cut"][-1]), - ["pass_angular_cut"], - ] = True - - return data - - -class CutsDiagnostic: - """ - Class used to get some diagnostic related to the optimal working point. - - Parameters - ---------- - config: `dict` - Configuration file - indir: `str` - Output directory where analysis results is located - """ - - def __init__(self, config, indir): - self.config = config - self.indir = indir - self.outdir = os.path.join(indir, "diagnostic") - if not os.path.exists(self.outdir): - os.makedirs(self.outdir) - self.table = Table.read( - os.path.join( - indir, "{}.fits".format(config["general"]["output_table_name"]) - ), - format="fits", - ) - - self.clf_output_bounds = self.config["column_definition"][ - "classification_output" - ]["range"] - - def plot_optimisation_summary(self): - """Plot efficiencies and angular cut as a function of energy bins""" - plt.figure(figsize=(5, 5)) - ax = plt.gca() - t = self.table[np.where(self.table["keep"].data)[0]] - - ax.plot( - np.sqrt(t["emin"] * t["emax"]), - t["eff_sig"], - color="blue", - marker="o", - label="Signal", - ) - ax.plot( - np.sqrt(t["emin"] * t["emax"]), - t["eff_bkg"], - color="red", - marker="o", - label="Background (p+e)", - ) - ax.grid(which="both") - ax.set_xlabel("Reco energy [TeV]") - ax.set_ylabel("Efficiencies") - ax.set_xscale("log") - ax.set_ylim([0.0, 1.1]) - - ax_th = ax.twinx() - ax_th.plot( - np.sqrt(t["emin"] * t["emax"]), - t["angular_cut"], - color="darkgreen", - marker="s", - ) - ax_th.set_ylabel("Angular cut [deg]", color="darkgreen") - ax_th.tick_params( - "y", colors="darkgreen", - ) - ax_th.set_ylim([0.0, 0.5]) - - ax.legend(loc="upper left") - - plt.tight_layout() - plt.savefig(os.path.join(self.outdir, "efficiencies.pdf")) - - return ax - - def plot_diagnostics(self): - """Plot efficiencies and rates as a function of score""" - - for info in self.table[np.where(self.table["keep"].data)[0]]: - obj_name = "diagnostic_data_emin{:.3f}_emax{:.3f}.pkl.gz".format( - info["emin"], info["emax"] - ) - data = load_obj(os.path.join(self.outdir, obj_name)) - - fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 5)) - ax_eff = axes[0] - ax_rate = axes[1] - - ax_eff = self.plot_efficiencies_vs_score(ax_eff, data, info) - ax_rate = self.plot_rates_vs_score( - ax_rate, data, info, self.config["analysis"]["obs_time"]["unit"] - ) - - ax_eff.set_xlim(self.clf_output_bounds) - ax_rate.set_xlim(self.clf_output_bounds) - # print('JLK HAAAAACCCCKKKKKKK!!!!') - # ax_eff.set_xlim(-0.5, 0.5) - # ax_rate.set_xlim(-0.5, 0.5) - - plt.tight_layout() - plt.savefig( - os.path.join( - self.outdir, - "diagnostic_{:.2f}_{:.2f}TeV.pdf".format( - info["emin"], info["emax"] - ), - ) - ) - - @classmethod - def plot_efficiencies_vs_score(cls, ax, data, info): - """Plot efficiencies as a function of score""" - ax.plot(data["score"], data["hist_eff_sig"], color="blue", label="Signal", lw=2) - - ax.plot( - data["score"], - data["hist_eff_bkg"], - color="red", - label="Background (p+e)", - lw=2, - ) - - ax.plot( - [info["best_cutoff"], info["best_cutoff"]], - [0, 1.1], - ls="--", - lw=2, - color="darkgreen", - label="Best cutoff", - ) - - ax.set_xlabel("Score") - ax.set_ylabel("Efficiencies") - ax.set_ylim([0.0, 1.1]) - ax.grid(which="both") - ax.legend(loc="lower left", framealpha=1) - return ax - - @classmethod - def plot_rates_vs_score(cls, ax, data, info, time_unit): - """Plot rates as a function of score""" - scale = info["min_flux"] - - opt = { - "edgecolor": "blue", - "color": "blue", - "label": "Excess in ON region", - "alpha": 0.2, - "fill": True, - "ls": "-", - "lw": 1, - } - error_kw = dict(ecolor="blue", lw=1, capsize=1, capthick=1, alpha=1) - ax = plot_hist( - ax=ax, - data=(data["cumul_excess"] * scale) - / (info["obs_time"] * u.Unit(time_unit).to("s")), - edges=data["score_edges"], - norm=False, - yerr=False, - error_kw=error_kw, - hist_kwargs=opt, - ) - - opt = { - "edgecolor": "red", - "color": "red", - "label": "Bkg in ON region", - "alpha": 0.2, - "fill": True, - "ls": "-", - "lw": 1, - } - error_kw = dict(ecolor="red", lw=1, capsize=1, capthick=1, alpha=1) - ax = plot_hist( - ax=ax, - data=data["cumul_noff"] - * info["alpha"] - / (info["obs_time"] * u.Unit(time_unit).to("s")), - edges=data["score_edges"], - norm=False, - yerr=False, - error_kw=error_kw, - hist_kwargs=opt, - ) - - ax.plot( - [info["best_cutoff"], info["best_cutoff"]], - [0, 1.1], - ls="--", - lw=2, - color="darkgreen", - label="Best cutoff", - ) - - max_rate_p = ( - data["cumul_noff"] - * info["alpha"] - / (info["obs_time"] * u.Unit(time_unit).to("s")) - ).max() - max_rate_g = ( - data["cumul_excess"] / (info["obs_time"] * u.Unit(time_unit).to("s")) - ).max() - - scaled_rate = max_rate_g * scale - max_rate = scaled_rate if scaled_rate >= max_rate_p else max_rate_p - - ax.set_ylim([0.0, max_rate * 1.15]) - ax.set_ylabel("Rates [HZ]") - ax.set_xlabel("Score") - ax.grid(which="both") - ax.legend(loc="upper right", framealpha=1) - - ax.text( - 0.52, - 0.35, - CutsDiagnostic.get_text(info), - horizontalalignment="left", - verticalalignment="bottom", - multialignment="left", - bbox=dict(facecolor="white", alpha=0.5), - transform=ax.transAxes, - ) - return ax - - @classmethod - def get_text(cls, info): - """Returns a text summarising the optimisation result""" - text = "E in [{:.2f},{:.2f}] TeV\n".format(info["emin"], info["emax"]) - text += "Theta={:.2f} deg\n".format(info["angular_cut"]) - text += "Best cutoff:\n" - text += "-min_flux={:.2f} Crab\n".format(info["min_flux"]) - text += "-score={:.2f}\n".format(info["best_cutoff"]) - text += "-non={:.2f}\n".format(info["non"]) - text += "-noff={:.2f}\n".format(info["noff"]) - text += "-alpha={:.2f}\n".format(info["alpha"]) - text += "-excess={:.2f}".format(info["excess"]) - if info["systematic"] is True: - text += "(syst.!)\n" - else: - text += "\n" - text += "-nbkg={:.2f}\n".format(info["background"]) - text += "-sigma={:.2f} (Li & Ma)".format(info["sigma"]) - - return text - - -class CutsOptimisation: - """ - Class used to find best cutoff to obtain minimal - sensitivity in a given amount of time. - - Parameters - ---------- - config: `dict` - Configuration file - evt_dict: `dict` - Dictionary of `pandas` files - """ - - def __init__(self, config, evt_dict, verbose_level=0): - self.config = config - self.evt_dict = evt_dict - self.verbose_level = verbose_level - - def weight_events(self, model_dict, colname_mc_energy): - """ - Add a weight column to the files, in order to scale simulated data to reality. - - Parameters - ---------- - model_dict: dict - Dictionary of models - colname_mc_energy: str - Column name for the true energy - """ - for particle in self.evt_dict.keys(): - self.evt_dict[particle]["weight"] = self.compute_weight( - energy=self.evt_dict[particle][colname_mc_energy] * u.TeV, - particle=particle, - model=model_dict[particle], - ) - - def compute_weight(self, energy, particle, model): - """ - Weight particles, according to: [phi_exp(E) / phi_simu(E)] * (t_obs / t_simu) - where E is the true energy of the particles - """ - conf_part = self.config["particle_information"][particle] - - area_simu = (np.pi * conf_part["gen_radius"] ** 2) * u.Unit("m2") - - omega_simu = ( - 2 * np.pi * (1 - np.cos(conf_part["diff_cone"] * np.pi / 180.0)) * u.sr - ) - if particle in "gamma": # Gamma are point-like - omega_simu = 1.0 - - nsimu = conf_part["n_simulated"] - index_simu = conf_part["gen_gamma"] - emin = conf_part["e_min"] * u.TeV - emax = conf_part["e_max"] * u.TeV - amplitude = 1.0 * u.Unit("1 / (cm2 s TeV)") - pwl_integral = PowerLaw(index=index_simu, amplitude=amplitude).integral( - emin=emin, emax=emax - ) - - tsimu = nsimu / (area_simu * omega_simu * pwl_integral) - tobs = self.config["analysis"]["obs_time"]["value"] * u.Unit( - self.config["analysis"]["obs_time"]["unit"] - ) - - phi_simu = amplitude * (energy / (1 * u.TeV)) ** (-index_simu) - - if particle in "proton": - phi_exp = model(energy, "proton") - elif particle in "electron": - phi_exp = model(energy, "electron") - elif particle in "gamma": - phi_exp = model(energy) - else: - print("oups...") - - return ((tobs / tsimu) * (phi_exp / phi_simu)).decompose() - - def find_best_cutoff(self, energy_values, angular_values): - """ - Find best cutoff to reach the best sensitivity. Optimisation is done as a function - of energy and theta square cut. Correct the number of events - according to the ON region which correspond to the angular cut applied to - the gamma-ray events. - - Parameters - ---------- - energy_values: `astropy.Quantity` - Energy bins - angular_values: `astropy.Quantity` - Angular cuts - """ - self.results_dict = dict() - # colname_reco_energy = self.config["column_definition"]["reco_energy"] - # colname_reco_energy = "ENERGY" - clf_output_bounds = self.config["column_definition"]["classification_output"][ - "range" - ] - # colname_angular_dist = self.config["column_definition"][ - # "angular_distance_to_the_src" - # ] - thsq_opt_type = self.config["analysis"]["thsq_opt"]["type"] - - # Loop on energy - for ibin in range(len(energy_values) - 1): - emin = energy_values[ibin] - emax = energy_values[ibin + 1] - print(f" ==> {ibin}) Working in E=[{emin:.3f},{emax:.3f}]") - - # OLDER PANDAS EQUIVALENT FROM PROTOPIPE - - # Apply cuts (energy and additional if there is) - # query_emin = "{} > {}".format(colname_reco_energy, emin.value) - # query_emax = "{} <= {}".format(colname_reco_energy, emax.value) - # energy_query = "{} and {}".format(query_emin, query_emax) - - # g = self.evt_dict["gamma"].query(energy_query).copy() - # p = self.evt_dict["proton"].query(energy_query).copy() - # e = self.evt_dict["electron"].query(energy_query).copy() - - # Apply cuts (energy and additional if there is) - energy_selected = dict() - for particle in ["gamma", "proton", "electron"]: - energy = self.evt_dict[particle]["ENERGY"] - mask_energy = (energy > emin.value) & (energy <= emax.value) - energy_selected[particle] = self.evt_dict[particle][mask_energy].copy() - - g = energy_selected["gamma"] - p = energy_selected["proton"] - e = energy_selected["electron"] - - if self.verbose_level > 0: - print( - "Total evts for optimisation: Ng={}, Np={}, Ne={}".format( - len(g), len(p), len(e) - ) - ) - - min_stat = 100 - if len(g) <= min_stat or len(p) <= min_stat or len(e) <= min_stat: - print("Not enough statistics") - print(" g={}, p={} e={}".format(len(g), len(p), len(e))) - key = CutsOptimisation._get_energy_key(emin, emax) - self.results_dict[key] = { - "emin": emin.value, - "emax": emax.value, - "keep": False, - } - continue - - # To store intermediate results - results_th_cut_dict = dict() - - theta_to_loop_on = angular_values - if thsq_opt_type in "r68": - theta_to_loop_on = [angular_values[ibin]] - - # Loop on angular cut - for th_cut in theta_to_loop_on: - if self.verbose_level > 0: - print(f"- Theta={th_cut:.2f}") - - # Select gamma-rays in ON region - - # OLD PANDAS VERSION FROM PROTOPIPE - # th_query = "{} <= {}".format(colname_angular_dist, th_cut.value) - # sel_g = g.query(th_query).copy() - - # ASTROPY VERSION - mask_theta = g["THETA"] <= th_cut.value - sel_g = g[mask_theta].copy() - - # Correct number of background due to acceptance - acceptance_g = 2 * np.pi * (1 - np.cos(th_cut.to("rad").value)) - acceptance_p = ( - 2 - * np.pi - * ( - 1 - - np.cos( - self.config["particle_information"]["proton"]["offset_cut"] - * u.deg.to("rad") - ) - ) - ) - acceptance_e = ( - 2 - * np.pi - * ( - 1 - - np.cos( - self.config["particle_information"]["electron"][ - "offset_cut" - ] - * u.deg.to("rad") - ) - ) - ) - - # Add corrected weight taking into account the angular cuts - # that have been applied to gamma-rays - sel_g["weight_corrected"] = sel_g["weight"] - p["weight_corrected"] = p["weight"] * acceptance_g / acceptance_p - e["weight_corrected"] = e["weight"] * acceptance_g / acceptance_e - - # Get binned data as a function of score - binned_data = self.get_binned_data( - sel_g, p, e, nbins=2000, score_range=clf_output_bounds - ) - - # Get re-binned data as a function of score for diagnostic plots - re_binned_data = self.get_binned_data( - sel_g, p, e, nbins=200, score_range=clf_output_bounds - ) - - # Get optimisation results - results_th_cut_dict[CutsOptimisation._get_angular_key(th_cut.value)] = { - "th_cut": th_cut, - "result": self.find_best_cutoff_for_one_bin( - binned_data=binned_data - ), - "diagnostic_data": re_binned_data, - } - - # Select best theta cut (lowest flux). - # In case of equality, select the one with the highest signal - # efficiency (flux are sorted as a function of decreasing signal - # efficiencies). - flux_list = [] - eff_sig = [] - th = [] - key_list = [] - for key in results_th_cut_dict: - key_list.append(key) - flux_list.append(results_th_cut_dict[key]["result"]["min_flux"]) - eff_sig.append(results_th_cut_dict[key]["result"]["eff_sig"]) - th.append(results_th_cut_dict[key]["th_cut"]) - - # In case of equal min fluxes, take the one with bigger sig efficiency - lower_flux_idx = np.where(np.array(flux_list) == np.array(flux_list).min())[ - 0 - ][0] - - if self.verbose_level > 0: - print( - "Select th={:.3f}, cutoff={:.3f} (eff_sig={:.3f}, eff_bkg={:.3f}, flux={:.3f}, syst={})".format( - results_th_cut_dict[key_list[lower_flux_idx]]["th_cut"], - results_th_cut_dict[key_list[lower_flux_idx]]["result"][ - "best_cutoff" - ], - results_th_cut_dict[key_list[lower_flux_idx]]["result"][ - "eff_sig" - ], - results_th_cut_dict[key_list[lower_flux_idx]]["result"][ - "eff_bkg" - ], - results_th_cut_dict[key_list[lower_flux_idx]]["result"][ - "min_flux" - ], - results_th_cut_dict[key_list[lower_flux_idx]]["result"][ - "systematic" - ], - ) - ) - - key = CutsOptimisation._get_energy_key(emin.value, emax.value) - self.results_dict[key] = { - "emin": emin.value, - "emax": emax.value, - "obs_time": self.config["analysis"]["obs_time"]["value"], - "th_cut": results_th_cut_dict[key_list[lower_flux_idx]]["th_cut"].value, - "keep": True, - "results": results_th_cut_dict[key_list[lower_flux_idx]]["result"], - "diagnostic_data": results_th_cut_dict[key_list[lower_flux_idx]][ - "diagnostic_data" - ], - } - - print( - " Ang. cut: {:.2f}, score cut: {}".format( - self.results_dict[key]["th_cut"], - self.results_dict[key]["results"]["best_cutoff"], - ) - ) - - def find_best_cutoff_for_one_bin(self, binned_data): - """ - Find the best cut off for one bin os the phase space - """ - alpha = self.config["analysis"]["alpha"] - - # Scan eff_bkg efficiency (going from 0.05 to 0.5, 10 bins as in MARS analysis) - fixed_bkg_eff = np.linspace(0.05, 0.5, 15) - - # Find corresponding indexes - fixed_bkg_eff_indexes = np.zeros(len(fixed_bkg_eff), dtype=int) - for idx in range(len(fixed_bkg_eff)): - the_idx = ( - np.abs(binned_data["hist_eff_bkg"] - fixed_bkg_eff[idx]) - ).argmin() - fixed_bkg_eff_indexes[idx] = the_idx - - # Will contain - minimal_fluxes = np.zeros(len(fixed_bkg_eff)) - minimal_sigma = np.zeros(len(fixed_bkg_eff)) - minimal_syst = np.zeros(len(fixed_bkg_eff), dtype=bool) - minimal_excess = np.zeros(len(fixed_bkg_eff)) - - for iflux in range(len(minimal_fluxes)): - - excess = binned_data["cumul_excess"][fixed_bkg_eff_indexes][iflux] - n_bkg = binned_data["cumul_noff"][fixed_bkg_eff_indexes][iflux] * alpha - effsig = binned_data["hist_eff_sig"][fixed_bkg_eff_indexes][iflux] - effbkg = binned_data["hist_eff_bkg"][fixed_bkg_eff_indexes][iflux] - score = binned_data["score"][fixed_bkg_eff_indexes][iflux] - minimal_syst[iflux] = False - - if n_bkg == 0: - if self.verbose_level > 0: - print("Warning> To be dealt with") - pass - - minimal_fluxes[iflux], minimal_sigma[iflux] = self._get_sigma_flux( - excess, n_bkg, alpha, self.config["analysis"]["min_sigma"] - ) - minimal_excess[iflux] = minimal_fluxes[iflux] * excess - - if self.verbose_level > 1: - print( - "eff_bkg={:.2f}, eff_sig={:.2f}, score={:.2f}, excess={:.2f}, bkg={:.2f}, min_flux={:.3f}, sigma={:.3f}".format( - effbkg, - effsig, - score, - minimal_excess[iflux], - n_bkg, - minimal_fluxes[iflux], - minimal_sigma[iflux], - ) - ) - - if minimal_excess[iflux] < self.config["analysis"]["min_excess"]: - minimal_syst[iflux] = True - # Rescale flux accodring to minimal acceptable excess - minimal_fluxes[iflux] = self.config["analysis"]["min_excess"] / excess - minimal_excess[iflux] = self.config["analysis"]["min_excess"] - if self.verbose_level > 1: - print(" WARNING> Not enough signal!") - - if minimal_excess[iflux] < self.config["analysis"]["bkg_syst"] * n_bkg: - minimal_syst[iflux] = True - minimal_fluxes[iflux] = ( - self.config["analysis"]["bkg_syst"] * n_bkg / excess - ) - if self.verbose_level > 1: - print(" WARNING> Bkg systematics!") - - # In case of equal min fluxes, take the one with bigger sig efficiency - # (last value) - opti_cut_index = np.where(minimal_fluxes == minimal_fluxes.min())[0][-1] - min_flux = minimal_fluxes[opti_cut_index] - min_sigma = minimal_sigma[opti_cut_index] - min_excess = minimal_excess[opti_cut_index] - min_syst = minimal_syst[opti_cut_index] - - best_cut_index = fixed_bkg_eff_indexes[opti_cut_index] # for fine binning - - return { - "best_cutoff": binned_data["score"][best_cut_index], - "noff": binned_data["cumul_noff"][best_cut_index], - "background": binned_data["cumul_noff"][best_cut_index] * alpha, - "non": binned_data["cumul_excess"][best_cut_index] * min_flux - + binned_data["cumul_noff"][best_cut_index] * alpha, - "alpha": alpha, - "eff_sig": binned_data["hist_eff_sig"][best_cut_index], - "eff_bkg": binned_data["hist_eff_bkg"][best_cut_index], - "min_flux": min_flux, - "excess": min_excess, - "sigma": min_sigma, - "systematic": min_syst, - } - - @classmethod - def _get_sigma_flux(cls, excess, bkg, alpha, min_sigma): - """Compute flux to get `min_sigma` sigma detection. Returns fraction - of minimal flux and the resulting signifiance""" - - # Gross binning - flux_level = np.arange(0.0, 10, 0.01)[1:] - sigma = significance_on_off( - n_on=excess * flux_level + bkg, - n_off=bkg / alpha, - alpha=alpha, - method="lima", - ) - - the_idx = (np.abs(sigma - min_sigma)).argmin() - min_flux = flux_level[the_idx] - - # Fine binning - flux_level = np.arange(min_flux - 0.05, min_flux + 0.05, 0.001) - sigma = significance_on_off( - n_on=excess * flux_level + bkg, - n_off=bkg / alpha, - alpha=alpha, - method="lima", - ) - the_idx = (np.abs(sigma - min_sigma)).argmin() - - return flux_level[the_idx], sigma[the_idx] - - @classmethod - def _get_energy_key(cls, emin, emax): - return f"{emin:.3f}-{emax:.3f}TeV" - - @classmethod - def _get_angular_key(cls, ang): - return f"{ang:.3f}deg" - - def get_binned_data(self, g, p, e, nbins=100, score_range=[-1, 1]): - """Returns binned data as a dictionnary""" - # colname_clf_output = self.config["column_definition"]["classification_output"][ - # "name" - # ] - colname_clf_output = "EVENT_TYPE" - - res = dict() - # Histogram of events - res["hist_sig"], edges = np.histogram( - # a=g[colname_clf_output].values, - a=g[colname_clf_output], - bins=nbins, - range=score_range, - # weights=g["weight_corrected"].values, - weights=g["weight_corrected"], - ) - res["hist_p"], edges = np.histogram( - # a=p[colname_clf_output].values, - a=p[colname_clf_output], - bins=nbins, - range=score_range, - # weights=p["weight_corrected"].values, - weights=p["weight_corrected"], - ) - res["hist_e"], edges = np.histogram( - # a=e[colname_clf_output].values, - a=e[colname_clf_output], - bins=nbins, - range=score_range, - # weights=e["weight_corrected"].values, - weights=e["weight_corrected"], - ) - res["hist_bkg"] = res["hist_p"] + res["hist_e"] - res["score"] = (edges[:-1] + edges[1:]) / 2.0 - res["score_edges"] = edges - - # Efficiencies - res["hist_eff_sig"] = 1.0 - np.cumsum(res["hist_sig"]) / np.sum(res["hist_sig"]) - res["hist_eff_bkg"] = 1.0 - np.cumsum(res["hist_bkg"]) / np.sum(res["hist_bkg"]) - - # Cumulative statistics - alpha = self.config["analysis"]["alpha"] - res["cumul_noff"] = res["hist_eff_bkg"] * sum(res["hist_bkg"]) / alpha - res["cumul_excess"] = sum(res["hist_sig"]) - np.cumsum(res["hist_sig"]) - res["cumul_non"] = res["cumul_excess"] + res["cumul_noff"] * alpha - res["cumul_sigma"] = significance_on_off( - n_on=res["cumul_non"], n_off=res["cumul_noff"], alpha=alpha, method="lima" - ) - - return res - - def write_results(self, outdir, outfile, format, overwrite=True): - """Write results with astropy utilities""" - # Declare and initialise vectors to save - n = len(self.results_dict) - feature_to_save = [ - ("best_cutoff", float), - ("non", float), - ("noff", float), - ("alpha", float), - ("background", float), - ("excess", float), - ("eff_sig", float), - ("eff_bkg", float), - ("systematic", bool), - ("min_flux", float), - ("sigma", float), - ] - emin = np.zeros(n) - emax = np.zeros(n) - angular_cut = np.zeros(n) - obs_time = np.zeros(n) - keep = np.zeros(n, dtype=bool) - - res_to_save = dict() - for feature in feature_to_save: - res_to_save[feature[0]] = np.zeros(n, dtype=feature[1]) - - # Fill data and save diagnostic result - for idx, key in enumerate(self.results_dict.keys()): - bin_info = self.results_dict[key] - if bin_info["keep"] is False: - keep[idx] = bin_info["keep"] - continue - bin_results = self.results_dict[key]["results"] - bin_data = self.results_dict[key]["diagnostic_data"] - - keep[idx] = bin_info["keep"] - emin[idx] = bin_info["emin"] - emax[idx] = bin_info["emax"] - angular_cut[idx] = bin_info["th_cut"] - obs_time[idx] = bin_info["obs_time"] - for feature in feature_to_save: - res_to_save[feature[0]][idx] = bin_results[feature[0]] - - obj_name = "diagnostic_data_emin{:.3f}_emax{:.3f}.pkl.gz".format( - bin_info["emin"], bin_info["emax"] - ) - - diagnostic_dir = os.path.join(outdir, "diagnostic") - if not os.path.exists(diagnostic_dir): - os.makedirs(diagnostic_dir) - save_obj(bin_data, os.path.join(outdir, "diagnostic", obj_name)) - - # Save data - t = Table() - t["keep"] = Column(keep, dtype=bool) - t["emin"] = Column(emin, unit="TeV") - t["emax"] = Column(emax, unit="TeV") - t["obs_time"] = Column( - obs_time, unit=self.config["analysis"]["obs_time"]["unit"] - ) - t["angular_cut"] = Column(angular_cut, unit="TeV") - for feature in feature_to_save: - t[feature[0]] = Column(res_to_save[feature[0]]) - t.write(os.path.join(outdir, outfile), format=format, overwrite=overwrite) diff --git a/pyirf/perf/irf_maker.py b/pyirf/perf/irf_maker.py deleted file mode 100644 index 0ec525e1e..000000000 --- a/pyirf/perf/irf_maker.py +++ /dev/null @@ -1,668 +0,0 @@ -import os -import numpy as np -import astropy.units as u -from astropy.coordinates import Angle -from astropy.table import Table, Column -from astropy.io import fits - -from gammapy.utils.nddata import NDDataArray, BinnedDataAxis -from gammapy.utils.energy import EnergyBounds -from gammapy.irf import EffectiveAreaTable2D, EnergyDispersion2D -from gammapy.spectrum import SensitivityEstimator - -__all__ = ["IrfMaker", "SensitivityMaker", "BkgData", "Irf"] - - -class BkgData: - """ - Class storing background data in a NDDataArray object. - - It's a bit hacky, but gammapy sensitivity estimator does not take individual IRF, - it takes a CTAPerf object. So i'm emulating that... We need a bkg format!!! - """ - - def __init__(self, data): - self.data = data - - @property - def energy(self): - """Get energy.""" - return self.data.axes[0] - - -class Irf: - """ - Class storing IRF for sensitivity computation (emulating CTAPerf) - """ - - def __init__(self, bkg, aeff, rmf): - self.bkg = bkg - self.aeff = aeff - self.rmf = rmf - - -class SensitivityMaker: - """ - Class which estimate sensitivity with IRF - - Parameters - ---------- - config: `dict` - Configuration file - outdir: `str` - Output directory where analysis results is saved - """ - - def __init__(self, config, outdir): - self.config = config - self.outdir = outdir - self.irf = None - - def load_irf(self): - filename = os.path.join(self.outdir, "irf.fits.gz") - with fits.open(filename, memmap=False) as hdulist: - aeff = EffectiveAreaTable2D.from_hdulist(hdulist=hdulist) - edisp = EnergyDispersion2D.read(filename, hdu="ENERGY DISPERSION") - - bkg_fits_table = hdulist["BACKGROUND"] - bkg_table = Table.read(bkg_fits_table) - energy_lo = bkg_table["ENERG_LO"].quantity - energy_hi = bkg_table["ENERG_HI"].quantity - bkg = bkg_table["BGD"].quantity - - axes = [ - BinnedDataAxis( - energy_lo, energy_hi, interpolation_mode="log", name="energy" - ) - ] - bkg = BkgData(data=NDDataArray(axes=axes, data=bkg)) - - # Create rmf with appropriate dimensions (e_reco->bkg, e_true->area) - e_reco_min = bkg.energy.lo[0] - e_reco_max = bkg.energy.hi[-1] - e_reco_bin = bkg.energy.nbins - e_reco_axis = EnergyBounds.equal_log_spacing( - e_reco_min, e_reco_max, e_reco_bin, "TeV" - ) - - e_true_min = aeff.data.axes[0].lo[0] - e_true_max = aeff.data.axes[0].hi[-1] - e_true_bin = len(aeff.data.axes[0].bins) - 1 - e_true_axis = EnergyBounds.equal_log_spacing( - e_true_min, e_true_max, e_true_bin, "TeV" - ) - - # Fake offset... - rmf = edisp.to_energy_dispersion( - offset=0.5 * u.deg, e_reco=e_reco_axis, e_true=e_true_axis - ) - - # This is required because in gammapy v0.8 - # gammapy.spectrum.utils.integrate_model - # calls the attribute aeff.energy which is an attribute of - # EffectiveAreaTable and not of EffectiveAreaTable2D - # WARNING the angle is not important, but only because we started with - # on-axis data! TO UPDATE - aeff = aeff.to_effective_area_table(Angle("1d")) - - self.irf = Irf(bkg=bkg, aeff=aeff, rmf=rmf) - - def estimate_sensitivity(self): - obs_time = self.config["analysis"]["obs_time"]["value"] * u.Unit( - self.config["analysis"]["obs_time"]["unit"] - ) - sensitivity_estimator = SensitivityEstimator(irf=self.irf, livetime=obs_time) - sensitivity_estimator.run() - self.sens = sensitivity_estimator.results_table - - self.add_sensitivity_to_irf() - - def add_sensitivity_to_irf(self): - t = Table() - t["ENERG_LO"] = Column( - self.irf.bkg.energy.lo.value, - unit="TeV", - description="energy min", - format="E", - ) - t["ENERG_HI"] = Column( - self.irf.bkg.energy.hi.value, - unit="TeV", - description="energy max", - format="E", - ) - t["SENSITIVITY"] = Column( - self.sens["e2dnde"], - unit="erg/(cm2 s)", - description="sensitivity", - format="E", - ) - t["EXCESS"] = Column( - self.sens["excess"], unit="", description="excess", format="E" - ) - t["BKG"] = Column( - self.sens["background"], unit="", description="bkg", format="E" - ) - - filename = os.path.join(self.outdir, "irf.fits.gz") - hdulist = fits.open(filename, memmap=False, mode="update") - col_list = [ - fits.Column(col.name, col.format, unit=str(col.unit), array=col.data) - for col in t.columns.values() - ] - sens_hdu = fits.BinTableHDU.from_columns(col_list) - sens_hdu.header.set("EXTNAME", "SENSITIVITY") - hdulist.append(sens_hdu) - hdulist.flush() - - -class IrfMaker: - """ - Class building IRF for point-like analysis. - - Parameters - ---------- - config: `dict` - Configuration file - evt_dict : `dict` - Dict for each particle type, containing a table with the required column for IRF computing. - TODO: define explicitely the name it expects. - outdir: `str` - Output directory where analysis results is saved - """ - - def __init__(self, config, evt_dict, outdir): - self.config = config - self.outdir = outdir - - # Read data saved on disk - self.evt_dict = evt_dict - # Loop on the particle type - for particle in evt_dict.keys(): - self.evt_dict[particle] = evt_dict[particle] - - cfg_binning_ereco = config["analysis"]["ereco_binning"] - cfg_binning_etrue = config["analysis"]["etrue_binning"] - self.nbin_ereco = cfg_binning_ereco["nbin"] - self.nbin_etrue = cfg_binning_etrue["nbin"] - # Binning - self.ereco = np.logspace( - np.log10(cfg_binning_ereco["emin"]), - np.log10(cfg_binning_ereco["emax"]), - self.nbin_ereco + 1, - ) - self.etrue = np.logspace( - np.log10(cfg_binning_etrue["emin"]), - np.log10(cfg_binning_etrue["emax"]), - self.nbin_etrue + 1, - ) - - def build_irf(self, angular_cut): - bkg_rate = self.make_bkg_rate(angular_cut) - psf = self.make_point_spread_function() - area = self.make_effective_area( - hdu_name="SPECRESP", apply_score_cut=True, apply_angular_cut=True - ) # Effective area with cuts applied - edisp = self.make_energy_dispersion() - - # Add usefull effective areas for debugging - area_no_cuts = self.make_effective_area( - apply_score_cut=False, - apply_angular_cut=False, - hdu_name="SPECRESP (NO CUTS)", - ) # Effective area with cuts applied - area_no_score_cut = self.make_effective_area( - apply_score_cut=False, - apply_angular_cut=True, - hdu_name="SPECRESP (WITH ANGULAR CUT)", - ) # Effective area with cuts applied - area_no_angular_cut = self.make_effective_area( - apply_score_cut=True, - apply_angular_cut=False, - hdu_name="SPECRESP (WITH SCORE CUT)", - ) # Effective area with cuts applied - - # Primary header - n = np.arange(100.0) - primary_hdu = fits.PrimaryHDU(n) - - # Fill HDU list - hdulist = fits.HDUList( - [ - primary_hdu, - area, - psf, - edisp, - bkg_rate, - area_no_cuts, - area_no_score_cut, - area_no_angular_cut, - ] - ) - - hdulist.writeto(os.path.join(self.outdir, "irf.fits.gz"), overwrite=True) - - def make_bkg_rate(self, angular_cut): - """Build background rate - - Parameters - ---------- - angular_cut: `astropy.units.Quantity`, dimension N reco energy bin - Array of angular cut to apply in each reconstructed energy bin - to estimate the acceptance ratio for the background estimate - """ - nbin = self.nbin_ereco - energ_lo = np.zeros(nbin) - energ_hi = np.zeros(nbin) - bgd = np.zeros(nbin) - - obs_time = self.config["analysis"]["obs_time"]["value"] * u.Unit( - self.config["analysis"]["obs_time"]["unit"] - ) - - for ibin, (emin, emax) in enumerate(zip(self.ereco[0:-1], self.ereco[1:])): - energ_lo[ibin] = emin - energ_hi[ibin] = emax - - # References - data_p = self.evt_dict["proton"] - data_e = self.evt_dict["electron"] - - # Compute number of events passing cuts selection - n_p = sum( - data_p[ - (data_p["ENERGY"] >= emin) - & (data_p["ENERGY"] < emax) - & (data_p["pass_best_cutoff"]) - ]["weight"] - ) - - n_e = sum( - data_e[ - (data_e["ENERGY"] >= emin) - & (data_e["ENERGY"] < emax) - & (data_e["pass_best_cutoff"]) - ]["weight"] - ) - - # Correct number of background due to acceptance - acceptance_g = 2 * np.pi * (1 - np.cos(angular_cut[ibin])) - acceptance_p = ( - 2 - * np.pi - * ( - 1 - - np.cos( - self.config["particle_information"]["proton"]["offset_cut"] - * u.deg.to("rad") - ) - ) - ) - acceptance_e = ( - 2 - * np.pi - * ( - 1 - - np.cos( - self.config["particle_information"]["electron"]["offset_cut"] - * u.deg.to("rad") - ) - ) - ) - - n_p *= acceptance_g / acceptance_p - n_e *= acceptance_g / acceptance_e - bgd[ibin] = (n_p + n_e) / obs_time.to("s").value - - t = Table() - t["ENERG_LO"] = Column( - energ_lo, unit="TeV", description="energy min", format="E" - ) - t["ENERG_HI"] = Column( - energ_hi, unit="TeV", description="energy max", format="E" - ) - t["BGD"] = Column(bgd, unit="TeV", description="Background", format="E") - - return IrfMaker._make_hdu("BACKGROUND", t, ["ENERG_LO", "ENERG_HI", "BGD"]) - - def make_point_spread_function(self, radius=68): - """Buil point spread function with radius containment `radius`""" - nbin = self.nbin_ereco - energ_lo = np.zeros(nbin) - energ_hi = np.zeros(nbin) - psf = np.zeros(nbin) - - for ibin, (emin, emax) in enumerate(zip(self.ereco[0:-1], self.ereco[1:])): - energ_lo[ibin] = emin - energ_hi[ibin] = emax - - # References - data_g = self.evt_dict["gamma"] - - # Select data passing cuts selection - sel = data_g.loc[ - (data_g["ENERGY"] >= emin) - & (data_g["ENERGY"] < emax) - & (data_g["pass_best_cutoff"]), - [self.config["column_definition"]["angular_distance_to_the_src"]], - ] - - # Compute PSF - psf[ibin] = np.percentile( - sel[self.config["column_definition"]["angular_distance_to_the_src"]], - radius, - ) - - t = Table() - t["ENERG_LO"] = Column( - energ_lo, unit="TeV", description="energy min", format="E" - ) - t["ENERG_HI"] = Column( - energ_hi, unit="TeV", description="energy max", format="E" - ) - t["PSF68"] = Column(psf, unit="TeV", description="PSF", format="E") - - return IrfMaker._make_hdu( - "POINT SPREAD FUNCTION", t, ["ENERG_LO", "ENERG_HI", "PSF68"] - ) - - def make_effective_area( - self, hdu_name, apply_score_cut=True, apply_angular_cut=True - ): - """ - Compute effective area. - - Parameters - ---------- - hdu_name: str - Name of the FITS file HDU to write in. - apply_score_cut: bool - If True, apply the best cut on particle classification. - apply_angular_cut: bool - If True, apply the best cut on angular separation. - - Returns - ------- - hdu: astropy.io.fits.HDUList - Bintable HDU for the effective area. - - """ - nbin = len(self.etrue) - 1 - energy_true_lo = np.zeros(nbin) - energy_true_hi = np.zeros(nbin) - area = np.zeros(nbin) - - # Get simulation infos - cfg_particule = self.config["particle_information"]["gamma"] - simu_index = cfg_particule["gen_gamma"] - index = 1.0 - simu_index # for futur integration - nsimu_tot = float(cfg_particule["n_files"]) * float( - cfg_particule["n_events_per_file"] - ) - emin_simu = cfg_particule["e_min"] - emax_simu = cfg_particule["e_max"] - area_simu = (np.pi * cfg_particule["gen_radius"] ** 2) * u.Unit("m2") - - for ibin in range(nbin): - - emin = self.etrue[ibin] * u.TeV - emax = self.etrue[ibin + 1] * u.TeV - - # References - data_g = self.evt_dict["gamma"] - - # Conditions to select gamma-rays - condition = (data_g["TRUE_ENERGY"] >= emin) & (data_g["TRUE_ENERGY"] < emax) - if apply_score_cut is True: - condition &= data_g["pass_best_cutoff"] - if apply_angular_cut is True: - condition &= data_g["pass_angular_cut"] - - # Compute number of events passing cuts selection - sel = len(data_g.loc[condition, ["weight"]]) - - # Compute number of number of events in simulation - simu_evts = ( - float(nsimu_tot) - * (emax.value ** index - emin.value ** index) - / (emax_simu ** index - emin_simu ** index) - ) - - area[ibin] = (sel / simu_evts) * area_simu.value - energy_true_lo[ibin] = emin.value - energy_true_hi[ibin] = emax.value - - table_energy = Table() - table_energy["ETRUE_LO"] = Column( - energy_true_lo, - unit="TeV", - description="energy min", - format=str(len(energy_true_lo)) + "E", - ) - table_energy["ETRUE_HI"] = Column( - energy_true_hi, - unit="TeV", - description="energy max", - format=str(len(energy_true_hi)) + "E", - ) - - # Needed for format, a bit hacky... - # Those value are artificial. In the DL3 format, the IRFs are offset FOV dependant, here name theta. - # For point-like MC simulation produced at only one offset theta0 this trick is needed because those IRF can - # only be use for sources located at theta0 from the camera center. We give the same value for the IRFs for two - # artificial offsets, like this the interpolation at theta0 in the high level analysis tools will - # be correct. This will be remove when we use diffuse MC simulation that will allow to define IRF properly at - # different offset in the FOV. - theta_lo = [0.0, 1.0] - theta_hi = [1.0, 2.0] - table_theta = Table() - table_theta["THETA_LO"] = Column( - theta_lo, - unit="deg", - description="theta min", - format=str(len(theta_lo)) + "E", - ) - table_theta["THETA_HI"] = Column( - theta_hi, - unit="deg", - description="theta max", - format=str(len(theta_hi)) + "E", - ) - - extended_area = np.resize(area, (len(theta_lo), area.shape[0])) - dim_extended_area = len(table_energy["ETRUE_LO"]) * len(table_theta["THETA_LO"]) - - aeff_2D = Table([extended_area], names=["AEFF"]) - aeff_2D["AEFF"].unit = u.Unit("m2") - aeff_2D["AEFF"].format = str(dim_extended_area) + "E" - - hdu = IrfMaker._make_aeff_hdu(table_energy, table_theta, aeff_2D) - - return hdu - - def make_energy_dispersion(self): - migra = np.linspace(0.0, 3.0, 300 + 1) - etrue = np.logspace(np.log10(0.01), np.log10(10000), 60 + 1) - counts = np.zeros([len(migra) - 1, len(etrue) - 1]) - - # Select events - data_g = self.evt_dict["gamma"] - data_g = data_g[ - (data_g["pass_best_cutoff"]) & (data_g["pass_angular_cut"]) - ].copy() - - for imigra in range(len(migra) - 1): - migra_min = migra[imigra] - migra_max = migra[imigra + 1] - - for ietrue in range(len(etrue) - 1): - emin = etrue[ietrue] - emax = etrue[ietrue + 1] - - sel = len( - data_g[ - (data_g["TRUE_ENERGY"] >= emin) - & (data_g["TRUE_ENERGY"] < emax) - & ((data_g["ENERGY"] / data_g["TRUE_ENERGY"]) >= migra_min) - & ((data_g["ENERGY"] / data_g["TRUE_ENERGY"]) < migra_max) - ] - ) - counts[imigra][ietrue] = sel - - table_energy = Table() - table_energy["ETRUE_LO"] = Column( - etrue[:-1], - unit="TeV", - description="energy min", - format=str(len(etrue) - 1) + "E", - ) - table_energy["ETRUE_HI"] = Column( - etrue[1:], - unit="TeV", - description="energy max", - format=str(len(etrue) - 1) + "E", - ) - - table_migra = Table() - table_migra["MIGRA_LO"] = Column( - migra[:-1], - unit="", - description="migra min", - format=str(len(migra) - 1) + "E", - ) - table_migra["MIGRA_HI"] = Column( - migra[1:], - unit="", - description="migra max", - format=str(len(migra) - 1) + "E", - ) - - # Needed for format, a bit hacky... - # Those value are artificial. In the DL3 format, the IRFs are offset FOV dependant, here name theta. - # For point-like MC simulation produced at only one offset theta0 this trick is needed because those IRF can - # only be use for sources located at theta0 from the camera center. We give the same value for the IRFs for two - # artificial offsets, like this the interpolation at theta0 in the high level analysis tools will - # be correct. This will be remove when we use diffuse MC simulation that will allow to define IRF properly at - # different offset in the FOV. - theta_lo = [0.0, 1.0] - theta_hi = [1.0, 2.0] - table_theta = Table() - table_theta["THETA_LO"] = Column( - theta_lo, - unit="deg", - description="theta min", - format=str(len(theta_lo)) + "E", - ) - table_theta["THETA_HI"] = Column( - theta_hi, - unit="deg", - description="theta max", - format=str(len(theta_hi)) + "E", - ) - - extended_mig_matrix = np.resize( - counts, (len(theta_lo), counts.shape[0], counts.shape[1]) - ) - dim_matrix = ( - len(table_energy["ETRUE_LO"]) - * len(table_migra["MIGRA_LO"]) - * len(table_theta["THETA_LO"]) - ) - matrix = Table([extended_mig_matrix], names=["MATRIX"]) - matrix["MATRIX"].unit = u.Unit("") - matrix["MATRIX"].format = str(dim_matrix) + "E" - hdu = IrfMaker._make_edisp_hdu(table_energy, table_migra, table_theta, matrix) - - return hdu - - @classmethod - def _make_hdu(cls, hdu_name, t, cols): - """List of columns""" - col_list = [ - fits.Column(col.name, col.format, unit=str(col.unit), array=col.data) - for col in t.columns.values() - ] - hdu = fits.BinTableHDU.from_columns(col_list) - hdu.header.set("EXTNAME", hdu_name) - return hdu - - @classmethod - def _make_aeff_hdu(cls, table_energy, table_theta, aeff): - """Create the Bintable HDU for the effective area describe here - https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/aeff/index.html#effective-area-vs-true-energy - """ - table = Table( - { - "ENERG_LO": table_energy["ETRUE_LO"][np.newaxis, :].data - * table_energy["ETRUE_LO"].unit, - "ENERG_HI": table_energy["ETRUE_HI"][np.newaxis, :].data - * table_energy["ETRUE_HI"].unit, - "THETA_LO": table_theta["THETA_LO"][np.newaxis, :].data - * table_theta["THETA_LO"].unit, - "THETA_HI": table_theta["THETA_HI"][np.newaxis, :].data - * table_theta["THETA_HI"].unit, - "EFFAREA": aeff["AEFF"].data[np.newaxis, :, :] * aeff["AEFF"].unit, - } - ) - - header = fits.Header() - header["HDUDOC"] = ( - "https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/index.html", - "", - ) - header["HDUCLASS"] = "GADF", "" - header["HDUCLAS1"] = "RESPONSE", "" - header["HDUCLAS2"] = "EFF_AREA", "" - header["HDUCLAS3"] = "POINT-LIKE", "" - header["HDUCLAS4"] = "AEFF_2D", "" - - aeff_hdu = fits.BinTableHDU(table, header, name="EFFECTIVE AREA") - - # primary_hdu = fits.PrimaryHDU() - # hdulist = fits.HDUList([primary_hdu, aeff_hdu]) - - # return hdulist - return aeff_hdu - - @classmethod - def _make_edisp_hdu(cls, table_energy, table_migra, table_theta, matrix): - """Create the Bintable HDU for the energy dispersion describe here - https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/edisp/index.html - """ - - table = Table( - { - "ENERG_LO": table_energy["ETRUE_LO"][np.newaxis, :].data - * table_energy["ETRUE_LO"].unit, - "ENERG_HI": table_energy["ETRUE_HI"][np.newaxis, :].data - * table_energy["ETRUE_HI"].unit, - "MIGRA_LO": table_migra["MIGRA_LO"][np.newaxis, :].data - * table_migra["MIGRA_LO"].unit, - "MIGRA_HI": table_migra["MIGRA_HI"][np.newaxis, :].data - * table_migra["MIGRA_HI"].unit, - "THETA_LO": table_theta["THETA_LO"][np.newaxis, :].data - * table_theta["THETA_LO"].unit, - "THETA_HI": table_theta["THETA_HI"][np.newaxis, :].data - * table_theta["THETA_HI"].unit, - "MATRIX": matrix["MATRIX"][np.newaxis, :, :] * matrix["MATRIX"].unit, - } - ) - - header = fits.Header() - header["HDUDOC"] = ( - "https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/index.html", - "", - ) - header["HDUCLASS"] = "GADF", "" - header["HDUCLAS1"] = "RESPONSE", "" - header["HDUCLAS2"] = "EDISP", "" - header["HDUCLAS3"] = "POINT-LIKE", "" - header["HDUCLAS4"] = "EDISP_2D", "" - - edisp_hdu = fits.BinTableHDU(table, header, name="ENERGY DISPERSION") - - # primary_hdu = fits.PrimaryHDU() - # hdulist = fits.HDUList([primary_hdu, edisp_hdu]) - # - # return hdulist - return edisp_hdu diff --git a/pyirf/perf/utils.py b/pyirf/perf/utils.py deleted file mode 100644 index bf070697e..000000000 --- a/pyirf/perf/utils.py +++ /dev/null @@ -1,67 +0,0 @@ -import numpy as np -import pickle -import gzip - - -def percentiles(values, bin_values, bin_edges, percentile): - # Seems complicated for vector defined as [inf, inf, .., inf] - percentiles_binned = np.squeeze( - np.full((len(bin_edges) - 1, len(values.shape)), np.inf) - ) - err_percentiles_binned = np.squeeze( - np.full((len(bin_edges) - 1, len(values.shape)), np.inf) - ) - for i, (bin_l, bin_h) in enumerate(zip(bin_edges[:-1], bin_edges[1:])): - try: - print(i) - print(bin_l) - print(bin_h) - distribution = values[(bin_values > bin_l) & (bin_values < bin_h)] - percentiles_binned[i] = np.percentile(distribution, percentile) - print(percentiles_binned[i]) - err_percentiles_binned[i] = percentiles_binned[i] / np.sqrt( - len(distribution) - ) - except IndexError: - pass - return percentiles_binned.T, err_percentiles_binned.T - - -def plot_hist(ax, data, edges, norm=False, yerr=False, hist_kwargs=None, error_kw=None): - """Utility function to plot histogram""" - - hist_kwargs = hist_kwargs or {} - error_kw = error_kw or {} - - weights = np.ones_like(data) - if norm is True: - weights = weights / float(np.sum(data)) - if yerr is True: - yerr = np.sqrt(data) * weights - else: - yerr = np.zeros(len(data)) - - centers = 0.5 * (edges[1:] + edges[:-1]) - width = edges[1:] - edges[:-1] - ax.bar( - centers, - data * weights, - width=width, - yerr=yerr, - error_kw=error_kw, - **hist_kwargs - ) - - return ax - - -def save_obj(obj, name): - """Save object in binary""" - with gzip.open(name, "wb") as f: - pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL) - - -def load_obj(name): - """Load object in binary""" - with gzip.open(name, "rb") as f: - return pickle.load(f) diff --git a/pyirf/resources/config.yml b/pyirf/resources/config.yml deleted file mode 100644 index f73baf0f8..000000000 --- a/pyirf/resources/config.yml +++ /dev/null @@ -1,114 +0,0 @@ -# Example configuration file for PYIRF - -# NOTE: It is still partly based on the one used in protopipe.perf, but it -# will progressively use GADF nomenclature and be generalized. - -general: - # part of the DL2 filename(s) common between particle types - template_input_file: '' - # Output table name - output_table_name: 'table_best_cutoff' - # where is the DL2 data - indir: '' - # where to store DL3 data - outdir: '' - -analysis: - - # Theta square cut optimisation (opti, fixed, r68) - thsq_opt: - type: 'r68' - value: 0.2 # In degree, necessary for type fixed - - # Normalisation between ON and OFF regions - alpha: 0.2 - - # Minimimal significance - min_sigma: 5 - - # Minimal number of gamma-ray-like - min_excess: 10 - - # Minimal fraction of background events for excess comparison - bkg_syst: 0.05 - - # Reco energy binning - ereco_binning: # TeV - emin: 0.05 - emax: 50 - nbin: 21 - - # True energy binning - etrue_binning: # TeV - emin: 0.05 - emax: 50 - nbin: 42 - -# ============================================================================= -# TO REVIEW / GENERALIZE -# ============================================================================= - -# This section comes from protopipe and is related to the particular production -# used for its testing - -particle_information: - gamma: - n_events_per_file: 22500000 # 10**5 * 10 - n_files: 1 - e_min: 0.05 - e_max: 50 - gen_radius: 1000 - diff_cone: 0 - gen_gamma: 2 - - proton: - n_events_per_file: 3750000000 # 2 * 10**5 * 20 - n_files: 1 - e_min: 0.01 - e_max: 100 - gen_radius: 2500 - diff_cone: 1 - gen_gamma: 2 - offset_cut: 1. - - electron: - n_events_per_file: 450000000 # 10**5 * 20 - n_files: 1 - e_min: 0.005 - e_max: 5 - gen_radius: 1000 - diff_cone: 1 - gen_gamma: 2 - offset_cut: 1. - -# ============================================================================= -# PLEASE, COMPILE THE FOLLOWING PART DEPENDING ON THE CONTENT OF YOUR DL2 FILES -# ============================================================================= - -column_definition: - - # Event identification number - EVENT_ID: 'EVENT_ID' - # Reconstructed event energy - ENERGY: 'ENERGY' - # Event quality partition - EVENT_TYPE: 'GH_MVA' - # Telescope multiplicity. Number of telescopes that have seen the event. - MULTIP: 'MULTIP' - # Reconstructed altitude - ALT: 'ALT' - # Reconstructed azimuth - AZ: 'AZ' - # Observation identification number - OBS_ID: 'OBS_ID' - # True energy - TRUE_ENERGY: 'MC_ENERGY' - # True altitude - TRUE_ALT: 'MC_ALT' - # True azimuth - TRUE_AZ: 'MC_AZ' - # Column names for classification output (protopipe) - classification_output: - name: 'gammaness' # should be substituted by EVENT_TYPE - range: [0, 1] # technically always true (some algorithms could have different domains?) - angular_distance_to_the_src: 'THETA' # WARNING: for point-source simulations! diff --git a/pyirf/scripts/lst_performance.py b/pyirf/scripts/lst_performance.py deleted file mode 100644 index c6f7380e2..000000000 --- a/pyirf/scripts/lst_performance.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env python - -import os -import argparse -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import astropy.units as u -from astropy.io import fits -from astropy.coordinates.angle_utilities import angular_separation -from gammapy.spectrum import cosmic_ray_flux, CrabSpectrum -import ctaplot -from copy import deepcopy - -from pyirf.io.io import load_config, get_simu_info -from pyirf.perf import (CutsOptimisation, - CutsDiagnostic, - CutsApplicator, - IrfMaker, - SensitivityMaker, - ) - - -from gammapy.irf import EnergyDispersion2D - - - - -def read_and_update_dl2(filepath, tel_id=1, filters=['intensity > 0']): - """ - read DL2 data from lstchain file and update it to be compliant with irf Maker - """ - dl2_params_lstcam_key = 'dl2/event/telescope/parameters/LST_LSTCam' # lstchain DL2 files - data = pd.read_hdf(filepath, key=dl2_params_lstcam_key) - data = deepcopy(data.query(f'tel_id == {tel_id}')) - for filter in filters: - data = deepcopy(data.query(filter)) - - # angles are in degrees in protopipe - data['xi'] = pd.Series(angular_separation(data.reco_az.values * u.rad, - data.reco_alt.values * u.rad, - data.mc_az.values * u.rad, - data.mc_alt.values * u.rad, - ).to(u.deg).value, - index=data.index) - - data['offset'] = pd.Series(angular_separation(data.reco_az.values * u.rad, - data.reco_alt.values * u.rad, - data.mc_az_tel.values * u.rad, - data.mc_alt_tel.values * u.rad, - ).to(u.deg).value, - index=data.index) - - for key in ['mc_alt', 'mc_az', 'reco_alt', 'reco_az', 'mc_alt_tel', 'mc_az_tel']: - data[key] = np.rad2deg(data[key]) - - return data - - -def main(args): - paths = {} - paths['gamma'] = args.dl2_gamma_filename - paths['proton'] = args.dl2_proton_filename - paths['electron'] = args.dl2_electron_filename - - # Read configuration file - cfg = load_config(args.config_file) - # cfg = configuration() - - cfg['analysis']['obs_time'] = {} - cfg['analysis']['obs_time']['unit'] = u.h - cfg['analysis']['obs_time']['value'] = args.obs_time - - cfg['general']['outdir'] = args.outdir - - # Create output directory if necessary - outdir = os.path.join(cfg['general']['outdir'], 'irf_ThSq_{}_Time{:.2f}{}'.format( - cfg['analysis']['thsq_opt']['type'], - cfg['analysis']['obs_time']['value'], - cfg['analysis']['obs_time']['unit']) - ) - if not os.path.exists(outdir): - os.makedirs(outdir) - - # Load data - particles = ['gamma', 'electron', 'proton'] - evt_dict = dict() # Contain DL2 file for each type of particle - for particle in particles: - infile = paths[particle] - evt_dict[particle] = read_and_update_dl2(infile) - cfg = get_simu_info(infile, particle, config=cfg) - - # Apply offset cut to proton and electron - for particle in ['electron', 'proton']: - evt_dict[particle] = evt_dict[particle].query('offset <= {}'.format( - cfg['particle_information'][particle]['offset_cut']) - ) - - # Add required data in configuration file for future computation - for particle in particles: - # cfg['particle_information'][particle]['n_files'] = \ - # len(np.unique(evt_dict[particle]['obs_id'])) - cfg['particle_information'][particle]['n_simulated'] = \ - cfg['particle_information'][particle]['n_files'] * cfg['particle_information'][particle][ - 'n_events_per_file'] - - # Define model for the particles - model_dict = {'gamma': CrabSpectrum('hegra').model, - 'proton': cosmic_ray_flux, - 'electron': cosmic_ray_flux} - - # Reco energy binning - cfg_binning = cfg['analysis']['ereco_binning'] - # ereco = np.logspace(np.log10(cfg_binning['emin']), - # np.log10(cfg_binning['emax']), - # cfg_binning['nbin'] + 1) * u.TeV - ereco = ctaplot.ana.irf_cta().E_bin * u.TeV - - # Handle theta square cut optimisation - # (compute 68 % containment radius PSF if necessary) - thsq_opt_type = cfg['analysis']['thsq_opt']['type'] - print(thsq_opt_type) - # if thsq_opt_type in 'fixed': - # thsq_values = np.array([cfg['analysis']['thsq_opt']['value']]) * u.deg - # print('Using fixed theta cut: {}'.format(thsq_values)) - # elif thsq_opt_type in 'opti': - # thsq_values = np.arange(0.05, 0.40, 0.01) * u.deg - # print('Optimising theta cut for: {}'.format(thsq_values)) - if thsq_opt_type != 'r68': - raise ValueError("only r68 supported at the moment") - elif thsq_opt_type in 'r68': - print('Using R68% theta cut') - print('Computing...') - cfg_binning = cfg['analysis']['ereco_binning'] - ereco = np.logspace(np.log10(cfg_binning['emin']), - np.log10(cfg_binning['emax']), - cfg_binning['nbin'] + 1) * u.TeV - ereco = ctaplot.ana.irf_cta().E_bin * u.TeV - radius = 68 - - thsq_values = list() - for ibin in range(len(ereco) - 1): - emin = ereco[ibin] - emax = ereco[ibin + 1] - - energy_query = 'reco_energy > {} and reco_energy <= {}'.format( - emin.value, emax.value - ) - data = evt_dict['gamma'].query(energy_query).copy() - - min_stat = 0 - if len(data) <= min_stat: - print(' ==> Not enough statistics:') - print('To be handled...') - thsq_values.append(0.3) - continue - - psf = np.percentile(data['offset'], radius) - - thsq_values.append(psf) - thsq_values = np.array(thsq_values) * u.deg - # Set 0.05 as a lower value - idx = np.where(thsq_values.value < 0.05) - thsq_values[idx] = 0.05 * u.deg - print(f'Using theta cut: {thsq_values}') - - # Cuts optimisation - print('### Finding best cuts...') - cut_optimiser = CutsOptimisation( - config=cfg, - evt_dict=evt_dict, - verbose_level=0 - ) - - # Weight events - print('- Weighting events...') - cut_optimiser.weight_events( - model_dict=model_dict, - colname_mc_energy=cfg['column_definition']['mc_energy'] - ) - - # Find best cutoff to reach best sensitivity - print('- Estimating cutoffs...') - cut_optimiser.find_best_cutoff(energy_values=ereco, angular_values=thsq_values) - - # Save results and auxiliary data for diagnostic - print('- Saving results to disk...') - cut_optimiser.write_results( - outdir, '{}.fits'.format(cfg['general']['output_table_name']), - format='fits' - ) - - # Cuts diagnostic - print('### Building cut diagnostics...') - cut_diagnostic = CutsDiagnostic(config=cfg, indir=outdir) - cut_diagnostic.plot_optimisation_summary() - cut_diagnostic.plot_diagnostics() - - # Apply cuts and save data - print('### Applying cuts to data...') - cut_applicator = CutsApplicator(config=cfg, evt_dict=evt_dict, outdir=outdir) - cut_applicator.apply_cuts() - - # Irf Maker - print('### Building IRF...') - irf_maker = IrfMaker(config=cfg, evt_dict=evt_dict, outdir=outdir) - irf_maker.build_irf() - - # Sensitivity maker - print('### Estimating sensitivity...') - sensitivity_maker = SensitivityMaker(config=cfg, outdir=outdir) - sensitivity_maker.load_irf() - sensitivity_maker.estimate_sensitivity() - - -def plot_sensitivity(irf_filename, ax=None, **kwargs): - """ - Plot the sensitivity - - Parameters - ---------- - irf_filename: path - ax: - kwargs: - - Returns - ------- - ax - """ - ax = ctaplot.plot_sensitivity_cta_performance('north', color='black', ax=ax) - - with fits.open(irf_filename) as irf: - t = irf['SENSITIVITY'] - elo = t.data['ENERG_LO'] - ehi = t.data['ENERG_HI'] - energy = (elo + ehi) / 2. - sens = t.data['SENSITIVITY'] - - if 'fmt' not in kwargs: - kwargs['fmt'] = 'o' - - ax.errorbar(energy, sens, - xerr=(ehi - elo) / 2., - **kwargs - ) - - ax.legend(fontsize=17) - ax.grid(which='both') - return ax - - -def plot_angular_resolution(irf_filename, ax=None, **kwargs): - """ - Plot angular resolution from an IRF file - - Parameters - ---------- - irf_filename - ax - kwargs - - Returns - ------- - - """ - - ax = ctaplot.plot_angular_resolution_cta_performance('north', color='black', ax=ax) - - with fits.open(irf_filename) as irf: - psf_hdu = irf['POINT SPREAD FUNCTION'] - e_lo = psf_hdu.data['ENERG_LO'] - e_hi = psf_hdu.data['ENERG_HI'] - energy = (e_lo + e_hi) / 2. - psf = psf_hdu.data['PSF68'] - - if 'fmt' not in kwargs: - kwargs['fmt'] = 'o' - - ax.errorbar(energy, psf, - xerr=(e_hi - e_lo) / 2., - **kwargs, - ) - - ax.legend(fontsize=17) - ax.grid(which='both') - return ax - - -def plot_energy_resolution_hdf(gamma_filename, ax=None, **kwargs): - """ - Plot angular resolution from an IRF file - - Parameters - ---------- - irf_filename - ax - kwargs - - Returns - ------- - - """ - data = pd.read_hdf(gamma_filename) - - ax = ctaplot.plot_angular_resolution_cta_performance('north', color='black', label='CTA North', ax=ax) - ax = ctaplot.plot_energy_resolution(data.mc_energy, data.reco_energy, ax=ax, **kwargs) - ax.grid(which='both') - ax.set_title('Energy resoluton', fontsize=18) - ax.legend() - return ax - - -def plot_energy_resolution(irf_file, ax=None, **kwargs): - """ - Plot angular resolution from an IRF file - Parameters - ---------- - irf_filename - ax - kwargs - Returns - ------- - """ - - e2d = EnergyDispersion2D.read(irf_file, hdu='ENERGY DISPERSION') - edisp = e2d.to_energy_dispersion('0 deg') - - energy_bin = np.logspace(-1.5, 1, 15) - e = np.sqrt(energy_bin[1:] * energy_bin[:-1]) - xerr = (e - energy_bin[:-1], energy_bin[1:] - e) - r = edisp.get_resolution(e) - - if 'fmt' not in kwargs: - kwargs['fmt'] = 'o' - - ax.errorbar(e, r, xerr=xerr, **kwargs) - ax.set_xscale('log') - ax.grid(True, which='both') - ax.set_title('Energy resoluton') - ax.set_xlabel('Energy [TeV]') - ax.legend() - return ax - - -def plot_background_rate(irf_filename, ax=None, **kwargs): - """ - - Returns - ------- - - """ - from ctaplot.io.dataset import load_any_resource - - ax = plt.gca() if ax is None else ax - - bkg = load_any_resource('CTA-Performance-prod3b-v2-North-20deg-50h-BackgroundSqdeg.txt') - ax.loglog((bkg[0] + bkg[1]) / 2., bkg[2], label='CTA performances North', color='black') - - with fits.open(irf_filename) as irf: - elo = irf['BACKGROUND'].data['ENERG_LO'] - ehi = irf['BACKGROUND'].data['ENERG_HI'] - energy = (elo + ehi) / 2. - bkg = irf['BACKGROUND'].data['BGD'] - - if 'fmt' not in kwargs: - kwargs['fmt'] = 'o' - - ax.errorbar(energy, bkg, - xerr=(ehi - elo) / 2., - **kwargs - ) - - ax.legend(fontsize=17) - ax.grid(which='both') - return ax - - -def plot_effective_area(irf_filename, ax=None, **kwargs): - """ - - Parameters - ---------- - irf_filename - ax - kwargs - - Returns - ------- - - """ - - ax = ctaplot.plot_effective_area_cta_performance('north', color='black', ax=ax) - - with fits.open(irf_filename) as irf: - elo = irf['SPECRESP'].data['ENERG_LO'] - ehi = irf['SPECRESP'].data['ENERG_HI'] - energy = (elo + ehi) / 2. - eff_area = irf['SPECRESP'].data['SPECRESP'] - eff_area_no_cut = irf['SPECRESP (NO CUTS)'].data['SPECRESP (NO CUTS)'] - - if 'label' not in kwargs: - kwargs['label'] = 'Effective area [m2]' - else: - user_label = kwargs['label'] - kwargs['label'] = f'{user_label}' - - ax.loglog(energy, eff_area, **kwargs) - - kwargs['label'] = f"{kwargs['label']} (no cuts)" - kwargs['linestyle'] = '--' - - ax.loglog(energy, eff_area_no_cut, **kwargs) - - ax.legend(fontsize=17) - ax.grid(which='both') - return ax - - -if __name__ == '__main__': - - # performance_default_config = pkg_resources.resource_filename('pyirf', 'resources/performance.yml') - performance_default_config = os.path.join(os.path.dirname(__file__), "../resources/performance.yml") - - parser = argparse.ArgumentParser(description='Make performance files') - - parser.add_argument( - '--obs_time', - dest='obs_time', - type=float, - default=50, - help='Observation time in hours' - ) - - parser.add_argument('--dl2_gamma', '-g', - dest='dl2_gamma_filename', - type=str, - required=True, - help='path to the gamma dl2 file' - ) - - parser.add_argument('--dl2_proton', '-p', - dest='dl2_proton_filename', - type=str, - required=True, - help='path to the proton dl2 file' - ) - - parser.add_argument('--dl2_electron', '-e', - dest='dl2_electron_filename', - type=str, - required=True, - help='path to the electron dl2 file' - ) - - parser.add_argument('--outdir', '-o', - dest='outdir', - type=str, - default='.', - help="Output directory" - ) - - parser.add_argument('--conf', '-c', - dest='config_file', - type=str, - default=performance_default_config, - help="Optional. Path to a config file." - " If none is given, the standard performance config is used" - ) - - args = parser.parse_args() - - main(args) - - irf_filename = os.path.join(args.outdir, 'irf_ThSq_r68_Time50.00h/irf.fits.gz') - fig_output = os.path.join(args.outdir, 'irf_ThSq_r68_Time50.00h/') - - fig, ax = plt.subplots(figsize=(12, 7)) - ax = plot_angular_resolution(irf_filename, ax=ax, label='LST1 (lstchain)') - fig.savefig(os.path.join(fig_output, 'angular_resolution.png'), dpi=200, fmt='png') - - fig, ax = plt.subplots(figsize=(12, 7)) - ax = plot_background_rate(irf_filename, ax=ax, label='LST1 (lstchain)') - fig.savefig(os.path.join(fig_output, 'background_rate.png'), dpi=200, fmt='png') - - fig, ax = plt.subplots(figsize=(12, 7)) - ax = plot_effective_area(irf_filename, ax=ax, label='LST1 (lstchain)') - fig.savefig(os.path.join(fig_output, 'effective_area.png'), dpi=200, fmt='png') - - fig, ax = plt.subplots(figsize=(12, 7)) - ax = plot_sensitivity(irf_filename, ax=ax, label='LST1 (lstchain)') - fig.savefig(os.path.join(fig_output, 'sensitivity.png'), dpi=200, fmt='png') - - gamma_filename = os.path.join(args.outdir, 'irf_ThSq_r68_Time50.00h/gamma_processed.h5') - fig, ax = plt.subplots(figsize=(12, 7)) - ax = plot_energy_resolution(irf_filename, ax=ax, label='LST1 (lstchain)') - fig.savefig(os.path.join(fig_output, 'energy_resolution.png'), dpi=200, fmt='png') diff --git a/pyirf/scripts/make_DL3.py b/pyirf/scripts/make_DL3.py deleted file mode 100644 index d5932f5d3..000000000 --- a/pyirf/scripts/make_DL3.py +++ /dev/null @@ -1,326 +0,0 @@ -"""Script to produce DL3 data from DL2 data and a configuration file. - -Is it initially thought as a clean start based on old code for reproducing -EventDisplay DL3 data based on the latest release of the GADF format. - -Todo: -- make some config arguments also CLI ones like in ctapipe-stage1-process - - -""" - -# ========================================================================= -# MODULE IMPORTS -# ========================================================================= - -# PYTHON STANDARD LIBRARY - -import argparse -import os - -# THIRD-PARTY MODULES - -import numpy as np -import astropy.units as u -from astropy.table import Table -from astropy.coordinates.angle_utilities import angular_separation -from gammapy.spectrum import cosmic_ray_flux, CrabSpectrum # UPDATE TO LATEST - -# THIS PACKAGE - -from pyirf.io.io import load_config, read_FITS -from pyirf.perf import ( - CutsOptimisation, - CutsDiagnostic, - CutsApplicator, - IrfMaker, - SensitivityMaker, -) - - -def main(): - - # ========================================================================= - # READ INPUT FROM CLI AND CONFIGURATION FILE - # ========================================================================= - - # INPUT FROM CLI - - parser = argparse.ArgumentParser(description="Produce DL3 data from DL2.") - - parser.add_argument( - "--config_file", - type=str, - required=True, - help="A configuration file like pyirf/resources/performance.yaml .", - ) - - parser.add_argument( - "--obs_time", - type=str, - required=True, - help="An observation time given as a string in astropy format e.g. '50h' or '30min'", - ) - - parser.add_argument( - "--pipeline", - type=str, - required=True, - help="Name of the pipeline that has produced the DL2 files.", - ) - - parser.add_argument( - "--debug", action="store_true", help="Print debugging information." - ) - - args = parser.parse_args() - - # INPUT FROM THE CONFIGURATION FILE - - cfg = load_config(args.config_file) - - # Add obs. time to the configuration file - obs_time = u.Quantity(args.obs_time) - cfg["analysis"]["obs_time"] = { - "value": obs_time.value, - "unit": obs_time.unit.to_string("fits"), - } - - # Get input directory - indir = cfg["general"]["indir"] - - # Get template of the input file(s) - template_input_file = cfg["general"]["template_input_file"] - - # Get output directory - outdir = os.path.join( - cfg["general"]["outdir"], - "irf_{}_Time{}{}".format( - args.pipeline, - cfg["analysis"]["obs_time"]["value"], - cfg["analysis"]["obs_time"]["unit"], - ), - ) # and create it if necessary - os.makedirs(outdir, exist_ok=True) - - # ========================================================================= - # READ DL2 DATA AND STORE IT ACCORDING TO GADF - # ========================================================================= - - # Load FITS data - particles = ["gamma", "electron", "proton"] - evt_dict = dict() # Contain DL2 file for each type of particle - for particle in particles: - if args.debug: - print(f"Loading {particle} DL2 data...") - infile = os.path.join(indir, template_input_file.format(particle)) - evt_dict[particle] = read_FITS( - config=cfg, infile=infile, pipeline=args.pipeline, debug=args.debug - ) - - # ========================================================================= - # PRELIMINARY OPERATIONS FOR SPECIFIC PIPELINES - # ========================================================================= - - # Some pipelines could provide some of the DL2 data in different ways - # After this part, DL2 data is supposed to be equivalent, regardless - # of the original pipeline. - - # Later we should move this out of here, perhaps under a "utils" module. - - if args.pipeline == "EventDisplay": - - # EventDisplay provides true and reconstructed directions, so we - # calculate THETA here and we add it to the tables. - - for particle in particles: - - THETA = angular_separation( - evt_dict[particle]["TRUE_AZ"], - evt_dict[particle]["TRUE_ALT"], - evt_dict[particle]["AZ"], - evt_dict[particle]["ALT"], - ) # in degrees - - # Add THETA column - evt_dict[particle]["THETA"] = THETA - - # ========================================================================= - # REST OF THE OPERATIONS (TO BE REFACTORED) - # ========================================================================= - - # Apply offset cut to proton and electron - for particle in ["electron", "proton"]: - - # There seems to be a problem in using pandas from FITS data - # ValueError: Big-endian buffer not supported on little-endian compiler - # I convert to astropy table.... - # should we use only those? - - evt_dict[particle] = Table.from_pandas(evt_dict[particle]) - - if args.debug: - print(particle) - # print(evt_dict[particle].head(n=5)) - print(evt_dict[particle]) - - # print('Initial stat: {} {}'.format(len(evt_dict[particle]), particle)) - - mask_theta = ( - evt_dict[particle]["THETA"] - < cfg["particle_information"][particle]["offset_cut"] - ) - evt_dict[particle] = evt_dict[particle][mask_theta] - # PANDAS EQUIVALENT - # evt_dict[particle] = evt_dict[particle].query( - # "THETA <= {}".format(cfg["particle_information"][particle]["offset_cut"]) - # ) - - # Add required data in configuration file for future computation - for particle in particles: - n_files = cfg["particle_information"][particle]["n_files"] - print(f"{n_files} files for {particle}") - cfg["particle_information"][particle]["n_files"] = len( - np.unique(evt_dict[particle]["OBS_ID"]) - ) - cfg["particle_information"][particle]["n_simulated"] = ( - cfg["particle_information"][particle]["n_files"] - * cfg["particle_information"][particle]["n_events_per_file"] - ) - - # Define model for the particles - model_dict = { - "gamma": CrabSpectrum("hegra").model, - "proton": cosmic_ray_flux, - "electron": cosmic_ray_flux, - } - - # Reco energy binning - cfg_binning = cfg["analysis"]["ereco_binning"] - ereco = ( - np.logspace( - np.log10(cfg_binning["emin"]), - np.log10(cfg_binning["emax"]), - cfg_binning["nbin"] + 1, - ) - * u.TeV - ) - - # Handle theta square cut optimisation - # (compute 68 % containment radius PSF if necessary) - thsq_opt_type = cfg["analysis"]["thsq_opt"]["type"] - if thsq_opt_type == "fixed": - thsq_values = np.array([cfg["analysis"]["thsq_opt"]["value"]]) * u.deg - print(f"Using fixed theta cut: {thsq_values}") - elif thsq_opt_type == "opti": - thsq_values = np.arange(0.05, 0.40, 0.01) * u.deg - print(f"Optimising theta cut for: {thsq_values}") - elif thsq_opt_type == "r68": - print("Using R68% theta cut") - print("Computing...") - cfg_binning = cfg["analysis"]["ereco_binning"] - ereco = ( - np.logspace( - np.log10(cfg_binning["emin"]), - np.log10(cfg_binning["emax"]), - cfg_binning["nbin"] + 1, - ) - * u.TeV - ) - radius = 68 - - thsq_values = list() - - # There seems to be a problem in using pandas from FITS data - # ValueError: Big-endian buffer not supported on little-endian compiler - - # I convert to astropy table.... - # should we use only those? - - evt_dict["gamma"] = Table.from_pandas(evt_dict["gamma"]) - if args.debug: - print("GAMMAS") - # print(evt_dict["gamma"].head(n=5)) - print(evt_dict["gamma"]) - - for ibin in range(len(ereco) - 1): - emin = ereco[ibin] - emax = ereco[ibin + 1] - - # PANDAS EQUIVALENT - # energy_query = "reco_energy > {} and reco_energy <= {}".format( - # emin.value, emax.value - # ) - # data = evt_dict["gamma"].query(energy_query).copy() - - mask_energy = (evt_dict["gamma"]["ENERGY"] > emin.value) & ( - evt_dict["gamma"]["ENERGY"] < emax.value - ) - data = evt_dict["gamma"][mask_energy] - - min_stat = 0 - if len(data) <= min_stat: - print(" ==> Not enough statistics:") - print("To be handled...") - thsq_values.append(0.3) - continue - # import sys - # sys.exit() - - psf = np.percentile(data["THETA"], radius) - # psf_err = psf / np.sqrt(len(data)) # not used after? - - thsq_values.append(psf) - thsq_values = np.array(thsq_values) * u.deg - # Set 0.05 as a lower value - idx = np.where(thsq_values.value < 0.05) - thsq_values[idx] = 0.05 * u.deg - print(f"Using theta cut: {thsq_values}") - - # Cuts optimisation - print("### Finding best cuts...") - cut_optimiser = CutsOptimisation(config=cfg, evt_dict=evt_dict, verbose_level=0) - - # Weight events - print("- Weighting events...") - cut_optimiser.weight_events( - model_dict=model_dict, - # colname_mc_energy=cfg["column_definition"]["TRUE_ENERGY"], - colname_mc_energy="TRUE_ENERGY", - ) - - # Find best cutoff to reach best sensitivity - print("- Estimating cutoffs...") - cut_optimiser.find_best_cutoff(energy_values=ereco, angular_values=thsq_values) - - # Save results and auxiliary data for diagnostic - print("- Saving results to disk...") - cut_optimiser.write_results( - outdir, "{}.fits".format(cfg["general"]["output_table_name"]), format="fits" - ) - - # Cuts diagnostic - print("### Building cut diagnostics...") - cut_diagnostic = CutsDiagnostic(config=cfg, indir=outdir) - cut_diagnostic.plot_optimisation_summary() - cut_diagnostic.plot_diagnostics() - - # Apply cuts and save data - print("### Applying cuts to data...") - cut_applicator = CutsApplicator(config=cfg, evt_dict=evt_dict, outdir=outdir) - cut_applicator.apply_cuts(args.debug) - - # Irf Maker - print("### Building IRF...") - irf_maker = IrfMaker(config=cfg, evt_dict=evt_dict, outdir=outdir) - irf_maker.build_irf(thsq_values) - - # Sensitivity maker - print("### Estimating sensitivity...") - sensitivity_maker = SensitivityMaker(config=cfg, outdir=outdir) - sensitivity_maker.load_irf() - sensitivity_maker.estimate_sensitivity() - - -if __name__ == "__main__": - main() diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py new file mode 100644 index 000000000..cfa975a16 --- /dev/null +++ b/pyirf/sensitivity.py @@ -0,0 +1,180 @@ +""" +Functions to calculate sensitivity +""" +import astropy.units as u +import numpy as np +from scipy.optimize import brentq +from astropy.table import QTable +import logging + +from .statistics import li_ma_significance +from .utils import check_histograms + + +__all__ = ["relative_sensitivity", "calculate_sensitivity"] + + +log = logging.getLogger(__name__) + + +def relative_sensitivity( + n_on, + n_off, + alpha, + target_significance=5, + significance_function=li_ma_significance, + initial_guess=0.01, +): + """ + Calculate the relative sensitivity defined as the flux + relative to the reference source that is detectable with + significance ``target_significance``. + + Given measured ``n_on`` and ``n_off``, + we estimate the number of gamma events ``n_signal`` as ``n_on - alpha * n_off``. + + The number of background events ``n_background` is estimated as ``n_off * alpha``. + + In the end, we find the relative sensitivity as the scaling factor for ``n_signal`` + that yields a significance of ``target_significance``. + + The reference time should be incorporated by appropriately weighting the events + before calculating ``n_on`` and ``n_off`` + + Parameters + ---------- + n_on: int or array-like + Number of signal-like events for the on observations + n_off: int or array-like + Number of signal-like events for the off observations + alpha: float + Scaling factor between on and off observations. + 1 / number of off regions for wobble observations. + significance: float + Significance necessary for a detection + significance_function: function + A function f(n_on, n_off, alpha) -> significance in sigma + Used to calculate the significance, default is the Li&Ma + likelihood ratio test formula. + Li, T-P., and Y-Q. Ma. + "Analysis methods for results in gamma-ray astronomy." + The Astrophysical Journal 272 (1983): 317-324. + Formula (17) + initial_guess: float + Initial guess for the root finder + """ + n_background = n_off * alpha + n_signal = n_on - n_background + + if np.isnan(n_on) or np.isnan(n_off): + return np.nan + + if n_on < 1 or n_off < 1: + return np.nan + + if n_signal <= 0: + return np.nan + + def equation(relative_flux): + n_on = n_signal * relative_flux + n_background + s = significance_function(n_on, n_off, alpha) + return s - target_significance + + try: + # brentq needs a lower and an upper bound + # lower can be trivially set to zero, but the upper bound is more tricky + # we will use the simple, analytically solvable significance formula and scale it + # with 10 to be sure it's above the Li and Ma solution + # so rel * n_signal / sqrt(n_background) = target_significance + upper_bound = 10 * target_significance * np.sqrt(n_background) / n_signal + result = brentq(equation, 0, upper_bound,) + except (RuntimeError, ValueError): + log.warn( + "Could not calculate relative significance for" + f" n_signal={n_signal:.1f}, n_off={n_off:.1f}, returning nan" + ) + return np.nan + + return result + + +def calculate_sensitivity( + signal_hist, + background_hist, + alpha, + target_significance=5, + significance_function=li_ma_significance, +): + """ + Calculate sensitivity for DL2 event lists in bins of reconstructed energy. + + Sensitivity is defined as the minimum flux detectable with ``target_significance`` + sigma significance in a certain time. + + This time must be incorporated into the event weights. + + Parameters + ---------- + signal_hist: astropy.table.QTable + Histogram of detected signal events as a table. + Required columns: n and n_weighted. + See ``pyirf.binning.create_histogram_table`` + background_hist: astropy.table.QTable + Histogram of detected events as a table. + Required columns: n and n_weighted. + See ``pyirf.binning.create_histogram_table`` + alpha: float + Size ratio of signal region to background region + target_significance: float + Required significance + significance_function: callable + A function with signature (n_on, n_off, alpha) -> significance. + Default is the Li & Ma likelihood ratio test. + + Returns + ------- + sensitivity_table: astropy.table.QTable + Table with sensitivity information. + Contains weighted and unweighted number of signal and background events + and the ``relative_sensitivity``, the scaling applied to the signal events + that yields ``target_significance`` sigma of significance according to + the ``significance_function`` + """ + assert len(signal_hist) == len(background_hist) + + check_histograms(signal_hist, background_hist) + sensitivity = QTable() + for key in ("low", "high", "center"): + k = "reco_energy_" + key + sensitivity[k] = signal_hist[k] + + # add event number information + sensitivity["n_signal"] = signal_hist["n"] + sensitivity["n_signal_weighted"] = signal_hist["n_weighted"] + sensitivity["n_background"] = background_hist["n"] + sensitivity["n_background_weighted"] = background_hist["n_weighted"] + + sensitivity["relative_sensitivity"] = [ + relative_sensitivity( + n_on=n_signal_hist + alpha * n_background_hist, + n_off=n_background_hist, + alpha=alpha, + ) + for n_signal_hist, n_background_hist in zip( + signal_hist["n_weighted"], background_hist["n_weighted"] + ) + ] + + # safety checks + invalid = ( + (sensitivity["n_signal_weighted"] < 10) + | ( + sensitivity["n_signal_weighted"] + < (0.05 * alpha * sensitivity["n_background_weighted"]) + ) + | (sensitivity["n_background"] < 5) + | (sensitivity["n_background_weighted"] < 10) + ) + sensitivity["relative_sensitivity"][invalid] = np.nan + + return sensitivity diff --git a/pyirf/simulations.py b/pyirf/simulations.py new file mode 100644 index 000000000..3850e5b92 --- /dev/null +++ b/pyirf/simulations.py @@ -0,0 +1,91 @@ +import astropy.units as u + + +class SimulatedEventsInfo: + """ + Information about all simulated events, + needed for calculating event weights. + + Attributes + ---------- + + n_showers: int + Total number of simulated showers. If reuse was used, this + should already include the reuse. + energy_min: u.Quantity[energy] + Lower limit of the simulated energy range + energy_max: u.Quantity[energy] + Upper limit of the simulated energy range + max_impact: u.Quantity[length] + Maximum simulated impact parameter + spectral_index: float + Spectral Index of the simulated power law with sign included. + """ + + __slots__ = ( + "n_showers", + "energy_min", + "energy_max", + "max_impact", + "spectral_index", + "viewcone", + ) + + @u.quantity_input( + energy_min=u.TeV, energy_max=u.TeV, max_impact=u.m, viewcone=u.deg + ) + def __init__( + self, n_showers, energy_min, energy_max, max_impact, spectral_index, viewcone + ): + self.n_showers = n_showers + self.energy_min = energy_min + self.energy_max = energy_max + self.max_impact = max_impact + self.spectral_index = spectral_index + self.viewcone = viewcone + + if spectral_index > -1: + raise ValueError("spectral index must be <= -1") + + @u.quantity_input(energy_bins=u.TeV) + def calculate_n_showers(self, energy_bins): + """ + Calculate number of showers that were simulated in the given interval + + Parameters + ---------- + energy_bins: ``~astropy.units.Quantity``[energy] + The interval edges for which to calculate the number of simulated showers + + Returns + ------- + n_showers: ``~numpy.ndarray`` + The expected number of events inside each of the ``energy_bins``. + This is a floating point number. + The actual numbers will follow a poissionian distribution around this + expected value. + """ + bins = energy_bins.to_value(u.TeV) + e_low = bins[:-1] + e_high = bins[1:] + + int_index = self.spectral_index + 1 + e_min = self.energy_min.to_value(u.TeV) + e_max = self.energy_max.to_value(u.TeV) + + e_term = e_low ** int_index - e_high ** int_index + normalization = int_index / (e_max ** int_index - e_min ** int_index) + + return self.n_showers * normalization * e_term + + def __repr__(self): + return ( + f"{self.__class__.__name__}(" + f"n_showers={self.n_showers}, " + f"energy_min={self.energy_min:.3f}, " + f"energy_max={self.energy_max:.2f}, " + f"spectral_index={self.spectral_index:.1f}, " + f"max_impact={self.max_impact:.2f}, " + f"viewcone={self.viewcone}" + ")" + ) diff --git a/pyirf/spectral.py b/pyirf/spectral.py new file mode 100644 index 000000000..dbaa0024b --- /dev/null +++ b/pyirf/spectral.py @@ -0,0 +1,274 @@ +""" +Functions and classes for calculating spectral weights +""" +import astropy.units as u +import numpy as np + + +#: Unit of a point source flux +#: +#: Number of particles per Energy, time and area +POINT_SOURCE_FLUX_UNIT = (1 / u.TeV / u.s / u.m ** 2).unit + +#: Unit of a diffuse flux +#: +#: Number of particles per Energy, time, area and solid_angle +DIFFUSE_FLUX_UNIT = POINT_SOURCE_FLUX_UNIT / u.sr + + +__all__ = [ + "POINT_SOURCE_FLUX_UNIT", + "DIFFUSE_FLUX_UNIT", + "calculate_event_weights", + "PowerLaw", + "LogParabola", + "PowerLawWithExponentialGaussian", + "CRAB_HEGRA", + "CRAB_MAGIC_JHEAP2015", + "PDG_ALL_PARTICLE", + "IRFDOC_PROTON_SPECTRUM", + "IRFDOC_ELECTRON_SPECTRUM", +] + + +@u.quantity_input(true_energy=u.TeV) +def calculate_event_weights(true_energy, target_spectrum, simulated_spectrum): + r""" + Calculate event weights + + Events with a certain ``simulated_spectrum`` are reweighted to ``target_spectrum``. + + .. math:: + w_i = \frac{\Phi_\text{Target}(E_i)}{\Phi_\text{Simulation}(E_i)} + + Parameters + ---------- + true_energy: astropy.units.Quantity[energy] + True energy of the event + target_spectrum: callable + The target spectrum. Must be a allable with signature (energy) -> flux + simulated_spectrum: callable + The simulated spectrum. Must be a callable with signature (energy) -> flux + + Returns + ------- + weights: numpy.ndarray + Weights for each event + """ + return (target_spectrum(true_energy) / simulated_spectrum(true_energy)).to_value( + u.one + ) + + +class PowerLaw: + r""" + A power law with normalization, reference energy and index. + Index includes the sign: + + .. math:: + + \Phi(E, \Phi_0, \gamma, E_\text{ref}) = + \Phi_0 \left(\frac{E}{E_\text{ref}}\right)^{\gamma} + + Attributes + ---------- + normalization: astropy.units.Quantity[flux] + :math:`\Phi_0`, + index: float + :math:`\gamma` + e_ref: astropy.units.Quantity[energy] + :math:`E_\text{ref}` + """ + + @u.quantity_input( + normalization=[DIFFUSE_FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], e_ref=u.TeV + ) + def __init__(self, normalization, index, e_ref=1 * u.TeV): + self.normalization = normalization + self.index = index + self.e_ref = e_ref + + @u.quantity_input(energy=u.TeV) + def __call__(self, energy): + return self.normalization * (energy / self.e_ref) ** self.index + + @classmethod + @u.quantity_input(obstime=u.hour, e_ref=u.TeV) + def from_simulation(cls, simulated_event_info, obstime, e_ref=1 * u.TeV): + """ + Calculate the flux normalization for simulated events drawn + from a power law for a certain observation time. + """ + e_min = simulated_event_info.energy_min + e_max = simulated_event_info.energy_max + index = simulated_event_info.spectral_index + n_showers = simulated_event_info.n_showers + viewcone = simulated_event_info.viewcone + + if viewcone.value > 0: + solid_angle = 2 * np.pi * (1 - np.cos(viewcone)) * u.sr + else: + solid_angle = 1 + + A = np.pi * simulated_event_info.max_impact ** 2 + + delta = e_max ** (index + 1) - e_min ** (index + 1) + nom = (index + 1) * e_ref ** index * n_showers + denom = (A * obstime * solid_angle) * delta + + return cls(normalization=nom / denom, index=index, e_ref=e_ref,) + + def __repr__(self): + return f"{self.__class__.__name__}({self.normalization} * (E / {self.e_ref})**{self.index}" + + +class LogParabola: + r""" + A log parabola flux parameterization. + + .. math:: + + \Phi(E, \Phi_0, \alpha, \beta, E_\text{ref}) = + \Phi_0 \left( + \frac{E}{E_\text{ref}} + \right)^{\alpha + \beta \cdot \log_{10}(E / E_\text{ref})} + + Attributes + ---------- + normalization: astropy.units.Quantity[flux] + :math:`\Phi_0`, + a: float + :math:`\alpha` + b: float + :math:`\beta` + e_ref: astropy.units.Quantity[energy] + :math:`E_\text{ref}` + """ + + @u.quantity_input( + normalization=[DIFFUSE_FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], e_ref=u.TeV + ) + def __init__(self, normalization, a, b, e_ref=1 * u.TeV): + self.normalization = normalization + self.a = a + self.b = b + self.e_ref = e_ref + + @u.quantity_input(energy=u.TeV) + def __call__(self, energy): + e = (energy / self.e_ref).to_value(u.one) + return self.normalization * e ** (self.a + self.b * np.log10(e)) + + def __repr__(self): + return f"{self.__class__.__name__}({self.normalization} * (E / {self.e_ref})**({self.a} + {self.b} * log10(E / {self.e_ref}))" + + +class PowerLawWithExponentialGaussian(PowerLaw): + r""" + A power law with an additional Gaussian bump. + Beware that the Gaussian is not normalized! + + .. math:: + + \Phi(E, \Phi_0, \gamma, f, \mu, \sigma, E_\text{ref}) = + \Phi_0 \left( + \frac{E}{E_\text{ref}} + \right)^{\gamma} + \cdot \left( + 1 + f \cdot + \left( + \exp\left( + \operatorname{Gauss}(\log_{10}(E / E_\text{ref}), \mu, \sigma) + \right) - 1 + \right) + \right) + + Where :math:`\operatorname{Gauss}` is the unnormalized Gaussian distribution: + + .. math:: + \operatorname{Gauss}(x, \mu, \sigma) = \exp\left( + -\frac{1}{2} \left(\frac{x - \mu}{\sigma}\right)^2 + \right) + + Attributes + ---------- + normalization: astropy.units.Quantity[flux] + :math:`\Phi_0`, + a: float + :math:`\alpha` + b: float + :math:`\beta` + e_ref: astropy.units.Quantity[energy] + :math:`E_\text{ref}` + """ + + @u.quantity_input( + normalization=[DIFFUSE_FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], e_ref=u.TeV + ) + def __init__(self, normalization, index, e_ref, f, mu, sigma): + super().__init__(normalization=normalization, index=index, e_ref=e_ref) + self.f = f + self.mu = mu + self.sigma = sigma + + @u.quantity_input(energy=u.TeV) + def __call__(self, energy): + power = super().__call__(energy) + log10_e = np.log10(energy / self.e_ref) + # ROOT's TMath::Gauss does not add the normalization + # this is missing from the IRFDocs + # the code used for the plot can be found here: + # https://gitlab.cta-observatory.org/cta-consortium/aswg/irfs-macros/cosmic-rays-spectra/-/blob/master/electron_spectrum.C#L508 + gauss = np.exp(-0.5 * ((log10_e - self.mu) / self.sigma) ** 2) + return power * (1 + self.f * (np.exp(gauss) - 1)) + + +#: Power Law parametrization of the Crab Nebula spectrum as published by HEGRA +#: +#: From "The Crab Nebula and Pulsar between 500 GeV and 80 TeV: Observations with the HEGRA stereoscopic air Cherenkov telescopes", +#: Aharonian et al, 2004, ApJ 614.2 +#: doi.org/10.1086/423931 +CRAB_HEGRA = PowerLaw( + normalization=2.83e-11 / (u.TeV * u.cm ** 2 * u.s), index=-2.62, e_ref=1 * u.TeV, +) + +#: Log-Parabola parametrization of the Crab Nebula spectrum as published by MAGIC +#: +#: From "Measurement of the Crab Nebula spectrum over three decades in energy with the MAGIC telescopes", +#: Aleksìc et al., 2015, JHEAP +#: https://doi.org/10.1016/j.jheap.2015.01.002 +CRAB_MAGIC_JHEAP2015 = LogParabola( + normalization=3.23e-11 / (u.TeV * u.cm ** 2 * u.s), a=-2.47, b=-0.24, +) + + +#: All particle spectrum +#: +#: (30.2) from "The Review of Particle Physics (2020)" +#: https://pdg.lbl.gov/2020/reviews/rpp2020-rev-cosmic-rays.pdf +PDG_ALL_PARTICLE = PowerLaw( + normalization=1.8e4 / (u.GeV * u.m ** 2 * u.s * u.sr), index=-2.7, e_ref=1 * u.GeV, +) + +#: Proton spectrum definition defined in the CTA Prod3b IRF Document +#: +#: From "Description of CTA Instrument Response Functions +#: (Production 3b Simulation), section 4.3.1 +IRFDOC_PROTON_SPECTRUM = PowerLaw( + normalization=9.8e-6 / (u.cm ** 2 * u.s * u.TeV * u.sr), + index=-2.62, + e_ref=1 * u.TeV, +) + +#: Electron spectrum definition defined in the CTA Prod3b IRF Document +#: +#: From "Description of CTA Instrument Response Functions +#: (Production 3b Simulation), section 4.3.1 +IRFDOC_ELECTRON_SPECTRUM = PowerLawWithExponentialGaussian( + normalization=2.385e-9 / (u.TeV * u.cm ** 2 * u.s * u.sr), + index=-3.43, + e_ref=1 * u.TeV, + mu=-0.101, + sigma=0.741, + f=1.950, +) diff --git a/pyirf/statistics.py b/pyirf/statistics.py new file mode 100644 index 000000000..f7808b3ce --- /dev/null +++ b/pyirf/statistics.py @@ -0,0 +1,52 @@ +import numpy as np + +from .utils import is_scalar + + +def li_ma_significance(n_on, n_off, alpha=0.2): + """ + Calculate the Li & Ma significance. + + Formula (17) doi.org/10.1086/161295 + + This functions returns 0 significance when n_on < alpha * n_off + instead of the negative sensitivities that would result from naively + evaluating the formula. + + Parameters + ---------- + n_on: integer or array like + Number of events for the on observations + n_off: integer of array like + Number of events for the off observations + alpha: float + Ratio between the on region and the off region size or obstime. + + Returns + ------- + s_lima: float or array + The calculated significance + """ + + scalar = is_scalar(n_on) + + n_on = np.array(n_on, copy=False, ndmin=1) + n_off = np.array(n_off, copy=False, ndmin=1) + + with np.errstate(divide="ignore", invalid="ignore"): + p_on = n_on / (n_on + n_off) + p_off = n_off / (n_on + n_off) + + t1 = n_on * np.log(((1 + alpha) / alpha) * p_on) + t2 = n_off * np.log((1 + alpha) * p_off) + + ts = t1 + t2 + significance = np.sqrt(ts * 2) + + significance[np.isnan(significance)] = 0 + significance[n_on < alpha * n_off] = 0 + + if scalar: + return significance[0] + + return significance diff --git a/pyirf/tests/__init__.py b/pyirf/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyirf/tests/test_binning.py b/pyirf/tests/test_binning.py new file mode 100644 index 000000000..1f2ea21e9 --- /dev/null +++ b/pyirf/tests/test_binning.py @@ -0,0 +1,72 @@ +import astropy.units as u +import numpy as np + + +def test_add_overflow_bins(): + from pyirf.binning import add_overflow_bins + + bins = np.array([1, 2]) + bins_uo = add_overflow_bins(bins) + + assert len(bins_uo) == 4 + assert bins_uo[0] == 0 + assert np.isinf(bins_uo[-1]) + assert np.all(bins_uo[1:-1] == bins) + + bins = np.array([1, 2]) + bins_uo = add_overflow_bins(bins, positive=False) + + assert bins_uo[0] < 0 + assert np.isinf(bins_uo[0]) + + # make sure we don't any bins if over / under is already present + bins = np.array([0, 1, 2, np.inf]) + assert len(add_overflow_bins(bins)) == len(bins) + + +def test_add_overflow_bins_units(): + from pyirf.binning import add_overflow_bins + + bins = np.array([1, 2]) * u.TeV + bins_uo = add_overflow_bins(bins) + + assert bins_uo.unit == bins.unit + assert len(bins_uo) == 4 + assert bins_uo[0] == 0 * u.TeV + assert np.isinf(bins_uo[-1]) + assert np.all(bins_uo[1:-1] == bins) + + +def test_bins_per_decade(): + from pyirf.binning import create_bins_per_decade + + bins = create_bins_per_decade(100 * u.GeV, 100 * u.TeV) + + assert bins.unit == u.GeV + assert len(bins) == 15 # end non-inclusive + + assert bins[0] == 100 * u.GeV + assert np.allclose(np.diff(np.log10(bins.to_value(u.GeV))), 0.2) + + bins = create_bins_per_decade(100 * u.GeV, 100 * u.TeV, 10) + assert bins.unit == u.GeV + assert len(bins) == 30 # end non-inclusive + + assert bins[0] == 100 * u.GeV + assert np.allclose(np.diff(np.log10(bins.to_value(u.GeV))), 0.1) + + +def test_calculate_bin_indices(): + from pyirf.binning import calculate_bin_indices + + bins = np.array([0, 1, 2]) + values = [0.5, 0.5, 1, 1.1, 1.9, 2, -1, 2.5] + + true_idx = np.array([0, 0, 1, 1, 1, 2, -1, 2]) + + assert np.all(calculate_bin_indices(values, bins) == true_idx) + + # test with units + bins *= u.TeV + values *= 1000 * u.GeV + assert np.all(calculate_bin_indices(values, bins) == true_idx) diff --git a/pyirf/tests/test_cuts.py b/pyirf/tests/test_cuts.py new file mode 100644 index 000000000..36c615161 --- /dev/null +++ b/pyirf/tests/test_cuts.py @@ -0,0 +1,75 @@ +import operator +import numpy as np +from astropy.table import QTable, Table +import astropy.units as u +import pytest +from scipy.stats import norm + + +@pytest.fixture +def events(): + return QTable( + { + "bin_reco_energy": [0, 0, 1, 1, 2, 2], + "theta": [0.1, 0.02, 0.3, 0.15, 0.01, 0.1] * u.deg, + "gh_score": [1.0, -0.2, 0.5, 0.05, 1.0, 0.3], + } + ) + + +def test_calculate_percentile_cuts(): + from pyirf.cuts import calculate_percentile_cut + + np.random.seed(0) + + dist1 = norm(0, 1) + dist2 = norm(10, 1) + N = int(1e4) + + values = np.append(dist1.rvs(size=N), dist2.rvs(size=N)) * u.deg + bin_values = np.append(np.zeros(N), np.ones(N)) * u.m + bins = [-0.5, 0.5, 1.5] * u.m + + cuts = calculate_percentile_cut(values, bin_values, bins, fill_value=np.nan * u.deg) + assert np.all(cuts["low"] == bins[:-1]) + assert np.all(cuts["high"] == bins[1:]) + + assert np.allclose(cuts["cut"], [dist1.ppf(0.68), dist2.ppf(0.68)], rtol=0.1,) + + # test with min/max value + cuts = calculate_percentile_cut( + values, + bin_values, + bins, + fill_value=np.nan * u.deg, + min_value=1 * u.deg, + max_value=5 * u.deg, + ) + assert np.all(cuts["cut"].quantity == [1.0, 5.0] * u.deg) + + +def test_evaluate_binned_cut(): + from pyirf.cuts import evaluate_binned_cut + + cuts = Table({"low": [0, 1], "high": [1, 2], "cut": [100, 1000],}) + + survived = evaluate_binned_cut( + np.array([500, 1500, 50, 2000, 25, 800]), + np.array([0.5, 1.5, 0.5, 1.5, 0.5, 1.5]), + cut_table=cuts, + op=operator.ge, + ) + assert np.all(survived == [True, True, False, True, False, False]) + + # test with quantity + cuts = Table( + {"low": [0, 1] * u.TeV, "high": [1, 2] * u.TeV, "cut": [100, 1000] * u.m,} + ) + + survived = evaluate_binned_cut( + [500, 1500, 50, 2000, 25, 800] * u.m, + [0.5, 1.5, 0.5, 1.5, 0.5, 1.5] * u.TeV, + cut_table=cuts, + op=operator.ge, + ) + assert np.all(survived == [True, True, False, True, False, False]) diff --git a/pyirf/tests/test_utils.py b/pyirf/tests/test_utils.py new file mode 100644 index 000000000..6064673d9 --- /dev/null +++ b/pyirf/tests/test_utils.py @@ -0,0 +1,28 @@ +import numpy as np +import astropy.units as u + + +def test_is_scalar(): + from pyirf.utils import is_scalar + + assert is_scalar(1.0) + assert is_scalar(5 * u.m) + assert is_scalar(np.array(5)) + + assert not is_scalar([1, 2, 3]) + assert not is_scalar([1, 2, 3] * u.m) + assert not is_scalar(np.ones(5)) + assert not is_scalar(np.ones((3, 4))) + + +def test_cone_solid_angle(): + from pyirf.utils import cone_solid_angle + + # whole sphere + assert u.isclose(cone_solid_angle(np.pi * u.rad), 4 * np.pi * u.sr) + + # half the sphere + assert u.isclose(cone_solid_angle(90 * u.deg), 2 * np.pi * u.sr) + + # zero + assert u.isclose(cone_solid_angle(0 * u.deg), 0 * u.sr) diff --git a/pyirf/utils.py b/pyirf/utils.py new file mode 100644 index 000000000..677dfa6b3 --- /dev/null +++ b/pyirf/utils.py @@ -0,0 +1,118 @@ +import numpy as np +import astropy.units as u +from astropy.coordinates.angle_utilities import angular_separation + +__all__ = [ + "is_scalar", + "calculate_theta", + "calculate_source_fov_offset", + "check_histograms", + "cone_solid_angle", +] + + +def is_scalar(val): + """Workaround that also supports astropy quantities + + Parameters + ---------- + val : object + Any object (value, list, etc...) + + Returns + ------- + result: bool + True is if input object is a scalar, False otherwise. + """ + result = np.array(val, copy=False).shape == tuple() + return result + + +@u.quantity_input(assumed_source_az=u.deg, assumed_source_alt=u.deg) +def calculate_theta(events, assumed_source_az, assumed_source_alt): + """Calculate sky separation between assumed and reconstructed positions. + + Parameters + ---------- + events : astropy.QTable + Astropy Table object containing the reconstructed events information. + assumed_source_az: astropy.units.Quantity + Assumed Azimuth angle of the source. + assumed_source_alt: astropy.units.Quantity + Assumed Altitude angle of the source. + + Returns + ------- + theta: astropy.units.Quantity + Angular separation between the assumed and reconstructed positions + in the sky. + """ + theta = angular_separation( + assumed_source_az, assumed_source_alt, events["reco_az"], events["reco_alt"], + ) + + return theta.to(u.deg) + + +def calculate_source_fov_offset(events): + """Calculate angular separation between true and pointing positions. + + Parameters + ---------- + events : astropy.QTable + Astropy Table object containing the reconstructed events information. + + Returns + ------- + theta: astropy.units.Quantity + Angular separation between the true and pointing positions + in the sky. + """ + theta = angular_separation( + events["true_az"], + events["true_alt"], + events["pointing_az"], + events["pointing_alt"], + ) + + return theta.to(u.deg) + + +def check_histograms(hist1, hist2, key="reco_energy"): + """ + Check if two histogram tables have the same binning + + Parameters + ---------- + hist1: ``~astropy.table.Table`` + First histogram table, as created by + ``~pyirf.binning.create_histogram_table`` + hist2: ``~astropy.table.Table`` + Second histogram table + """ + + # check binning information and add to output + for k in ("low", "center", "high"): + k = key + "_" + k + if not np.all(hist1[k] == hist2[k]): + raise ValueError( + "Binning for signal_hist and background_hist must be equal" + ) + + +def cone_solid_angle(angle): + """Calculate the solid angle of a view cone. + + Parameters + ---------- + angle: astropy.units.Quantity or astropy.coordinates.Angle + Opening angle of the view cone. + + Returns + ------- + solid_angle: astropy.units.Quantity + Solid angle of a view cone with opening angle ``angle``. + + """ + solid_angle = 2 * np.pi * (1 - np.cos(angle)) * u.sr + return solid_angle diff --git a/pyirf/version.py b/pyirf/version.py index 3dc1f76bc..d3ec452c3 100644 --- a/pyirf/version.py +++ b/pyirf/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/setup.cfg b/setup.cfg index 6e054af30..0ab3c34d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = pyirf description = Python IACT IRF builder, url = https://github.com/cta-observatory/pyirf -author = Julien Lefaucheur, Michele Peresano, Thomas Vuillaume +author = CTA Consortium, Analysis and Simulation Working Group author_email = thomas.vuillaume@lapp.in2p3.fr license = MIT long_description = file: README.rst @@ -10,10 +10,3 @@ github_project = cta-observatory/pyirf [options] python_requires = >=3.6 -install_requires = - astropy - ctapipe - ctaplot - gammapy==0.8 - numpy - tables diff --git a/setup.py b/setup.py index 8050732de..ac2449043 100644 --- a/setup.py +++ b/setup.py @@ -4,20 +4,32 @@ with open("pyirf/version.py") as f: __version__ = re.search('^__version__ = "(.*)"$', f.read()).group(1) +extras_require = { + "docs": [ + "sphinx", + "sphinx_rtd_theme", + "sphinx_automodapi", + "numpydoc", + "nbsphinx", + "uproot~=3.0", + ], + "tests": ["pytest", "pytest-cov", "gammapy~=0.17"], +} + +extras_require["all"] = extras_require["tests"] + extras_require["docs"] + setup( version=__version__, packages=find_packages(), - package_data={"pyirf": ["resources/config.yml"]}, include_package_data=True, install_requires=[ - "astropy", - "ctaplot~=0.5.0", - "gammapy==0.8", + "astropy~=4.0", "matplotlib", - "numpy", + "numpy>=1.18", "pandas", "scipy", + "tqdm", "tables", - "ctapipe==0.7", ], + extras_require=extras_require, )