From 64abee58d66bb01609535568d4efa09cdfdd1336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 18 Sep 2020 17:20:19 +0200 Subject: [PATCH 001/105] Start rewriting pyIRF Implemented in this commit: * reading of EventDisplay DL2 fits files * Calculating event weights * Functions for binning events * Li&Ma Significance Definition of the internal data format, for now astropy.table.QTables with several columns expected for DL2 event lists. Co-authored-by: Michele Peresano Co-authored-by: Lukas Nickel --- .gitignore | 1 + .mailmap | 4 + environment.yml | 21 +- examples/calculate_eventdisplay_irfs.py | 40 ++++ pyirf/binning.py | 94 +++++++++ pyirf/io/__init__.py | 19 +- pyirf/io/eventdisplay.py | 51 +++++ pyirf/io/io.py | 257 ------------------------ pyirf/sensitiviy.py | 105 ++++++++++ pyirf/simulations.py | 56 ++++++ pyirf/spectral.py | 154 ++++++++++++++ pyirf/statistics.py | 46 +++++ pyirf/tests/test_binning.py | 72 +++++++ setup.cfg | 9 +- setup.py | 8 +- 15 files changed, 632 insertions(+), 305 deletions(-) create mode 100644 .mailmap create mode 100644 examples/calculate_eventdisplay_irfs.py create mode 100644 pyirf/binning.py create mode 100644 pyirf/io/eventdisplay.py delete mode 100644 pyirf/io/io.py create mode 100644 pyirf/sensitiviy.py create mode 100644 pyirf/simulations.py create mode 100644 pyirf/spectral.py create mode 100644 pyirf/statistics.py create mode 100644 pyirf/tests/test_binning.py diff --git a/.gitignore b/.gitignore index eeadad245..7899cde33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Byte-compiled / optimized / DLL files +data __pycache__/ *.py[cod] *$py.class 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/environment.yml b/environment.yml index 2066335e4..67301904d 100644 --- a/environment.yml +++ b/environment.yml @@ -1,32 +1,17 @@ # A conda environment with all useful package for ctapipe developers -name: pyirf +name: pyirf-dev + channels: - default - - cta-observatory dependencies: - - ctapipe==0.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 - sphinx - - sphinx-automodapi - sphinx_rtd_theme - pip - - pip: - - rinohtype - - ctaplot diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py new file mode 100644 index 000000000..2934db5f1 --- /dev/null +++ b/examples/calculate_eventdisplay_irfs.py @@ -0,0 +1,40 @@ +''' +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 astropy.units as u +from pyirf.io.eventdisplay import read_eventdisplay_fits + +from pyirf.spectral import PowerLaw, CRAB_HEGRA, IRFDOC_PROTON_SPECTRUM, calculate_event_weights + + +def main(): + # read gammas + gammas, gamma_info = read_eventdisplay_fits('data/gamma_onSource.S.3HB9-FD_ID0.eff-0.fits') + simulated_spectrum = PowerLaw.from_simulation(gamma_info, 50 * u.hour) + gammas['weight'] = calculate_event_weights( + true_energy=gammas['true_energy'], + target_spectrum=CRAB_HEGRA, + simulated_spectrum=simulated_spectrum, + ) + + # read protons + protons, proton_info = read_eventdisplay_fits('data/proton_onSource.S.3HB9-FD_ID0.eff-0.fits') + simulated_spectrum = PowerLaw.from_simulation(proton_info, 50 * u.hour) + protons['weight'] = calculate_event_weights( + true_energy=protons['true_energy'], + target_spectrum=IRFDOC_PROTON_SPECTRUM, + simulated_spectrum=simulated_spectrum, + ) + + # perform cut optimization + + # calculate IRFs for the best cuts + + # write OGADF output file + + +if __name__ == '__main__': + main() diff --git a/pyirf/binning.py b/pyirf/binning.py new file mode 100644 index 000000000..f60a2362f --- /dev/null +++ b/pyirf/binning.py @@ -0,0 +1,94 @@ +''' +Utility functions for binning +''' + +import numpy as np +import astropy.units as u + + +def add_overflow_bins(bins, positive=True): + ''' + Add under and overflow bins to a bin array. + + Arguments + --------- + bins: np.array or u.Quantity + Bin edges array + positive: bool + If True, the underflow array will start at 0, if not at ``-np.inf`` + ''' + lower = 0 if positive else -np.inf + upper = np.inf + + if hasattr(bins, 'unit'): + lower *= bins.unit + upper *= bins.unit + + if bins[0] > 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. + + Arguments + --------- + 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. + + + Arguments + --------- + 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') or hasattr(bins, 'unit'): + unit = data.unit + data = data.to_value(unit) + bins = bins.to_value(unit) + + return np.digitize(data, bins) - 1 diff --git a/pyirf/io/__init__.py b/pyirf/io/__init__.py index feb21ea05..0328e9c1b 100644 --- a/pyirf/io/__init__.py +++ b/pyirf/io/__init__.py @@ -1,19 +1,6 @@ -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 + __all__ = [ - "load_config", - "internal_dataformat_mapper", - "read_simu_info_hdf5", - "read_simu_info_merged_hdf5", - "get_simu_info", - "read_FITS", - "write", + 'read_eventdisplay_fits' ] diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py new file mode 100644 index 000000000..cebec403b --- /dev/null +++ b/pyirf/io/eventdisplay.py @@ -0,0 +1,51 @@ +from astropy.table import QTable +import astropy.units as u + +from ..simulations import SimulatedEventsInfo + + +COLUMN_MAP = { + 'obs_id': 'OBS_ID', + 'event_id': 'EVENT_ID', + 'true_energy': 'MC_ENERGY', + 'reco_energy': 'ENERGY', + 'true_alt': 'MC_ALT', + 'true_az': 'AZ', + 'reco_alt': 'ALT', + 'reco_az': 'AZ', + 'gh_score': 'GH_MVA', + 'multiplicity': 'MULTIP', +} + + +def read_eventdisplay_fits(infile): + """ + Read an DL2 Fits file as produced by the DL2 converter from root + here: https://github.com/Eventdisplay/Converters/blob/master/DL2/generate_DL2_file.py + + Returns + ------- + events: astropy.QTable + Astropy Table object containing the reconstructed events information. + simulated_events: ``~pyirf.simulations.SimulatedEventsInfo`` + + """ + events_table = QTable.read(infile, format='fits', hdu='EVENTS') + sim_events = QTable.read(infile, format='fits', hdu='SIMULATED EVENTS') + run_header = QTable.read(infile, format='fits', hdu='RUNHEADER')[0] + + events = QTable({ + new: events_table[old] + for new, old in COLUMN_MAP.items() + }) + + sim_info = SimulatedEventsInfo( + n_showers=sim_events['EVENTS'].sum(), + 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/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/sensitiviy.py b/pyirf/sensitiviy.py new file mode 100644 index 000000000..02aa61cc0 --- /dev/null +++ b/pyirf/sensitiviy.py @@ -0,0 +1,105 @@ +import astropy.units as u +import numpy as np +from scipy.optimize import newton +import warnings + + +from .statistics import li_ma_significance + + +@u.quantity_input(t_obs=u.hour, t_ref=u.hour) +def relative_sensitivity( + n_on, + n_off, + alpha, + t_obs, + t_ref=u.Quantity(50, u.hour), + target_significance=5, + significance_function=li_ma_significance, + initial_guess=0.5, +): + ''' + Calculate the relative sensitivity defined as the flux + relative to the reference source that is detectable with + significance ``target_significance`` in time ``t_ref``. + + Given measured ``n_on`` and ``n_off`` during a time period ``t_obs``, + 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``. + + + 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. + t_obs: astropy.units.Quantity of type time + Total observation time + t_ref: astropy.units.Quantity of type time + Reference time for the detection + 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 + ''' + + ratio = (t_ref / t_obs).si + n_on = n_on * ratio + n_off = n_off * ratio + + 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 == 0 or n_off == 0: + return np.nan + + if n_signal <= 0: + return np.nan + + def equation(relative_flux): + n_on = n_signal * relative_flux + n_background + return significance_function(n_on, n_off, alpha) - target_significance + + try: + result = newton( + equation, + x0=initial_guess, + ) + except RuntimeError: + warnings.warn('Could not calculate relative significance, returning nan') + return np.nan + + return result + + +# make the function accept numpy arrays for n_on, n_off, +# so we can provide all energy bins +relative_sensitivity = np.vectorize( + relative_sensitivity, + excluded=[ + 't_obs', + 't_ref', + 'alpha', + 'target_significance', + 'significance_function', + ] +) diff --git a/pyirf/simulations.py b/pyirf/simulations.py new file mode 100644 index 000000000..31f2ede01 --- /dev/null +++ b/pyirf/simulations.py @@ -0,0 +1,56 @@ +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') + + 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..1a87858cb --- /dev/null +++ b/pyirf/spectral.py @@ -0,0 +1,154 @@ +''' +Functions and classes for calculating spectral weights +''' +import astropy.units as u +import numpy as np +from scipy.stats import norm + + +POINT_SOURCE_FLUX_UNIT = (1 / u.TeV / u.s / u.m**2).unit +FLUX_UNIT = POINT_SOURCE_FLUX_UNIT / u.sr + + +@u.quantity_input(true_energy=u.TeV) +def calculate_event_weights(true_energy, target_spectrum, simulated_spectrum): + return ( + target_spectrum(true_energy) / simulated_spectrum(true_energy) + ).to_value(u.one) + + +class PowerLaw: + @u.quantity_input( + flux_normalization=[FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], + e_ref=u.TeV + ) + def __init__(self, flux_normalization, spectral_index, e_ref=1 * u.TeV): + self.flux_normalization = flux_normalization + self.spectral_index = spectral_index + self.e_ref = e_ref + + @u.quantity_input(energy=u.TeV) + def __call__(self, energy): + return ( + self.flux_normalization + * (energy / self.e_ref) ** self.spectral_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 + spectral_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**(spectral_index + 1) - e_min**(spectral_index + 1) + nom = (spectral_index + 1) * e_ref**spectral_index * n_showers + denom = (A * obstime * solid_angle) * delta + + return cls( + flux_normalization=nom / denom, + spectral_index=spectral_index, + e_ref=e_ref, + ) + + def __repr__(self): + return f'{self.__class__.__name__}({self.flux_normalization} * (E / {self.e_ref})**{self.spectral_index}' + + +class LogParabola: + @u.quantity_input( + flux_normalization=[FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], + e_ref=u.TeV + ) + def __init__(self, flux_normalization, a, b, e_ref=1 * u.TeV): + self.flux_normalization = flux_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.flux_normalization * e**(self.a + self.b * np.log10(e)) + + +class PowerLawWithExponentialGaussian(PowerLaw): + + @u.quantity_input( + flux_normalization=[FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], + e_ref=u.TeV + ) + def __init__(self, flux_normalization, spectral_index, e_ref, f, mu, sigma): + super().__init__( + flux_normalization=flux_normalization, + spectral_index=spectral_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()(energy) + log10_e = np.log10(energy / self.e_ref) + gauss = norm.pdf(log10_e, self.mu, self.sigma) + return power * (1 + self.f * gauss) + + +# 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( + flux_normalization=2.83e-11 / (u.TeV * u.cm**2 * u.s), + spectral_index=-2.62, + e_ref=1 * u.TeV, +) + +# 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( + flux_normalization=3.23e-11 / (u.TeV * u.cm**2 * u.s), + a=-2.47, + b=-0.24, +) + +PDG_ALL_PARTICLE = PowerLaw( + flux_normalization=1.8e4 * FLUX_UNIT, + spectral_index=-2.7, + e_ref=1 * u.GeV, +) + +# From "Description of CTA Instrument Response Functions (Production 3b Simulation)" +# section 4.3.1 +IRFDOC_PROTON_SPECTRUM = PowerLaw( + flux_normalization=9.8e-6 / (u.cm**2 * u.s * u.TeV * u.sr), + spectral_index=-2.62, + e_ref=1 * u.TeV, +) + +# section 4.3.2 +IRFDOC_ELECTRON_SPECTRUM = PowerLawWithExponentialGaussian( + flux_normalization=2.385e-9 / (u.TeV * u.cm**2 * u.s * u.sr), + spectral_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..c573c632a --- /dev/null +++ b/pyirf/statistics.py @@ -0,0 +1,46 @@ +import numpy as np + + +def li_ma_significance(n_on, n_off, alpha=0.2): + ''' + Calculate the Li & Ma significance for given + observations data. + + 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. + ''' + + scalar = np.isscalar(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/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/setup.cfg b/setup.cfg index 6e054af30..408ff7e9d 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 = Julien Lefaucheur, Michele Peresano, Thomas Vuillaume, Maximilian Nöthe 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..5c228d4b8 100644 --- a/setup.py +++ b/setup.py @@ -7,17 +7,13 @@ 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", "tables", - "ctapipe==0.7", ], ) From 194f768caab61ef2dd6ad13645de5cd3c3de02aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 18 Sep 2020 18:43:44 +0200 Subject: [PATCH 002/105] Fix docs and include new pages in it Co-authored-by: Michele Peresano Co-authored-by: Lukas Nickel --- .travis.yml | 41 +++++++++++++++++++++-------------------- docs/binning.rst | 11 +++++++++++ docs/conf.py | 9 +++------ docs/index.rst | 10 ++++++++++ docs/requirements.txt | 16 ---------------- docs/spectral.rst | 11 +++++++++++ environment.yml | 8 ++++++++ pyirf/spectral.py | 4 ++-- setup.py | 11 +++++++++++ 9 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 docs/binning.rst delete mode 100644 docs/requirements.txt create mode 100644 docs/spectral.rst diff --git a/.travis.yml b/.travis.yml index e3a951283..ff5d2a703 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,29 @@ 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 + - 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 + - pip install travis-sphinx codecov pytest-cov + - pip install . + - python --version script: - - pytest --cov=pyirf + - pytest --cov=pyirf after_script: - - if [[ "$CONDA" == "true" ]];then - conda deactivate - fi + - if [[ "$CONDA" == "true" ]];then + conda deactivate + fi after_success: - - codecov + - codecov 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/conf.py b/docs/conf.py index f7dbbe2aa..b17916220 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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,8 +54,6 @@ "sphinx.ext.napoleon", "sphinx_automodapi.automodapi", "sphinx_automodapi.smart_resolver", - "nbsphinx", - "IPython.sphinxext.ipython_console_highlighting", ] # nbsphinx @@ -79,7 +76,7 @@ # 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", diff --git a/docs/index.rst b/docs/index.rst index fe708db40..aa98d5576 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -52,14 +52,24 @@ which this documentation is linked. :caption: Structure :maxdepth: 1 + spectral + binning io/index resources/index perf/index scripts/index + +Reference/API +------------- + +.. automodapi:: pyirf + :no-inheritance-diagram: + Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` + 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/spectral.rst b/docs/spectral.rst new file mode 100644 index 000000000..6073309eb --- /dev/null +++ b/docs/spectral.rst @@ -0,0 +1,11 @@ +.. _spectral: + +Event Weighting and Spectrum Definitions +======================================== + + +Reference/API +------------- + +.. automodapi:: pyirf.spectral + :no-inheritance-diagram: diff --git a/environment.yml b/environment.yml index 67301904d..001306e88 100644 --- a/environment.yml +++ b/environment.yml @@ -4,6 +4,7 @@ name: pyirf-dev channels: - default dependencies: + - python=3.7 - astropy - numpy - ipython @@ -12,6 +13,13 @@ dependencies: - pytest - scipy - setuptools + # docs + - numpydoc - sphinx - sphinx_rtd_theme - pip + - pip: + - rinohtype + - nbsphinx + - gammapy~=0.8.0 + - sphinx_automodapi diff --git a/pyirf/spectral.py b/pyirf/spectral.py index 1a87858cb..746b2d9bf 100644 --- a/pyirf/spectral.py +++ b/pyirf/spectral.py @@ -135,8 +135,8 @@ def __call__(self, energy): e_ref=1 * u.GeV, ) -# From "Description of CTA Instrument Response Functions (Production 3b Simulation)" -# section 4.3.1 +#: 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( flux_normalization=9.8e-6 / (u.cm**2 * u.s * u.TeV * u.sr), spectral_index=-2.62, diff --git a/setup.py b/setup.py index 5c228d4b8..e1c6c812e 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,16 @@ "pandas", "scipy", "tables", + "gammapy~=0.8.0", ], + extras_require={ + 'docs': [ + 'rinohtype', + 'sphinx', + 'sphinx_rtd_theme', + 'sphinx_automodapi', + 'numpydoc', + 'nbsphinx' + ] + } ) From b5e1630549e9e1a1452960a9e44a416da3e99a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 18 Sep 2020 18:48:32 +0200 Subject: [PATCH 003/105] Remove test for old io --- pyirf/io/tests/test_io.py | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 pyirf/io/tests/test_io.py 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 From af007e1164ad35e32ac1dd0784c2f9c16f13bbfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 18 Sep 2020 19:10:35 +0200 Subject: [PATCH 004/105] Fix unit of PDG spectrum, fix electron class, add example plot --- examples/plot_spectra.py | 63 ++++++++++++++++++++++++++++++++++++++++ pyirf/spectral.py | 9 ++++-- 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 examples/plot_spectra.py diff --git a/examples/plot_spectra.py b/examples/plot_spectra.py new file mode 100644 index 000000000..18a0634c5 --- /dev/null +++ b/examples/plot_spectra.py @@ -0,0 +1,63 @@ +import matplotlib.pyplot as plt +import numpy as np +import astropy.units as u + +from pyirf.spectral import ( + IRFDOC_ELECTRON_SPECTRUM, + IRFDOC_PROTON_SPECTRUM, + PDG_ALL_PARTICLE, + CRAB_HEGRA, + CRAB_MAGIC_JHEAP2015, + POINT_SOURCE_FLUX_UNIT, + FLUX_UNIT, +) + + +cr_spectra = { + 'PDG All Particle Spectrum': PDG_ALL_PARTICLE, + 'ATIC Proton Fit (from IRF Document)': IRFDOC_PROTON_SPECTRUM, + 'Electron Spectrum (from IRFall Document)': IRFDOC_ELECTRON_SPECTRUM, +} + + +if __name__ == '__main__': + + energy = np.geomspace(0.01, 100, 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(f'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(): + + plt.plot( + energy.to_value(u.TeV), + spectrum(energy).to_value(FLUX_UNIT), + label=label, + ) + + plt.legend() + plt.xscale('log') + plt.yscale('log') + plt.xlabel(f'E / TeV') + plt.ylabel(f'Flux / ({FLUX_UNIT.to_string("latex")})') + + plt.show() diff --git a/pyirf/spectral.py b/pyirf/spectral.py index 746b2d9bf..9692dd5f3 100644 --- a/pyirf/spectral.py +++ b/pyirf/spectral.py @@ -105,7 +105,7 @@ def __init__(self, flux_normalization, spectral_index, e_ref, f, mu, sigma): @u.quantity_input(energy=u.TeV) def __call__(self, energy): - power = super()(energy) + power = super().__call__(energy) log10_e = np.log10(energy / self.e_ref) gauss = norm.pdf(log10_e, self.mu, self.sigma) return power * (1 + self.f * gauss) @@ -121,7 +121,7 @@ def __call__(self, energy): ) # From "Measurement of the Crab Nebula spectrum over three decades in energy with the MAGIC telescopes", -#Aleksìc et al., 2015, JHEAP +# Aleksìc et al., 2015, JHEAP # https://doi.org/10.1016/j.jheap.2015.01.002 CRAB_MAGIC_JHEAP2015 = LogParabola( flux_normalization=3.23e-11 / (u.TeV * u.cm**2 * u.s), @@ -129,8 +129,11 @@ def __call__(self, energy): b=-0.24, ) + +# (30.2) from "The Review of Particle Physics (2020)" +# https://pdg.lbl.gov/2020/reviews/rpp2020-rev-cosmic-rays.pdf PDG_ALL_PARTICLE = PowerLaw( - flux_normalization=1.8e4 * FLUX_UNIT, + flux_normalization=1.8e4 / (u.GeV * u.m**2 * u.s * u.sr), spectral_index=-2.7, e_ref=1 * u.GeV, ) From 8def2e2823e6c63c2a6c4bd87b9e6d0aa040d8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 18 Sep 2020 19:25:20 +0200 Subject: [PATCH 005/105] Fix electron spectrum --- examples/plot_spectra.py | 2 +- pyirf/spectral.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/plot_spectra.py b/examples/plot_spectra.py index 18a0634c5..a9f3505a0 100644 --- a/examples/plot_spectra.py +++ b/examples/plot_spectra.py @@ -16,7 +16,7 @@ cr_spectra = { 'PDG All Particle Spectrum': PDG_ALL_PARTICLE, 'ATIC Proton Fit (from IRF Document)': IRFDOC_PROTON_SPECTRUM, - 'Electron Spectrum (from IRFall Document)': IRFDOC_ELECTRON_SPECTRUM, + 'Electron Spectrum (from IRF Document)': IRFDOC_ELECTRON_SPECTRUM, } diff --git a/pyirf/spectral.py b/pyirf/spectral.py index 9692dd5f3..78366d04b 100644 --- a/pyirf/spectral.py +++ b/pyirf/spectral.py @@ -108,7 +108,7 @@ def __call__(self, energy): power = super().__call__(energy) log10_e = np.log10(energy / self.e_ref) gauss = norm.pdf(log10_e, self.mu, self.sigma) - return power * (1 + self.f * gauss) + return power * (1 + self.f * (np.exp(gauss) - 1)) # From "The Crab Nebula and Pulsar between 500 GeV and 80 TeV: Observations with the HEGRA stereoscopic air Cherenkov telescopes", From 55ce33af9884edac55c8a5e9cb3c2d1dfbc33c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 18 Sep 2020 20:06:00 +0200 Subject: [PATCH 006/105] Fix docstring of lima --- pyirf/statistics.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyirf/statistics.py b/pyirf/statistics.py index c573c632a..8d2a39b70 100644 --- a/pyirf/statistics.py +++ b/pyirf/statistics.py @@ -3,8 +3,7 @@ def li_ma_significance(n_on, n_off, alpha=0.2): ''' - Calculate the Li & Ma significance for given - observations data. + Calculate the Li & Ma significance. Formula (17) doi.org/10.1086/161295 @@ -20,6 +19,11 @@ def li_ma_significance(n_on, n_off, alpha=0.2): 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 = np.isscalar(n_on) From 8e0240fadda0f890195001d0a9a91350401c5a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 10:47:48 +0200 Subject: [PATCH 007/105] Electron spectrum now correct, IRF Document was relying on ROOT internals --- examples/calculate_eventdisplay_irfs.py | 6 +++-- examples/plot_spectra.py | 35 ++++++++++++++++++++----- pyirf/spectral.py | 6 ++++- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 2934db5f1..690eb44b9 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -9,11 +9,13 @@ from pyirf.spectral import PowerLaw, CRAB_HEGRA, IRFDOC_PROTON_SPECTRUM, calculate_event_weights +T_OBS = 50 * u.hour + def main(): # read gammas gammas, gamma_info = read_eventdisplay_fits('data/gamma_onSource.S.3HB9-FD_ID0.eff-0.fits') - simulated_spectrum = PowerLaw.from_simulation(gamma_info, 50 * u.hour) + simulated_spectrum = PowerLaw.from_simulation(gamma_info, T_OBS) gammas['weight'] = calculate_event_weights( true_energy=gammas['true_energy'], target_spectrum=CRAB_HEGRA, @@ -22,7 +24,7 @@ def main(): # read protons protons, proton_info = read_eventdisplay_fits('data/proton_onSource.S.3HB9-FD_ID0.eff-0.fits') - simulated_spectrum = PowerLaw.from_simulation(proton_info, 50 * u.hour) + simulated_spectrum = PowerLaw.from_simulation(proton_info, T_OBS) protons['weight'] = calculate_event_weights( true_energy=protons['true_energy'], target_spectrum=IRFDOC_PROTON_SPECTRUM, diff --git a/examples/plot_spectra.py b/examples/plot_spectra.py index a9f3505a0..ae1c3bad5 100644 --- a/examples/plot_spectra.py +++ b/examples/plot_spectra.py @@ -1,6 +1,7 @@ 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, @@ -16,13 +17,12 @@ cr_spectra = { 'PDG All Particle Spectrum': PDG_ALL_PARTICLE, 'ATIC Proton Fit (from IRF Document)': IRFDOC_PROTON_SPECTRUM, - 'Electron Spectrum (from IRF Document)': IRFDOC_ELECTRON_SPECTRUM, } if __name__ == '__main__': - energy = np.geomspace(0.01, 100, 1000) * u.TeV + energy = np.geomspace(0.001, 300, 1000) * u.TeV plt.figure(constrained_layout=True) plt.title('Crab Nebula Flux') @@ -40,24 +40,45 @@ plt.legend() plt.xscale('log') plt.yscale('log') - plt.xlabel(f'E / TeV') + 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 * FLUX_UNIT plt.plot( energy.to_value(u.TeV), - spectrum(energy).to_value(FLUX_UNIT), + (spectrum(energy) * energy**2).to_value(unit), label=label, ) plt.legend() plt.xscale('log') plt.yscale('log') - plt.xlabel(f'E / TeV') - plt.ylabel(f'Flux / ({FLUX_UNIT.to_string("latex")})') + 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.yscale('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/pyirf/spectral.py b/pyirf/spectral.py index 78366d04b..c3821e732 100644 --- a/pyirf/spectral.py +++ b/pyirf/spectral.py @@ -107,7 +107,11 @@ def __init__(self, flux_normalization, spectral_index, e_ref, f, mu, sigma): def __call__(self, energy): power = super().__call__(energy) log10_e = np.log10(energy / self.e_ref) - gauss = norm.pdf(log10_e, self.mu, self.sigma) + # 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)) From 30bd560f78ae6524d50eeefb320023884075c057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 11:06:05 +0200 Subject: [PATCH 008/105] Add readthedocs config --- .readthedocs.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..1d6318ab3 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,10 @@ +version: 2 + +python: + version: 3.7 + install: + - method: pip + path: . + extra_requirements: + - docs + system_packages: false From c13edb3636ea83b293be3c4cf28d17f589a51d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 11:07:05 +0200 Subject: [PATCH 009/105] Fix indentation in readthedocs config --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 1d6318ab3..f943a0de6 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,5 +6,5 @@ python: - method: pip path: . extra_requirements: - - docs + - docs system_packages: false From 21ad595bc6124de0611d42fd365dbf786e5cd7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 11:12:03 +0200 Subject: [PATCH 010/105] Add test requirements --- .readthedocs.yml | 1 + setup.py | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index f943a0de6..87b6bf2c3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,4 +7,5 @@ python: path: . extra_requirements: - docs + - tests system_packages: false diff --git a/setup.py b/setup.py index e1c6c812e..df0b33d88 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,22 @@ with open("pyirf/version.py") as f: __version__ = re.search('^__version__ = "(.*)"$', f.read()).group(1) +extras_require = { + 'docs': [ + 'rinohtype', + 'sphinx', + 'sphinx_rtd_theme', + 'sphinx_automodapi', + 'numpydoc', + 'nbsphinx' + ], + 'tests': [ + 'pytest', + ], +} + +extras_require['all'] = extras_require['tests'] + extras_require['docs'] + setup( version=__version__, packages=find_packages(), @@ -17,14 +33,5 @@ "tables", "gammapy~=0.8.0", ], - extras_require={ - 'docs': [ - 'rinohtype', - 'sphinx', - 'sphinx_rtd_theme', - 'sphinx_automodapi', - 'numpydoc', - 'nbsphinx' - ] - } + extras_require=extras_require, ) From 4eb556a1096cffc9d5a3b87a9b376250eb97b27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 12:05:43 +0200 Subject: [PATCH 011/105] Add calculating sensitivity for a fixed set of cuts Co-authored-by: Michele Peresano Co-authored-by: Lukas Nickel --- examples/calculate_eventdisplay_irfs.py | 37 ++++++++++++++++++- examples/plot_spectra.py | 2 -- pyirf/binning.py | 11 ++++++ pyirf/sensitiviy.py | 47 +++++++++++++++++++------ 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 690eb44b9..53b941c40 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -5,7 +5,11 @@ https://github.com/Eventdisplay/Converters/blob/master/DL2/generate_DL2_file.py ''' import astropy.units as u +from astropy.coordinates.angle_utilities import angular_separation from pyirf.io.eventdisplay import read_eventdisplay_fits +from pyirf.binning import create_bins_per_decade, add_overflow_bins, calculate_bin_indices, create_histogram_table +from pyirf.sensitiviy import calculate_sensitivity +import numpy as np from pyirf.spectral import PowerLaw, CRAB_HEGRA, IRFDOC_PROTON_SPECTRUM, calculate_event_weights @@ -22,6 +26,7 @@ def main(): simulated_spectrum=simulated_spectrum, ) + # read protons protons, proton_info = read_eventdisplay_fits('data/proton_onSource.S.3HB9-FD_ID0.eff-0.fits') simulated_spectrum = PowerLaw.from_simulation(proton_info, T_OBS) @@ -31,7 +36,37 @@ def main(): simulated_spectrum=simulated_spectrum, ) - # perform cut optimization + # sensitivity binning + bins_e_reco = add_overflow_bins(create_bins_per_decade(1e-2 * u.TeV, 200 * u.TeV, 5)) + + # calculate theta (angular distance from source pos to reco pos) + + for tab in (gammas, protons): + tab['theta'] = angular_separation( + tab['true_az'], tab['true_alt'], + tab['reco_az'], tab['reco_alt'], + ) + tab['bin_reco_energy'] = calculate_bin_indices( + tab['reco_energy'], bins_e_reco + ) + + theta_cut = np.percentile(gammas['theta'], 68) + print(f'Using theta cut: {theta_cut.to(u.deg):.2f}') + gh_cut = 0.0 + + for tab in (gammas, protons): + tab['selected'] = (tab['gh_score'] > gh_cut) & (tab['theta'] < theta_cut) + + print(f'Remaining gammas: {np.count_nonzero(gammas["selected"])} of {len(gammas)}') + print(f'Remaining protons: {np.count_nonzero(protons["selected"])} of {len(protons)}') + + signal = create_histogram_table(gammas[gammas['selected']], bins_e_reco, 'reco_energy') + background = create_histogram_table(protons[protons['selected']], bins_e_reco, 'reco_energy') + print(signal) + + print(calculate_sensitivity(signal, background, 1, T_OBS)) + + # calculate sensitivity for best cuts # calculate IRFs for the best cuts diff --git a/examples/plot_spectra.py b/examples/plot_spectra.py index ae1c3bad5..45ded626f 100644 --- a/examples/plot_spectra.py +++ b/examples/plot_spectra.py @@ -60,7 +60,6 @@ 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') @@ -74,7 +73,6 @@ plt.legend() plt.xscale('log') - # plt.yscale('log') plt.xlim(5e-3, 10) plt.ylim(1e-5, 0.25e-3) plt.xlabel(r'$E \,\,/\,\, \mathrm{TeV}$') diff --git a/pyirf/binning.py b/pyirf/binning.py index f60a2362f..7fadaa928 100644 --- a/pyirf/binning.py +++ b/pyirf/binning.py @@ -4,6 +4,7 @@ import numpy as np import astropy.units as u +from astropy.table import QTable def add_overflow_bins(bins, positive=True): @@ -92,3 +93,13 @@ def calculate_bin_indices(data, bins): bins = bins.to_value(unit) return np.digitize(data, bins) - 1 + + +def create_histogram_table(events, bins, key='reco_energy'): + 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) + hist['n_weighted'], _ = np.histogram(events[key], bins, weights=events['weight']) + return hist diff --git a/pyirf/sensitiviy.py b/pyirf/sensitiviy.py index 02aa61cc0..716006107 100644 --- a/pyirf/sensitiviy.py +++ b/pyirf/sensitiviy.py @@ -2,6 +2,7 @@ import numpy as np from scipy.optimize import newton import warnings +from astropy.table import QTable from .statistics import li_ma_significance @@ -88,18 +89,42 @@ def equation(relative_flux): warnings.warn('Could not calculate relative significance, returning nan') return np.nan + if result.size == 1: + return result[0] + return result -# make the function accept numpy arrays for n_on, n_off, -# so we can provide all energy bins -relative_sensitivity = np.vectorize( - relative_sensitivity, - excluded=[ - 't_obs', - 't_ref', - 'alpha', - 'target_significance', - 'significance_function', +@u.quantity_input(t_obs=u.hour, t_ref=u.hour) +def calculate_sensitivity( + signal, + background, + alpha, + t_obs, + t_ref=u.Quantity(50, u.hour), + target_significance=5, + significance_function=li_ma_significance, + initial_guess=0.5, +): + assert len(signal) == len(background) + assert np.all(signal['reco_energy_low'] == background['reco_energy_low']) + + sensitivity = QTable() + sensitivity['reco_energy_low'] = signal['reco_energy_low'] + sensitivity['reco_energy_high'] = signal['reco_energy_high'] + sensitivity['n_signal'] = signal['n'] + sensitivity['n_signal_weighted'] = signal['n_weighted'] + sensitivity['n_background'] = background['n'] + sensitivity['n_background_weighted'] = background['n_weighted'] + + sensitivity['relative_sensitivity'] = [ + relative_sensitivity( + n_on=n_signal + alpha * n_background, + n_off=n_background, + alpha=1.0, + t_obs=t_obs, + ) + for n_signal, n_background in zip(signal['n_weighted'], background['n_weighted']) ] -) + + return sensitivity From 1ed76c70e8cb02004e7ed4509af7186d54cdc6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 13:44:11 +0200 Subject: [PATCH 012/105] Add bin center, improve check --- pyirf/sensitiviy.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyirf/sensitiviy.py b/pyirf/sensitiviy.py index 716006107..e56235c23 100644 --- a/pyirf/sensitiviy.py +++ b/pyirf/sensitiviy.py @@ -107,11 +107,18 @@ def calculate_sensitivity( initial_guess=0.5, ): assert len(signal) == len(background) - assert np.all(signal['reco_energy_low'] == background['reco_energy_low']) sensitivity = QTable() - sensitivity['reco_energy_low'] = signal['reco_energy_low'] - sensitivity['reco_energy_high'] = signal['reco_energy_high'] + + # check binning information and add to output + for k in ('low', 'center', 'high'): + k = 'reco_energy_' + k + if not np.all(signal[k] == background[k]): + raise ValueError('Binning for signal and background must be equal') + + sensitivity[k] = signal[k] + + # add event number information sensitivity['n_signal'] = signal['n'] sensitivity['n_signal_weighted'] = signal['n_weighted'] sensitivity['n_background'] = background['n'] From b6a52527f9a50565797953506a91b1aeafd30946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 13:46:39 +0200 Subject: [PATCH 013/105] Do not give format, prevents reading root files accidentally --- pyirf/io/eventdisplay.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py index cebec403b..e1ee54d5a 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -30,9 +30,9 @@ def read_eventdisplay_fits(infile): simulated_events: ``~pyirf.simulations.SimulatedEventsInfo`` """ - events_table = QTable.read(infile, format='fits', hdu='EVENTS') - sim_events = QTable.read(infile, format='fits', hdu='SIMULATED EVENTS') - run_header = QTable.read(infile, format='fits', hdu='RUNHEADER')[0] + 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] From cc58e6686e64529b01a79575406d612caf6a36d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 13:48:04 +0200 Subject: [PATCH 014/105] Add flux sensitivity --- examples/calculate_eventdisplay_irfs.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 53b941c40..dcb109e66 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -62,9 +62,11 @@ def main(): signal = create_histogram_table(gammas[gammas['selected']], bins_e_reco, 'reco_energy') background = create_histogram_table(protons[protons['selected']], bins_e_reco, 'reco_energy') - print(signal) - print(calculate_sensitivity(signal, background, 1, T_OBS)) + sensitivity = calculate_sensitivity(signal, background, 1, T_OBS) + sensitivity['flux_sensitivity'] = sensitivity['relative_sensitivity'] * CRAB_HEGRA(sensitivity['reco_energy_center']) + + print(sensitivity) # calculate sensitivity for best cuts From 56de70308c30b3ffd829e2066d6bac7f0a100c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 15:04:37 +0200 Subject: [PATCH 015/105] Add electrons --- examples/calculate_eventdisplay_irfs.py | 79 ++++++++++++++++--------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index dcb109e66..87ad4cb4e 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -10,38 +10,46 @@ from pyirf.binning import create_bins_per_decade, add_overflow_bins, calculate_bin_indices, create_histogram_table from pyirf.sensitiviy import calculate_sensitivity import numpy as np +from astropy import table -from pyirf.spectral import PowerLaw, CRAB_HEGRA, IRFDOC_PROTON_SPECTRUM, calculate_event_weights +from pyirf.spectral import PowerLaw, CRAB_HEGRA, IRFDOC_PROTON_SPECTRUM, calculate_event_weights, IRFDOC_ELECTRON_SPECTRUM T_OBS = 50 * u.hour -def main(): - # read gammas - gammas, gamma_info = read_eventdisplay_fits('data/gamma_onSource.S.3HB9-FD_ID0.eff-0.fits') - simulated_spectrum = PowerLaw.from_simulation(gamma_info, T_OBS) - gammas['weight'] = calculate_event_weights( - true_energy=gammas['true_energy'], - target_spectrum=CRAB_HEGRA, - simulated_spectrum=simulated_spectrum, - ) +particles = { + 'gamma': { + 'file': 'data/gamma_onSource.S.3HB9-FD_ID0.eff-0.fits', + 'target_spectrum': CRAB_HEGRA, + }, + 'proton': { + 'file': 'data/proton_onSource.S.3HB9-FD_ID0.eff-0.fits', + 'target_spectrum': IRFDOC_PROTON_SPECTRUM, + }, + 'electron': { + 'file': 'data/electron_onSource.S.3HB9-FD_ID0.eff-0.fits', + 'target_spectrum': IRFDOC_ELECTRON_SPECTRUM, + }, +} - # read protons - protons, proton_info = read_eventdisplay_fits('data/proton_onSource.S.3HB9-FD_ID0.eff-0.fits') - simulated_spectrum = PowerLaw.from_simulation(proton_info, T_OBS) - protons['weight'] = calculate_event_weights( - true_energy=protons['true_energy'], - target_spectrum=IRFDOC_PROTON_SPECTRUM, - simulated_spectrum=simulated_spectrum, - ) +def main(): + for p in particles.values(): + 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'] + ) # sensitivity binning - bins_e_reco = add_overflow_bins(create_bins_per_decade(1e-2 * u.TeV, 200 * u.TeV, 5)) + bins_e_reco = add_overflow_bins(create_bins_per_decade( + 10**-1.9 * u.TeV, 10**2.31 * u.TeV, bins_per_decade=5 + )) # calculate theta (angular distance from source pos to reco pos) - for tab in (gammas, protons): + for p in particles.values(): + tab = p['events'] tab['theta'] = angular_separation( tab['true_az'], tab['true_alt'], tab['reco_az'], tab['reco_alt'], @@ -50,23 +58,36 @@ def main(): tab['reco_energy'], bins_e_reco ) - theta_cut = np.percentile(gammas['theta'], 68) + theta_cut = np.percentile(particles['gamma']['events']['theta'], 68) print(f'Using theta cut: {theta_cut.to(u.deg):.2f}') gh_cut = 0.0 - for tab in (gammas, protons): - tab['selected'] = (tab['gh_score'] > gh_cut) & (tab['theta'] < theta_cut) + for k, p in particles.items(): + tab = p['events'] + tab['selected'] = ( + (tab['gh_score'] > gh_cut) + & (tab['theta'] < theta_cut) + ) + + print(f'Remaining {k}s: {np.count_nonzero(tab["selected"])} of {len(tab)}') - print(f'Remaining gammas: {np.count_nonzero(gammas["selected"])} of {len(gammas)}') - print(f'Remaining protons: {np.count_nonzero(protons["selected"])} of {len(protons)}') + gammas = particles['gamma']['events'] + signal = gammas[gammas['selected']] + signal_hist = create_histogram_table(signal, bins_e_reco, 'reco_energy') - signal = create_histogram_table(gammas[gammas['selected']], bins_e_reco, 'reco_energy') - background = create_histogram_table(protons[protons['selected']], bins_e_reco, 'reco_energy') + background = table.vstack([ + particles['proton']['events'], + particles['electron']['events'] + ]) + background_hist = create_histogram_table( + background[background['selected']], bins_e_reco, 'reco_energy' + ) - sensitivity = calculate_sensitivity(signal, background, 1, T_OBS) + sensitivity = calculate_sensitivity(signal_hist, background_hist, 1, T_OBS) sensitivity['flux_sensitivity'] = sensitivity['relative_sensitivity'] * CRAB_HEGRA(sensitivity['reco_energy_center']) - print(sensitivity) + sensitivity.meta['EXTNAME'] = 'SENSITIVITY' + sensitivity.write('sensitivity.fits', overwrite=True) # calculate sensitivity for best cuts From 8c709ca9db993541cc5cd3ca962a9fd00823732e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 16:02:18 +0200 Subject: [PATCH 016/105] Add general function to apply cuts --- examples/calculate_eventdisplay_irfs.py | 21 ++++--- pyirf/cuts.py | 58 ++++++++++++++++++++ pyirf/tests/test_cuts.py | 73 +++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 pyirf/cuts.py create mode 100644 pyirf/tests/test_cuts.py diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 87ad4cb4e..ce7047bb9 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -9,6 +9,7 @@ from pyirf.io.eventdisplay import read_eventdisplay_fits from pyirf.binning import create_bins_per_decade, add_overflow_bins, calculate_bin_indices, create_histogram_table from pyirf.sensitiviy import calculate_sensitivity +from pyirf.cuts import is_selected import numpy as np from astropy import table @@ -58,17 +59,21 @@ def main(): tab['reco_energy'], bins_e_reco ) - theta_cut = np.percentile(particles['gamma']['events']['theta'], 68) - print(f'Using theta cut: {theta_cut.to(u.deg):.2f}') - gh_cut = 0.0 + cuts = { + 'theta': { + 'operator': 'le', + 'cut_values': np.percentile(particles['gamma']['events']['theta'], 68), + }, + 'gh_score': { + 'operator': 'ge', + 'cut_values': 0.0, + } + } + print("Using the cuts:", cuts) for k, p in particles.items(): tab = p['events'] - tab['selected'] = ( - (tab['gh_score'] > gh_cut) - & (tab['theta'] < theta_cut) - ) - + tab['selected'] = is_selected(tab, cuts, tab['bin_reco_energy']) print(f'Remaining {k}s: {np.count_nonzero(tab["selected"])} of {len(tab)}') gammas = particles['gamma']['events'] diff --git a/pyirf/cuts.py b/pyirf/cuts.py new file mode 100644 index 000000000..4b70f6bb5 --- /dev/null +++ b/pyirf/cuts.py @@ -0,0 +1,58 @@ +import operator +import numpy as np + + +def is_scalar(val): + '''Workaround that also supports astropy quantities''' + return np.array(val, copy=False).shape == tuple() + + +def is_selected(events, cut_definition, bin_index=None): + ''' + Retun a boolean mask, if the given ``events`` survive the cuts defined + in ``cut_definition``. + This function supports bin-wise cuts when given the ``bin_index`` argument. + + Parameters + ---------- + events: ``~astropy.table.QTable`` + events table + cut_definition: dict + A dict describing the cuts to make. + The keys are column names in ``events`` to which a cut should be applied. + The values must be dictionaries with the key ``'operator'`` containing the + name of the binary comparison operator to use and the key ``'cut_values'``, + which is either a single number of quantity or a Quantity or an array + containing the cut value for each bin. + bin_index: np.ndarray[int] + Bin index for each event in the ``events`` table, only needed if + bin-wise cut values are used. + + Returns + ------- + selected: np.ndarray[bool] + Boolean mask if an event survived the specified cuts. + ''' + mask = np.ones(len(events), dtype=np.bool) + + for key, definition in cut_definition.items(): + + op = getattr(operator, definition['operator']) + + # for a single number, just use the value + + if is_scalar(definition['cut_values']): + cut_value = definition['cut_values'] + + # if it is an array, it is per bin, so we get the correct + # cut value for each event + else: + if bin_index is None: + raise ValueError( + 'You need to provide `bin_index` if cut_values are per bin' + ) + cut_value = np.asanyarray(definition['cut_values'])[bin_index] + + mask &= op(events[key], cut_value) + + return mask diff --git a/pyirf/tests/test_cuts.py b/pyirf/tests/test_cuts.py new file mode 100644 index 000000000..939964a22 --- /dev/null +++ b/pyirf/tests/test_cuts.py @@ -0,0 +1,73 @@ +import numpy as np +from astropy.table import QTable +import astropy.units as u +import pytest + + +@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_is_selected(events): + from pyirf.cuts import is_selected + + cut_definition = { + 'theta': { + 'operator': 'le', + 'cut_values': [0.05, 0.15, 0.25] * u.deg, + }, + 'gh_score': { + 'operator': 'ge', + 'cut_values': np.array([0.0, 0.1, 0.5]), + } + } + + # if you make no cuts, all events are selected + assert np.all(is_selected(events, {}, bin_index=events['bin_reco_energy']) == True) # noqa + + selected = is_selected( + events, cut_definition, bin_index=events['bin_reco_energy'] + ) + + assert selected.dtype == np.bool + assert np.all(selected == [False, False, False, False, True, False]) + + +def test_is_selected_single_numbers(events): + from pyirf.cuts import is_selected + + cut_definition = { + 'theta': { + 'operator': 'le', + 'cut_values': 0.05 * u.deg, + }, + 'gh_score': { + 'operator': 'ge', + 'cut_values': 0.5, + } + } + + selected = is_selected( + events, cut_definition, bin_index=events['bin_reco_energy'] + ) + + assert selected.dtype == np.bool + assert np.all(selected == [False, False, False, False, True, False]) + + +def test_is_scalar(): + from pyirf.cuts 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))) From 81e1aa97292c4aec199535c8a823f91baa22e91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Mon, 21 Sep 2020 16:25:50 +0200 Subject: [PATCH 017/105] Fix typo --- examples/calculate_eventdisplay_irfs.py | 2 +- pyirf/{sensitiviy.py => sensitivity.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pyirf/{sensitiviy.py => sensitivity.py} (100%) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index ce7047bb9..acf247eaf 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -8,7 +8,7 @@ from astropy.coordinates.angle_utilities import angular_separation from pyirf.io.eventdisplay import read_eventdisplay_fits from pyirf.binning import create_bins_per_decade, add_overflow_bins, calculate_bin_indices, create_histogram_table -from pyirf.sensitiviy import calculate_sensitivity +from pyirf.sensitivity import calculate_sensitivity from pyirf.cuts import is_selected import numpy as np from astropy import table diff --git a/pyirf/sensitiviy.py b/pyirf/sensitivity.py similarity index 100% rename from pyirf/sensitiviy.py rename to pyirf/sensitivity.py From 2e50c0f574a5091bef660b53b0f9a9ea480ed113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 12:15:37 +0200 Subject: [PATCH 018/105] Implement functions to calculate and apply binned cuts --- pyirf/cuts.py | 99 +++++++++++++++++++++++++++++++++++++-- pyirf/tests/test_cuts.py | 66 ++++++++++++++++++++------ pyirf/tests/test_utils.py | 15 ++++++ pyirf/utils.py | 6 +++ 4 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 pyirf/tests/test_utils.py create mode 100644 pyirf/utils.py diff --git a/pyirf/cuts.py b/pyirf/cuts.py index 4b70f6bb5..c0ee059bc 100644 --- a/pyirf/cuts.py +++ b/pyirf/cuts.py @@ -1,10 +1,101 @@ import operator + import numpy as np +from astropy.table import Table + +from .binning import calculate_bin_indices +from .utils import is_scalar + + +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 -def is_scalar(val): - '''Workaround that also supports astropy quantities''' - return np.array(val, copy=False).shape == tuple() + # 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) def is_selected(events, cut_definition, bin_index=None): @@ -44,7 +135,7 @@ def is_selected(events, cut_definition, bin_index=None): if is_scalar(definition['cut_values']): cut_value = definition['cut_values'] - # if it is an array, it is per bin, so we get the correct + # if it is an array, it is per bin, so we get the correct # cut value for each event else: if bin_index is None: diff --git a/pyirf/tests/test_cuts.py b/pyirf/tests/test_cuts.py index 939964a22..4398ab2dc 100644 --- a/pyirf/tests/test_cuts.py +++ b/pyirf/tests/test_cuts.py @@ -1,7 +1,9 @@ +import operator import numpy as np -from astropy.table import QTable +from astropy.table import QTable, Table import astropy.units as u import pytest +from scipy.stats import norm @pytest.fixture @@ -13,6 +15,55 @@ def events(): }) +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 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]) + + def test_is_selected(events): from pyirf.cuts import is_selected @@ -58,16 +109,3 @@ def test_is_selected_single_numbers(events): assert selected.dtype == np.bool assert np.all(selected == [False, False, False, False, True, False]) - - -def test_is_scalar(): - from pyirf.cuts 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))) diff --git a/pyirf/tests/test_utils.py b/pyirf/tests/test_utils.py new file mode 100644 index 000000000..c75e297e5 --- /dev/null +++ b/pyirf/tests/test_utils.py @@ -0,0 +1,15 @@ +import numpy as np +import astropy.units as u + + +def test_is_scalar(): + from pyirf.cuts 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))) diff --git a/pyirf/utils.py b/pyirf/utils.py new file mode 100644 index 000000000..bb0708194 --- /dev/null +++ b/pyirf/utils.py @@ -0,0 +1,6 @@ +import numpy as np + + +def is_scalar(val): + '''Workaround that also supports astropy quantities''' + return np.array(val, copy=False).shape == tuple() From 0d6cfe32d91f2b601de8f179e59ea2023814c5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 12:16:03 +0200 Subject: [PATCH 019/105] Use smaller initial guess in sensitivity --- pyirf/sensitivity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index e56235c23..8ac98fbcb 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -17,7 +17,7 @@ def relative_sensitivity( t_ref=u.Quantity(50, u.hour), target_significance=5, significance_function=li_ma_significance, - initial_guess=0.5, + initial_guess=0.01, ): ''' Calculate the relative sensitivity defined as the flux @@ -60,7 +60,7 @@ def relative_sensitivity( Initial guess for the root finder ''' - ratio = (t_ref / t_obs).si + ratio = (t_ref / t_obs).to(u.one) n_on = n_on * ratio n_off = n_off * ratio From 87ca45bef37891e7a0d83d7657c023cde2e34cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 12:16:21 +0200 Subject: [PATCH 020/105] Calculate number of showers from run header info The hEMC contains events weighted to a spectrum of -2.5, not the number of simulated CORSIKA showers. We now compute this information from the guesstimated number of runs (number of unique obs ids from the events table) and the run header information. --- pyirf/io/eventdisplay.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py index e1ee54d5a..f2b069d9c 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -3,6 +3,12 @@ from ..simulations import SimulatedEventsInfo +import logging +import numpy as np + + +log = logging.getLogger(__name__) + COLUMN_MAP = { 'obs_id': 'OBS_ID', @@ -39,8 +45,15 @@ def read_eventdisplay_fits(infile): 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=sim_events['EVENTS'].sum(), + 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), From 0288711beee1a8886cc962f39680aff6ed71be19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 12:18:31 +0200 Subject: [PATCH 021/105] Use 40% efficiency gh cut and finer binning for theta cut --- examples/calculate_eventdisplay_irfs.py | 75 ++++++++++++++++++------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index acf247eaf..ecf135076 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -4,20 +4,26 @@ 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.coordinates.angle_utilities import angular_separation + from pyirf.io.eventdisplay import read_eventdisplay_fits from pyirf.binning import create_bins_per_decade, add_overflow_bins, calculate_bin_indices, create_histogram_table from pyirf.sensitivity import calculate_sensitivity -from pyirf.cuts import is_selected -import numpy as np -from astropy import table +from pyirf.cuts import calculate_percentile_cut, evaluate_binned_cut from pyirf.spectral import PowerLaw, CRAB_HEGRA, IRFDOC_PROTON_SPECTRUM, calculate_event_weights, IRFDOC_ELECTRON_SPECTRUM T_OBS = 50 * u.hour + + particles = { 'gamma': { 'file': 'data/gamma_onSource.S.3HB9-FD_ID0.eff-0.fits', @@ -35,13 +41,20 @@ def main(): - for p in particles.values(): + logging.basicConfig(level=logging.DEBUG) + + for k, p in particles.items(): + 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'] ) + print(f'Simulated {k.title()} Events:') + print(p['simulation_info']) + print() + # sensitivity binning bins_e_reco = add_overflow_bins(create_bins_per_decade( 10**-1.9 * u.TeV, 10**2.31 * u.TeV, bins_per_decade=5 @@ -59,31 +72,51 @@ def main(): tab['reco_energy'], bins_e_reco ) - cuts = { - 'theta': { - 'operator': 'le', - 'cut_values': np.percentile(particles['gamma']['events']['theta'], 68), - }, - 'gh_score': { - 'operator': 'ge', - 'cut_values': 0.0, - } - } - print("Using the cuts:", cuts) + gammas = particles['gamma']['events'] - for k, p in particles.items(): + # 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, + 100, + )) + + theta_cuts = calculate_percentile_cut( + gammas['theta'], + gammas['reco_energy'], + bins=theta_bins, + min_value=0.05 * u.deg, + fill_value=np.nan * u.deg, + percentile=68, + ) + theta_cuts.meta['EXTNAME'] = 'THETACUTS' + theta_cuts.write('theta_cuts.fits', overwrite=True) + + # get cut with fixed efficiency of 40% events left + gh_cut = np.percentile(gammas['gh_score'], 60) + print(f'Using fixed G/H cut of {gh_cut}') + + for p in particles.values(): tab = p['events'] - tab['selected'] = is_selected(tab, cuts, tab['bin_reco_energy']) - print(f'Remaining {k}s: {np.count_nonzero(tab["selected"])} of {len(tab)}') + tab['selected_theta'] = evaluate_binned_cut( + tab['theta'], + tab['reco_energy'], + theta_cuts, + operator.le, + ) - gammas = particles['gamma']['events'] - signal = gammas[gammas['selected']] - signal_hist = create_histogram_table(signal, bins_e_reco, 'reco_energy') + tab['selected_gh'] = tab['gh_score'] > gh_cut + + tab['selected'] = tab['selected_gh'] & tab['selected_theta'] + signal = gammas[gammas['selected']] background = table.vstack([ particles['proton']['events'], particles['electron']['events'] ]) + + signal_hist = create_histogram_table(signal, bins_e_reco, 'reco_energy') background_hist = create_histogram_table( background[background['selected']], bins_e_reco, 'reco_energy' ) From 380a08320f5749bf5b30d50c83739a62a5d25ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 13:22:43 +0200 Subject: [PATCH 022/105] Optmize gh cut --- examples/calculate_eventdisplay_irfs.py | 80 +++++++++++++------------ pyirf/sensitivity.py | 8 ++- setup.py | 1 + 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index ecf135076..e020cb61e 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -13,15 +13,24 @@ from astropy.coordinates.angle_utilities import angular_separation from pyirf.io.eventdisplay import read_eventdisplay_fits -from pyirf.binning import create_bins_per_decade, add_overflow_bins, calculate_bin_indices, create_histogram_table -from pyirf.sensitivity import calculate_sensitivity +from pyirf.binning import create_bins_per_decade, add_overflow_bins from pyirf.cuts import calculate_percentile_cut, evaluate_binned_cut +from astropy.io import fits -from pyirf.spectral import PowerLaw, CRAB_HEGRA, IRFDOC_PROTON_SPECTRUM, calculate_event_weights, IRFDOC_ELECTRON_SPECTRUM +from pyirf.spectral import ( + calculate_event_weights, + PowerLaw, + CRAB_HEGRA, + IRFDOC_PROTON_SPECTRUM, + IRFDOC_ELECTRON_SPECTRUM, +) +from pyirf.cut_optimization import optimize_gh_cut -T_OBS = 50 * u.hour + +log = logging.getLogger('pyirf') +T_OBS = 50 * u.hour particles = { @@ -44,6 +53,7 @@ def main(): logging.basicConfig(level=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) @@ -51,14 +61,8 @@ def main(): p['events']['true_energy'], p['target_spectrum'], p['simulated_spectrum'] ) - print(f'Simulated {k.title()} Events:') - print(p['simulation_info']) - print() - - # sensitivity binning - bins_e_reco = add_overflow_bins(create_bins_per_decade( - 10**-1.9 * u.TeV, 10**2.31 * u.TeV, bins_per_decade=5 - )) + log.info(p['simulation_info']) + log.info('') # calculate theta (angular distance from source pos to reco pos) @@ -68,12 +72,12 @@ def main(): tab['true_az'], tab['true_alt'], tab['reco_az'], tab['reco_alt'], ) - tab['bin_reco_energy'] = calculate_bin_indices( - tab['reco_energy'], bins_e_reco - ) gammas = particles['gamma']['events'] + gh_cut = 0.0 + log.info(f'Using fixed G/H cut of {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( @@ -82,21 +86,19 @@ def main(): 100, )) + # theta cut is 68 percent containmente of the gammas + # for now with a fixed global, unoptimized score cut + mask_theta_cuts = gammas['gh_score'] >= gh_cut theta_cuts = calculate_percentile_cut( - gammas['theta'], - gammas['reco_energy'], + 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, ) - theta_cuts.meta['EXTNAME'] = 'THETACUTS' - theta_cuts.write('theta_cuts.fits', overwrite=True) - - # get cut with fixed efficiency of 40% events left - gh_cut = np.percentile(gammas['gh_score'], 60) - print(f'Using fixed G/H cut of {gh_cut}') + # evaluate the theta cut for p in particles.values(): tab = p['events'] tab['selected_theta'] = evaluate_binned_cut( @@ -106,32 +108,36 @@ def main(): operator.le, ) - tab['selected_gh'] = tab['gh_score'] > gh_cut - - tab['selected'] = tab['selected_gh'] & tab['selected_theta'] - - signal = gammas[gammas['selected']] + # background table composed of both electrons and protons background = table.vstack([ particles['proton']['events'], particles['electron']['events'] ]) - signal_hist = create_histogram_table(signal, bins_e_reco, 'reco_energy') - background_hist = create_histogram_table( - background[background['selected']], bins_e_reco, 'reco_energy' + # 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 + )) + sensitivity, 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, ) - sensitivity = calculate_sensitivity(signal_hist, background_hist, 1, T_OBS) sensitivity['flux_sensitivity'] = sensitivity['relative_sensitivity'] * CRAB_HEGRA(sensitivity['reco_energy_center']) - sensitivity.meta['EXTNAME'] = 'SENSITIVITY' - sensitivity.write('sensitivity.fits', overwrite=True) - - # calculate sensitivity for best cuts - # calculate IRFs for the best cuts # write OGADF output file + hdus = [ + fits.PrimaryHDU(), + fits.BinTableHDU(sensitivity, name='SENSITIVITY'), + fits.BinTableHDU(theta_cuts, name='THETA_CUTS'), + fits.BinTableHDU(gh_cuts, name='GH_CUTS'), + ] + fits.HDUList(hdus).writeto('sensitivity.fits.gz', overwrite=True) if __name__ == '__main__': diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index 8ac98fbcb..fa9ea6892 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -104,7 +104,6 @@ def calculate_sensitivity( t_ref=u.Quantity(50, u.hour), target_significance=5, significance_function=li_ma_significance, - initial_guess=0.5, ): assert len(signal) == len(background) @@ -134,4 +133,11 @@ def calculate_sensitivity( for n_signal, n_background in zip(signal['n_weighted'], background['n_weighted']) ] + # safety checks + invalid = ( + (sensitivity['n_signal_weighted'] < 10) | + (sensitivity['n_signal_weighted'] < 0.05 * sensitivity['n_background_weighted']) + ) + sensitivity['relative_sensitivity'][invalid] = np.nan + return sensitivity diff --git a/setup.py b/setup.py index df0b33d88..393b13f0e 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "numpy>=1.18", "pandas", "scipy", + "tqdm", "tables", "gammapy~=0.8.0", ], From 1f03df74f12bf7a5ca446436c01674f9251e15e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 17:17:31 +0200 Subject: [PATCH 023/105] Correct reading of true azimuth for event display data --- pyirf/io/eventdisplay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py index f2b069d9c..219ad10eb 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -16,7 +16,7 @@ 'true_energy': 'MC_ENERGY', 'reco_energy': 'ENERGY', 'true_alt': 'MC_ALT', - 'true_az': 'AZ', + 'true_az': 'MC_AZ', 'reco_alt': 'ALT', 'reco_az': 'AZ', 'gh_score': 'GH_MVA', From eeadddea1b64f0965ab2b697b7a8b379363edec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 17:27:11 +0200 Subject: [PATCH 024/105] Recalculate theta cut after optimizing gh cut --- examples/calculate_eventdisplay_irfs.py | 38 ++++++++++-- pyirf/cut_optimization.py | 80 +++++++++++++++++++++++++ pyirf/sensitivity.py | 26 ++++---- 3 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 pyirf/cut_optimization.py diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index e020cb61e..e6d76d80e 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -10,12 +10,13 @@ import numpy as np from astropy import table import astropy.units as u +from astropy.io import fits from astropy.coordinates.angle_utilities import angular_separation from pyirf.io.eventdisplay import read_eventdisplay_fits -from pyirf.binning import create_bins_per_decade, add_overflow_bins +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 astropy.io import fits +from pyirf.sensitivity import calculate_sensitivity from pyirf.spectral import ( calculate_event_weights, @@ -83,7 +84,7 @@ def main(): theta_bins = add_overflow_bins(create_bins_per_decade( 10**(-1.9) * u.TeV, 10**2.3005 * u.TeV, - 100, + 50, )) # theta cut is 68 percent containmente of the gammas @@ -118,7 +119,9 @@ def main(): sensitivity_bins = add_overflow_bins(create_bins_per_decade( 10**-1.9 * u.TeV, 10**2.31 * u.TeV, bins_per_decade=5 )) - sensitivity, gh_cuts = optimize_gh_cut( + + 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, @@ -126,7 +129,30 @@ def main(): op=operator.ge, ) - sensitivity['flux_sensitivity'] = sensitivity['relative_sensitivity'] * CRAB_HEGRA(sensitivity['reco_energy_center']) + # 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, + ) + + for tab in (gammas, background): + tab['selected_theta'] = evaluate_binned_cut(tab['theta'], tab['reco_energy'], theta_cuts_opt, 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=1, t_obs=T_OBS) + + # 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']) # calculate IRFs for the best cuts @@ -134,7 +160,9 @@ def main(): 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'), ] fits.HDUList(hdus).writeto('sensitivity.fits.gz', overwrite=True) diff --git a/pyirf/cut_optimization.py b/pyirf/cut_optimization.py new file mode 100644 index 000000000..bfa30b65c --- /dev/null +++ b/pyirf/cut_optimization.py @@ -0,0 +1,80 @@ +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 + + +def optimize_gh_cut(signal, background, bins, cut_values, op, 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=1, + t_obs=50 * u.hour, + ) + 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/sensitivity.py b/pyirf/sensitivity.py index fa9ea6892..18e91f214 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -97,40 +97,40 @@ def equation(relative_flux): @u.quantity_input(t_obs=u.hour, t_ref=u.hour) def calculate_sensitivity( - signal, - background, + signal_hist, + background_hist, alpha, t_obs, t_ref=u.Quantity(50, u.hour), target_significance=5, significance_function=li_ma_significance, ): - assert len(signal) == len(background) + assert len(signal_hist) == len(background_hist) sensitivity = QTable() # check binning information and add to output for k in ('low', 'center', 'high'): k = 'reco_energy_' + k - if not np.all(signal[k] == background[k]): - raise ValueError('Binning for signal and background must be equal') + if not np.all(signal_hist[k] == background_hist[k]): + raise ValueError('Binning for signal_hist and background_hist must be equal') - sensitivity[k] = signal[k] + sensitivity[k] = signal_hist[k] # add event number information - sensitivity['n_signal'] = signal['n'] - sensitivity['n_signal_weighted'] = signal['n_weighted'] - sensitivity['n_background'] = background['n'] - sensitivity['n_background_weighted'] = background['n_weighted'] + 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 + alpha * n_background, - n_off=n_background, + n_on=n_signal_hist + alpha * n_background_hist, + n_off=n_background_hist, alpha=1.0, t_obs=t_obs, ) - for n_signal, n_background in zip(signal['n_weighted'], background['n_weighted']) + for n_signal_hist, n_background_hist in zip(signal_hist['n_weighted'], background_hist['n_weighted']) ] # safety checks From cbf3d6af2b0139abeb8a29c4acaed2aa1e1c1623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 18:12:39 +0200 Subject: [PATCH 025/105] Use brentq instead of newton for guaranteed convergence --- pyirf/sensitivity.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index 18e91f214..b4f36c679 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -1,13 +1,15 @@ import astropy.units as u import numpy as np -from scipy.optimize import newton -import warnings +from scipy.optimize import brentq from astropy.table import QTable - +import logging from .statistics import li_ma_significance +log = logging.getLogger(__name__) + + @u.quantity_input(t_obs=u.hour, t_ref=u.hour) def relative_sensitivity( n_on, @@ -59,7 +61,6 @@ def relative_sensitivity( initial_guess: float Initial guess for the root finder ''' - ratio = (t_ref / t_obs).to(u.one) n_on = n_on * ratio n_off = n_off * ratio @@ -78,20 +79,27 @@ def relative_sensitivity( def equation(relative_flux): n_on = n_signal * relative_flux + n_background - return significance_function(n_on, n_off, alpha) - target_significance + s = significance_function(n_on, n_off, alpha) + return s - target_significance try: - result = newton( + # 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, - x0=initial_guess, + 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' ) - except RuntimeError: - warnings.warn('Could not calculate relative significance, returning nan') return np.nan - if result.size == 1: - return result[0] - return result From 7dd600d735a4f8d97aada2abe70a7a8823a373c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 18:13:04 +0200 Subject: [PATCH 026/105] Fix significance for scalar quantitities --- pyirf/statistics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyirf/statistics.py b/pyirf/statistics.py index 8d2a39b70..1b28cf96f 100644 --- a/pyirf/statistics.py +++ b/pyirf/statistics.py @@ -1,5 +1,7 @@ import numpy as np +from .utils import is_scalar + def li_ma_significance(n_on, n_off, alpha=0.2): ''' @@ -26,7 +28,7 @@ def li_ma_significance(n_on, n_off, alpha=0.2): The calculated significance ''' - scalar = np.isscalar(n_on) + 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) From 170a06cd7394df307476f994747589271b71fc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 18:15:52 +0200 Subject: [PATCH 027/105] Scale off region size by alpha=0.2 --- examples/calculate_eventdisplay_irfs.py | 45 +- notebooks/comparison_with_EventDisplay.ipynb | 485 ++++++++++--------- pyirf/cut_optimization.py | 4 +- 3 files changed, 275 insertions(+), 259 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index e6d76d80e..0e5423b38 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -33,6 +33,11 @@ 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.2 + particles = { 'gamma': { @@ -50,6 +55,13 @@ } +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.DEBUG) @@ -75,6 +87,11 @@ def main(): ) gammas = particles['gamma']['events'] + # background table composed of both electrons and protons + background = table.vstack([ + particles['proton']['events'], + particles['electron']['events'] + ]) gh_cut = 0.0 log.info(f'Using fixed G/H cut of {gh_cut} to calculate theta cuts') @@ -100,20 +117,11 @@ def main(): ) # evaluate the theta cut - for p in particles.values(): - tab = p['events'] - tab['selected_theta'] = evaluate_binned_cut( - tab['theta'], - tab['reco_energy'], - theta_cuts, - operator.le, - ) - - # background table composed of both electrons and protons - background = table.vstack([ - particles['proton']['events'], - particles['electron']['events'] - ]) + 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( @@ -127,6 +135,7 @@ def main(): 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 @@ -141,14 +150,16 @@ def main(): min_value=0.05 * u.deg, ) - for tab in (gammas, background): - tab['selected_theta'] = evaluate_binned_cut(tab['theta'], tab['reco_energy'], theta_cuts_opt, operator.le) + 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=1, t_obs=T_OBS) + sensitivity = calculate_sensitivity(signal_hist, background_hist, alpha=ALPHA, t_obs=T_OBS) # scale relative sensitivity by Crab flux to get the flux sensitivity for s in (sensitivity_step_2, sensitivity): diff --git a/notebooks/comparison_with_EventDisplay.ipynb b/notebooks/comparison_with_EventDisplay.ipynb index 7ec9b696b..73aabe053 100644 --- a/notebooks/comparison_with_EventDisplay.ipynb +++ b/notebooks/comparison_with_EventDisplay.ipynb @@ -2,19 +2,9 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Remove input cells at runtime (nbsphinx)\n", "import IPython.core.display as d\n", @@ -82,26 +72,24 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import uproot\n", "from astropy.io import fits\n", - "import matplotlib.pyplot as plt" + "import matplotlib.pyplot as plt\n", + "import os" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "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" + "import astropy.units as u" ] }, { @@ -151,7 +139,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": { "tags": [ "parameters" @@ -161,15 +149,15 @@ "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", + "indir_EventDisplay = \"../../data/event_display_irfs/data/WPPhys201890925LongObs/\"\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}')" + "irf_eventdisplay = uproot.open(os.path.join(indir_EventDisplay, infile_EventDisplay))" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -202,73 +190,252 @@ "The following is the current IRF + sensititivy output FITS format provided by this software." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Differential sensitivity\n", + "[back to top](#Table-of-contents)" + ] + }, { "cell_type": "code", "execution_count": 5, - "metadata": { - "tags": [ - "parameters" - ] - }, + "metadata": {}, + "outputs": [], + "source": [ + "from astropy.table import QTable" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "# remove under/overflow bin\n", + "sensitivity = QTable.read('../sensitivity.fits.gz', hdu='SENSITIVITY')[1:-1]\n", + "sensitivity_step_2 = QTable.read('../sensitivity.fits.gz', hdu='SENSITIVITY_STEP_2')[1:-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "QTable length=21\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
reco_energy_lowreco_energy_centerreco_energy_highn_signaln_signal_weightedn_backgroundn_background_weightedrelative_sensitivityflux_sensitivity
TeVTeVTeV1 / (cm2 s TeV)
float64float64float64int64float64int64float64float64float64
0.0125892541179416750.0162709386338152350.019952623149688811294052.053463808505542992.5270484090060.240877728044806753.308887876558572e-07
0.01995262314968880.0257876998756862950.031622776601683793003991562.7694828028912870110.961290897830.0531325695835334342.1839677676726172e-08
0.031622776601683790.040870749982205510.0501187233627272281059199760.3410993994319988491.910109132850.016407202600624262.0179943490542266e-09
0.050118723362727220.064775773417577680.0794328234724281487327162217.56743659418721069.03328015140.0071469907447199552.6303202261989764e-10
0.079432823472428140.10266268232592240.12589254117941667148820206544.827269805487314021.2530220911430.0043507674332908274.7912769368599987e-11
0.125892541179416670.16270938633815230.1995262314968879159947165880.4769129249556700.8911879692170.00368412773856457771.2140039080035877e-11
0.19952623149688790.25787699875686280.31622776601683786357149130.405934001325227.093989494198470.00243726657375082252.4031918205848773e-12
0.31622776601683780.40870749982205490.5011872336272729131753854.34600845945514397.05608340457550.00286876151078318278.464081769258541e-13
0.5011872336272720.64775773417577650.794328234724280911014249009.42956253711613183.456478494859770.0022214189685543991.9611728881779492e-13
0.79432823472428091.02662682325922351.25892541179416629368631112.367732431274861.772658555855740.00220628436861759455.828367031206147e-14
1.25892541179416621.62709386338152151.99526231496887689826524432.00688285986314.2784969306958370.00164309636551475551.2988184218839263e-14
1.99526231496887682.57876998756862633.16227766016837610131519029.95516490307611.5654880720539950.00123462429106852582.9202512751664235e-15
3.1622776601683764.0870749982205475.0118723362727198831512502.44564402452732.82220312397112140.00209058216831906761.4796283591700434e-15
5.0118723362727196.4775773417577627.943282347242805687467324.81079526478440.87990858804550950.00296094848505348156.270703356119615e-16
7.94328234724280510.26626823259222812.58925411794165528824228.9695477217970.80988124676514420.0050791811631698033.218689685381609e-16
12.5892541179416516.2709386338152119.95262314968877371212228.42071432201250.26396481870324350.0087663630207754661.6622825886631686e-16
19.9526231496887725.78769987568626531.62277660168376264081189.0203963271342233.931439948966730.0467337662196003552.651649920643812e-16
31.6227766016837640.8707499822054550.11872336272714517073577.6697452813387228.2866548432793930.091472760821964861.5530205124073486e-16
50.11872336272714564.7757734175775579.4328234724279711443291.2577633045148113.2048529201420020.139415695295483377.082671084248521e-17
79.43282347242797102.66268232592222125.892541179416487394140.6052294790279214.673237658105790.314132977628681144.775281035142733e-17
125.89254117941648162.7093863381521199.52623149688768470066.60329778515734212.115744835056830.66923989854115913.0441582790076444e-17
" + ], + "text/plain": [ + "\n", + " reco_energy_low reco_energy_center ... flux_sensitivity \n", + " TeV TeV ... 1 / (cm2 s TeV) \n", + " float64 float64 ... float64 \n", + "-------------------- -------------------- ... ----------------------\n", + "0.012589254117941675 0.016270938633815235 ... 3.308887876558572e-07\n", + " 0.0199526231496888 0.025787699875686295 ... 2.1839677676726172e-08\n", + " 0.03162277660168379 0.04087074998220551 ... 2.0179943490542266e-09\n", + " 0.05011872336272722 0.06477577341757768 ... 2.6303202261989764e-10\n", + " 0.07943282347242814 0.1026626823259224 ... 4.7912769368599987e-11\n", + " 0.12589254117941667 0.1627093863381523 ... 1.2140039080035877e-11\n", + " 0.1995262314968879 0.2578769987568628 ... 2.4031918205848773e-12\n", + " 0.3162277660168378 0.4087074998220549 ... 8.464081769258541e-13\n", + " 0.501187233627272 0.6477577341757765 ... 1.9611728881779492e-13\n", + " 0.7943282347242809 1.0266268232592235 ... 5.828367031206147e-14\n", + " 1.2589254117941662 1.6270938633815215 ... 1.2988184218839263e-14\n", + " 1.9952623149688768 2.5787699875686263 ... 2.9202512751664235e-15\n", + " 3.162277660168376 4.087074998220547 ... 1.4796283591700434e-15\n", + " 5.011872336272719 6.477577341757762 ... 6.270703356119615e-16\n", + " 7.943282347242805 10.266268232592228 ... 3.218689685381609e-16\n", + " 12.58925411794165 16.27093863381521 ... 1.6622825886631686e-16\n", + " 19.95262314968877 25.787699875686265 ... 2.651649920643812e-16\n", + " 31.62277660168376 40.87074998220545 ... 1.5530205124073486e-16\n", + " 50.118723362727145 64.77577341757755 ... 7.082671084248521e-17\n", + " 79.43282347242797 102.66268232592222 ... 4.775281035142733e-17\n", + " 125.89254117941648 162.7093863381521 ... 3.0441582790076444e-17" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sensitivity" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, "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" - ] + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAugAAAHkCAYAAABscNp2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAAA+OUlEQVR4nO3de5xcZZXv/+9KIxDpkCBg1AZNgHgF0qGDyCXSzRAmCCHKxcAAxwQIPxjACXNgDg7OMTMSYTwKGW6iIgRtTEAuDhcZuXUpCGgS0oQAckACnqAOCdqQjkGkWb8/dqXpdKq6aj+1q/qp1Of9eu0XXftZez+rk/VqVu/s/WxzdwEAAACIw4jhTgAAAADAO2jQAQAAgIjQoAMAAAARoUEHAAAAIkKDDgAAAESEBh0AAACIyFbDnUBsdtppJx83bpzWr1+v7bbbrmBM6FhsapVrVvOEnifNceXGloqrZJwaqt48jVBD9VQ/EjVUSSw1lKCGwmOpoUQt8i00x7Jly9a6+84FD3B3tgFbW1ubu7t3dXV5MaFjsalVrlnNE3qeNMeVG1sqrpJxaqh68zRCDdVT/bhTQ5XEUkMJaig8lhpK1CLfQnNIWupF+lFucQEAAAAiYs6bRCVJZjZd0vSWlpY5nZ2d6u3tVXNzc8HY0LHY1CrXrOYJPU+a48qNLRVXyTg1VL15GqGG6ql+JGqoklhqKEENhcdSQ4la5Ftojo6OjmXuPrngAcUurTfqxi0u8c7TCP8smCaHGFBD4bH803KCGgqPpYYS1FB4LDWUiPEWFx4SBQAA2IKYmVatWqU33nhjyLjRo0frmWeeyXy81HGxqXa+2267rcws1TE06AAAAFuQ7bbbTqNGjdK4ceOGbAzXrVunUaNGZT5e6rjYVDNfd9err76aepUYHhIFAADYgjQ1NWnHHXdMfdUW2TMz7bjjjmpqakp1HA06AADAFobmPB4hfxc06AAAAMhUU1OTWltb+7dLLrkk0/Pncjk98sgj/Z/nzZunlpYWtba2asKECTr66KP19NNP94+fdtppm3wu18KFC3X22WdnknMa3IMOAACATI0cOVLd3d1VO38ul1Nzc7MOOOCA/n3nnnuuzjvvPEnSTTfdpEMOOURPPvmkdt55Z1177bVVy6UauIIOAACAqrvnnnv0+c9/vv9zLpfT9OnTJUn33nuv9t9/f+2zzz467rjj1NvbK0kaN26cvvKVr2ifffbRXnvtpV//+td68cUXdc011+iyyy5Ta2urHnrooc3mmjlzpg477DD98Ic/lCS1t7dr6dKl6uvr06xZs7Tnnntqr7320mWXXdY/PnfuXB1wwAHac8899atf/Wqzc955553ab7/9NGnSJB166KH67//+b7399tuaMGGC1qxZI0l6++23tccee2jt2rUV/VnRoAMAACBTGzZs2OQWl5tuuklTp07VY489pvXr10tKrnLPnDlTa9eu1UUXXaT7779fjz/+uCZPnqxLL720/1w77bSTHn/8cZ155pn6xje+oXHjxumMM87Queeeq+7ubk2ZMqVgDvvss49+/etfb7Kvu7tbL7/8slauXKknn3xSs2fP7h9bv369HnnkEV199dU65ZRTNjvfQQcdpMcee0zLly/X8ccfr69//esaMWKETjrpJN14442SpPvvv18TJ07UTjvtVNGfH7e4AAAANJj2dqmvb6SGWlykr2+kClycLkuxW1ymTZumO++8U8cee6zuvvtuff3rX9fPfvYzPf300zrwwAMlSW+++ab233///mOOPvpoSVJbW5tuu+22snNI3gW0qd12200vvPCCzjnnHB1xxBE67LDD+n9hOOGEEyRJn/70p/X666+rp6dnk2NXr16tmTNn6ve//73efPNNjR8/XpJ0yimnaMaMGZo7d66uu+66TZr+UFxBBwAAQE3MnDlTN998sx588EHtu+++GjVqlNxdU6dOVXd3t7q7u/X000/re9/7Xv8x22yzjaTkwdO33nqr7LmWL1+uj33sY5vs22GHHfTEE0+ovb1dV111lU477bT+scGrrQz+fM455+jss8/Wk08+qW9/+9v9L4LaddddNXbsWD344IP65S9/qcMPP7zsHIvhCjoAAECDyeWkdes2lHgR0QZJ2b7Ap729Xaeeeqq++93vaubMmZKkT33qUzrrrLP0/PPPa4899tCf//xnrV69Wh/+8IeLnmfUqFF6/fXXi47feuutuvfee/XNb35zk/1r167V1ltvrWOOOUa77767Zs2a1T920003qaOjQw8//LBGjx6t0aNHb3Lsa6+9ppaWFknSDTfcsMnYaaedppNOOkknn3xy6jXPC+EKOgAAADI1+B70Cy64QFJyFfzII4/UPffcoyOPPFKStPPOO2vhwoU64YQTtPfee+tTn/rUZveODzZ9+nTdfvvtmzwkuvGh0QkTJqizs1MPPvigdt55502Oe/nll9Xe3q7W1lbNmjVLF198cf/YDjvsoAMOOEBnnHHGJlfwN5o3b56OO+44TZkyZbN7zI866ij19vZmcnuLxBV0AAAAZKyvr6/o2JVXXqkrr7xyk32HHHKIlixZslnsiy++2P/15MmTlcvlJEkf/vCHtWLFiv6xKVOmaN68eUXn3HicJD3++OObjK1bt06SdMwxx2zSsEvSrFmz+q+yz5gxQzNmzCh4/ieeeEITJ07URz/60aI5pEGDDgAAAAS65JJL9K1vfat/JZcs0KDH4vojaj/n+PNrPycAAEBkBl5hT+uCCy7ov4UnK9yDDgAAAESEK+ixmH137ees4LdFAAAAVAdX0AEAAICI0KADAAAAEaFBBwAAwLAaN26c1q5dKylZK721tVV77rmnpk+frp6eHknJkosjR47cZH31N998c5Pz5HI5jR49un/80EMPlZSsYf6Nb3xDUrJ0YktLi/7yl79Ikl599VWNGzduk/Ncdtll2nbbbfXaa69tcu6Na7dXGw06AAAAojFy5Eh1d3dr5cqVes973qOrrrqqf2z33XdXd3d3/7b11ltvdvyUKVP6x++///6CczQ1Nem6664rmsOiRYu077776vbbb6/8GwpAgw4AAIDMvPTSS/roRz+qL3zhC9p777117LHH6s9//rMeeOABfe5zn+uPu++++3T00UcPea79999fL7/8cuY5zp07V5dddpneeuutzcZ+85vfqLe3VxdddJEWLVqU+dzloEEHAABApp599lmdfvrpWrFihbbffntdffXVOuSQQ/TMM89ozZo1kqTrr79es2fPLnqOvr4+PfDAAzrqqKP69/3mN7/pv33lrLPOKnjcQw891B8zf/78gjEf/OAHddBBB+kHP/jBZmOLFi3SCSecoClTpujZZ5/VK6+8kuZbzwTLLAIAADSa64/QyL63pKbireDIvrek034adPpdd91VBx54oCTppJNO0uWXX67zzjtPJ598sjo7OzV79mw9+uij+v73v7/ZsRs2bFBra6tefPFFtbW1aerUqf1jG29xGcqUKVN01113lczxn//5n3XUUUfp4IMP3mT/4sWLdfvtt2vEiBE6+uij9aMf/ajoLwPVwhV0AAAAZMrMCn6ePXu2Ojs7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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" } ], "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", + "plt.figure(figsize=(12,8))\n", + "\n", + "# Data\n", + "h = irf_eventdisplay[\"DiffSens\"]\n", + "\n", + "#x = np.asarray([(x_bin[1]+x_bin[0])/2. for x_bin in h.allbins[2:-1]])\n", + "bins = 10**h.edges\n", + "x = 0.5 * (bins[:-1] + bins[1:])\n", + "width = np.diff(bins)\n", + "y = h.values\n", + "\n", + "# Plot function\n", + "plt.errorbar(\n", + " x,\n", + " y, \n", + " xerr=width/2,\n", + " yerr=None,\n", + " label=\"EventDisplay\",\n", + " ecolor = \"blue\",\n", + " ls=''\n", + ")\n", "\n", - "hdul_pyirf = fits.open(f'{indir_pyirf}/{infile_pyirf}') # will be closed at the end of the notebook\n", + "unit = u.Unit('erg cm-2 s-1')\n", "\n", - "# Contents of the FITS file\n", - "hdul_pyirf.info()" + "sensitivities = {\n", + " 'pyIRF FINAL': sensitivity,\n", + " # 'pyIRF after first theta cuts': sensitivity_step_2,\n", + "}\n", + "\n", + "for label, sens in sensitivities.items():\n", + " e = sens['reco_energy_center']\n", + " s = (e**2 * sens['flux_sensitivity'])\n", + "\n", + " plt.errorbar(\n", + " e.to_value(u.TeV),\n", + " s.to_value(unit),\n", + " xerr=(sens['reco_energy_high'] - sens['reco_energy_low']).to_value(u.TeV) / 2,\n", + " ls='',\n", + " label=label\n", + " )\n", + "\n", + "\n", + "\n", + "# Style settings\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", + "\n", + "plt.legend(loc=\"best\")\n", + "plt.savefig('/home/maxnoe/pyirf_sensitivity.png', dpi=300)" ] }, { "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, + "metadata": {}, "source": [ - "### Setup of output data" + "## Compare Theta Cuts" ] }, { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEOCAYAAACXX1DeAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAolklEQVR4nO3deXjU9bn38ffNJotAJXAsAoIsGlYjpGIUT2m1GgpoK0j0lFqCCrZHz9FWnkdbBR5K3eDAqT1YQWSwLdCIOy5IFTluVGVRlBhbF1A0LkAblKgB8n3+mCWTYWYyE2afz+u65iK/de78rsncfHdzziEiIhJJi3QHICIimU2JQkREolKiEBGRqJQoREQkKiUKERGJSolCRESiapXuAJKha9eurk+fPukOQ0Qka2zevHm3c65buGM5mSj69OnDpk2b0h2GiEjWMLOdkY6p6klERKJSohARkaiUKEREJKqcbKMQyVUHDhxg165dfPXVV+kORbJU27Zt6dmzJ61bt475GiUKkSyya9cuOnbsSJ8+fTCzdIcjWcY5x549e9i1axcnnHBCzNep6kkki3z11VcUFBQoSUizmBkFBQVxl0iVKESyjJKEHInmfH5U9RSifG05VXurIh4v7FKIp9STwohERNJLJYo4RUsiItLY97//ff75z3+GPbZ69WoGDhyImVFUVERRURFHH300J510EkVFRVxyySVs2LCBcePGNbpuypQp3HfffQCMHj06cH5RURETJ07kN7/5TWC7ZcuWgZ9vv/12AP7zP/+THj16UF9f32T8TzzxBMXFxQwcOJDCwkKuvfbaw2LwO/roo3n99dcD79elSxdOOOEEioqKOPvss6mvr+c//uM/GDJkCEOHDuVb3/oW7733XryPND2cczn3GjFihEuWKU9McVOemJK0+4tEU1lZme4Qjlh9fb07dOiQO/fcc9369esbHfv2t7/tXnnllcD2M88848aOHdvonJ/85Cdu9erVYc8P1aFDh0bbhw4dcr169XIjR450zzzzTNQ4X3/9dde3b1/35ptvOuecO3DggFu0aNFhMUR6r9BzVq5c6SZMmOAOHTrknHPugw8+cHv37o0aQ7KE+xwBm1yE71RVPcXJU+qhZGUJ5WvLVQUlGals8caYzquYXhL3vXfs2EFpaSkjR45k69atnHjiiZSXl7N06VIefPBBAP7y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19O/f392796d2IeXBXKqRJEtAo3aNEzvoQkDJVFi/U9Hcz9rX375JUVFRYHt66+/ngkTJjB9+nT2799Phw4dqKiooKysjN27dzN37lyeeuopOnTowK233sqCBQuYOXMm4J1yY8uWLdxxxx3Mnz+fpUuXcsUVV3D00UcHShBPP/30YTEMHz6cqqrGVbavvvoqH374YWCcR/DKevv37+fFF1/k2WefZerUqYeNBRk1ahR//etfMTOWLl3Kbbfdxn/9138xefJkVqxYwdVXX81TTz3FySefHNPkf7lGiSINAqO2PWPB97da2F1tFZIdIlU9lZaWsmbNGiZOnMhjjz3Gbbfdxv/+7/9SWVkZmJa8rq6OkpKGWQsuuOACwDuy+4EHHog5hnDjv/r27cu7777LVVddxdixYznnnHMCxy6++GIA/vVf/5V9+/Ydtjzrrl27KCsro7q6mrq6Ok444QQApk6dyvnnn8/VV1/NsmXLwq6zkQ+UKNIoeGoPtVVIoqSrVFpWVsaiRYvo0qUL3/rWt+jYsSPOOb73ve+xatWqsNccddRRgLeB/ODBgzG/19atWw9bg+KYY47htdde48knn2TRokXce++9LFu2DAg/NXqwq666ip///Oecd955bNiwgdmzZwPetTeOPfZY1q9fz0svvcSKFStijjGX5FQbRbYJ7gEFaquQ7DZ69Gi2bNnCXXfdFZiN9bTTTuOFF14ILNNZW1sbmLY7ko4dO/L5559HPH7//fezbt26QCnBb/fu3dTX1zNhwgR+/etfs2XLlsAx/5Kizz//PJ07dya0HbOmpoYePbyrHdxzzz2Njl122WVMnjyZSZMmNVpxLp/kVIkiG3o9BQvtAZXslclEEiG0jaK0tJRbbrmFli1bMm7cOJYvXx74su3WrRvLly/n4osvDsxnNHfuXE488cSI9x8/fjwTJ07k4Ycf5ne/+x0ACxcu5E9/+hP79+9nyJAhrF+/ntB1Zz788EPKy8upr68H4OabGyakPuaYYzj99NPZt29foJQRbPbs2Vx44YX06NGD0047jffeey9w7LzzzqO8vDxvq51Acz2lnX+yQPBO9eEvUahRW8LRXE/xGz16NPPnz49pudRwNm3axDXXXBO2B1a20lxPWSa4B5S/dKHR2iKZ4ZZbbmHChAmNSif5SIkizULXrdBobZHE2rBhQ7NLE9dddx07d+5k1KhRTZ+cw3KqjSKRghcmCv4yTwZ/qWLmnhngGettruiu9bVFJDOoRJEBwk0YqB5QEkkutitK6jTn86MSRQTJLkWEM6dgXmCdbTxjofu/pDwGyWxt27Zlz549FBQUHDYWQKQpzjn27NlD27Zt47pOiSKDeLvLNu7frWk9JFjPnj3ZtWtXYP4hkXi1bduWnj17xnVNTiWKbBtHEU5Z3Q2Bn9uzRHNASSOtW7cOTC8hkio51UaRLbPHxko9oEQkE+RUiSIXhE4YqB5QIpJuOVWiyFXqASUi6aQSRYYKbquoQOtViEj6KFFksIausp0pVPWTiKSJqp4yVLipPURE0kEligwWOrUH9kmaIxKRfJRTJQozG29mS2pqapo+OQsET+2x//2t/M/Ojyhf3rzJzUREmiunEkWujaPwm1Mwjx2t+7KjdV+qqFMPKBFJqZxKFLmqYnpJYMnUejdA61WISEopUWSZZdWfUFh3AD5+Pd2hiEieUGN2lmgYsd0Zj+tMuRq2RSRFlCiyjH8g3o4289MciYjkC1U9ZaGZe2awrPoT9YASkZRQosgyFdNLGNy9M4O7d1YPKBFJCVU9ZSF/9VMv+6katUUk6ZQogpQt3hj4OR1LocZr1u6jmX/cV+kOQ0RynKqespB/xPacgnlUut6qfhKRpMqpEsWRLoWaDaWIUH3qrgWWpDsMEclh5pxLdwwJV1xc7DZt2pTuMFLDMzYwpsIzJU9+ZxFJODPb7JwL25VSVU8iIhKVEkWWK6u7gdqvf6e2ChFJmoiJwsy+YWbH+X4emLqQJF4z98yg94F3qPp4c7pDEZEcFK1EUQHcambnANekKB6Jk38A3r2te1BIm3SHIyI5KFqvp3eccz8zs7nAkFQFJPHzD8ATEUmGaCWKF3z/3gi8mIJYJAHUTiEiiabusbnEM5YS+4BC2qirrIjE5Yi6x5rZ52a2L+T1gZk9aGZ9Ex+uNNf26hoGfF3Hoa+/AM/YdIcjIjkilu6xC4AZQA+gJ3AtcBfwZ2BZ8kKTeM0pmMf/3dOVr6ytFjYSkYSJJVGUOucWO+c+d87tc84tAb7vnKsAjklyfBIH//xPO1v3o9L1Tnc4IpIjYkkU9WY2ycxa+F6Tgo7lXgNHDvDO/yQikhixTAr4I+C3wB14E8Nfgclm1g64MomxSTP4JzYsX76zoZ2i/LE0RiQi2a7JEoVz7l3n3HjnXFfnXDffz2875750zj2fiiBjZWbjzWxJTU1NukNJO487lvID78LH29SwLSJHJJZeTyea2dNm9oZve5iZZeQIL+fcGufctM6dO6c7lLQrq7uBytatmFRQkO5QRCTLxdJGcRdwPXAAwDm3DbgomUFJYtR+3Zc3W7XWyG0ROSKxJIr2zrmXQ/YdTEYwkjgV00sYZNfR3o7H2U+91U+qghKRZoglUew2s374ejiZ2USgOqlRSUJUTC/hpfL7+aBNvcZViEizxZIo/h1YDBSa2YfA1cBPkxmUJFa9G0Cl+5pJBz5UqUJE4tZk91jn3LvA2WbWAWjhnPs8+WFJIvWpu9Zb/SQi0gwRE4WZ/TzCfgCccwuSFJMkWMX0EsoW/54dbeazvfoTBnvGamyFiMQsWtVTR9+rGG9VUw/f6wpgUPJDk2SY1eVztldrnImIxC5iicI59/8AzGwdMNxf5WRms4HVKYlOEsY7Yvt+Tl5+KlO7H8tL6Q5IRLJGLFN4HA/UBW3XAX2SEo0k3fBvDoaPX9f0HiISs1h6Pf0ReNnMZpvZLOAl4J7khiXJ4in1pDsEEckyscz19BugHPgH8E+g3Dl3c5LjkiSqdL2ZdOBDb1uFusuKSBNiqXrCObcF2JLkWCRF+tRdyyF3BbV1h9heXcPgdAckIhktlqonyTEV00v44Kj+XNn7OO8OTe8hIlEoUeSpQd070eKoj5h/3Ffeqcg1HbmIRBAxUZjZk2Z2jZkVpjIgSQ1PqYfCLoVsoQXbXW+2a+lUEYkgWhvFT4BSYLaZnYi3t9Na4Gnn3BepCE6Sy1PqYaRnAhfVTQNgUF0nKtIck4hkHnOu6WWvzawFMBIYA5wFfAmsc87dltzwmqe4uNht2rQp3WFkjZGeCfSpu5aZe2YwuLtv0SeNrxDJK2a22TlXHO5YTG0Uzrl659xG59xM59wZeBcu+jCRQSaClkJtnhZHfUT73ksA2P/+Vva/v1XtFSIS0KzGbOfcbufcikQHc6S0FGrzFHYppGpvFVO7H8uO1n3Z0bpvukMSkQwSU9VTtlHVU/zK15YDULvT216haiiR/HJEVU9mdkIs+yS7aWoPEYkklpHZ9wPDQ/bdB4xIfDiSbt5ZZqFs8bzAVJDqCSWS36ItXFQIDAY6m9kFQYc6AW2THZiknqfUQ/nacpUuRKSRaCWKk4BxwDeA8UH7PwcuT2JMkkZVe6soX1tOxXRPQ88nD2qnEMlj0RYuehh42MxKnHMbUxiTpJG/B1Qj/uk9lCxE8lIsbRTTzOywEoRzbmoS4pE081c/la8tp7buBgBmuhmaYVYkj8UyjuJR4DHf62m8bRSawiOHeUo9VO2tYkeb+QBcVHcjZb6kISL5p8kShXPu/uBtM1sFPJW0iCQj+Kug2ndfwiCmpTscEUmj5ozMHoB3HW3JYf7ZZav2VlExvYSKNnO1boVInoplwN3nZrbP/wLWAP83+aFJuvmThYjkt1jWzO7onOsU9DoxtDpKcpe/cbus7ga2V9donW2RPBRLieKHZtY5aPsbZvaDpEYlGSW4YVtE8k8s3WNnOece9G845/5pZrOAh5IWlWQUf1vFnIIF3h11mtZDJJ/E0pgd7pxYEozkCH9bhb9UMXPPDDVsi+SRWBLFJjNbYGb9zKyvmS0ENic7MMksnlKPFjgSyVOxJIqr8M4jWgHci3cZ1H9PZlCSmbTAkUh+imXA3X7gOgAz6+6cq056VJKR/D2g1F4hkl/iHXCnWeHynNorRPJPvInCkhKFZBVPqYdB3TsFFjkKjK8QkZwUb6K4KylRSFYqX1vOnIJ5DTtUqhDJSRETha+H0wjfz2cCOOfuSFVgkvmq9lbRvvcSBnfvzODunZu+QESyUrQSxRKgzMwuAX6congkSwRPGlhWd0Ngig+1V4jknmiJ4k3n3P8BjgFOS1E8kkU0aaBIfojWPfZxAOfcb83sUIrikSzj7zLrKfVQtnied8QN6jIrkkuirZn9hJm1B8YArc3sauB9YK1zrjZF8UkWqNpbRfnacmCat7ssgKez1tgWyRHRGrMnAM8CpwI/BzoCpcBWM/thasKTbBC8wJFfYHoPtVeIZD1zzoU/YLYdONU5t9/MtjrnTvHt7wq84Jw7KYVxxqW4uNht2rQp3WHkFW+JAmp3epdNnblnRkNPKJUsRDKemW12zhWHOxatMduAet/Pwdkkpe0VZvYDM7vLzB42s3NS+d4SO0+pB8C7bOr0EuYUzNNCRyI5IlqimA08a2ZzgH8xs1+a2R3AC8D1sdzczJaZ2adm9kbI/lIze8vM3jaz66Ldwzn3kHPucmAKUBbL+0r6+EsWIpI7ojVm32tmj+Ntl1iAt4SxHpjhmygwFsuB/wH+4N9hZi2BRcD3gF3AK2b2CNASuDnk+qnOuU99P9/gu04yWNXeKkpWlrBx+kZvLyjQxIEiWS5iG0XC3sCsD/Coc26Ib7sEmO2cO9e3fT2Acy40SfivN+AW4C/OuaeivM80YBrA8ccfP2Lnzp2J/DUkRv7ZZQu7FFK7s6EX1ODu6gUlksma20aRLD2AD4K2d/n2RXIVcDYw0cyuiHSSc26Jc67YOVfcrVu3xEQqcfOUetj4bxsBb3uFpvcQyX7pWNI03Ay0EYs1zrnbgduTF44kS/nacmrrbgB8vaD8jdoqWYhklXSUKHYBvYK2ewIfpSEOSbKqvVWBdStEJHulI1G8AgwwsxPMrA1wEfBIGuKQJPLPA9XiqI/UXVYkyyU1UZjZKmAjcJKZ7TKzS51zB4ErgSeBN4F7nXPbE/R+481sSU2NFtHJBP6xFeoyK5Ldkt7rKR00MjtzhPaCAo3aFslEmdbrSfJI8LoVfrV1h7R8qkgWUaKQpPMni/a9l1AxvYT53RcEHVR7hUimU6KQlPC3VwCNZplVqUIk86VjHIXkMf8iR3MK5gX2aXoPkcyWU4nCzMYD4/v375/uUCQC/yJHFdN9JQzPWPAXNtSwLZKRcqrqyTm3xjk3rXNnTRmRicI1bPsbtVUFJZK5cipRSObzJwv/2IrgKig1bItkJiUKSblIDdtaPlUkMylRSNoElyrmFMxjR+u+aY5IRMLJqcZsyS7+tgp/qaJs8Tyo8x5TTyiRzJFTJQrN9ZQ9Qtsq/GbumeFd7EjVTyIZI6cShXo9ZZfgtgrQQDyRTJVTiUKyU3CpolEvqJt7qXFbJAMoUUja+QfhQUgvqLpD6QpJRIKoMVvSylPqCUxF7ucvVVRW72NQXSdAjdsi6aT1KCQj+EsUwe0WZYs3ehu2wbt+hab4EEkarUchGc9T6qFqbxUlK0vCVkOpcVskfVT1JBnDPw9UuGooUPWTSLrkVNVT0Oyxl//9739PdzjSTOGqofCMhY+3eX/+5jBVQ4kkWN5UPWkcRW7wV0MFd5vdXl3D/rpD6gklkgaqepKMFDod+ZyCeVRW7wNgUF0nVUOJpFBOlSgkd4RO8VExvYRB3TsxqHunhik+NBBPJCWUKCRjhVZBVUwvCfSE0mJHIqmjRCEZzV8FVbKyoausFjsSSS0lCslo/iooIPw0H1rsSCTplCgk43lKPWz8t42N9mmxI5HUyalEofUocp+/CsrfXjGnYF5De4VKFSJJkVOJQuMo8kPoYkcAfQ686x2Qp2QhknA5lSgkt/mroEJ7QvmroLa73mmOUCQ3KVFI1vH3hAouWVxUdyMX1d3YUAWlkoVIwihRSNbx94Tyj9wOHoxXW3dIYyxEEiynJgX003oU+SHSGhZ+wd1oRSS6aJMCKlFIVitZWUJhl8LDZ5r10yyzIjHJm9ljJf+Ea6/wVz1pMJ5IYihRSFYLba+AhsF4la43+9/f2pAwRKRZlCgk64WbabZiegnzuy9gR+u+Gr0tcoTURiE5wz9qO3i6D3/j9sw9Mxjc3TcQU+0WIofJmzYKTeGR30InDxSRxFCJQnJOuJ5Q6jYrEl3elChEIHxPKPBWPwVWxxORmClRSM4J1xMquBShUdsi8VGikJwU2hMKGrrNek/Q+AqRWClRSM6KtOZ2oPeTiMREiUJyWmgVFEBZ3Q1a7EgkDkoUktP8VVAlK0sOa9zWYkcisVGikJzXaMJAtNiRSLyUKCQv+Edra7EjkfgpUUhe8TduBy92BHiroFQNJRKWEoXkDX97hZ+/F9Scgnm88nVPXvm6p8ZYiIShRCF5xd9eEdqwPZXZTGW27yRVQ4kEU6KQvFS1tyow22zF9BJen30ur88+F0BrWIiEyKlEodljJRbBVVChJQv/gkeVrreqoUR8NHus5LVoM83O3DODwbbTu/ObwxK/joXW9pYMotljRSIIN9Osv5EbYN/XB9n39cHkli4+3gY39/K+VN0lGahVugMQSSdPqYfyteWBZBFcsphTMI/K6n0ADCroRMWRlAA8Y70Jwe+bwyiruwGAmS6o5CKSgVT1JAKBZBFaDRXgGcv+97fS1n3JV9aODm1aNj7+zWG+Gz122HVA4yTh4x8VflHdjYF9f27zay3ZKmkRrepJJQoRGpcswtleXUOt681Jbgdv0ZtBdQ0lgEPO0fL9rQB0uLlX+Otd70YJYRmzad/G+/Og7p0CJRegIanc3KshAfkpeUgaqEQhEsTfVhFaqvA3cFdW7zv8ix3vFz/AoJAqpB2t+wKNSw1+/lHhoUuzbr9pFOCdtLDD8ac0HPh4W+SSi8gRilaiUKIQCeEfXxFNbd2hwM/t27QMbNfzFdQfddj5LVoY7UOrqyLct76+4W+yHV81Ot6S+ibvkUq1LYz29dn5HdKc2DP59623FrxU/nqzr1evJ5E4BE/zEUn7Ni0Dr+BtMFq0OPwVS5LwC74u2Je05RAtOKQ/W0kxtVGIhAjbmC2Sx/RfExERiUqJQkREolKiEBGRqJQoREQkKiUKERGJSolCRESiUqIQEZGolChERCSqnJzCw8w+A/4JBC8i0DlouyuwOwlvHfweibwm2jmRjoXbH7oveDv0WDKeUTY/n9DtTPkMxXp+vM8oV55PrNck4jPU1DPL9L+x3s65bmHPcM7l5AtYEmkb2JSK90zUNdHOiXQs3P4mnknosYQ/o2x+Ppn6GYr1/HifUa48n1R+hpp6Ztn8N5bLVU9rmthOxXsm6ppo50Q6Fm5/tGei59P0ZyYTn1Gs58f7jHLl+cR6TSI+Q009s6x9PjlZ9dQUM9vkIsySKF56RtHp+USn59O0bHpGuVyiiGZJugPIAnpG0en5RKfn07SseUZ5WaIQEZHY5WuJQkREYqREISIiUSlRiIhIVEoUIczsB2Z2l5k9bGbnpDueTGNmfc3sbjO7L92xZBIz62Bm9/g+Oz9KdzyZRp+b6DL9eyenEoWZLTOzT83sjZD9pWb2lpm9bWbXRbuHc+4h59zlwBSgLInhplyCns+7zrlLkxtpZojzeV0A3Of77JyX8mDTIJ7nk0+fG784n09Gf+/kVKIAlgOlwTvMrCWwCBgDDAIuNrNBZjbUzB4Nef1L0KU3+K7LJctJ3PPJB8uJ8XkBPYEPfKcdSmGM6bSc2J9PPlpO/M8nI793WqU7gERyzj1rZn1Cdp8KvO2cexfAzP4MnO+cuxkYF3oPMzPgFuAJ59yWJIecUol4PvkknucF7MKbLF4l9/4DFlacz6cyxeGlXTzPx8zeJIO/d/LhA92Dhv/pgfcPukeU868CzgYmmtkVyQwsQ8T1fMyswMzuBE4xs+uTHVwGivS8HgAmmNnvSc1UDZkq7PPR5yYg0ucno793cqpEEYGF2RdxlKFz7nbg9uSFk3HifT57gIz7IKdQ2OflnNsPlKc6mAwU6fnk++fGL9LzyejvnXwoUewCegVt9wQ+SlMsmUjPJz56XtHp+USXlc8nHxLFK8AAMzvBzNoAFwGPpDmmTKLnEx89r+j0fKLLyueTU4nCzFYBG4GTzGyXmV3qnDsIXAk8CbwJ3Ouc257OONNFzyc+el7R6flEl0vPR5MCiohIVDlVohARkcRTohARkaiUKEREJColChERiUqJQkREolKiEBGRqJQoREQkKiUKERGJSolCJAIzm25m1Wb2atBraALuu9jMzgi658dm9mHQdpsI120ws3ND9l1tZnccaUwi0WhktkgEZrYI2OKcuzvB930VGOGcO+Tbng184Zyb38R104HTnHPlQfv+Csxwzj2XyBhFgqlEIRLZULwLESWMmQ0E/uZPElHOm2xmL/tKGIt9K6PdB4wzs6N85/QBjgOeT2SMIqGUKEQiGwx4gqqEpiXgnmOAtdFO8CWTMuAM51wR3qVVf+Rb0+FlGpbXvAiocKoWkCTLh4WLROJmZr2AT51zwyIcb+Gcq2/Grc+l6QWOzgJGAK94V+alHfCp79gqvAniYd+/U5sRg0hclChEwhsGVIXuNLMpeJes3GRmDwK/wLtq2TvAg8BcvF/qDwIfA7OBr/Auj/oX4BvOuaYWqjHgHudcuCVDHwIWmNlwoF0mrq8suUeJQiS8oYRJFD5POOdWmNmtwJe+11C87QVznHN/BzCz+cCNzrn3zGw1cBB4Job3fhp42MwWOuc+NbMuQEfn3E7n3BdmtgFYhrd0IZJ0ShQi4Q0Fvm1mY3zbDjjT93ON798WwB+dc9sAzGweEFwdZTSsP+7wtk/c19QbO+cqzewGYJ2ZtQAOAP8O7PSdsgp4AG/Vk0jSqXusSBx8VU+7nXOPmllv4CagGvgc+APeqqZqvMtb7gZuBGrxNmDfCIx0zh1IfeQizadEISIiUal7rIiIRKVEISIiUSlRiIhIVEoUIiISlRKFiIhEpUQhIiJRKVGIiEhUShQiIhKVEoWIiET1/wGEQeR4YLWzmgAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ - "It is possible to extract data from pyirf FITS file with different approaches, e.g.\n", + "from astropy.table import Table\n", + "\n", + "for key in ('THETA_CUTS', 'THETA_CUTS_OPT'):\n", + " theta_cut = Table.read('../sensitivity.fits.gz', hdu=key)\n", + "\n", + " plt.errorbar(\n", + " theta_cut['low'],\n", + " theta_cut['cut'].quantity.to_value(u.deg)**2,\n", + " xerr=theta_cut['high'] - theta_cut['low'],\n", + " ls='',\n", + " label='pyirf' + key,\n", + " )\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", + "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", - "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", + "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", - "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." + "plt.savefig('/home/maxnoe/theta2_cut.png')" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from astropy.coordinates import Angle\n", + "from gammapy.maps import MapAxis\n", + "from gammapy.irf import EffectiveAreaTable2D, EnergyDispersion2D, BgRateTable" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -292,7 +459,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -306,18 +473,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "# Energy dispersion\n", "\n", @@ -341,7 +499,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -352,7 +510,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -402,22 +560,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "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" - } - ], + "outputs": [], "source": [ "zoom = 2\n", "plt.figure(figsize=(zoom*4,zoom*3))\n", @@ -456,22 +601,9 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "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" - } - ], + "outputs": [], "source": [ "zoom = 2\n", "plt.figure(figsize=(zoom*4,zoom*3))\n", @@ -513,22 +645,9 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "zoom = 2\n", "plt.figure(figsize=(zoom*4,zoom*3))\n", @@ -567,22 +686,9 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAfoAAAF3CAYAAABNO4lPAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nO3de5wU1Zn/8c8DIYJCxABLImLALIKuKAi/qLhmBxMVo0YCKLqIEUKMJCSa30oW1uxPsyYLr5jobuKNCIhRQ0y8EEUU18BE8MplwEGUaAxxGYIKZsTBQXF4fn90zdDT05fqma7pnurv+/XqF12nTlU900XPM6fq1Dnm7oiIiEg8dSp2ACIiIhIdJXoREZEYU6IXERGJMSV6ERGRGFOiFxERiTElehERkRj7WLEDiELv3r29T58+HHLIIc3K9+zZE6osaoU4Zr77CFM/V51M6/MpTy3T5x++Xr7r2lIWtVI9B/oORLsPnYPscbVlH+vWrdvp7n3SVnT32L1GjBjhK1eu9FRhy6JWiGPmu48w9XPVybQ+n/LUMn3+4evlu07fgfzr6zsQ7T50Dgp7zOR9AGs9Q07UpXsREZEYU6IXERGJMSV6ERGRGItlZzwRESmMffv2sW3bNvbu3dus/NBDD+Xll1/Oa19htslVJ9P6fMpTy1rzs7RVa4/ZtWtXjjjiCLp06RJ6GyV6ERHJaNu2bfTo0YMBAwZgZk3l7733Hj169MhrX2G2yVUn0/p8ylPLWvOztFVrjunu7Nq1i23btjFw4MDQ2+nSvYiIZLR371569erVLMlLcZgZvXr1anF1JRclehERyUpJvnS05lwo0YuISKx86Utfora2Nu263/72t4wcOZLRo0e3c1TFo3v0IiISK8uWLWtR1jh4zIIFC7jxxhs555xzihBZcahFLyIiJW3r1q0MGTKEr371q5xyyilMmDCBRx99lK985StNdVasWMG4ceMAGDBgADt37mTr1q2MHDmSb37zm5x44olcf/31rF69mquuuoqZM2cW68dpd2rRl6AlVTXcsHwL22vrObxnN2aeNZixw/sVOywRKXePzYId1QB0a/gIOueXQtJu86mhcPbcnNtu2bKFBQsWcPzxx3PllVeyefNmXn75Zd5++2369OnDPffcw5QpU1ps9+qrr3LXXXdx6623ArBy5Up+8IMf8E//9E95xd6RqUVfYpZU1TD7wWpqautxoKa2ntkPVrOkqqbYoYmIFE3//v059dRTAbjkkkt4+umnmTx5Mvfccw+1tbWsWbOGs88+u8V2Rx55JCeffHJ7h1tS1KIvgjnP13PblmfTrqt6o5YPG/Y3K6vf18D37n+RxS+80VRWW5vYx33fOCXSWEVEmiS1vOtb8Rx4a7ZplNrb3MyYMmUK5513Hl27dmXs2LF87GMtU9rBBx/cquPFiVr0JSY1yecqFxEpB2+88QbPPptoIC1evJh//Md/5PDDD+fwww/nhz/8IZMmTSpyhKVLLfoimH1SNyoq0rfET527gpra+hbl/Xp2a9Z6r6yszLgPEZG4OeaYY7jrrrtYvXo1gwcPZvr06QBMmjSJt99+myFDhhQ5wtKlRF9iZp41mNkPVlO/r6GprFuXzsw8a3ARoxIRKa5OnTpx++23txg6dvXq1Xz9619vVnfr1q0A9O7dm+eff77ZusrKSt57773I4y0lSvQlprF3vXrdi4hkN2LECA455BB++tOf8uGHHxY7nJKlRF+Cxg7vp8QuIhIYMGAAmzZtalG+bt26pvdK9JmpM56IiEiMqUVfBpZU1XB95fu88/ijuhUgIlJmlOhjrnEAnvp9DhwYgAdQshcRKQNK9B3cxHnpB95pFHYAHoDp6tgvIgXQ+HtJA3qVBt2jjzkNwCMiHV3nzp0ZNmwYw4YN49RTT2Xu3Nxj4+dj1apVPPPMM03L1113Hf369WPYsGEMGjSIcePGsXnz5qb106ZNa7Yc1qJFi5gxY0ZBYs6HWvQdXK6/mMMOwAOJ50tFRNpiSVVN05XEU+euKEifoG7durFhwwaAFs/RF8KqVavo1asXo0aNair77ne/y9VXXw3Afffdx+mnn051dTV9+vRh/vz5BT1+1NSij7mZZw2mW5fOzco0AI+IRKGxT1DjFcMoJ+V67LHHuPDCC5uWV61axXnnnQfAE088wSmnnMKJJ57IpZdeSl1dHZB4TO/aa6/ltNNOY+jQobzyyits3bqVhQsXctNNNzFs2DBWrVrV4lgTJ07kzDPP5Fe/+hUAFRUVrF27loaGBi677DKOO+44hg4dyk033dS0/qqrrmLUqFEcd9xxvPDCC2njP+mkkxg+fDhf/OIXefPNN9m/fz+DBg3i7bffBmD//v38/d//PTt37mzTZ6VEH3Njh/djzrih9OpqGImW/JxxQ9URT0QKZuK8Z5k471m+d/+LzUb1hAN9gnL1J8qmvr6+2aX7++67jzPOOIPnnnuOPXv2APDggw8yceJEdu7cyQ9/+EOefPJJ1q9fz/Dhw7nxxhub9tW7d29WrVrF9OnT+clPfsKAAQOYOnUq3/3ud9mwYQOnnXZa2hhOPPFEXnnllWZlGzZsoKamhk2bNlFdXd1smtw9e/bwzDPPcOuttzJ16tQW+zv55JN57rnnqKqq4qKLLuLHP/4xnTp14pJLLuHee+8F4Mknn+SEE06gd+/erf7sQJfuy8LY4f3o+e6rVFRUFDsUEYmxqPoEZbp0P2bMGB555BEmTJjA8uXLuemmm/jDH/7A5s2bm6a03bt3b9N7gHHjxgGJUfUefPDB0DG4e4uyo446itdff51vf/vbnHPOOZx55plN6y6++GIAPv/5z7N7925qa2ubbbt9+3amTZvGX//6Vz788EMGDhwIwNSpUzn//PO56qqrWLhwYbM/HlpLLXoREWmT+75xCvd94xT69eyWdn26PkGFMHHiRH7zm9+wYsUKTjzxRHr06IG7c8YZZ7BhwwY2bNjAmjVrWLBgQdM2Bx10EJDo4PfRRx+FPlZVVRXHHHNMs7LDDjuMjRs3UlFRwS233MK0adOa1qWbVjfZzJkzmTFjBtXV1cybN4+9e/cC0L9/f/r27cuKFSt4/vnnOfvss0PHmIkSvYiIFER79wmqqKhg/fr13HHHHU0t9ZNPPpmnn36a1157DYD333+fP/7xj1n306NHj6wT3TzwwAM88cQTTa30Rjt37mT//v2MHz+e66+/nvXr1zetu++++4DEpDuHHnoohx56aLNtd+/eTb9+iVuod911V7N106ZN45JLLuHCCy+kc+fmn2drKNGLiEhBNPYJ+njnRGopVJ+g1Hv0s2bNAhKt8nPPPZfHHnuMMWPGANCnTx8WLVrExRdfzPHHH88XvvCFFvfWU40ZM4aHHnqoWWe8xs55gwYN4p577mHFihX06dOn2XY1NTVUVFQwbNgwLrvsMubMmdO07rDDDmPUqFFcccUVza4oNJo9ezYXXHABp512Wot78F/+8pepq6sryGV70D16EREpoLHD+zUNxlWoy/UNDQc6+KU+XnfzzTdz8803N2uRn3766axZs6ZF/cbpa9977z1GjhzZ9EjxoEGDePHFF5u2P+2007juuusyxpP8KHJyKz7Z+PHjmyV+gMsuu4zLLrsMgHPOOYeLLroo7bYbN27khBNOYMiQIRljyIcSvYiIFJRGxGu9uXPncttttzX1vC8EJXoREZECasvgY7NmzWq6NVEoJX+P3syOMrMFZnZ/sWMRERHpaCJN9Ga20MzeMrNNKeVjzGyLmb1mZln/dHH31939a1HGWQhLqmo4de4KBs56lFPnrohkJCgRkWJI9wy5FEdrzkXUl+4XATcDv2wsMLPOwC3AGcA2YI2ZPQx0BuakbD/V3d+KOMY2OzAVbKLDiKaCFZG46Nq1K7t27aJXr14tngWX9uXu7Nq1i65du+a1nUX9l5qZDQCWuvtxwfIpwHXuflawPBvA3VOTfOp+7nf3CVnWXw5cDtC3b98R8+fPp3v37s3q1NXVhSpLNef5lpPCJPvTu/v5KM3ATx/rBJ89tOVFk2//Q0POY+YSJu586+eqU1dXx4u7D+KBP+5j116nV1dj/NFdOP4TH6TdLsznne/PUQiFOGYUn3+uevmua0tZ1Er1HIT5DoT9v56pvKN9B8yMQw45pMXz3O6ed+IPs02uOpnW51OeWtaan6WtWnvMhoYG9uzZg7s3O4+jR49e5+4jMx4syhcwANiUtDwBmJ+0PBm4Ocv2vYDbgT8Bs8Mcc8SIEb5y5UpPFbYs1YW3P5P19Zl/XZrxla5+mGPmku8+wtTPVedH9z7hQ77/WLOfb8j3H/Mf3ftE6P2llhXis8hXqX7+uerlu66Q34FCK9VzkKtOpvX5lJfrdyDsNjoHrdsHsNYz5MRi9LpP9ydMxssK7r4LuCK6cHIr5FSwULrTwc55vp7btmSeeGLdXz5sceWifl8DCzc1sDFlwgo9XiMiUhqK0et+G9A/afkIYHsR4iiYcpkKNt3tiWzlIiJSfMVo0a8BBpnZQKAGuAj45yLEUTCNHe5uWL6F7bX1HN6zGzPPGtzhOuLNPqkbFRWZW+IjrlvGrr0tL7706mpqwYuIlKhIE72ZLQYqgN5mtg241t0XmNkMYDmJnvYL3f2lKONoD2OH9+twiT1f44/uwt0vNzSbb7pbl86MP7rtky6IiEg0Ik307n5xhvJlwLIojy2FN+rwLhx7zLEtrlz0fPfVYocmIiIZaAhcyUu6KxeVlUr0IiKlquSHwBUREZHWU4teimZJVQ3XV77PO48/2mE7MIqIlDoleimKA8MGJ3rxa9hgEZFoKNFLJCbOe5ba2pYD8DSWVb1Ry4cNzR/Ar9/XwPfuf5HFL7zRYn96fE9EpHV0j16KIjXJ5yoXEZHWUYteInHfN06hsrKyxQA8jWX5DhssIiKtoxa9FEW5DBssIlJsatFLUTR2uLv+dxt5Z6+r172ISESU6KVoxg7vR893X6WioqLYoYiIxJYu3YuIiMSYEr2IiEiMKdGLiIjEmBK9iIhIjCnRi4iIxJgSvYiISIwp0YuIiMSYEr2IiEiMacAciY0lVTXcsHwL22vrNdKeiEhAiV5i4cD89g2A5rcXEWmkRC8dwsR5z2Zdn8/89tM1b46IlBHdo5dY0Pz2IiLpqUUvHUKuOerzmd++srKykKGJiJQ0teglFjS/vYhIemrRSyw0drhTr3sRkeaU6CU2xg7vp8QuIpJCl+5FRERiTIleREQkxpToRUREYkyJXkREJMaU6EVERGJMiV5ERCTGlOhFRERiTIleREQkxjRgjkgamtteROJCiV4khea2F5E4UaKXsjPn+Xpu25J5fvswc9vX1ib2kWtWPRGRYtM9epEUmtteROJELXopO7NP6kZFReaWeJi57SsrK7PuQ0SkVKhFL5JCc9uLSJyoRS+SQnPbi0icKNGLpKG57UUkLnTpXkREJMaU6EVERGJMiV5ERCTGlOhFRERiTIleREQkxtTrXiRiS6pquL7yfd55/FE9qici7U6JXiRCBybIcUAT5IhI+yv5RG9mxwBXAr2B37v7bUUOSaTJxHmZJ8eBcBPkJJuuwfdEpMAivUdvZgvN7C0z25RSPsbMtpjZa2Y2K9s+3P1ld78CuBAYGWW8IoWmCXJEpNiibtEvAm4GftlYYGadgVuAM4BtwBozexjoDMxJ2X6qu79lZl8GZgX7EikZuaapDTNBTrLKyspChSYiAkTconf3p4B3Uoo/B7zm7q+7+4fAr4Hz3b3a3c9Neb0V7Odhdx8FTIoyXpFC0wQ5IlJs5u7RHsBsALDU3Y8LlicAY9x9WrA8GTjJ3Wdk2L4CGAccBLzo7rdkqHc5cDlA3759R8yfP5/u3bs3q1NXVxeqLGqFOGa++whTP1edTOvzKU8tK4fP/5nt+/jtlg/42wdGr67G+KO7MOrwLnnvN991bSmLmr4D5fUdyGcbnYPW7WP06NHr3D397W13j/QFDAA2JS1fAMxPWp4M/LyQxxwxYoSvXLnSU4Uti1ohjpnvPsLUz1Un0/p8ylPL9PmHr5fvOn0H8q+v70C0+9A5KOwxk/cBrPUMObEYA+ZsA/onLR8BbC9CHCIiIrFXjES/BhhkZgPN7OPARcDDRYhDREQk9qJ+vG4x8Cww2My2mdnX3P0jYAawHHgZ+I27vxRlHCIiIuUq0sfr3P3iDOXLgGVRHltEREQ0qY2IiEisZWzRm9knQ2y/391rCxiPiOSwpKqGG5ZvYXttfdMkOT2LHZSIlKxsl+63By/LUqczcGRBIxKRjA5MktMAHJgkZ/IxnakobmgiUqKyJfqX3X14to3NrKrA8YiUtTnP13PblvQT5dTW1vPn3S+mnSRn4aYGNqZMsKMJckQEst+jzz6Id/g6IlIgmSbD+Uhz5IhIBhlb9O6+N9M6M+vu7nXZ6ohI/maf1I2KivR/P1dWVnLNc/vTTpLTq6u1mCRHE+SICLS+1/3mgkYhIqFkmiRn/NHpx84XEcnW6/7/ZloFtO/o/yICwNjh/QBa9rp/99UiRyYipSpbZ7z/BG4APkqzTs/fixTJ2OH9mhJ+o8pKJXoRSS9bol8PLHH3dakrzGxadCGJiIhIoWRL9FOAXRnWpZ/zVkREREpKxkvw7r7F3Xcml5nZp4J1b0YdmIiIiLRdvvfaNRGNiIhIB5Jvos82HK6IiIiUmHwT/R2RRCEiIiKRyGs+ene/NapARKT9Lamq4frK93nn8UebnslPfXRPRDq2nC16M7uuHeIQkXbWOBPerr2Oc2AmvCVVNcUOTUQKKNvIeJ1IXKp/q/3CEZFCSTcTXm3tgbKqN2rTzoT3vftfZPELbzQrTx1HX0Q6jmwt+keAd9x9dnsFIyLtJ9NMeJnKRaRjynaPfiTwo/YKREQKK91MeJWVlU1lp85dkXYmvH49u6kFLxIj2Vr0o4F5ZnZSewUjIu0n00x4M88aXKSIRCQK2UbG2wycRWJiGxGJmbHD+zFn3FB6dTWMREt+zrih6nUvEjNZH69z9+1mdk57BSMi7Wvs8H70fPdVKioq2ryvJVU1LabP1R8NIsWX8zl6d3+v8X3QE7+7u++ONCoR6VAaH9Wr39cAHHhUD1CyFymynInezH4FXAE0AOuAQ83sRnfXJX2RMjFx3rNZ1+fzqB7AdHUDEGk3YYbAPTZowY8lManNkcDkSKMSkQ5Fj+qJlK4wQ+B2MbMuJBL9ze6+z8w84rhEpITketwu30f1KisrCxWaiOQQpkU/D9gKHAI8ZWafAXSPXkSa6FE9kdIVpjPez4CfNS6b2RsknrEXEQEOdLgrZK979eIXKYxsY92f6+5LU8vd3YGPstURkfIzdni/giVi9eIXKZxsLfobzKwGsCx1/hNQoheRvKSbcCdZmF78jRP0aLhekeyyJfo3gRtzbP9qAWMREQHUi1+kkDImenevaMc4RKSMpJtwJ1mYXvzJE/TksqSqhusr3+edxx/V/X4pO2F63YuItKtC9uJvvN+/a6/jHLjfv6SqpkDRipS2MM/Ri4i0q3x68Rdy1D6N2CdxpEQvIiWpUL34db9fyl2Yse4PBv4FONLdv25mg4DBeqxOREpBIUft04h9Ekdh7tHfCXwANH4jtgE/jCwiEZEC0qh9Uu7CJPrPuvuPgX0A7l5P9mfrRURKxtjh/Zgzbii9uhpGoiU/Z9xQ9bqXshHmHv2HZtYNcAAz+yyJFr6ISIcwdng/er77KhUVFcUORaTdhUn01wGPA/3N7F7gVGBKlEGJiJSqdGPw9yx2UCJZhJnU5gkzWwecTOKS/ZXuvjPyyERESswz2/dx9+9bjsE/+ZjOVBQ3NJGMwvS6/727fwF4NE2ZiEhs5BqDf91fPuSjlKfy6vc1sHBTAxvTPM+v5/KlFGSbva4rcDDQ28wO40AHvE8Ah7dDbCIiJSU1yecqFykF2Vr03wCuIpHU13Eg0e8Gbok4LhGRdpdrDP4R1y1j115vUd6rq6V9nl/P5UspyPh4nbv/t7sPBK5296PcfWDwOsHdb27HGEVESsL4o7ukfSZ//NFdihSRSG5hOuP93MyOA44FuiaV/zLKwERESs2ow7tw7DHHtux1/65m7JbSFaYz3rVABYlEvww4G1gNKNGLSNlJNwZ/ZaUSvZSuMCPjTQC+AOxw9ynACcBBkUYlIiIiBREm0de7+37gIzP7BPAWcFS0YYmIiEghhEn0a82sJ3AHid7364EXIo0qiZlVmNkqM7vdzCra67giIiJxkDXRm5kBc9y91t1vB84Avhpcws/JzBaa2VtmtimlfIyZbTGz18xsVo7dOFBHoiPgtjDHFRERkYSsnfHc3c1sCTAiWN6a5/4XATeT1HHPzDqTeA7/DBKJe42ZPQx0BuakbD8VWOXufzCzvsCNwKQ8YxARESlbYSa1ec7M/o+7r8l35+7+lJkNSCn+HPCau78OYGa/Bs539znAuVl29zfUCVBERCQv5t5ylKdmFcw2A0cDfwH2kBghz939+FAHSCT6pe5+XLA8ARjj7tOC5cnASe4+I8P244CzgJ7Abe5emaHe5cDlAH379h0xf/58unfv3qxOXV1dqLKoFeKY+e4jTP1cdTKtz6c8tUyff/h6+a5rS1nUSvUctPd34Jnt+/jtlg/42wdGr67G+KO7MOrw6AffKcbnH3Yb/R5q3T5Gjx69zt1Hpq3o7llfwGfSvXJtl7T9AGBT0vIFwPyk5cnAz8PuL8xrxIgRvnLlSk8VtixqhThmvvsIUz9XnUzr8ylPLdPnH75evuv0Hci/fnt+Bx5av82HfP8x/8y/Lm16Dfn+Y/7Q+m0542yrYnz+YbfR76HW7QNY6xlyYpiR8f7Slr840tgG9E9aPgLYXuBjiIgUXbrZ8GprE2VVb9TyYUPz2XDq9zXwvftfZPELb7TYV7qx9EXCCPN4XaGtAQaZ2UAz+zhwEfBwEeIQESma1CSfq1yktcJ0xms1M1tMYvjc3ma2DbjW3ReY2QxgOYme9gvd/aUo4xARKYZ0s+FVVlZSUXEKp85dQU1tfYtt+vXspta7FFSkid7dL85QvozEuPkiImVp5lmDmf1gNfX7GprKunXpzMyzBhcxKomjjInezN4jMVhNWu7+iUgiEhEpA40T41z/u428s9ebZsJLnTBHpK0yJnp37wFgZv8B7ADuJvFo3SSgR7tEJyISY2OH96Pnu69SUVFR7FAkxsJ0xjvL3W919/fcfbe73waMjzowERERabswib7BzCaZWWcz62Rmk4CGnFuJiIhI0YVJ9P8MXAi8GbwuCMpERESkxIUZMGcrcH70oYiIiEih5Uz0ZtYH+DqJoWyb6rv71OjCEhERkUII8xz974BVwJPo3ryISMlaUlXDDcu3sL22Xo/rSZMwif5gd//XyCMREZFWW1JV02wAnpraemY/WA2gZF/mwiT6pWb2pWA0OxERKYKJ857Nuj6fSXKma/C9shKm1/2VJJJ9vZntNrP3zGx31IGJiEh4miRHMgnT616j4ImIFFmuiW7ymSSnsrKykKFJiQvT6/7z6crd/anChyMiIq2hSXIkkzD36Gcmve8KfA5YB5weSUQiIpK3xg536nUvqcJcuj8vednM+gM/jiwiERFplbHD+ymxSwthOuOl2gYcV+hAREREpPDC3KP/OQfmpe8EDAM2RhmUiIiIFEaYe/Rrk95/BCx296cjikdEREQKKMw9+rvM7OPA0UHRlmhDEhERkUIJc+m+ArgL2AoY0N/MvqrH60REREpfmEv3PwXOdPctAGZ2NLAYGBFlYCIiItJ2YXrdd2lM8gDu/kegS3QhiYiISKGE6oxnZguAu4PlSSQGzBERkZjSlLfxESbRTwe+BXyHxD36p4BbowxKRESKR1PexkvWRG9mnYEF7n4JcGP7hCRF8dgshr2yCv7cM2OVYbW1add/+qDjgYroYhORgprzfD23bck87W2YKW9raw/sI9eEO1JcWRO9uzeYWR8z+7i7f9heQUkH8pfVDGY13Plii1Xp/jBoVjZ0AjCwHYIUkXxoytt4CXPpfivwtJk9DOxpLHR3tfDj5Oy5bOhWSUVFRcYqGyrTrF97J7Wr5pP5OkAGOxKXARk4M3s9ESm42Sd1o6Iicys8zJS3lZWVWfchpSNMot8evDoBmptemhs5hQ11A9P+gZDuD4OmsjvPaZfwRCR/mvI2XsKMjPeD9ghERERKg6a8jZcwI+M9woFJbRq9S2IM/HnuvjeKwKQM7KhmWO01WTsAtjB0AoycEl1MIgJoyts4CTNgzutAHXBH8NoNvEli7Ps7ogtNYm3oBPjU0Py22VEN1fdHE4+ISEyFuUc/3N0/n7T8iJk95e6fN7OXogpMYm7klMT9/XQd/DLRfX0RkbyFadH3MbMjGxeC932CRT1yJyIiUsLCtOj/BVhtZn8iMTLeQOCbZnYIiVntREREpESF6XW/zMwGAUNIJPpXEsX+AfBfEccnIiIibZDz0r2ZLXT3D9x9o7tvADoDy6IPTURERNoqzKX7GjO7zd2nm9lhwKOot72IiEgzpTrjX84Wvbv/O7DbzG4HngB+6u53Rh6ZiIhIB9E4419NbT3OgRn/llTVFDu0zC16MxuXtPgC8O/Bv25m49z9waiDExERKRUT57Vtxr9k09txNOFsl+7PS1muAroE5Q4o0Uv721HduufpNaKeiESolGf8y5jo3V2/FaW0DJ3Quu0aZ8pToheRNmicuS+dMDP+JausrCxkaFmFGev+LuBKd68Nlg8jcZ9+atTBiTQTjKaXN42oJyIRK+UZ/8L0uj++MckDuPvfzGx4hDGJiIh0KKU841+YRN/JzA5z978BmNknQ24nIiJSNkp1xr8wCfunwDNm1jht2AXAj6ILSURERAolzBC4vzSzdcBoEkPgjnP3zZFHJiIiIm0W6hK8u79kZm8DXSExg527t3wwUEREREpKmLHuv2xmrwJ/Bv4AbAUeizguERERKYAwLfrrgZOBJ919uJmNBi6ONiyRAksaaGdYbS38uWe47TTQjoh0cDlb9MA+d99Fovd9J3dfCQyLOC6Rwhk6AT41NP/tdlRD9f2564mIlLAwLdrw5fkAABGgSURBVPpaM+sOPAXca2ZvAR9FG9YBZnYaMIlErMe6+6j2OrbERMpAOxsqK6moqMi9nQbaEZEYCNOiPx94H/gu8DjwJ1qOg5+WmS00s7fMbFNK+Rgz22Jmr5nZrGz7cPdV7n4FsBS4K8xxRUREJCHM43V7grf7zexRYJe7e8j9LwJuBn7ZWGBmnYFbgDOAbcAaM3sY6AzMSdl+qru/Fbz/Z2BayOOKiIgI2aepPRmYC7xDokPe3UBvEvfqL3X3x3Pt3N2fMrMBKcWfA15z99eD4/waON/d5wDnZojlSOBdd9+d8ycSERGRJpapcW5ma4F/Aw4FfgGc7e7PmdkQYLG7hxrvPkj0S939uGB5AjDG3acFy5OBk9x9RpZ9/ABY7u7PZKlzOXA5QN++fUfMnz+f7t27N6tTV1cXqixqhThmvvsIUz9XnUzr8ylPLSvlz39Y1TUAbBjeciDIKD7/XPXyXdeWsqjpO9AxvgOF3ofOQfa42rKP0aNHr3P3kWkrunvaF7Ah6f3LKeuqMm2XZj8DgE1JyxcA85OWJwM/D7u/MK8RI0b4ypUrPVXYsqgV4pj57iNM/Vx1Mq3Ppzy1rKQ//4VfSrzaso8862erl+86fQfyr6/vQLT70Dko7DGT9wGs9Qw5MVtnvP1J71Mn2Q17jz6dbUD/pOUjgO1t2J+IiIhkkK0z3glmtpvE+PbdgvcEy13bcMw1wCAzGwjUABeR6GgnUnqSBtpJlnPQHQ20IyIlImOid/fObd25mS0GKoDeZrYNuNbdF5jZDGA5iZ72C939pbYeS6Tghk5o3XY7qhP/KtGLSAmIdF55d087VK67LwOWRXlskTZLGWgnWdZBdzTQjoiUkDAD5oiIiEgHpUQvIiISY0r0IiIiMaZELyIiEmORdsYTKVspj+XlfBwv8OmDjifxoIqISGEo0YsUWhsey+vbtbawsYhI2VOiFym0NI/lZX0cr9Gd50CtEr2IFJbu0YuIiMSYEr2IiEiMKdGLiIjEmBK9iIhIjCnRi4iIxJh63YuUkO51f844KU7GZ/GHTgAGRhuYiHRYatGLlIqhE6jrnmfC3lEN1fdHE4+IxIJa9CKlYuQUNtQNzPi8fdpn8TUlrojkoBa9iIhIjCnRi4iIxJgSvYiISIwp0YuIiMSYOuOJdHQ7qhlWe02LR+/SPY7XrGzohBaT74hI/CjRi3RkjVPi5jvr3Y7qxL9K9CKxp0Qv0pEFU+Kme/Qua5keyxMpG7pHLyIiEmNK9CIiIjGmRC8iIhJjukcvUq52VMOd52SeLCcT9dYX6VCU6EXKUWNv/Xz9ZXXi1ZqJdPQHgkhRKNGLlKOgtz5kmCwnk7V3ti7J63E+kaJRoheR8JL+QMiLHucTKRolehFpH0GfAEg/al9GuuQv0iZK9CISvdb2CdAlf5E2U6IXkeilXPIP3S9Al/xF2kzP0YuIiMSYEr2IiEiMKdGLiIjEmO7Ri0hpS+qtnyxnz3311hcBlOhFpJSpt75ImynRi0jpyjJAT9ae++qtL9JE9+hFRERiTIleREQkxpToRUREYkz36EUknlJ664cZX//TBx0PVEQbl0g7U6IXkfhpTW/9HdX07Vpb+FhEikyJXkTiJ01v/Zzj6995DtQq0Uv86B69iIhIjCnRi4iIxJgu3YuIBLrX/TnrYDuZOvSpE5+UMiV6ERGAoROoq60le7/8NNSJT0qcEr2ICMDIKWyoG5i1w17aDn3qxCclTvfoRUREYqzkE72ZHWtmvzGz28yslVNZiYiIlKdIE72ZLTSzt8xsU0r5GDPbYmavmdmsHLs5G/i5u08HLo0sWBERkRiK+h79IuBm4JeNBWbWGbgFOAPYBqwxs4eBzsCclO2nAncD15rZl4FeEccrIiISK5Emend/yswGpBR/DnjN3V8HMLNfA+e7+xzg3Ay7+lbwB8KDUcUqIiISR+bu0R4gkeiXuvtxwfIEYIy7TwuWJwMnufuMLNv/G3AIcJu7r85Q73LgcoC+ffuOmD9/Pt27d29Wp66uLlRZ1ApxzHz3EaZ+rjqZ1udTnlqmzz98vXzXtaUsaqV6DlrzHRhWdQ0NDQ1Uj5wbqn65fgfCbqPfQ63bx+jRo9e5+8i0Fd090hcwANiUtHwBMD9peTKJe/AFO+aIESN85cqVnipsWdQKccx89xGmfq46mdbnU55aps8/fL181+k7kH/9Vn0HFn7J/3bjqND1y/U7EHYb/R5q3T6AtZ4hJxbjOfptQP+k5SOA7UWIQ0SkIDKNqJduJL1mZUMnAAPbIUIpZ8V4vG4NMMjMBprZx4GLgIeLEIeISNsNnUBd91Yk6x3VUH1/4eMRSRFpi97MFpMYALq3mW0DrnX3BWY2A1hOoqf9Qnd/Kco4REQik2VEvXQj6TWVZRlTX6SQou51f3GG8mXAsiiPLSIiIh1gZDwRERFpPU1qIyJSLDuqGVZ7Tdqpb7MaOgFGTokmJokdJXoRkWIYGkzdke/MdzuqE/8q0UtISvQiIsUwckqiI1+6qW+zUSc+yZPu0YuIiMSYEr2IiEiM6dK9iEhHs6O6dZfw1YmvLCnRi4h0JI2d+PKlTnxlS4leRKQjCTrx5U2d+MqW7tGLiIjEmBK9iIhIjCnRi4iIxJju0YuIlIugt/6w2trww+6qp36Hp0QvIlIOWtNbXz31Y0GJXkSkHCT11g897K566seC7tGLiIjEmBK9iIhIjCnRi4iIxJgSvYiISIypM56IiGSWYQKdnI/o6bG8kqFELyIi6WkCnVhQohcRkfSyTKCT9RE9PZZXUnSPXkREJMaU6EVERGJMiV5ERCTGlOhFRERiTIleREQkxtTrXkRECutTQ4sdgSRRohcRkcI6e26xI5AkunQvIiISY0r0IiIiMaZELyIiEmNK9CIiIjGmRC8iIhJjSvQiIiIxpkQvIiISY0r0IiIiMaZELyIiEmNK9CIiIjGmRC8iIhJjSvQiIiIxpkQvIiISY+buxY6h4MzsbaAWeDdl1aFpynoDO9sjrhxxRL2PMPVz1cm0Pp/y1DJ9/uHr5bsubJnOQfg6+g60bR86B5ljaOs+PuPufdLWcvdYvoBfhCxbWwqxRb2PMPVz1cm0Pp/y1DJ9/uHr5btO34H86+s7EO0+dA6Kcw7ifOn+kZBlxVCIOPLdR5j6uepkWp9PeSmcg1L9/HPVy3ddqX7+ULrnQN+BaPehc3BAu52DWF66z4eZrXX3kcWOo1zp8y8+nYPi0udffHE/B3Fu0Yf1i2IHUOb0+RefzkFx6fMvvlifg7Jv0YuIiMSZWvQiIiIxpkQvIiISY0r0IiIiMaZEn4GZjTWzO8zsd2Z2ZrHjKUdmdpSZLTCz+4sdS7kws0PM7K7g//6kYsdTjvT/vvji9vs/lonezBaa2VtmtimlfIyZbTGz18xsVrZ9uPsSd/86cBkwMcJwY6lA5+B1d/9atJHGX57nYhxwf/B//8vtHmxM5XMO9P8+Gnmeg1j9/o9logcWAWOSC8ysM3ALcDZwLHCxmR1rZkPNbGnK6++SNv1+sJ3kZxGFOwfSNosIeS6AI4D/Dao1tGOMcbeI8OdAorGI/M9BLH7/f6zYAUTB3Z8yswEpxZ8DXnP31wHM7NfA+e4+Bzg3dR9mZsBc4DF3Xx9txPFTiHMghZHPuQC2kUj2G4hvQ6Dd5XkONrdvdOUhn3NgZi8To9//5fRF7seBlgokfqH1y1L/28AXgQlmdkWUgZWRvM6BmfUys9uB4WY2O+rgykymc/EgMN7MbqM0hgmNs7TnQP/v21Wm70Gsfv/HskWfgaUpyzhakLv/DPhZdOGUpXzPwS6gw3/JSlTac+Hue4Ap7R1Mmcp0DvT/vv1kOgex+v1fTi36bUD/pOUjgO1FiqVc6RyUDp2L4tM5KL6yOAfllOjXAIPMbKCZfRy4CHi4yDGVG52D0qFzUXw6B8VXFucglonezBYDzwKDzWybmX3N3T8CZgDLgZeB37j7S8WMM850DkqHzkXx6RwUXzmfA01qIyIiEmOxbNGLiIhIghK9iIhIjCnRi4iIxJgSvYiISIwp0YuIiMSYEr2IiEiMKdGLtJKZNZjZBjPbZGaPmFnPIsfzbwXcV08z+2YrtrvOzK4uVBxRC+KtMbP/MLMpwfncYGYfmll18H5uhm17mNkuM+ueUr7UzMaZ2aRg6tMl7fPTiKSnRC/SevXuPszdjwPeAb5V5HjSJnpLyPe73hPIO9G3t2Ca0ba6yd3/n7vfGZzPYSSGQR0dLM9Kt5G7vwesIDHjXGM8hwEnAcvc/V40Zr2UACV6kcJ4lqSZ+MxsppmtMbMXzewHSeWXBmUbzezuoOwzZvb7oPz3ZnZkUL7IzH5mZs+Y2etmNiEo/7SZPZV0NeG0oNXZLSi718wGmNnLZnYrsB7ob2Z1SXFMMLNFwfu+ZvZQENNGMxtFYorOzwb7uyHHz3SNmW0xsyeBwek+HDPrY2YPBNuvMbNTg/LrzGyhmVUGP+N3kra5xMxeCGKY15jUzawuaIE/D5xiZl8ys1fMbHXweS01s05m9qqZ9Qm26RS0rnu35uSaWffgfLxgZlVmdl6wajGJYVMbjQcedfe9rTmOSCTcXS+99GrFC6gL/u0M/BYYEyyfCfyCxMxYnYClwOeBfwC2AL2Dep8M/n0E+GrwfiqwJHi/KNhvJ+BYEvNmA/wLcE3SsXskxxO8HwDsB05OjTd4PwFYFLy/D7gqaX+HBttvSqqf6WcaAVQDBwOfAF4Drk7zWf0K+Mfg/ZHAy8H764BngIOA3sAuoAtwTPC5dAnq3QpcGrx34MLgfVcS04wODJYXA0uD99cm/VxnAg+kieu6DPFubTxPwfKPgYuC94cBfwyOfRDwNnBYsO5J4Kyk7b7YeD710qtYr3Kaplak0LqZ2QYSSXEd8D9B+ZnBqypY7g4MAk4A7nf3nQDu/k6w/hRgXPD+bhJJpdESd98PbDazvkHZGmChmXUJ1m/IEN9f3P25ED/H6cClQUwNwLvBJehkmX6mHsBD7v4+gJllmhDki8CxZk2zgn7CzHoE7x919w+AD8zsLaAv8AUSf0SsCbbpBrwV1G8AHgjeDwFed/c/B8uLgcuD9wuB3wH/ReIPqDtzfhKZnQmcbWaNl/G7Ake6+x/N7FFgnJktJfHH3O/bcByRglOiF2m9encfZmaHkmjhfovEHNYGzHH3ecmVg8vSYSaXSK7zQfIuANz9KTP7PHAOcLeZ3eDuv0yznz1Z9ts1RBzJMv1MVxHuZ+oEnOLu9SnbQ/OfsYHE7yUD7nL32Wn2tTf4g6QxrrTc/X/N7E0zO53EffNJIeLMxICx7v6nNOsWA1eT+GPkQU9MlCJSMnSPXqSN3P1d4DvA1UErezkwtbE3tpn1M7O/I9HSu9DMegXlnwx28QwH7vNOAlZnO56ZfQZ4y93vABYAJwar9gXHz+RNMzsm6Jj3laTy3wPTg313NrNPAO+RaK03yvQzPQV8xcy6BS3080jvCRKzhDX+DMOy/YxBTBOCY2Bmnwx+7lSvAEeZ2YBgeWLK+vnAPSRmJWug9ZaTOMcE8QxPWvckiZb8FSSSvkhJUaIXKQB3rwI2kriP+wSJe9LPmlk1cD+J++gvAT8C/mBmG4Ebg82/A0wxsxeBycCVOQ5XAWwwsyoSnb/+Oyj/BfCimd2bYbtZJK48rAD+mlR+JTA6iHUd8A/uvgt4Oujsd0OWn2k9iXv8G0hcTl+V4djfAUYGHfk2k6M3urtvBr4PPBF8Lv8DfDpNvXoSTwc8bmargTeBd5OqPEziNkNbLtsD/AA42BKP3L1E4t5+YwwNwEMk+ig83cbjiBScpqkVkQ7NzLq7e50l7gPcArzq7jcF60aSeHzutAzbXkeik+JPIorti8AMdx8bxf5FwlCLXkQ6uq8HnSJfIvHEwDyAoOPcA0C6+/yN6oDLzew/Ch2UmU0i0Wfjb4Xet0g+1KIXERGJMbXoRUREYkyJXkREJMaU6EVERGJMiV5ERCTGlOhFRERiTIleREQkxv4/ms/5TUNsbq4AAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "zoom = 2\n", "plt.figure(figsize=(zoom*4,zoom*3))\n", @@ -612,110 +718,9 @@ "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", @@ -731,7 +736,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.9" } }, "nbformat": 4, diff --git a/pyirf/cut_optimization.py b/pyirf/cut_optimization.py index bfa30b65c..f4f7487f0 100644 --- a/pyirf/cut_optimization.py +++ b/pyirf/cut_optimization.py @@ -8,7 +8,7 @@ from .binning import create_histogram_table -def optimize_gh_cut(signal, background, bins, cut_values, op, progress=True): +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. @@ -53,7 +53,7 @@ def optimize_gh_cut(signal, background, bins, cut_values, op, progress=True): sensitivity = calculate_sensitivity( signal_hist, background_hist, - alpha=1, + alpha=alpha, t_obs=50 * u.hour, ) sensitivities.append(sensitivity) From 34691fd012ac8174fbe00dbeb6964c08012f540a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Tue, 22 Sep 2020 18:53:25 +0200 Subject: [PATCH 028/105] Add repr to LogParabola --- pyirf/spectral.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyirf/spectral.py b/pyirf/spectral.py index c3821e732..7fdea06a6 100644 --- a/pyirf/spectral.py +++ b/pyirf/spectral.py @@ -86,6 +86,9 @@ def __call__(self, energy): e = (energy / self.e_ref).to_value(u.one) return self.flux_normalization * e**(self.a + self.b * np.log10(e)) + def __repr__(self): + return f'{self.__class__.__name__}({self.flux_normalization} * (E / {self.e_ref})**({self.a} + {self.b} * log10(E / {self.e_ref}))' + class PowerLawWithExponentialGaussian(PowerLaw): From 35d0ca25f7ba67021fc298f2d7961cb811185dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 10:10:52 +0200 Subject: [PATCH 029/105] Fix hard coded alpha=1 leftover --- pyirf/sensitivity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index b4f36c679..7f8ef2f12 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -135,7 +135,7 @@ def calculate_sensitivity( relative_sensitivity( n_on=n_signal_hist + alpha * n_background_hist, n_off=n_background_hist, - alpha=1.0, + alpha=alpha, t_obs=t_obs, ) for n_signal_hist, n_background_hist in zip(signal_hist['n_weighted'], background_hist['n_weighted']) From 8141796af0b3e0338aa8366f1f4646b5c9d31855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 10:11:15 +0200 Subject: [PATCH 030/105] Add some minimum requirements for background statistics --- pyirf/sensitivity.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index 7f8ef2f12..0e9627824 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -143,8 +143,10 @@ def calculate_sensitivity( # safety checks invalid = ( - (sensitivity['n_signal_weighted'] < 10) | - (sensitivity['n_signal_weighted'] < 0.05 * sensitivity['n_background_weighted']) + (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 From 9fb4956d48b16714dfdce7db988967bafc30dad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 12:31:19 +0200 Subject: [PATCH 031/105] Add calculate_theta function --- examples/calculate_eventdisplay_irfs.py | 24 ++++++++++-------------- pyirf/io/eventdisplay.py | 1 + pyirf/utils.py | 11 +++++++++++ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 0e5423b38..9009301f5 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -11,12 +11,12 @@ from astropy import table import astropy.units as u from astropy.io import fits -from astropy.coordinates.angle_utilities import angular_separation 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 from pyirf.spectral import ( calculate_event_weights, @@ -36,7 +36,10 @@ # scaling between on and off region. # Make off region 5 times larger than on region for better # background statistics -ALPHA = 0.2 +ALPHA = 0.1 + +# gh cut used for first calculation of the binned theta cuts +INITIAL_GH_CUT = 0.0 particles = { @@ -74,18 +77,12 @@ def main(): p['events']['true_energy'], p['target_spectrum'], p['simulated_spectrum'] ) + # calculate theta / distance between reco and true source pos + p['events']['theta'] = calculate_theta(p['events']) + log.info(p['simulation_info']) log.info('') - # calculate theta (angular distance from source pos to reco pos) - - for p in particles.values(): - tab = p['events'] - tab['theta'] = angular_separation( - tab['true_az'], tab['true_alt'], - tab['reco_az'], tab['reco_alt'], - ) - gammas = particles['gamma']['events'] # background table composed of both electrons and protons background = table.vstack([ @@ -93,8 +90,7 @@ def main(): particles['electron']['events'] ]) - gh_cut = 0.0 - log.info(f'Using fixed G/H cut of {gh_cut} to calculate theta cuts') + 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 @@ -106,7 +102,7 @@ def main(): # theta cut is 68 percent containmente of the gammas # for now with a fixed global, unoptimized score cut - mask_theta_cuts = gammas['gh_score'] >= gh_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], diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py index 219ad10eb..0d08396f8 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -36,6 +36,7 @@ def read_eventdisplay_fits(infile): 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] diff --git a/pyirf/utils.py b/pyirf/utils.py index bb0708194..f7d6cb9f2 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -1,6 +1,17 @@ import numpy as np +import astropy.units as u +from astropy.coordinates.angle_utilities import angular_separation def is_scalar(val): '''Workaround that also supports astropy quantities''' return np.array(val, copy=False).shape == tuple() + + +def calculate_theta(events): + theta = angular_separation( + events['true_az'], events['true_alt'], + events['reco_az'], events['reco_alt'], + ) + + return theta.to(u.deg) From b66ea825da354874fdce11ea84cdb08773449e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 16:50:49 +0200 Subject: [PATCH 032/105] Calculate Effective Area Co-authored-by: Michele Peresano --- examples/calculate_eventdisplay_irfs.py | 21 +++++++++++++- pyirf/irf/__init__.py | 6 ++++ pyirf/irf/effective_area.py | 38 +++++++++++++++++++++++++ pyirf/irf/energy_dispersion.py | 0 pyirf/irf/psf.py | 0 pyirf/sensitivity.py | 11 +++---- pyirf/utils.py | 21 ++++++++++++++ 7 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 pyirf/irf/__init__.py create mode 100644 pyirf/irf/effective_area.py create mode 100644 pyirf/irf/energy_dispersion.py create mode 100644 pyirf/irf/psf.py diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 9009301f5..2df0bf3a5 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -17,6 +17,7 @@ from pyirf.cuts import calculate_percentile_cut, evaluate_binned_cut from pyirf.sensitivity import calculate_sensitivity from pyirf.utils import calculate_theta +from pyirf.irf import point_like_effective_area from pyirf.spectral import ( calculate_event_weights, @@ -129,7 +130,7 @@ def main(): gammas[gammas['selected_theta']], background[background['selected_theta']], bins=sensitivity_bins, - cut_values=np.arange(-1.0, 1.005, 0.05), + cut_values=np.arange(-1.0, 1.005, 0.2), op=operator.ge, alpha=ALPHA, ) @@ -162,6 +163,9 @@ def main(): s['flux_sensitivity'] = s['relative_sensitivity'] * CRAB_HEGRA(s['reco_energy_center']) # calculate IRFs for the best cuts + true_e_bins_eff_area = add_overflow_bins(create_bins_per_decade( + 10**-1.9 * u.TeV, 10**2.31 * u.TeV, 10, + )) # write OGADF output file hdus = [ @@ -172,6 +176,21 @@ def main(): fits.BinTableHDU(theta_cuts_opt, name='THETA_CUTS_OPT'), fits.BinTableHDU(gh_cuts, name='GH_CUTS'), ] + + masks = { + 'EFFECTIVE_AREA_NO_CUTS': slice(None), + 'EFFECTIVE_AREA_ONLY_GH': gammas['selected_gh'], + 'EFFECTIVE_AREA_ONLY_THETA': gammas['selected_theta'], + 'EFFECTIVE_AREA': gammas['selected'] + } + for extname, mask in masks.items(): + effective_area = point_like_effective_area( + gammas[mask], + particles['gamma']['simulation_info'], + true_energy_bins=true_e_bins_eff_area, + ) + hdus.append(fits.BinTableHDU(effective_area, name=extname)) + fits.HDUList(hdus).writeto('sensitivity.fits.gz', overwrite=True) diff --git a/pyirf/irf/__init__.py b/pyirf/irf/__init__.py new file mode 100644 index 000000000..248df3360 --- /dev/null +++ b/pyirf/irf/__init__.py @@ -0,0 +1,6 @@ +from .effective_area import effective_area, point_like_effective_area + +__all__ = [ + 'effective_area', + 'point_like_effective_area', +] diff --git a/pyirf/irf/effective_area.py b/pyirf/irf/effective_area.py new file mode 100644 index 000000000..1c356e6fa --- /dev/null +++ b/pyirf/irf/effective_area.py @@ -0,0 +1,38 @@ +import numpy as np +import astropy.units as u +from astropy.table import QTable +from ..binning import create_histogram_table + + +@u.quantity_input(area=u.m**2) +def effective_area(selected_n, simulated_n, area): + return (selected_n / simulated_n) * area + + +def calculate_simulated_events(simulation_info, bins): + bins = bins.to_value(u.TeV) + e_low = bins[:-1] + e_high = bins[1:] + + int_index = simulation_info.spectral_index + 1 + e_min = simulation_info.energy_min.to_value(u.TeV) + e_max = simulation_info.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 simulation_info.n_showers * normalization * e_term + + +def point_like_effective_area(selected_events, simulation_info, true_energy_bins): + area = np.pi * simulation_info.max_impact**2 + + hist_selected = create_histogram_table(selected_events, true_energy_bins, 'true_energy') + hist_simulated = calculate_simulated_events(simulation_info, true_energy_bins) + + area_table = QTable(hist_selected[['true_energy_' + k for k in ('low', 'high')]]) + area_table['effective_area'] = effective_area( + hist_selected['n'], hist_simulated, area + ) + + return area_table diff --git a/pyirf/irf/energy_dispersion.py b/pyirf/irf/energy_dispersion.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyirf/irf/psf.py b/pyirf/irf/psf.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index 0e9627824..dcfee2ca0 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -5,6 +5,7 @@ import logging from .statistics import li_ma_significance +from .utils import check_histograms log = logging.getLogger(__name__) @@ -115,14 +116,10 @@ def calculate_sensitivity( ): assert len(signal_hist) == len(background_hist) + check_histograms(signal_hist, background_hist) sensitivity = QTable() - - # check binning information and add to output - for k in ('low', 'center', 'high'): - k = 'reco_energy_' + k - if not np.all(signal_hist[k] == background_hist[k]): - raise ValueError('Binning for signal_hist and background_hist must be equal') - + for key in ('low', 'high', 'center'): + k = 'reco_energy_' + key sensitivity[k] = signal_hist[k] # add event number information diff --git a/pyirf/utils.py b/pyirf/utils.py index f7d6cb9f2..01bb81281 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -15,3 +15,24 @@ def calculate_theta(events): ) 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' + ) From c0e15b429ebaaf2b9baad391dfe5927393b7769b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 17:42:30 +0200 Subject: [PATCH 033/105] Calculate energy dispersion Co-authored-by: Michele Peresano Co-authored-by: Lukas Nickel --- examples/calculate_eventdisplay_irfs.py | 29 +++++++++------- pyirf/irf/__init__.py | 2 ++ pyirf/irf/energy_dispersion.py | 44 +++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 2df0bf3a5..49047572a 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -17,7 +17,7 @@ from pyirf.cuts import calculate_percentile_cut, evaluate_binned_cut from pyirf.sensitivity import calculate_sensitivity from pyirf.utils import calculate_theta -from pyirf.irf import point_like_effective_area +from pyirf.irf import point_like_effective_area, point_like_energy_dispersion from pyirf.spectral import ( calculate_event_weights, @@ -162,10 +162,6 @@ def main(): for s in (sensitivity_step_2, sensitivity): s['flux_sensitivity'] = s['relative_sensitivity'] * CRAB_HEGRA(s['reco_energy_center']) - # calculate IRFs for the best cuts - true_e_bins_eff_area = add_overflow_bins(create_bins_per_decade( - 10**-1.9 * u.TeV, 10**2.31 * u.TeV, 10, - )) # write OGADF output file hdus = [ @@ -178,18 +174,29 @@ def main(): ] masks = { - 'EFFECTIVE_AREA_NO_CUTS': slice(None), - 'EFFECTIVE_AREA_ONLY_GH': gammas['selected_gh'], - 'EFFECTIVE_AREA_ONLY_THETA': gammas['selected_theta'], - 'EFFECTIVE_AREA': gammas['selected'] + '_NO_CUTS': slice(None), + '_ONLY_GH': gammas['selected_gh'], + '_ONLY_THETA': gammas['selected_theta'], + '': gammas['selected'] } + # calculate IRFs for the best cuts + true_energy_bins = add_overflow_bins(create_bins_per_decade( + 10**-1.9 * u.TeV, 10**2.31 * u.TeV, 10, + )) for extname, mask in masks.items(): effective_area = point_like_effective_area( gammas[mask], particles['gamma']['simulation_info'], - true_energy_bins=true_e_bins_eff_area, + true_energy_bins=true_energy_bins, + ) + edisp = point_like_energy_dispersion( + gammas[mask], + true_energy_bins=true_energy_bins, + migration_bins=np.geomspace(0.2, 5, 200), + max_theta=np.nanmax(theta_cuts_opt['cut'].max()), ) - hdus.append(fits.BinTableHDU(effective_area, name=extname)) + hdus.append(fits.BinTableHDU(effective_area, name='EFFECTIVE_AREA' + extname)) + hdus.append(fits.BinTableHDU(edisp, name='EDISP' + extname)) fits.HDUList(hdus).writeto('sensitivity.fits.gz', overwrite=True) diff --git a/pyirf/irf/__init__.py b/pyirf/irf/__init__.py index 248df3360..576cd11a4 100644 --- a/pyirf/irf/__init__.py +++ b/pyirf/irf/__init__.py @@ -1,6 +1,8 @@ from .effective_area import effective_area, point_like_effective_area +from .energy_dispersion import point_like_energy_dispersion __all__ = [ 'effective_area', 'point_like_effective_area', + 'point_like_energy_dispersion' ] diff --git a/pyirf/irf/energy_dispersion.py b/pyirf/irf/energy_dispersion.py index e69de29bb..a5f6dbc86 100644 --- a/pyirf/irf/energy_dispersion.py +++ b/pyirf/irf/energy_dispersion.py @@ -0,0 +1,44 @@ +import numpy as np +import astropy.units as u +from astropy.table import QTable + + +def _normalize_hist(hist): + with np.errstate(invalid='ignore'): + h = hist.T + h = h / h.sum(axis=0) + return np.nan_to_num(h).T + + +def point_like_energy_dispersion( + selected_events, + true_energy_bins, + migration_bins, + max_theta, +): + mu = (selected_events['reco_energy'] / selected_events['true_energy']).to_value(u.one) + + energy_dispersion, _, _ = np.histogram2d( + selected_events['true_energy'].to_value(u.TeV), + mu, + bins=[ + true_energy_bins.to_value(u.TeV), + migration_bins, + ] + ) + + 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) + + edisp = QTable({ + 'true_energy_low': [true_energy_bins[:-1]], + 'true_energy_high': [true_energy_bins[1:]], + 'migration_low': [migration_bins[:-1]], + 'migration_high': [migration_bins[1:]], + 'theta_low': [0 * u.deg], + 'theta_high': [max_theta], + 'energy_dispersion': [energy_dispersion[:, :, np.newaxis]], + }) + + return edisp From d259b513372c665f626a862bd26790e2f3ce8cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 18:14:05 +0200 Subject: [PATCH 034/105] Calculate energy bias / resolution Co-authored-by: Michele Peresano Co-authored-by: Lukas Nickel --- examples/calculate_eventdisplay_irfs.py | 9 +++- pyirf/benchmarks/__init__.py | 6 +++ pyirf/benchmarks/energy_bias_resolution.py | 51 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 pyirf/benchmarks/__init__.py create mode 100644 pyirf/benchmarks/energy_bias_resolution.py diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 49047572a..724d5172e 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -18,6 +18,7 @@ from pyirf.sensitivity import calculate_sensitivity from pyirf.utils import calculate_theta from pyirf.irf import point_like_effective_area, point_like_energy_dispersion +from pyirf.benchmarks import energy_bias_resolution from pyirf.spectral import ( calculate_event_weights, @@ -198,7 +199,13 @@ def main(): hdus.append(fits.BinTableHDU(effective_area, name='EFFECTIVE_AREA' + extname)) hdus.append(fits.BinTableHDU(edisp, name='EDISP' + extname)) - fits.HDUList(hdus).writeto('sensitivity.fits.gz', overwrite=True) + bias_resolution = energy_bias_resolution( + gammas[gammas['selected']], + true_energy_bins, + ) + + hdus.append(fits.BinTableHDU(bias_resolution, name='ENERGY_BIAS_RESOLUTION')) + fits.HDUList(hdus).writeto('pyirf_eventdisplay.fits.gz', overwrite=True) if __name__ == '__main__': diff --git a/pyirf/benchmarks/__init__.py b/pyirf/benchmarks/__init__.py new file mode 100644 index 000000000..d8f9edf65 --- /dev/null +++ b/pyirf/benchmarks/__init__.py @@ -0,0 +1,6 @@ +from .energy_bias_resolution import energy_bias_resolution + + +__all__ = [ + 'energy_bias_resolution', +] diff --git a/pyirf/benchmarks/energy_bias_resolution.py b/pyirf/benchmarks/energy_bias_resolution.py new file mode 100644 index 000000000..b3bc95f88 --- /dev/null +++ b/pyirf/benchmarks/energy_bias_resolution.py @@ -0,0 +1,51 @@ +from astropy.table import Table +import numpy as np +from scipy.stats import norm + +from ..binning import calculate_bin_indices + + +NORM_UPPER_SIGMA = norm(0, 1).cdf(1) +NORM_LOWER_SIGMA = norm(0, 1).cdf(-1) + + +def resolution_abelardo(rel_error): + return np.percentile(np.abs(rel_error), 68) + + +def inter_quantile_distance(rel_error): + upper_sigma = np.percentile(rel_error, 100 * NORM_UPPER_SIGMA) + lower_sigma = np.percentile(rel_error, 100 * NORM_LOWER_SIGMA) + return 0.5 * (upper_sigma - lower_sigma) + + +def energy_bias_resolution( + events, + true_energy_bins, + bias_function=np.median, + resolution_function=inter_quantile_distance +): + + # create a table to make use of groupby operations + table = Table(events[['true_energy', 'reco_energy']]) + table['rel_error'] = (events['reco_energy'] / events['true_energy']) - 1 + + table['bin_index'] = calculate_bin_indices( + table['true_energy'].quantity, true_energy_bins + ) + + result = Table() + result['true_energy_low'] = true_energy_bins[:-1] + result['true_energy_high'] = true_energy_bins[1:] + result['true_energy_center'] = 0.5 * (true_energy_bins[:-1] + true_energy_bins[1:]) + + result['bias'] = np.nan + result['resolution'] = np.nan + + # use groupby operations to calculate the percentile in each bin + by_bin = table.group_by('bin_index') + + index = by_bin.groups.keys['bin_index'] + result['bias'][index] = by_bin['rel_error'].groups.aggregate(bias_function) + result['resolution'][index] = by_bin['rel_error'].groups.aggregate(resolution_function) + return result From 92d782313fef0df294dd64ec35a6f45b8a9cfa69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 18:23:28 +0200 Subject: [PATCH 035/105] Try running example script on travis --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index ff5d2a703..366390270 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,9 @@ matrix: - CONDA=true before_install: + - curl -Lo data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download + - unzip data.zip + - mv eventdisplay_dl2 data - 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 @@ -38,6 +41,7 @@ install: script: - pytest --cov=pyirf + - python examples/calculate_eventdisplay_irfs.py after_script: - if [[ "$CONDA" == "true" ]];then From 01d31dfc6ac84ca0fa83bc275d621f39fc8c6177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 18:29:38 +0200 Subject: [PATCH 036/105] Add curl options --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 366390270..936529027 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: - CONDA=true before_install: - - curl -Lo data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download + - curl -sSfL -o data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download - unzip data.zip - mv eventdisplay_dl2 data - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; From 4d5b9312f75ae94bdbdd9ffba419644438a23af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 19:47:15 +0200 Subject: [PATCH 037/105] Add psf and angular resolution Co-authored-by: Lukas Nickel --- examples/calculate_eventdisplay_irfs.py | 22 +++++++++- pyirf/benchmarks/__init__.py | 2 + pyirf/benchmarks/angular_resolution.py | 39 ++++++++++++++++++ pyirf/io/eventdisplay.py | 2 + pyirf/irf/__init__.py | 4 +- pyirf/irf/psf.py | 53 +++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 pyirf/benchmarks/angular_resolution.py diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 724d5172e..550757984 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -17,8 +17,7 @@ from pyirf.cuts import calculate_percentile_cut, evaluate_binned_cut from pyirf.sensitivity import calculate_sensitivity from pyirf.utils import calculate_theta -from pyirf.irf import point_like_effective_area, point_like_energy_dispersion -from pyirf.benchmarks import energy_bias_resolution +from pyirf.benchmarks import energy_bias_resolution, angular_resolution from pyirf.spectral import ( calculate_event_weights, @@ -29,6 +28,12 @@ ) from pyirf.cut_optimization import optimize_gh_cut +from pyirf.irf import ( + point_like_effective_area, + point_like_energy_dispersion, + psf_table, +) + log = logging.getLogger('pyirf') @@ -203,7 +208,20 @@ def main(): gammas[gammas['selected']], true_energy_bins, ) + ang_res = angular_resolution( + gammas[gammas['selected_gh']], + true_energy_bins, + ) + + psf = psf_table( + gammas[gammas['selected']], + true_energy_bins, + np.arange(0, 1, 5e-4) * u.deg, + [0, 0.1] * u.deg, + ) + hdus.append(fits.BinTableHDU(psf, name='PSF')) + 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) diff --git a/pyirf/benchmarks/__init__.py b/pyirf/benchmarks/__init__.py index d8f9edf65..a9892ae4c 100644 --- a/pyirf/benchmarks/__init__.py +++ b/pyirf/benchmarks/__init__.py @@ -1,6 +1,8 @@ from .energy_bias_resolution import energy_bias_resolution +from .angular_resolution import angular_resolution __all__ = [ 'energy_bias_resolution', + 'angular_resolution', ] diff --git a/pyirf/benchmarks/angular_resolution.py b/pyirf/benchmarks/angular_resolution.py new file mode 100644 index 000000000..06bc965c2 --- /dev/null +++ b/pyirf/benchmarks/angular_resolution.py @@ -0,0 +1,39 @@ +import numpy as np +from astropy.table import Table +from scipy.stats import norm +import astropy.units as u + +from ..binning import calculate_bin_indices + + +ONE_SIGMA_PERCENTILE = norm.cdf(1) - norm.cdf(-1) +print(f'{ONE_SIGMA_PERCENTILE:.2%}') + + +def angular_resolution( + events, + true_energy_bins, +): + + # create a table to make use of groupby operations + table = Table(events[['true_energy', 'theta']]) + + table['bin_index'] = calculate_bin_indices( + table['true_energy'].quantity, true_energy_bins + ) + + result = Table() + result['true_energy_low'] = true_energy_bins[:-1] + result['true_energy_high'] = true_energy_bins[1:] + result['true_energy_center'] = 0.5 * (true_energy_bins[:-1] + true_energy_bins[1:]) + + result['angular_resolution'] = np.nan * u.deg + + # use groupby operations to calculate the percentile in each bin + by_bin = table.group_by('bin_index') + + index = by_bin.groups.keys['bin_index'] + result['angular_resolution'][index] = by_bin['theta'].groups.aggregate( + lambda x: np.percentile(x, 100 * ONE_SIGMA_PERCENTILE) + ) + return result diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py index 0d08396f8..64ce44143 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -17,6 +17,8 @@ 'reco_energy': 'ENERGY', 'true_alt': 'MC_ALT', 'true_az': 'MC_AZ', + 'pointing_alt': 'MC_ALT', + 'pointing_az': 'MC_AZ', 'reco_alt': 'ALT', 'reco_az': 'AZ', 'gh_score': 'GH_MVA', diff --git a/pyirf/irf/__init__.py b/pyirf/irf/__init__.py index 576cd11a4..d0627c50f 100644 --- a/pyirf/irf/__init__.py +++ b/pyirf/irf/__init__.py @@ -1,8 +1,10 @@ from .effective_area import effective_area, point_like_effective_area from .energy_dispersion import point_like_energy_dispersion +from .psf import psf_table __all__ = [ 'effective_area', 'point_like_effective_area', - 'point_like_energy_dispersion' + 'point_like_energy_dispersion', + 'psf_table', ] diff --git a/pyirf/irf/psf.py b/pyirf/irf/psf.py index e69de29bb..9a63e99bb 100644 --- a/pyirf/irf/psf.py +++ b/pyirf/irf/psf.py @@ -0,0 +1,53 @@ +import numpy as np +import astropy.units as u +from astropy.table import QTable + +from astropy.coordinates.angle_utilities import angular_separation + + +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) + ''' + + source_fov_offset = angular_separation( + events['true_az'], events['true_alt'], + events['pointing_az'], events['pointing_alt'], + ) + + array = np.column_stack([ + events['true_energy'].to_value(u.TeV), + 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), + ] + ) + + solid_angle = np.diff( + 2 * np.pi + * (1 - np.cos(source_offset_bins.to_value(u.rad))), + ) * u.sr + + # ignore numpy zero division warning + with np.errstate(invalid='ignore'): + # normalize and replace nans with 0 + psf = np.nan_to_num(hist / hist.sum(axis=2) / solid_angle) + + result = QTable({ + 'true_energy_low': [true_energy_bins[:-1]], + 'true_energy_high': [true_energy_bins[1:]], + 'source_offset_low': [source_offset_bins[:-1]], + 'source_offset_high': [source_offset_bins[1:]], + 'fov_offset_low': [fov_offset_bins[:-1]], + 'fov_offset_high': [fov_offset_bins[1:]], + 'psf': [psf], + }) + + return result From 5713c22d3f2d691716dd0e7160979f4e46934c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 19:47:53 +0200 Subject: [PATCH 038/105] Update notebook --- notebooks/comparison_with_EventDisplay.ipynb | 532 ++++++------------- 1 file changed, 173 insertions(+), 359 deletions(-) diff --git a/notebooks/comparison_with_EventDisplay.ipynb b/notebooks/comparison_with_EventDisplay.ipynb index 73aabe053..3f4be1e3b 100644 --- a/notebooks/comparison_with_EventDisplay.ipynb +++ b/notebooks/comparison_with_EventDisplay.ipynb @@ -1,16 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Remove input cells at runtime (nbsphinx)\n", - "import IPython.core.display as d\n", - "d.display_html('', raw=True)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -24,11 +13,7 @@ "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", + "Compare IRF and Sensitivity as computed by pyirf and EventDisplay on the same DL2 results\n", "\n", "**Notes:**\n", "\n", @@ -42,9 +27,14 @@ "\n", "_EventDisplay_ DL2 data, https://forge.in2p3.fr/projects/cta_analysis-and-simulations/wiki/Eventdisplay_Prod3b_DL2_Lists\n", "\n", - "**TO-DOs:**\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", + "```" ] }, { @@ -72,42 +62,29 @@ }, { "cell_type": "code", - "execution_count": 1, + "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", - "import os" + "from astropy.table import QTable\n", + "\n", + "%matplotlib inline" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import astropy.units as u" - ] - }, - { - "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!" + "plt.rcParams['figure.figsize'] = (9, 6)" ] }, { @@ -139,7 +116,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "tags": [ "parameters" @@ -149,20 +126,10 @@ "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 = \"../../data/event_display_irfs/data/WPPhys201890925LongObs/\"\n", - "infile_EventDisplay = \"DESY.d20180113.V3.ID0_180degNIM2LST4MST4SST4SCMST4.prod3b-paranal20degs05b-NN.S.3HB9-FD.180000s.root\"\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_EventDisplay, infile_EventDisplay))" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# Contents of the ROOT file\n", - "# input_EventDisplay.keys()" + "irf_eventdisplay = uproot.open(os.path.join(indir, irf_file_event_display))" ] }, { @@ -187,7 +154,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The following is the current IRF + sensititivy output FITS format provided by this software." + "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'" ] }, { @@ -200,7 +178,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -209,152 +187,65 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# remove under/overflow bin\n", - "sensitivity = QTable.read('../sensitivity.fits.gz', hdu='SENSITIVITY')[1:-1]\n", - "sensitivity_step_2 = QTable.read('../sensitivity.fits.gz', hdu='SENSITIVITY_STEP_2')[1:-1]" + "# [1:-1] removes under/overflow bin\n", + "sensitivity = QTable.read(pyirf_file, hdu='SENSITIVITY')[1:-1]" ] }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "QTable length=21\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
reco_energy_lowreco_energy_centerreco_energy_highn_signaln_signal_weightedn_backgroundn_background_weightedrelative_sensitivityflux_sensitivity
TeVTeVTeV1 / (cm2 s TeV)
float64float64float64int64float64int64float64float64float64
0.0125892541179416750.0162709386338152350.019952623149688811294052.053463808505542992.5270484090060.240877728044806753.308887876558572e-07
0.01995262314968880.0257876998756862950.031622776601683793003991562.7694828028912870110.961290897830.0531325695835334342.1839677676726172e-08
0.031622776601683790.040870749982205510.0501187233627272281059199760.3410993994319988491.910109132850.016407202600624262.0179943490542266e-09
0.050118723362727220.064775773417577680.0794328234724281487327162217.56743659418721069.03328015140.0071469907447199552.6303202261989764e-10
0.079432823472428140.10266268232592240.12589254117941667148820206544.827269805487314021.2530220911430.0043507674332908274.7912769368599987e-11
0.125892541179416670.16270938633815230.1995262314968879159947165880.4769129249556700.8911879692170.00368412773856457771.2140039080035877e-11
0.19952623149688790.25787699875686280.31622776601683786357149130.405934001325227.093989494198470.00243726657375082252.4031918205848773e-12
0.31622776601683780.40870749982205490.5011872336272729131753854.34600845945514397.05608340457550.00286876151078318278.464081769258541e-13
0.5011872336272720.64775773417577650.794328234724280911014249009.42956253711613183.456478494859770.0022214189685543991.9611728881779492e-13
0.79432823472428091.02662682325922351.25892541179416629368631112.367732431274861.772658555855740.00220628436861759455.828367031206147e-14
1.25892541179416621.62709386338152151.99526231496887689826524432.00688285986314.2784969306958370.00164309636551475551.2988184218839263e-14
1.99526231496887682.57876998756862633.16227766016837610131519029.95516490307611.5654880720539950.00123462429106852582.9202512751664235e-15
3.1622776601683764.0870749982205475.0118723362727198831512502.44564402452732.82220312397112140.00209058216831906761.4796283591700434e-15
5.0118723362727196.4775773417577627.943282347242805687467324.81079526478440.87990858804550950.00296094848505348156.270703356119615e-16
7.94328234724280510.26626823259222812.58925411794165528824228.9695477217970.80988124676514420.0050791811631698033.218689685381609e-16
12.5892541179416516.2709386338152119.95262314968877371212228.42071432201250.26396481870324350.0087663630207754661.6622825886631686e-16
19.9526231496887725.78769987568626531.62277660168376264081189.0203963271342233.931439948966730.0467337662196003552.651649920643812e-16
31.6227766016837640.8707499822054550.11872336272714517073577.6697452813387228.2866548432793930.091472760821964861.5530205124073486e-16
50.11872336272714564.7757734175775579.4328234724279711443291.2577633045148113.2048529201420020.139415695295483377.082671084248521e-17
79.43282347242797102.66268232592222125.892541179416487394140.6052294790279214.673237658105790.314132977628681144.775281035142733e-17
125.89254117941648162.7093863381521199.52623149688768470066.60329778515734212.115744835056830.66923989854115913.0441582790076444e-17
" - ], - "text/plain": [ - "\n", - " reco_energy_low reco_energy_center ... flux_sensitivity \n", - " TeV TeV ... 1 / (cm2 s TeV) \n", - " float64 float64 ... float64 \n", - "-------------------- -------------------- ... ----------------------\n", - "0.012589254117941675 0.016270938633815235 ... 3.308887876558572e-07\n", - " 0.0199526231496888 0.025787699875686295 ... 2.1839677676726172e-08\n", - " 0.03162277660168379 0.04087074998220551 ... 2.0179943490542266e-09\n", - " 0.05011872336272722 0.06477577341757768 ... 2.6303202261989764e-10\n", - " 0.07943282347242814 0.1026626823259224 ... 4.7912769368599987e-11\n", - " 0.12589254117941667 0.1627093863381523 ... 1.2140039080035877e-11\n", - " 0.1995262314968879 0.2578769987568628 ... 2.4031918205848773e-12\n", - " 0.3162277660168378 0.4087074998220549 ... 8.464081769258541e-13\n", - " 0.501187233627272 0.6477577341757765 ... 1.9611728881779492e-13\n", - " 0.7943282347242809 1.0266268232592235 ... 5.828367031206147e-14\n", - " 1.2589254117941662 1.6270938633815215 ... 1.2988184218839263e-14\n", - " 1.9952623149688768 2.5787699875686263 ... 2.9202512751664235e-15\n", - " 3.162277660168376 4.087074998220547 ... 1.4796283591700434e-15\n", - " 5.011872336272719 6.477577341757762 ... 6.270703356119615e-16\n", - " 7.943282347242805 10.266268232592228 ... 3.218689685381609e-16\n", - " 12.58925411794165 16.27093863381521 ... 1.6622825886631686e-16\n", - " 19.95262314968877 25.787699875686265 ... 2.651649920643812e-16\n", - " 31.62277660168376 40.87074998220545 ... 1.5530205124073486e-16\n", - " 50.118723362727145 64.77577341757755 ... 7.082671084248521e-17\n", - " 79.43282347242797 102.66268232592222 ... 4.775281035142733e-17\n", - " 125.89254117941648 162.7093863381521 ... 3.0441582790076444e-17" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "sensitivity" ] }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAugAAAHkCAYAAABscNp2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAAA+OUlEQVR4nO3de5xcZZXv/+9KIxDpkCBg1AZNgHgF0qGDyCXSzRAmCCHKxcAAxwQIPxjACXNgDg7OMTMSYTwKGW6iIgRtTEAuDhcZuXUpCGgS0oQAckACnqAOCdqQjkGkWb8/dqXpdKq6aj+1q/qp1Of9eu0XXftZez+rk/VqVu/s/WxzdwEAAACIw4jhTgAAAADAO2jQAQAAgIjQoAMAAAARoUEHAAAAIkKDDgAAAESEBh0AAACIyFbDnUBsdtppJx83bpzWr1+v7bbbrmBM6FhsapVrVvOEnifNceXGloqrZJwaqt48jVBD9VQ/EjVUSSw1lKCGwmOpoUQt8i00x7Jly9a6+84FD3B3tgFbW1ubu7t3dXV5MaFjsalVrlnNE3qeNMeVG1sqrpJxaqh68zRCDdVT/bhTQ5XEUkMJaig8lhpK1CLfQnNIWupF+lFucQEAAAAiYs6bRCVJZjZd0vSWlpY5nZ2d6u3tVXNzc8HY0LHY1CrXrOYJPU+a48qNLRVXyTg1VL15GqGG6ql+JGqoklhqKEENhcdSQ4la5Ftojo6OjmXuPrngAcUurTfqxi0u8c7TCP8smCaHGFBD4bH803KCGgqPpYYS1FB4LDWUiPEWFx4SBQAA2IKYmVatWqU33nhjyLjRo0frmWeeyXy81HGxqXa+2267rcws1TE06AAAAFuQ7bbbTqNGjdK4ceOGbAzXrVunUaNGZT5e6rjYVDNfd9err76aepUYHhIFAADYgjQ1NWnHHXdMfdUW2TMz7bjjjmpqakp1HA06AADAFobmPB4hfxc06AAAAMhUU1OTWltb+7dLLrkk0/Pncjk98sgj/Z/nzZunlpYWtba2asKECTr66KP19NNP94+fdtppm3wu18KFC3X22WdnknMa3IMOAACATI0cOVLd3d1VO38ul1Nzc7MOOOCA/n3nnnuuzjvvPEnSTTfdpEMOOURPPvmkdt55Z1177bVVy6UauIIOAACAqrvnnnv0+c9/vv9zLpfT9OnTJUn33nuv9t9/f+2zzz467rjj1NvbK0kaN26cvvKVr2ifffbRXnvtpV//+td68cUXdc011+iyyy5Ta2urHnrooc3mmjlzpg477DD98Ic/lCS1t7dr6dKl6uvr06xZs7Tnnntqr7320mWXXdY/PnfuXB1wwAHac8899atf/Wqzc955553ab7/9NGnSJB166KH67//+b7399tuaMGGC1qxZI0l6++23tccee2jt2rUV/VnRoAMAACBTGzZs2OQWl5tuuklTp07VY489pvXr10tKrnLPnDlTa9eu1UUXXaT7779fjz/+uCZPnqxLL720/1w77bSTHn/8cZ155pn6xje+oXHjxumMM87Queeeq+7ubk2ZMqVgDvvss49+/etfb7Kvu7tbL7/8slauXKknn3xSs2fP7h9bv369HnnkEV199dU65ZRTNjvfQQcdpMcee0zLly/X8ccfr69//esaMWKETjrpJN14442SpPvvv18TJ07UTjvtVNGfH7e4AAAANJj2dqmvb6SGWlykr2+kClycLkuxW1ymTZumO++8U8cee6zuvvtuff3rX9fPfvYzPf300zrwwAMlSW+++ab233///mOOPvpoSVJbW5tuu+22snNI3gW0qd12200vvPCCzjnnHB1xxBE67LDD+n9hOOGEEyRJn/70p/X666+rp6dnk2NXr16tmTNn6ve//73efPNNjR8/XpJ0yimnaMaMGZo7d66uu+66TZr+UFxBBwAAQE3MnDlTN998sx588EHtu+++GjVqlNxdU6dOVXd3t7q7u/X000/re9/7Xv8x22yzjaTkwdO33nqr7LmWL1+uj33sY5vs22GHHfTEE0+ovb1dV111lU477bT+scGrrQz+fM455+jss8/Wk08+qW9/+9v9L4LaddddNXbsWD344IP65S9/qcMPP7zsHIvhCjoAAECDyeWkdes2lHgR0QZJ2b7Ap729Xaeeeqq++93vaubMmZKkT33qUzrrrLP0/PPPa4899tCf//xnrV69Wh/+8IeLnmfUqFF6/fXXi47feuutuvfee/XNb35zk/1r167V1ltvrWOOOUa77767Zs2a1T920003qaOjQw8//LBGjx6t0aNHb3Lsa6+9ppaWFknSDTfcsMnYaaedppNOOkknn3xy6jXPC+EKOgAAADI1+B70Cy64QFJyFfzII4/UPffcoyOPPFKStPPOO2vhwoU64YQTtPfee+tTn/rUZveODzZ9+nTdfvvtmzwkuvGh0QkTJqizs1MPPvigdt55502Oe/nll9Xe3q7W1lbNmjVLF198cf/YDjvsoAMOOEBnnHHGJlfwN5o3b56OO+44TZkyZbN7zI866ij19vZmcnuLxBV0AAAAZKyvr6/o2JVXXqkrr7xyk32HHHKIlixZslnsiy++2P/15MmTlcvlJEkf/vCHtWLFiv6xKVOmaN68eUXn3HicJD3++OObjK1bt06SdMwxx2zSsEvSrFmz+q+yz5gxQzNmzCh4/ieeeEITJ07URz/60aI5pEGDDgAAAAS65JJL9K1vfat/JZcs0KDH4vojaj/n+PNrPycAAEBkBl5hT+uCCy7ov4UnK9yDDgAAAESEK+ixmH137ees4LdFAAAAVAdX0AEAAICI0KADAAAAEaFBBwAAwLAaN26c1q5dKylZK721tVV77rmnpk+frp6eHknJkosjR47cZH31N998c5Pz5HI5jR49un/80EMPlZSsYf6Nb3xDUrJ0YktLi/7yl79Ikl599VWNGzduk/Ncdtll2nbbbfXaa69tcu6Na7dXGw06AAAAojFy5Eh1d3dr5cqVes973qOrrrqqf2z33XdXd3d3/7b11ltvdvyUKVP6x++///6CczQ1Nem6664rmsOiRYu077776vbbb6/8GwpAgw4AAIDMvPTSS/roRz+qL3zhC9p777117LHH6s9//rMeeOABfe5zn+uPu++++3T00UcPea79999fL7/8cuY5zp07V5dddpneeuutzcZ+85vfqLe3VxdddJEWLVqU+dzloEEHAABApp599lmdfvrpWrFihbbffntdffXVOuSQQ/TMM89ozZo1kqTrr79es2fPLnqOvr4+PfDAAzrqqKP69/3mN7/pv33lrLPOKnjcQw891B8zf/78gjEf/OAHddBBB+kHP/jBZmOLFi3SCSecoClTpujZZ5/VK6+8kuZbzwTLLAIAADSa64/QyL63pKbireDIvrek034adPpdd91VBx54oCTppJNO0uWXX67zzjtPJ598sjo7OzV79mw9+uij+v73v7/ZsRs2bFBra6tefPFFtbW1aerUqf1jG29xGcqUKVN01113lczxn//5n3XUUUfp4IMP3mT/4sWLdfvtt2vEiBE6+uij9aMf/ajoLwPVwhV0AAAAZMrMCn6ePXu2Ojs7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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "plt.figure(figsize=(12,8))\n", "\n", - "# Data\n", + "# Get data from event display file\n", "h = irf_eventdisplay[\"DiffSens\"]\n", - "\n", - "#x = np.asarray([(x_bin[1]+x_bin[0])/2. for x_bin in h.allbins[2:-1]])\n", "bins = 10**h.edges\n", "x = 0.5 * (bins[:-1] + bins[1:])\n", "width = np.diff(bins)\n", "y = h.values\n", "\n", - "# Plot function\n", "plt.errorbar(\n", " x,\n", " y, \n", " xerr=width/2,\n", " yerr=None,\n", " label=\"EventDisplay\",\n", - " ecolor = \"blue\",\n", " ls=''\n", ")\n", "\n", "unit = u.Unit('erg cm-2 s-1')\n", "\n", - "sensitivities = {\n", - " 'pyIRF FINAL': sensitivity,\n", - " # 'pyIRF after first theta cuts': sensitivity_step_2,\n", - "}\n", "\n", - "for label, sens in sensitivities.items():\n", - " e = sens['reco_energy_center']\n", - " s = (e**2 * sens['flux_sensitivity'])\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=(sens['reco_energy_high'] - sens['reco_energy_low']).to_value(u.TeV) / 2,\n", - " ls='',\n", - " label=label\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", @@ -369,40 +260,20 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Compare Theta Cuts" + "## Theta Cuts" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEOCAYAAACXX1DeAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAolklEQVR4nO3deXjU9bn38ffNJotAJXAsAoIsGlYjpGIUT2m1GgpoK0j0lFqCCrZHz9FWnkdbBR5K3eDAqT1YQWSwLdCIOy5IFTluVGVRlBhbF1A0LkAblKgB8n3+mCWTYWYyE2afz+u65iK/de78rsncfHdzziEiIhJJi3QHICIimU2JQkREolKiEBGRqJQoREQkKiUKERGJSolCRESiapXuAJKha9eurk+fPukOQ0Qka2zevHm3c65buGM5mSj69OnDpk2b0h2GiEjWMLOdkY6p6klERKJSohARkaiUKEREJKqcbKMQyVUHDhxg165dfPXVV+kORbJU27Zt6dmzJ61bt475GiUKkSyya9cuOnbsSJ8+fTCzdIcjWcY5x549e9i1axcnnHBCzNep6kkki3z11VcUFBQoSUizmBkFBQVxl0iVKESyjJKEHInmfH5U9RSifG05VXurIh4v7FKIp9STwohERNJLJYo4RUsiItLY97//ff75z3+GPbZ69WoGDhyImVFUVERRURFHH300J510EkVFRVxyySVs2LCBcePGNbpuypQp3HfffQCMHj06cH5RURETJ07kN7/5TWC7ZcuWgZ9vv/12AP7zP/+THj16UF9f32T8TzzxBMXFxQwcOJDCwkKuvfbaw2LwO/roo3n99dcD79elSxdOOOEEioqKOPvss6mvr+c//uM/GDJkCEOHDuVb3/oW7733XryPND2cczn3GjFihEuWKU9McVOemJK0+4tEU1lZme4Qjlh9fb07dOiQO/fcc9369esbHfv2t7/tXnnllcD2M88848aOHdvonJ/85Cdu9erVYc8P1aFDh0bbhw4dcr169XIjR450zzzzTNQ4X3/9dde3b1/35ptvOuecO3DggFu0aNFhMUR6r9BzVq5c6SZMmOAOHTrknHPugw8+cHv37o0aQ7KE+xwBm1yE71RVPcXJU+qhZGUJ5WvLVQUlGals8caYzquYXhL3vXfs2EFpaSkjR45k69atnHjiiZSXl7N06VIefPBBAP7y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19O/f392796d2IeXBXKqRJEtAo3aNEzvoQkDJVFi/U9Hcz9rX375JUVFRYHt66+/ngkTJjB9+nT2799Phw4dqKiooKysjN27dzN37lyeeuopOnTowK233sqCBQuYOXMm4J1yY8uWLdxxxx3Mnz+fpUuXcsUVV3D00UcHShBPP/30YTEMHz6cqqrGVbavvvoqH374YWCcR/DKevv37+fFF1/k2WefZerUqYeNBRk1ahR//etfMTOWLl3Kbbfdxn/9138xefJkVqxYwdVXX81TTz3FySefHNPkf7lGiSINAqO2PWPB97da2F1tFZIdIlU9lZaWsmbNGiZOnMhjjz3Gbbfdxv/+7/9SWVkZmJa8rq6OkpKGWQsuuOACwDuy+4EHHog5hnDjv/r27cu7777LVVddxdixYznnnHMCxy6++GIA/vVf/5V9+/Ydtjzrrl27KCsro7q6mrq6Ok444QQApk6dyvnnn8/VV1/NsmXLwq6zkQ+UKNIoeGoPtVVIoqSrVFpWVsaiRYvo0qUL3/rWt+jYsSPOOb73ve+xatWqsNccddRRgLeB/ODBgzG/19atWw9bg+KYY47htdde48knn2TRokXce++9LFu2DAg/NXqwq666ip///Oecd955bNiwgdmzZwPetTeOPfZY1q9fz0svvcSKFStijjGX5FQbRbYJ7gEFaquQ7DZ69Gi2bNnCXXfdFZiN9bTTTuOFF14ILNNZW1sbmLY7ko4dO/L5559HPH7//fezbt26QCnBb/fu3dTX1zNhwgR+/etfs2XLlsAx/5Kizz//PJ07dya0HbOmpoYePbyrHdxzzz2Njl122WVMnjyZSZMmNVpxLp/kVIkiG3o9BQvtAZXslclEEiG0jaK0tJRbbrmFli1bMm7cOJYvXx74su3WrRvLly/n4osvDsxnNHfuXE488cSI9x8/fjwTJ07k4Ycf5ne/+x0ACxcu5E9/+hP79+9nyJAhrF+/ntB1Zz788EPKy8upr68H4OabGyakPuaYYzj99NPZt29foJQRbPbs2Vx44YX06NGD0047jffeey9w7LzzzqO8vDxvq51Acz2lnX+yQPBO9eEvUahRW8LRXE/xGz16NPPnz49pudRwNm3axDXXXBO2B1a20lxPWSa4B5S/dKHR2iKZ4ZZbbmHChAmNSif5SIkizULXrdBobZHE2rBhQ7NLE9dd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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from astropy.table import Table\n", "\n", - "for key in ('THETA_CUTS', 'THETA_CUTS_OPT'):\n", - " theta_cut = Table.read('../sensitivity.fits.gz', hdu=key)\n", "\n", - " plt.errorbar(\n", - " theta_cut['low'],\n", - " theta_cut['cut'].quantity.to_value(u.deg)**2,\n", - " xerr=theta_cut['high'] - theta_cut['low'],\n", - " ls='',\n", - " label='pyirf' + key,\n", - " )\n", + "theta_cut = Table.read(pyirf_file, hdu='THETA_CUTS_OPT')[1:-1]\n", + "\n", "\n", "theta_cut_ed = irf_eventdisplay['ThetaCut;1']\n", "plt.errorbar(\n", @@ -413,6 +284,14 @@ " label='EventDisplay',\n", ")\n", "\n", + "plt.errorbar(\n", + " 0.5 * (theta_cut['low'] + theta_cut['high']),\n", + " theta_cut['cut'].quantity.to_value(u.deg)**2,\n", + " xerr=0.5 * (theta_cut['high'] - theta_cut['low']),\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", @@ -423,38 +302,19 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "from astropy.coordinates import Angle\n", - "from gammapy.maps import MapAxis\n", - "from gammapy.irf import EffectiveAreaTable2D, EnergyDispersion2D, BgRateTable" + "### IRFs\n", + "[back to top](#Table-of-contents)" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "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?" + "#### Effective area\n", + "[back to top](#Table-of-contents)" ] }, { @@ -463,49 +323,48 @@ "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", + "# Data from EventDisplay\n", + "h = irf_eventdisplay[\"EffectiveAreaEtrue\"]\n", "\n", - "psf_pyirf = hdul_pyirf[2]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Energy dispersion\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", - "# here I open manually, but I use gammapy.irf.EnergyDispersion2D to get the energy resolution\n", + "plt.errorbar(x, y, xerr=xerr, yerr=yerr, ls='', label=\"EventDisplay\")\n", "\n", - "eDisp_pyirf = hdul_pyirf[3]\n", + "for name in ('', '_NO_CUTS', '_ONLY_GH', '_ONLY_THETA'):\n", "\n", - "eDisp_pyirf_ENERG_LO = eDisp_pyirf.data[0][0]\n", - "eDisp_pyirf_ENERG_HI = eDisp_pyirf.data[0][1]\n", + " area = QTable.read('../sensitivity.fits.gz', hdu='EFFECTIVE_AREA' + name)[1:-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", + " plt.errorbar(\n", + " 0.5 * (area['true_energy_low'] + area['true_energy_high']).to_value(u.TeV),\n", + " area['effective_area'].to_value(u.m**2),\n", + " xerr=0.5 * (area['true_energy_high'] - area['true_energy_low']).to_value(u.TeV),\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", "\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)" + "plt.legend(loc=4)\n", + "plt.show()" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "# Background rate\n", - "\n", - "background_pyirf = hdul_pyirf[4]" + "#### Angular resolution\n", + "[back to top](#Table-of-contents)" ] }, { @@ -514,88 +373,87 @@ "metadata": {}, "outputs": [], "source": [ - "# Differential sensitivity\n", + "# 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", - "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", + "# pyirf\n", "\n", - "- the configuration in config.yaml,\n", - "- the specific cuts optiization performed by EventDisplay (which has not yet been replicated in pyirf)\n", + "ang_res = QTable.read(pyirf_file, hdu='ANGULAR_RESOLUTION')[1:-1]\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", + "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", - "A more detailed and complete version of this notebook will be provided with an official DL3 benchmarking." + "\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", + "\n", + "\n", + "\n", + "plt.legend(loc=\"best\")\n", + "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### IRFs\n", - "[back to top](#Table-of-contents)" + "### Energy Dispersion" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "#### Effective area\n", - "[back to top](#Table-of-contents)" + "edisp = QTable.read(pyirf_file, hdu='EDISP')[0]" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": false + }, "outputs": [], "source": [ - "zoom = 2\n", - "plt.figure(figsize=(zoom*4,zoom*3))\n", + "edisp\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", + "e_bins = edisp['true_energy_low'][1:]\n", + "migra_bins = edisp['migration_low'][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", + "plt.title('pyirf')\n", + "plt.pcolormesh(e_bins.to_value(u.TeV), migra_bins, edisp['energy_dispersion'][1:-1, 1:-1, 0].T, cmap='inferno')\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", + "plt.xscale('log')\n", + "plt.yscale('log')\n", + "plt.colorbar(label='PDF Value')\n", "\n", - "plt.legend(loc=4)\n", - "plt.show()" + "plt.xlabel(r'$E_\\mathrm{True} / \\mathrm{TeV}$')\n", + "plt.ylabel(r'$E_\\mathrm{Reco} / E_\\mathrm{True}$')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Angular resolution\n", + "#### Energy resolution\n", "[back to top](#Table-of-contents)" ] }, @@ -605,42 +463,7 @@ "metadata": {}, "outputs": [], "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)" + "bias_resolution = QTable.read(pyirf_file, hdu='ENERGY_BIAS_RESOLUTION')[1:-1]\n" ] }, { @@ -649,28 +472,29 @@ "metadata": {}, "outputs": [], "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", + "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", + "# 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.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.xlabel(r\"$E_\\mathrm{True} / \\mathrm{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()" @@ -690,31 +514,21 @@ "metadata": {}, "outputs": [], "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", + "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 = h.variances\n", + "yerr = np.sqrt(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.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.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()" ] From 07ec860a23e917b80319fa33a97db50e11cbb476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 19:49:45 +0200 Subject: [PATCH 039/105] Execute notebook on travis --- .travis.yml | 4 +++- environment.yml | 1 + setup.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 936529027..0b71ff56c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,12 +36,14 @@ install: - conda env create -f environment.yml - conda activate pyirf-dev - pip install travis-sphinx codecov pytest-cov - - pip install . + - pip install .[all] - python --version script: - pytest --cov=pyirf - python examples/calculate_eventdisplay_irfs.py + - cd notebooks + - jupyter nbconvert --to notebook --execute comparison_with_EventDisplay.ipynb after_script: - if [[ "$CONDA" == "true" ]];then diff --git a/environment.yml b/environment.yml index 001306e88..eee9fbc98 100644 --- a/environment.yml +++ b/environment.yml @@ -23,3 +23,4 @@ dependencies: - nbsphinx - gammapy~=0.8.0 - sphinx_automodapi + - uproot diff --git a/setup.py b/setup.py index 393b13f0e..05b115db5 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ ], 'tests': [ 'pytest', + 'uproot', ], } From 693fbde2c2c6b21c15be669171aa13e276b5b39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Wed, 23 Sep 2020 20:44:07 +0200 Subject: [PATCH 040/105] Remove savefigs from notebook --- notebooks/comparison_with_EventDisplay.ipynb | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/notebooks/comparison_with_EventDisplay.ipynb b/notebooks/comparison_with_EventDisplay.ipynb index 3f4be1e3b..6373a11a9 100644 --- a/notebooks/comparison_with_EventDisplay.ipynb +++ b/notebooks/comparison_with_EventDisplay.ipynb @@ -251,9 +251,7 @@ "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", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.savefig('/home/maxnoe/pyirf_sensitivity.png', dpi=300)" + "plt.legend()" ] }, { @@ -296,9 +294,7 @@ "plt.ylabel('θ²-cut / deg²')\n", "plt.xlabel(r'$E_\\mathrm{reco} / \\mathrm{TeV}$')\n", "plt.xscale('log')\n", - "plt.yscale('log')\n", - "\n", - "plt.savefig('/home/maxnoe/theta2_cut.png')" + "plt.yscale('log')" ] }, { @@ -336,7 +332,7 @@ "\n", "for name in ('', '_NO_CUTS', '_ONLY_GH', '_ONLY_THETA'):\n", "\n", - " area = QTable.read('../sensitivity.fits.gz', hdu='EFFECTIVE_AREA' + name)[1:-1]\n", + " area = QTable.read(pyirf_file, hdu='EFFECTIVE_AREA' + name)[1:-1]\n", "\n", " \n", " plt.errorbar(\n", @@ -353,9 +349,6 @@ "plt.xlabel(\"True energy / TeV\")\n", "plt.ylabel(\"Effective collection area / m²\")\n", "plt.grid(which=\"both\")\n", - "\n", - "\n", - "plt.legend(loc=4)\n", "plt.show()" ] }, @@ -403,10 +396,7 @@ "plt.grid(which=\"both\")\n", "\n", "\n", - "\n", - "\n", - "plt.legend(loc=\"best\")\n", - "plt.show()" + "plt.legend(loc=\"best\")" ] }, { From 9ba7201ed0667cefe6c0bc1077391b62d9001835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 11:12:18 +0200 Subject: [PATCH 041/105] Fix psf normalization --- examples/calculate_eventdisplay_irfs.py | 2 +- notebooks/comparison_with_EventDisplay.ipynb | 76 ++++++++++++++++++++ pyirf/irf/psf.py | 38 +++++++--- 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 550757984..244b39be0 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -216,7 +216,7 @@ def main(): psf = psf_table( gammas[gammas['selected']], true_energy_bins, - np.arange(0, 1, 5e-4) * u.deg, + np.arange(0, 1 + 1e-4, 1e-3) * u.deg, [0, 0.1] * u.deg, ) diff --git a/notebooks/comparison_with_EventDisplay.ipynb b/notebooks/comparison_with_EventDisplay.ipynb index 6373a11a9..336beb19d 100644 --- a/notebooks/comparison_with_EventDisplay.ipynb +++ b/notebooks/comparison_with_EventDisplay.ipynb @@ -352,6 +352,82 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PSF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "psf_table = QTable.read(pyirf_file, hdu='PSF')[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# select the only fov offset bin\n", + "\n", + "psf = psf_table['psf'][:, 0, :].to_value(1 / u.sr)\n", + "\n", + "offset_bins = np.append(psf_table['source_offset_low'], psf_table['source_offset_high'][-1])\n", + "phi_bins = np.linspace(0, 2 * np.pi, 1000)\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", + "x = r * np.cos(phi)\n", + "y = r * np.sin(phi)\n", + "\n", + "\n", + "# look at a single energy bin\n", + "# repeat values for each phi bin\n", + "image = np.tile(psf[10], (len(phi_bins) - 1, 1))\n", + "plt.pcolormesh(x, y, image)\n", + "plt.xlim(-0.25, 0.25)\n", + "plt.ylim(-0.25, 0.25)\n", + "plt.xlabel('Distance from source x')\n", + "plt.ylabel('Distance from source y')\n", + "plt.gca().set_aspect(1)" + ] + }, + { + "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 [5, 10, 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')" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/pyirf/irf/psf.py b/pyirf/irf/psf.py index 9a63e99bb..4b06d763e 100644 --- a/pyirf/irf/psf.py +++ b/pyirf/irf/psf.py @@ -30,6 +30,23 @@ def psf_table(events, true_energy_bins, source_offset_bins, fov_offset_bins): ] ) + psf = _normalize_psf(hist, source_offset_bins) + + result = QTable({ + 'true_energy_low': u.Quantity(true_energy_bins[:-1], ndmin=2), + 'true_energy_high': u.Quantity(true_energy_bins[1:], ndmin=2), + 'source_offset_low': u.Quantity(source_offset_bins[:-1], ndmin=2), + 'source_offset_high': u.Quantity(source_offset_bins[1:], ndmin=2), + 'fov_offset_low': u.Quantity(fov_offset_bins[:-1], ndmin=2), + 'fov_offset_high': u.Quantity(fov_offset_bins[1:], ndmin=2), + 'psf': [psf], + }) + + return result + + +def _normalize_psf(hist, source_offset_bins): + '''Normalize the psf histogram to a probability densitity over solid angle''' solid_angle = np.diff( 2 * np.pi * (1 - np.cos(source_offset_bins.to_value(u.rad))), @@ -37,17 +54,16 @@ def psf_table(events, true_energy_bins, source_offset_bins, fov_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 / hist.sum(axis=2) / solid_angle) + psf = np.nan_to_num(hist / n_events) - result = QTable({ - 'true_energy_low': [true_energy_bins[:-1]], - 'true_energy_high': [true_energy_bins[1:]], - 'source_offset_low': [source_offset_bins[:-1]], - 'source_offset_high': [source_offset_bins[1:]], - 'fov_offset_low': [fov_offset_bins[:-1]], - 'fov_offset_high': [fov_offset_bins[1:]], - 'psf': [psf], - }) + # swap axes back to order required by GADF + psf = np.swapaxes(psf, 0, 2) - return result + return psf / solid_angle From 786072e9dee3d6e6d165562eaa2160838d4a6e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 11:48:48 +0200 Subject: [PATCH 042/105] Fix test, remove unused code --- .travis.yml | 2 +- environment.yml | 4 ++- pyirf/binning.py | 6 +++- pyirf/cuts.py | 54 ------------------------------------ pyirf/tests/__init__.py | 0 pyirf/tests/test_cuts.py | 58 +++++++++------------------------------ pyirf/tests/test_utils.py | 2 +- setup.py | 1 + 8 files changed, 24 insertions(+), 103 deletions(-) create mode 100644 pyirf/tests/__init__.py diff --git a/.travis.yml b/.travis.yml index 0b71ff56c..988f49eff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,7 @@ install: - python --version script: - - pytest --cov=pyirf + - pytest --cov=pyirf --cov-report=xml - python examples/calculate_eventdisplay_irfs.py - cd notebooks - jupyter nbconvert --to notebook --execute comparison_with_EventDisplay.ipynb diff --git a/environment.yml b/environment.yml index eee9fbc98..e833e8512 100644 --- a/environment.yml +++ b/environment.yml @@ -10,9 +10,11 @@ dependencies: - ipython - jupyter - matplotlib - - pytest - scipy - setuptools + # tests + - pytest + - pytest-cov # docs - numpydoc - sphinx diff --git a/pyirf/binning.py b/pyirf/binning.py index 7fadaa928..ae432f30d 100644 --- a/pyirf/binning.py +++ b/pyirf/binning.py @@ -87,7 +87,11 @@ def calculate_bin_indices(data, bins): Indices of the histogram bin the values in data belong to ''' - if hasattr(data, 'unit') or hasattr(bins, 'unit'): + 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) diff --git a/pyirf/cuts.py b/pyirf/cuts.py index c0ee059bc..6cc86cf4c 100644 --- a/pyirf/cuts.py +++ b/pyirf/cuts.py @@ -1,10 +1,7 @@ -import operator - import numpy as np from astropy.table import Table from .binning import calculate_bin_indices -from .utils import is_scalar def calculate_percentile_cut( @@ -96,54 +93,3 @@ def evaluate_binned_cut(values, bin_values, cut_table, op): 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) - - -def is_selected(events, cut_definition, bin_index=None): - ''' - Retun a boolean mask, if the given ``events`` survive the cuts defined - in ``cut_definition``. - This function supports bin-wise cuts when given the ``bin_index`` argument. - - Parameters - ---------- - events: ``~astropy.table.QTable`` - events table - cut_definition: dict - A dict describing the cuts to make. - The keys are column names in ``events`` to which a cut should be applied. - The values must be dictionaries with the key ``'operator'`` containing the - name of the binary comparison operator to use and the key ``'cut_values'``, - which is either a single number of quantity or a Quantity or an array - containing the cut value for each bin. - bin_index: np.ndarray[int] - Bin index for each event in the ``events`` table, only needed if - bin-wise cut values are used. - - Returns - ------- - selected: np.ndarray[bool] - Boolean mask if an event survived the specified cuts. - ''' - mask = np.ones(len(events), dtype=np.bool) - - for key, definition in cut_definition.items(): - - op = getattr(operator, definition['operator']) - - # for a single number, just use the value - - if is_scalar(definition['cut_values']): - cut_value = definition['cut_values'] - - # if it is an array, it is per bin, so we get the correct - # cut value for each event - else: - if bin_index is None: - raise ValueError( - 'You need to provide `bin_index` if cut_values are per bin' - ) - cut_value = np.asanyarray(definition['cut_values'])[bin_index] - - mask &= op(events[key], cut_value) - - return mask 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_cuts.py b/pyirf/tests/test_cuts.py index 4398ab2dc..0a5b2fe35 100644 --- a/pyirf/tests/test_cuts.py +++ b/pyirf/tests/test_cuts.py @@ -46,7 +46,7 @@ def test_calculate_percentile_cuts(): assert np.all(cuts['cut'].quantity == [1.0, 5.0] * u.deg) -def evaluate_binned_cut(): +def test_evaluate_binned_cut(): from pyirf.cuts import evaluate_binned_cut cuts = Table({ @@ -63,49 +63,17 @@ def evaluate_binned_cut(): ) 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, + }) -def test_is_selected(events): - from pyirf.cuts import is_selected - - cut_definition = { - 'theta': { - 'operator': 'le', - 'cut_values': [0.05, 0.15, 0.25] * u.deg, - }, - 'gh_score': { - 'operator': 'ge', - 'cut_values': np.array([0.0, 0.1, 0.5]), - } - } - - # if you make no cuts, all events are selected - assert np.all(is_selected(events, {}, bin_index=events['bin_reco_energy']) == True) # noqa - - selected = is_selected( - events, cut_definition, bin_index=events['bin_reco_energy'] - ) - - assert selected.dtype == np.bool - assert np.all(selected == [False, False, False, False, True, False]) - - -def test_is_selected_single_numbers(events): - from pyirf.cuts import is_selected - - cut_definition = { - 'theta': { - 'operator': 'le', - 'cut_values': 0.05 * u.deg, - }, - 'gh_score': { - 'operator': 'ge', - 'cut_values': 0.5, - } - } - - selected = is_selected( - events, cut_definition, bin_index=events['bin_reco_energy'] + 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 selected.dtype == np.bool - assert np.all(selected == [False, False, False, False, True, False]) + assert np.all(survived == [True, True, False, True, False, False]) diff --git a/pyirf/tests/test_utils.py b/pyirf/tests/test_utils.py index c75e297e5..64d3a3628 100644 --- a/pyirf/tests/test_utils.py +++ b/pyirf/tests/test_utils.py @@ -3,7 +3,7 @@ def test_is_scalar(): - from pyirf.cuts import is_scalar + from pyirf.utils import is_scalar assert is_scalar(1.0) assert is_scalar(5 * u.m) diff --git a/setup.py b/setup.py index 05b115db5..6d8cb489f 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ ], 'tests': [ 'pytest', + 'pytest-cov', 'uproot', ], } From 36a18ace0383724ba80533a240c0a7db7d96ff61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 14:01:34 +0200 Subject: [PATCH 043/105] Add test for psf --- pyirf/irf/psf.py | 8 +++--- pyirf/irf/tests/test_psf.py | 50 +++++++++++++++++++++++++++++++++++++ pyirf/tests/test_utils.py | 13 ++++++++++ pyirf/utils.py | 5 ++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 pyirf/irf/tests/test_psf.py diff --git a/pyirf/irf/psf.py b/pyirf/irf/psf.py index 4b06d763e..540a25828 100644 --- a/pyirf/irf/psf.py +++ b/pyirf/irf/psf.py @@ -5,6 +5,9 @@ from astropy.coordinates.angle_utilities import angular_separation +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) @@ -47,10 +50,7 @@ def psf_table(events, true_energy_bins, source_offset_bins, fov_offset_bins): def _normalize_psf(hist, source_offset_bins): '''Normalize the psf histogram to a probability densitity over solid angle''' - solid_angle = np.diff( - 2 * np.pi - * (1 - np.cos(source_offset_bins.to_value(u.rad))), - ) * u.sr + solid_angle = np.diff(cone_solid_angle(source_offset_bins)) # ignore numpy zero division warning with np.errstate(invalid='ignore'): diff --git a/pyirf/irf/tests/test_psf.py b/pyirf/irf/tests/test_psf.py new file mode 100644 index 000000000..cdff9de2b --- /dev/null +++ b/pyirf/irf/tests/test_psf.py @@ -0,0 +1,50 @@ +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 + + N = 1000 + + np.random.seed() + 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, + 'pointing_az': np.zeros(2 * N) * u.deg, + 'pointing_alt': np.full(2 * N, 70) * u.deg, + 'true_az': np.zeros(2 * N) * u.deg, + 'true_alt': np.full(2 * N, 70) * 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)[0] + + # 2 energy bins, 1 fov bin, 200 source distance bins + assert psf['psf'].shape == (2, 1, 200) + assert psf['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['psf'] * bin_solid_angle, axis=2), 1.0) + + cumulated = np.cumsum(psf['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/tests/test_utils.py b/pyirf/tests/test_utils.py index 64d3a3628..6064673d9 100644 --- a/pyirf/tests/test_utils.py +++ b/pyirf/tests/test_utils.py @@ -13,3 +13,16 @@ def test_is_scalar(): 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 index 01bb81281..0b1f050bb 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -36,3 +36,8 @@ def check_histograms(hist1, hist2, key='reco_energy'): 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 with opening angle ``angle``.''' + return 2 * np.pi * (1 - np.cos(angle)) * u.sr From d53440d3f9772d5b7ddee62b90645491ed6e1936 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Thu, 24 Sep 2020 15:13:50 +0200 Subject: [PATCH 044/105] Update notebook and docs --- .gitignore | 4 + .travis.yml | 4 +- docs/conf.py | 2 +- .../comparison_with_EventDisplay.ipynb | 739 ----------- docs/contribute/index.rst | 4 +- .../comparison_with_EventDisplay.ipynb | 150 +-- notebooks/irf_maker.ipynb | 1169 ----------------- 7 files changed, 76 insertions(+), 1996 deletions(-) delete mode 100644 docs/contribute/comparison_with_EventDisplay.ipynb rename {notebooks => docs/contribute/notebooks}/comparison_with_EventDisplay.ipynb (92%) delete mode 100644 notebooks/irf_maker.ipynb diff --git a/.gitignore b/.gitignore index 7899cde33..3da2895e8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,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/.travis.yml b/.travis.yml index 988f49eff..530634ede 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,13 +42,13 @@ install: script: - pytest --cov=pyirf --cov-report=xml - python examples/calculate_eventdisplay_irfs.py - - cd notebooks - - jupyter nbconvert --to notebook --execute comparison_with_EventDisplay.ipynb after_script: - if [[ "$CONDA" == "true" ]];then conda deactivate fi + - python setup.py build_sphinx after_success: - codecov + - travis-sphinx -v --outdir=docbuild deploy diff --git a/docs/conf.py b/docs/conf.py index b17916220..5c8fda46f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,7 +57,7 @@ ] # nbsphinx -nbsphinx_execute = "never" +# nbsphinx_execute = "never" numpydoc_show_class_members = False autosummary_generate = True 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/Xhg0b9PnPf14fffSRWlpaNHr0aLl7Zz87d+7s/HnffffVO++80/n+448/1qBBgzo/77777tNjjz2mH/zgB53fxd69e7V161YNGjRIZ5xxhg499FBdeeWVnfW/+c1v1NDQoNWrV2vIkCEaMGCAdu3apd27d2vHjh16//33NXToUO3YsUN33XVXt7/fJZdcoksuuUQXXXSRPvroo17fxa5du3L77yBdbtxKfpHrvm/HkUu9MP0wP9kxP7mXFVquue7vW7/dR3/vYT/yxod84twn/L7123sdk2tO+AEDBnhdXZ2PHTvWx44d6zfeeGNn3dVXX+3777+/79y5s7PvJ554whsaGryurs7r6ur8/vvvd3f3I4880l9//XV3d1+3bp2fcsop7u6+ZcuWzmOffPJJ//73v+8jRozwsWPH+lFHHeVf/epX/cUXX+z8zFNOOcVjsZhv2LDB6+vrO8e1dOnSzvqbbrrJJ0yY4Mcdd5yvWbPG3d0XLlzoV199tbu7L1++3EeNGuUnn3yyf/e73+0ci7v77t27fciQIf7SSy+l/D5yzXUfqRU9AKC0ptXXaPHabZIU2un6jtVuqsfrfv7zn3fe7d7h1FNP1bp163odu3Xr1s5Vc0NDQ+equLa2Vhs3buz8jC9/+cu65ZZbOtv1/OyO7W6HDBmi9evXdzuuwwUXXKC5c+d2+/zLLrtMl112mSTpvPPO03nnnZfy7/vCCy9o7NixOvroo1PW54pADwAIVSGux/cX8+bN05133tl5530YCPQAAISoL/dR3HTTTbrpppvCG4y46x4AgEgj0AMAMvIUz5CjNPKZCwI9ACCtwYMH69133yXYlwF317vvvqvBgwfn1I5r9ACAtA477DBt375d8Xg85wCTzq5du3LuK2ibbMdlqk9Xl6o8aFnYBg8erMMOOyynNgR6AEBagwYN0qhRoxSLxVRfXx9Kn/n0FbRNtuMy1aerS1UetKwccOoeAIAII9ADABBhFqUbLLrkur+iY9/gYmttbVV1dXVJ+gnaJttxmerT1QUtD+v7yRfzk7mc+Smf+QlaVixhfjbzE77Jkyc/7+4NKSvT5cat5Be57vt2HLnUC9MP85Md85N7WbGE+dnMT/iUIdc9p+4BAIgwAj0AABFGoAcAIMII9AAARBiBHgCACCPQAwAQYQR6AAAijFz3/c0jN2ncy3+UXh+a9pBx8Xja+uH7nSCpsTBjAwCEjkCP4N5q1iGD46UeBQAgBwT6/uasedpQFVNjY2PaQzbE0tQvPEeKE+gBoJJwjR4AgAgj0AMAEGEEegAAIoxADwBAhLEffciivJ/2uKab1d7eruaGeYHbsN957m3Yj575yaWsWNiPvrznh/3oiyjS+2kvONvfv31iTm3Y7zz3NuxHX5p+ym1+ym2/c/ajz6+sWMR+9AAA9E8EegAAIoxADwBAhBHoAQCIMAI9AAARRqAHACDC2NQGOalufT2xuU0P6ba27VZeN13SqAKPEADQFSt6BFc3Xa3VeQbqt5ql5qXhjgcAkBUregTXMFMbWkel3MI23da2neUpzgIAAAqPFT0AABFGoAcAIMII9AAARBiBHgCACCPQAwAQYQR6AAAijEAPAECEWWK/+mgws6mSpg4fPvyKRYsWlWQMra2tqq6uLkk/QdtkOy5Tfbq6bOXjmm6WJD01ek4o30++mJ/M5WF9P/lifnIvK5YwP5v5Cd/kyZOfd/eGlJXuHrlXbW2tl8qqVatK1k/QNtmOy1Sfri5r+YKz3RecHdr3ky/mJ3M589P348Kan6BlxRLmZzM/4ZP0nKeJiZy6BwAgwgj0AABEGLnuUTxvNWtc/OaUu9xlVTddapgZ/pgAIOII9CiOuumJP+Px3Nu+1Zz4k0APADkj0KM4GmYmdr9Ls8tdRux8BwB5I9AHsLypRT9+bIvejLdpxNAqzZ4yRtPqa0o9LAAAsiLQZ7G8qUVzljWrbU+7JKkl3qY5yxKnkgn2AIBy168D/Yz5q7Me07Qtrt3te7uVte1p1w1LN2rx2m29jr9qTGjDAwCgz3i8LoueQT5bOQAA5aRfr+iXzJqQ9ZhJ81aqJd7Wq7xmaFXK9rFYLIyhAQAQClb0WcyeMkZVgwZ2K6saNFCzp3COHgBQ/vr1ij6IjhvuuOseAFCJCPQBTKuvIbADACoSp+4BAIgwAj0AABHGqfsSINMeAKBYCPRFRqY9AEAxEehDNndNm+7ckj7jXtBMe/H4p/0Eed4fAIBUuEZfZGTaAwAUEyv6kM05qUqNjelX4EEz7cVisYz99DtvNee/XW3ddPayB9BvsaIvMjLt5aFuunRoXX5t32qWmpeGOx4AqCCs6IuMTHt5aJiZ/4o837MAABAR5u6lHkNozGyqpKnDhw+/YtGiRSUZQ2trq6qrq0vST9A22Y7LVJ+uLmh5WN9PUOOabpYkbai/LdTPZ34Kg/nJvaxYwvxs5id8kydPft7dG1JWunvkXrW1tV4qq1atKlk/QdtkOy5Tfbq6oOVhfT+BLTg78Qr585mfwmB+ci8rljA/m/kJn6TnPE1M5Bo9AAARlvYavZmND9B+j7s3hzgeAAAQokw34/1B0jpJluGYUZJGhjkgAAAQnkyBfp27n5qpsZmtDHk8AAAgRGmv0WcL8kGPAQAApZP1Ofo01+o/kPSGu38S/pAAAEBYgiTMuUPSeEkblbhef3zy52FmdqW7/76A4wMAAH0Q5PG6rZLq3b3B3U+UVC9pk6TTJP2ogGMDAAB9FCTQH+3uL3a8cffNSgT+1wo3LAAAEIYgp+63mNmdkn6XfD9D0p/MbD9Jewo2MgAA0GdBVvSXSXpV0nWSrpf0WrJsj6TJhRoYAADou6wrendvM7M7JD3k7lt6VLcWZlhAiLrsZT8uHpdeHxqsHfvYA4iArCt6MztX0gZJjybfjzOzBwo9MCAU+e5lzz72ACIiyDX670v6oqSYJLn7BjMbWbghASHqsZf9hlhMjY2N2duxjz2AiAgS6D9x9w/MMqW8R6ktb2rRrbGP9N6jKzRiaJVmTxmjafU1pR4WAKDEggT6TWb2t5IGmtloSd+R9Exhh4VcLG9q0ZxlzWrb45Kklnib5ixLbCpIsAeA/i1IoL9W0s2SPpa0WNJjkm4t5KDQ3Yz5qzPWN22La3f73m5lbXvadcPSjVq8dluv468aE+rwAABlLMhd9x8pEehvLvxwkI+eQT5bOQCg/0gb6M3sQUmert7dzy3IiNDLklkTMtZPmrdSLfG2XuU1Q6tSto3FYmENDQBQ5jI9XvcTSf8o6XVJbZL+NflqVSLXPcrE7CljVDVoYLeyqkEDNXsK5+gBoL9Lu6J39z9Ikpnd6u5f6VL1oJk9WfCRIbCOG+5uvf8FvbfLueseANApyM14B5vZ5zo2sTGzUZIOLuywkKtp9TUa+sErwZ4RBwD0G0EC/fWSYmbWsVvdSEnfKtiIAABAaILcdf9o8vn5o5NFL7v7x4UdFgAACEPam/HMbHzHz+7+sbu/kHx9nOoYAABQfjKt6BeaWaOkTLlv75ZUH+qIAABAaDIF+gMlPa/Mgf7tcIcDlJEu29v2lHW7W7a4BVAmMj1eN7KI4wDKS930/Nu+ldhngEAPoBwEuese6H96bG/bU8btbtniFkAZyZQZDwAAVDgCPQAAERbo1L2Z1Ug6suvx7k4aXAAAypy5p92gLnGA2f8vaYakzZLak8VejrvXmdlUSVOHDx9+xaJFi0oyhtbWVlVXV5ekn6Btsh2XqT5dXdDysL6ffBVjfsY1JXZ03lB/W16fzfzw+5NLWbGE+dnMT/gmT578vLs3pKx094wvSVsk7ZftuHJ61dbWeqmsWrWqZP0EbZPtuEz16eqClof1/eSrKPOz4OzEK8/PZn5K00+5zU/QsmIJ87OZn/BJes7TxMQg1+hfkzQotH92AACAoglyjf4jSRvM7AlJnelv3f07BRsVAAAIRZBA/0DyBQAAKkyQ3et+ZWb7SqpNFm1x9z2FHRYAAAhD1kCf3NjmV5K2KpH3/nAz+5/O43UAAJS9IKfu/1HSGe6+RZLMrFbSYkknFnJgKL3lTS368WNb9Ga8TSOGVmn2lDHKsI0LukqxIU7WjXCShu93gqTGwowLQL8TJNAP6gjykuTufzIz7sKPuOVNLZqzrFltexKpE1ribZqzrFnfOGYgISibPm6Ic8jgeHhjAdDvBQn0z5nZ3ZJ+nXx/iRLb16JCzV3Tpju3rE5bH4+36fUPN2p3+95u5W172rVgU7temN+77VVjQh9m5UqzIU7GjXA6LDxHihPoAYQnSKC/StLVkr6jxDX6JyXdUchBofR6BvkOn6QuBgCUqSB33X8s6fbkCxEw56QqNTZOSFsfi8V087N71RJv61U3bLBpyazebWOxWJhDBACEJG1mPDO7N/lns5lt7Pkq3hBRCrOnjFHVoIHdyqoGDdQFtdyeAQCVJNOK/n8l//xqMQaC8jKtvkaSet91/8ErJR4ZACAXaQO9u/85+eO33f3GrnXJHe1u7N0KUTKtvqYz4HeIxQj0AFBJgmxqc3qKsrPCHggAAAhf2hW9mV0l6duSPt/jmvwQSU8XemAAAKDvMl2jXyTpEUlzJd3UpXyHu79X0FEBAIBQZLpG/4GkD8ys57X4ajOrdvdthR0aAADoqyAJc1ZIciWS5QyWNErSFknHFXBcQL9V3fp6rzz5XWXKmU+efAA9BUmYU9f1vZmNlzSrYCMC+rO66WqNx/PbPIg8+QBSCLKi78bd15vZFwoxGKDfa5ipDa2jMubET5sznzz5AFIIsh/9/+7ydoCk8ZLeLtiIAABAaIKs6Id0+fkTJa7Z/0dhhgMAAMIU5Br9D4oxEAAAEL5MCXMeVOJu+5Tc/dyCjAgAAIQm04r+J0UbBQAAKIhMCXP+0PGzme0rqTb5dou77yn0wAAAQN8Fueu+UdKvJG1VImnO4Wb2P939ycIODVH0zJt7dPO8ld22vu25Qx4AIDxB7rr/R0lnuPsWSTKzWkmLJZ1YyIEhepY3teieTbu1e2/ifUu8TXOWNUsSwT4k6bLqpcum1628broSiS8BREmQQD+oI8hLkrv/ycwGFXBMqEBz17Tpzi2re5XH45+WN22Ldwb5Dm172nXD0o1avLb31glLZk0oyFgjq49Z9SRJo2aHOSIAZSBIoH/OzO6W9Ovk+0slPV+4ISGqdrfvzakcOcqQVS9dNr3O8gy59bN6bqHUvDT/9l1kyuPfS910qWFmKJ8LRFmQQH+VpKslfUeJa/RPSrqjkINC5ZlzUpUaG3uvwGOxWGf5pHkr1RJv63VMzdAqVu+VrHlp4ozAoXXZjw1LxxkIAj2QVZCEOR9Lul3S7Wb2PyQdliwDcjJ7yhjd8O8bup2+rxo0ULOnjCndoPCpt5o1Ln5z8BV1l3Y6tE6auaLPQ0ibx7+nvpyBAPqZAdkOMLOYmR2QDPIbJC00s9sLPzREzbT6Gl12/L6qGVolU2IlP/f8Om7EKwd10/NfkR9al7yRD0A5CnLq/kB3/9DMvilpobt/38w2FnpgiKaJIwbpe3/bWOphoKeGmYlr/EFX1AAqRtYVvaR9zGy4pAslPVTg8QAAgBAFCfT/R9Jjkv7L3deZ2eckvVLYYQEAgDAEuRnv3yX9e5f3r0m6oJCDAoCs3mpOe1Ne1sf0eDQP/UiQm/FqzewJM9uUfH+Cmf1D4YcGAGn05ebBt5pDe+4fqARBbsb7V0mzJc2XJHffaGaLJP2wkAMDgLSSNw+mk/GmQh7NQz8T5Br9Z9x9bY+yTwoxGAAAEK4ggf4dM/u8JJckM5su6c8FHRUAAAhFkFP3V0v6F0lHm1mLpNclXVLQUQEAgFBkDPRmNkBSg7ufZmb7Sxrg7juKMzQAANBXGU/du/teSdckf95JkAcAoLIEOXX/n2b2XUlLJO3sKHT39wo2KgAopBTP4AfdInf4fidIaizMuIACCBLoL0/+eXWXMpf0ufCHAwAF1pcNeN5q1iGD4+GNBSiCIJnxRhVjIABQFGmewQ+0oc/Cc6Q4gR6VJciKHihby5ta9OPHtujNeJtGDK3S7Clj2PYWALog0KNiLW9q0ZxlzWrb0y5Jaom3ac6yZkki2ANAUrbH60zSYe7+30UaD9BpxvzVGeubtsW1u31vt7K2Pe26YelGLV67LWWbq8aENjwAqAjZHq9zScuLNBYgJz2DfLZyAOiPgpy6f9bMvuDu6wo+GqCLJbMmZKyfNG+lWuJtvcprhlalbRuLxcIYGgBUjCCBfrKkWWb2hhLP0ZsSi/0TCjqyJDP7nKSbJR3o7n14LgZRM3vKmG7X6CWpatBAzZ7C+XkUTnXr6xl3wMv0PD7P4KMUggT6s/Lt3MwWSPqqpL+6+/Fdys+U9H8lDZR0l7vPS9eHu78m6e/MjA2k0U3HDXfcdY+iqZuu1nhc2dPqpMAz+CiRIM/RvyFJZvY3kgbn2P89kn4u6d86CsxsoKRfSDpd0nZJ68zsASWC/twe7S9397/m+JnoR6bV1xDYUTwNM7WhdVTG5+3TPo/PM/gokayB3szOlfSPkkZI+qukIyW9JOm4bG3d/UkzG9mj+IuSXk2u1GVmv5N0nrvPVWL1DwAAQmKJG+szHGD2gqRTJT3u7vVmNlnSxe7+rUAfkAj0D3Wcuk/uZ3+mu38z+f4bkk5y92vStB8m6TYlzgDclfwHQarjviXpW5J08MEHn3jvvfcGGV7oWltbVV1dXZJ+grbJdlym+nR1QcvD+n7yxfxkLmd+Cjc/45puVnt7u5obel+pTNUmaFmxhPnZ5Tg/meoqYX4mT578vLs3pKx094wvSc8l/3xBiW1qJWlttnZd2o+UtKnL+68rEbA73n9D0s+C9hfkVVtb66WyatWqkvUTtE224zLVp6sLWh7W95Mv5idzOfPT9+PS1i8429+/fWLgNkHLiiXMzy7L+clQVwnz0xGrU72C3IwXN7NqSU9K+q2Z/VXSJ3n/syNxXf7wLu8Pk/RmH/oDAABpZEyYk3SepDZJ10t6VNJ/SZrah89cJ2m0mY0ys30lXSTpgT70BwAA0ghy1/3OLm9/lUvnZrZYiYdGDzKz7ZK+7+53m9k1kh5T4k77Be7+Yi79AgCAYNIGejPbocS+872qlEiYc0C2zt394jTlD0t6OOggASAK0iXbSZVkp1tZ3fSUW+sCQaQN9O4+pJgDAYBIyzfZzluJHRkJ9MhXkOfoj0hV7u6ptwcDAPSWIdlOqiQ7nWUZ0u0CQQS5635Fl58HSxolaYsCJMwBAACllTVhTq8GZuMlzXL3WYUZUv7MbKqkqcOHD79i0aJFJRlDlBN+ZKojIUt4bZgf5qdr2bimmyVJG+pvI2FOGc5PPmMshD4lzEn1krQ+n3bFepEwp2/H9feELPet3+4T5z7hI298yCfOfcLvW789r37ybcP8lKafcpufzrIFZydeAT67kEiYk19ZsagvCXPM7H93eTtA0nhJb4fwDxCg7Cxvaum29W1LvE1zliVuhmLzHACVKMg1+q5338Zzn04AABMQSURBVH+ixDX7/yjMcIDCmrumTXduWZ22vmlbXLvb93Yra9vTrhuWbtTitZ/efxqPJ/pZMmtCwcYKdHqrWVp4Tsa97tPi0bx+L0jCnB8UYyBAOegZ5LOVAwVXNz3/tjyaBwV7vC5VetoPJD0nab677wp9VECBzDmpSo2N6Vfhk+atVEu8rVd5zdCqbqv3WCyWsR8gNA0zOwN12r3u0+HRPChYrvvXJbVK+tfk60NJf5FUm3wPRMbsKWNUNWhgt7KqQQM1e8qYEo0IAPomyDX6enf/Spf3D5rZk+7+FTMjRz0ipeOGux8/tkVvxts0YmiVZk8Zw414ACpWkEB/sJkd4clMeMlMeQcl63YXbGRAiUyrryGwA4iMrAlzzOxsSb9UYntaUyIz3rclxSRd4e4/LfAYAyNhTnkllEhVTkIW5icT5if3skzGNd2s6tbX1Vo9KnCbrv5yyFf05xFT8vrsTJif8PU5YY6k/SSNlTRO0uAgbUr5ImFO344jIUth+mF+smN+ci/LaN2CTxPu5Pr6/gGJV/L9+7dPzK39ugU5fw+ZRHJ+QqS+JMxJOlHSSCVO9Z9gZnL3f+vrv0AAAAXU5Y79nD23UGpeml9bHusrK0Eer/u1pM9L2iCpPVnskgj0ABBVPf6RkNOjfTzWV1aCrOgbJB2bPDUAAAAqSJDn6DdJOrTQAwEAAOELsqI/SNJmM1sr6eOOQnc/t2CjAgBUtmR+/lSy5uwnP3+oggT6Wwo9CABAhPQlP/8bTyX+JNCHJsimNn/o+t7MJkn6W0l/SN0CANCvZbnbP+ONfY/cVJgx9WOBHq8zs3FKBPcLlch9zza1QA6WN7Xo1thHeu/RFaTVBTI5a16pRxA5aQO9mdVKukjSxZLelbREiUx6k4s0NiASlje1aM6yZrXtSTy40hJv05xlieeMCfYACi3Tiv5lSX+UNNXdX5UkM7u+KKMCKsSM+auzHtO0Ld5rP/u2Pe26YelGLV67rdfxV7FRHoAQpc11b2ZfU2JFP1HSo5J+J+kud88vaXIRkOu+vHJBpyqPWi71uWt6713f05b396atG/PZ3k+4XntcO/NTgn7K7fen3HKpk+u+vOenT7nuJe0v6RJJD0n6SNKdks7I1q6UL3Ld9+04cqmH28/EuU/4kTc+1Os1ce4TefXN/BSmn3L7/Sm3XOphfjbzEz5lyHWfNWGOu+9099+6+1clHaZEKlxuiwQCmj1ljKoGDexWVjVooGZP4Rw9gMILkhmvk7u/5+7z3f3UQg0IiJpp9TWae36dhg02maSaoVWae34dN+IBKIqgu9cB6INp9TUa+sErwTcFAYCQ5LSiBwAAlYVADwBAhBHoAQCIMAI9AAARRqAHACDCCPQAAEQYgR4AgAhLm+u+EpHrvrxyQacqJ5d6ePPzzJt79B9/2qN3d7mGDTZdUDtIJxzwMfOTZz/l9vtTbrnUyXVf3vPTp1z3lfgi133fjiOXemH6CXN+7lu/3Y/+h0e65c4/+h8e8dt++/uc+mR+cm/TX3Opk+s+v7JiUYZc92TGA8rM3DVtunNL+u1v4/E2vf7hxpRb3y7Y1K4XUmydy9a3QP/FNXqgAvUM8h0+Sb8jLoB+ihU9UGbmnFSlxsYJaetjsZhufnavWuJtveqGDTYtmdW7bSwWC3OIACoIK3qgAqXb+vaC2kElGhGAcsWKHqhAHVvc/vixLXoz3qYRQ6s0e8oYDf3glRKPDEC5IdADFWpafU2vPe1jMQI9gO44dQ8AQIQR6AEAiDACPQAAEcY1egCSpOVNLbo19pHee3RF5819Pe8BAFB5CPQAtLypRXOWNattT2Lvi5Z4m+Ysa5Ykgj1Q4Qj0QD+QLq1uPJ4ob9oWT5lS94alG7V47baUfaZKzAOg/HCNHkDalLrpygFUDlb0QD+QLq1uLBZTY+METZq3MmVK3ZqhVX1auS9vaumV1IdLAUBxsR99yNhPO3M5+52X5/w88+Ye3bNpt3Z3WcDvO0C67Ph9NXFEfml18+mT+cm9rFjYj76854f96IuI/bQzl7Pfed+PK9T83Ld+u4///gofeeNDPnHuE37f+u0Zx3HhL5/J+Br9vYf9yBsf6vUa/b2HUx6f7e/WdZwT5z6RcZxRmJ9y2++c/ejzKysWsR89gGym1ddo6AevqLGxMZT+CnHd/9OnA9ol8XQAEASBHkBesl27z/W6/4z5qzufAkgn6NMBHf3wZADAXfcACiTdVrqzp4zJu0+eDgByx4oeQEGk20o33Sn2JbMmdD4FkE7QswTZ+gH6EwI9gIJJtZVuX8yeMqbbNXqp72cJgKgj0AOoGLmeJQBAoAdQYcI+SwBEHTfjAQAQYazoAfR7bNGLKCPQA+jX2KIXUUegBxBZM+anT77TIdcteq/iBn9UGK7RA+jXSMKDqGNFDyCygqTAzTVVbywWC2NoQNGwogfQrxUiVS9QTljRA+jXOm64u/X+F/TeLueue0QOgR5Avxf2Fr1AObHEfvXRYGZTJU0dPnz4FYsWLSrJGFpbW1VdXV2SfoK2yXZcpvp0dUHLw/p+8sX8ZC5nfspnfoKWFUuYn838hG/y5MnPu3tDykp3j9yrtrbWS2XVqlUl6ydom2zHZapPVxe0PKzvJ1/MT+Zy5qfvx4U1P0HLiiXMz2Z+wifpOU8TEzl1DwAhW97UwsY7KBsEegAI0TNv7tGvn/h0K92umfaGlnJg6LcI9ACQg7lr2nTnlvQZ955/Y7c+6ZFrpyPT3qgDlLIt2fZQSDxHDwAh6hnkO5BpD6XCih4AcjDnpCo1NqbPuHfiLQ/r3V29n2aqGVqlOScNSNmWbHsoJFb0ABCiC2oHkWkPZYVADwAhmjhikOaeX6eaoVUyJVbyc8+v4657lAyn7gEgZNPqawjsKBus6AEAiDACPQAAEUagBwAgwrhGDwAVYHlTi26NfaT3Hl1BWl3khEAPAGVueVOL5ixrVtuexPP5XdPqEuyRDYEeAEosVVrdePzTsqZt8V6Z9TrS6i5eu61Xf0tmpU/og/6Ha/QAUObSpc8lrS6CYEUPACWWKq1uLBbrLJs0b6Va4m292tUMrWL1jqxY0QNAmZs9ZQxpdZE3VvQAUOY6bri79f4X9N4u56575IRADwAVYFp9jYZ+8IoaGxtLPRRUGE7dAwAQYazoAaCfWt7Uoh8/tkVvxtu4HBBhBHoA6Ic+TcLTLokkPFFm7l7qMYTGzKZKmjp8+PArFi1aVJIxtLa2qrq6uiT9BG2T7bhM9enqgpaH9f3ki/nJXM78lM/8BC1LZe6a3o/i9fRfH+zVJykew99ngPT5A3tf1b32uPbQ/tvo7/NTCJMnT37e3RtSVrp75F61tbVeKqtWrSpZP0HbZDsuU326uqDlYX0/+WJ+MpczP30/Lqz5CVqWyoW/fCbr68gbH0r7SnV8mP9t9Pf5KQRJz3mamMipewCImCBJdHJNwhOLxcIYGnIQ1j0U3HUPAP0QSXjKW8c9FC3xNrk+vYdieVNLzn2xogeAfqhjZRj2Xff99U7+XLYRnjF/dcryrnLdyCgTAj0A9FPT6mtCDcL99U7+QmwjHOZGRgR6AEBWqbbS7SnoKrRjC95K2JBnxvzV3bYM7lDobYRzvYfi3ivT98U1egBAKAq1ne7yphb9fewjjbpphSbNW5nXdeqe/U2atzK0/grx9w7zHgpW9ACArFJtpdtT0FVo1y14swn7tPgzb+7Rr59IfXlhaIrjl8yakHK8hd5GOMx7KAj0AIBQzJ4ypts1einzKrQQN6WlOs3e1fNv7O6VKKijv1EHqFfbIIE61793UGHdQ0GgBwCEohB38od9WjxVNsBP+8vvana5byNMoAcAhCaXVWihEvtkuixw4i0P691dvVO/1wyt0pyTBgS+pNBTOW8jzM14AICyFXZinwtqB/W7REEEegBA2ZpWX6O559dp2GCTKbHynnt+Xd6nxSeOGKS559epZmhVKP1VAk7dAwDKWtinxcNOFFTuWNEDABBhBHoAACKMQA8AQIQR6AEAiDACPQAAEUagBwAgwgj0AABEGIEeAIAII9ADABBhBHoAACKMQA8AQIQR6AEAiDACPQAAEUagBwAgwgj0AABEGIEeAIAII9ADABBhBHoAACKMQA8AQITtU+oBhMnMpkqaKmmXmb1YomEcKOmDEvUTtE224zLVp6sLWn6QpHcCjLFQmJ/M5cxP348La35SlZVyfsKam3z7Yn4yG522xt0j95L0XAk/+19K1U/QNtmOy1Sfri5oeSnnhvlhfippftKUVfz/25if4s8Pp+7D92AJ+wnaJttxmerT1eVaXirMT26fVWzMT/DPKbYwx8P8hC/teCz5L4FIMbPn3L2h1ONAb8xNeWN+yhvzU97KdX6iuqL/l1IPAGkxN+WN+SlvzE95K8v5ieSKHgAAJER1RQ8AAESgBwAg0gj0AABEWL8L9GY2zcz+1czuN7MzSj0efMrMPmdmd5vZ0lKPBQlmtr+Z/Sr5O3NJqceD7vidKW/lEm8qKtCb2QIz+6uZbepRfqaZbTGzV83spkx9uPtyd79C0mWSZhRwuP1KSHPzmrv/XWFHihzn6nxJS5O/M+cWfbD9UC7zw+9M8eU4P2URbyoq0Eu6R9KZXQvMbKCkX0g6S9Kxki42s2PNrM7MHurx+psuTf8h2Q7huEfhzQ0K6x4FnCtJh0n67+Rh7UUcY392j4LPD4rvHuU+PyWNNxWV697dnzSzkT2KvyjpVXd/TZLM7HeSznP3uZK+2rMPMzNJ8yQ94u7rCzvi/iOMuUFx5DJXkrYrEew3qPIWBhUpx/nZXNzRIZf5MbOXVAbxJgq/uDX6dMUhJf7HVJPh+GslnSZpupldWciBIbe5MbNhZvZLSfVmNqfQg0M36eZqmaQLzOxOlV/Kz/4k5fzwO1M20v3+lEW8qagVfRqWoixtFiB3/2dJ/1y44aCLXOfmXUn846s0Us6Vu++UNLPYg0Ev6eaH35nykG5+yiLeRGFFv13S4V3eHybpzRKNBd0xN5WDuSpvzE95K+v5iUKgXydptJmNMrN9JV0k6YESjwkJzE3lYK7KG/NT3sp6fioq0JvZYkmrJY0xs+1m9nfu/omkayQ9JuklSfe6+4ulHGd/xNxUDuaqvDE/5a0S54dNbQAAiLCKWtEDAIDcEOgBAIgwAj0AABFGoAcAIMII9AAARBiBHgCACCPQA2UomcN8Q/L1lpm1dHm/b6nHV2hmdpqZfWBmD5jZuC5/9/fM7PXkz49laP+smZ3So+wmM7s9uYPiC2b2TuH/JkDp8Rw9UObM7BZJre7+kx7lpsTv8N6SDCwDM9snmUQk3/anSbrG3af1KP+NpKXuvjxL+/8l6Wh3v6pL2QZJV7j7OjMbLGm7ux+U7xiBSsGKHqggZnaUmW1K7li2XtLhZhbvUn+Rmd2V/PkQM1tmZs+Z2Voz+1KK/vZJrnLXmtlGM/tmsvw0M3si2X6Lmf1blzZfMLM/mNnzZvaImR2SLH/KzG4zsyclXWNmo81sTbLvWzvGaWaLzeycLv0tMbOz+/Cd3Gxm65Lj/16yeImkr5nZPsljxkiqdvd1+X4OUKkI9EDlOVbS3e5eL6klw3H/LOlH7t4g6UJJd6U45luS/uruX5T0BUlXm9kRybrxkq5Oft4xZvYlM9tP0v+VdIG7nyjpN5Ju7dLfAe7+FXf/qaSfSfpJsu+/dDnmLiV3xDOzzyY/N+1p+EzM7FxJhyqxH3i9pMlm9kV3f0vSi5L+v+ShF0tanM9nAJUuCtvUAv3NfwVcmZ6mRD7ujvefNbMqd2/rcswZSgTxi5LvD5Q0Ovnzs+7+Z6nztPdISbskHSfp8WS/A5XYuavD77r8fJKkjpX6Ikk/TP68UtLPzGyYEgH4XndvD/D3SeWM5Gd8Ofm+WlKtpLVKBPaLlPhHxAxJ0/P8DKCiEeiByrOzy8971X0v7MFdfjZJX3T33Rn6MknfdvcnuhUmrpF/3KWoXYn/X5ikje7+ZaW2M015J3d3M/utpL+VdFnyz3yZpB+4+69S1P2HpNvM7CRJu8tpkxGgmDh1D1Sw5I147yevhw+Q9LUu1Y8rcepdkmRm41J08Zikb3e9lm1mVRk+crOkGjP7YvL4fc3suDTHru0ynot61C2UNFvSLnffkuHzsnlM0jfN7DPJ8RyRPFMgd39f0hpJ88Vpe/RjBHqg8t0o6VFJT6j7afSrJU1K3qS2WdIVKdrOl/SKpA1mtknSncpwps/dP1biFPjtZvaCpCYlTtGn8h1JN5rZWkl/I+mDLv28KelPSgT8vLn7A0rs+73GzJqVCOj7dzlksaSx6n5JAehXeLwOQEGY2f6SPkqeqr9U0tfc/YIudc2Sxrr7jhRtUz5eF+LYeLwO/QYregCF8gVJTWa2UYmzCbMlycymSHpJ0j+lCvJJH0saZ2YPhD0oMztW0rPq/iQAEFms6AEAiDBW9AAARBiBHgCACCPQAwAQYQR6AAAijEAPAECEEegBAIiw/wet+4N0m0rSKAAAAABJRU5ErkJggg==\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 index 7b2fe59b2..ac38cdf7d 100644 --- a/docs/contribute/index.rst +++ b/docs/contribute/index.rst @@ -7,7 +7,7 @@ How to contribute :hidden: repo.rst - ./comparison_with_EventDisplay.ipynb + ./notebooks/comparison_with_EventDisplay.ipynb Development procedure --------------------- @@ -58,4 +58,4 @@ Further details Benchmarks ----------- -- `Comparison with EventDisplay <./comparison_with_EventDisplay.ipynb>`__ | *comparison_with_EventDisplay.ipynb* +- `Comparison with EventDisplay <./notebooks/comparison_with_EventDisplay.ipynb>`__ | *comparison_with_EventDisplay.ipynb* diff --git a/notebooks/comparison_with_EventDisplay.ipynb b/docs/contribute/notebooks/comparison_with_EventDisplay.ipynb similarity index 92% rename from notebooks/comparison_with_EventDisplay.ipynb rename to docs/contribute/notebooks/comparison_with_EventDisplay.ipynb index 336beb19d..d281225b4 100644 --- a/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/contribute/notebooks/comparison_with_EventDisplay.ipynb @@ -43,12 +43,16 @@ "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", - " - [Angular resolution](#Angular-resolution)\n", - " - [Energy resolution](#Energy-resolution)\n", - " - [Background rate](#Background-rate)\n", - "* [Differential sensitivity](#Differential-sensitivity)" + " - [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)" ] }, { @@ -126,7 +130,7 @@ "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", + "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))" @@ -172,17 +176,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Differential sensitivity\n", + "## Optimized cuts\n", "[back to top](#Table-of-contents)" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "from astropy.table import QTable" + "### Direction cut\n", + "[back to top](#Table-of-contents)" ] }, { @@ -191,8 +194,42 @@ "metadata": {}, "outputs": [], "source": [ - "# [1:-1] removes under/overflow bin\n", - "sensitivity = QTable.read(pyirf_file, hdu='SENSITIVITY')[1:-1]" + "from astropy.table import Table\n", + "\n", + "\n", + "theta_cut = Table.read(pyirf_file, hdu='THETA_CUTS_OPT')[1:-1]\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 * (theta_cut['low'] + theta_cut['high']),\n", + " theta_cut['cut'].quantity.to_value(u.deg)**2,\n", + " xerr=0.5 * (theta_cut['high'] - theta_cut['low']),\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')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Differential sensitivity from cuts optimization\n", + "[back to top](#Table-of-contents)" ] }, { @@ -201,6 +238,8 @@ "metadata": {}, "outputs": [], "source": [ + "# [1:-1] removes under/overflow bin\n", + "sensitivity = QTable.read(pyirf_file, hdu='SENSITIVITY')[1:-1]\n", "sensitivity" ] }, @@ -258,50 +297,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Theta Cuts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from astropy.table import Table\n", - "\n", - "\n", - "theta_cut = Table.read(pyirf_file, hdu='THETA_CUTS_OPT')[1:-1]\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 * (theta_cut['low'] + theta_cut['high']),\n", - " theta_cut['cut'].quantity.to_value(u.deg)**2,\n", - " xerr=0.5 * (theta_cut['high'] - theta_cut['low']),\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')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### IRFs\n", + "## IRFs\n", "[back to top](#Table-of-contents)" ] }, @@ -309,7 +305,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Effective area\n", + "### Effective area\n", "[back to top](#Table-of-contents)" ] }, @@ -356,16 +352,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### PSF" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "psf_table = QTable.read(pyirf_file, hdu='PSF')[0]" + "### Point Spread Function\n", + "[back to top](#Table-of-contents)" ] }, { @@ -374,8 +362,8 @@ "metadata": {}, "outputs": [], "source": [ + "psf_table = QTable.read(pyirf_file, hdu='PSF')[0]\n", "# select the only fov offset bin\n", - "\n", "psf = psf_table['psf'][:, 0, :].to_value(1 / u.sr)\n", "\n", "offset_bins = np.append(psf_table['source_offset_low'], psf_table['source_offset_high'][-1])\n", @@ -479,7 +467,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Energy Dispersion" + "### Energy dispersion\n", + "[back to top](#Table-of-contents)" ] }, { @@ -488,7 +477,8 @@ "metadata": {}, "outputs": [], "source": [ - "edisp = QTable.read(pyirf_file, hdu='EDISP')[0]" + "edisp = QTable.read(pyirf_file, hdu='EDISP')[0]\n", + "edisp" ] }, { @@ -499,8 +489,6 @@ }, "outputs": [], "source": [ - "edisp\n", - "\n", "e_bins = edisp['true_energy_low'][1:]\n", "migra_bins = edisp['migration_low'][1:]\n", "\n", @@ -512,7 +500,9 @@ "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}$')" + "plt.ylabel(r'$E_\\mathrm{Reco} / E_\\mathrm{True}$')\n", + "\n", + "plt.show()" ] }, { @@ -523,15 +513,6 @@ "[back to top](#Table-of-contents)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bias_resolution = QTable.read(pyirf_file, hdu='ENERGY_BIAS_RESOLUTION')[1:-1]\n" - ] - }, { "cell_type": "code", "execution_count": null, @@ -545,6 +526,9 @@ "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", @@ -570,7 +554,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Background rate\n", + "### Background rate\n", "[back to top](#Table-of-contents)" ] }, 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', ' Date: Thu, 24 Sep 2020 15:34:21 +0200 Subject: [PATCH 045/105] Move notebook out of contribute --- docs/conf.py | 24 +++++++++++-------- docs/contribute/index.rst | 3 +-- docs/index.rst | 1 + .../comparison_with_EventDisplay.ipynb | 13 ++-------- docs/notebooks/index.rst | 10 ++++++++ docs/usage/EventDisplay.rst | 2 +- pyirf/binning.py | 12 +++++----- 7 files changed, 35 insertions(+), 30 deletions(-) rename docs/{contribute => }/notebooks/comparison_with_EventDisplay.ipynb (98%) create mode 100644 docs/notebooks/index.rst diff --git a/docs/conf.py b/docs/conf.py index 5c8fda46f..bf97f628c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,6 +58,10 @@ # nbsphinx # nbsphinx_execute = "never" +nbsphinx_execute_arguments = [ + "--InlineBackend.figure_formats={'svg', 'pdf'}", + "--InlineBackend.rc={'figure.dpi': 96}", +] numpydoc_show_class_members = False autosummary_generate = True @@ -78,16 +82,16 @@ # 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", -} +# 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", +# } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/contribute/index.rst b/docs/contribute/index.rst index ac38cdf7d..9baf73730 100644 --- a/docs/contribute/index.rst +++ b/docs/contribute/index.rst @@ -7,7 +7,6 @@ How to contribute :hidden: repo.rst - ./notebooks/comparison_with_EventDisplay.ipynb Development procedure --------------------- @@ -58,4 +57,4 @@ Further details Benchmarks ----------- -- `Comparison with EventDisplay <./notebooks/comparison_with_EventDisplay.ipynb>`__ | *comparison_with_EventDisplay.ipynb* +- `Comparison with EventDisplay <../notebooks/comparison_with_EventDisplay.ipynb>`__ | *comparison_with_EventDisplay.ipynb* diff --git a/docs/index.rst b/docs/index.rst index aa98d5576..c608b0260 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ which this documentation is linked. install/index usage/index + notebooks/index contribute/index changelog AUTHORS diff --git a/docs/contribute/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb similarity index 98% rename from docs/contribute/notebooks/comparison_with_EventDisplay.ipynb rename to docs/notebooks/comparison_with_EventDisplay.ipynb index d281225b4..e13f5fd97 100644 --- a/docs/contribute/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -130,21 +130,12 @@ "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", + "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": [ - "### Setup of output data" - ] - }, { "cell_type": "markdown", "metadata": { @@ -169,7 +160,7 @@ "metadata": {}, "outputs": [], "source": [ - "pyirf_file = '../pyirf_eventdisplay.fits.gz'" + "pyirf_file = '../../pyirf_eventdisplay.fits.gz'" ] }, { 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/usage/EventDisplay.rst b/docs/usage/EventDisplay.rst index eec161110..8f0580ee7 100644 --- a/docs/usage/EventDisplay.rst +++ b/docs/usage/EventDisplay.rst @@ -7,7 +7,7 @@ How to build DL3 data from EventDisplay files .. toctree:: :hidden: - ../contribute/comparison_with_EventDisplay.ipynb + ../notebooks/comparison_with_EventDisplay.ipynb Retrieve EventDisplay data -------------------------- diff --git a/pyirf/binning.py b/pyirf/binning.py index ae432f30d..5e7e25a6e 100644 --- a/pyirf/binning.py +++ b/pyirf/binning.py @@ -11,8 +11,8 @@ def add_overflow_bins(bins, positive=True): ''' Add under and overflow bins to a bin array. - Arguments - --------- + Parameters + ---------- bins: np.array or u.Quantity Bin edges array positive: bool @@ -40,8 +40,8 @@ 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. - Arguments - --------- + Parameters + ---------- e_min: u.Quantity[energy] Minimum energy, inclusive e_max: u.Quantity[energy] @@ -72,8 +72,8 @@ def calculate_bin_indices(data, bins): function will always be a valid index into the resultung histogram. - Arguments - --------- + Parameters + ---------- data: ``~np.ndarray`` or ``~astropy.units.Quantity`` Array with the data From 4c50171e10f23de06aa2a19fc76654ac5e86d25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 15:37:28 +0200 Subject: [PATCH 046/105] Build docs on travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 530634ede..d2eba65d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,7 @@ install: script: - 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 From 2fa0a5ad4c241d868db7be38cdd5479e6c988aa4 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Thu, 24 Sep 2020 17:30:06 +0200 Subject: [PATCH 047/105] Remove material from first version. --- pyirf/perf/__init__.py | 12 - pyirf/perf/cut_optimisation.py | 929 ------------------------------- pyirf/perf/irf_maker.py | 668 ---------------------- pyirf/perf/utils.py | 67 --- pyirf/resources/config.yml | 114 ---- pyirf/scripts/lst_performance.py | 496 ----------------- pyirf/scripts/make_DL3.py | 326 ----------- 7 files changed, 2612 deletions(-) delete mode 100644 pyirf/perf/__init__.py delete mode 100644 pyirf/perf/cut_optimisation.py delete mode 100644 pyirf/perf/irf_maker.py delete mode 100644 pyirf/perf/utils.py delete mode 100644 pyirf/resources/config.yml delete mode 100644 pyirf/scripts/lst_performance.py delete mode 100644 pyirf/scripts/make_DL3.py 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() From 62a8cfd6b35b23c32e84cef129bbd5dd07714a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 17:34:01 +0200 Subject: [PATCH 048/105] Simplify function return values and add io for GADF format * pyirf functions now return psf, aeff, edisp as simple quantities * add functions to create the gadf hdus * adapt plotting and tests * adapt example Co-authored-by: Lukas Nickel Co-authored-by: Michele Peresano --- .../comparison_with_EventDisplay.ipynb | 30 ++-- examples/calculate_eventdisplay_irfs.py | 52 +++++-- pyirf/io/__init__.py | 13 +- pyirf/io/gadf.py | 142 ++++++++++++++++++ pyirf/irf/__init__.py | 4 +- pyirf/irf/effective_area.py | 8 +- pyirf/irf/energy_dispersion.py | 40 ++--- pyirf/irf/psf.py | 24 +-- pyirf/irf/tests/test_psf.py | 15 +- pyirf/utils.py | 9 ++ 10 files changed, 253 insertions(+), 84 deletions(-) create mode 100644 pyirf/io/gadf.py diff --git a/docs/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb index e13f5fd97..d271c4564 100644 --- a/docs/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -319,13 +319,13 @@ "\n", "for name in ('', '_NO_CUTS', '_ONLY_GH', '_ONLY_THETA'):\n", "\n", - " area = QTable.read(pyirf_file, hdu='EFFECTIVE_AREA' + name)[1:-1]\n", + " area = QTable.read(pyirf_file, hdu='EFFECTIVE_AREA' + name)[0]\n", "\n", " \n", " plt.errorbar(\n", - " 0.5 * (area['true_energy_low'] + area['true_energy_high']).to_value(u.TeV),\n", - " area['effective_area'].to_value(u.m**2),\n", - " xerr=0.5 * (area['true_energy_high'] - area['true_energy_low']).to_value(u.TeV),\n", + " 0.5 * (area['ENERG_LO'] + area['ENERG_HI']).to_value(u.TeV)[1:-1],\n", + " area['EFFAREA'].to_value(u.m**2)[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", @@ -336,9 +336,19 @@ "plt.xlabel(\"True energy / TeV\")\n", "plt.ylabel(\"Effective collection area / m²\")\n", "plt.grid(which=\"both\")\n", + "plt.legend()\n", "plt.show()" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "QTable.read(pyirf_file, hdu='EFFECTIVE_AREA')" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -355,9 +365,9 @@ "source": [ "psf_table = QTable.read(pyirf_file, hdu='PSF')[0]\n", "# select the only fov offset bin\n", - "psf = psf_table['psf'][:, 0, :].to_value(1 / u.sr)\n", + "psf = psf_table['RPSF'][:, 0, :].to_value(1 / u.sr)\n", "\n", - "offset_bins = np.append(psf_table['source_offset_low'], psf_table['source_offset_high'][-1])\n", + "offset_bins = np.append(psf_table['RAD_LO'], psf_table['RAD_HI'][-1])\n", "phi_bins = np.linspace(0, 2 * np.pi, 1000)\n", "\n", "\n", @@ -468,7 +478,7 @@ "metadata": {}, "outputs": [], "source": [ - "edisp = QTable.read(pyirf_file, hdu='EDISP')[0]\n", + "edisp = QTable.read(pyirf_file, hdu='ENERGY_DISPERSION')[0]\n", "edisp" ] }, @@ -480,11 +490,11 @@ }, "outputs": [], "source": [ - "e_bins = edisp['true_energy_low'][1:]\n", - "migra_bins = edisp['migration_low'][1:]\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['energy_dispersion'][1:-1, 1:-1, 0].T, cmap='inferno')\n", + "plt.pcolormesh(e_bins.to_value(u.TeV), migra_bins, edisp['MATRIX'][1:-1, 1:-1, 0].T, cmap='inferno')\n", "\n", "plt.xscale('log')\n", "plt.yscale('log')\n", diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 244b39be0..760383b39 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -16,7 +16,7 @@ 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 +from pyirf.utils import calculate_theta, calculate_source_fov_offset from pyirf.benchmarks import energy_bias_resolution, angular_resolution from pyirf.spectral import ( @@ -30,10 +30,17 @@ from pyirf.irf import ( point_like_effective_area, - point_like_energy_dispersion, + 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') @@ -86,6 +93,7 @@ def main(): # calculate theta / distance between reco and true source pos p['events']['theta'] = calculate_theta(p['events']) + p['events']['source_fov_offset'] = calculate_source_fov_offset(p['events']) log.info(p['simulation_info']) log.info('') @@ -180,29 +188,45 @@ def main(): ] masks = { + '': gammas['selected'], '_NO_CUTS': slice(None), '_ONLY_GH': gammas['selected_gh'], '_ONLY_THETA': gammas['selected_theta'], - '': gammas['selected'] } - # calculate IRFs for the best cuts + + # 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, )) - for extname, mask in masks.items(): + 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, ) - edisp = point_like_energy_dispersion( + 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, - migration_bins=np.geomspace(0.2, 5, 200), - max_theta=np.nanmax(theta_cuts_opt['cut'].max()), + fov_offset_bins=fov_offset_bins, + migration_bins=energy_migration_bins, ) - hdus.append(fits.BinTableHDU(effective_area, name='EFFECTIVE_AREA' + extname)) - hdus.append(fits.BinTableHDU(edisp, name='EDISP' + extname)) + 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']], @@ -216,11 +240,13 @@ def main(): psf = psf_table( gammas[gammas['selected']], true_energy_bins, - np.arange(0, 1 + 1e-4, 1e-3) * u.deg, - [0, 0.1] * u.deg, + fov_offset_bins=fov_offset_bins, + source_offset_bins=source_offset_bins, ) - hdus.append(fits.BinTableHDU(psf, name='PSF')) + hdus.append(create_psf_table_hdu( + psf, true_energy_bins, source_offset_bins, fov_offset_bins, + )) 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) diff --git a/pyirf/io/__init__.py b/pyirf/io/__init__.py index 0328e9c1b..96771d4ce 100644 --- a/pyirf/io/__init__.py +++ b/pyirf/io/__init__.py @@ -1,6 +1,17 @@ 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__ = [ - 'read_eventdisplay_fits' + '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/gadf.py b/pyirf/io/gadf.py new file mode 100644 index 000000000..e3aa175dd --- /dev/null +++ b/pyirf/io/gadf.py @@ -0,0 +1,142 @@ +from astropy.table import QTable +import astropy.units as u +from astropy.io.fits import Header, BinTableHDU +import numpy as np + +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 +): + 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) + aeff['EFFAREA'] = effective_area[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' + _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 +): + + 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), + 'RPSF': psf[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' + _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 +): + + 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), + 'MATRIX': u.Quantity(energy_dispersion[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' + _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 +): + 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), + 'RAD_MAX': rad_max[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' + _add_header_cards(header, **header_cards) + + return BinTableHDU(rad_max_table, header=header, name=extname) diff --git a/pyirf/irf/__init__.py b/pyirf/irf/__init__.py index d0627c50f..fc35edec9 100644 --- a/pyirf/irf/__init__.py +++ b/pyirf/irf/__init__.py @@ -1,10 +1,10 @@ from .effective_area import effective_area, point_like_effective_area -from .energy_dispersion import point_like_energy_dispersion +from .energy_dispersion import energy_dispersion from .psf import psf_table __all__ = [ 'effective_area', 'point_like_effective_area', - 'point_like_energy_dispersion', + 'energy_dispersion', 'psf_table', ] diff --git a/pyirf/irf/effective_area.py b/pyirf/irf/effective_area.py index 1c356e6fa..975904c17 100644 --- a/pyirf/irf/effective_area.py +++ b/pyirf/irf/effective_area.py @@ -1,6 +1,5 @@ import numpy as np import astropy.units as u -from astropy.table import QTable from ..binning import create_histogram_table @@ -30,9 +29,4 @@ def point_like_effective_area(selected_events, simulation_info, true_energy_bins hist_selected = create_histogram_table(selected_events, true_energy_bins, 'true_energy') hist_simulated = calculate_simulated_events(simulation_info, true_energy_bins) - area_table = QTable(hist_selected[['true_energy_' + k for k in ('low', 'high')]]) - area_table['effective_area'] = effective_area( - hist_selected['n'], hist_simulated, area - ) - - return area_table + return effective_area(hist_selected['n'], hist_simulated, area) diff --git a/pyirf/irf/energy_dispersion.py b/pyirf/irf/energy_dispersion.py index a5f6dbc86..18aeac4eb 100644 --- a/pyirf/irf/energy_dispersion.py +++ b/pyirf/irf/energy_dispersion.py @@ -1,29 +1,39 @@ import numpy as np import astropy.units as u -from astropy.table import QTable 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 = hist.T - h = h / h.sum(axis=0) - return np.nan_to_num(h).T + h /= norm + + h = np.swapaxes(h, 0, 1) + return np.nan_to_num(h) -def point_like_energy_dispersion( +def energy_dispersion( selected_events, true_energy_bins, + fov_offset_bins, migration_bins, - max_theta, ): mu = (selected_events['reco_energy'] / selected_events['true_energy']).to_value(u.one) - energy_dispersion, _, _ = np.histogram2d( - selected_events['true_energy'].to_value(u.TeV), - mu, + 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), ] ) @@ -31,14 +41,4 @@ def point_like_energy_dispersion( assert len(n_events_per_energy) == len(true_energy_bins) - 1 energy_dispersion = _normalize_hist(energy_dispersion) - edisp = QTable({ - 'true_energy_low': [true_energy_bins[:-1]], - 'true_energy_high': [true_energy_bins[1:]], - 'migration_low': [migration_bins[:-1]], - 'migration_high': [migration_bins[1:]], - 'theta_low': [0 * u.deg], - 'theta_high': [max_theta], - 'energy_dispersion': [energy_dispersion[:, :, np.newaxis]], - }) - - return edisp + return energy_dispersion diff --git a/pyirf/irf/psf.py b/pyirf/irf/psf.py index 540a25828..b9895776d 100644 --- a/pyirf/irf/psf.py +++ b/pyirf/irf/psf.py @@ -1,9 +1,5 @@ import numpy as np import astropy.units as u -from astropy.table import QTable - -from astropy.coordinates.angle_utilities import angular_separation - from ..utils import cone_solid_angle @@ -13,14 +9,9 @@ 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) ''' - source_fov_offset = angular_separation( - events['true_az'], events['true_alt'], - events['pointing_az'], events['pointing_alt'], - ) - array = np.column_stack([ events['true_energy'].to_value(u.TeV), - source_fov_offset.to_value(u.deg), + events['source_fov_offset'].to_value(u.deg), events['theta'].to_value(u.deg) ]) @@ -34,18 +25,7 @@ def psf_table(events, true_energy_bins, source_offset_bins, fov_offset_bins): ) psf = _normalize_psf(hist, source_offset_bins) - - result = QTable({ - 'true_energy_low': u.Quantity(true_energy_bins[:-1], ndmin=2), - 'true_energy_high': u.Quantity(true_energy_bins[1:], ndmin=2), - 'source_offset_low': u.Quantity(source_offset_bins[:-1], ndmin=2), - 'source_offset_high': u.Quantity(source_offset_bins[1:], ndmin=2), - 'fov_offset_low': u.Quantity(fov_offset_bins[:-1], ndmin=2), - 'fov_offset_high': u.Quantity(fov_offset_bins[1:], ndmin=2), - 'psf': [psf], - }) - - return result + return psf def _normalize_psf(hist, source_offset_bins): diff --git a/pyirf/irf/tests/test_psf.py b/pyirf/irf/tests/test_psf.py index cdff9de2b..e16edd8e1 100644 --- a/pyirf/irf/tests/test_psf.py +++ b/pyirf/irf/tests/test_psf.py @@ -18,10 +18,7 @@ def test_psf(): # and a psf per energy bin, point-like events = QTable({ 'true_energy': np.append(np.full(N, 1), np.full(N, 2)) * u.TeV, - 'pointing_az': np.zeros(2 * N) * u.deg, - 'pointing_alt': np.full(2 * N, 70) * u.deg, - 'true_az': np.zeros(2 * N) * u.deg, - 'true_alt': np.full(2 * N, 70) * u.deg, + 'source_fov_offset': np.zeros(2 * N) * u.deg, 'theta': np.random.normal(0, TRUE_SIGMA) * u.deg, }) @@ -30,17 +27,17 @@ def test_psf(): 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)[0] + psf = psf_table(events, energy_bins, source_bins, fov_bins) # 2 energy bins, 1 fov bin, 200 source distance bins - assert psf['psf'].shape == (2, 1, 200) - assert psf['psf'].unit == u.Unit('sr-1') + 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['psf'] * bin_solid_angle, axis=2), 1.0) + assert np.allclose(np.sum(psf * bin_solid_angle, axis=2), 1.0) - cumulated = np.cumsum(psf['psf'] * bin_solid_angle, axis=2) + 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]) diff --git a/pyirf/utils.py b/pyirf/utils.py index 0b1f050bb..79d4b33fa 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -17,6 +17,15 @@ def calculate_theta(events): return theta.to(u.deg) +def calculate_source_fov_offset(events): + 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 From 52fee6b7b4f449ed0d3e8755106a759a4dde151f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 18:21:41 +0200 Subject: [PATCH 049/105] Start updating the docs --- MANIFEST.in | 8 -- docs/benchmarks/index.rst | 11 ++ docs/cut_optimization.rst | 11 ++ docs/index.rst | 12 +- docs/irf/index.rst | 34 +++++ docs/perf/index.rst | 246 ------------------------------------ docs/scripts/index.rst | 84 ------------ docs/sensitivity.rst | 11 ++ docs/usage/EventDisplay.rst | 9 +- pyirf/cut_optimization.py | 9 +- pyirf/sensitivity.py | 6 + setup.py | 2 +- 12 files changed, 90 insertions(+), 353 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 docs/benchmarks/index.rst create mode 100644 docs/cut_optimization.rst create mode 100644 docs/irf/index.rst delete mode 100644 docs/perf/index.rst delete mode 100644 docs/scripts/index.rst create mode 100644 docs/sensitivity.rst 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/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/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/index.rst b/docs/index.rst index c608b0260..2c5f064fc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,10 +18,10 @@ 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. @@ -53,12 +53,14 @@ which this documentation is linked. :caption: Structure :maxdepth: 1 + irf/index + sensitivity + benchmarks/index + cut_optimization spectral binning io/index resources/index - perf/index - scripts/index Reference/API diff --git a/docs/irf/index.rst b/docs/irf/index.rst new file mode 100644 index 000000000..ff17a6364 --- /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/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/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/usage/EventDisplay.rst b/docs/usage/EventDisplay.rst index 8f0580ee7..37bdee00e 100644 --- a/docs/usage/EventDisplay.rst +++ b/docs/usage/EventDisplay.rst @@ -42,15 +42,10 @@ are related to the DL2 data above and replicated for different observing times i Launch *pyirf* -------------- -To create the DL3 data you will need to +To create the sensitivity and IRFs 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. +- launch the ``examples/calculate_eventdisplay_irfs.py`` 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 ------- diff --git a/pyirf/cut_optimization.py b/pyirf/cut_optimization.py index f4f7487f0..8b9125506 100644 --- a/pyirf/cut_optimization.py +++ b/pyirf/cut_optimization.py @@ -8,14 +8,19 @@ 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 + # we apply each cut for all bins globally, calculate the + # sensitivity and then lookup the best sensitivity for each # bin independently sensitivities = [] diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index dcfee2ca0..4f3b7eaaf 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -8,6 +8,12 @@ from .utils import check_histograms +__all__ = [ + 'relative_sensitivity', + 'calculate_sensitivity' +] + + log = logging.getLogger(__name__) diff --git a/setup.py b/setup.py index 6d8cb489f..3fcbd7322 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ "scipy", "tqdm", "tables", - "gammapy~=0.8.0", + "gammapy~=0.17", ], extras_require=extras_require, ) From 275617ef67628fd522d326ca0d2ea49275a9d447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 18:37:13 +0200 Subject: [PATCH 050/105] Install cython on travis for gammapy --- .travis.yml | 3 ++- docs/install/basic.rst | 1 - docs/install/developer.rst | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d2eba65d5..5281c68f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,8 @@ install: - sed -i -e "s/- python=.*/- python=$PYTHON_VERSION/g" environment.yml - conda env create -f environment.yml - conda activate pyirf-dev - - pip install travis-sphinx codecov pytest-cov + # cython needed for building gammapy + - pip install travis-sphinx codecov pytest-cov Cython - pip install .[all] - python --version diff --git a/docs/install/basic.rst b/docs/install/basic.rst index c7f8e9d62..211e37d0d 100644 --- a/docs/install/basic.rst +++ b/docs/install/basic.rst @@ -25,6 +25,5 @@ Steps for installation: 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 index ccd9ddce7..e4bca55ed 100644 --- a/docs/install/developer.rst +++ b/docs/install/developer.rst @@ -16,6 +16,5 @@ 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`). From 0fbf9a31c266dbae6e1c45ecf761ab05707e1921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 18:38:33 +0200 Subject: [PATCH 051/105] Bump version to 0.2 --- pyirf/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 4e696d54b3edf4b5f12995af5d9aedf281b269ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 19:29:47 +0200 Subject: [PATCH 052/105] Increase timeout for data download --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5281c68f7..825858404 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: - CONDA=true before_install: - - curl -sSfL -o data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download + - travis_wait 15 curl -sSfL -o data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download - unzip data.zip - mv eventdisplay_dl2 data - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; From 03ba047be8e5f2e3e7fbe8c80404e91fe381a56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 20:07:32 +0200 Subject: [PATCH 053/105] Add some docstrings --- pyirf/irf/effective_area.py | 42 +++++++++++++++++++++++-------------- pyirf/simulations.py | 31 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/pyirf/irf/effective_area.py b/pyirf/irf/effective_area.py index 975904c17..2c6f5712b 100644 --- a/pyirf/irf/effective_area.py +++ b/pyirf/irf/effective_area.py @@ -5,28 +5,38 @@ @u.quantity_input(area=u.m**2) def effective_area(selected_n, simulated_n, 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 (selected_n / simulated_n) * area -def calculate_simulated_events(simulation_info, bins): - bins = bins.to_value(u.TeV) - e_low = bins[:-1] - e_high = bins[1:] - - int_index = simulation_info.spectral_index + 1 - e_min = simulation_info.energy_min.to_value(u.TeV) - e_max = simulation_info.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 simulation_info.n_showers * normalization * e_term - - 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 = calculate_simulated_events(simulation_info, true_energy_bins) + hist_simulated = simulation_info.calculate_n_showers(true_energy_bins) return effective_area(hist_selected['n'], hist_simulated, area) diff --git a/pyirf/simulations.py b/pyirf/simulations.py index 31f2ede01..d1948c9aa 100644 --- a/pyirf/simulations.py +++ b/pyirf/simulations.py @@ -43,6 +43,37 @@ def __init__(self, n_showers, energy_min, energy_max, max_impact, spectral_index 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__}(' From 3ff55464cfaa1eaed99ab61da2e66b02c46e9c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 24 Sep 2020 20:36:49 +0200 Subject: [PATCH 054/105] Add effective area unit test --- pyirf/binning.py | 7 +++- pyirf/irf/effective_area.py | 4 +-- pyirf/irf/tests/test_effective_area.py | 44 ++++++++++++++++++++++++++ pyirf/irf/tests/test_psf.py | 3 +- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 pyirf/irf/tests/test_effective_area.py diff --git a/pyirf/binning.py b/pyirf/binning.py index 5e7e25a6e..5b43b0e96 100644 --- a/pyirf/binning.py +++ b/pyirf/binning.py @@ -105,5 +105,10 @@ def create_histogram_table(events, bins, key='reco_energy'): hist[key + '_high'] = bins[1:] hist[key + '_center'] = 0.5 * (hist[key + '_low'] + hist[key + '_high']) hist['n'], _ = np.histogram(events[key], bins) - hist['n_weighted'], _ = np.histogram(events[key], bins, weights=events['weight']) + + # 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/irf/effective_area.py b/pyirf/irf/effective_area.py index 2c6f5712b..f72e51701 100644 --- a/pyirf/irf/effective_area.py +++ b/pyirf/irf/effective_area.py @@ -4,7 +4,7 @@ @u.quantity_input(area=u.m**2) -def effective_area(selected_n, simulated_n, area): +def effective_area(n_selected, n_simulated, area): ''' Calculate effective area for histograms of selected and total simulated events @@ -17,7 +17,7 @@ def effective_area(selected_n, simulated_n, area): area: ``~astropy.units.Quantity``[area] Area in which particle's core position was simulated ''' - return (selected_n / simulated_n) * area + return (n_selected / n_simulated) * area def point_like_effective_area(selected_events, simulation_info, true_energy_bins): diff --git a/pyirf/irf/tests/test_effective_area.py b/pyirf/irf/tests/test_effective_area.py new file mode 100644 index 000000000..9dc5a0f3e --- /dev/null +++ b/pyirf/irf/tests/test_effective_area.py @@ -0,0 +1,44 @@ +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(100, 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, 100] * u.m**2) diff --git a/pyirf/irf/tests/test_psf.py b/pyirf/irf/tests/test_psf.py index e16edd8e1..0f09a4bb1 100644 --- a/pyirf/irf/tests/test_psf.py +++ b/pyirf/irf/tests/test_psf.py @@ -7,9 +7,10 @@ def test_psf(): from pyirf.irf import psf_table from pyirf.utils import cone_solid_angle + np.random.seed(0) + N = 1000 - np.random.seed() 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)) From 1ef961fd134b2a164862750fb39ab5d92efaaba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 11:35:08 +0200 Subject: [PATCH 055/105] Fix warnings for low number of background events --- pyirf/sensitivity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index 4f3b7eaaf..470f6d6e0 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -78,7 +78,7 @@ def relative_sensitivity( if np.isnan(n_on) or np.isnan(n_off): return np.nan - if n_on == 0 or n_off == 0: + if n_on < 1 or n_off < 1: return np.nan if n_signal <= 0: From cc820e223e25af0914086827081083a4995faf33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 11:36:49 +0200 Subject: [PATCH 056/105] Fix handling of pointing position --- .../comparison_with_EventDisplay.ipynb | 35 ++++++++++++++----- examples/calculate_eventdisplay_irfs.py | 28 ++++++++------- pyirf/cuts.py | 1 - pyirf/io/eventdisplay.py | 4 +-- pyirf/irf/tests/test_effective_area.py | 5 ++- pyirf/utils.py | 4 +-- 6 files changed, 48 insertions(+), 29 deletions(-) diff --git a/docs/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb index d271c4564..5222023b4 100644 --- a/docs/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -215,6 +215,32 @@ "plt.yscale('log')" ] }, + { + "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('θ²-cut / deg²')\n", + "plt.xlabel(r'$E_\\mathrm{reco} / \\mathrm{TeV}$')\n", + "plt.xscale('log')" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -340,15 +366,6 @@ "plt.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "QTable.read(pyirf_file, hdu='EFFECTIVE_AREA')" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 760383b39..588495b30 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -50,23 +50,22 @@ # scaling between on and off region. # Make off region 5 times larger than on region for better # background statistics -ALPHA = 0.1 +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', + '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', + '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', + 'file': 'data/electron_onSource.S.3HB9-FD_ID0.eff-0.fits.gz', 'target_spectrum': IRFDOC_ELECTRON_SPECTRUM, }, } @@ -80,21 +79,26 @@ def get_bg_cuts(cuts, alpha): def main(): - logging.basicConfig(level=logging.DEBUG) + 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'] ) - - # calculate theta / distance between reco and true source pos - p['events']['theta'] = calculate_theta(p['events']) 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('') @@ -144,7 +148,7 @@ def main(): gammas[gammas['selected_theta']], background[background['selected_theta']], bins=sensitivity_bins, - cut_values=np.arange(-1.0, 1.005, 0.2), + cut_values=np.arange(-1.0, 1.005, 0.05), op=operator.ge, alpha=ALPHA, ) diff --git a/pyirf/cuts.py b/pyirf/cuts.py index 6cc86cf4c..28a72c214 100644 --- a/pyirf/cuts.py +++ b/pyirf/cuts.py @@ -35,7 +35,6 @@ def calculate_percentile_cut( 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) diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py index 64ce44143..d008f6e31 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -17,8 +17,8 @@ 'reco_energy': 'ENERGY', 'true_alt': 'MC_ALT', 'true_az': 'MC_AZ', - 'pointing_alt': 'MC_ALT', - 'pointing_az': 'MC_AZ', + 'pointing_alt': 'PNT_ALT', + 'pointing_az': 'PNT_AZ', 'reco_alt': 'ALT', 'reco_az': 'AZ', 'gh_score': 'GH_MVA', diff --git a/pyirf/irf/tests/test_effective_area.py b/pyirf/irf/tests/test_effective_area.py index 9dc5a0f3e..e995f0a78 100644 --- a/pyirf/irf/tests/test_effective_area.py +++ b/pyirf/irf/tests/test_effective_area.py @@ -17,14 +17,13 @@ def test_effective_area(): ) - 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(100, 5)), + '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 @@ -41,4 +40,4 @@ def test_pointlike_effective_area(): assert area.shape == (len(true_energy_bins) - 1, ) assert area.unit == u.m**2 - assert u.allclose(area, [100, 100] * u.m**2) + assert u.allclose(area, [100, 10] * u.m**2) diff --git a/pyirf/utils.py b/pyirf/utils.py index 79d4b33fa..1bf89ccb0 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -8,9 +8,9 @@ def is_scalar(val): return np.array(val, copy=False).shape == tuple() -def calculate_theta(events): +def calculate_theta(events, assumed_source_az, assumed_source_alt): theta = angular_separation( - events['true_az'], events['true_alt'], + assumed_source_az, assumed_source_alt, events['reco_az'], events['reco_alt'], ) From 80b3583ceebd143698f5faa16a11b675c5531b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 12:49:22 +0200 Subject: [PATCH 057/105] Start updating the docs --- docs/AUTHORS.rst | 18 ++++---- docs/changelog.rst | 20 ++++---- docs/contribute.rst | 63 ++++++++++++++++++++++++++ docs/contribute/index.rst | 60 ------------------------ docs/contribute/repo.rst | 55 ---------------------- docs/index.rst | 6 +-- docs/io/index.rst | 23 ++-------- pyirf/benchmarks/angular_resolution.py | 1 - pyirf/io/eventdisplay.py | 10 +++- 9 files changed, 96 insertions(+), 160 deletions(-) create mode 100644 docs/contribute.rst delete mode 100644 docs/contribute/index.rst delete mode 100644 docs/contribute/repo.rst diff --git a/docs/AUTHORS.rst b/docs/AUTHORS.rst index 67cbfa798..1c74352f5 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 `__, +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/changelog.rst b/docs/changelog.rst index ebd8f0f16..74a40a452 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-alpha `__ (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/contribute.rst b/docs/contribute.rst new file mode 100644 index 000000000..6fd3839be --- /dev/null +++ b/docs/contribute.rst @@ -0,0 +1,63 @@ +.. _contribute: + +How to contribute +================= + + +Issue Tracker +------------- + +We use the `GitHub issue tracker `__. + +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. +- `CTA IRF working group wiki (internal) `__ + +Benchmarks +---------- + +- `Comparison with EventDisplay <../notebooks/comparison_with_EventDisplay.ipynb>`__ | *comparison_with_EventDisplay.ipynb* diff --git a/docs/contribute/index.rst b/docs/contribute/index.rst deleted file mode 100644 index 9baf73730..000000000 --- a/docs/contribute/index.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. _contribute: - -How to contribute -================= - -.. toctree:: - :hidden: - - repo.rst - -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 <../notebooks/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/index.rst b/docs/index.rst index 2c5f064fc..04de6d03f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,13 +44,13 @@ which this documentation is linked. install/index usage/index notebooks/index - contribute/index + contribute changelog AUTHORS -.. _pyirf_structure: +.. _pyirf_api_docs: .. toctree:: - :caption: Structure + :caption: API Documentation :maxdepth: 1 irf/index 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/pyirf/benchmarks/angular_resolution.py b/pyirf/benchmarks/angular_resolution.py index 06bc965c2..c53c25f11 100644 --- a/pyirf/benchmarks/angular_resolution.py +++ b/pyirf/benchmarks/angular_resolution.py @@ -7,7 +7,6 @@ ONE_SIGMA_PERCENTILE = norm.cdf(1) - norm.cdf(-1) -print(f'{ONE_SIGMA_PERCENTILE:.2%}') def angular_resolution( diff --git a/pyirf/io/eventdisplay.py b/pyirf/io/eventdisplay.py index d008f6e31..86ee77646 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -28,8 +28,14 @@ def read_eventdisplay_fits(infile): """ - Read an DL2 Fits file as produced by the DL2 converter from root - here: https://github.com/Eventdisplay/Converters/blob/master/DL2/generate_DL2_file.py + 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 ------- From 1ecef0ed5a05e999d4219db0d6aa75688769921d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 13:00:47 +0200 Subject: [PATCH 058/105] Fix duplicated changelog entry --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 74a40a452..73b23b375 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,8 +33,8 @@ We use a one-line description of every pull request. .. _pyirf_0p3_release: -`0.1.0-alpha `__ (2020-09-16) ------ +`0.1.0 `__ (2020-09-16) +------------------------------------------------------------------------------------- This is a pre-release. From df591b208eafbb4d97496b3df5028f17098f87d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 13:17:11 +0200 Subject: [PATCH 059/105] Add IRF description document --- docs/contribute.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contribute.rst b/docs/contribute.rst index 6fd3839be..8240e2874 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -57,6 +57,8 @@ Please also have a look at the which also describe the IRF formats and their definitions. - `CTA IRF working group wiki (internal) `__ +- `CTA IRF Description Document for Prod3b (internal) `__ + Benchmarks ---------- From 7d168e6f8bdbe2c15f3e866fdb9255afb5612130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 13:31:04 +0200 Subject: [PATCH 060/105] Clear up install docs --- docs/index.rst | 2 +- docs/install.rst | 42 +++++++++++++++++++++++++++++++++++++ docs/install/basic.rst | 29 -------------------------- docs/install/index.rst | 47 ------------------------------------------ 4 files changed, 43 insertions(+), 77 deletions(-) create mode 100644 docs/install.rst delete mode 100644 docs/install/basic.rst delete mode 100644 docs/install/index.rst diff --git a/docs/index.rst b/docs/index.rst index 04de6d03f..2db5b699c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,7 +41,7 @@ which this documentation is linked. :caption: Overview :maxdepth: 1 - install/index + install usage/index notebooks/index contribute diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 000000000..ad0fb8bbf --- /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 211e37d0d..000000000 --- a/docs/install/basic.rst +++ /dev/null @@ -1,29 +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: - - * 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 From 9135dc004b110c6d11c0973acb72c2ef5b6359cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 13:41:50 +0200 Subject: [PATCH 061/105] Remove leftover file --- docs/install.rst | 4 ++-- docs/install/developer.rst | 20 -------------------- 2 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 docs/install/developer.rst diff --git a/docs/install.rst b/docs/install.rst index ad0fb8bbf..0069fc02b 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -4,7 +4,7 @@ Installation ============ -``pyirf`` requires Python >= 3.6 and the packages as defined in the ``setup.py``. +``pyirf`` requires Python ≥3.6 and the packages as defined in the ``setup.py``. Core dependencies are * ``numpy`` @@ -14,7 +14,7 @@ Core dependencies are Dependencies for development, like unit tests and building the documentation are defined in ``extras``. -Installing a released Version +Installing a released version ----------------------------- diff --git a/docs/install/developer.rst b/docs/install/developer.rst deleted file mode 100644 index e4bca55ed..000000000 --- a/docs/install/developer.rst +++ /dev/null @@ -1,20 +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: - - * start using *pyirf* (:ref:`usage`), - * for bugs and new features, please contribute to the project (:ref:`contribute`). From 7f76ceeb6f3112288dd3a3467f7e21cb13431386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 13:42:10 +0200 Subject: [PATCH 062/105] Cleanup dependencies --- environment.yml | 1 - setup.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/environment.yml b/environment.yml index e833e8512..f62a3b622 100644 --- a/environment.yml +++ b/environment.yml @@ -23,6 +23,5 @@ dependencies: - pip: - rinohtype - nbsphinx - - gammapy~=0.8.0 - sphinx_automodapi - uproot diff --git a/setup.py b/setup.py index 3fcbd7322..1805334e2 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,6 @@ extras_require = { 'docs': [ - 'rinohtype', 'sphinx', 'sphinx_rtd_theme', 'sphinx_automodapi', @@ -34,7 +33,6 @@ "scipy", "tqdm", "tables", - "gammapy~=0.17", ], extras_require=extras_require, ) From 60e8bdaf65ce1649afc3652f4677b8ac1d61c527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 14:41:19 +0200 Subject: [PATCH 063/105] Change usage instruction to describe library-like usage --- docs/contribute.rst | 2 + docs/index.rst | 18 +++----- docs/introduction.rst | 86 +++++++++++++++++++++++++++++++++++++ docs/usage/EventDisplay.rst | 69 ----------------------------- docs/usage/index.rst | 41 ------------------ docs/usage/lstchain_irf.rst | 31 ------------- docs/usage/protopipe.rst | 5 --- 7 files changed, 94 insertions(+), 158 deletions(-) create mode 100644 docs/introduction.rst delete mode 100644 docs/usage/EventDisplay.rst delete mode 100644 docs/usage/index.rst delete mode 100644 docs/usage/lstchain_irf.rst delete mode 100644 docs/usage/protopipe.rst diff --git a/docs/contribute.rst b/docs/contribute.rst index 8240e2874..cbd167540 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -55,10 +55,12 @@ 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 ---------- diff --git a/docs/index.rst b/docs/index.rst index 2db5b699c..24bcf1b2e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,22 +36,23 @@ 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 - usage/index + introduction notebooks/index contribute changelog AUTHORS -.. _pyirf_api_docs: + .. toctree:: - :caption: API Documentation :maxdepth: 1 + :caption: API Documentation + :name: _pyirf_api_docs irf/index sensitivity @@ -63,16 +64,9 @@ which this documentation is linked. resources/index -Reference/API -------------- - -.. automodapi:: pyirf - :no-inheritance-diagram: - Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 100644 index 000000000..f4c4f46c5 --- /dev/null +++ b/docs/introduction.rst @@ -0,0 +1,86 @@ +.. _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 documentation for the +`Data Formats for Gamma-Ray Astronomy Specification `_ +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 `Data Formats for Gamma-Ray Astronomy Specification `_ + + +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 | + +----------------+--------+---------------------------------------------------------+ + | 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 | + +----------------+--------+---------------------------------------------------------+ diff --git a/docs/usage/EventDisplay.rst b/docs/usage/EventDisplay.rst deleted file mode 100644 index 37bdee00e..000000000 --- a/docs/usage/EventDisplay.rst +++ /dev/null @@ -1,69 +0,0 @@ -.. _EventDisplay: - -============================================= -How to build DL3 data from EventDisplay files -============================================= - -.. toctree:: - :hidden: - - ../notebooks/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 sensitivity and IRFs you will need to - -- launch the ``examples/calculate_eventdisplay_irfs.py`` script. - - -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 -====================================== From 8c607262c4f109a571e184f056eb8453fbb11d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 15:05:43 +0200 Subject: [PATCH 064/105] Fix duplicated reference --- docs/introduction.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index f4c4f46c5..e34105e9a 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -11,7 +11,7 @@ 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 documentation for the -`Data Formats for Gamma-Ray Astronomy Specification `_ +`gadf_irfs`_ or the `ctools documentation on IRFs `. @@ -41,7 +41,7 @@ All functions take ``numpy`` arrays, astropy quantities or astropy tables for th 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 `Data Formats for Gamma-Ray Astronomy Specification `_ +to FITS files following the `gadf_irfs`_ DL2 event lists @@ -84,3 +84,6 @@ Most functions only need a small subgroup of these columns. +----------------+--------+---------------------------------------------------------+ | multiplicity | | Number of telescopes used in the reconstruction | +----------------+--------+---------------------------------------------------------+ + + +.. _gadf_irfs: https://indico.e5.physik.tu-dortmund.de/rooms/rooms From 4179fe29b9ce62a958f4077f0a3f2fe8a15657da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 15:17:32 +0200 Subject: [PATCH 065/105] Store radmax table --- examples/calculate_eventdisplay_irfs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 588495b30..6e05ae306 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -251,6 +251,10 @@ def main(): 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) From 8c7816bfe788c24b2e20fead89013792902f1868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 16:06:50 +0200 Subject: [PATCH 066/105] Add download_test_data --- .travis.yml | 4 +--- download_test_data.sh | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100755 download_test_data.sh diff --git a/.travis.yml b/.travis.yml index 825858404..bbd7e43ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,9 +20,7 @@ matrix: - CONDA=true before_install: - - travis_wait 15 curl -sSfL -o data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download - - unzip data.zip - - mv eventdisplay_dl2 data + - 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 diff --git a/download_test_data.sh b/download_test_data.sh new file mode 100755 index 000000000..4824991d2 --- /dev/null +++ b/download_test_data.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +curl -sSfL -o data.zip https://nextcloud.e5.physik.tu-dortmund.de/index.php/s/Cstsf8MWZjnz92L/download +unzip data.zip +mv eventdisplay_dl2 data From 0cca4960e0281a507dbc930d751ba98c88fba4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 16:07:43 +0200 Subject: [PATCH 067/105] Use correct gadf link --- pyirf/io/gadf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyirf/io/gadf.py b/pyirf/io/gadf.py index e3aa175dd..0265cc7db 100644 --- a/pyirf/io/gadf.py +++ b/pyirf/io/gadf.py @@ -16,7 +16,7 @@ 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['HDUDOC'] = 'https://gamma-astro-data-formats.readthedocs.io' DEFAULT_HEADER['HDUVERS'] = '0.2' DEFAULT_HEADER['HDUCLASS'] = 'GADF' From bb3affdb5f40adc036ead2b51f1a7f7d29cd5f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 16:08:12 +0200 Subject: [PATCH 068/105] Remove zip after unpacking --- download_test_data.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/download_test_data.sh b/download_test_data.sh index 4824991d2..08605c637 100755 --- a/download_test_data.sh +++ b/download_test_data.sh @@ -2,4 +2,5 @@ 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 From f3afad812825d2608ae2bfd923448e149d165a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 16:31:53 +0200 Subject: [PATCH 069/105] Fix links in introduction --- docs/introduction.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index e34105e9a..12709e088 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -10,9 +10,8 @@ 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 documentation for the -`gadf_irfs`_ -or the `ctools documentation on IRFs `. +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: @@ -41,7 +40,7 @@ All functions take ``numpy`` arrays, astropy quantities or astropy tables for th 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 `gadf_irfs`_ +to FITS files following the `Specification of the Data Formats for Gamma-Ray Astronomy`_ DL2 event lists @@ -86,4 +85,4 @@ Most functions only need a small subgroup of these columns. +----------------+--------+---------------------------------------------------------+ -.. _gadf_irfs: https://indico.e5.physik.tu-dortmund.de/rooms/rooms +.. _Specification of the Data Formats for Gamma-Ray Astronomy: https://gamma-astro-data-formats.readthedocs.io From 7b6e8fb484b27d514650b2c50c39cbc20d90c354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 16:42:40 +0200 Subject: [PATCH 070/105] Remove pdf, mention github projects --- docs/conf.py | 2 +- docs/contribute.rst | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index bf97f628c..cbcb8649f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,7 +59,7 @@ # nbsphinx # nbsphinx_execute = "never" nbsphinx_execute_arguments = [ - "--InlineBackend.figure_formats={'svg', 'pdf'}", + "--InlineBackend.figure_formats={'svg', }", "--InlineBackend.rc={'figure.dpi': 96}", ] diff --git a/docs/contribute.rst b/docs/contribute.rst index cbd167540..fd10dc3d0 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -7,7 +7,8 @@ How to contribute Issue Tracker ------------- -We use the `GitHub 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. From 537b8219b9e3b5ff1fe5cbdc0b309340a45268eb Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:00:54 +0200 Subject: [PATCH 071/105] Add missing docstrings in pyirf/utils.py --- pyirf/utils.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/pyirf/utils.py b/pyirf/utils.py index 1bf89ccb0..2ebee70d1 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -8,7 +8,25 @@ def is_scalar(val): return np.array(val, copy=False).shape == tuple() +@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'], @@ -18,6 +36,19 @@ def calculate_theta(events, assumed_source_az, assumed_source_alt): 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'], @@ -33,7 +64,8 @@ def check_histograms(hist1, hist2, key='reco_energy'): Parameters ---------- hist1: ``~astropy.table.Table`` - First histogram table, as created by ``~pyirf.binning.create_histogram_table`` + First histogram table, as created by + ``~pyirf.binning.create_histogram_table`` hist2: ``~astropy.table.Table`` Second histogram table ''' @@ -48,5 +80,18 @@ def check_histograms(hist1, hist2, key='reco_energy'): def cone_solid_angle(angle): - '''Calculate the solid angle of a view cone with opening angle ``angle``.''' - return 2 * np.pi * (1 - np.cos(angle)) * u.sr + '''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 From c94750943e3440b91633284995255ad81038be8d Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:10:49 +0200 Subject: [PATCH 072/105] Add __all__ magic variable --- pyirf/utils.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pyirf/utils.py b/pyirf/utils.py index 2ebee70d1..5ca6c05f2 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -2,10 +2,29 @@ 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''' - return np.array(val, copy=False).shape == tuple() + '''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) From fba1cf901e201892d96d84ee5aeb0548e22b2faa Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:11:32 +0200 Subject: [PATCH 073/105] Added utils section of API documentation. --- docs/utils.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/utils.rst 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: From 0253289ed8fe680bd633e7f9153857193df6527f Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:16:09 +0200 Subject: [PATCH 074/105] Update main index --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 24bcf1b2e..e955d8b77 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,7 +61,7 @@ which this documentation is linked. spectral binning io/index - resources/index + utils/index Indices and tables From 583a44653cc4030d0e50470b33370b1e48db9620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 17:06:41 +0200 Subject: [PATCH 075/105] Add docstrings and creation date to GADF HDU --- pyirf/io/gadf.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/pyirf/io/gadf.py b/pyirf/io/gadf.py index 0265cc7db..010d2698b 100644 --- a/pyirf/io/gadf.py +++ b/pyirf/io/gadf.py @@ -2,6 +2,7 @@ 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__ @@ -31,6 +32,29 @@ 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) @@ -44,6 +68,7 @@ def create_aeff2d_hdu( header['HDUCLAS2'] = 'EFF_AREA' header['HDUCLAS3'] = 'POINT-LIKE' if point_like else 'FULL-ENCLOSURE' header['HDUCLAS4'] = 'AEFF_2D' + header['DATE'] = Time.now(scale='utc').iso _add_header_cards(header, **header_cards) return BinTableHDU(aeff, header=header, name=extname) @@ -58,6 +83,32 @@ def create_psf_table_hdu( 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), @@ -75,6 +126,7 @@ def create_psf_table_hdu( header['HDUCLAS2'] = 'PSF' header['HDUCLAS3'] = 'POINT-LIKE' if point_like else 'FULL-ENCLOSURE' header['HDUCLAS4'] = 'PSF_TABLE' + header['DATE'] = Time.now(scale='utc').iso _add_header_cards(header, **header_cards) return BinTableHDU(psf, header=header, name=extname) @@ -92,6 +144,32 @@ def create_energy_dispersion_hdu( 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), @@ -109,6 +187,7 @@ def create_energy_dispersion_hdu( header['HDUCLAS2'] = 'EDISP' header['HDUCLAS3'] = 'POINT-LIKE' if point_like else 'FULL-ENCLOSURE' header['HDUCLAS4'] = 'EDISP_2D' + header['DATE'] = Time.now(scale='utc').iso _add_header_cards(header, **header_cards) return BinTableHDU(psf, header=header, name=extname) @@ -123,6 +202,27 @@ def create_rad_max_hdu( 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), @@ -137,6 +237,7 @@ def create_rad_max_hdu( header['HDUCLAS2'] = 'RAD_MAX' header['HDUCLAS3'] = 'POINT-LIKE' header['HDUCLAS4'] = 'RAD_MAX_2D' + header['DATE'] = Time.now(scale='utc').iso _add_header_cards(header, **header_cards) return BinTableHDU(rad_max_table, header=header, name=extname) From 569b8bc9b7c8f56975b35dd13bb7a88523d7af67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 17:24:23 +0200 Subject: [PATCH 076/105] More docstrings --- docs/AUTHORS.rst | 2 +- docs/introduction.rst | 53 ++++++++++++++++++---------------- pyirf/io/gadf.py | 28 +++++++++--------- pyirf/irf/effective_area.py | 12 ++++---- pyirf/irf/energy_dispersion.py | 30 +++++++++++++++++++ 5 files changed, 79 insertions(+), 46 deletions(-) diff --git a/docs/AUTHORS.rst b/docs/AUTHORS.rst index 1c74352f5..39c519851 100644 --- a/docs/AUTHORS.rst +++ b/docs/AUTHORS.rst @@ -12,7 +12,7 @@ or run git shortlog -sne -``pyirf`` started as part of `protopipe `__, +``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/introduction.rst b/docs/introduction.rst index 12709e088..80b68b7e8 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -58,31 +58,34 @@ 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 | - +----------------+--------+---------------------------------------------------------+ - | 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 | - +----------------+--------+---------------------------------------------------------+ + +-------------------+--------+----------------------------------------------------+ + | 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/pyirf/io/gadf.py b/pyirf/io/gadf.py index 010d2698b..c81e57bae 100644 --- a/pyirf/io/gadf.py +++ b/pyirf/io/gadf.py @@ -39,11 +39,11 @@ def create_aeff2d_hdu( Parameters ---------- - effective_area: ``astropy.units.Quantity``[area] + 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] + true_energy_bins: astropy.units.Quantity[energy] Bin edges in true energy - fov_offset_bins: ``astropy.units.Quantity``[angle] + 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 @@ -90,14 +90,14 @@ def create_psf_table_hdu( Parameters ---------- - psf: ``astropy.units.Quantity``[(solid angle)^-1] + 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] + true_energy_bins: astropy.units.Quantity[energy] Bin edges in true energy - source_offset_bins: ``astropy.units.Quantity``[angle] + source_offset_bins: astropy.units.Quantity[angle] Bin edges in the source offset. - fov_offset_bins: ``astropy.units.Quantity``[angle] + 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 @@ -151,14 +151,14 @@ def create_energy_dispersion_hdu( Parameters ---------- - energy_dispersion: ``numpy.ndarray`` + 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] + true_energy_bins: astropy.units.Quantity[energy] Bin edges in true energy - migration_bins: ``numpy.ndarray`` + migration_bins: numpy.ndarray Bin edges for the relative energy migration (``reco_energy / true_energy``) - fov_offset_bins: ``astropy.units.Quantity``[angle] + 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 @@ -209,12 +209,12 @@ def create_rad_max_hdu( Parameters ---------- - reco_energy_bins: ``astropy.units.Quantity``[energy] + reco_energy_bins: astropy.units.Quantity[energy] Bin edges in reconstructed energy - fov_offset_bins: ``astropy.units.Quantity``[angle] + 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] + 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 diff --git a/pyirf/irf/effective_area.py b/pyirf/irf/effective_area.py index f72e51701..2854f5595 100644 --- a/pyirf/irf/effective_area.py +++ b/pyirf/irf/effective_area.py @@ -10,11 +10,11 @@ def effective_area(n_selected, n_simulated, area): Parameters ---------- - n_selected: int or ``~numpy.ndarray``[int] + n_selected: int or numpy.ndarray[int] The number of surviving (e.g. triggered, analysed, after cuts) - n_simulated: int or ``~numpy.ndarray``[int] + n_simulated: int or numpy.ndarray[int] The total number of events simulated - area: ``~astropy.units.Quantity``[area] + area: astropy.units.Quantity[area] Area in which particle's core position was simulated ''' return (n_selected / n_simulated) * area @@ -27,11 +27,11 @@ def point_like_effective_area(selected_events, simulation_info, true_energy_bins Parameters ---------- - selected_events: ``~astropy.table.QTable`` + selected_events: astropy.table.QTable DL2 events table, required columns for this function: `true_energy`. - simulation_info: ``~pyirf.simulations.SimulatedEventsInfo`` + simulation_info: pyirf.simulations.SimulatedEventsInfo The overall statistics of the simulated events - true_energy_bins: ``astropy.units.Quantity``[energy] + true_energy_bins: astropy.units.Quantity[energy] The bin edges in which to calculate effective area. ''' area = np.pi * simulation_info.max_impact**2 diff --git a/pyirf/irf/energy_dispersion.py b/pyirf/irf/energy_dispersion.py index 18aeac4eb..810c918de 100644 --- a/pyirf/irf/energy_dispersion.py +++ b/pyirf/irf/energy_dispersion.py @@ -2,6 +2,11 @@ import astropy.units as u +__all__ = [ + 'energy_dispersion', +] + + def _normalize_hist(hist): # (N_E, N_MIGRA, N_FOV) # (N_E, N_FOV) @@ -22,6 +27,31 @@ def energy_dispersion( 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( From d403011e4274ffbe7a656919f4aca9104e6d5fef Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:25:15 +0200 Subject: [PATCH 077/105] Fix typo in main index --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index e955d8b77..24f9ac178 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,7 +61,7 @@ which this documentation is linked. spectral binning io/index - utils/index + utils Indices and tables From 847fe5a7a2c1140c82b9c80c17653c0f15fd94e4 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:25:52 +0200 Subject: [PATCH 078/105] Remove old API section for resources --- docs/resources/index.rst | 147 --------------------------------------- 1 file changed, 147 deletions(-) delete mode 100644 docs/resources/index.rst 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! From 2a7194b6beff14d4eb99477635d1b1a0f093acf5 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:26:09 +0200 Subject: [PATCH 079/105] Update authors copyright --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cbcb8649f..d58d5c98c 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__ From 8ea3d52fa642d8ddebaad3d0b74b8ab463fc8b68 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 17:32:25 +0200 Subject: [PATCH 080/105] Fix headings in IRF index API docs --- docs/irf/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/irf/index.rst b/docs/irf/index.rst index ff17a6364..d0c9de164 100644 --- a/docs/irf/index.rst +++ b/docs/irf/index.rst @@ -5,7 +5,7 @@ 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 @@ -13,7 +13,7 @@ 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 @@ -21,7 +21,7 @@ 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. From 062ed3594c6ed37b822ffa0c46807981ca6645a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 18:00:36 +0200 Subject: [PATCH 081/105] Fix creation time --- pyirf/io/gadf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyirf/io/gadf.py b/pyirf/io/gadf.py index c81e57bae..628abda09 100644 --- a/pyirf/io/gadf.py +++ b/pyirf/io/gadf.py @@ -68,7 +68,7 @@ def create_aeff2d_hdu( header['HDUCLAS2'] = 'EFF_AREA' header['HDUCLAS3'] = 'POINT-LIKE' if point_like else 'FULL-ENCLOSURE' header['HDUCLAS4'] = 'AEFF_2D' - header['DATE'] = Time.now(scale='utc').iso + header['DATE'] = Time.now().utc.iso _add_header_cards(header, **header_cards) return BinTableHDU(aeff, header=header, name=extname) @@ -126,7 +126,7 @@ def create_psf_table_hdu( header['HDUCLAS2'] = 'PSF' header['HDUCLAS3'] = 'POINT-LIKE' if point_like else 'FULL-ENCLOSURE' header['HDUCLAS4'] = 'PSF_TABLE' - header['DATE'] = Time.now(scale='utc').iso + header['DATE'] = Time.now().utc.iso _add_header_cards(header, **header_cards) return BinTableHDU(psf, header=header, name=extname) @@ -187,7 +187,7 @@ def create_energy_dispersion_hdu( header['HDUCLAS2'] = 'EDISP' header['HDUCLAS3'] = 'POINT-LIKE' if point_like else 'FULL-ENCLOSURE' header['HDUCLAS4'] = 'EDISP_2D' - header['DATE'] = Time.now(scale='utc').iso + header['DATE'] = Time.now().utc.iso _add_header_cards(header, **header_cards) return BinTableHDU(psf, header=header, name=extname) @@ -237,7 +237,7 @@ def create_rad_max_hdu( header['HDUCLAS2'] = 'RAD_MAX' header['HDUCLAS3'] = 'POINT-LIKE' header['HDUCLAS4'] = 'RAD_MAX_2D' - header['DATE'] = Time.now(scale='utc').iso + header['DATE'] = Time.now().utc.iso _add_header_cards(header, **header_cards) return BinTableHDU(rad_max_table, header=header, name=extname) From 4438b67be9eb72a0bcd9088f69c455b2a608cffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 18:01:11 +0200 Subject: [PATCH 082/105] Remove time arguments from sensitivity, add docstrings --- examples/calculate_eventdisplay_irfs.py | 2 +- pyirf/cut_optimization.py | 1 - pyirf/sensitivity.py | 59 ++++++++++++++++++------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 6e05ae306..ed7eaa8b6 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -174,7 +174,7 @@ def main(): 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, t_obs=T_OBS) + 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): diff --git a/pyirf/cut_optimization.py b/pyirf/cut_optimization.py index 8b9125506..6cf759556 100644 --- a/pyirf/cut_optimization.py +++ b/pyirf/cut_optimization.py @@ -59,7 +59,6 @@ def optimize_gh_cut(signal, background, bins, cut_values, op, alpha=1.0, progres signal_hist, background_hist, alpha=alpha, - t_obs=50 * u.hour, ) sensitivities.append(sensitivity) diff --git a/pyirf/sensitivity.py b/pyirf/sensitivity.py index 470f6d6e0..9fd5a5aa7 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -1,3 +1,6 @@ +''' +Functions to calculate sensitivity +''' import astropy.units as u import numpy as np from scipy.optimize import brentq @@ -17,13 +20,10 @@ log = logging.getLogger(__name__) -@u.quantity_input(t_obs=u.hour, t_ref=u.hour) def relative_sensitivity( n_on, n_off, alpha, - t_obs, - t_ref=u.Quantity(50, u.hour), target_significance=5, significance_function=li_ma_significance, initial_guess=0.01, @@ -31,9 +31,9 @@ def relative_sensitivity( ''' Calculate the relative sensitivity defined as the flux relative to the reference source that is detectable with - significance ``target_significance`` in time ``t_ref``. + significance ``target_significance``. - Given measured ``n_on`` and ``n_off`` during a time period ``t_obs``, + 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``. @@ -41,6 +41,8 @@ def relative_sensitivity( 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 ---------- @@ -51,10 +53,6 @@ def relative_sensitivity( alpha: float Scaling factor between on and off observations. 1 / number of off regions for wobble observations. - t_obs: astropy.units.Quantity of type time - Total observation time - t_ref: astropy.units.Quantity of type time - Reference time for the detection significance: float Significance necessary for a detection significance_function: function @@ -68,10 +66,6 @@ def relative_sensitivity( initial_guess: float Initial guess for the root finder ''' - ratio = (t_ref / t_obs).to(u.one) - n_on = n_on * ratio - n_off = n_off * ratio - n_background = n_off * alpha n_signal = n_on - n_background @@ -110,16 +104,48 @@ def equation(relative_flux): return result -@u.quantity_input(t_obs=u.hour, t_ref=u.hour) def calculate_sensitivity( signal_hist, background_hist, alpha, - t_obs, - t_ref=u.Quantity(50, u.hour), 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) @@ -139,7 +165,6 @@ def calculate_sensitivity( n_on=n_signal_hist + alpha * n_background_hist, n_off=n_background_hist, alpha=alpha, - t_obs=t_obs, ) for n_signal_hist, n_background_hist in zip(signal_hist['n_weighted'], background_hist['n_weighted']) ] From c2edc3749d86a10efa9456ae594be614400f049a Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 18:18:13 +0200 Subject: [PATCH 083/105] Add docstrings for angular resolution and energy resolution/bias. --- pyirf/benchmarks/angular_resolution.py | 19 ++++++++ pyirf/benchmarks/energy_bias_resolution.py | 57 ++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/pyirf/benchmarks/angular_resolution.py b/pyirf/benchmarks/angular_resolution.py index c53c25f11..4fd0c576a 100644 --- a/pyirf/benchmarks/angular_resolution.py +++ b/pyirf/benchmarks/angular_resolution.py @@ -13,6 +13,25 @@ def angular_resolution( events, true_energy_bins, ): + """ + Calculate the angular resolutionself. + + This implementation corresponds to the 68% containment of the angular + distance distribution. + + Parameters + ---------- + events : astropy.table.QTable + Astropy Table object containing the reconstructed events information. + true_energy_bins: numpy.ndarray(dtype=float, ndim=1) + Bin edges in true energy. + + Returns + ------- + result : astropy.table.Table + Table containing the 68% containment of the angular + distance distribution per each true energy bin. + """ # create a table to make use of groupby operations table = Table(events[['true_energy', 'theta']]) diff --git a/pyirf/benchmarks/energy_bias_resolution.py b/pyirf/benchmarks/energy_bias_resolution.py index b3bc95f88..0bfaeab10 100644 --- a/pyirf/benchmarks/energy_bias_resolution.py +++ b/pyirf/benchmarks/energy_bias_resolution.py @@ -9,14 +9,45 @@ NORM_LOWER_SIGMA = norm(0, 1).cdf(-1) -def resolution_abelardo(rel_error): - return np.percentile(np.abs(rel_error), 68) +def energy_resolution_absolute_68(rel_error): + """Calculate the energy resolution as the central 68% interval. + + Utility function for pyirf.benchmarks.energy_bias_resolution + + Parameters + ---------- + rel_error : numpy.ndarray(dtype=float, ndim=1) + Array of float on which the quantile is calculated. + + Returns + ------- + resolution: numpy.ndarray(dtype=float, ndim=1) + Array containing the 68% intervals + """ + resolution = np.percentile(np.abs(rel_error), 68) + return resolution def inter_quantile_distance(rel_error): + """Calculate the energy resolution as the half of the 68% containment. + + Percentile equivalent of the standard deviation. + Utility function for pyirf.benchmarks.energy_bias_resolution + + Parameters + ---------- + rel_error : numpy.ndarray(dtype=float, ndim=1) + Array of float on which the quantile is calculated. + + Returns + ------- + resolution: numpy.ndarray(dtype=float, ndim=1) + Array containing the resolution values. + """ upper_sigma = np.percentile(rel_error, 100 * NORM_UPPER_SIGMA) lower_sigma = np.percentile(rel_error, 100 * NORM_LOWER_SIGMA) - return 0.5 * (upper_sigma - lower_sigma) + resolution = 0.5 * (upper_sigma - lower_sigma) + return resolution def energy_bias_resolution( @@ -25,6 +56,26 @@ def energy_bias_resolution( bias_function=np.median, resolution_function=inter_quantile_distance ): + """ + Calculate bias and energy resolution. + + Parameters + ---------- + events : astropy.table.QTable + Astropy Table object containing the reconstructed events information. + true_energy_bins : numpy.ndarray(dtype=float, ndim=1) + Bin edges in true energy. + bias_function : callable + Function used to calculate the energy bias + resolution_function : callable + Function used to calculate the energy resolution + + Returns + ------- + result : astropy.table.Table + Table containing the energy bias and resolution + per each bin in true energy. + """ # create a table to make use of groupby operations table = Table(events[['true_energy', 'reco_energy']]) From dc3b651c87ca1b86b1afa1247de8fc6e89ef98e9 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 18:20:50 +0200 Subject: [PATCH 084/105] Fix typo in angular_resolution docstring --- pyirf/benchmarks/angular_resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyirf/benchmarks/angular_resolution.py b/pyirf/benchmarks/angular_resolution.py index 4fd0c576a..4b2d7bc8e 100644 --- a/pyirf/benchmarks/angular_resolution.py +++ b/pyirf/benchmarks/angular_resolution.py @@ -14,7 +14,7 @@ def angular_resolution( true_energy_bins, ): """ - Calculate the angular resolutionself. + Calculate the angular resolution. This implementation corresponds to the 68% containment of the angular distance distribution. From ea5d950dbb20dc44af9a870cb03b65a04fdc4358 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Fri, 25 Sep 2020 18:40:55 +0200 Subject: [PATCH 085/105] Add docstring to pyirf.binning.create_histogram_table --- pyirf/binning.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pyirf/binning.py b/pyirf/binning.py index 5b43b0e96..0f307de40 100644 --- a/pyirf/binning.py +++ b/pyirf/binning.py @@ -100,6 +100,24 @@ def calculate_bin_indices(data, bins): 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:] From fb8d8c67c50fe7ce7481ee8a7637cda5ff0aeaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 19:11:20 +0200 Subject: [PATCH 086/105] More docs --- docs/spectral.rst | 4 +- pyirf/benchmarks/energy_bias_resolution.py | 8 +- pyirf/irf/effective_area.py | 6 + pyirf/spectral.py | 216 +++++++++++++++++---- 4 files changed, 186 insertions(+), 48 deletions(-) diff --git a/docs/spectral.rst b/docs/spectral.rst index 6073309eb..1b8189382 100644 --- a/docs/spectral.rst +++ b/docs/spectral.rst @@ -7,5 +7,7 @@ Event Weighting and Spectrum Definitions Reference/API ------------- + .. automodapi:: pyirf.spectral - :no-inheritance-diagram: + :no-inheritance-diagram: + :include-all-objects: diff --git a/pyirf/benchmarks/energy_bias_resolution.py b/pyirf/benchmarks/energy_bias_resolution.py index 0bfaeab10..933326db2 100644 --- a/pyirf/benchmarks/energy_bias_resolution.py +++ b/pyirf/benchmarks/energy_bias_resolution.py @@ -61,13 +61,13 @@ def energy_bias_resolution( Parameters ---------- - events : astropy.table.QTable + events: astropy.table.QTable Astropy Table object containing the reconstructed events information. - true_energy_bins : numpy.ndarray(dtype=float, ndim=1) + true_energy_bins: numpy.ndarray(dtype=float, ndim=1) Bin edges in true energy. - bias_function : callable + bias_function: callable Function used to calculate the energy bias - resolution_function : callable + resolution_function: callable Function used to calculate the energy resolution Returns diff --git a/pyirf/irf/effective_area.py b/pyirf/irf/effective_area.py index 2854f5595..72c5c40a6 100644 --- a/pyirf/irf/effective_area.py +++ b/pyirf/irf/effective_area.py @@ -3,6 +3,12 @@ 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): ''' diff --git a/pyirf/spectral.py b/pyirf/spectral.py index 7fdea06a6..dd39c1634 100644 --- a/pyirf/spectral.py +++ b/pyirf/spectral.py @@ -3,35 +3,96 @@ ''' import astropy.units as u import numpy as np -from scipy.stats import norm +#: 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 -FLUX_UNIT = POINT_SOURCE_FLUX_UNIT / u.sr + +#: 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( - flux_normalization=[FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], + normalization=[DIFFUSE_FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], e_ref=u.TeV ) - def __init__(self, flux_normalization, spectral_index, e_ref=1 * u.TeV): - self.flux_normalization = flux_normalization - self.spectral_index = spectral_index + 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.flux_normalization - * (energy / self.e_ref) ** self.spectral_index + self.normalization + * (energy / self.e_ref) ** self.index ) @classmethod @@ -45,7 +106,7 @@ def from_simulation( ''' e_min = simulated_event_info.energy_min e_max = simulated_event_info.energy_max - spectral_index = simulated_event_info.spectral_index + index = simulated_event_info.spectral_index n_showers = simulated_event_info.n_showers viewcone = simulated_event_info.viewcone @@ -56,27 +117,49 @@ def from_simulation( A = np.pi * simulated_event_info.max_impact**2 - delta = e_max**(spectral_index + 1) - e_min**(spectral_index + 1) - nom = (spectral_index + 1) * e_ref**spectral_index * n_showers + 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( - flux_normalization=nom / denom, - spectral_index=spectral_index, + normalization=nom / denom, + index=index, e_ref=e_ref, ) def __repr__(self): - return f'{self.__class__.__name__}({self.flux_normalization} * (E / {self.e_ref})**{self.spectral_index}' + 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( - flux_normalization=[FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], + normalization=[DIFFUSE_FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], e_ref=u.TeV ) - def __init__(self, flux_normalization, a, b, e_ref=1 * u.TeV): - self.flux_normalization = flux_normalization + 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 @@ -84,22 +167,59 @@ def __init__(self, flux_normalization, a, b, e_ref=1 * u.TeV): @u.quantity_input(energy=u.TeV) def __call__(self, energy): e = (energy / self.e_ref).to_value(u.one) - return self.flux_normalization * e**(self.a + self.b * np.log10(e)) + return self.normalization * e**(self.a + self.b * np.log10(e)) def __repr__(self): - return f'{self.__class__.__name__}({self.flux_normalization} * (E / {self.e_ref})**({self.a} + {self.b} * log10(E / {self.e_ref}))' + 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( - flux_normalization=[FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], + normalization=[DIFFUSE_FLUX_UNIT, POINT_SOURCE_FLUX_UNIT], e_ref=u.TeV ) - def __init__(self, flux_normalization, spectral_index, e_ref, f, mu, sigma): + def __init__(self, normalization, index, e_ref, f, mu, sigma): super().__init__( - flux_normalization=flux_normalization, - spectral_index=spectral_index, + normalization=normalization, + index=index, e_ref=e_ref ) self.f = f @@ -117,46 +237,56 @@ def __call__(self, energy): gauss = np.exp(-0.5 * ((log10_e - self.mu) / self.sigma)**2) return power * (1 + self.f * (np.exp(gauss) - 1)) - -# 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 +#: 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( - flux_normalization=2.83e-11 / (u.TeV * u.cm**2 * u.s), - spectral_index=-2.62, + normalization=2.83e-11 / (u.TeV * u.cm**2 * u.s), + index=-2.62, e_ref=1 * u.TeV, ) -# 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 +#: 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( - flux_normalization=3.23e-11 / (u.TeV * u.cm**2 * u.s), + normalization=3.23e-11 / (u.TeV * u.cm**2 * u.s), a=-2.47, b=-0.24, ) -# (30.2) from "The Review of Particle Physics (2020)" -# https://pdg.lbl.gov/2020/reviews/rpp2020-rev-cosmic-rays.pdf +#: 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( - flux_normalization=1.8e4 / (u.GeV * u.m**2 * u.s * u.sr), - spectral_index=-2.7, + 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 +#: +#: From "Description of CTA Instrument Response Functions +#: (Production 3b Simulation), section 4.3.1 IRFDOC_PROTON_SPECTRUM = PowerLaw( - flux_normalization=9.8e-6 / (u.cm**2 * u.s * u.TeV * u.sr), - spectral_index=-2.62, + normalization=9.8e-6 / (u.cm**2 * u.s * u.TeV * u.sr), + index=-2.62, e_ref=1 * u.TeV, ) -# section 4.3.2 +#: 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( - flux_normalization=2.385e-9 / (u.TeV * u.cm**2 * u.s * u.sr), - spectral_index=-3.43, + 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, From 228e3d1d3c36f562d53c56e980c7b376d356ca7b Mon Sep 17 00:00:00 2001 From: Lukas Nickel Date: Fri, 25 Sep 2020 13:00:23 +0200 Subject: [PATCH 087/105] Add energy dispersion unit test --- pyirf/irf/tests/test_energy_dispersion.py | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 pyirf/irf/tests/test_energy_dispersion.py diff --git a/pyirf/irf/tests/test_energy_dispersion.py b/pyirf/irf/tests/test_energy_dispersion.py new file mode 100644 index 000000000..d17d90075 --- /dev/null +++ b/pyirf/irf/tests/test_energy_dispersion.py @@ -0,0 +1,42 @@ +import astropy.units as u +import numpy as np +from astropy.table import QTable + + +def test_energy_dispersion(): + from pyirf.irf import energy_dispersion + selected_events = QTable({ + 'reco_energy': np.concatenate([ + np.random.uniform(0.081, 0.099, size=3), + np.random.uniform(0.1, 0.119, size=7), + np.random.uniform(0.81, 0.99, size=5), + np.random.uniform(1.0, 1.19, size=5), + np.random.uniform(8.1, 9.9, size=8), + np.random.uniform(10.0, 10.9, size=2), + ])*u.TeV, + 'true_energy': np.concatenate([ + np.full(10, 0.1), + np.full(10, 1.0), + np.full(10, 10.0) + ])*u.TeV, + 'source_fov_offset': np.concatenate([ + np.full(20, 0.2), + np.full(10, 1.5) + ])*u.deg + }) + + true_energy_bins = np.array([0.1, 1.0, 10.0]) * u.TeV + fov_offset_bins = np.array([0, 1, 2]) * u.deg + migration_bins = np.array([0.8, 1.0, 1.2]) + + result = energy_dispersion( + selected_events, + true_energy_bins, + fov_offset_bins, + migration_bins) + + assert result.sum() == 3.0 + assert (result == np.array([ + [[0.3, 0.0], [0.7, 0.0]], + [[0.5, 0.8], [0.5, 0.2]] + ])).all() From 09bf11a80d126599a3ed4e6b4b824dedb628358a Mon Sep 17 00:00:00 2001 From: Lukas Nickel Date: Fri, 25 Sep 2020 14:22:17 +0200 Subject: [PATCH 088/105] Randomly generate energiey and estimate sigma from dispersion matrix --- pyirf/irf/tests/test_energy_dispersion.py | 61 ++++++++++++++++------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/pyirf/irf/tests/test_energy_dispersion.py b/pyirf/irf/tests/test_energy_dispersion.py index d17d90075..82cd36eb5 100644 --- a/pyirf/irf/tests/test_energy_dispersion.py +++ b/pyirf/irf/tests/test_energy_dispersion.py @@ -5,29 +5,36 @@ def test_energy_dispersion(): from pyirf.irf import energy_dispersion + + N = 1000 + TRUE_SIGMA_1 = 0.20 + TRUE_SIGMA_2 = 0.10 + TRUE_SIGMA_3 = 0.05 + selected_events = QTable({ 'reco_energy': np.concatenate([ - np.random.uniform(0.081, 0.099, size=3), - np.random.uniform(0.1, 0.119, size=7), - np.random.uniform(0.81, 0.99, size=5), - np.random.uniform(1.0, 1.19, size=5), - np.random.uniform(8.1, 9.9, size=8), - np.random.uniform(10.0, 10.9, size=2), + 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(10, 0.1), - np.full(10, 1.0), - np.full(10, 10.0) + np.full(N, 0.5), + np.full(N, 5.0), + np.full(N, 50.0) ])*u.TeV, 'source_fov_offset': np.concatenate([ - np.full(20, 0.2), - np.full(10, 1.5) + np.full(500, 0.2), + np.full(500, 1.5), + np.full(500, 0.2), + np.full(500, 1.5), + np.full(500, 0.2), + np.full(500, 1.5), ])*u.deg }) - true_energy_bins = np.array([0.1, 1.0, 10.0]) * u.TeV + 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.array([0.8, 1.0, 1.2]) + migration_bins = np.linspace(0, 2, 1001) result = energy_dispersion( selected_events, @@ -35,8 +42,26 @@ def test_energy_dispersion(): fov_offset_bins, migration_bins) - assert result.sum() == 3.0 - assert (result == np.array([ - [[0.3, 0.0], [0.7, 0.0]], - [[0.5, 0.8], [0.5, 0.2]] - ])).all() + assert result.shape == (3, 1000, 2) + assert np.isclose(result.sum(), 6.0) + + cumulated = 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(cumulated[0, :, :] >= 0.84)[0][0]] + - bin_centers[np.where(cumulated[0, :, :] >= 0.16)[0][0]])/2, + rtol=0.1 + ) + assert np.isclose( + TRUE_SIGMA_2, + (bin_centers[np.where(cumulated[1, :, :] >= 0.84)[0][0]] + - bin_centers[np.where(cumulated[1, :, :] >= 0.16)[0][0]])/2, + rtol=0.1 + ) + assert np.isclose( + TRUE_SIGMA_3, + (bin_centers[np.where(cumulated[2, :, :] >= 0.84)[0][0]] + - bin_centers[np.where(cumulated[2, :, :] >= 0.16)[0][0]])/2, + rtol=0.1 + ) From 007e29131d145cdd17146e912d52bdc4bbb05c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 19:13:33 +0200 Subject: [PATCH 089/105] Set seed in test, increase N --- pyirf/irf/tests/test_energy_dispersion.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pyirf/irf/tests/test_energy_dispersion.py b/pyirf/irf/tests/test_energy_dispersion.py index 82cd36eb5..bfcc32bb9 100644 --- a/pyirf/irf/tests/test_energy_dispersion.py +++ b/pyirf/irf/tests/test_energy_dispersion.py @@ -6,7 +6,9 @@ def test_energy_dispersion(): from pyirf.irf import energy_dispersion - N = 1000 + np.random.seed(0) + + N = 10000 TRUE_SIGMA_1 = 0.20 TRUE_SIGMA_2 = 0.10 TRUE_SIGMA_3 = 0.05 @@ -23,12 +25,12 @@ def test_energy_dispersion(): np.full(N, 50.0) ])*u.TeV, 'source_fov_offset': np.concatenate([ - np.full(500, 0.2), - np.full(500, 1.5), - np.full(500, 0.2), - np.full(500, 1.5), - np.full(500, 0.2), - np.full(500, 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), + np.full(N // 2, 0.2), + np.full(N // 2, 1.5), ])*u.deg }) From bde866f8db4a86168ee3571ce67e62dca42c4049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Fri, 25 Sep 2020 19:26:39 +0200 Subject: [PATCH 090/105] Improve plots in notebook --- .../comparison_with_EventDisplay.ipynb | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb index 5222023b4..7dd8e1355 100644 --- a/docs/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -236,7 +236,7 @@ ")\n", "\n", "plt.legend()\n", - "plt.ylabel('θ²-cut / deg²')\n", + "plt.ylabel('G/H-cut')\n", "plt.xlabel(r'$E_\\mathrm{reco} / \\mathrm{TeV}$')\n", "plt.xscale('log')" ] @@ -397,13 +397,20 @@ "\n", "# look at a single energy bin\n", "# repeat values for each phi bin\n", - "image = np.tile(psf[10], (len(phi_bins) - 1, 1))\n", - "plt.pcolormesh(x, y, image)\n", - "plt.xlim(-0.25, 0.25)\n", - "plt.ylim(-0.25, 0.25)\n", - "plt.xlabel('Distance from source x')\n", - "plt.ylabel('Distance from source y')\n", - "plt.gca().set_aspect(1)" + "center = 0.5 * (psf_table['ENERG_LO'] + psf_table['ENERG_HI'])\n", + "fig, axs = plt.subplots(1, 3)\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(x, y, image)\n", + " \n", + " ax.set_xlim(-0.25, 0.25)\n", + " ax.set_ylim(-0.25, 0.25)\n", + " ax.set_xlabel('Distance from source x')\n", + " ax.set_ylabel('Distance from source y')\n", + " ax.set_aspect(1)" ] }, { @@ -518,9 +525,7 @@ "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", - "plt.show()" + "plt.ylabel(r'$E_\\mathrm{Reco} / E_\\mathrm{True}$')" ] }, { From 38ed5118ce08fb34b44c00ea0f53bdd756cedcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 11:32:30 +0200 Subject: [PATCH 091/105] Update README.rst --- README.rst | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 2080443f7..90a0edcab 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,13 @@ -================================================== -pyirf |travis| |codacy| |coverage| |documentation| -================================================== +========================================= +pyirf |travis| |coverage| |documentation| +========================================= .. |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 .. |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 - -=== Under construction ==== - -The current version works only for point sources. - -=== Documentation ==== - -https://pyirf.readthedocs.io/ +Python library to calculate IACT IRFs and Sensitivities. From 48be1d77fb52c110ec90aee96bcbed02931be05e Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Sat, 26 Sep 2020 11:55:36 +0200 Subject: [PATCH 092/105] fix some nomenclature in test dispersion --- pyirf/irf/tests/test_energy_dispersion.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyirf/irf/tests/test_energy_dispersion.py b/pyirf/irf/tests/test_energy_dispersion.py index bfcc32bb9..b9955fee3 100644 --- a/pyirf/irf/tests/test_energy_dispersion.py +++ b/pyirf/irf/tests/test_energy_dispersion.py @@ -47,23 +47,23 @@ def test_energy_dispersion(): assert result.shape == (3, 1000, 2) assert np.isclose(result.sum(), 6.0) - cumulated = np.cumsum(result, axis=1) + 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(cumulated[0, :, :] >= 0.84)[0][0]] - - bin_centers[np.where(cumulated[0, :, :] >= 0.16)[0][0]])/2, + (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(cumulated[1, :, :] >= 0.84)[0][0]] - - bin_centers[np.where(cumulated[1, :, :] >= 0.16)[0][0]])/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(cumulated[2, :, :] >= 0.84)[0][0]] - - bin_centers[np.where(cumulated[2, :, :] >= 0.16)[0][0]])/2, + (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 ) From f6765d84303ed405a004ef9253f7c6220e0b6e57 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Sat, 26 Sep 2020 11:57:16 +0200 Subject: [PATCH 093/105] Global formatting compliant with PEP8 via black. --- examples/calculate_eventdisplay_irfs.py | 225 +++++++++++---------- examples/plot_spectra.py | 42 ++-- pyirf/benchmarks/__init__.py | 4 +- pyirf/benchmarks/angular_resolution.py | 23 +-- pyirf/benchmarks/energy_bias_resolution.py | 30 +-- pyirf/binning.py | 48 +++-- pyirf/cut_optimization.py | 42 ++-- pyirf/cuts.py | 48 ++--- pyirf/io/__init__.py | 12 +- pyirf/io/eventdisplay.py | 55 +++-- pyirf/io/gadf.py | 183 +++++++++-------- pyirf/irf/__init__.py | 8 +- pyirf/irf/effective_area.py | 22 +- pyirf/irf/energy_dispersion.py | 31 +-- pyirf/irf/psf.py | 22 +- pyirf/irf/tests/test_effective_area.py | 19 +- pyirf/irf/tests/test_energy_dispersion.py | 82 ++++---- pyirf/irf/tests/test_psf.py | 26 ++- pyirf/sensitivity.py | 59 +++--- pyirf/simulations.py | 50 ++--- pyirf/spectral.py | 116 +++++------ pyirf/statistics.py | 8 +- pyirf/tests/test_cuts.py | 44 ++-- pyirf/utils.py | 40 ++-- setup.py | 16 +- 25 files changed, 638 insertions(+), 617 deletions(-) diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index ed7eaa8b6..35b215dcc 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -1,9 +1,9 @@ -''' +""" 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 @@ -13,7 +13,11 @@ 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.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 @@ -42,7 +46,7 @@ ) -log = logging.getLogger('pyirf') +log = logging.getLogger("pyirf") T_OBS = 50 * u.hour @@ -56,75 +60,72 @@ INITIAL_GH_CUT = 0.0 particles = { - 'gamma': { - 'file': 'data/gamma_onSource.S.3HB9-FD_ID0.eff-0.fits.gz', - 'target_spectrum': CRAB_HEGRA, + "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, + "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, + "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''' + """Rescale the cut values to enlarge the background region""" cuts = cuts.copy() - cuts['cut'] /= np.sqrt(alpha) + cuts["cut"] /= np.sqrt(alpha) return cuts def main(): logging.basicConfig(level=logging.INFO) - logging.getLogger('pyirf').setLevel(logging.DEBUG) + 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']) + 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["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']) + 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'], + 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('') + log.info(p["simulation_info"]) + log.info("") - gammas = particles['gamma']['events'] + gammas = particles["gamma"]["events"] # background table composed of both electrons and protons - background = table.vstack([ - particles['proton']['events'], - particles['electron']['events'] - ]) + 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') + 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_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 + 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], + 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, @@ -132,21 +133,27 @@ def main(): ) # evaluate the theta cut - gammas['selected_theta'] = evaluate_binned_cut(gammas['theta'], gammas['reco_energy'], theta_cuts, operator.le) + 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) + 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 - )) + 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') + 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']], + gammas[gammas["selected_theta"]], + background[background["selected_theta"]], bins=sensitivity_bins, cut_values=np.arange(-1.0, 1.005, 0.05), op=operator.ge, @@ -156,10 +163,14 @@ def main(): # 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) + 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, + gammas["theta"], + gammas["reco_energy"], + theta_bins, fill_value=np.nan * u.deg, percentile=68, min_value=0.05 * u.deg, @@ -168,40 +179,47 @@ def main(): 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'] + 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) + 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']) - + 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'), + 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'], + "": 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, - )) + 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) @@ -209,56 +227,59 @@ def main(): for label, mask in masks.items(): effective_area = point_like_effective_area( gammas[mask], - particles['gamma']['simulation_info'], + 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, - )) + 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, - )) + 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, + gammas[gammas["selected"]], true_energy_bins, ) + ang_res = angular_resolution(gammas[gammas["selected_gh"]], true_energy_bins,) psf = psf_table( - gammas[gammas['selected']], + gammas[gammas["selected"]], 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) + 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__': +if __name__ == "__main__": main() diff --git a/examples/plot_spectra.py b/examples/plot_spectra.py index 45ded626f..3d3071f02 100644 --- a/examples/plot_spectra.py +++ b/examples/plot_spectra.py @@ -15,67 +15,67 @@ cr_spectra = { - 'PDG All Particle Spectrum': PDG_ALL_PARTICLE, - 'ATIC Proton Fit (from IRF Document)': IRFDOC_PROTON_SPECTRUM, + "PDG All Particle Spectrum": PDG_ALL_PARTICLE, + "ATIC Proton Fit (from IRF Document)": IRFDOC_PROTON_SPECTRUM, } -if __name__ == '__main__': +if __name__ == "__main__": energy = np.geomspace(0.001, 300, 1000) * u.TeV plt.figure(constrained_layout=True) - plt.title('Crab Nebula Flux') + plt.title("Crab Nebula Flux") plt.plot( energy.to_value(u.TeV), CRAB_HEGRA(energy).to_value(POINT_SOURCE_FLUX_UNIT), - label='HEGRA', + label="HEGRA", ) plt.plot( energy.to_value(u.TeV), CRAB_MAGIC_JHEAP2015(energy).to_value(POINT_SOURCE_FLUX_UNIT), - label='MAGIC JHEAP 2015' + label="MAGIC JHEAP 2015", ) plt.legend() - plt.xscale('log') - plt.yscale('log') - plt.xlabel('E / TeV') + 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') + plt.title("Cosmic Ray Flux") for label, spectrum in cr_spectra.items(): - unit = energy.unit**2 * FLUX_UNIT + unit = energy.unit ** 2 * FLUX_UNIT plt.plot( energy.to_value(u.TeV), - (spectrum(energy) * energy**2).to_value(unit), + (spectrum(energy) * energy ** 2).to_value(unit), label=label, ) plt.legend() - plt.xscale('log') - plt.yscale('log') - plt.xlabel(r'$E \,\,/\,\, \mathrm{TeV}$') + 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') + plt.title("Electron Flux") - unit = u.TeV**2 / u.m**2 / u.s / u.sr + 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)', + (energy ** 3 * IRFDOC_ELECTRON_SPECTRUM(energy)).to_value(unit), + label="IFAE 2013 (from IRF Document)", ) plt.legend() - plt.xscale('log') + plt.xscale("log") plt.xlim(5e-3, 10) plt.ylim(1e-5, 0.25e-3) - plt.xlabel(r'$E \,\,/\,\, \mathrm{TeV}$') + plt.xlabel(r"$E \,\,/\,\, \mathrm{TeV}$") plt.ylabel(rf'$E^3 \cdot \Phi \,\,/\,\,$ ({unit.to_string("latex")})') plt.grid() diff --git a/pyirf/benchmarks/__init__.py b/pyirf/benchmarks/__init__.py index a9892ae4c..72c721b25 100644 --- a/pyirf/benchmarks/__init__.py +++ b/pyirf/benchmarks/__init__.py @@ -3,6 +3,6 @@ __all__ = [ - 'energy_bias_resolution', - 'angular_resolution', + "energy_bias_resolution", + "angular_resolution", ] diff --git a/pyirf/benchmarks/angular_resolution.py b/pyirf/benchmarks/angular_resolution.py index 4b2d7bc8e..51f0bb668 100644 --- a/pyirf/benchmarks/angular_resolution.py +++ b/pyirf/benchmarks/angular_resolution.py @@ -10,8 +10,7 @@ def angular_resolution( - events, - true_energy_bins, + events, true_energy_bins, ): """ Calculate the angular resolution. @@ -34,24 +33,24 @@ def angular_resolution( """ # create a table to make use of groupby operations - table = Table(events[['true_energy', 'theta']]) + table = Table(events[["true_energy", "theta"]]) - table['bin_index'] = calculate_bin_indices( - table['true_energy'].quantity, true_energy_bins + table["bin_index"] = calculate_bin_indices( + table["true_energy"].quantity, true_energy_bins ) result = Table() - result['true_energy_low'] = true_energy_bins[:-1] - result['true_energy_high'] = true_energy_bins[1:] - result['true_energy_center'] = 0.5 * (true_energy_bins[:-1] + true_energy_bins[1:]) + result["true_energy_low"] = true_energy_bins[:-1] + result["true_energy_high"] = true_energy_bins[1:] + result["true_energy_center"] = 0.5 * (true_energy_bins[:-1] + true_energy_bins[1:]) - result['angular_resolution'] = np.nan * u.deg + result["angular_resolution"] = np.nan * u.deg # use groupby operations to calculate the percentile in each bin - by_bin = table.group_by('bin_index') + by_bin = table.group_by("bin_index") - index = by_bin.groups.keys['bin_index'] - result['angular_resolution'][index] = by_bin['theta'].groups.aggregate( + index = by_bin.groups.keys["bin_index"] + result["angular_resolution"][index] = by_bin["theta"].groups.aggregate( lambda x: np.percentile(x, 100 * ONE_SIGMA_PERCENTILE) ) return result diff --git a/pyirf/benchmarks/energy_bias_resolution.py b/pyirf/benchmarks/energy_bias_resolution.py index 933326db2..2f69f43db 100644 --- a/pyirf/benchmarks/energy_bias_resolution.py +++ b/pyirf/benchmarks/energy_bias_resolution.py @@ -54,7 +54,7 @@ def energy_bias_resolution( events, true_energy_bins, bias_function=np.median, - resolution_function=inter_quantile_distance + resolution_function=inter_quantile_distance, ): """ Calculate bias and energy resolution. @@ -78,25 +78,27 @@ def energy_bias_resolution( """ # create a table to make use of groupby operations - table = Table(events[['true_energy', 'reco_energy']]) - table['rel_error'] = (events['reco_energy'] / events['true_energy']) - 1 + table = Table(events[["true_energy", "reco_energy"]]) + table["rel_error"] = (events["reco_energy"] / events["true_energy"]) - 1 - table['bin_index'] = calculate_bin_indices( - table['true_energy'].quantity, true_energy_bins + table["bin_index"] = calculate_bin_indices( + table["true_energy"].quantity, true_energy_bins ) result = Table() - result['true_energy_low'] = true_energy_bins[:-1] - result['true_energy_high'] = true_energy_bins[1:] - result['true_energy_center'] = 0.5 * (true_energy_bins[:-1] + true_energy_bins[1:]) + result["true_energy_low"] = true_energy_bins[:-1] + result["true_energy_high"] = true_energy_bins[1:] + result["true_energy_center"] = 0.5 * (true_energy_bins[:-1] + true_energy_bins[1:]) - result['bias'] = np.nan - result['resolution'] = np.nan + result["bias"] = np.nan + result["resolution"] = np.nan # use groupby operations to calculate the percentile in each bin - by_bin = table.group_by('bin_index') + by_bin = table.group_by("bin_index") - index = by_bin.groups.keys['bin_index'] - result['bias'][index] = by_bin['rel_error'].groups.aggregate(bias_function) - result['resolution'][index] = by_bin['rel_error'].groups.aggregate(resolution_function) + index = by_bin.groups.keys["bin_index"] + result["bias"][index] = by_bin["rel_error"].groups.aggregate(bias_function) + result["resolution"][index] = by_bin["rel_error"].groups.aggregate( + resolution_function + ) return result diff --git a/pyirf/binning.py b/pyirf/binning.py index 0f307de40..5eeca864c 100644 --- a/pyirf/binning.py +++ b/pyirf/binning.py @@ -1,6 +1,6 @@ -''' +""" Utility functions for binning -''' +""" import numpy as np import astropy.units as u @@ -8,7 +8,7 @@ def add_overflow_bins(bins, positive=True): - ''' + """ Add under and overflow bins to a bin array. Parameters @@ -17,11 +17,11 @@ def add_overflow_bins(bins, positive=True): Bin edges array positive: bool If True, the underflow array will start at 0, if not at ``-np.inf`` - ''' + """ lower = 0 if positive else -np.inf upper = np.inf - if hasattr(bins, 'unit'): + if hasattr(bins, "unit"): lower *= bins.unit upper *= bins.unit @@ -36,7 +36,7 @@ def add_overflow_bins(bins, positive=True): @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. @@ -54,17 +54,17 @@ def create_bins_per_decade(e_min, e_max, bins_per_decade=5): 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) + 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. @@ -85,13 +85,11 @@ def calculate_bin_indices(data, bins): ------- 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}' - ) + 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) @@ -99,8 +97,8 @@ def calculate_bin_indices(data, bins): return np.digitize(data, bins) - 1 -def create_histogram_table(events, bins, key='reco_energy'): - ''' +def create_histogram_table(events, bins, key="reco_energy"): + """ Histogram a variable from events data into an astropy table. Parameters @@ -117,16 +115,16 @@ def create_histogram_table(events, bins, key='reco_energy'): ------- 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) + 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'] + 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 index 6cf759556..8c2a62168 100644 --- a/pyirf/cut_optimization.py +++ b/pyirf/cut_optimization.py @@ -9,15 +9,15 @@ __all__ = [ - 'optimize_gh_cut', + "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 @@ -28,48 +28,38 @@ def optimize_gh_cut(signal, background, bins, cut_values, op, alpha=1.0, progres # 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 + 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, + signal["gh_score"], signal["reco_energy"], cut_table, op, ) background_selected = evaluate_binned_cut( - background['gh_score'], - background['reco_energy'], - cut_table, - op, + background["gh_score"], background["reco_energy"], cut_table, op, ) # create the histograms signal_hist = create_histogram_table( - signal[signal_selected], bins, 'reco_energy' + signal[signal_selected], bins, "reco_energy" ) background_hist = create_histogram_table( - background[background_selected], bins, 'reco_energy' + background[background_selected], bins, "reco_energy" ) - sensitivity = calculate_sensitivity( - signal_hist, - background_hist, - alpha=alpha, - ) + 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_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] + 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 @@ -79,6 +69,6 @@ def optimize_gh_cut(signal, background, bins, cut_values, op, alpha=1.0, progres best = 0 best_sensitivity[bin_id] = sensitivities[best][bin_id] - best_cut_table['cut'][bin_id] = cut_values[best] + 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 index 28a72c214..1a9b26098 100644 --- a/pyirf/cuts.py +++ b/pyirf/cuts.py @@ -5,15 +5,9 @@ def calculate_percentile_cut( - values, - bin_values, - bins, - fill_value, - percentile=68, - min_value=None, - max_value=None, + 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. @@ -34,42 +28,40 @@ def calculate_percentile_cut( 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 = Table({"values": values, "bin_values": bin_values}, copy=False) - table['bin_index'] = calculate_bin_indices( - table['bin_values'].quantity, bins - ) + 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 + 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') + 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'] + 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) + .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']) + 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']) + 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 @@ -88,7 +80,7 @@ def evaluate_binned_cut(values, bin_values, cut_table, op): 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]) + """ + 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) + return op(values, cut_table["cut"][bin_index].quantity) diff --git a/pyirf/io/__init__.py b/pyirf/io/__init__.py index 96771d4ce..48dc60941 100644 --- a/pyirf/io/__init__.py +++ b/pyirf/io/__init__.py @@ -8,10 +8,10 @@ __all__ = [ - 'read_eventdisplay_fits', - 'create_psf_table_hdu', - 'create_aeff2d_hdu', - 'create_energy_dispersion_hdu', - 'create_psf_table_hdu', - 'create_rad_max_hdu', + "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 index 86ee77646..4c356d1e0 100644 --- a/pyirf/io/eventdisplay.py +++ b/pyirf/io/eventdisplay.py @@ -11,18 +11,18 @@ 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', + "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", } @@ -44,30 +44,27 @@ def read_eventdisplay_fits(infile): 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] + 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() - }) + 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_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}') + 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), + 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 index 628abda09..01001a385 100644 --- a/pyirf/io/gadf.py +++ b/pyirf/io/gadf.py @@ -8,18 +8,18 @@ __all__ = [ - 'create_aeff2d_hdu', - 'create_energy_dispersion_hdu', - 'create_psf_table_hdu', - 'create_rad_max_hdu', + "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://gamma-astro-data-formats.readthedocs.io' -DEFAULT_HEADER['HDUVERS'] = '0.2' -DEFAULT_HEADER['HDUCLASS'] = 'GADF' +DEFAULT_HEADER["CREATOR"] = f"pyirf v{__version__}" +DEFAULT_HEADER["HDUDOC"] = "https://gamma-astro-data-formats.readthedocs.io" +DEFAULT_HEADER["HDUVERS"] = "0.2" +DEFAULT_HEADER["HDUCLASS"] = "GADF" def _add_header_cards(header, **header_cards): @@ -27,12 +27,18 @@ def _add_header_cards(header, **header_cards): header[k] = v -@u.quantity_input(effective_area=u.m**2, true_energy_bins=u.TeV, fov_offset_bins=u.deg) +@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 + 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 @@ -54,36 +60,42 @@ def create_aeff2d_hdu( **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) - aeff['EFFAREA'] = effective_area[np.newaxis, ...].to(u.m**2) + 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) + aeff["EFFAREA"] = effective_area[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 + 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, + 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, + psf, + true_energy_bins, + source_offset_bins, + fov_offset_bins, point_like=True, - extname='PSF', **header_cards + 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 @@ -108,33 +120,34 @@ def create_psf_table_hdu( **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), - 'RPSF': psf[np.newaxis, ...].to(1 / u.sr), - }) + """ + + 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), + "RPSF": psf[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 + 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, + true_energy_bins=u.TeV, fov_offset_bins=u.deg, ) def create_energy_dispersion_hdu( energy_dispersion, @@ -142,9 +155,10 @@ def create_energy_dispersion_hdu( migration_bins, fov_offset_bins, point_like=True, - extname='EDISP', **header_cards + 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 @@ -169,40 +183,47 @@ def create_energy_dispersion_hdu( **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), - 'MATRIX': u.Quantity(energy_dispersion[np.newaxis, ...]).to(u.one), - }) + """ + + 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), + "MATRIX": u.Quantity(energy_dispersion[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 + 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, + 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, + reco_energy_bins, + fov_offset_bins, + rad_max, point_like=True, - extname='RAD_MAX', **header_cards + 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 @@ -222,22 +243,24 @@ def create_rad_max_hdu( **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), - 'RAD_MAX': rad_max[np.newaxis, ...].to(u.deg) - }) + """ + 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), + "RAD_MAX": rad_max[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 + 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/irf/__init__.py b/pyirf/irf/__init__.py index fc35edec9..6a5e7dfba 100644 --- a/pyirf/irf/__init__.py +++ b/pyirf/irf/__init__.py @@ -3,8 +3,8 @@ from .psf import psf_table __all__ = [ - 'effective_area', - 'point_like_effective_area', - 'energy_dispersion', - 'psf_table', + "effective_area", + "point_like_effective_area", + "energy_dispersion", + "psf_table", ] diff --git a/pyirf/irf/effective_area.py b/pyirf/irf/effective_area.py index 72c5c40a6..017240540 100644 --- a/pyirf/irf/effective_area.py +++ b/pyirf/irf/effective_area.py @@ -4,14 +4,14 @@ __all__ = [ - 'effective_area', - 'point_like_effective_area', + "effective_area", + "point_like_effective_area", ] -@u.quantity_input(area=u.m**2) +@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 @@ -22,12 +22,12 @@ def effective_area(n_selected, n_simulated, area): 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. @@ -39,10 +39,12 @@ def point_like_effective_area(selected_events, simulation_info, true_energy_bins 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 + """ + area = np.pi * simulation_info.max_impact ** 2 - hist_selected = create_histogram_table(selected_events, true_energy_bins, 'true_energy') + 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) + return effective_area(hist_selected["n"], hist_simulated, area) diff --git a/pyirf/irf/energy_dispersion.py b/pyirf/irf/energy_dispersion.py index 810c918de..2ef4faaaf 100644 --- a/pyirf/irf/energy_dispersion.py +++ b/pyirf/irf/energy_dispersion.py @@ -3,7 +3,7 @@ __all__ = [ - 'energy_dispersion', + "energy_dispersion", ] @@ -14,7 +14,7 @@ def _normalize_hist(hist): norm = hist.sum(axis=1) h = np.swapaxes(hist, 0, 1) - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): h /= norm h = np.swapaxes(h, 0, 1) @@ -22,12 +22,9 @@ def _normalize_hist(hist): def energy_dispersion( - selected_events, - true_energy_bins, - fov_offset_bins, - migration_bins, + 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 @@ -51,20 +48,24 @@ def energy_dispersion( 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) + """ + 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), - ]), + 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) diff --git a/pyirf/irf/psf.py b/pyirf/irf/psf.py index b9895776d..290ed0358 100644 --- a/pyirf/irf/psf.py +++ b/pyirf/irf/psf.py @@ -5,15 +5,17 @@ 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) - ]) + 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, @@ -21,7 +23,7 @@ def psf_table(events, true_energy_bins, source_offset_bins, fov_offset_bins): 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) @@ -29,11 +31,11 @@ def psf_table(events, true_energy_bins, source_offset_bins, fov_offset_bins): def _normalize_psf(hist, source_offset_bins): - '''Normalize the psf histogram to a probability densitity over solid angle''' + """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'): + with np.errstate(invalid="ignore"): # to correctly divide by using broadcasting here, # we need to swap the axis order diff --git a/pyirf/irf/tests/test_effective_area.py b/pyirf/irf/tests/test_effective_area.py index e995f0a78..ce5495e98 100644 --- a/pyirf/irf/tests/test_effective_area.py +++ b/pyirf/irf/tests/test_effective_area.py @@ -9,11 +9,10 @@ def test_effective_area(): n_selected = np.array([10, 20, 30]) n_simulated = np.array([100, 2000, 15000]) - area = 1e5 * u.m**2 + area = 1e5 * u.m ** 2 assert u.allclose( - effective_area(n_selected, n_simulated, area), - [1e4, 1e3, 200] * u.m**2 + effective_area(n_selected, n_simulated, area), [1e4, 1e3, 200] * u.m ** 2 ) @@ -22,22 +21,22 @@ def test_pointlike_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)), - }) + 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 + 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) + 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 index b9955fee3..ded38273e 100644 --- a/pyirf/irf/tests/test_energy_dispersion.py +++ b/pyirf/irf/tests/test_energy_dispersion.py @@ -13,57 +13,71 @@ def test_energy_dispersion(): 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 - }) + 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) + selected_events, true_energy_bins, fov_offset_bins, migration_bins + ) assert result.shape == (3, 1000, 2) - assert np.isclose(result.sum(), 6.0) + 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 + ( + 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 + ( + 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 + ( + 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 index 0f09a4bb1..0fd8810ae 100644 --- a/pyirf/irf/tests/test_psf.py +++ b/pyirf/irf/tests/test_psf.py @@ -17,11 +17,13 @@ def test_psf(): # 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, - }) + 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 @@ -32,7 +34,7 @@ def test_psf(): # 2 energy bins, 1 fov bin, 200 source distance bins assert psf.shape == (2, 1, 200) - assert psf.unit == u.Unit('sr-1') + assert psf.unit == u.Unit("sr-1") # check that psf is normalized bin_solid_angle = np.diff(cone_solid_angle(source_bins)) @@ -42,7 +44,15 @@ def test_psf(): # 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) + 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) + 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/sensitivity.py b/pyirf/sensitivity.py index 9fd5a5aa7..cfa975a16 100644 --- a/pyirf/sensitivity.py +++ b/pyirf/sensitivity.py @@ -1,6 +1,6 @@ -''' +""" Functions to calculate sensitivity -''' +""" import astropy.units as u import numpy as np from scipy.optimize import brentq @@ -11,10 +11,7 @@ from .utils import check_histograms -__all__ = [ - 'relative_sensitivity', - 'calculate_sensitivity' -] +__all__ = ["relative_sensitivity", "calculate_sensitivity"] log = logging.getLogger(__name__) @@ -28,7 +25,7 @@ def relative_sensitivity( 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``. @@ -65,7 +62,7 @@ def relative_sensitivity( Formula (17) initial_guess: float Initial guess for the root finder - ''' + """ n_background = n_off * alpha n_signal = n_on - n_background @@ -89,15 +86,12 @@ def equation(relative_flux): # 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, - ) + 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' + "Could not calculate relative significance for" + f" n_signal={n_signal:.1f}, n_off={n_off:.1f}, returning nan" ) return np.nan @@ -111,7 +105,7 @@ def calculate_sensitivity( 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`` @@ -145,37 +139,42 @@ def calculate_sensitivity( 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 + 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["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'] = [ + 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']) + 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["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 + sensitivity["relative_sensitivity"][invalid] = np.nan return sensitivity diff --git a/pyirf/simulations.py b/pyirf/simulations.py index d1948c9aa..3850e5b92 100644 --- a/pyirf/simulations.py +++ b/pyirf/simulations.py @@ -2,7 +2,7 @@ class SimulatedEventsInfo: - ''' + """ Information about all simulated events, needed for calculating event weights. @@ -20,19 +20,23 @@ class SimulatedEventsInfo: 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', + "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): + @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 @@ -41,11 +45,11 @@ def __init__(self, n_showers, energy_min, energy_max, max_impact, spectral_index self.viewcone = viewcone if spectral_index > -1: - raise ValueError('spectral index must be <= -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 @@ -60,7 +64,7 @@ def calculate_n_showers(self, 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:] @@ -69,19 +73,19 @@ def calculate_n_showers(self, energy_bins): 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) + 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}' - ')' + 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 index dd39c1634..dbaa0024b 100644 --- a/pyirf/spectral.py +++ b/pyirf/spectral.py @@ -1,6 +1,6 @@ -''' +""" Functions and classes for calculating spectral weights -''' +""" import astropy.units as u import numpy as np @@ -8,7 +8,7 @@ #: 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 +POINT_SOURCE_FLUX_UNIT = (1 / u.TeV / u.s / u.m ** 2).unit #: Unit of a diffuse flux #: @@ -17,23 +17,23 @@ __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' + "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''' + r""" Calculate event weights Events with a certain ``simulated_spectrum`` are reweighted to ``target_spectrum``. @@ -54,14 +54,14 @@ def calculate_event_weights(true_energy, target_spectrum, simulated_spectrum): ------- weights: numpy.ndarray Weights for each event - ''' - return ( - target_spectrum(true_energy) / simulated_spectrum(true_energy) - ).to_value(u.one) + """ + return (target_spectrum(true_energy) / simulated_spectrum(true_energy)).to_value( + u.one + ) class PowerLaw: - r''' + r""" A power law with normalization, reference energy and index. Index includes the sign: @@ -78,10 +78,10 @@ class PowerLaw: :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 + 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 @@ -90,20 +90,15 @@ def __init__(self, normalization, index, e_ref=1 * u.TeV): @u.quantity_input(energy=u.TeV) def __call__(self, energy): - return ( - self.normalization - * (energy / self.e_ref) ** self.index - ) + 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 - ): - ''' + 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 @@ -115,24 +110,20 @@ def from_simulation( else: solid_angle = 1 - A = np.pi * simulated_event_info.max_impact**2 + 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 + 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, - ) + 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}' + return f"{self.__class__.__name__}({self.normalization} * (E / {self.e_ref})**{self.index}" class LogParabola: - r''' + r""" A log parabola flux parameterization. .. math:: @@ -152,11 +143,10 @@ class LogParabola: :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 + 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 @@ -167,14 +157,14 @@ def __init__(self, normalization, a, b, e_ref=1 * u.TeV): @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)) + 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}))' + return f"{self.__class__.__name__}({self.normalization} * (E / {self.e_ref})**({self.a} + {self.b} * log10(E / {self.e_ref}))" class PowerLawWithExponentialGaussian(PowerLaw): - r''' + r""" A power law with an additional Gaussian bump. Beware that the Gaussian is not normalized! @@ -210,18 +200,13 @@ class PowerLawWithExponentialGaussian(PowerLaw): :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 + 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 - ) + super().__init__(normalization=normalization, index=index, e_ref=e_ref) self.f = f self.mu = mu self.sigma = sigma @@ -234,18 +219,17 @@ def __call__(self, energy): # 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) + 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, + 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 @@ -254,9 +238,7 @@ def __call__(self, energy): #: 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, + normalization=3.23e-11 / (u.TeV * u.cm ** 2 * u.s), a=-2.47, b=-0.24, ) @@ -265,9 +247,7 @@ def __call__(self, energy): #: (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, + 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 @@ -275,7 +255,7 @@ def __call__(self, energy): #: 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), + normalization=9.8e-6 / (u.cm ** 2 * u.s * u.TeV * u.sr), index=-2.62, e_ref=1 * u.TeV, ) @@ -285,7 +265,7 @@ def __call__(self, energy): #: 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), + normalization=2.385e-9 / (u.TeV * u.cm ** 2 * u.s * u.sr), index=-3.43, e_ref=1 * u.TeV, mu=-0.101, diff --git a/pyirf/statistics.py b/pyirf/statistics.py index 1b28cf96f..f7808b3ce 100644 --- a/pyirf/statistics.py +++ b/pyirf/statistics.py @@ -4,7 +4,7 @@ def li_ma_significance(n_on, n_off, alpha=0.2): - ''' + """ Calculate the Li & Ma significance. Formula (17) doi.org/10.1086/161295 @@ -26,21 +26,21 @@ def li_ma_significance(n_on, n_off, alpha=0.2): ------- 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'): + 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) + ts = t1 + t2 significance = np.sqrt(ts * 2) significance[np.isnan(significance)] = 0 diff --git a/pyirf/tests/test_cuts.py b/pyirf/tests/test_cuts.py index 0a5b2fe35..36c615161 100644 --- a/pyirf/tests/test_cuts.py +++ b/pyirf/tests/test_cuts.py @@ -8,15 +8,18 @@ @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], - }) + 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) @@ -28,32 +31,27 @@ def test_calculate_percentile_cuts(): 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.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, - ) + 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, + 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) + 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], - }) + cuts = Table({"low": [0, 1], "high": [1, 2], "cut": [100, 1000],}) survived = evaluate_binned_cut( np.array([500, 1500, 50, 2000, 25, 800]), @@ -64,11 +62,9 @@ def test_evaluate_binned_cut(): 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, - }) + 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, diff --git a/pyirf/utils.py b/pyirf/utils.py index 5ca6c05f2..677dfa6b3 100644 --- a/pyirf/utils.py +++ b/pyirf/utils.py @@ -3,15 +3,16 @@ from astropy.coordinates.angle_utilities import angular_separation __all__ = [ - 'is_scalar', - 'calculate_theta', - 'calculate_source_fov_offset', - 'check_histograms', - 'cone_solid_angle', + "is_scalar", + "calculate_theta", + "calculate_source_fov_offset", + "check_histograms", + "cone_solid_angle", ] + def is_scalar(val): - '''Workaround that also supports astropy quantities + """Workaround that also supports astropy quantities Parameters ---------- @@ -22,7 +23,7 @@ def is_scalar(val): ------- result: bool True is if input object is a scalar, False otherwise. - ''' + """ result = np.array(val, copy=False).shape == tuple() return result @@ -47,8 +48,7 @@ def calculate_theta(events, assumed_source_az, assumed_source_alt): in the sky. """ theta = angular_separation( - assumed_source_az, assumed_source_alt, - events['reco_az'], events['reco_alt'], + assumed_source_az, assumed_source_alt, events["reco_az"], events["reco_alt"], ) return theta.to(u.deg) @@ -69,15 +69,17 @@ def calculate_source_fov_offset(events): in the sky. """ theta = angular_separation( - events['true_az'], events['true_alt'], - events['pointing_az'], events['pointing_alt'], + 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'): - ''' +def check_histograms(hist1, hist2, key="reco_energy"): + """ Check if two histogram tables have the same binning Parameters @@ -87,19 +89,19 @@ def check_histograms(hist1, hist2, key='reco_energy'): ``~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 + 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' + "Binning for signal_hist and background_hist must be equal" ) def cone_solid_angle(angle): - '''Calculate the solid angle of a view cone. + """Calculate the solid angle of a view cone. Parameters ---------- @@ -111,6 +113,6 @@ def cone_solid_angle(angle): 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/setup.py b/setup.py index 1805334e2..885dab826 100644 --- a/setup.py +++ b/setup.py @@ -5,21 +5,11 @@ __version__ = re.search('^__version__ = "(.*)"$', f.read()).group(1) extras_require = { - 'docs': [ - 'sphinx', - 'sphinx_rtd_theme', - 'sphinx_automodapi', - 'numpydoc', - 'nbsphinx' - ], - 'tests': [ - 'pytest', - 'pytest-cov', - 'uproot', - ], + "docs": ["sphinx", "sphinx_rtd_theme", "sphinx_automodapi", "numpydoc", "nbsphinx"], + "tests": ["pytest", "pytest-cov", "uproot",], } -extras_require['all'] = extras_require['tests'] + extras_require['docs'] +extras_require["all"] = extras_require["tests"] + extras_require["docs"] setup( version=__version__, From 645b8a036e8aa92d4781154dea2f56d032940f78 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Sat, 26 Sep 2020 12:00:24 +0200 Subject: [PATCH 094/105] Fix bug in plot spectra example --- examples/plot_spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/plot_spectra.py b/examples/plot_spectra.py index 3d3071f02..b7fb25f2a 100644 --- a/examples/plot_spectra.py +++ b/examples/plot_spectra.py @@ -10,7 +10,7 @@ CRAB_HEGRA, CRAB_MAGIC_JHEAP2015, POINT_SOURCE_FLUX_UNIT, - FLUX_UNIT, + DIFFUSE_FLUX_UNIT, ) @@ -47,7 +47,7 @@ plt.title("Cosmic Ray Flux") for label, spectrum in cr_spectra.items(): - unit = energy.unit ** 2 * FLUX_UNIT + unit = energy.unit ** 2 * DIFFUSE_FLUX_UNIT plt.plot( energy.to_value(u.TeV), (spectrum(energy) * energy ** 2).to_value(unit), From 420ae42d1e56de30676e2c699ae5faf87aefcea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 12:47:54 +0200 Subject: [PATCH 095/105] Add examples section to docs --- docs/conf.py | 14 ++++---------- docs/examples.rst | 39 +++++++++++++++++++++++++++++++++++++++ docs/index.rst | 4 ++++ 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 docs/examples.rst diff --git a/docs/conf.py b/docs/conf.py index d58d5c98c..5f4904e53 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -82,16 +82,10 @@ # 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", -# } +html_theme_options = { + 'canonical_url': 'https://cta-observatory.github.io/pyirf', + 'display_version': True, +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, 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 24f9ac178..b19fcecc6 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! ================================= @@ -43,6 +46,7 @@ which this documentation is linked. install introduction + examples notebooks/index contribute changelog From 166e920455cd12763cefc4305423ea17695f0407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 13:00:07 +0200 Subject: [PATCH 096/105] Calculate PSF on gammas without theta cut, update notebook --- .../comparison_with_EventDisplay.ipynb | 39 ++++++++----------- examples/calculate_eventdisplay_irfs.py | 2 +- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/docs/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb index 7dd8e1355..5a952be25 100644 --- a/docs/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -385,32 +385,33 @@ "psf = psf_table['RPSF'][:, 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, 1000)\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", - "x = r * np.cos(phi)\n", - "y = r * np.sin(phi)\n", - "\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", - "fig, axs = plt.subplots(1, 3)\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", + "from scipy.ndimage import gaussian_filter\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(x, y, image)\n", - " \n", - " ax.set_xlim(-0.25, 0.25)\n", - " ax.set_ylim(-0.25, 0.25)\n", - " ax.set_xlabel('Distance from source x')\n", - " ax.set_ylabel('Distance from source y')\n", - " ax.set_aspect(1)" + " ax.pcolormesh(phi, r, image)\n", + " ax.set_ylim(0, 0.25)\n", + " ax.set_aspect(1)\n", + " \n", + "fig.tight_layout()" ] }, { @@ -425,7 +426,7 @@ "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 [5, 10, 30]:\n", + "for bin_id in [10, 20, 30]:\n", " plt.errorbar(\n", " center.to_value(u.deg),\n", " psf[bin_id],\n", @@ -496,16 +497,6 @@ "[back to top](#Table-of-contents)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "edisp = QTable.read(pyirf_file, hdu='ENERGY_DISPERSION')[0]\n", - "edisp" - ] - }, { "cell_type": "code", "execution_count": null, @@ -514,6 +505,8 @@ }, "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", diff --git a/examples/calculate_eventdisplay_irfs.py b/examples/calculate_eventdisplay_irfs.py index 35b215dcc..94208ce3f 100644 --- a/examples/calculate_eventdisplay_irfs.py +++ b/examples/calculate_eventdisplay_irfs.py @@ -260,7 +260,7 @@ def main(): ang_res = angular_resolution(gammas[gammas["selected_gh"]], true_energy_bins,) psf = psf_table( - gammas[gammas["selected"]], + gammas[gammas["selected_gh"]], true_energy_bins, fov_offset_bins=fov_offset_bins, source_offset_bins=source_offset_bins, From 73757537a66a2314be2f905940229713a6076821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 13:14:09 +0200 Subject: [PATCH 097/105] Remove mpl clutter from notebook cell output --- .../comparison_with_EventDisplay.ipynb | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/docs/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb index 5a952be25..04c94f06e 100644 --- a/docs/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -212,7 +212,9 @@ "plt.ylabel('θ²-cut / deg²')\n", "plt.xlabel(r'$E_\\mathrm{reco} / \\mathrm{TeV}$')\n", "plt.xscale('log')\n", - "plt.yscale('log')" + "plt.yscale('log')\n", + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -238,7 +240,9 @@ "plt.legend()\n", "plt.ylabel('G/H-cut')\n", "plt.xlabel(r'$E_\\mathrm{reco} / \\mathrm{TeV}$')\n", - "plt.xscale('log')" + "plt.xscale('log')\n", + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -307,7 +311,9 @@ "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()" + "plt.legend()\n", + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -363,7 +369,8 @@ "plt.ylabel(\"Effective collection area / m²\")\n", "plt.grid(which=\"both\")\n", "plt.legend()\n", - "plt.show()" + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -400,8 +407,6 @@ "fig = plt.figure(figsize=(15, 5))\n", "axs = [fig.add_subplot(1, 3, i, projection='polar') for i in range(1, 4)]\n", "\n", - "from scipy.ndimage import gaussian_filter\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", @@ -411,7 +416,9 @@ " ax.set_ylim(0, 0.25)\n", " ax.set_aspect(1)\n", " \n", - "fig.tight_layout()" + "fig.tight_layout()\n", + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -439,7 +446,9 @@ "plt.legend()\n", "plt.xlim(0, 0.25)\n", "plt.ylabel('PSF PDF / sr⁻¹')\n", - "plt.xlabel('Distance from True Source / deg')" + "plt.xlabel('Distance from True Source / deg')\n", + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -481,12 +490,13 @@ "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", - "\n", - "plt.legend(loc=\"best\")" + "None # to remove clutter by mpl objects" ] }, { @@ -518,7 +528,9 @@ "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}$')" + "plt.ylabel(r'$E_\\mathrm{Reco} / E_\\mathrm{True}$')\n", + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -560,10 +572,9 @@ "plt.xlabel(r\"$E_\\mathrm{True} / \\mathrm{TeV}$\")\n", "plt.ylabel(\"Energy resolution\")\n", "plt.grid(which=\"both\")\n", - "\n", - "\n", "plt.legend(loc=\"best\")\n", - "plt.show()" + "\n", + "None # to remove clutter by mpl objects" ] }, { @@ -596,7 +607,8 @@ "# Plot function\n", "plt.errorbar(x, y, xerr=xerr, yerr=yerr, fmt=\"o\", label=\"EventDisplay\")\n", "plt.legend(loc=\"best\")\n", - "plt.show()" + "\n", + "None # to remove clutter by mpl objects" ] } ], From 7cf1f2d92f097f8ea20ed3a2ffd2f8e5a7346aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 13:27:50 +0200 Subject: [PATCH 098/105] Fix HDUDOC link for GADF --- pyirf/io/gadf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyirf/io/gadf.py b/pyirf/io/gadf.py index 01001a385..aa1cd3e85 100644 --- a/pyirf/io/gadf.py +++ b/pyirf/io/gadf.py @@ -17,7 +17,7 @@ DEFAULT_HEADER = Header() DEFAULT_HEADER["CREATOR"] = f"pyirf v{__version__}" -DEFAULT_HEADER["HDUDOC"] = "https://gamma-astro-data-formats.readthedocs.io" +DEFAULT_HEADER["HDUDOC"] = "https://github.com/open-gamma-ray-astro/gamma-astro-data-formats" DEFAULT_HEADER["HDUVERS"] = "0.2" DEFAULT_HEADER["HDUCLASS"] = "GADF" From 2c12c543cf95c957917c62643110af93cab025d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 14:04:38 +0200 Subject: [PATCH 099/105] Make sure uproot is version 3 for the moment --- docs/index.rst | 2 +- environment.yml | 3 +-- setup.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index b19fcecc6..eafebd5a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ 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 diff --git a/environment.yml b/environment.yml index f62a3b622..c6a5beba0 100644 --- a/environment.yml +++ b/environment.yml @@ -21,7 +21,6 @@ dependencies: - sphinx_rtd_theme - pip - pip: - - rinohtype - nbsphinx - sphinx_automodapi - - uproot + - uproot~=3.0 diff --git a/setup.py b/setup.py index 885dab826..08a5da36f 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,15 @@ __version__ = re.search('^__version__ = "(.*)"$', f.read()).group(1) extras_require = { - "docs": ["sphinx", "sphinx_rtd_theme", "sphinx_automodapi", "numpydoc", "nbsphinx"], - "tests": ["pytest", "pytest-cov", "uproot",], + "docs": [ + "sphinx", + "sphinx_rtd_theme", + "sphinx_automodapi", + "numpydoc", + "nbsphinx", + "uproot~=3.0", + ], + "tests": ["pytest", "pytest-cov"], } extras_require["all"] = extras_require["tests"] + extras_require["docs"] From 42d88b35204f357adaa66639fdbc18148a25979b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 15:52:33 +0200 Subject: [PATCH 100/105] Change author in setup to CTA ASWG --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 408ff7e9d..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, Maximilian Nöthe +author = CTA Consortium, Analysis and Simulation Working Group author_email = thomas.vuillaume@lapp.in2p3.fr license = MIT long_description = file: README.rst From aba7ed4430eefb9d7c0a4bbba35005a01e27632e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 17:42:21 +0200 Subject: [PATCH 101/105] Transpose irf arrays when writing to fits to match GADF order, add test reading IRFs with gammapy --- .../comparison_with_EventDisplay.ipynb | 20 +++-- pyirf/io/gadf.py | 12 ++- pyirf/io/tests/__init__.py | 0 pyirf/io/tests/test_gadf.py | 87 +++++++++++++++++++ setup.py | 2 +- 5 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 pyirf/io/tests/__init__.py create mode 100644 pyirf/io/tests/test_gadf.py diff --git a/docs/notebooks/comparison_with_EventDisplay.ipynb b/docs/notebooks/comparison_with_EventDisplay.ipynb index 04c94f06e..5df5c1a8d 100644 --- a/docs/notebooks/comparison_with_EventDisplay.ipynb +++ b/docs/notebooks/comparison_with_EventDisplay.ipynb @@ -182,13 +182,15 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": false + }, "outputs": [], "source": [ - "from astropy.table import Table\n", + "from astropy.table import QTable\n", "\n", "\n", - "theta_cut = Table.read(pyirf_file, hdu='THETA_CUTS_OPT')[1:-1]\n", + "rad_max = QTable.read(pyirf_file, hdu='RAD_MAX')[0]\n", "\n", "\n", "theta_cut_ed = irf_eventdisplay['ThetaCut;1']\n", @@ -201,9 +203,9 @@ ")\n", "\n", "plt.errorbar(\n", - " 0.5 * (theta_cut['low'] + theta_cut['high']),\n", - " theta_cut['cut'].quantity.to_value(u.deg)**2,\n", - " xerr=0.5 * (theta_cut['high'] - theta_cut['low']),\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", @@ -356,7 +358,7 @@ " \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)[1:-1, 0],\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", @@ -389,7 +391,7 @@ "source": [ "psf_table = QTable.read(pyirf_file, hdu='PSF')[0]\n", "# select the only fov offset bin\n", - "psf = psf_table['RPSF'][:, 0, :].to_value(1 / u.sr)\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", @@ -521,7 +523,7 @@ "migra_bins = edisp['MIGRA_LO'][1:]\n", "\n", "plt.title('pyirf')\n", - "plt.pcolormesh(e_bins.to_value(u.TeV), migra_bins, edisp['MATRIX'][1:-1, 1:-1, 0].T, cmap='inferno')\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", diff --git a/pyirf/io/gadf.py b/pyirf/io/gadf.py index aa1cd3e85..16c4a7870 100644 --- a/pyirf/io/gadf.py +++ b/pyirf/io/gadf.py @@ -66,7 +66,8 @@ def create_aeff2d_hdu( 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) - aeff["EFFAREA"] = effective_area[np.newaxis, ...].to(u.m ** 2) + # 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() @@ -130,7 +131,8 @@ def create_psf_table_hdu( "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), - "RPSF": psf[np.newaxis, ...].to(1 / u.sr), + # transpose as FITS uses opposite dimension order + "RPSF": psf.T[np.newaxis, ...].to(1 / u.sr), } ) @@ -193,7 +195,8 @@ def create_energy_dispersion_hdu( "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), - "MATRIX": u.Quantity(energy_dispersion[np.newaxis, ...]).to(u.one), + # transpose as FITS uses opposite dimension order + "MATRIX": u.Quantity(energy_dispersion.T[np.newaxis, ...]).to(u.one), } ) @@ -250,7 +253,8 @@ def create_rad_max_hdu( "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), - "RAD_MAX": rad_max[np.newaxis, ...].to(u.deg), + # transpose as FITS uses opposite dimension order + "RAD_MAX": rad_max.T[np.newaxis, ...].to(u.deg), } ) 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/setup.py b/setup.py index 08a5da36f..ac2449043 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ "nbsphinx", "uproot~=3.0", ], - "tests": ["pytest", "pytest-cov"], + "tests": ["pytest", "pytest-cov", "gammapy~=0.17"], } extras_require["all"] = extras_require["tests"] + extras_require["docs"] From d66fc2fe6892bdd1e6bd9181831c58fb0edaadad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sat, 26 Sep 2020 18:13:31 +0200 Subject: [PATCH 102/105] Add tests for benchmarks --- pyirf/benchmarks/tests/__init__.py | 0 .../tests/test_angular_resolution.py | 27 +++++++++++++ .../benchmarks/tests/test_bias_resolution.py | 40 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 pyirf/benchmarks/tests/__init__.py create mode 100644 pyirf/benchmarks/tests/test_angular_resolution.py create mode 100644 pyirf/benchmarks/tests/test_bias_resolution.py diff --git a/pyirf/benchmarks/tests/__init__.py b/pyirf/benchmarks/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyirf/benchmarks/tests/test_angular_resolution.py b/pyirf/benchmarks/tests/test_angular_resolution.py new file mode 100644 index 000000000..f0a888347 --- /dev/null +++ b/pyirf/benchmarks/tests/test_angular_resolution.py @@ -0,0 +1,27 @@ +from astropy.table import QTable +import astropy.units as u +import numpy as np + + +def test_angular_resolution(): + from pyirf.benchmarks import angular_resolution + + np.random.seed(1337) + + TRUE_RES_1 = 0.2 + TRUE_RES_2 = 0.05 + true_resolution = np.append(np.full(1000, TRUE_RES_1), np.full(1000, TRUE_RES_2)) + + events = QTable({ + 'true_energy': np.append(np.full(1000, 5.0), np.full(1000, 50.0)) * u.TeV, + 'theta': np.abs(np.random.normal(0, true_resolution)) * u.deg + }) + + ang_res = angular_resolution( + events, + [1, 10, 100] * u.TeV + )['angular_resolution'].quantity + + assert len(ang_res) == 2 + assert u.isclose(ang_res[0], TRUE_RES_1 * u.deg, rtol=0.05) + assert u.isclose(ang_res[1], TRUE_RES_2 * u.deg, rtol=0.05) diff --git a/pyirf/benchmarks/tests/test_bias_resolution.py b/pyirf/benchmarks/tests/test_bias_resolution.py new file mode 100644 index 000000000..a0c13a2b9 --- /dev/null +++ b/pyirf/benchmarks/tests/test_bias_resolution.py @@ -0,0 +1,40 @@ +from astropy.table import QTable +import astropy.units as u +import numpy as np + + +def test_energy_bias_resolution(): + from pyirf.benchmarks import energy_bias_resolution + + np.random.seed(1337) + + TRUE_RES_1 = 0.2 + TRUE_RES_2 = 0.05 + TRUE_BIAS_1 = 0.1 + TRUE_BIAS_2 = -0.05 + + true_bias = np.append(np.full(1000, TRUE_BIAS_1), np.full(1000, TRUE_BIAS_2)) + true_resolution = np.append(np.full(1000, TRUE_RES_1), np.full(1000, TRUE_RES_2)) + + true_energy = np.append(np.full(1000, 5.0), np.full(1000, 50.0)) * u.TeV + reco_energy = true_energy * (1 + np.random.normal(true_bias, true_resolution)) + + events = QTable({ + 'true_energy': true_energy, + 'reco_energy': reco_energy, + }) + + bias_resolution = energy_bias_resolution( + events, + [1, 10, 100] * u.TeV + ) + + bias = bias_resolution['bias'].quantity + resolution = bias_resolution['resolution'].quantity + + assert len(bias) == len(resolution) == 2 + + assert u.isclose(bias[0], TRUE_BIAS_1, rtol=0.05) + assert u.isclose(bias[1], TRUE_BIAS_2, rtol=0.05) + assert u.isclose(resolution[0], TRUE_RES_1, rtol=0.05) + assert u.isclose(resolution[1], TRUE_RES_2, rtol=0.05) From 4bd9d377c6d0d47dc089b1ec35167e05fbb27c5f Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Sun, 27 Sep 2020 13:31:07 +0200 Subject: [PATCH 103/105] Update README --- README.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 90a0edcab..d2e15a73f 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,15 @@ -========================================= -pyirf |travis| |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/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 library to calculate IACT IRFs and Sensitivities. + +**Documentation:** https://cta-observatory.github.io/pyirf/ From 15beef2f8ac5cc50212a681740ff357c68570241 Mon Sep 17 00:00:00 2001 From: Michele Peresano Date: Sun, 27 Sep 2020 14:57:25 +0200 Subject: [PATCH 104/105] Add cython to installation test extras --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ac2449043..320caee6e 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ "nbsphinx", "uproot~=3.0", ], - "tests": ["pytest", "pytest-cov", "gammapy~=0.17"], + "tests": ["pytest", "pytest-cov", "cython", "gammapy~=0.17"], } extras_require["all"] = extras_require["tests"] + extras_require["docs"] From 7c802a5b655c8a7ef7c71e47b4a90247be8e37e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Sun, 27 Sep 2020 15:13:15 +0200 Subject: [PATCH 105/105] Remove cython, does not fix the problem with installing gammapy --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 320caee6e..ac2449043 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ "nbsphinx", "uproot~=3.0", ], - "tests": ["pytest", "pytest-cov", "cython", "gammapy~=0.17"], + "tests": ["pytest", "pytest-cov", "gammapy~=0.17"], } extras_require["all"] = extras_require["tests"] + extras_require["docs"]