Repository: gammapy/gammapy

Issue:
Improve HGPS catalog spatial model
I improved the HESS HGPS catalog code and tests in #1231 .

The main remaining thing I'm not happy with yet is the spatial model. This is a reminder issue to improve that.

- [ ] give access to source morphology from catalog table for all sources (i.e. the moment-based measures for multi-gauss). Probably via a choice string argument to the existing spatial_model method
- [x] once we have better source model classes, spatial and spectral should be separated, so that it is no longer necessary to give an energy band when asking for a spatial model.



Discussion:
The 2nd item has been implemented in  https://github.com/gammapy/gammapy/commit/99740667fa3cdbdf3d6b55d45cd26d0720414cae.

Concerning the first item: I'm not sure I would give access to the moment-based measures as a morphology model (a as single Gaussian), because it's misleading and unclear what to do with it.
It is not really an alternative morphology model for the source, because it hasn't been fit to the data.

Apart from these items, there are 2 remaining TODOs in the source code:

https://github.com/gammapy/gammapy/blob/master/gammapy/catalog/hess.py#L522
https://github.com/gammapy/gammapy/blob/master/gammapy/catalog/hess.py#L556

Both are easy to implement, I could do it for v0.10...

Resolving patch:
diff --git a/gammapy/catalog/hess.py b/gammapy/catalog/hess.py
--- a/gammapy/catalog/hess.py
+++ b/gammapy/catalog/hess.py
@@ -1,19 +1,17 @@
 # Licensed under a 3-clause BSD style license - see LICENSE.rst
 """HESS Galactic plane survey (HGPS) catalog."""
 from __future__ import absolute_import, division, print_function, unicode_literals
-import os
 from collections import OrderedDict
 import numpy as np
 import astropy.units as u
 from astropy.table import Table
 from astropy.coordinates import Angle
 from astropy.modeling.models import Gaussian1D
-from ..extern.pathlib import Path
 from ..utils.scripts import make_path
 from ..utils.table import table_row_to_dict
 from ..utils.interpolation import ScaledRegularGridInterpolator
 from ..spectrum import FluxPoints
-from ..spectrum.models import PowerLaw, PowerLaw2, ExponentialCutoffPowerLaw
+from ..spectrum.models import PowerLaw, ExponentialCutoffPowerLaw
 from ..image.models import SkyPointSource, SkyGaussian, SkyShell
 from ..cube.models import SkyModel, SkyModels
 from .core import SourceCatalog, SourceCatalogObject
@@ -89,21 +87,6 @@ def spatial_model(self):
         )
         return model
 
-    @property
-    def spectral_model(self):
-        """Component spectral model (`gammapy.spectrum.models.PowerLaw2`)."""
-        d = self.data
-        model = PowerLaw2(
-            amplitude=d["Flux_Map"], index=2.3, emin="1 TeV", emax="1e5 TeV"
-        )
-        model.parameters.set_parameter_errors(dict(amplitude=d["Flux_Map_Err"]))
-        return model
-
-    @property
-    def sky_model(self):
-        """Component sky model (`gammapy.cube.models.SkyModel`)."""
-        return SkyModel(self.spatial_model, self.spectral_model)
-
 
 class SourceCatalogObjectHGPS(SourceCatalogObject):
     """One object from the HGPS catalog.
@@ -518,8 +501,6 @@ def spatial_model(self):
         - ``Gaussian``: `~gammapy.image.models.SkyGaussian`
         - ``2-Gaussian`` or ``3-Gaussian``: composite model (using ``+`` with Gaussians)
         - ``Shell``: `~gammapy.image.models.SkyShell`
-
-        TODO: add parameter errors
         """
         d = self.data
         glon = d["GLON"]
@@ -535,7 +516,7 @@ def spatial_model(self):
             raise ValueError("For Gaussian or Multi-Gaussian models, use sky_model()!")
         elif spatial_type == "shell":
             # HGPS contains no information on shell width
-            # Here we assuma a 5% shell width for all shells.
+            # Here we assume a 5% shell width for all shells.
             r_out = d["Size"]
             radius = 0.95 * r_out
             width = r_out - radius
