We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Allow to specify spectral model in ExcessMapEstimator
Currently the `ExcessMapEstimator` does not allow to define the spectral model, that is used for the flux computation. It is easy to support and should be done...


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gammapy/estimators/excess_map.py`
Content:
```
1 # Licensed under a 3-clause BSD style license - see LICENSE.rst
2 import copy
3 import logging
4 import numpy as np
5 import astropy.units as u
6 from astropy.convolution import Tophat2DKernel
7 from astropy.coordinates import Angle
8 from gammapy.datasets import MapDataset, MapDatasetOnOff
9 from gammapy.maps import Map, MapAxis
10 from gammapy.stats import CashCountsStatistic, WStatCountsStatistic
11 from .core import Estimator
12 from .utils import estimate_exposure_reco_energy
13 
14 __all__ = [
15     "ExcessMapEstimator",
16 ]
17 
18 log = logging.getLogger(__name__)
19 
20 
21 def convolved_map_dataset_counts_statistics(dataset, kernel, mask, correlate_off):
22     """Return CountsDataset objects containing smoothed maps from the MapDataset"""
23     # Kernel is modified later make a copy here
24     kernel = copy.deepcopy(kernel)
25     kernel.normalize("peak")
26 
27     # fft convolution adds numerical noise, to ensure integer results we call
28     # np.rint
29     n_on = dataset.counts * mask
30     n_on_conv = np.rint(n_on.convolve(kernel.array).data)
31 
32     if isinstance(dataset, MapDatasetOnOff):
33         n_off = dataset.counts_off * mask
34         npred_sig = dataset.npred_signal() * mask
35         acceptance_on = dataset.acceptance * mask
36         acceptance_off = dataset.acceptance_off * mask
37 
38         npred_sig_convolve = npred_sig.convolve(kernel.array)
39         acceptance_on_convolve = acceptance_on.convolve(kernel.array)
40         if correlate_off:
41             n_off = n_off.convolve(kernel.array)
42             acceptance_off = acceptance_off.convolve(kernel.array)
43 
44         with np.errstate(invalid="ignore", divide="ignore"):
45             alpha = acceptance_on_convolve / acceptance_off
46 
47         return WStatCountsStatistic(
48             n_on_conv.data, n_off.data, alpha.data, npred_sig_convolve.data
49         )
50     else:
51 
52         npred = dataset.npred() * mask
53         background_conv = npred.convolve(kernel.array)
54         return CashCountsStatistic(n_on_conv.data, background_conv.data)
55 
56 
57 class ExcessMapEstimator(Estimator):
58     """Computes correlated excess, sqrt TS (i.e. Li-Ma significance) and errors for MapDatasets.
59 
60     If a model is set on the dataset the excess map estimator will compute the excess taking into account
61     the predicted counts of the model.
62 
63     Some background estimation techniques like ring background or adaptive ring background will provide already
64     correlated data for OFF. In the case of already correlated OFF data, the OFF data should not be correlated again,
65     and so the option correlate_off should set to False (default).
66 
67     Parameters
68     ----------
69     correlation_radius : ~astropy.coordinate.Angle
70         correlation radius to use
71     n_sigma : float
72         Confidence level for the asymmetric errors expressed in number of sigma.
73         Default is 1.
74     n_sigma_ul : float
75         Confidence level for the upper limits expressed in number of sigma.
76         Default is 3.
77     selection_optional : list of str
78         Which additional maps to estimate besides delta TS, significance and symmetric error.
79         Available options are:
80 
81             * "errn-errp": estimate asymmetric errors.
82             * "ul": estimate upper limits.
83 
84         By default all additional quantities are estimated.
85     energy_edges : `~astropy.units.Quantity`
86         Energy edges of the target excess maps bins.
87     apply_mask_fit : Bool
88         Apply a mask for the computation.
89         A `~gammapy.datasets.MapDataset.mask_fit` must be present on the input dataset
90     correlate_off : Bool
91         Correlate OFF events in the case of a MapDatasetOnOff
92     """
93 
94     tag = "ExcessMapEstimator"
95     _available_selection_optional = ["errn-errp", "ul"]
96 
97     def __init__(
98         self,
99         correlation_radius="0.1 deg",
100         n_sigma=1,
101         n_sigma_ul=3,
102         selection_optional=None,
103         energy_edges=None,
104         apply_mask_fit=False,
105         correlate_off=False
106     ):
107         self.correlation_radius = correlation_radius
108         self.n_sigma = n_sigma
109         self.n_sigma_ul = n_sigma_ul
110         self.apply_mask_fit = apply_mask_fit
111         self.selection_optional = selection_optional
112         self.energy_edges = energy_edges
113         self.correlate_off = correlate_off
114 
115     @property
116     def correlation_radius(self):
117         return self._correlation_radius
118 
119     @correlation_radius.setter
120     def correlation_radius(self, correlation_radius):
121         """Sets radius"""
122         self._correlation_radius = Angle(correlation_radius)
123 
124     def run(self, dataset):
125         """Compute correlated excess, Li & Ma significance and flux maps
126 
127         If a model is set on the dataset the excess map estimator will compute the excess taking into account
128         the predicted counts of the model.
129 
130         Parameters
131         ----------
132         dataset : `~gammapy.datasets.MapDataset` or `~gammapy.datasets.MapDatasetOnOff`
133             input dataset
134 
135         Returns
136         -------
137         images : dict
138             Dictionary containing result correlated maps. Keys are:
139 
140                 * counts : correlated counts map
141                 * background : correlated background map
142                 * excess : correlated excess map
143                 * ts : TS map
144                 * sqrt_ts : sqrt(delta TS), or Li-Ma significance map
145                 * err : symmetric error map (from covariance)
146                 * flux : flux map. An exposure map must be present in the dataset to compute flux map
147                 * errn : negative error map
148                 * errp : positive error map
149                 * ul : upper limit map
150 
151         """
152         if not isinstance(dataset, MapDataset):
153             raise ValueError("Unsupported dataset type")
154 
155         if self.energy_edges is None:
156             energy_axis = dataset.counts.geom.axes["energy"]
157             energy_edges = u.Quantity([energy_axis.edges[0], energy_axis.edges[-1]])
158         else:
159             energy_edges = self.energy_edges
160 
161         axis = MapAxis.from_energy_edges(energy_edges)
162 
163         resampled_dataset = dataset.resample_energy_axis(energy_axis=axis)
164 
165         # Beware we rely here on the correct npred background in MapDataset.resample_energy_axis
166         resampled_dataset.models = dataset.models
167 
168         result = self.estimate_excess_map(resampled_dataset)
169 
170         return result
171 
172     def estimate_excess_map(self, dataset):
173         """Estimate excess and ts maps for single dataset.
174 
175         If exposure is defined, a flux map is also computed.
176 
177         Parameters
178         ----------
179         dataset : `MapDataset`
180             Map dataset
181         """
182 
183         pixel_size = np.mean(np.abs(dataset.counts.geom.wcs.wcs.cdelt))
184         size = self.correlation_radius.deg / pixel_size
185         kernel = Tophat2DKernel(size)
186 
187         geom = dataset.counts.geom
188 
189         if self.apply_mask_fit:
190             mask = dataset.mask
191         elif dataset.mask_safe:
192             mask = dataset.mask_safe
193         else:
194             mask = np.ones(dataset.data_shape, dtype=bool)
195 
196         counts_stat = convolved_map_dataset_counts_statistics(dataset, kernel, mask, self.correlate_off)
197 
198         n_on = Map.from_geom(geom, data=counts_stat.n_on)
199         bkg = Map.from_geom(geom, data=counts_stat.n_on - counts_stat.n_sig)
200         excess = Map.from_geom(geom, data=counts_stat.n_sig)
201 
202         result = {"counts": n_on, "background": bkg, "excess": excess}
203 
204         tsmap = Map.from_geom(geom, data=counts_stat.ts)
205         sqrt_ts = Map.from_geom(geom, data=counts_stat.sqrt_ts)
206         result.update({"ts": tsmap, "sqrt_ts": sqrt_ts})
207 
208         err = Map.from_geom(geom, data=counts_stat.error * self.n_sigma)
209         result.update({"err": err})
210 
211         if dataset.exposure:
212             reco_exposure = estimate_exposure_reco_energy(dataset)
213             with np.errstate(invalid="ignore", divide="ignore"):
214                 flux = excess / reco_exposure
215             flux.quantity = flux.quantity.to("1 / (cm2 s)")
216         else:
217             flux = Map.from_geom(
218                 geom=dataset.counts.geom, data=np.nan * np.ones(dataset.data_shape)
219             )
220         result.update({"flux": flux})
221 
222         if "errn-errp" in self.selection_optional:
223             errn = Map.from_geom(geom, data=counts_stat.compute_errn(self.n_sigma))
224             errp = Map.from_geom(geom, data=counts_stat.compute_errp(self.n_sigma))
225             result.update({"errn": errn, "errp": errp})
226 
227         if "ul" in self.selection_optional:
228             ul = Map.from_geom(
229                 geom, data=counts_stat.compute_upper_limit(self.n_sigma_ul)
230             )
231             result.update({"ul": ul})
232 
233         # return nan values outside mask
234         for key in result:
235             result[key].data[~mask] = np.nan
236 
237         return result
238
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gammapy/estimators/excess_map.py b/gammapy/estimators/excess_map.py
--- a/gammapy/estimators/excess_map.py
+++ b/gammapy/estimators/excess_map.py
@@ -89,6 +89,9 @@
         A `~gammapy.datasets.MapDataset.mask_fit` must be present on the input dataset
     correlate_off : Bool
         Correlate OFF events in the case of a MapDatasetOnOff
