Repository: gammapy/gammapy

Issue:
SkyMap unit handling
As noticed by @leajouvin at https://github.com/gammapy/gammapy/pull/549/files/0f834b47d09f623762b77288277eec320aa6e16f#r66772852 , FITS I/O currently doesn't work for SkyMap units.

Example:

``` python
>>> import numpy as np
>>> from gammapy.image import SkyMap
>>> map = SkyMap(data=np.zeros((3,2)), unit='cm^-2')
>>> map.write('test.fits', clobber=True)
>>> map2 = SkyMap.read('test.fits')
No valid units found for extension None
>>> map.unit
'cm^-2'
>>> map2.unit
```

@adonath - Is this a simple quick fix, or is fixing this part of a larger change where e.g. `data` and `unit` attributes of `SkyMap` are combined into a `Quantity`?



Discussion:
Yes, this seems to be bug, the unit is only written if a WCS system is defined (see https://github.com/gammapy/gammapy/blob/master/gammapy/image/maps.py#L490). Should be easy to fix.

The following works:

```
import numpy as np
from gammapy.image import SkyMap
map = SkyMap.empty(unit='cm-2')
map.write('test.fits', clobber=True)
map2 = SkyMap.read('test.fits', ext=1)
map.unit
```

`ext=1` is necessary, because `ImageHDU.write()` adds an empty `PrimaryHDU`. Maybe this should be handled better. E.g. by adding a `primary=True` option to `SkyMap.write()`.

> ext=1 is necessary, because ImageHDU.write() adds an empty PrimaryHDU. Maybe this should be handled better.

It can be handled either way of course (empty primary or not).
But I'm +1 to not put an empty primary HDU.

You can achieve this by just always using `PrimaryHDU` as far as I know.
I.e. in a SkyMapCollection on write you'd create mutliple `PrimaryHDU` and write them.
But I'm not sure this is true and haven't tested myself.

@adonath - OK if this issue is assigned to you? I think it would take longer to describe to others the solution you want to put and tests instead of just implementing it?


Resolving patch:
diff --git a/gammapy/image/maps.py b/gammapy/image/maps.py
--- a/gammapy/image/maps.py
+++ b/gammapy/image/maps.py
@@ -489,17 +489,26 @@ def copy(self):
 
     def to_image_hdu(self):
         """
-        Convert sky map to `~astropy.fits.ImageHDU`.
+        Convert sky map to `~astropy.fits.PrimaryHDU`.
+        
+        Returns
+        -------
+        primaryhdu : `~astropy.fits.PrimaryHDU`
+            Primary image hdu object.
         """
-        if not self.wcs is None:
+        if self.wcs is not None:
             header = self.wcs.to_header()
-        
-            # Add meta data
-            header.update(self.meta)
-            header['BUNIT'] = self.unit
         else:
-            header = None
-        return fits.ImageHDU(data=self.data, header=header, name=self.name)
+            header = fits.Header()
+
+        # Add meta data
+        header.update(self.meta)
+        if self.unit is not None:
+            header['BUNIT'] = Unit(self.unit).to_string('fits')
+        if self.name is not None:
+            header['EXTNAME'] = self.name
+            header['HDUNAME'] = self.name
+        return fits.PrimaryHDU(data=self.data, header=header)
 
     def reproject(self, reference, mode='interp', *args, **kwargs):
         """
@@ -624,12 +633,12 @@ def __str__(self):
         String representation of the class.
         """
         info = "Name: {}\n".format(self.name)
-        if not self.data is None:
+        if self.data is not None:
             info += "Data shape: {}\n".format(self.data.shape)
             info += "Data type: {}\n".format(self.data.dtype)
             info += "Data unit: {}\n".format(self.unit)
             info += "Data mean: {:.3e}\n".format(np.nanmean(self.data))
-        if not self.wcs is None:
+        if self.wcs is not None:
             info += "WCS type: {}\n".format(self.wcs.wcs.ctype)
         return info
 
@@ -746,6 +755,7 @@ def write(self, filename=None, header=None, **kwargs):
         for name in self.get('_map_names', sorted(self)):
             if isinstance(self[name], SkyMap):
                 hdu = self[name].to_image_hdu()
+
                 # For now add common collection meta info to the single map headers
                 hdu.header.update(self.meta)
                 hdu.name = name


Test patch:
diff --git a/gammapy/image/tests/test_maps.py b/gammapy/image/tests/test_maps.py
--- a/gammapy/image/tests/test_maps.py
+++ b/gammapy/image/tests/test_maps.py
@@ -36,6 +36,13 @@ def test_io(self, tmpdir):
         skymap = SkyMap.read(str(filename))
         assert self.skymap.name == skymap.name
 
+    def test_unit_io(self, tmpdir):
+        filename = tmpdir / 'test_skymap_unit.fits'
+        skymap_ref = SkyMap(data=np.zeros((3, 3)), unit='1 / cm2')
+        skymap_ref.write(str(filename))
+        skymap = SkyMap.read(str(filename))
+        assert skymap.unit == skymap_ref.unit
+
     def test_lookup_skycoord(self):
         position = SkyCoord(0, 0, frame='galactic', unit='deg')
         assert self.skymap.lookup(position) == 5