Repository: gammapy/gammapy

Issue:
IRFs are missing required GADF headers 
**Gammapy version**

Current master

**Bug description**

HDU tables of IRFs directly created from gammapy (at least effective area, will check the others) lack several required GADF header keywords. 

**Expected behavior**

Output is compatible to GADF.

**To Reproduce**

```python
from gammapy.irf import EffectiveAreaTable2D
from gammapy.maps import MapAxis
import astropy.units as u
import numpy as np

energy_axis = MapAxis.from_energy_bounds('1 TeV', '10 TeV', nbin=3, name='energy_true')
offset_axis = MapAxis.from_bounds(0 * u.deg, 2 * u.deg, nbin=2, name='offset')

aeff = EffectiveAreaTable2D(data=np.ones((3, 2)) * u.m**2, axes=[energy_axis, offset_axis])

print(repr(aeff.to_table_hdu(format='gadf-dl3').header))
```

Result:
```
XTENSION= 'BINTABLE'           / binary table extension                         
BITPIX  =                    8 / array data type                                
NAXIS   =                    2 / number of array dimensions                     
NAXIS1  =                  128 / length of dimension 1                          
NAXIS2  =                    1 / length of dimension 2                          
PCOUNT  =                    0 / number of group parameters                     
GCOUNT  =                    1 / number of groups                               
TFIELDS =                    5 / number of table fields                         
HDUCLAS2= 'EFF_AREA'                                                            
EXTNAME = 'EFFECTIVE AREA'     / extension name                                 
TTYPE1  = 'ENERG_LO'                                                            
TFORM1  = '3D      '                                                            
TUNIT1  = 'TeV     '                                                            
TDIM1   = '(3)     '                                                            
TTYPE2  = 'ENERG_HI'                                                            
TFORM2  = '3D      '                                                            
TUNIT2  = 'TeV     '                                                            
TDIM2   = '(3)     '                                                            
TTYPE3  = 'THETA_LO'                                                            
TFORM3  = '2D      '                                                            
TUNIT3  = 'deg     '                                                            
TDIM3   = '(2)     '                                                            
TTYPE4  = 'THETA_HI'                                                            
TFORM4  = '2D      '                                                            
TUNIT4  = 'deg     '                                                            
TDIM4   = '(2)     '                                                            
TTYPE5  = 'EFFAREA '                                                            
TFORM5  = '6D      '                                                            
TUNIT5  = 'm2      '                                                            
TDIM5   = '(3,2)   '  
```

Missing are at least:

* `HDUDOC`
* `HDUVERS`
* `HDUCLASS`
* `HDUCLAS1`
* `HDUCLAS3`
* `HDUCLAS4`



Discussion:

Resolving patch:
diff --git a/gammapy/irf/core.py b/gammapy/irf/core.py
--- a/gammapy/irf/core.py
+++ b/gammapy/irf/core.py
@@ -418,10 +418,14 @@ def to_table(self, format="gadf-dl3"):
         if format == "gadf-dl3":
             table.meta = self.meta.copy()
             spec = IRF_DL3_HDU_SPECIFICATION[self.tag]
-            # TODO: add missing required meta data!
-            table.meta["HDUCLAS2"] = spec["hduclas2"]
+
+            table.meta.update(spec["mandatory_keywords"])
+
             if self.is_pointlike:
                 table.meta["HDUCLAS3"] = "POINT-LIKE"
+            else:
+                table.meta["HDUCLAS3"] = "FULL-ENCLOSURE"
+
             table.meta.pop("is_pointlike", None)
             table[spec["column_name"]] = self.quantity.T[np.newaxis]
         else:
diff --git a/gammapy/irf/io.py b/gammapy/irf/io.py
--- a/gammapy/irf/io.py
+++ b/gammapy/irf/io.py
@@ -20,6 +20,18 @@
     "MIGRA": {"name": "migra", "interp": "lin"},
 }
 
+COMMON_HEADERS = {
+    'HDUCLASS': 'GADF',
+    'HDUDOC': 'https://github.com/open-gamma-ray-astro/gamma-astro-data-formats',
+    'HDUVERS': '0.2',
+}
+
+COMMON_IRF_HEADERS = {
+    **COMMON_HEADERS,
+    'HDUCLAS1': 'RESPONSE',
+}
+
+
 
 # The key is the class tag.
 # TODO: extend the info here with the minimal header info
