Repository: gammapy/gammapy

Issue:
ReflectedRegions analysis fails when no off regions are found
**Gammapy version**

0.19, dev

**Bug description**

Reflected Regions analysis fails when no off regions are found. While the `ReflectedRegionsBackgroundMaker` correctly raises a warning, the error happens in `SafeMaskMaker`

```
ReflectedRegionsBackgroundMaker failed. No OFF region found outside exclusion mask for jTTbJpk3.
ReflectedRegionsBackgroundMaker failed. Setting jTTbJpk3 mask to False.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<timed exec> in <module>

~/opt/anaconda3/envs/gammapy-0.19/lib/python3.9/site-packages/gammapy/makers/safe.py in run(self, dataset, observation)
    283         mask_safe = np.ones(dataset._geom.data_shape, dtype=bool)
    284 
--> 285         if dataset.background is not None:
    286             # apply it first so only clipped values are removed for "bkg-peak"
    287             mask_safe &= self.make_mask_bkg_invalid(dataset)

~/opt/anaconda3/envs/gammapy-0.19/lib/python3.9/site-packages/gammapy/datasets/map.py in background(self)
   1971             Background map
   1972         """
-> 1973         return self.alpha * self.counts_off
   1974 
   1975     def stat_array(self):

~/opt/anaconda3/envs/gammapy-0.19/lib/python3.9/site-packages/gammapy/maps/core.py in __mul__(self, other)
   1579 
   1580     def __mul__(self, other):
-> 1581         return self._arithmetics(np.multiply, other, copy=True)
   1582 
   1583     def __imul__(self, other):

~/opt/anaconda3/envs/gammapy-0.19/lib/python3.9/site-packages/gammapy/maps/core.py in _arithmetics(self, operator, other, copy)
   1543                 raise ValueError("Map Arithmetic: Inconsistent geometries.")
   1544         else:
-> 1545             q = u.Quantity(other, copy=False)
   1546 
   1547         out = self.copy() if copy else self

~/opt/anaconda3/envs/gammapy-0.19/lib/python3.9/site-packages/astropy/units/quantity.py in __new__(cls, value, unit, dtype, copy, order, subok, ndmin)
    382             not (value.dtype.kind == 'O' and
    383                  isinstance(value.item(0), numbers.Number))):
--> 384             raise TypeError("The value must be a valid Python or "
    385                             "Numpy numeric type.")
    386 

TypeError: The value must be a valid Python or Numpy numeric type.
```



Discussion:
The solution is to either set counts_off to all zeros here: https://github.com/gammapy/gammapy/blob/9ac71fa8579014d655dce844da1f3b8a9030c70d/gammapy/makers/background/reflected.py#L334

or to return `dataset.background` as all zeros here https://github.com/gammapy/gammapy/blob/9ac71fa8579014d655dce844da1f3b8a9030c70d/gammapy/datasets/map.py#L1973

or to simply implement Exception handling in `SafeMaskMaker`

Is there any preference physically @registerrier @adonath @bkhelifi ?
Indeed, even if the mask_safe of the SpectrumDatasetOnOff is false, we do not take this information into account.,,
In https://github.com/gammapy/gammapy/blob/9ac71fa8579014d655dce844da1f3b8a9030c70d/gammapy/makers/safe.py#L283, we do not 'read' the content of the dataset mask before making computation, which is surprising.
Thanks @bkhelifi ! We should start with `dataset.mask_safe.data` if it exists, and then I suppose it makes sense that `dataset.background` is `None` if `dataset.alpha` or `dataset.counts_off` is None? 

dataset.mask_safe.data should be all at False if there is no OFF region:
https://github.com/gammapy/gammapy/blob/9ac71fa8579014d655dce844da1f3b8a9030c70d/gammapy/makers/background/reflected.py#L371
Maybe a test of a mask with no valid entry should be enough, is not it?


Resolving patch:
diff --git a/gammapy/datasets/map.py b/gammapy/datasets/map.py
--- a/gammapy/datasets/map.py
+++ b/gammapy/datasets/map.py
@@ -2077,6 +2077,8 @@ def background(self):
         background : `Map`
             Background map
         """
+        if self.counts_off is None:
+            return None
         return self.alpha * self.counts_off
 
     def stat_array(self):
diff --git a/gammapy/makers/safe.py b/gammapy/makers/safe.py
--- a/gammapy/makers/safe.py
+++ b/gammapy/makers/safe.py
@@ -281,7 +281,11 @@ def run(self, dataset, observation=None):
         dataset : `Dataset`
             Dataset with defined safe range mask.
         """
-        mask_safe = np.ones(dataset._geom.data_shape, dtype=bool)
+
+        if dataset.mask_safe:
+            mask_safe = dataset.mask_safe.data
+        else:
+            mask_safe = np.ones(dataset._geom.data_shape, dtype=bool)
 
         if dataset.background is not None:
             # apply it first so only clipped values are removed for "bkg-peak"


