Exemple #1
0
class Background2D:
    """Background 2D.

    Data format specification: :ref:`gadf:bkg_2d`

    Parameters
    ----------
    energy_lo, energy_hi : `~astropy.units.Quantity`
        Energy binning
    offset_lo, offset_hi : `~astropy.units.Quantity`
        FOV coordinate offset-axis binning
    data : `~astropy.units.Quantity`
        Background rate (usually: ``s^-1 MeV^-1 sr^-1``)
    """

    default_interp_kwargs = dict(bounds_error=False, fill_value=None)
    """Default Interpolation kwargs for `~gammapy.utils.nddata.NDDataArray`. Extrapolate."""
    def __init__(
        self,
        energy_lo,
        energy_hi,
        offset_lo,
        offset_hi,
        data,
        meta=None,
        interp_kwargs=None,
    ):
        if interp_kwargs is None:
            interp_kwargs = self.default_interp_kwargs

        e_edges = edges_from_lo_hi(energy_lo, energy_hi)
        energy_axis = MapAxis.from_edges(e_edges, interp="log", name="energy")

        offset_edges = edges_from_lo_hi(offset_lo, offset_hi)
        offset_axis = MapAxis.from_edges(offset_edges,
                                         interp="lin",
                                         name="offset")

        self.data = NDDataArray(axes=[energy_axis, offset_axis],
                                data=data,
                                interp_kwargs=interp_kwargs)
        self.meta = meta or {}

    def __str__(self):
        ss = self.__class__.__name__
        ss += f"\n{self.data}"
        return ss

    @classmethod
    def from_table(cls, table):
        """Read from `~astropy.table.Table`."""
        # Spec says key should be "BKG", but there are files around
        # (e.g. CTA 1DC) that use "BGD". For now we support both
        if "BKG" in table.colnames:
            bkg_name = "BKG"
        elif "BGD" in table.colnames:
            bkg_name = "BGD"
        else:
            raise ValueError('Invalid column names. Need "BKG" or "BGD".')

        # Currently some files (e.g. CTA 1DC) contain unit in the FITS file
        # '1/s/MeV/sr', which is invalid ( try: astropy.units.Unit('1/s/MeV/sr')
        # This should be corrected.
        # For now, we hard-code the unit here:
        data_unit = u.Unit("s-1 MeV-1 sr-1")
        return cls(
            energy_lo=table["ENERG_LO"].quantity[0],
            energy_hi=table["ENERG_HI"].quantity[0],
            offset_lo=table["THETA_LO"].quantity[0],
            offset_hi=table["THETA_HI"].quantity[0],
            data=table[bkg_name].data[0] * data_unit,
            meta=table.meta,
        )

    @classmethod
    def from_hdulist(cls, hdulist, hdu="BACKGROUND"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="BACKGROUND"):
        """Read from file."""
        with fits.open(make_path(filename), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu=hdu)

    def to_table(self):
        """Convert to `~astropy.table.Table`."""
        meta = self.meta.copy()
        table = Table(meta=meta)

        theta = self.data.axis("offset").edges
        energy = self.data.axis("energy").edges

        table["THETA_LO"] = theta[:-1][np.newaxis]
        table["THETA_HI"] = theta[1:][np.newaxis]
        table["ENERG_LO"] = energy[:-1][np.newaxis]
        table["ENERG_HI"] = energy[1:][np.newaxis]
        table["BKG"] = self.data.data[np.newaxis]
        return table

    def to_fits(self, name="BACKGROUND"):
        """Convert to `~astropy.io.fits.BinTableHDU`."""
        return fits.BinTableHDU(self.to_table(), name=name)

    def evaluate(self,
                 fov_lon,
                 fov_lat,
                 energy_reco,
                 method="linear",
                 **kwargs):
        """Evaluate at a given FOV position and energy.

        The fov_lon, fov_lat, energy_reco has to have the same shape
        since this is a set of points on which you want to evaluate.

        To have the same API than background 3D for the
        background evaluation, the offset is ``fov_altaz_lon``.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame, same shape than energy_reco
        energy_reco : `~astropy.units.Quantity`
            Reconstructed energy, same dimension than fov_lat and fov_lat
        method : str {'linear', 'nearest'}, optional
            Interpolation method
        kwargs : dict
            option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        array : `~astropy.units.Quantity`
            Interpolated values, axis order is the same as for the NDData array
        """
        offset = np.sqrt(fov_lon**2 + fov_lat**2)
        return self.data.evaluate(offset=offset,
                                  energy=energy_reco,
                                  method=method,
                                  **kwargs)

    def evaluate_integrate(self,
                           fov_lon,
                           fov_lat,
                           energy_reco,
                           method="linear"):
        """Evaluate at given FOV position and energy, by integrating over the energy range.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame.
        energy_reco: `~astropy.units.Quantity`
            Reconstructed energy edges.
        method : {'linear', 'nearest'}, optional
            Interpolation method

        Returns
        -------
        array : `~astropy.units.Quantity`
            Returns 2D array with axes offset
        """
        data = self.evaluate(fov_lon, fov_lat, energy_reco, method=method)
        return trapz_loglog(data, energy_reco, axis=0)

    def to_3d(self):
        """Convert to `Background3D`.

        Fill in a radially symmetric way.
        """
        raise NotImplementedError

    def plot(self, ax=None, add_cbar=True, **kwargs):
        """Plot energy offset dependence of the background model.
        """
        import matplotlib.pyplot as plt
        from matplotlib.colors import LogNorm

        ax = plt.gca() if ax is None else ax

        x = self.data.axis("energy").edges.to_value("TeV")
        y = self.data.axis("offset").edges.to_value("deg")
        z = self.data.data.T.value

        kwargs.setdefault("cmap", "GnBu")
        kwargs.setdefault("edgecolors", "face")

        caxes = ax.pcolormesh(x, y, z, norm=LogNorm(), **kwargs)
        ax.set_xscale("log")
        ax.set_ylabel(f"Offset (deg)")
        ax.set_xlabel(f"Energy (TeV)")

        xmin, xmax = x.min(), x.max()
        ax.set_xlim(xmin, xmax)

        if add_cbar:
            label = f"Background rate ({self.data.data.unit})"
            ax.figure.colorbar(caxes, ax=ax, label=label)

    def peek(self):
        from .effective_area import EffectiveAreaTable2D

        return EffectiveAreaTable2D.peek(self)
