def test_mixed_axes(): label_axis = LabelMapAxis(labels=["label-1", "label-2", "label-3"], name="label") time_axis = TimeMapAxis( edges_min=[1, 10] * u.day, edges_max=[2, 13] * u.day, reference_time=Time("2020-03-19"), ) energy_axis = MapAxis.from_energy_bounds("1 TeV", "10 TeV", nbin=4) axes = MapAxes(axes=[energy_axis, time_axis, label_axis]) coords = axes.get_coord() assert coords["label"].shape == (1, 1, 3) assert coords["energy"].shape == (4, 1, 1) assert coords["time"].shape == (1, 2, 1) idx = axes.coord_to_idx(coords) assert_allclose(idx[0], np.arange(4).reshape((4, 1, 1))) assert_allclose(idx[1], np.arange(2).reshape((1, 2, 1))) assert_allclose(idx[2], np.arange(3).reshape((1, 1, 3))) hdu = axes.to_table_hdu(format="gadf") table = Table.read(hdu) assert table["LABEL"].dtype == np.dtype("<U7") assert len(table) == 24
def to_psf3d(self, rad=None): """Create a PSF3D from a parametric PSF. It will be defined on the same energy and offset values than the input psf. Parameters ---------- rad : `~astropy.units.Quantity` Rad values Returns ------- psf3d : `~gammapy.irf.PSF3D` PSF3D. """ from gammapy.irf import PSF3D from gammapy.datasets.map import RAD_AXIS_DEFAULT offset_axis = self.axes["offset"] energy_axis_true = self.axes["energy_true"] if rad is None: rad_axis = RAD_AXIS_DEFAULT.center else: rad_axis = MapAxis.from_edges(rad, name="rad") axes = MapAxes([energy_axis_true, offset_axis, rad_axis]) data = self.evaluate(**axes.get_coord()) return PSF3D(axes=axes, data=data.value, unit=data.unit, meta=self.meta.copy())
def to_energy_dependent_table_psf(self, offset, rad=None): """Convert to energy-dependent table PSF. Parameters ---------- offset : `~astropy.coordinates.Angle` Offset in the field of view. Default theta = 0 deg rad : `~astropy.coordinates.Angle` Offset from PSF center used for evaluating the PSF on a grid. Default offset = [0, 0.005, ..., 1.495, 1.5] deg. Returns ------- table_psf : `~gammapy.irf.EnergyDependentTablePSF` Energy-dependent PSF """ from gammapy.irf import EnergyDependentTablePSF from gammapy.datasets.map import RAD_AXIS_DEFAULT energy_axis_true = self.axes["energy_true"] if rad is None: rad_axis = RAD_AXIS_DEFAULT else: rad_axis = MapAxis.from_edges(rad, name="rad") axes = MapAxes([energy_axis_true, rad_axis]) data = self.evaluate(**axes.get_coord(), offset=offset) return EnergyDependentTablePSF(axes=axes, data=data.value, unit=data.unit)
def from_parametrization(cls, energy_axis_true=None, 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)} This method does not model the offset dependence of the effective area, but just assumes that it is constant. Parameters ---------- energy_axis_true : `MapAxis` Energy binning, analytic function is evaluated at log centers instrument : {'HESS', 'HESS2', 'CTA'} Instrument name Returns ------- aeff : `EffectiveAreaTable2D` Effective area table """ # Put the parameters g in a dictionary. # Units: g1 (cm^2), g2 (), g3 (MeV) 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 += f"Valid instruments: {list(pars.keys())}" raise ValueError(ss) if energy_axis_true is None: energy_axis_true = MapAxis.from_energy_bounds("2 GeV", "200 TeV", nbin=20, per_decade=True, name="energy_true") g1, g2, g3 = pars[instrument] offset_axis = MapAxis.from_edges([0., 5.] * u.deg, name="offset") axes = MapAxes([energy_axis_true, offset_axis]) coords = axes.get_coord() energy, offset = coords["energy_true"].to_value( "MeV"), coords["offset"] data = np.ones_like(offset.value) * g1 * energy**(-g2) * np.exp( -g3 / energy) # TODO: fake offset dependence? meta = {"TELESCOP": instrument} return cls(axes=axes, data=data, unit="cm2", meta=meta)
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.axes["energy_true"].copy(name="energy") else: energy_axis = MapAxis.from_energy_edges(energy) if energy_true is None: energy_axis_true = self.axes["energy_true"] else: energy_axis_true = MapAxis.from_energy_edges( energy_true, name="energy_true", ) axes = MapAxes([energy_axis_true, energy_axis]) coords = axes.get_coord(mode="edges", axis_name="energy") # migration value of energy bounds migra = coords["energy"] / coords["energy_true"] values = self.integral( axis_name="migra", offset=offset, energy_true=coords["energy_true"], migra=migra, ) data = np.diff(values) return EDispKernel( axes=axes, data=data.to_value(""), )
def from_gauss(cls, energy_axis_true, migra_axis, offset_axis, bias, sigma, 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_axis_true : `MapAxis` True energy axis migra_axis : `~astropy.units.Quantity` Migra axis offset_axis : `~astropy.units.Quantity` Bin edges of offset bias : float or `~numpy.ndarray` Center of Gaussian energy dispersion, bias sigma : float or `~numpy.ndarray` RMS width of Gaussian energy dispersion, resolution. pdf_threshold : float, optional Zero suppression threshold """ axes = MapAxes([energy_axis_true, migra_axis, offset_axis]) coords = axes.get_coord(mode="edges", axis_name="migra") migra_min = coords["migra"][:, :-1, :] migra_max = coords["migra"][:, 1:, :] # Analytical formula for integral of Gaussian s = np.sqrt(2) * sigma t1 = (migra_max - 1 - bias) / s t2 = (migra_min - 1 - bias) / s pdf = (scipy.special.erf(t1) - scipy.special.erf(t2)) / 2 pdf = pdf / (migra_max - migra_min) # no offset dependence data = pdf * np.ones(axes.shape) data[data < pdf_threshold] = 0 return cls( axes=axes, data=data.value, )
def get_edisp_kernel(self, position=None, energy_axis=None): """Get energy dispersion at a given position. Parameters ---------- position : `~astropy.coordinates.SkyCoord` the target position. Should be a single coordinates energy_axis : `MapAxis` Reconstructed energy axis Returns ------- edisp : `~gammapy.irf.EnergyDispersion` the energy dispersion (i.e. rmf object) """ if position is None: position = self.edisp_map.geom.center_skydir if position.size != 1: raise ValueError( "EnergyDispersion can be extracted at one single position only." ) position = self._get_nearest_valid_position(position) energy_axis_true = self.edisp_map.geom.axes["energy_true"] axes = MapAxes([energy_axis_true, energy_axis]) coords = axes.get_coord(mode="edges", axis_name="energy") # migration value of energy bounds migra = coords["energy"] / coords["energy_true"] coords = { "skycoord": position, "energy_true": coords["energy_true"], "migra": migra, } values = self.edisp_map.integral(axis_name="migra", coords=coords) data = np.diff(np.clip(values, 0, np.inf)) return EDispKernel(axes=axes, data=data.to_value(""))
def to_3d(self): """ "Convert to Background3D""" edges = np.concatenate(( np.negative(self.axes["offset"].edges)[::-1][:-1], self.axes["offset"].edges, )) fov_lat = MapAxis.from_edges(edges=edges, name="fov_lat") fov_lon = MapAxis.from_edges(edges=edges, name="fov_lon") axes = MapAxes([self.axes["energy"], fov_lon, fov_lat]) coords = axes.get_coord() offset = np.sqrt(coords["fov_lat"]**2 + coords["fov_lon"]**2) data = self.evaluate(offset=offset, energy=coords["energy"]) return Background3D( axes=axes, data=data, )
def from_gauss(cls, energy_axis_true, rad_axis=None, sigma=0.1 * u.deg): """Create all -sky PSF map from Gaussian width. This is used for testing and examples. The width can be the same for all energies or be an array with one value per energy node. It does not depend on position. Parameters ---------- energy_axis_true : `~gammapy.maps.MapAxis` True energy axis. rad_axis : `~gammapy.maps.MapAxis` Offset angle wrt source position axis. sigma : `~astropy.coordinates.Angle` Gaussian width. Returns ------- psf_map : `PSFMap` Point spread function map. """ from gammapy.datasets.map import RAD_AXIS_DEFAULT if rad_axis is None: rad_axis = RAD_AXIS_DEFAULT.copy() axes = MapAxes([energy_axis_true, rad_axis]) coords = axes.get_coord() sigma = np.broadcast_to(u.Quantity(sigma), energy_axis_true.nbin, subok=True) gauss = Gauss2DPDF(sigma=sigma.reshape((-1, 1))) data = gauss(coords["rad"]) table_psf = EnergyDependentTablePSF(axes=axes, unit=data.unit, data=data.value) return cls.from_energy_dependent_table_psf(table_psf)