@@ -27,26 +39,45 @@
     "bkg_3d": {
         "extname": "BACKGROUND",
         "column_name": "BKG",
-        "hduclas2": "BKG",
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "BKG",
+            "HDUCLAS3": "FULL-ENCLOSURE",  # added here to have HDUCLASN in order
+            "HDUCLAS4": "BKG_3D",
+        }
     },
     "bkg_2d": {
         "extname": "BACKGROUND",
         "column_name": "BKG",
-        "hduclas2": "BKG",
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "BKG",
+            "HDUCLAS3": "FULL-ENCLOSURE",  # added here to have HDUCLASN in order
+            "HDUCLAS4": "BKG_2D",
+        }
     },
     "edisp_2d": {
         "extname": "ENERGY DISPERSION",
         "column_name": "MATRIX",
-        "hduclas2": "EDISP",
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "EDISP",
+            "HDUCLAS3": "FULL-ENCLOSURE",  # added here to have HDUCLASN in order
+            "HDUCLAS4": "EDISP_2D",
+        }
     },
     "psf_table": {
         "extname": "PSF_2D_TABLE",
         "column_name": "RPSF",
-        "hduclas2": "PSF",
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "RPSF",
+            "HDUCLAS3": "FULL-ENCLOSURE",  # added here to have HDUCLASN in order
+            "HDUCLAS4": "PSF_TABLE",
+        }
     },
     "psf_3gauss": {
         "extname": "PSF_2D_GAUSS",
-        "hduclas2": "PSF",
         "column_name": {
             "sigma_1": "SIGMA_1",
             "sigma_2": "SIGMA_2",
@@ -55,24 +86,45 @@
             "ampl_2": "AMPL_2",
             "ampl_3": "AMPL_3",
         },
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "RPSF",
+            "HDUCLAS3": "FULL-ENCLOSURE",  # added here to have HDUCLASN in order
+            "HDUCLAS4": "PSF_3GAUSS",
+        }
     },
     "psf_king": {
         "extname": "PSF_2D_KING",
-        "hduclas2": "PSF",
         "column_name": {
             "sigma": "SIGMA",
             "gamma": "GAMMA",
         },
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "RPSF",
+            "HDUCLAS3": "FULL-ENCLOSURE",  # added here to have HDUCLASN in order
+            "HDUCLAS4": "PSF_KING",
+        }
     },
     "aeff_2d": {
         "extname": "EFFECTIVE AREA",
         "column_name": "EFFAREA",
-        "hduclas2": "EFF_AREA",
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "EFF_AREA",
+            "HDUCLAS3": "FULL-ENCLOSURE",  # added here to have HDUCLASN in order
+            "HDUCLAS4": "AEFF_2D",
+        }
     },
     "rad_max_2d": {
         "extname": "RAD_MAX",
         "column_name": "RAD_MAX",
-        "hduclas2": "RAD_MAX",
+        "mandatory_keywords": {
+            **COMMON_IRF_HEADERS,
+            "HDUCLAS2": "RAD_MAX",
+            "HDUCLAS3": "POINT-LIKE",
+            "HDUCLAS4": "RAD_MAX_2D",
+        }
     },
 }
 