Test patch:
diff --git a/gammapy/makers/background/tests/test_reflected.py b/gammapy/makers/background/tests/test_reflected.py
--- a/gammapy/makers/background/tests/test_reflected.py
+++ b/gammapy/makers/background/tests/test_reflected.py
@@ -18,6 +18,7 @@
     ReflectedRegionsFinder,
     WobbleRegionsFinder,
     SpectrumDatasetMaker,
+    SafeMaskMaker,
 )
 from gammapy.maps import MapAxis, RegionGeom, WcsGeom
 from gammapy.utils.regions import compound_region_to_regions
@@ -82,7 +83,8 @@ def test_find_reflected_regions(
         min_distance_input="0 deg",
     )
     regions, _ = finder.run(
-        center=pointing, region=on_region,
+        center=pointing,
+        region=on_region,
         exclusion_mask=exclusion_mask,
     )
     assert len(regions) == nreg1
@@ -95,7 +97,8 @@ def test_find_reflected_regions(
     # Test with too small exclusion
     small_mask = exclusion_mask.cutout(pointing, Angle("0.1 deg"))
     regions, _ = finder.run(
-        center=pointing, region=on_region,
+        center=pointing,
+        region=on_region,
         exclusion_mask=small_mask,
     )
     assert len(regions) == nreg3
@@ -103,7 +106,8 @@ def test_find_reflected_regions(
     # Test with maximum number of regions
     finder.max_region_number = 5
     regions, _ = finder.run(
-        center=pointing, region=on_region,
+        center=pointing,
+        region=on_region,
         exclusion_mask=small_mask,
     )
     assert len(regions) == 5
@@ -140,9 +144,7 @@ def test_find_reflected_regions(
 def test_non_circular_regions(region, nreg):
     pointing = SkyCoord(0.0, 0.0, unit="deg")
 
-    finder = ReflectedRegionsFinder(
-        min_distance_input="0 deg"
-    )
+    finder = ReflectedRegionsFinder(min_distance_input="0 deg")
     regions, _ = finder.run(center=pointing, region=region)
     assert len(regions) == nreg
 
@@ -193,7 +195,7 @@ def test_reflected_bkg_maker_no_off(reflected_bkg_maker, observations, caplog):
     radius = Angle(0.11, "deg")
     region = CircleSkyRegion(pos, radius)
 
-    maker = SpectrumDatasetMaker(selection=["counts"])
+    maker = SpectrumDatasetMaker(selection=["counts", "exposure"])
 
     datasets = []
 
@@ -202,9 +204,11 @@ def test_reflected_bkg_maker_no_off(reflected_bkg_maker, observations, caplog):
     geom = RegionGeom.create(region=region, axes=[e_reco])
     dataset_empty = SpectrumDataset.create(geom=geom, energy_axis_true=e_true)
 
+    safe_mask_masker = SafeMaskMaker(methods=["aeff-max"], aeff_percent=10)
     for obs in observations:
         dataset = maker.run(dataset_empty, obs)
         dataset_on_off = reflected_bkg_maker.run(dataset, obs)
+        dataset_on_off = safe_mask_masker.run(dataset_on_off, obs)
         datasets.append(dataset_on_off)
 
     assert datasets[0].counts_off is None
@@ -263,7 +267,9 @@ def test_wobble_regions_finder():
         position_angle=180 * u.deg + source_angle,
     )
 
-    assert u.isclose(pointing.position_angle(on_region.center).to(u.deg), source_angle, rtol=0.05)
+    assert u.isclose(
+        pointing.position_angle(on_region.center).to(u.deg), source_angle, rtol=0.05
+    )
     n_off_regions = 3
 
     finder = WobbleRegionsFinder(n_off_regions)
@@ -317,15 +323,19 @@ def test_wobble_regions_finder_overlapping(caplog):
         regions, _ = finder.run(on_region, pointing)
 
     assert len(regions) == 0
-    assert caplog.record_tuples == [(
-        'gammapy.makers.background.reflected',
-        logging.WARNING,
-        'Found overlapping off regions, returning no regions',
-    )]
+    assert caplog.record_tuples == [
+        (
+            "gammapy.makers.background.reflected",
+            logging.WARNING,
+            "Found overlapping off regions, returning no regions",
+        )
+    ]
 
 
 @requires_data()
-def test_reflected_bkg_maker_with_wobble_finder(on_region, observations, exclusion_mask):
+def test_reflected_bkg_maker_with_wobble_finder(
+    on_region, observations, exclusion_mask
+):
     datasets = []
 
     reflected_bkg_maker = ReflectedRegionsBackgroundMaker(