# Licensed under a 3-clause BSD style license - see LICENSE.rstimportcopyimportloggingimportnumpyasnpfromastropy.convolutionimportTophat2DKernelfromastropy.coordinatesimportAnglefromgammapy.datasetsimportMapDataset,MapDatasetOnOfffromgammapy.mapsimportMapfromgammapy.modeling.modelsimportPowerLawSpectralModel,SkyModelfromgammapy.statsimportCashCountsStatistic,WStatCountsStatisticfrom..coreimportEstimatorfrom..utilsimportestimate_exposure_reco_energyfrom.coreimportFluxMaps__all__=["ExcessMapEstimator",]log=logging.getLogger(__name__)defconvolved_map_dataset_counts_statistics(dataset,kernel,mask,correlate_off):"""Return CountsDataset objects containing smoothed maps from the MapDataset"""# Kernel is modified later make a copy herekernel=copy.deepcopy(kernel)kernel.normalize("peak")# fft convolution adds numerical noise, to ensure integer results we call# np.rintn_on=dataset.counts*maskn_on_conv=np.rint(n_on.convolve(kernel.array).data)ifisinstance(dataset,MapDatasetOnOff):n_off=dataset.counts_off*masknpred_sig=dataset.npred_signal()*maskacceptance_on=dataset.acceptance*maskacceptance_off=dataset.acceptance_off*masknpred_sig_convolve=npred_sig.convolve(kernel.array)ifcorrelate_off:background=dataset.background*maskbackground.data[dataset.acceptance_off==0]=0.0background_conv=background.convolve(kernel.array)n_off=n_off.convolve(kernel.array)withnp.errstate(invalid="ignore",divide="ignore"):alpha=background_conv/n_offelse:acceptance_on_convolve=acceptance_on.convolve(kernel.array)withnp.errstate(invalid="ignore",divide="ignore"):alpha=acceptance_on_convolve/acceptance_offreturnWStatCountsStatistic(n_on_conv.data,n_off.data,alpha.data,npred_sig_convolve.data)else:npred=dataset.npred()*maskbackground_conv=npred.convolve(kernel.array)returnCashCountsStatistic(n_on_conv.data,background_conv.data)
[docs]classExcessMapEstimator(Estimator):"""Computes correlated excess, significance and error maps from a map dataset. If a model is set on the dataset the excess map estimator will compute the excess taking into account the predicted counts of the model. ..note:: By default the excess estimator correlates the off counts as well to avoid artifacts at the edges of the :term:`FoV` for stacked on-off datasets. However when the on-off dataset has been derived from a ring background estimate, this leads to the off counts being correlated twice. To avoid artifacts and the double correlation, the `ExcessMapEstimator` has to be applied per dataset and the resulting maps need to be stacked, taking the :term:`FoV` cut into account. Parameters ---------- correlation_radius : ~astropy.coordinate.Angle correlation radius to use n_sigma : float Confidence level for the asymmetric errors expressed in number of sigma. n_sigma_ul : float Confidence level for the upper limits expressed in number of sigma. selection_optional : list of str Which additional maps to estimate besides delta TS, significance and symmetric error. Available options are: * "all": all the optional steps are executed * "errn-errp": estimate asymmetric errors. * "ul": estimate upper limits. Default is None so the optional steps are not executed. energy_edges : `~astropy.units.Quantity` Energy edges of the target excess maps bins. correlate_off : bool Correlate OFF events. Default is True. spectral_model : `~gammapy.modeling.models.SpectralModel` Spectral model used for the computation of the flux map. If None, a Power Law of index 2 is assumed (default). Examples -------- >>> from gammapy.datasets import MapDataset >>> from gammapy.estimators import ExcessMapEstimator >>> dataset = MapDataset.read("$GAMMAPY_DATA/cta-1dc-gc/cta-1dc-gc.fits.gz") >>> estimator = ExcessMapEstimator(correlation_radius="0.1 deg") >>> result = estimator.run(dataset) >>> print(result) FluxMaps -------- <BLANKLINE> geom : WcsGeom axes : ['lon', 'lat', 'energy'] shape : (320, 240, 1) quantities : ['npred', 'npred_excess', 'counts', 'ts', 'sqrt_ts', 'norm', 'norm_err'] # noqa: E501 ref. model : pl n_sigma : 1 n_sigma_ul : 2 sqrt_ts_threshold_ul : 2 sed type init : likelihood """tag="ExcessMapEstimator"_available_selection_optional=["errn-errp","ul"]def__init__(self,correlation_radius="0.1 deg",n_sigma=1,n_sigma_ul=2,selection_optional=None,energy_edges=None,correlate_off=True,spectral_model=None,):self.correlation_radius=correlation_radiusself.n_sigma=n_sigmaself.n_sigma_ul=n_sigma_ulself.selection_optional=selection_optionalself.energy_edges=energy_edgesself.correlate_off=correlate_offifspectral_modelisNone:spectral_model=PowerLawSpectralModel(index=2)self.spectral_model=spectral_model@propertydefcorrelation_radius(self):returnself._correlation_radius@correlation_radius.setterdefcorrelation_radius(self,correlation_radius):"""Sets radius"""self._correlation_radius=Angle(correlation_radius)
[docs]defrun(self,dataset):"""Compute correlated excess, Li & Ma significance and flux maps If a model is set on the dataset the excess map estimator will compute the excess taking into account the predicted counts of the model. Parameters ---------- dataset : `~gammapy.datasets.MapDataset` or `~gammapy.datasets.MapDatasetOnOff` Map dataset Returns ------- maps : `FluxMaps` Flux maps """ifnotisinstance(dataset,MapDataset):raiseValueError("Unsupported dataset type. Excess map is not applicable to 1D datasets.")axis=self._get_energy_axis(dataset)resampled_dataset=dataset.resample_energy_axis(energy_axis=axis,name=dataset.name)ifisinstance(dataset,MapDatasetOnOff):resampled_dataset.models=dataset.modelselse:resampled_dataset.background=dataset.npred().resample_axis(axis=axis)resampled_dataset.models=Noneresult=self.estimate_excess_map(resampled_dataset)returnresult
[docs]defestimate_excess_map(self,dataset):"""Estimate excess and ts maps for single dataset. If exposure is defined, a flux map is also computed. Parameters ---------- dataset : `MapDataset` Map dataset """pixel_size=np.mean(np.abs(dataset.counts.geom.wcs.wcs.cdelt))size=self.correlation_radius.deg/pixel_sizekernel=Tophat2DKernel(size)geom=dataset.counts.geomifdataset.mask_fit:mask=dataset.maskelifdataset.mask_safe:mask=dataset.mask_safeelse:mask=Map.from_geom(geom,data=True,dtype=bool)counts_stat=convolved_map_dataset_counts_statistics(dataset,kernel,mask,self.correlate_off)maps={}maps["npred"]=Map.from_geom(geom,data=counts_stat.n_on)maps["npred_excess"]=Map.from_geom(geom,data=counts_stat.n_sig)maps["counts"]=maps["npred"]maps["ts"]=Map.from_geom(geom,data=counts_stat.ts)maps["sqrt_ts"]=Map.from_geom(geom,data=counts_stat.sqrt_ts)ifdataset.exposure:reco_exposure=estimate_exposure_reco_energy(dataset,self.spectral_model,normalize=False)withnp.errstate(invalid="ignore",divide="ignore"):reco_exposure=reco_exposure.convolve(kernel.array)/mask.convolve(kernel.array)else:reco_exposure=1withnp.errstate(invalid="ignore",divide="ignore"):maps["norm"]=maps["npred_excess"]/reco_exposuremaps["norm_err"]=(Map.from_geom(geom,data=counts_stat.error*self.n_sigma)/reco_exposure)if"errn-errp"inself.selection_optional:maps["norm_errn"]=(Map.from_geom(geom,data=counts_stat.compute_errn(self.n_sigma))/reco_exposure)maps["norm_errp"]=(Map.from_geom(geom,data=counts_stat.compute_errp(self.n_sigma))/reco_exposure)if"ul"inself.selection_optional:maps["norm_ul"]=(Map.from_geom(geom,data=counts_stat.compute_upper_limit(self.n_sigma_ul))/reco_exposure)# return nan values outside maskfornameinmaps:maps[name].data[~mask]=np.nanmeta={"n_sigma":self.n_sigma,"n_sigma_ul":self.n_sigma_ul,"sed_type_init":"likelihood",}returnFluxMaps.from_maps(maps=maps,meta=meta,reference_model=SkyModel(self.spectral_model),sed_type="likelihood",)