Test patch:
diff --git a/gammapy/irf/tests/test_gadf.py b/gammapy/irf/tests/test_gadf.py
new file mode 100644
--- /dev/null
+++ b/gammapy/irf/tests/test_gadf.py
@@ -0,0 +1,96 @@
+import astropy.units as u
+import numpy as np
+
+
+def test_effective_area_2d_to_gadf():
+    from gammapy.irf import EffectiveAreaTable2D
+    from gammapy.maps import MapAxis
+
+    energy_axis = MapAxis.from_energy_bounds(1 * u.TeV, 10 * u.TeV, nbin=3, name='energy_true')
+    offset_axis = MapAxis.from_bounds(0 * u.deg, 2 * u.deg, nbin=2, name='offset')
+    data = np.ones((energy_axis.nbin, offset_axis.nbin)) * u.m**2
+
+    aeff = EffectiveAreaTable2D(data=data, axes=[energy_axis, offset_axis])
+    hdu = aeff.to_table_hdu(format='gadf-dl3')
+
+    columns = {column.name for column in hdu.columns}
+    mandatory_columns = {
+        'ENERG_LO', 'ENERG_HI', 'THETA_LO', 'THETA_HI', 'EFFAREA',
+    }
+
+    missing = mandatory_columns.difference(columns)
+    assert len(missing) == 0, f'GADF HDU missing required column(s) {missing}'
+
+    header = hdu.header
+    assert header['HDUCLASS'] == 'GADF'
+    assert header['HDUDOC'] == 'https://github.com/open-gamma-ray-astro/gamma-astro-data-formats'
+    assert header['HDUVERS'] == '0.2'
+    assert header['HDUCLAS1'] == 'RESPONSE'
+    assert header['HDUCLAS2'] == 'EFF_AREA'
+    assert header['HDUCLAS3'] == 'FULL-ENCLOSURE'
+    assert header['HDUCLAS4'] == 'AEFF_2D'
+    assert header['EXTNAME'] == 'EFFECTIVE AREA'
+
+
+def test_energy_dispersion_2d_to_gadf():
+    from gammapy.irf import EnergyDispersion2D
+    from gammapy.maps import MapAxis
+
+    energy_axis = MapAxis.from_energy_bounds(1 * u.TeV, 10 * u.TeV, nbin=3, name='energy_true')
+    offset_axis = MapAxis.from_bounds(0 * u.deg, 2 * u.deg, nbin=2, name='offset')
+    migra_axis = MapAxis.from_bounds(0.2, 5, nbin=5, interp='log', name='migra')
+    data = np.zeros((energy_axis.nbin, migra_axis.nbin, offset_axis.nbin)) * u.m**2
+
+    edisp = EnergyDispersion2D(data=data, axes=[energy_axis, migra_axis, offset_axis])
+    hdu = edisp.to_table_hdu(format='gadf-dl3')
+
+    mandatory_columns = {
+        'ENERG_LO', 'ENERG_HI',
+        'MIGRA_LO', 'MIGRA_HI',
+        'THETA_LO', 'THETA_HI',
+        'MATRIX',
+    }
+    columns = {column.name for column in hdu.columns}
+    missing = mandatory_columns.difference(columns)
+    assert len(missing) == 0, f'GADF HDU missing required column(s) {missing}'
+
+    header = hdu.header
+    assert header['HDUCLASS'] == 'GADF'
+    assert header['HDUDOC'] == 'https://github.com/open-gamma-ray-astro/gamma-astro-data-formats'
+    assert header['HDUVERS'] == '0.2'
+    assert header['HDUCLAS1'] == 'RESPONSE'
+    assert header['HDUCLAS2'] == 'EDISP'
+    assert header['HDUCLAS3'] == 'FULL-ENCLOSURE'
+    assert header['HDUCLAS4'] == 'EDISP_2D'
+
+
+def test_psf_3d_to_gadf():
+    from gammapy.irf import PSF3D
+    from gammapy.maps import MapAxis
+
+    energy_axis = MapAxis.from_energy_bounds(1 * u.TeV, 10 * u.TeV, nbin=3, name='energy_true')
+    offset_axis = MapAxis.from_bounds(0 * u.deg, 2 * u.deg, nbin=2, name='offset')
+    rad_axis = MapAxis.from_bounds(0.0 * u.deg, 1 * u.deg, nbin=10, name='rad')
+    data = np.zeros((energy_axis.nbin, offset_axis.nbin, rad_axis.nbin)) / u.sr
+
+    psf = PSF3D(data=data, axes=[energy_axis, offset_axis, rad_axis])
+    hdu = psf.to_table_hdu(format='gadf-dl3')
+
+    mandatory_columns = {
+        'ENERG_LO', 'ENERG_HI',
+        'THETA_LO', 'THETA_HI',
+        'RAD_LO', 'RAD_HI',
+        'RPSF',
+    }
+    columns = {column.name for column in hdu.columns}
+    missing = mandatory_columns.difference(columns)
+    assert len(missing) == 0, f'GADF HDU missing required column(s) {missing}'
+
+    header = hdu.header
+    assert header['HDUCLASS'] == 'GADF'
+    assert header['HDUDOC'] == 'https://github.com/open-gamma-ray-astro/gamma-astro-data-formats'
+    assert header['HDUVERS'] == '0.2'
+    assert header['HDUCLAS1'] == 'RESPONSE'
+    assert header['HDUCLAS2'] == 'RPSF'
+    assert header['HDUCLAS3'] == 'FULL-ENCLOSURE'
+    assert header['HDUCLAS4'] == 'PSF_TABLE'