+    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). 
     """
 
     tag = "ExcessMapEstimator"
@@ -102,7 +105,8 @@
         selection_optional=None,
         energy_edges=None,
         apply_mask_fit=False,
-        correlate_off=False
+        correlate_off=False,
+        spectral_model=None,
     ):
         self.correlation_radius = correlation_radius
         self.n_sigma = n_sigma
@@ -111,6 +115,7 @@
         self.selection_optional = selection_optional
         self.energy_edges = energy_edges
         self.correlate_off = correlate_off
+        self.spectral_model = spectral_model
 
     @property
     def correlation_radius(self):
@@ -193,7 +198,9 @@
         else:
             mask = np.ones(dataset.data_shape, dtype=bool)
 
-        counts_stat = convolved_map_dataset_counts_statistics(dataset, kernel, mask, self.correlate_off)
+        counts_stat = convolved_map_dataset_counts_statistics(
+            dataset, kernel, mask, self.correlate_off
+        )
 
         n_on = Map.from_geom(geom, data=counts_stat.n_on)
         bkg = Map.from_geom(geom, data=counts_stat.n_on - counts_stat.n_sig)
@@ -209,7 +216,7 @@
         result.update({"err": err})
 
         if dataset.exposure:
-            reco_exposure = estimate_exposure_reco_energy(dataset)
+            reco_exposure = estimate_exposure_reco_energy(dataset, self.spectral_model)
             with np.errstate(invalid="ignore", divide="ignore"):
                 flux = excess / reco_exposure
             flux.quantity = flux.quantity.to("1 / (cm2 s)")

{"golden_diff": "diff --git a/gammapy/estimators/excess_map.py b/gammapy/estimators/excess_map.py\n--- a/gammapy/estimators/excess_map.py\n+++ b/gammapy/estimators/excess_map.py\n@@ -89,6 +89,9 @@\n         A `~gammapy.datasets.MapDataset.mask_fit` must be present on the input dataset\n     correlate_off : Bool\n         Correlate OFF events in the case of a MapDatasetOnOff\n+    spectral_model : `~gammapy.modeling.models.SpectralModel`\n+        Spectral model used for the computation of the flux map. \n+        If None, a Power Law of index 2 is assumed (default). \n     \"\"\"\n \n     tag = \"ExcessMapEstimator\"\n@@ -102,7 +105,8 @@\n         selection_optional=None,\n         energy_edges=None,\n         apply_mask_fit=False,\n-        correlate_off=False\n+        correlate_off=False,\n+        spectral_model=None,\n     ):\n         self.correlation_radius = correlation_radius\n         self.n_sigma = n_sigma\n@@ -111,6 +115,7 @@\n         self.selection_optional = selection_optional\n         self.energy_edges = energy_edges\n         self.correlate_off = correlate_off\n+        self.spectral_model = spectral_model\n \n     @property\n     def correlation_radius(self):\n@@ -193,7 +198,9 @@\n         else:\n             mask = np.ones(dataset.data_shape, dtype=bool)\n \n-        counts_stat = convolved_map_dataset_counts_statistics(dataset, kernel, mask, self.correlate_off)\n+        counts_stat = convolved_map_dataset_counts_statistics(\n+            dataset, kernel, mask, self.correlate_off\n+        )\n \n         n_on = Map.from_geom(geom, data=counts_stat.n_on)\n         bkg = Map.from_geom(geom, data=counts_stat.n_on - counts_stat.n_sig)\n@@ -209,7 +216,7 @@\n         result.update({\"err\": err})\n \n         if dataset.exposure:\n-            reco_exposure = estimate_exposure_reco_energy(dataset)\n+            reco_exposure = estimate_exposure_reco_energy(dataset, self.spectral_model)\n             with np.errstate(invalid=\"ignore\", divide=\"ignore\"):\n                 flux = excess / reco_exposure\n             flux.quantity = flux.quantity.to(\"1 / (cm2 s)\")\n", "issue": "Allow to specify spectral model in ExcessMapEstimator\nCurrently the `ExcessMapEstimator` does not allow to define the spectral model, that is used for the flux computation. It is easy to support and should be done...\r\n\n", "before_files": [{"content": "# Licensed under a 3-clause BSD style license - see LICENSE.rst\nimport copy\nimport logging\nimport numpy as np\nimport astropy.units as u\nfrom astropy.convolution import Tophat2DKernel\nfrom astropy.coordinates import Angle\nfrom gammapy.datasets import MapDataset, MapDatasetOnOff\nfrom gammapy.maps import Map, MapAxis\nfrom gammapy.stats import CashCountsStatistic, WStatCountsStatistic\nfrom .core import Estimator\nfrom .utils import estimate_exposure_reco_energy\n\n__all__ = [\n    \"ExcessMapEstimator\",\n]\n\nlog = logging.getLogger(__name__)\n\n\ndef convolved_map_dataset_counts_statistics(dataset, kernel, mask, correlate_off):\n    \"\"\"Return CountsDataset objects containing smoothed maps from the MapDataset\"\"\"\n    # Kernel is modified later make a copy here\n    kernel = copy.deepcopy(kernel)\n    kernel.normalize(\"peak\")\n\n    # fft convolution adds numerical noise, to ensure integer results we call\n    # np.rint\n    n_on = dataset.counts * mask\n    n_on_conv = np.rint(n_on.convolve(kernel.array).data)\n\n    if isinstance(dataset, MapDatasetOnOff):\n        n_off = dataset.counts_off * mask\n        npred_sig = dataset.npred_signal() * mask\n        acceptance_on = dataset.acceptance * mask\n        acceptance_off = dataset.acceptance_off * mask\n\n        npred_sig_convolve = npred_sig.convolve(kernel.array)\n        acceptance_on_convolve = acceptance_on.convolve(kernel.array)\n        if correlate_off:\n            n_off = n_off.convolve(kernel.array)\n            acceptance_off = acceptance_off.convolve(kernel.array)\n\n        with np.errstate(invalid=\"ignore\", divide=\"ignore\"):\n            alpha = acceptance_on_convolve / acceptance_off\n\n        return WStatCountsStatistic(\n            n_on_conv.data, n_off.data, alpha.data, npred_sig_convolve.data\n        )\n    else:\n\n        npred = dataset.npred() * mask\n        background_conv = npred.convolve(kernel.array)\n        return CashCountsStatistic(n_on_conv.data, background_conv.data)\n\n\nclass ExcessMapEstimator(Estimator):\n    \"\"\"Computes correlated excess, sqrt TS (i.e. Li-Ma significance) and errors for MapDatasets.\n\n    If a model is set on the dataset the excess map estimator will compute the excess taking into account\n    the predicted counts of the model.\n\n    Some background estimation techniques like ring background or adaptive ring background will provide already\n    correlated data for OFF. In the case of already correlated OFF data, the OFF data should not be correlated again,\n    and so the option correlate_off should set to False (default).\n\n    Parameters\n    ----------\n    correlation_radius : ~astropy.coordinate.Angle\n        correlation radius to use\n    n_sigma : float\n        Confidence level for the asymmetric errors expressed in number of sigma.\n        Default is 1.\n    n_sigma_ul : float\n        Confidence level for the upper limits expressed in number of sigma.\n        Default is 3.\n    selection_optional : list of str\n        Which additional maps to estimate besides delta TS, significance and symmetric error.\n        Available options are:\n\n            * \"errn-errp\": estimate asymmetric errors.\n            * \"ul\": estimate upper limits.\n\n        By default all additional quantities are estimated.\n    energy_edges : `~astropy.units.Quantity`\n        Energy edges of the target excess maps bins.\n    apply_mask_fit : Bool\n        Apply a mask for the computation.\n        A `~gammapy.datasets.MapDataset.mask_fit` must be present on the input dataset\n    correlate_off : Bool\n        Correlate OFF events in the case of a MapDatasetOnOff\n    \"\"\"\n\n    tag = \"ExcessMapEstimator\"\n    _available_selection_optional = [\"errn-errp\", \"ul\"]\n\n    def __init__(\n        self,\n        correlation_radius=\"0.1 deg\",\n        n_sigma=1,\n        n_sigma_ul=3,\n        selection_optional=None,\n        energy_edges=None,\n        apply_mask_fit=False,\n        correlate_off=False\n    ):\n        self.correlation_radius = correlation_radius\n        self.n_sigma = n_sigma\n        self.n_sigma_ul = n_sigma_ul\n        self.apply_mask_fit = apply_mask_fit\n        self.selection_optional = selection_optional\n        self.energy_edges = energy_edges\n        self.correlate_off = correlate_off\n\n    @property\n    def correlation_radius(self):\n        return self._correlation_radius\n\n    @correlation_radius.setter\n    def correlation_radius(self, correlation_radius):\n        \"\"\"Sets radius\"\"\"\n        self._correlation_radius = Angle(correlation_radius)\n\n    def run(self, dataset):\n        \"\"\"Compute correlated excess, Li & Ma significance and flux maps\n\n        If a model is set on the dataset the excess map estimator will compute the excess taking into account\n        the predicted counts of the model.\n\n        Parameters\n        ----------\n        dataset : `~gammapy.datasets.MapDataset` or `~gammapy.datasets.MapDatasetOnOff`\n            input dataset\n\n        Returns\n        -------\n        images : dict\n            Dictionary containing result correlated maps. Keys are:\n\n                * counts : correlated counts map\n                * background : correlated background map\n                * excess : correlated excess map\n                * ts : TS map\n                * sqrt_ts : sqrt(delta TS), or Li-Ma significance map\n                * err : symmetric error map (from covariance)\n                * flux : flux map. An exposure map must be present in the dataset to compute flux map\n                * errn : negative error map\n                * errp : positive error map\n                * ul : upper limit map\n\n        \"\"\"\n        if not isinstance(dataset, MapDataset):\n            raise ValueError(\"Unsupported dataset type\")\n\n        if self.energy_edges is None:\n            energy_axis = dataset.counts.geom.axes[\"energy\"]\n            energy_edges = u.Quantity([energy_axis.edges[0], energy_axis.edges[-1]])\n        else:\n            energy_edges = self.energy_edges\n\n        axis = MapAxis.from_energy_edges(energy_edges)\n\n        resampled_dataset = dataset.resample_energy_axis(energy_axis=axis)\n\n        # Beware we rely here on the correct npred background in MapDataset.resample_energy_axis\n        resampled_dataset.models = dataset.models\n\n        result = self.estimate_excess_map(resampled_dataset)\n\n        return result\n\n    def estimate_excess_map(self, dataset):\n        \"\"\"Estimate excess and ts maps for single dataset.\n\n        If exposure is defined, a flux map is also computed.\n\n        Parameters\n        ----------\n        dataset : `MapDataset`\n            Map dataset\n        \"\"\"\n\n        pixel_size = np.mean(np.abs(dataset.counts.geom.wcs.wcs.cdelt))\n        size = self.correlation_radius.deg / pixel_size\n        kernel = Tophat2DKernel(size)\n\n        geom = dataset.counts.geom\n\n        if self.apply_mask_fit:\n            mask = dataset.mask\n        elif dataset.mask_safe:\n            mask = dataset.mask_safe\n        else:\n            mask = np.ones(dataset.data_shape, dtype=bool)\n\n        counts_stat = convolved_map_dataset_counts_statistics(dataset, kernel, mask, self.correlate_off)\n\n        n_on = Map.from_geom(geom, data=counts_stat.n_on)\n        bkg = Map.from_geom(geom, data=counts_stat.n_on - counts_stat.n_sig)\n        excess = Map.from_geom(geom, data=counts_stat.n_sig)\n\n        result = {\"counts\": n_on, \"background\": bkg, \"excess\": excess}\n\n        tsmap = Map.from_geom(geom, data=counts_stat.ts)\n        sqrt_ts = Map.from_geom(geom, data=counts_stat.sqrt_ts)\n        result.update({\"ts\": tsmap, \"sqrt_ts\": sqrt_ts})\n\n        err = Map.from_geom(geom, data=counts_stat.error * self.n_sigma)\n        result.update({\"err\": err})\n\n        if dataset.exposure:\n            reco_exposure = estimate_exposure_reco_energy(dataset)\n            with np.errstate(invalid=\"ignore\", divide=\"ignore\"):\n                flux = excess / reco_exposure\n            flux.quantity = flux.quantity.to(\"1 / (cm2 s)\")\n        else:\n            flux = Map.from_geom(\n                geom=dataset.counts.geom, data=np.nan * np.ones(dataset.data_shape)\n            )\n        result.update({\"flux\": flux})\n\n        if \"errn-errp\" in self.selection_optional:\n            errn = Map.from_geom(geom, data=counts_stat.compute_errn(self.n_sigma))\n            errp = Map.from_geom(geom, data=counts_stat.compute_errp(self.n_sigma))\n            result.update({\"errn\": errn, \"errp\": errp})\n\n        if \"ul\" in self.selection_optional:\n            ul = Map.from_geom(\n                geom, data=counts_stat.compute_upper_limit(self.n_sigma_ul)\n            )\n            result.update({\"ul\": ul})\n\n        # return nan values outside mask\n        for key in result:\n            result[key].data[~mask] = np.nan\n\n        return result\n", "path": "gammapy/estimators/excess_map.py"}], "after_files": [{"content": "# Licensed under a 3-clause BSD style license - see LICENSE.rst\nimport copy\nimport logging\nimport numpy as np\nimport astropy.units as u\nfrom astropy.convolution import Tophat2DKernel\nfrom astropy.coordinates import Angle\nfrom gammapy.datasets import MapDataset, MapDatasetOnOff\nfrom gammapy.maps import Map, MapAxis\nfrom gammapy.stats import CashCountsStatistic, WStatCountsStatistic\nfrom .core import Estimator\nfrom .utils import estimate_exposure_reco_energy\n\n__all__ = [\n    \"ExcessMapEstimator\",\n]\n\nlog = logging.getLogger(__name__)\n\n\ndef convolved_map_dataset_counts_statistics(dataset, kernel, mask, correlate_off):\n    \"\"\"Return CountsDataset objects containing smoothed maps from the MapDataset\"\"\"\n    # Kernel is modified later make a copy here\n    kernel = copy.deepcopy(kernel)\n    kernel.normalize(\"peak\")\n\n    # fft convolution adds numerical noise, to ensure integer results we call\n    # np.rint\n    n_on = dataset.counts * mask\n    n_on_conv = np.rint(n_on.convolve(kernel.array).data)\n\n    if isinstance(dataset, MapDatasetOnOff):\n        n_off = dataset.counts_off * mask\n        npred_sig = dataset.npred_signal() * mask\n        acceptance_on = dataset.acceptance * mask\n        acceptance_off = dataset.acceptance_off * mask\n\n        npred_sig_convolve = npred_sig.convolve(kernel.array)\n        acceptance_on_convolve = acceptance_on.convolve(kernel.array)\n        if correlate_off:\n            n_off = n_off.convolve(kernel.array)\n            acceptance_off = acceptance_off.convolve(kernel.array)\n\n        with np.errstate(invalid=\"ignore\", divide=\"ignore\"):\n            alpha = acceptance_on_convolve / acceptance_off\n\n        return WStatCountsStatistic(\n            n_on_conv.data, n_off.data, alpha.data, npred_sig_convolve.data\n        )\n    else:\n\n        npred = dataset.npred() * mask\n        background_conv = npred.convolve(kernel.array)\n        return CashCountsStatistic(n_on_conv.data, background_conv.data)\n\n\nclass ExcessMapEstimator(Estimator):\n    \"\"\"Computes correlated excess, sqrt TS (i.e. Li-Ma significance) and errors for MapDatasets.\n\n    If a model is set on the dataset the excess map estimator will compute the excess taking into account\n    the predicted counts of the model.\n\n    Some background estimation techniques like ring background or adaptive ring background will provide already\n    correlated data for OFF. In the case of already correlated OFF data, the OFF data should not be correlated again,\n    and so the option correlate_off should set to False (default).\n\n    Parameters\n    ----------\n    correlation_radius : ~astropy.coordinate.Angle\n        correlation radius to use\n    n_sigma : float\n        Confidence level for the asymmetric errors expressed in number of sigma.\n        Default is 1.\n    n_sigma_ul : float\n        Confidence level for the upper limits expressed in number of sigma.\n        Default is 3.\n    selection_optional : list of str\n        Which additional maps to estimate besides delta TS, significance and symmetric error.\n        Available options are:\n\n            * \"errn-errp\": estimate asymmetric errors.\n            * \"ul\": estimate upper limits.\n\n        By default all additional quantities are estimated.\n    energy_edges : `~astropy.units.Quantity`\n        Energy edges of the target excess maps bins.\n    apply_mask_fit : Bool\n        Apply a mask for the computation.\n        A `~gammapy.datasets.MapDataset.mask_fit` must be present on the input dataset\n    correlate_off : Bool\n        Correlate OFF events in the case of a MapDatasetOnOff\n    spectral_model : `~gammapy.modeling.models.SpectralModel`\n        Spectral model used for the computation of the flux map. \n        If None, a Power Law of index 2 is assumed (default). \n    \"\"\"\n\n    tag = \"ExcessMapEstimator\"\n    _available_selection_optional = [\"errn-errp\", \"ul\"]\n\n    def __init__(\n        self,\n        correlation_radius=\"0.1 deg\",\n        n_sigma=1,\n        n_sigma_ul=3,\n        selection_optional=None,\n        energy_edges=None,\n        apply_mask_fit=False,\n        correlate_off=False,\n        spectral_model=None,\n    ):\n        self.correlation_radius = correlation_radius\n        self.n_sigma = n_sigma\n        self.n_sigma_ul = n_sigma_ul\n        self.apply_mask_fit = apply_mask_fit\n        self.selection_optional = selection_optional\n        self.energy_edges = energy_edges\n        self.correlate_off = correlate_off\n        self.spectral_model = spectral_model\n\n    @property\n    def correlation_radius(self):\n        return self._correlation_radius\n\n    @correlation_radius.setter\n    def correlation_radius(self, correlation_radius):\n        \"\"\"Sets radius\"\"\"\n        self._correlation_radius = Angle(correlation_radius)\n\n    def run(self, dataset):\n        \"\"\"Compute correlated excess, Li & Ma significance and flux maps\n\n        If a model is set on the dataset the excess map estimator will compute the excess taking into account\n        the predicted counts of the model.\n\n        Parameters\n        ----------\n        dataset : `~gammapy.datasets.MapDataset` or `~gammapy.datasets.MapDatasetOnOff`\n            input dataset\n\n        Returns\n        -------\n        images : dict\n            Dictionary containing result correlated maps. Keys are:\n\n                * counts : correlated counts map\n                * background : correlated background map\n                * excess : correlated excess map\n                * ts : TS map\n                * sqrt_ts : sqrt(delta TS), or Li-Ma significance map\n                * err : symmetric error map (from covariance)\n                * flux : flux map. An exposure map must be present in the dataset to compute flux map\n                * errn : negative error map\n                * errp : positive error map\n                * ul : upper limit map\n\n        \"\"\"\n        if not isinstance(dataset, MapDataset):\n            raise ValueError(\"Unsupported dataset type\")\n\n        if self.energy_edges is None:\n            energy_axis = dataset.counts.geom.axes[\"energy\"]\n            energy_edges = u.Quantity([energy_axis.edges[0], energy_axis.edges[-1]])\n        else:\n            energy_edges = self.energy_edges\n\n        axis = MapAxis.from_energy_edges(energy_edges)\n\n        resampled_dataset = dataset.resample_energy_axis(energy_axis=axis)\n\n        # Beware we rely here on the correct npred background in MapDataset.resample_energy_axis\n        resampled_dataset.models = dataset.models\n\n        result = self.estimate_excess_map(resampled_dataset)\n\n        return result\n\n    def estimate_excess_map(self, dataset):\n        \"\"\"Estimate excess and ts maps for single dataset.\n\n        If exposure is defined, a flux map is also computed.\n\n        Parameters\n        ----------\n        dataset : `MapDataset`\n            Map dataset\n        \"\"\"\n\n        pixel_size = np.mean(np.abs(dataset.counts.geom.wcs.wcs.cdelt))\n        size = self.correlation_radius.deg / pixel_size\n        kernel = Tophat2DKernel(size)\n\n        geom = dataset.counts.geom\n\n        if self.apply_mask_fit:\n            mask = dataset.mask\n        elif dataset.mask_safe:\n            mask = dataset.mask_safe\n        else:\n            mask = np.ones(dataset.data_shape, dtype=bool)\n\n        counts_stat = convolved_map_dataset_counts_statistics(\n            dataset, kernel, mask, self.correlate_off\n        )\n\n        n_on = Map.from_geom(geom, data=counts_stat.n_on)\n        bkg = Map.from_geom(geom, data=counts_stat.n_on - counts_stat.n_sig)\n        excess = Map.from_geom(geom, data=counts_stat.n_sig)\n\n        result = {\"counts\": n_on, \"background\": bkg, \"excess\": excess}\n\n        tsmap = Map.from_geom(geom, data=counts_stat.ts)\n        sqrt_ts = Map.from_geom(geom, data=counts_stat.sqrt_ts)\n        result.update({\"ts\": tsmap, \"sqrt_ts\": sqrt_ts})\n\n        err = Map.from_geom(geom, data=counts_stat.error * self.n_sigma)\n        result.update({\"err\": err})\n\n        if dataset.exposure:\n            reco_exposure = estimate_exposure_reco_energy(dataset, self.spectral_model)\n            with np.errstate(invalid=\"ignore\", divide=\"ignore\"):\n                flux = excess / reco_exposure\n            flux.quantity = flux.quantity.to(\"1 / (cm2 s)\")\n        else:\n            flux = Map.from_geom(\n                geom=dataset.counts.geom, data=np.nan * np.ones(dataset.data_shape)\n            )\n        result.update({\"flux\": flux})\n\n        if \"errn-errp\" in self.selection_optional:\n            errn = Map.from_geom(geom, data=counts_stat.compute_errn(self.n_sigma))\n            errp = Map.from_geom(geom, data=counts_stat.compute_errp(self.n_sigma))\n            result.update({\"errn\": errn, \"errp\": errp})\n\n        if \"ul\" in self.selection_optional:\n            ul = Map.from_geom(\n                geom, data=counts_stat.compute_upper_limit(self.n_sigma_ul)\n            )\n            result.update({\"ul\": ul})\n\n        # return nan values outside mask\n        for key in result:\n            result[key].data[~mask] = np.nan\n\n        return result\n", "path": "gammapy/estimators/excess_map.py"}]}