Exemple #2
0
class BgRateTable(object):
    """Background rate table.

    The IRF format should be compliant with the one discussed
    at http://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/.
    Work will be done to fix this.

    Parameters
    -----------
    energy_lo, energy_hi : `~astropy.units.Quantity`, `~gammapy.utils.nddata.BinnedDataAxis`
        Bin edges of energy axis
    data : `~astropy.units.Quantity`
        Background rate
    """
    def __init__(self, energy_lo, energy_hi, data):
        axes = [
            BinnedDataAxis(energy_lo,
                           energy_hi,
                           interpolation_mode='log',
                           name='energy'),
        ]
        self.data = NDDataArray(axes=axes, data=data)

    @property
    def energy(self):
        return self.data.axes[0]

    @classmethod
    def from_table(cls, table):
        """Background rate reader"""
        energy_lo = table['ENERG_LO'].quantity
        energy_hi = table['ENERG_HI'].quantity
        data = table['BGD'].quantity
        return cls(energy_lo=energy_lo, energy_hi=energy_hi, data=data)

    @classmethod
    def from_hdulist(cls, hdulist, hdu='BACKGROUND'):
        fits_table = hdulist[hdu]
        table = Table.read(fits_table)
        return cls.from_table(table)

    @classmethod
    def read(cls, filename, hdu='BACKGROUND'):
        filename = make_path(filename)
        with fits.open(str(filename), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu=hdu)

    def plot(self, ax=None, energy=None, **kwargs):
        """Plot background rate.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        energy : `~astropy.units.Quantity`
            Energy nodes

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt
        ax = plt.gca() if ax is None else ax

        energy = energy or self.energy.nodes
        values = self.data.evaluate(energy=energy)
        xerr = (
            energy.value - self.energy.lo.value,
            self.energy.hi.value - energy.value,
        )
        ax.errorbar(energy.value, values.value, xerr=xerr, fmt='o', **kwargs)
        ax.set_xscale('log')
        ax.set_yscale('log')
        ax.set_xlabel('Energy [{}]'.format(self.energy.unit))
        ax.set_ylabel('Background rate [{}]'.format(self.data.data.unit))

        return ax
Exemple #3
0
class Background3D:
    """Background 3D.

    Data format specification: :ref:`gadf:bkg_3d`

    Parameters
    ----------
    energy_lo, energy_hi : `~astropy.units.Quantity`
        Energy binning
    fov_lon_lo, fov_lon_hi : `~astropy.units.Quantity`
        FOV coordinate X-axis binning.
    fov_lat_lo, fov_lat_hi : `~astropy.units.Quantity`
        FOV coordinate Y-axis binning.
    data : `~astropy.units.Quantity`
        Background rate (usually: ``s^-1 MeV^-1 sr^-1``)

    Examples
    --------
    Here's an example you can use to learn about this class:

    >>> from gammapy.irf import Background3D
    >>> filename = '$GAMMAPY_DATA/cta-1dc/caldb/data/cta/1dc/bcf/South_z20_50h/irf_file.fits'
    >>> bkg_3d = Background3D.read(filename, hdu='BACKGROUND')
    >>> print(bkg_3d)
    Background3D
    NDDataArray summary info
    energy         : size =    21, min =  0.016 TeV, max = 158.489 TeV
    fov_lon           : size =    36, min = -5.833 deg, max =  5.833 deg
    fov_lat           : size =    36, min = -5.833 deg, max =  5.833 deg
    Data           : size = 27216, min =  0.000 1 / (MeV s sr), max =  0.421 1 / (MeV s sr)
    """

    default_interp_kwargs = dict(bounds_error=False,
                                 fill_value=None,
                                 values_scale="log")
    """Default Interpolation kwargs for `~gammapy.utils.nddata.NDDataArray`. Extrapolate."""
    def __init__(
        self,
        energy_lo,
        energy_hi,
        fov_lon_lo,
        fov_lon_hi,
        fov_lat_lo,
        fov_lat_hi,
        data,
        meta=None,
        interp_kwargs=None,
    ):
        if interp_kwargs is None:
            interp_kwargs = self.default_interp_kwargs

        e_edges = edges_from_lo_hi(energy_lo, energy_hi)
        energy_axis = MapAxis.from_edges(e_edges, interp="log", name="energy")

        fov_lon_edges = edges_from_lo_hi(fov_lon_lo, fov_lon_hi)
        fov_lon_axis = MapAxis.from_edges(fov_lon_edges,
                                          interp="lin",
                                          name="fov_lon")

        fov_lat_edges = edges_from_lo_hi(fov_lat_lo, fov_lat_hi)
        fov_lat_axis = MapAxis.from_edges(fov_lat_edges,
                                          interp="lin",
                                          name="fov_lat")

        self.data = NDDataArray(
            axes=[energy_axis, fov_lon_axis, fov_lat_axis],
            data=data,
            interp_kwargs=interp_kwargs,
        )
        self.meta = meta or {}

    def __str__(self):
        ss = self.__class__.__name__
        ss += f"\n{self.data}"
        return ss

    @classmethod
    def from_table(cls, table):
        """Read from `~astropy.table.Table`."""
        # Spec says key should be "BKG", but there are files around
        # (e.g. CTA 1DC) that use "BGD". For now we support both
        if "BKG" in table.colnames:
            bkg_name = "BKG"
        elif "BGD" in table.colnames:
            bkg_name = "BGD"
        else:
            raise ValueError('Invalid column names. Need "BKG" or "BGD".')

        # Currently some files (e.g. CTA 1DC) contain unit in the FITS file
        # '1/s/MeV/sr', which is invalid ( try: astropy.units.Unit('1/s/MeV/sr')
        # This should be corrected.
        # For now, we hard-code the unit here:
        data_unit = u.Unit("s-1 MeV-1 sr-1")

        return cls(
            energy_lo=table["ENERG_LO"].quantity[0],
            energy_hi=table["ENERG_HI"].quantity[0],
            fov_lon_lo=table["DETX_LO"].quantity[0],
            fov_lon_hi=table["DETX_HI"].quantity[0],
            fov_lat_lo=table["DETY_LO"].quantity[0],
            fov_lat_hi=table["DETY_HI"].quantity[0],
            data=table[bkg_name].data[0] * data_unit,
            meta=table.meta,
        )

    @classmethod
    def from_hdulist(cls, hdulist, hdu="BACKGROUND"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="BACKGROUND"):
        """Read from file."""
        with fits.open(make_path(filename), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu=hdu)

    def to_table(self):
        """Convert to `~astropy.table.Table`."""
        meta = self.meta.copy()

        detx = self.data.axis("fov_lon").edges
        dety = self.data.axis("fov_lat").edges
        energy = self.data.axis("energy").edges

        table = Table(meta=meta)
        table["DETX_LO"] = detx[:-1][np.newaxis]
        table["DETX_HI"] = detx[1:][np.newaxis]
        table["DETY_LO"] = dety[:-1][np.newaxis]
        table["DETY_HI"] = dety[1:][np.newaxis]
        table["ENERG_LO"] = energy[:-1][np.newaxis]
        table["ENERG_HI"] = energy[1:][np.newaxis]
        table["BKG"] = self.data.data[np.newaxis]
        return table

    def to_fits(self, name="BACKGROUND"):
        """Convert to `~astropy.io.fits.BinTableHDU`."""
        return fits.BinTableHDU(self.to_table(), name=name)

    def evaluate(self,
                 fov_lon,
                 fov_lat,
                 energy_reco,
                 method="linear",
                 **kwargs):
        """Evaluate at given FOV position and energy.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame.
        energy_reco : `~astropy.units.Quantity`
            energy on which you want to interpolate. Same dimension than fov_lat and fov_lat
        method : str {'linear', 'nearest'}, optional
            Interpolation method
        kwargs : dict
            option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        array : `~astropy.units.Quantity`
            Interpolated values, axis order is the same as for the NDData array
        """
        values = self.data.evaluate(
            fov_lon=fov_lon,
            fov_lat=fov_lat,
            energy=energy_reco,
            method=method,
            **kwargs,
        )
        return values

    def evaluate_integrate(self,
                           fov_lon,
                           fov_lat,
                           energy_reco,
                           method="linear",
                           **kwargs):
        """Integrate in a given energy band.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame.
        energy_reco: `~astropy.units.Quantity`
            Reconstructed energy edges.
        method : {'linear', 'nearest'}, optional
            Interpolation method

        Returns
        -------
        array : `~astropy.units.Quantity`
            Returns 2D array with axes offset
        """
        data = self.evaluate(fov_lon, fov_lat, energy_reco, method=method)
        return trapz_loglog(data, energy_reco, axis=0)

    def to_2d(self):
        """Convert to `Background2D`.

        This takes the values at Y = 0 and X >= 0.
        """
        idx_lon = self.data.axis("fov_lon").coord_to_idx(0 * u.deg)[0]
        idx_lat = self.data.axis("fov_lat").coord_to_idx(0 * u.deg)[0]
        data = self.data.data[:, idx_lon:, idx_lat].copy()

        energy = self.data.axis("energy").edges
        offset = self.data.axis("fov_lon").edges[idx_lon:]

        return Background2D(
            energy_lo=energy[:-1],
            energy_hi=energy[1:],
            offset_lo=offset[:-1],
            offset_hi=offset[1:],
            data=data,
        )
Exemple #4
0
class EffectiveAreaTable:
    """Effective area table.

    TODO: Document

    Parameters
    ----------
    energy_axis_true : `MapAxis`
        Energy axis
    data : `~astropy.units.Quantity`
        Effective area

    Examples
    --------
    Plot parametrized effective area for HESS, HESS2 and CTA.

    .. plot::
        :include-source:

        import numpy as np
        import matplotlib.pyplot as plt
        import astropy.units as u
        from gammapy.irf import EffectiveAreaTable

        energy = np.logspace(-3, 3, 100) * u.TeV

        for instrument in ['HESS', 'HESS2', 'CTA']:
            aeff = EffectiveAreaTable.from_parametrization(energy, instrument)
            ax = aeff.plot(label=instrument)

        ax.set_yscale('log')
        ax.set_xlim([1e-3, 1e3])
        ax.set_ylim([1e3, 1e12])
        plt.legend(loc='best')
        plt.show()

    Find energy where the effective area is at 10% of its maximum value

    >>> import numpy as np
    >>> import astropy.units as u
    >>> from gammapy.irf import EffectiveAreaTable
    >>> energy = np.logspace(-1, 2) * u.TeV
    >>> aeff_max = aeff.max_area
    >>> print(aeff_max).to('m2')
    156909.413371 m2
    >>> energy_threshold = aeff.find_energy(0.1 * aeff_max)
    >>> print(energy_threshold)
    0.185368478744 TeV
    """
    def __init__(self, energy_axis_true, data, meta=None):
        interp_kwargs = {"extrapolate": False, "bounds_error": False}

        assert energy_axis_true.name == "energy_true"

        self.data = NDDataArray(axes=[energy_axis_true],
                                data=data,
                                interp_kwargs=interp_kwargs)
        self.meta = meta or {}

    @property
    def energy(self):
        return self.data.axes["energy_true"]

    def plot(self, ax=None, energy=None, show_energy=None, **kwargs):
        """Plot effective area.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        energy : `~astropy.units.Quantity`
            Energy nodes
        show_energy : `~astropy.units.Quantity`, optional
            Show energy, e.g. threshold, as vertical line

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        kwargs.setdefault("lw", 2)

        if energy is None:
            energy = self.energy.center

        eff_area = self.data.evaluate(energy_true=energy)

        xerr = (
            (energy - self.energy.edges[:-1]).value,
            (self.energy.edges[1:] - energy).value,
        )

        ax.errorbar(energy.value, eff_area.value, xerr=xerr, **kwargs)
        if show_energy is not None:
            ener_val = u.Quantity(show_energy).to_value(self.energy.unit)
            ax.vlines(ener_val,
                      0,
                      1.1 * self.max_area.value,
                      linestyles="dashed")
        ax.set_xscale("log")
        ax.set_xlabel(f"Energy [{self.energy.unit}]")
        ax.set_ylabel(f"Effective Area [{self.data.data.unit}]")

        return ax

    @classmethod
    def from_parametrization(cls, energy, instrument="HESS"):
        r"""Create parametrized effective area.

        Parametrizations of the effective areas of different Cherenkov
        telescopes taken from Appendix B of Abramowski et al. (2010), see
        https://ui.adsabs.harvard.edu/abs/2010MNRAS.402.1342A .

        .. math::
            A_{eff}(E) = g_1 \left(\frac{E}{\mathrm{MeV}}\right)^{-g_2}\exp{\left(-\frac{g_3}{E}\right)}

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy binning, analytic function is evaluated at log centers
        instrument : {'HESS', 'HESS2', 'CTA'}
            Instrument name
        """
        energy = u.Quantity(energy)
        # Put the parameters g in a dictionary.
        # Units: g1 (cm^2), g2 (), g3 (MeV)
        # Note that whereas in the paper the parameter index is 1-based,
        # here it is 0-based
        pars = {
            "HESS": [6.85e9, 0.0891, 5e5],
            "HESS2": [2.05e9, 0.0891, 1e5],
            "CTA": [1.71e11, 0.0891, 1e5],
        }

        if instrument not in pars.keys():
            ss = f"Unknown instrument: {instrument}\n"
            ss += "Valid instruments: HESS, HESS2, CTA"
            raise ValueError(ss)

        energy_axis_true = MapAxis.from_edges(energy,
                                              interp="log",
                                              name="energy_true")

        g1 = pars[instrument][0]
        g2 = pars[instrument][1]
        g3 = -pars[instrument][2]

        energy = energy_axis_true.center.to_value("MeV")
        value = g1 * energy**(-g2) * np.exp(g3 / energy)
        data = u.Quantity(value, "cm2", copy=False)

        return cls(energy_axis_true=energy_axis_true, data=data)

    @classmethod
    def from_constant(cls, energy, value):
        """Create constant value effective area.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy binning, analytic function is evaluated at log centers
        value : `~astropy.units.Quantity`
            Effective area
        """
        data = np.ones((len(energy) - 1)) * u.Quantity(value)
        energy_axis_true = MapAxis.from_energy_edges(energy,
                                                     name="energy_true")
        return cls(energy_axis_true=energy_axis_true, data=data)

    @classmethod
    def from_table(cls, table):
        """Create from `~astropy.table.Table` in ARF format.

        Data format specification: :ref:`gadf:ogip-arf`
        """
        energy_axis_true = MapAxis.from_table(table, format="ogip-arf")
        data = table["SPECRESP"].quantity
        return cls(energy_axis_true=energy_axis_true, data=data)

    @classmethod
    def from_hdulist(cls, hdulist, hdu="SPECRESP"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="SPECRESP"):
        """Read from file."""
        filename = str(make_path(filename))
        with fits.open(filename, memmap=False) as hdulist:
            try:
                return cls.from_hdulist(hdulist, hdu=hdu)
            except KeyError:
                raise ValueError(f"File {filename} contains no HDU {hdu!r}\n"
                                 f"Available: {[_.name for _ in hdulist]}")

    def to_table(self):
        """Convert to `~astropy.table.Table` in ARF format.

        Data format specification: :ref:`gadf:ogip-arf`
        """
        table = Table()
        table.meta = {
            "EXTNAME": "SPECRESP",
            "hduclass": "OGIP",
            "hduclas1": "RESPONSE",
            "hduclas2": "SPECRESP",
        }

        energy = self.energy.edges
        table["ENERG_LO"] = energy[:-1]
        table["ENERG_HI"] = energy[1:]
        table["SPECRESP"] = self.evaluate_fill_nan()
        return table

    def to_region_map(self, region=None):
        """"""
        axis = self.data.axes["energy_true"]
        geom = RegionGeom(region=region, axes=[axis])
        return RegionNDMap.from_geom(geom=geom,
                                     data=self.data.data.value,
                                     unit=self.data.data.unit)

    def to_hdulist(self, name=None, use_sherpa=False):
        """Convert to `~astropy.io.fits.HDUList`."""
        table = self.to_table()

        if use_sherpa:
            table["ENERG_HI"] = table["ENERG_HI"].quantity.to("keV")
            table["ENERG_LO"] = table["ENERG_LO"].quantity.to("keV")
            table["SPECRESP"] = table["SPECRESP"].quantity.to("cm2")

        return fits.HDUList(
            [fits.PrimaryHDU(),
             fits.BinTableHDU(table, name=name)])

    def write(self, filename, use_sherpa=False, **kwargs):
        """Write to file."""
        filename = str(make_path(filename))
        self.to_hdulist(use_sherpa=use_sherpa).writeto(filename, **kwargs)

    def evaluate_fill_nan(self, **kwargs):
        """Modified evaluate function.

        Calls :func:`gammapy.utils.nddata.NDDataArray.evaluate` and replaces
        possible nan values. Below the finite range the effective area is set
        to zero and above to value of the last valid note. This is needed since
        other codes, e.g. sherpa, don't like nan values in FITS files. Make
        sure that the replacement happens outside of the energy range, where
        the `~gammapy.irf.EffectiveAreaTable` is used.
        """
        retval = self.data.evaluate(**kwargs)
        idx = np.where(np.isfinite(retval))[0]
        retval[np.arange(idx[0])] = 0
        retval[np.arange(idx[-1], len(retval))] = retval[idx[-1]]
        return retval

    @property
    def max_area(self):
        """Maximum effective area."""
        cleaned_data = self.data.data[np.where(~np.isnan(self.data.data))]
        return cleaned_data.max()

    def find_energy(self, aeff, emin=None, emax=None):
        """Find energy for a given effective area.

        In case the solution is not unique, provide the `emin` or `emax` arguments
        to limit the solution to the given range. By default the peak energy of the
        effective area is chosen as `emax`.

        Parameters
        ----------
        aeff : `~astropy.units.Quantity`
            Effective area value
        emin : `~astropy.units.Quantity`
            Lower bracket value in case solution is not unique.
        emax : `~astropy.units.Quantity`
            Upper bracket value in case solution is not unique.

        Returns
        -------
        energy : `~astropy.units.Quantity`
            Energy corresponding to the given aeff.
        """
        from gammapy.modeling.models import TemplateSpectralModel

        energy = self.energy.center

        if emin is None:
            emin = energy[0]
        if emax is None:
            # use the peak effective area as a default for the energy maximum
            emax = energy[np.argmax(self.data.data)]

        aeff_spectrum = TemplateSpectralModel(energy, self.data.data)
        return aeff_spectrum.inverse(aeff, emin=emin, emax=emax)