@@ -545,18 +526,34 @@ def spatial_model(self):
 
         return model
 
-    @property
-    def sky_model(self):
-        """Source sky model (`~gammapy.cube.models.SkyModel`)."""
+    def sky_model(self, which="best"):
+        """Source sky model.
+
+        Parameters
+        ----------
+        which : {'best', 'pl', 'ecpl'}
+            Which spectral model
+
+        Returns
+        -------
+        sky_model : `~gammapy.cube.models.SkyModel`
+            Sky model of the catalog object.
+        """
         if self.spatial_model_type in {"2-gaussian", "3-gaussian"}:
-            models = [c.sky_model for c in self.components]
+            models = []
+
+            spectral_model = self.spectral_model(which=which)
+            for component in self.components:
+                weight = component.data["Flux_Map"] / self.data["Flux_Map"]
+                spectral_model_comp = spectral_model.copy()
+                # weight amplitude of the component
+                spectral_model_comp.parameters["amplitude"].value *= weight
+                models.append(SkyModel(component.spatial_model, spectral_model_comp))
+
             return SkyModels(models)
         else:
             spatial_model = self.spatial_model
-            # TODO: there are two spectral models
-            # Here we only expose the default
-            # Change sky_model into a method and re-expose option to select spectral model?
-            spectral_model = self.spectral_model()
+            spectral_model = self.spectral_model(which=which)
             return SkyModel(spatial_model, spectral_model)
 
     @property
diff --git a/gammapy/catalog/registry.py b/gammapy/catalog/registry.py
--- a/gammapy/catalog/registry.py
+++ b/gammapy/catalog/registry.py
@@ -29,10 +29,9 @@ def builtins(cls):
 
         import os
 
-        if "HGPS_ANALYSIS" in os.environ:
-            from .hess import SourceCatalogHGPS
+        from .hess import SourceCatalogHGPS
 
-            source_catalogs.register("hgps", SourceCatalogHGPS)
+        source_catalogs.register("hgps", SourceCatalogHGPS)
 
         if "GAMMA_CAT" in os.environ:
             from .gammacat import SourceCatalogGammaCat


Test patch:
diff --git a/gammapy/catalog/tests/test_hess.py b/gammapy/catalog/tests/test_hess.py
--- a/gammapy/catalog/tests/test_hess.py
+++ b/gammapy/catalog/tests/test_hess.py
@@ -67,7 +67,7 @@ def test_all_sources(cat):
             source.spectral_model()
             source.spatial_model_type
             source.is_pointlike
-            source.sky_model
+            source.sky_model()
             source.flux_points
 
     @staticmethod
@@ -174,7 +174,7 @@ def test_spatial_model_type(cat):
 
     @staticmethod
     def test_sky_model_point(cat):
-        model = cat["HESS J1826-148"].sky_model
+        model = cat["HESS J1826-148"].sky_model()
         p = model.parameters
         assert_allclose(p["amplitude"].value, 9.815771242691063e-13)
         assert_allclose(p["lon_0"].value, 16.882482528686523)
@@ -182,54 +182,55 @@ def test_sky_model_point(cat):
 
     @staticmethod
     def test_sky_model_gaussian(cat):
-        model = cat["HESS J1119-614"].sky_model
+        model = cat["HESS J1119-614"].sky_model()
         p = model.parameters
         assert_allclose(p["amplitude"].value, 7.959899015960725e-13)
         assert_allclose(p["lon_0"].value, 292.1280822753906)
         assert_allclose(p["lat_0"].value, -0.5332353711128235)
         assert_allclose(p["sigma"].value, 0.09785966575145721)
 
-        # TODO: bring back the bounding box in the new model classes
-        # bbox = model.bounding_box
-        # assert_allclose(bbox, [[-1.07146, 0.00499], [-68.41014, -67.33368]], atol=0.001)
-
     @staticmethod
     def test_sky_model_gaussian2(cat):
-        model = cat["HESS J1843-033"].sky_model
+        model = cat["HESS J1843-033"].sky_model()
 
         p = model.skymodels[0].parameters
