Source code for ctapipe.irf.binning

"""Collection of binning related functionality for the irf tools"""

import logging
from dataclasses import dataclass

import astropy.units as u
import numpy as np

from ..compat import COPY_IF_NEEDED
from ..core import Component
from ..core.traits import AstroQuantity, Integer

__all__ = [
    "ResultValidRange",
    "check_bins_in_range",
    "make_bins_per_decade",
    "DefaultTrueEnergyBins",
    "DefaultRecoEnergyBins",
    "DefaultFoVOffsetBins",
]

logger = logging.getLogger(__name__)


[docs] def check_bins_in_range(bins, valid_range, source="result", raise_error=True): """ Check whether ``bins`` are within a ``valid_range`` and either warn or raise an error if not. Parameters ---------- bins: u.Quantity The bins to be checked. valid_range: ctapipe.irf.ResultValidRange Range for which bins are valid. E.g. the range in which G/H cuts are calculated. source: str Description of which bins are being checked to give useful warnings/ error messages. raise_error: bool Whether to raise an error (True) or give a warning (False) if ``bins`` exceed ``valid_range``. """ low = bins >= valid_range.min hig = bins <= valid_range.max if not all(low & hig): with np.printoptions(edgeitems=2, threshold=6, precision=4): bins = np.array2string(bins) min_val = np.array2string(valid_range.min) max_val = np.array2string(valid_range.max) if raise_error: raise ValueError( f"Valid range for {source} is {min_val} to {max_val}, got {bins}" ) else: logger.warning( f"Valid range for {source} is {min_val} to {max_val}, got {bins}", )
[docs] @u.quantity_input(e_min=u.TeV, e_max=u.TeV) def make_bins_per_decade(e_min, e_max, n_bins_per_decade=5): """ Create energy bins with at least ``n_bins_per_decade`` bins per decade, while using the exact lower (``e_min``) and upper (``e_max``) limits. If you want to get exactly ``n_bins_per_decade`` use `pyirf.binning.create_bins_per_decade`. The number of bins is calculated as ``n_bins = ceil((log10(e_max) - log10(e_min)) * n_bins_per_decade)``. Parameters ---------- e_min: u.Quantity[energy] Minimum energy, inclusive e_max: u.Quantity[energy] Maximum energy, inclusive n_bins_per_decade: int Minimum 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)) n_bins = int(np.ceil((log_upper - log_lower) * n_bins_per_decade)) return u.Quantity( np.logspace(log_lower, log_upper, n_bins + 1), unit, copy=COPY_IF_NEEDED )
[docs] @dataclass class ResultValidRange: min: u.Quantity max: u.Quantity
[docs] class DefaultTrueEnergyBins(Component): """Base class for creating irfs or benchmarks binned in true energy.""" true_energy_min = AstroQuantity( help="Minimum value for True Energy bins", default_value=u.Quantity(0.015, u.TeV), physical_type=u.physical.energy, ).tag(config=True) true_energy_max = AstroQuantity( help="Maximum value for True Energy bins", default_value=u.Quantity(150, u.TeV), physical_type=u.physical.energy, ).tag(config=True) true_energy_n_bins_per_decade = Integer( help="Number of bins per decade for True Energy bins", default_value=10, ).tag(config=True) def __init__(self, config=None, parent=None, **kwargs): super().__init__(config=config, parent=parent, **kwargs) self.true_energy_bins = make_bins_per_decade( self.true_energy_min.to(u.TeV), self.true_energy_max.to(u.TeV), self.true_energy_n_bins_per_decade, )
[docs] class DefaultRecoEnergyBins(Component): """Base class for creating irfs or benchmarks binned in reconstructed energy.""" reco_energy_min = AstroQuantity( help="Minimum value for Reco Energy bins", default_value=u.Quantity(0.015, u.TeV), physical_type=u.physical.energy, ).tag(config=True) reco_energy_max = AstroQuantity( help="Maximum value for Reco Energy bins", default_value=u.Quantity(150, u.TeV), physical_type=u.physical.energy, ).tag(config=True) reco_energy_n_bins_per_decade = Integer( help="Number of bins per decade for Reco Energy bins", default_value=5, ).tag(config=True) def __init__(self, config=None, parent=None, **kwargs): super().__init__(config=config, parent=parent, **kwargs) self.reco_energy_bins = make_bins_per_decade( self.reco_energy_min.to(u.TeV), self.reco_energy_max.to(u.TeV), self.reco_energy_n_bins_per_decade, )
[docs] class DefaultFoVOffsetBins(Component): """Base class for creating radially symmetric irfs or benchmarks.""" fov_offset_min = AstroQuantity( help="Minimum value for FoV Offset bins", default_value=u.Quantity(0, u.deg), physical_type=u.physical.angle, ).tag(config=True) fov_offset_max = AstroQuantity( help="Maximum value for FoV offset bins", default_value=u.Quantity(5, u.deg), physical_type=u.physical.angle, ).tag(config=True) fov_offset_n_bins = Integer( help="Number of FoV offset bins", default_value=1, ).tag(config=True) def __init__(self, config=None, parent=None, **kwargs): super().__init__(config=config, parent=parent, **kwargs) self.fov_offset_bins = u.Quantity( np.linspace( self.fov_offset_min.to_value(u.deg), self.fov_offset_max.to_value(u.deg), self.fov_offset_n_bins + 1, ), u.deg, )