Exemple #5
0
class EffectiveAreaTable2D:
    """2D effective area table.

    Data format specification: :ref:`gadf:aeff_2d`

    Parameters
    ----------
    energy_axis_true : `MapAxis`
        True energy axis
    offset_axis : `MapAxis`
        Field of view offset axis.
    data : `~astropy.units.Quantity`
        Effective area

    Examples
    --------
    Here's an example you can use to learn about this class:

    >>> from gammapy.irf import EffectiveAreaTable2D
    >>> filename = '$GAMMAPY_DATA/cta-1dc/caldb/data/cta/1dc/bcf/South_z20_50h/irf_file.fits'
    >>> aeff = EffectiveAreaTable2D.read(filename, hdu='EFFECTIVE AREA')
    >>> print(aeff)
    EffectiveAreaTable2D
    NDDataArray summary info
    energy         : size =    42, min =  0.014 TeV, max = 177.828 TeV
    offset         : size =     6, min =  0.500 deg, max =  5.500 deg
    Data           : size =   252, min =  0.000 m2, max = 5371581.000 m2

    Here's another one, created from scratch, without reading a file:

    >>> from gammapy.irf import EffectiveAreaTable2D
    >>> import astropy.units as u
    >>> import numpy as np
    >>> energy = np.logspace(0,1,11) * u.TeV
    >>> offset = np.linspace(0,1,4) * u.deg
    >>> data = np.ones(shape=(10,3)) * u.cm * u.cm
    >>> aeff = EffectiveAreaTable2D(energy_lo=energy[:-1], energy_hi=energy[1:], offset_lo=offset[:-1],
    >>>                             offset_hi=offset[1:], data= data)
    >>> print(aeff)
    Data array summary info
    energy         : size =    11, min =  1.000 TeV, max = 10.000 TeV
    offset         : size =     4, min =  0.000 deg, max =  1.000 deg
    Data           : size =    30, min =  1.000 cm2, max =  1.000 cm2
    """
    tag = "aeff_2d"
    default_interp_kwargs = dict(bounds_error=False, fill_value=None)
    """Default Interpolation kwargs for `~NDDataArray`. Extrapolate."""
    def __init__(
        self,
        energy_axis_true,
        offset_axis,
        data,
        meta=None,
        interp_kwargs=None,
    ):
        assert energy_axis_true.name == "energy_true"
        assert offset_axis.name == "offset"

        if interp_kwargs is None:
            interp_kwargs = self.default_interp_kwargs

        self.data = NDDataArray(axes=[energy_axis_true, offset_axis],
                                data=data,
                                interp_kwargs=interp_kwargs)
        self.meta = meta or {}

    def __str__(self):
        ss = self.__class__.__name__
        ss += f"\n{self.data}"
        return ss

    @property
    def low_threshold(self):
        """Low energy threshold"""
        return self.meta["LO_THRES"] * u.TeV

    @property
    def high_threshold(self):
        """High energy threshold"""
        return self.meta["HI_THRES"] * u.TeV

    @classmethod
    def from_table(cls, table):
        """Read from `~astropy.table.Table`."""
        energy_axis_true = MapAxis.from_table(table,
                                              column_prefix="ENERG",
                                              format="gadf-dl3")
        offset_axis = MapAxis.from_table(table,
                                         column_prefix="THETA",
                                         format="gadf-dl3")

        return cls(
            energy_axis_true=energy_axis_true,
            offset_axis=offset_axis,
            data=table["EFFAREA"].quantity[0].transpose(),
            meta=table.meta,
        )

    @classmethod
    def from_hdulist(cls, hdulist, hdu="EFFECTIVE AREA"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="EFFECTIVE AREA"):
        """Read from file."""
        with fits.open(str(make_path(filename)), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu=hdu)

    def to_effective_area_table(self, offset, energy=None):
        """Evaluate at a given offset and return `~gammapy.irf.EffectiveAreaTable`.

        Parameters
        ----------
        offset : `~astropy.coordinates.Angle`
            Offset
        energy : `~astropy.units.Quantity`
            Energy axis bin edges
        """
        if energy is None:
            energy_axis_true = self.data.axes["energy_true"]
        else:
            energy_axis_true = MapAxis.from_energy_edges(energy,
                                                         name="energy_true")

        area = self.data.evaluate(offset=offset,
                                  energy_true=energy_axis_true.center)

        return EffectiveAreaTable(energy_axis_true=energy_axis_true, data=area)

    def plot_energy_dependence(self,
                               ax=None,
                               offset=None,
                               energy=None,
                               **kwargs):
        """Plot effective area versus energy for a given offset.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`
            Offset
        energy : `~astropy.units.Quantity`
            Energy axis
        kwargs : dict
            Forwarded tp plt.plot()

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        if offset is None:
            off_min, off_max = self.data.axes["offset"].center[[0, -1]]
            offset = np.linspace(off_min.value, off_max.value,
                                 4) * off_min.unit

        if energy is None:
            energy = self.data.axes["energy_true"].center

        for off in offset:
            area = self.data.evaluate(offset=off, energy_true=energy)
            kwargs.setdefault("label", f"offset = {off:.1f}")
            ax.plot(energy, area.value, **kwargs)

        ax.set_xscale("log")
        ax.set_xlabel(f"Energy [{energy.unit}]")
        ax.set_ylabel(f"Effective Area [{self.data.data.unit}]")
        ax.set_xlim(min(energy.value), max(energy.value))
        return ax

    def plot_offset_dependence(self,
                               ax=None,
                               offset=None,
                               energy=None,
                               **kwargs):
        """Plot effective area versus offset for a given energy.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`
            Offset axis
        energy : `~astropy.units.Quantity`
            Energy

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        if energy is None:
            energy_axis = self.data.axes["energy_true"]
            e_min, e_max = np.log10(energy_axis.center.value[[0, -1]])
            energy = np.logspace(e_min, e_max, 4) * energy_axis.unit

        if offset is None:
            offset = self.data.axes["offset"].center

        for ee in energy:
            area = self.data.evaluate(offset=offset, energy_true=ee)
            area /= np.nanmax(area)
            if np.isnan(area).all():
                continue
            label = f"energy = {ee:.1f}"
            ax.plot(offset, area, label=label, **kwargs)

        ax.set_ylim(0, 1.1)
        ax.set_xlabel(f"Offset ({self.data.axes['offset'].unit})")
        ax.set_ylabel("Relative Effective Area")
        ax.legend(loc="best")

        return ax

    def plot(self, ax=None, add_cbar=True, **kwargs):
        """Plot effective area image."""
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        energy = self.data.axes["energy_true"].edges
        offset = self.data.axes["offset"].edges
        aeff = self.data.evaluate(offset=offset,
                                  energy_true=energy[:, np.newaxis])

        vmin, vmax = np.nanmin(aeff.value), np.nanmax(aeff.value)

        kwargs.setdefault("cmap", "GnBu")
        kwargs.setdefault("edgecolors", "face")
        kwargs.setdefault("vmin", vmin)
        kwargs.setdefault("vmax", vmax)

        caxes = ax.pcolormesh(energy.value, offset.value, aeff.value.T,
                              **kwargs)

        ax.set_xscale("log")
        ax.set_ylabel(f"Offset ({offset.unit})")
        ax.set_xlabel(f"Energy ({energy.unit})")

        xmin, xmax = energy.value.min(), energy.value.max()
        ax.set_xlim(xmin, xmax)

        if add_cbar:
            label = f"Effective Area ({aeff.unit})"
            ax.figure.colorbar(caxes, ax=ax, label=label)

        return ax

    def peek(self, figsize=(15, 5)):
        """Quick-look summary plots."""
        import matplotlib.pyplot as plt

        fig, axes = plt.subplots(nrows=1, ncols=3, figsize=figsize)
        self.plot(ax=axes[2])
        self.plot_energy_dependence(ax=axes[0])
        self.plot_offset_dependence(ax=axes[1])
        plt.tight_layout()

    def to_table(self):
        """Convert to `~astropy.table.Table`."""
        table = self.data.axes.to_table(format="gadf-dl3")
        table.meta = self.meta.copy()
        table["EFFAREA"] = self.data.data.T[np.newaxis]
        return table

    def to_table_hdu(self, name="EFFECTIVE AREA"):
        """Convert to `~astropy.io.fits.BinTableHDU`."""
        return fits.BinTableHDU(self.to_table(), name=name)
Exemple #6
0
class Background3D:
    """Background 3D.

    Data format specification: :ref:`gadf:bkg_3d`

    Parameters
    ----------
    energy_axis : `MapAxis`
        Energy axis
    fov_lon_axis: `MapAxis`
        FOV coordinate X-axis
    fov_lat_axis : `MapAxis`
        FOV coordinate Y-axis.
    data : `~astropy.units.Quantity`
        Background rate (usually: ``s^-1 MeV^-1 sr^-1``)

    Examples
    --------
    Here's an example you can use to learn about this class:

    >>> from gammapy.irf import Background3D
    >>> filename = '$GAMMAPY_DATA/cta-1dc/caldb/data/cta/1dc/bcf/South_z20_50h/irf_file.fits'
    >>> bkg_3d = Background3D.read(filename, hdu='BACKGROUND')
    >>> print(bkg_3d)
    Background3D
    NDDataArray summary info
    energy         : size =    21, min =  0.016 TeV, max = 158.489 TeV
    fov_lon           : size =    36, min = -5.833 deg, max =  5.833 deg
    fov_lat           : size =    36, min = -5.833 deg, max =  5.833 deg
    Data           : size = 27216, min =  0.000 1 / (MeV s sr), max =  0.421 1 / (MeV s sr)
    """

    tag = "bkg_3d"
    default_interp_kwargs = dict(
        bounds_error=False, fill_value=None, values_scale="log"
    )
    """Default Interpolation kwargs for `~gammapy.utils.nddata.NDDataArray`. Extrapolate."""

    def __init__(
        self,
        energy_axis,
        fov_lon_axis,
        fov_lat_axis,
        data,
        meta=None,
        interp_kwargs=None,
    ):
        if interp_kwargs is None:
            interp_kwargs = self.default_interp_kwargs

        self.data = NDDataArray(
            axes=[energy_axis, fov_lon_axis, fov_lat_axis],
            data=data,
            interp_kwargs=interp_kwargs,
        )
        self.meta = meta or {}

    def __str__(self):
        ss = self.__class__.__name__
        ss += f"\n{self.data}"
        return ss

    @classmethod
    def from_table(cls, table):
        """Read from `~astropy.table.Table`."""
        # Spec says key should be "BKG", but there are files around
        # (e.g. CTA 1DC) that use "BGD". For now we support both
        if "BKG" in table.colnames:
            bkg_name = "BKG"
        elif "BGD" in table.colnames:
            bkg_name = "BGD"
        else:
            raise ValueError('Invalid column names. Need "BKG" or "BGD".')

        data_unit = table[bkg_name].unit
        if data_unit is not None:
            data_unit = u.Unit(table[bkg_name].unit, parse_strict="silent")
        if isinstance(data_unit, u.UnrecognizedUnit) or (data_unit is None):
            data_unit = u.Unit("s-1 MeV-1 sr-1")
            log.warning(
                "Invalid unit found in background table! Assuming (s-1 MeV-1 sr-1)"
            )

        energy_axis = MapAxis.from_table(
            table, column_prefix="ENERG", format="gadf-dl3"
        )
        fov_lon_axis = MapAxis.from_table(
            table, column_prefix="DETX", format="gadf-dl3"
        )
        fov_lat_axis = MapAxis.from_table(
            table, column_prefix="DETY", format="gadf-dl3"
        )

        # TODO: The present HESS and CTA backgroundfits files
        #  have a reverse order (lon, lat, E) than recommened in GADF(E, lat, lon)
        #  For now, we suport both.

        data = table[bkg_name].data[0].T * data_unit
        shape = (energy_axis.nbin, fov_lon_axis.nbin, fov_lat_axis.nbin)

        if shape == shape[::-1]:
            log.error("Ambiguous axes order in Background fits files!")

        if np.shape(data) != shape:
            log.debug("Transposing background table on read")
            data = data.transpose()

        return cls(
            energy_axis=energy_axis,
            fov_lon_axis=fov_lon_axis,
            fov_lat_axis=fov_lat_axis,
            data=data,
            meta=table.meta,
        )

    @classmethod
    def from_hdulist(cls, hdulist, hdu="BACKGROUND"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="BACKGROUND"):
        """Read from file."""
        with fits.open(str(make_path(filename)), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu=hdu)

    def to_table(self):
        """Convert to `~astropy.table.Table`."""
        # TODO: fix axis order
        axes = MapAxes(self.data.axes[::-1])
        table = axes.to_table(format="gadf-dl3")
        table.meta = self.meta.copy()
        table["BKG"] = self.data.data.T[np.newaxis]
        return table

    def to_table_hdu(self, name="BACKGROUND"):
        """Convert to `~astropy.io.fits.BinTableHDU`."""
        return fits.BinTableHDU(self.to_table(), name=name)

    def evaluate(self, fov_lon, fov_lat, energy_reco, method="linear", **kwargs):
        """Evaluate at given FOV position and energy.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame.
        energy_reco : `~astropy.units.Quantity`
            energy on which you want to interpolate. Same dimension than fov_lat and fov_lat
        method : str {'linear', 'nearest'}, optional
            Interpolation method
        kwargs : dict
            option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        array : `~astropy.units.Quantity`
            Interpolated values, axis order is the same as for the NDData array
        """
        values = self.data.evaluate(
            fov_lon=fov_lon,
            fov_lat=fov_lat,
            energy=energy_reco,
            method=method,
            **kwargs,
        )
        return values

    def evaluate_integrate(
        self, fov_lon, fov_lat, energy_reco, method="linear", **kwargs
    ):
        """Integrate in a given energy band.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame.
        energy_reco: `~astropy.units.Quantity`
            Reconstructed energy edges.
        method : {'linear', 'nearest'}, optional
            Interpolation method

        Returns
        -------
        array : `~astropy.units.Quantity`
            Returns 2D array with axes offset
        """
        data = self.evaluate(fov_lon, fov_lat, energy_reco, method=method)
        return trapz_loglog(data, energy_reco, axis=0)

    def to_2d(self):
        """Convert to `Background2D`.

        This takes the values at Y = 0 and X >= 0.
        """
        # TODO: this is incorrect as it misses the Jacobian?
        idx_lon = self.data.axes["fov_lon"].coord_to_idx(0 * u.deg)[0]
        idx_lat = self.data.axes["fov_lat"].coord_to_idx(0 * u.deg)[0]
        data = self.data.data[:, idx_lon:, idx_lat].copy()

        offset = self.data.axes["fov_lon"].edges[idx_lon:]

        offset_axis = MapAxis.from_edges(offset, name="offset")
        return Background2D(
            energy_axis=self.data.axes["energy"], offset_axis=offset_axis, data=data,
        )

    def peek(self, figsize=(10, 8)):
        return self.to_2d().peek(figsize)
Exemple #7
0
class Background2D:
    """Background 2D.

    Data format specification: :ref:`gadf:bkg_2d`

    Parameters
    ----------
    energy_axis : `MapAxis`
        Energy axis
    offset_axis : `MapAxis`
        FOV coordinate offset-axis
    data : `~astropy.units.Quantity`
        Background rate (usually: ``s^-1 MeV^-1 sr^-1``)
    """

    tag = "bkg_2d"
    default_interp_kwargs = dict(bounds_error=False, fill_value=None)
    """Default Interpolation kwargs for `~gammapy.utils.nddata.NDDataArray`. Extrapolate."""

    def __init__(
        self, energy_axis, offset_axis, data, meta=None, interp_kwargs=None,
    ):
        if interp_kwargs is None:
            interp_kwargs = self.default_interp_kwargs

        assert offset_axis.name == "offset"

        self.data = NDDataArray(
            axes=[energy_axis, offset_axis], data=data, interp_kwargs=interp_kwargs
        )
        self.meta = meta or {}

    def __str__(self):
        ss = self.__class__.__name__
        ss += f"\n{self.data}"
        return ss

    @classmethod
    def from_table(cls, table):
        """Read from `~astropy.table.Table`."""
        # Spec says key should be "BKG", but there are files around
        # (e.g. CTA 1DC) that use "BGD". For now we support both

        if "BKG" in table.colnames:
            bkg_name = "BKG"
        elif "BGD" in table.colnames:
            bkg_name = "BGD"
        else:
            raise ValueError('Invalid column names. Need "BKG" or "BGD".')

        data_unit = table[bkg_name].unit
        if data_unit is not None:
            data_unit = u.Unit(data_unit, parse_strict="silent")
        if isinstance(data_unit, u.UnrecognizedUnit) or (data_unit is None):
            data_unit = u.Unit("s-1 MeV-1 sr-1")
            log.warning(
                "Invalid unit found in background table! Assuming (s-1 MeV-1 sr-1)"
            )

        energy_axis = MapAxis.from_table(
            table, column_prefix="ENERG", format="gadf-dl3"
        )
        offset_axis = MapAxis.from_table(
            table, column_prefix="THETA", format="gadf-dl3"
        )

        # TODO: The present HESS and CTA backgroundfits files
        # have a reverse order (theta, E) than recommened in GADF(E, theta)
        # For now, we suport both.

        data = table[bkg_name].data[0].T * data_unit
        shape = (energy_axis.nbin, offset_axis.nbin)

        if shape == shape[::-1]:
            log.error("Ambiguous axes order in Background fits files!")

        if np.shape(data) != shape:
            log.debug("Transposing background table on read")
            data = data.transpose()

        return cls(
            energy_axis=energy_axis,
            offset_axis=offset_axis,
            data=data,
            meta=table.meta,
        )

    @classmethod
    def from_hdulist(cls, hdulist, hdu="BACKGROUND"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="BACKGROUND"):
        """Read from file."""
        with fits.open(str(make_path(filename)), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu=hdu)

    def to_table(self):
        """Convert to `~astropy.table.Table`."""
        table = self.data.axes.to_table(format="gadf-dl3")
        table.meta = self.meta.copy()
        table["BKG"] = self.data.data.T[np.newaxis]
        return table

    def to_table_hdu(self, name="BACKGROUND"):
        """Convert to `~astropy.io.fits.BinTableHDU`."""
        return fits.BinTableHDU(self.to_table(), name=name)

    def evaluate(self, fov_lon, fov_lat, energy_reco, method="linear", **kwargs):
        """Evaluate at a given FOV position and energy.

        The fov_lon, fov_lat, energy_reco has to have the same shape
        since this is a set of points on which you want to evaluate.

        To have the same API than background 3D for the
        background evaluation, the offset is ``fov_altaz_lon``.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame, same shape than energy_reco
        energy_reco : `~astropy.units.Quantity`
            Reconstructed energy, same dimension than fov_lat and fov_lat
        method : str {'linear', 'nearest'}, optional
            Interpolation method
        kwargs : dict
            option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        array : `~astropy.units.Quantity`
            Interpolated values, axis order is the same as for the NDData array
        """
        offset = np.sqrt(fov_lon ** 2 + fov_lat ** 2)
        return self.data.evaluate(
            offset=offset, energy=energy_reco, method=method, **kwargs
        )

    def evaluate_integrate(self, fov_lon, fov_lat, energy_reco, method="linear"):
        """Evaluate at given FOV position and energy, by integrating over the energy range.

        Parameters
        ----------
        fov_lon, fov_lat : `~astropy.coordinates.Angle`
            FOV coordinates expecting in AltAz frame.
        energy_reco: `~astropy.units.Quantity`
            Reconstructed energy edges.
        method : {'linear', 'nearest'}, optional
            Interpolation method

        Returns
        -------
        array : `~astropy.units.Quantity`
            Returns 2D array with axes offset
        """
        data = self.evaluate(fov_lon, fov_lat, energy_reco, method=method)
        return trapz_loglog(data, energy_reco, axis=0)

    def to_3d(self):
        """Convert to `Background3D`.

        Fill in a radially symmetric way.
        """
        raise NotImplementedError

    def plot(self, ax=None, add_cbar=True, **kwargs):
        """Plot energy offset dependence of the background model.
        """
        import matplotlib.pyplot as plt
        from matplotlib.colors import LogNorm

        ax = plt.gca() if ax is None else ax

        x = self.data.axes["energy"].edges.to_value("TeV")
        y = self.data.axes["offset"].edges.to_value("deg")
        z = self.data.data.T.value

        kwargs.setdefault("cmap", "GnBu")
        kwargs.setdefault("edgecolors", "face")

        caxes = ax.pcolormesh(x, y, z, norm=LogNorm(), **kwargs)
        ax.set_xscale("log")
        ax.set_ylabel(f"Offset (deg)")
        ax.set_xlabel(f"Energy (TeV)")

        xmin, xmax = x.min(), x.max()
        ax.set_xlim(xmin, xmax)

        if add_cbar:
            label = f"Background rate ({self.data.data.unit})"
            ax.figure.colorbar(caxes, ax=ax, label=label)

    def plot_offset_dependence(self, ax=None, offset=None, energy=None, **kwargs):
        """Plot background rate versus offset for a given energy.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`
            Offset axis
        energy : `~astropy.units.Quantity`
            Energy

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        if energy is None:
            energy_axis = self.data.axes["energy"]
            e_min, e_max = np.log10(energy_axis.center.value[[0, -1]])
            energy = np.logspace(e_min, e_max, 4) * energy_axis.unit

        if offset is None:
            offset = self.data.axes["offset"].center

        for ee in energy:
            bkg = self.data.evaluate(offset=offset, energy=ee)
            if np.isnan(bkg).all():
                continue
            label = f"energy = {ee:.1f}"
            ax.plot(offset, bkg.value, label=label, **kwargs)

        ax.set_xlabel(f"Offset ({self.data.axes['offset'].unit})")
        ax.set_ylabel(f"Background rate ({self.data.data.unit})")
        ax.set_yscale("log")
        ax.legend(loc="upper right")
        return ax

    def plot_energy_dependence(self, ax=None, offset=None, energy=None, **kwargs):
        """Plot background rate versus energy for a given offset.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`
            Offset
        energy : `~astropy.units.Quantity`
            Energy axis
        kwargs : dict
            Forwarded tp plt.plot()

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        if offset is None:
            offset_axis = self.data.axes["offset"]
            off_min, off_max = offset_axis.center.value[[0, -1]]
            offset = np.linspace(off_min, off_max, 4) * offset_axis.unit

        if energy is None:
            energy = self.data.axes["energy"].center

        for off in offset:
            bkg = self.data.evaluate(offset=off, energy=energy)
            kwargs.setdefault("label", f"offset = {off:.1f}")
            ax.plot(energy, bkg.value, **kwargs)

        ax.set_xscale("log")
        ax.set_yscale("log")
        ax.set_xlabel(f"Energy [{energy.unit}]")
        ax.set_ylabel(f"Background rate ({self.data.data.unit})")
        ax.set_xlim(min(energy.value), max(energy.value))
        ax.legend(loc="best")

        return ax

    def plot_spectrum(self, ax=None, **kwargs):
        """Plot angle integrated background rate versus energy.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        kwargs : dict
            Forwarded tp plt.plot()

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax
        offset = self.data.axes["offset"].edges
        energy = self.data.axes["energy"].center

        bkg = []
        for ee in energy:
            data = self.data.evaluate(offset=offset, energy=ee)
            val = np.nansum(trapz_loglog(data, offset, axis=0))
            bkg.append(val.value)

        ax.plot(energy, bkg, label="integrated spectrum", **kwargs)

        unit = self.data.data.unit * offset.unit * offset.unit

        ax.set_xscale("log")
        ax.set_yscale("log")
        ax.set_xlabel(f"Energy [{energy.unit}]")
        ax.set_ylabel(f"Background rate ({unit})")
        ax.set_xlim(min(energy.value), max(energy.value))
        ax.legend(loc="best")

        return ax

    def peek(self, figsize=(10, 8)):
        """Quick-look summary plots."""
        import matplotlib.pyplot as plt

        fig, axes = plt.subplots(nrows=2, ncols=2, figsize=figsize)
        self.plot(ax=axes[1][1])
        self.plot_offset_dependence(ax=axes[0][0])
        self.plot_energy_dependence(ax=axes[1][0])
        self.plot_spectrum(ax=axes[0][1])
        plt.tight_layout()