-        assert_allclose(p["amplitude"].value, 1.343344814726255e-12)
+        assert_allclose(p["amplitude"].value, 4.259815e-13, rtol=1e-5)
         assert_allclose(p["lon_0"].value, 29.047216415405273)
         assert_allclose(p["lat_0"].value, 0.24389676749706268)
         assert_allclose(p["sigma"].value, 0.12499100714921951)
 
         p = model.skymodels[1].parameters
-        assert_allclose(p["amplitude"].value, 1.5390372353277226e-12)
+        assert_allclose(p["amplitude"].value, 4.880365e-13, rtol=1e-5)
         assert_allclose(p["lon_0"].value, 28.77037811279297)
         assert_allclose(p["lat_0"].value, -0.0727819949388504)
         assert_allclose(p["sigma"].value, 0.2294706553220749)
 
-        # TODO: bounding boxes need to be re-added to the new model classes
-        # bbox = model.bounding_box
-        # assert_allclose(bbox, [[-1.33487, 1.18930], [27.50829, 30.03246]], atol=0.001)
-
     @staticmethod
     def test_sky_model_gaussian3(cat):
-        model = cat["HESS J1825-137"].sky_model
-        assert_allclose(
-            model.skymodels[0].parameters["amplitude"].value, 5.022436459778401e-12
-        )
-        assert_allclose(
-            model.skymodels[1].parameters["amplitude"].value, 1.1829840926291801e-11
-        )
-        assert_allclose(
-            model.skymodels[2].parameters["amplitude"].value, 1.5557788347539403e-12
-        )
+        model = cat["HESS J1825-137"].sky_model()
+
+        p = model.skymodels[0].parameters
+        assert_allclose(p["amplitude"].value, 1.8952104218765842e-11)
+        assert_allclose(p["lon_0"].value, 16.988601684570312)
+        assert_allclose(p["lat_0"].value, -0.4913068115711212)
+        assert_allclose(p["sigma"].value, 0.47650089859962463)
+
+        p = model.skymodels[1].parameters
+        assert_allclose(p["amplitude"].value, 4.4639763971527836e-11)
+        assert_allclose(p["lon_0"].value, 17.71169090270996)
+        assert_allclose(p["lat_0"].value, -0.6598004102706909)
+        assert_allclose(p["sigma"].value, 0.3910967707633972)
+
+        p = model.skymodels[2].parameters
+        assert_allclose(p["amplitude"].value, 5.870712920658374e-12)
+        assert_allclose(p["lon_0"].value, 17.840524673461914)
+        assert_allclose(p["lat_0"].value, -0.7057178020477295)
+        assert_allclose(p["sigma"].value, 0.10932201147079468)
 
     @staticmethod
     def test_sky_model_gaussian_extern(cat):
         # special test for the only extern source with a gaussian morphology
-        model = cat["HESS J1801-233"].sky_model
+        model = cat["HESS J1801-233"].sky_model()
         p = model.parameters
         assert_allclose(p["amplitude"].value, 7.499999970031479e-13)
         assert_allclose(p["lon_0"].value, 6.656888961791992)
@@ -238,7 +239,7 @@ def test_sky_model_gaussian_extern(cat):
 
     @staticmethod
     def test_sky_model_shell(cat):
-        model = cat["Vela Junior"].sky_model
+        model = cat["Vela Junior"].sky_model()
         p = model.parameters
         assert_allclose(p["amplitude"].value, 3.2163001428830995e-11)
         assert_allclose(p["lon_0"].value, 266.2873840332031)
@@ -280,18 +281,6 @@ def test_spatial_model(component):
         assert_allclose(p["sigma"].value, 0.2294706553220749)
         assert_allclose(p.error("sigma"), 0.04618723690509796)
 
-    @staticmethod
-    def test_spectral_model(component):
-        model = component.spectral_model
-        p = model.parameters
-        assert_allclose(p["amplitude"].value, 1.5390372353277226e-12)
-        assert_allclose(p.error("amplitude"), 4.721826770727466e-13)
-
-    @staticmethod
-    def test_sky_model(component):
-        model = component.sky_model
-        assert "SkyModel" in str(model)
-
 
 class TestSourceCatalogLargeScaleHGPS:
     def setup(self):