class EnergyDispersion2D:
    """Offset-dependent energy dispersion matrix.

    Data format specification: :ref:`gadf:edisp_2d`

    Parameters
    ----------
    energy_axis_true : `MapAxis`
        True energy axis
    migra_axis : `MapAxis`
        Energy migration axis
    offset_axis : `MapAxis`
        Field of view offset axis
    data : `~numpy.ndarray`
        Energy dispersion probability density

    Examples
    --------
    Read energy dispersion IRF from disk:

    >>> from gammapy.maps import MapAxis
    >>> from gammapy.irf import EnergyDispersion2D
    >>> filename = '$GAMMAPY_DATA/hess-dl3-dr1/data/hess_dl3_dr1_obs_id_020136.fits.gz'
    >>> edisp2d = EnergyDispersion2D.read(filename, hdu="EDISP")

    Create energy dispersion matrix (`~gammapy.irf.EnergyDispersion`)
    for a given field of view offset and energy binning:

    >>> energy = MapAxis.from_bounds(0.1, 20, nbin=60, unit="TeV", interp="log").edges
    >>> edisp = edisp2d.to_edisp_kernel(offset='1.2 deg', e_reco=energy, energy_true=energy)

    See Also
    --------
    EnergyDispersion
    """

    tag = "edisp_2d"
    default_interp_kwargs = dict(bounds_error=False, fill_value=None)
    """Default Interpolation kwargs for `~gammapy.utils.nddata.NDDataArray`. Extrapolate."""
    def __init__(
        self,
        energy_axis_true,
        migra_axis,
        offset_axis,
        data,
        interp_kwargs=None,
        meta=None,
    ):
        if interp_kwargs is None:
            interp_kwargs = self.default_interp_kwargs

        axes = [energy_axis_true, migra_axis, offset_axis]

        self.data = NDDataArray(axes=axes,
                                data=data,
                                interp_kwargs=interp_kwargs)
        self.meta = meta or {}

    def __str__(self):
        ss = self.__class__.__name__
        ss += f"\n{self.data}"
        return ss

    @classmethod
    def from_gauss(cls,
                   energy_true,
                   migra,
                   bias,
                   sigma,
                   offset,
                   pdf_threshold=1e-6):
        """Create Gaussian energy dispersion matrix (`EnergyDispersion2D`).

        The output matrix will be Gaussian in (energy_true / energy).

        The ``bias`` and ``sigma`` should be either floats or arrays of same dimension than
        ``energy_true``. ``bias`` refers to the mean value of the ``migra``
        distribution minus one, i.e. ``bias=0`` means no bias.

        Note that, the output matrix is flat in offset.

        Parameters
        ----------
        energy_true : `~astropy.units.Quantity`
            Bin edges of true energy axis
        migra : `~astropy.units.Quantity`
            Bin edges of migra axis
        bias : float or `~numpy.ndarray`
            Center of Gaussian energy dispersion, bias
        sigma : float or `~numpy.ndarray`
            RMS width of Gaussian energy dispersion, resolution
        offset : `~astropy.units.Quantity`
            Bin edges of offset
        pdf_threshold : float, optional
            Zero suppression threshold
        """
        energy_true = Quantity(energy_true)
        # erf does not work with Quantities
        energy_axis_true = MapAxis.from_energy_edges(energy_true,
                                                     interp="log",
                                                     name="energy_true")

        true2d, migra2d = np.meshgrid(energy_axis_true.center, migra)

        migra2d_lo = migra2d[:-1, :]
        migra2d_hi = migra2d[1:, :]

        # Analytical formula for integral of Gaussian
        s = np.sqrt(2) * sigma
        t1 = (migra2d_hi - 1 - bias) / s
        t2 = (migra2d_lo - 1 - bias) / s
        pdf = (scipy.special.erf(t1) - scipy.special.erf(t2)) / 2

        data = pdf.T[:, :, np.newaxis] * np.ones(len(offset) - 1)

        data[data < pdf_threshold] = 0

        offset_axis = MapAxis.from_edges(offset, name="offset")
        migra_axis = MapAxis.from_edges(migra, name="migra")
        return cls(
            energy_axis_true=energy_axis_true,
            migra_axis=migra_axis,
            offset_axis=offset_axis,
            data=data,
        )

    @classmethod
    def from_table(cls, table):
        """Create from `~astropy.table.Table`."""
        # TODO: move this to MapAxis.from_table()

        if "ENERG_LO" in table.colnames:
            energy_axis_true = MapAxis.from_table(table,
                                                  column_prefix="ENERG",
                                                  format="gadf-dl3")
        elif "ETRUE_LO" in table.colnames:
            energy_axis_true = MapAxis.from_table(table,
                                                  column_prefix="ETRUE",
                                                  format="gadf-dl3")
        else:
            raise ValueError(
                'Invalid column names. Need "ENERG_LO/ENERG_HI" or "ETRUE_LO/ETRUE_HI"'
            )

        offset_axis = MapAxis.from_table(table,
                                         column_prefix="THETA",
                                         format="gadf-dl3")
        migra_axis = MapAxis.from_table(table,
                                        column_prefix="MIGRA",
                                        format="gadf-dl3")

        matrix = table["MATRIX"].quantity[0].transpose()

        return cls(
            energy_axis_true=energy_axis_true,
            offset_axis=offset_axis,
            migra_axis=migra_axis,
            data=matrix,
        )

    @classmethod
    def from_hdulist(cls, hdulist, hdu="edisp_2d"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="edisp_2d"):
        """Read from FITS file.

        Parameters
        ----------
        filename : str
            File name
        """
        with fits.open(str(make_path(filename)), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu)

    def to_edisp_kernel(self, offset, energy_true=None, energy=None):
        """Detector response R(Delta E_reco, Delta E_true)

        Probability to reconstruct an energy in a given true energy band
        in a given reconstructed energy band

        Parameters
        ----------
        offset : `~astropy.coordinates.Angle`
            Offset
        energy_true : `~astropy.units.Quantity`, None
            True energy axis
        energy : `~astropy.units.Quantity`
            Reconstructed energy axis

        Returns
        -------
        edisp : `~gammapy.irf.EDispKernel`
            Energy dispersion matrix
        """
        offset = Angle(offset)

        # TODO: expect directly MapAxis here?
        if energy is None:
            energy_axis = self.data.axes["energy_true"].copy(name="energy")
        else:
            energy_axis = MapAxis.from_energy_edges(energy)

        if energy_true is None:
            energy_axis_true = self.data.axes["energy_true"]
        else:
            energy_axis_true = MapAxis.from_energy_edges(energy_true,
                                                         name="energy_true")

        data = []
        for value in energy_axis_true.center:
            vec = self.get_response(offset=offset,
                                    energy_true=value,
                                    energy=energy_axis.edges)
            data.append(vec)

        return EDispKernel(
            energy_axis=energy_axis,
            energy_axis_true=energy_axis_true,
            data=np.asarray(data),
        )

    def get_response(self, offset, energy_true, energy=None):
        """Detector response R(Delta E_reco, E_true)

        Probability to reconstruct a given true energy in a given reconstructed
        energy band. In each reco bin, you integrate with a riemann sum over
        the default migra bin of your analysis.

        Parameters
        ----------
        energy_true : `~astropy.units.Quantity`
            True energy
        energy : `~astropy.units.Quantity`, None
            Reconstructed energy axis
        offset : `~astropy.coordinates.Angle`
            Offset

        Returns
        -------
        rv : `~numpy.ndarray`
            Redistribution vector
        """
        energy_true = Quantity(energy_true)

        migra_axis = self.data.axes["migra"]

        if energy is None:
            # Default: energy nodes = migra nodes * energy_true nodes
            energy = migra_axis.edges * energy_true
        else:
            # Translate given energy binning to migra at bin center
            energy = Quantity(energy)

        # migration value of energy bounds
        migra = energy / energy_true

        values = self.data.evaluate(offset=offset,
                                    energy_true=energy_true,
                                    migra=migra_axis.center)

        cumsum = np.insert(values, 0, 0).cumsum()

        with np.errstate(invalid="ignore"):
            cumsum = np.nan_to_num(cumsum / cumsum[-1])

        f = interp1d(
            migra_axis.edges.value,
            cumsum,
            kind="linear",
            bounds_error=False,
            fill_value=(0, 1),
        )

        # We compute the difference between 2 successive bounds in energy
        # to get integral over reco energy bin
        integral = np.diff(np.clip(f(migra), a_min=0, a_max=1))

        return integral

    def plot_migration(self,
                       ax=None,
                       offset=None,
                       energy_true=None,
                       migra=None,
                       **kwargs):
        """Plot energy dispersion for given offset and true energy.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`, optional
            Offset
        energy_true : `~astropy.units.Quantity`, optional
            True energy
        migra : `~numpy.ndarray`, optional
            Migration nodes

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        if offset is None:
            offset = Angle([1], "deg")
        else:
            offset = np.atleast_1d(Angle(offset))

        if energy_true is None:
            energy_true = Quantity([0.1, 1, 10], "TeV")
        else:
            energy_true = np.atleast_1d(Quantity(energy_true))

        migra = self.data.axes["migra"].center if migra is None else migra

        for ener in energy_true:
            for off in offset:
                disp = self.data.evaluate(offset=off,
                                          energy_true=ener,
                                          migra=migra)
                label = f"offset = {off:.1f}\nenergy = {ener:.1f}"
                ax.plot(migra, disp, label=label, **kwargs)

        ax.set_xlabel(r"$E_\mathrm{{Reco}} / E_\mathrm{{True}}$")
        ax.set_ylabel("Probability density")
        ax.legend(loc="upper left")

        return ax

    def plot_bias(self, ax=None, offset=None, add_cbar=False, **kwargs):
        """Plot migration as a function of true energy for a given offset.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`, optional
            Offset
        add_cbar : bool
            Add a colorbar to the plot.
        kwargs : dict
            Keyword arguments passed to `~matplotlib.pyplot.pcolormesh`.

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        from matplotlib.colors import PowerNorm
        import matplotlib.pyplot as plt

        kwargs.setdefault("cmap", "GnBu")
        kwargs.setdefault("norm", PowerNorm(gamma=0.5))

        ax = plt.gca() if ax is None else ax

        if offset is None:
            offset = Angle(1, "deg")

        energy_true = self.data.axes["energy_true"]
        migra = self.data.axes["migra"]

        x = energy_true.edges.value
        y = migra.edges.value

        z = self.data.evaluate(
            offset=offset,
            energy_true=energy_true.center.reshape(1, -1, 1),
            migra=migra.center.reshape(1, 1, -1),
        ).value[0]

        caxes = ax.pcolormesh(x, y, z.T, **kwargs)

        if add_cbar:
            label = "Probability density (A.U.)"
            ax.figure.colorbar(caxes, ax=ax, label=label)

        ax.set_xlabel(fr"$E_\mathrm{{True}}$ [{energy_true.unit}]")
        ax.set_ylabel(r"$E_\mathrm{{Reco}} / E_\mathrm{{True}}$")
        ax.set_xlim(x.min(), x.max())
        ax.set_ylim(y.min(), y.max())
        ax.set_xscale("log")
        return ax

    def peek(self, figsize=(15, 5)):
        """Quick-look summary plots.

        Parameters
        ----------
        figsize : (float, float)
            Size of the resulting plot
        """
        import matplotlib.pyplot as plt

        fig, axes = plt.subplots(nrows=1, ncols=3, figsize=figsize)
        self.plot_bias(ax=axes[0])
        self.plot_migration(ax=axes[1])
        edisp = self.to_edisp_kernel(offset="1 deg")
        edisp.plot_matrix(ax=axes[2])

        plt.tight_layout()

    def to_table(self):
        """Convert to `~astropy.table.Table`."""
        table = self.data.axes.to_table(format="gadf-dl3")
        table.meta = self.meta.copy()
        table["MATRIX"] = self.data.data.T[np.newaxis]
        return table

    def to_table_hdu(self, name="ENERGY DISPERSION"):
        """Convert to `~astropy.io.fits.BinTable`."""
        return fits.BinTableHDU(self.to_table(), name=name)
Exemple #9
0
# In[ ]:


energies = Energy.equal_log_spacing(10, 100, 10, unit=u.TeV)
x_axis = DataAxis(energies, name="energy", interpolation_mode="log")
data = np.arange(20, 0, -2) / u.cm ** 2 / u.s
nddata = NDDataArray(axes=[x_axis], data=data)
print(nddata)
print(nddata.axis("energy"))


# In[ ]:


eval_energies = np.linspace(2, 6, 20) * 1e4 * u.GeV
eval_exposure = nddata.evaluate(energy=eval_energies, method="linear")

plt.plot(
    nddata.axis("energy").nodes.value,
    nddata.data.value,
    ".",
    label="Interpolation nodes",
)
print(nddata.axis("energy").nodes)
plt.plot(
    eval_energies.to("TeV").value,
    eval_exposure,
    "--",
    label="Interpolated values",
)
plt.xlabel("{} [{}]".format(nddata.axes[0].name, nddata.axes[0].unit))
Exemple #10
0
class EnergyDispersion2D:
    """Offset-dependent energy dispersion matrix.

    Data format specification: :ref:`gadf:edisp_2d`

    Parameters
    ----------
    e_true_lo, e_true_hi : `~astropy.units.Quantity`
        True energy axis binning
    migra_lo, migra_hi : `~numpy.ndarray`
        Energy migration axis binning
    offset_lo, offset_hi : `~astropy.coordinates.Angle`
        Field of view offset axis binning
    data : `~numpy.ndarray`
        Energy dispersion probability density

    Examples
    --------
    Read energy dispersion IRF from disk:

    >>> from gammapy.maps import MapAxis
    >>> from gammapy.irf import EnergyDispersion2D
    >>> filename = '$GAMMAPY_DATA/hess-dl3-dr1/data/hess_dl3_dr1_obs_id_020136.fits.gz'
    >>> edisp2d = EnergyDispersion2D.read(filename, hdu="EDISP")

    Create energy dispersion matrix (`~gammapy.irf.EnergyDispersion`)
    for a given field of view offset and energy binning:

    >>> energy = MapAxis.from_bounds(0.1, 20, nbin=60, unit="TeV", interp="log").edges
    >>> edisp = edisp2d.to_energy_dispersion(offset='1.2 deg', e_reco=energy, e_true=energy)

    See Also
    --------
    EnergyDispersion
    """

    default_interp_kwargs = dict(bounds_error=False, fill_value=None)
    """Default Interpolation kwargs for `~gammapy.utils.nddata.NDDataArray`. Extrapolate."""
    def __init__(
        self,
        e_true_lo,
        e_true_hi,
        migra_lo,
        migra_hi,
        offset_lo,
        offset_hi,
        data,
        interp_kwargs=None,
        meta=None,
    ):
        if interp_kwargs is None:
            interp_kwargs = self.default_interp_kwargs

        e_true_edges = edges_from_lo_hi(e_true_lo, e_true_hi)
        e_true_axis = MapAxis.from_edges(e_true_edges,
                                         interp="log",
                                         name="e_true")

        migra_edges = edges_from_lo_hi(migra_lo, migra_hi)
        migra_axis = MapAxis.from_edges(migra_edges,
                                        interp="log",
                                        name="migra",
                                        unit="")

        # TODO: for some reason the H.E.S.S. DL3 files contain the same values for offset_hi and offset_lo
        if np.allclose(offset_lo.to_value("deg"), offset_hi.to_value("deg")):
            offset_axis = MapAxis.from_nodes(offset_lo,
                                             interp="lin",
                                             name="offset")
        else:
            offset_edges = edges_from_lo_hi(offset_lo, offset_hi)
            offset_axis = MapAxis.from_edges(offset_edges,
                                             interp="lin",
                                             name="offset")

        axes = [e_true_axis, migra_axis, offset_axis]

        self.data = NDDataArray(axes=axes,
                                data=data,
                                interp_kwargs=interp_kwargs)
        self.meta = meta or {}

    def __str__(self):
        ss = self.__class__.__name__
        ss += f"\n{self.data}"
        return ss

    @classmethod
    def from_gauss(cls,
                   e_true,
                   migra,
                   bias,
                   sigma,
                   offset,
                   pdf_threshold=1e-6):
        """Create Gaussian energy dispersion matrix (`EnergyDispersion2D`).

        The output matrix will be Gaussian in (e_true / e_reco).

        The ``bias`` and ``sigma`` should be either floats or arrays of same dimension than
        ``e_true``. ``bias`` refers to the mean value of the ``migra``
        distribution minus one, i.e. ``bias=0`` means no bias.

        Note that, the output matrix is flat in offset.

        Parameters
        ----------
        e_true : `~astropy.units.Quantity`
            Bin edges of true energy axis
        migra : `~astropy.units.Quantity`
            Bin edges of migra axis
        bias : float or `~numpy.ndarray`
            Center of Gaussian energy dispersion, bias
        sigma : float or `~numpy.ndarray`
            RMS width of Gaussian energy dispersion, resolution
        offset : `~astropy.units.Quantity`
            Bin edges of offset
        pdf_threshold : float, optional
            Zero suppression threshold
        """
        e_true = Quantity(e_true)
        # erf does not work with Quantities
        true = MapAxis.from_edges(e_true, interp="log").center.to_value("TeV")

        true2d, migra2d = np.meshgrid(true, migra)

        migra2d_lo = migra2d[:-1, :]
        migra2d_hi = migra2d[1:, :]

        # Analytical formula for integral of Gaussian
        s = np.sqrt(2) * sigma
        t1 = (migra2d_hi - 1 - bias) / s
        t2 = (migra2d_lo - 1 - bias) / s
        pdf = (scipy.special.erf(t1) - scipy.special.erf(t2)) / 2

        pdf_array = pdf.T[:, :, np.newaxis] * np.ones(len(offset) - 1)

        pdf_array = np.where(pdf_array > pdf_threshold, pdf_array, 0)

        return cls(
            e_true[:-1],
            e_true[1:],
            migra[:-1],
            migra[1:],
            offset[:-1],
            offset[1:],
            pdf_array,
        )

    @classmethod
    def from_table(cls, table):
        """Create from `~astropy.table.Table`."""
        if "ENERG_LO" in table.colnames:
            e_lo = table["ENERG_LO"].quantity[0]
            e_hi = table["ENERG_HI"].quantity[0]
        elif "ETRUE_LO" in table.colnames:
            e_lo = table["ETRUE_LO"].quantity[0]
            e_hi = table["ETRUE_HI"].quantity[0]
        else:
            raise ValueError(
                'Invalid column names. Need "ENERG_LO/ENERG_HI" or "ETRUE_LO/ETRUE_HI"'
            )
        o_lo = table["THETA_LO"].quantity[0]
        o_hi = table["THETA_HI"].quantity[0]
        m_lo = table["MIGRA_LO"].quantity[0]
        m_hi = table["MIGRA_HI"].quantity[0]

        # TODO Why does this need to be transposed?
        matrix = table["MATRIX"].quantity[0].transpose()

        return cls(
            e_true_lo=e_lo,
            e_true_hi=e_hi,
            offset_lo=o_lo,
            offset_hi=o_hi,
            migra_lo=m_lo,
            migra_hi=m_hi,
            data=matrix,
        )

    @classmethod
    def from_hdulist(cls, hdulist, hdu="edisp_2d"):
        """Create from `~astropy.io.fits.HDUList`."""
        return cls.from_table(Table.read(hdulist[hdu]))

    @classmethod
    def read(cls, filename, hdu="edisp_2d"):
        """Read from FITS file.

        Parameters
        ----------
        filename : str
            File name
        """
        with fits.open(make_path(filename), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu)

    def to_energy_dispersion(self, offset, e_true=None, e_reco=None):
        """Detector response R(Delta E_reco, Delta E_true)

        Probability to reconstruct an energy in a given true energy band
        in a given reconstructed energy band

        Parameters
        ----------
        offset : `~astropy.coordinates.Angle`
            Offset
        e_true : `~astropy.units.Quantity`, None
            True energy axis
        e_reco : `~astropy.units.Quantity`
            Reconstructed energy axis

        Returns
        -------
        edisp : `~gammapy.irf.EnergyDispersion`
            Energy dispersion matrix
        """
        offset = Angle(offset)
        e_true = self.data.axis("e_true").edges if e_true is None else e_true
        e_reco = self.data.axis("e_true").edges if e_reco is None else e_reco

        data = []
        for energy in MapAxis.from_edges(e_true, interp="log").center:
            vec = self.get_response(offset=offset,
                                    e_true=energy,
                                    e_reco=e_reco)
            data.append(vec)

        data = np.asarray(data)
        e_lo, e_hi = e_true[:-1], e_true[1:]
        ereco_lo, ereco_hi = (e_reco[:-1], e_reco[1:])

        return EnergyDispersion(
            e_true_lo=e_lo,
            e_true_hi=e_hi,
            e_reco_lo=ereco_lo,
            e_reco_hi=ereco_hi,
            data=data,
        )

    def get_response(self, offset, e_true, e_reco=None, migra_step=5e-3):
        """Detector response R(Delta E_reco, E_true)

        Probability to reconstruct a given true energy in a given reconstructed
        energy band. In each reco bin, you integrate with a riemann sum over
        the default migra bin of your analysis.

        Parameters
        ----------
        e_true : `~astropy.units.Quantity`
            True energy
        e_reco : `~astropy.units.Quantity`, None
            Reconstructed energy axis
        offset : `~astropy.coordinates.Angle`
            Offset
        migra_step : float
            Integration step in migration

        Returns
        -------
        rv : `~numpy.ndarray`
            Redistribution vector
        """
        e_true = Quantity(e_true)

        if e_reco is None:
            # Default: e_reco nodes = migra nodes * e_true nodes
            e_reco = self.data.axis("migra").edges * e_true
        else:
            # Translate given e_reco binning to migra at bin center
            e_reco = Quantity(e_reco)

        # migration value of e_reco bounds
        migra_e_reco = e_reco / e_true

        # Define a vector of migration with mig_step step
        mrec_min = self.data.axis("migra").edges[0]
        mrec_max = self.data.axis("migra").edges[-1]
        mig_array = np.arange(mrec_min, mrec_max, migra_step)

        # Compute energy dispersion probability dP/dm for each element of migration array
        vals = self.data.evaluate(offset=offset,
                                  e_true=e_true,
                                  migra=mig_array)

        # Compute normalized cumulative sum to prepare integration
        with np.errstate(invalid="ignore"):
            tmp = np.nan_to_num(np.cumsum(vals) / np.sum(vals))

        # Determine positions (bin indices) of e_reco bounds in migration array
        pos_mig = np.digitize(migra_e_reco, mig_array) - 1
        # We ensure that no negative values are found
        pos_mig = np.maximum(pos_mig, 0)

        # We compute the difference between 2 successive bounds in e_reco
        # to get integral over reco energy bin
        integral = np.diff(tmp[pos_mig])

        return integral

    def plot_migration(self,
                       ax=None,
                       offset=None,
                       e_true=None,
                       migra=None,
                       **kwargs):
        """Plot energy dispersion for given offset and true energy.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`, optional
            Offset
        e_true : `~astropy.units.Quantity`, optional
            True energy
        migra : `~numpy.ndarray`, optional
            Migration nodes

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        if offset is None:
            offset = Angle([1], "deg")
        else:
            offset = np.atleast_1d(Angle(offset))

        if e_true is None:
            e_true = Quantity([0.1, 1, 10], "TeV")
        else:
            e_true = np.atleast_1d(Quantity(e_true))

        migra = self.data.axis("migra").center if migra is None else migra

        for ener in e_true:
            for off in offset:
                disp = self.data.evaluate(offset=off, e_true=ener, migra=migra)
                label = f"offset = {off:.1f}\nenergy = {ener:.1f}"
                ax.plot(migra, disp, label=label, **kwargs)

        ax.set_xlabel(r"$E_\mathrm{{Reco}} / E_\mathrm{{True}}$")
        ax.set_ylabel("Probability density")
        ax.legend(loc="upper left")

        return ax

    def plot_bias(self, ax=None, offset=None, add_cbar=False, **kwargs):
        """Plot migration as a function of true energy for a given offset.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        offset : `~astropy.coordinates.Angle`, optional
            Offset
        add_cbar : bool
            Add a colorbar to the plot.
        kwargs : dict
            Keyword arguments passed to `~matplotlib.pyplot.pcolormesh`.

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        from matplotlib.colors import PowerNorm
        import matplotlib.pyplot as plt

        kwargs.setdefault("cmap", "GnBu")
        kwargs.setdefault("norm", PowerNorm(gamma=0.5))

        ax = plt.gca() if ax is None else ax

        if offset is None:
            offset = Angle(1, "deg")

        e_true = self.data.axis("e_true").edges
        migra = self.data.axis("migra").edges

        x = e_true.value
        y = migra.value
        z = self.data.evaluate(
            offset=offset,
            e_true=e_true.reshape(1, -1, 1),
            migra=migra.reshape(1, 1, -1),
        ).value[0]

        caxes = ax.pcolormesh(x, y, z.T, **kwargs)

        if add_cbar:
            label = "Probability density (A.U.)"
            ax.figure.colorbar(caxes, ax=ax, label=label)

        ax.set_xlabel(fr"$E_\mathrm{{True}}$ [{e_true.unit}]")
        ax.set_ylabel(r"$E_\mathrm{{Reco}} / E_\mathrm{{True}}$")
        ax.set_xlim(x.min(), x.max())
        ax.set_ylim(y.min(), y.max())
        ax.set_xscale("log")
        return ax

    def peek(self, figsize=(15, 5)):
        """Quick-look summary plots.

        Parameters
        ----------
        figsize : (float, float)
            Size of the resulting plot
        """
        import matplotlib.pyplot as plt

        fig, axes = plt.subplots(nrows=1, ncols=3, figsize=figsize)
        self.plot_bias(ax=axes[0])
        self.plot_migration(ax=axes[1])
        edisp = self.to_energy_dispersion(offset="1 deg")
        edisp.plot_matrix(ax=axes[2])

        plt.tight_layout()

    def to_table(self):
        """Convert to `~astropy.table.Table`."""
        meta = self.meta.copy()

        energy = self.data.axis("e_true").edges
        migra = self.data.axis("migra").edges
        theta = self.data.axis("offset").edges

        table = Table(meta=meta)
        table["ENERG_LO"] = energy[:-1][np.newaxis]
        table["ENERG_HI"] = energy[1:][np.newaxis]
        table["MIGRA_LO"] = migra[:-1][np.newaxis]
        table["MIGRA_HI"] = migra[1:][np.newaxis]
        table["THETA_LO"] = theta[:-1][np.newaxis]
        table["THETA_HI"] = theta[1:][np.newaxis]
        table["MATRIX"] = self.data.data.T[np.newaxis]
        return table

    def to_fits(self, name="ENERGY DISPERSION"):
        """Convert to `~astropy.io.fits.BinTable`."""
        return fits.BinTableHDU(self.to_table(), name=name)
Exemple #11
0
class TablePSF:
    """Radially-symmetric table PSF.

    Parameters
    ----------
    rad_axis : `~astropy.units.Quantity` with angle units
        Offset wrt source position
    data : `~astropy.units.Quantity` with sr^-1 units
        PSF value array
    interp_kwargs : dict
        Keyword arguments passed to `ScaledRegularGridInterpolator`
    """
    def __init__(self, rad_axis, data, interp_kwargs=None):
        interp_kwargs = interp_kwargs or {}

        rad_axis.assert_name("rad")

        self.data = NDDataArray(axes=[rad_axis],
                                data=u.Quantity(data).to("sr^-1"),
                                interp_kwargs=interp_kwargs)

    @property
    def rad_axis(self):
        return self.data.axes["rad"]

    @classmethod
    def from_shape(cls, shape, width, rad):
        """Make TablePSF objects with commonly used shapes.

        This function is mostly useful for examples and testing.

        Parameters
        ----------
        shape : {'disk', 'gauss'}
            PSF shape.
        width : `~astropy.units.Quantity` with angle units
            PSF width angle (radius for disk, sigma for Gauss).
        rad : `~astropy.units.Quantity` with angle units
            Offset angle

        Returns
        -------
        psf : `TablePSF`
            Table PSF

        Examples
        --------
        >>> import numpy as np
        >>> from astropy.coordinates import Angle
        >>> from gammapy.irf import TablePSF
        >>> rad = Angle(np.linspace(0, 0.7, 100), 'deg')
        >>> psf = TablePSF.from_shape(shape='gauss', width='0.2 deg', rad=rad)
        """
        width = Angle(width)
        rad = Angle(rad)

        if shape == "disk":
            amplitude = 1 / (np.pi * width.radian**2)
            data = np.where(rad < width, amplitude, 0)
        elif shape == "gauss":
            gauss2d_pdf = Gauss2DPDF(sigma=width.radian)
            data = gauss2d_pdf(rad.radian)
        else:
            raise ValueError(f"Invalid shape: {shape}")

        data = u.Quantity(data, "sr^-1")
        rad_axis = MapAxis.from_nodes(rad, name="rad")
        return cls(rad_axis=rad_axis, data=data)

    def info(self):
        """Print basic info."""
        ss = array_stats_str(self.rad_axis.center, "offset")
        ss += f"integral = {self.containment(self.rad_axis.edges[-1])}\n"

        for containment in [68, 80, 95]:
            radius = self.containment_radius(0.01 * containment)
            ss += f"containment radius {radius.deg} deg for {containment}%\n"

        return ss

    def evaluate(self, rad):
        r"""Evaluate PSF.

        The following PSF quantities are available:

        * 'dp_domega': PDF per 2-dim solid angle :math:`\Omega` in sr^-1

            .. math:: \frac{dP}{d\Omega}


        Parameters
        ----------
        rad : `~astropy.coordinates.Angle`
            Offset wrt source position

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        return self.data.evaluate(rad=rad)

    def containment(self, rad_max):
        """Compute PSF containment fraction.

        Parameters
        ----------
        rad_max : `~astropy.units.Quantity`
            Offset angle range

        Returns
        -------
        integral : float
            PSF integral
        """
        rad_max = np.atleast_1d(rad_max)
        return self.data._integrate_rad((rad_max, ))

    def containment_radius(self, fraction):
        """Containment radius.

        Parameters
        ----------
        fraction : array_like
            Containment fraction (range 0 .. 1)

        Returns
        -------
        rad : `~astropy.coordinates.Angle`
            Containment radius angle
        """
        # TODO: check whether starting
        rad_max = Angle(
            np.linspace(0 * u.deg, self.rad_axis.center[-1],
                        10 * self.rad_axis.nbin),
            "rad",
        )

        containment = self.containment(rad_max=rad_max)

        fraction = np.atleast_1d(fraction)

        fraction_idx = np.argmin(np.abs(containment - fraction[:, np.newaxis]),
                                 axis=1)
        return rad_max[fraction_idx].to("deg")

    def normalize(self):
        """Normalize PSF to unit integral.

        Computes the total PSF integral via the :math:`dP / dr` spline
        and then divides the :math:`dP / dr` array.
        """
        integral = self.containment(self.rad_axis.edges[-1])
        self.data /= integral

    def plot_psf_vs_rad(self, ax=None, **kwargs):
        """Plot PSF vs radius.

        Parameters
        ----------
        ax : ``

        kwargs : dict
            Keyword arguments passed to `matplotlib.pyplot.plot`
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        ax.plot(
            self.rad_axis.center.to_value("deg"),
            self.data.data.to_value("sr-1"),
            **kwargs,
        )
        ax.set_yscale("log")
        ax.set_xlabel("Radius (deg)")
        ax.set_ylabel("PSF (sr-1)")
Exemple #12
0
class BgRateTable(object):
    """Background rate table.

    The IRF format should be compliant with the one discussed
    at http://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/.
    Work will be done to fix this.

    Parameters
    -----------
    energy_lo, energy_hi : `~astropy.units.Quantity`, `~gammapy.utils.nddata.BinnedDataAxis`
        Bin edges of energy axis
    data : `~astropy.units.Quantity`
        Background rate
    """

    def __init__(self, energy_lo, energy_hi, data):
        axes = [
            BinnedDataAxis(energy_lo, energy_hi, interpolation_mode='log', name='energy'),
        ]
        self.data = NDDataArray(axes=axes, data=data)

    @property
    def energy(self):
        return self.data.axes[0]

    @classmethod
    def from_table(cls, table):
        """Background rate reader"""
        energy_lo = table['ENERG_LO'].quantity
        energy_hi = table['ENERG_HI'].quantity
        data = table['BGD'].quantity
        return cls(energy_lo=energy_lo, energy_hi=energy_hi, data=data)

    @classmethod
    def from_hdulist(cls, hdulist, hdu='BACKGROUND'):
        fits_table = hdulist[hdu]
        table = Table.read(fits_table)
        return cls.from_table(table)

    @classmethod
    def read(cls, filename, hdu='BACKGROUND'):
        filename = make_path(filename)
        with fits.open(str(filename), memmap=False) as hdulist:
            return cls.from_hdulist(hdulist, hdu=hdu)

    def plot(self, ax=None, energy=None, **kwargs):
        """Plot background rate.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`, optional
            Axis
        energy : `~astropy.units.Quantity`
            Energy nodes

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axis
        """
        import matplotlib.pyplot as plt
        ax = plt.gca() if ax is None else ax

        energy = energy or self.energy.nodes
        values = self.data.evaluate(energy=energy)
        xerr = (
            energy.value - self.energy.lo.value,
            self.energy.hi.value - energy.value,
        )
        ax.errorbar(energy.value, values.value, xerr=xerr, fmt='o', **kwargs)
        ax.set_xscale('log')
        ax.set_yscale('log')
        ax.set_xlabel('Energy [{}]'.format(self.energy.unit))
        ax.set_ylabel('Background rate [{}]'.format(self.data.data.unit))

        return ax