Issues (144)

gammapy/catalog/hess.py (1 issue)

1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2
"""HESS Galactic plane survey (HGPS) catalog."""
3
import numpy as np
4
import astropy.units as u
5
from astropy.coordinates import Angle
6
from astropy.modeling.models import Gaussian1D
7
from astropy.table import Table
8
from gammapy.estimators import FluxPoints
9
from gammapy.maps import MapAxis, RegionGeom
10
from gammapy.modeling.models import Model, Models, SkyModel
11
from gammapy.utils.interpolation import ScaledRegularGridInterpolator
12
from gammapy.utils.scripts import make_path
13
from gammapy.utils.table import table_row_to_dict
14
from .core import SourceCatalog, SourceCatalogObject, format_flux_points_table
15
16
__all__ = [
17
    "SourceCatalogHGPS",
18
    "SourceCatalogLargeScaleHGPS",
19
    "SourceCatalogObjectHGPS",
20
    "SourceCatalogObjectHGPSComponent",
21
]
22
23
# Flux factor, used for printing
24
FF = 1e-12
25
26
# Multiplicative factor to go from cm^-2 s^-1 to % Crab for integral flux > 1 TeV
27
# Here we use the same Crab reference that's used in the HGPS paper
28
# CRAB = crab_integral_flux(energy_min=1, reference='hess_ecpl')
29
FLUX_TO_CRAB = 100 / 2.26e-11
30
FLUX_TO_CRAB_DIFF = 100 / 3.5060459323111307e-11
31
32
33
class SourceCatalogObjectHGPSComponent(SourceCatalogObject):
34
    """One Gaussian component from the HGPS catalog.
35
36
    See Also
37
    --------
38
    SourceCatalogHGPS, SourceCatalogObjectHGPS
39
    """
40
41
    _source_name_key = "Component_ID"
42
43
    def __init__(self, data):
44
        self.data = data
45
46
    def __repr__(self):
47
        return f"{self.__class__.__name__}({self.name!r})"
48
49
    def __str__(self):
50
        """Pretty-print source data"""
51
        d = self.data
52
        ss = "Component {}:\n".format(d["Component_ID"])
53
        fmt = "{:<20s} : {:8.3f} +/- {:.3f} deg\n"
54
        ss += fmt.format("GLON", d["GLON"].value, d["GLON_Err"].value)
55
        ss += fmt.format("GLAT", d["GLAT"].value, d["GLAT_Err"].value)
56
        fmt = "{:<20s} : {:.3f} +/- {:.3f} deg\n"
57
        ss += fmt.format("Size", d["Size"].value, d["Size_Err"].value)
58
        val, err = d["Flux_Map"].value, d["Flux_Map_Err"].value
59
        fmt = "{:<20s} : ({:.2f} +/- {:.2f}) x 10^-12 cm^-2 s^-1 = ({:.1f} +/- {:.1f}) % Crab"
60
        ss += fmt.format(
61
            "Flux (>1 TeV)", val / FF, err / FF, val * FLUX_TO_CRAB, err * FLUX_TO_CRAB
62
        )
63
        return ss
64
65
    @property
66
    def name(self):
67
        """Source name (str)"""
68
        return self.data[self._source_name_key]
69
70
    def spatial_model(self):
71
        """Component spatial model (`~gammapy.modeling.models.GaussianSpatialModel`)."""
72
        d = self.data
73
        tag = "GaussianSpatialModel"
74
        pars = {
75
            "lon_0": d["GLON"],
76
            "lat_0": d["GLAT"],
77
            "sigma": d["Size"],
78
            "frame": "galactic",
79
        }
80
        errs = {"lon_0": d["GLON_Err"], "lat_0": d["GLAT_Err"], "sigma": d["Size_Err"]}
81
        model = Model.create(tag, "spatial", **pars)
82
83
        for name, value in errs.items():
84
            model.parameters[name].error = value
85
86
        return model
87
88
89
class SourceCatalogObjectHGPS(SourceCatalogObject):
90
    """One object from the HGPS catalog.
91
92
    The catalog is represented by `SourceCatalogHGPS`.
93
    Examples are given there.
94
    """
95
96
    def __repr__(self):
97
        return f"{self.__class__.__name__}({self.name!r})"
98
99
    def __str__(self):
100
        return self.info()
101
102
    @property
103
    def flux_points(self):
104
        """Flux points (`~gammapy.estimators.FluxPoints`)."""
105
        reference_model = SkyModel(spectral_model=self.spectral_model(), name=self.name)
106
        return FluxPoints.from_table(
107
            self.flux_points_table,
108
            reference_model=reference_model,
109
        )
110
111
    def info(self, info="all"):
112
        """Info string.
113
114
        Parameters
115
        ----------
116
        info : {'all', 'basic', 'map', 'spec', 'flux_points', 'components', 'associations', 'id'}
117
            Comma separated list of options
118
        """
119
        if info == "all":
120
            info = "basic,associations,id,map,spec,flux_points,components"
121
122
        ss = ""
123
        ops = info.split(",")
124
        if "basic" in ops:
125
            ss += self._info_basic()
126
        if "map" in ops:
127
            ss += self._info_map()
128
        if "spec" in ops:
129
            ss += self._info_spec()
130
        if "flux_points" in ops:
131
            ss += self._info_flux_points()
132
        if "components" in ops and hasattr(self, "components"):
133
            ss += self._info_components()
134
        if "associations" in ops:
135
            ss += self._info_associations()
136
        if "id" in ops and hasattr(self, "identification_info"):
137
            ss += self._info_id()
138
        return ss
139
140
    def _info_basic(self):
141
        """Print basic info."""
142
        d = self.data
143
        ss = "\n*** Basic info ***\n\n"
144
        ss += "Catalog row index (zero-based) : {}\n".format(self.row_index)
145
        ss += "{:<20s} : {}\n".format("Source name", self.name)
146
147
        ss += "{:<20s} : {}\n".format("Analysis reference", d["Analysis_Reference"])
148
        ss += "{:<20s} : {}\n".format("Source class", d["Source_Class"])
149
        ss += "{:<20s} : {}\n".format("Identified object", d["Identified_Object"])
150
        ss += "{:<20s} : {}\n".format("Gamma-Cat id", d["Gamma_Cat_Source_ID"])
151
        ss += "\n"
152
        return ss
153
154
    def _info_associations(self):
155
        ss = "\n*** Source associations info ***\n\n"
156
        lines = self.associations.pformat(max_width=-1, max_lines=-1)
157
        ss += "\n".join(lines)
158
        return ss + "\n"
159
160
    def _info_id(self):
161
        ss = "\n*** Source identification info ***\n\n"
162
        ss += "\n".join(f"{k}: {v}" for k, v in self.identification_info.items())
163
        return ss + "\n"
164
165
    def _info_map(self):
166
        """Print info from map analysis."""
167
        d = self.data
168
        ss = "\n*** Info from map analysis ***\n\n"
169
170
        ra_str = Angle(d["RAJ2000"], "deg").to_string(unit="hour", precision=0)
171
        dec_str = Angle(d["DEJ2000"], "deg").to_string(unit="deg", precision=0)
172
        ss += "{:<20s} : {:8.3f} = {}\n".format("RA", d["RAJ2000"], ra_str)
173
        ss += "{:<20s} : {:8.3f} = {}\n".format("DEC", d["DEJ2000"], dec_str)
174
175
        ss += "{:<20s} : {:8.3f} +/- {:.3f} deg\n".format(
176
            "GLON", d["GLON"].value, d["GLON_Err"].value
177
        )
178
        ss += "{:<20s} : {:8.3f} +/- {:.3f} deg\n".format(
179
            "GLAT", d["GLAT"].value, d["GLAT_Err"].value
180
        )
181
182
        ss += "{:<20s} : {:.3f}\n".format("Position Error (68%)", d["Pos_Err_68"])
183
        ss += "{:<20s} : {:.3f}\n".format("Position Error (95%)", d["Pos_Err_95"])
184
185
        ss += "{:<20s} : {:.0f}\n".format("ROI number", d["ROI_Number"])
186
        ss += "{:<20s} : {}\n".format("Spatial model", d["Spatial_Model"])
187
        ss += "{:<20s} : {}\n".format("Spatial components", d["Components"])
188
189
        ss += "{:<20s} : {:.1f}\n".format("TS", d["Sqrt_TS"] ** 2)
190
        ss += "{:<20s} : {:.1f}\n".format("sqrt(TS)", d["Sqrt_TS"])
191
192
        ss += "{:<20s} : {:.3f} +/- {:.3f} (UL: {:.3f}) deg\n".format(
193
            "Size", d["Size"].value, d["Size_Err"].value, d["Size_UL"].value
194
        )
195
196
        ss += "{:<20s} : {:.3f}\n".format("R70", d["R70"])
197
        ss += "{:<20s} : {:.3f}\n".format("RSpec", d["RSpec"])
198
199
        ss += "{:<20s} : {:.1f}\n".format("Total model excess", d["Excess_Model_Total"])
200
        ss += "{:<20s} : {:.1f}\n".format("Excess in RSpec", d["Excess_RSpec"])
201
        ss += "{:<20s} : {:.1f}\n".format(
202
            "Model Excess in RSpec", d["Excess_RSpec_Model"]
203
        )
204
        ss += "{:<20s} : {:.1f}\n".format("Background in RSpec", d["Background_RSpec"])
205
206
        ss += "{:<20s} : {:.1f} hours\n".format("Livetime", d["Livetime"].value)
207
208
        ss += "{:<20s} : {:.2f}\n".format("Energy threshold", d["Energy_Threshold"])
209
210
        val, err = d["Flux_Map"].value, d["Flux_Map_Err"].value
211
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 cm^-2 s^-1 = ({:.2f} +/- {:.2f}) % Crab\n".format(  # noqa: 501
212
            "Source flux (>1 TeV)",
213
            val / FF,
214
            err / FF,
215
            val * FLUX_TO_CRAB,
216
            err * FLUX_TO_CRAB,
217
        )
218
219
        ss += "\nFluxes in RSpec (> 1 TeV):\n"
220
221
        ss += "{:<30s} : {:.3f} x 10^-12 cm^-2 s^-1 = {:5.2f} % Crab\n".format(
222
            "Map measurement",
223
            d["Flux_Map_RSpec_Data"].value / FF,
224
            d["Flux_Map_RSpec_Data"].value * FLUX_TO_CRAB,
225
        )
226
227
        ss += "{:<30s} : {:.3f} x 10^-12 cm^-2 s^-1 = {:5.2f} % Crab\n".format(
228
            "Source model",
229
            d["Flux_Map_RSpec_Source"].value / FF,
230
            d["Flux_Map_RSpec_Source"].value * FLUX_TO_CRAB,
231
        )
232
233
        ss += "{:<30s} : {:.3f} x 10^-12 cm^-2 s^-1 = {:5.2f} % Crab\n".format(
234
            "Other component model",
235
            d["Flux_Map_RSpec_Other"].value / FF,
236
            d["Flux_Map_RSpec_Other"].value * FLUX_TO_CRAB,
237
        )
238
239
        ss += "{:<30s} : {:.3f} x 10^-12 cm^-2 s^-1 = {:5.2f} % Crab\n".format(
240
            "Large scale component model",
241
            d["Flux_Map_RSpec_LS"].value / FF,
242
            d["Flux_Map_RSpec_LS"].value * FLUX_TO_CRAB,
243
        )
244
245
        ss += "{:<30s} : {:.3f} x 10^-12 cm^-2 s^-1 = {:5.2f} % Crab\n".format(
246
            "Total model",
247
            d["Flux_Map_RSpec_Total"].value / FF,
248
            d["Flux_Map_RSpec_Total"].value * FLUX_TO_CRAB,
249
        )
250
251
        ss += "{:<35s} : {:5.1f} %\n".format(
252
            "Containment in RSpec", 100 * d["Containment_RSpec"]
253
        )
254
        ss += "{:<35s} : {:5.1f} %\n".format(
255
            "Contamination in RSpec", 100 * d["Contamination_RSpec"]
256
        )
257
        label, val = (
258
            "Flux correction (RSpec -> Total)",
259
            100 * d["Flux_Correction_RSpec_To_Total"],
260
        )
261
        ss += f"{label:<35s} : {val:5.1f} %\n"
262
        label, val = (
263
            "Flux correction (Total -> RSpec)",
264
            100 * (1 / d["Flux_Correction_RSpec_To_Total"]),
265
        )
266
        ss += f"{label:<35s} : {val:5.1f} %\n"
267
268
        return ss
269
270
    def _info_spec(self):
271
        """Print info from spectral analysis."""
272
        d = self.data
273
        ss = "\n*** Info from spectral analysis ***\n\n"
274
275
        ss += "{:<20s} : {:.1f} hours\n".format("Livetime", d["Livetime_Spec"].value)
276
277
        lo = d["Energy_Range_Spec_Min"].value
278
        hi = d["Energy_Range_Spec_Max"].value
279
        ss += "{:<20s} : {:.2f} to {:.2f} TeV\n".format("Energy range:", lo, hi)
280
281
        ss += "{:<20s} : {:.1f}\n".format("Background", d["Background_Spec"])
282
        ss += "{:<20s} : {:.1f}\n".format("Excess", d["Excess_Spec"])
283
        ss += "{:<20s} : {}\n".format("Spectral model", d["Spectral_Model"])
284
285
        val = d["TS_ECPL_over_PL"]
286
        ss += "{:<20s} : {:.1f}\n".format("TS ECPL over PL", val)
287
288
        val = d["Flux_Spec_Int_1TeV"].value
289
        err = d["Flux_Spec_Int_1TeV_Err"].value
290
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 cm^-2 s^-1  = ({:.2f} +/- {:.2f}) % Crab\n".format(  # noqa: E501
291
            "Best-fit model flux(> 1 TeV)",
292
            val / FF,
293
            err / FF,
294
            val * FLUX_TO_CRAB,
295
            err * FLUX_TO_CRAB,
296
        )
297
298
        val = d["Flux_Spec_Energy_1_10_TeV"].value
299
        err = d["Flux_Spec_Energy_1_10_TeV_Err"].value
300
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 erg cm^-2 s^-1\n".format(
301
            "Best-fit model energy flux(1 to 10 TeV)", val / FF, err / FF
302
        )
303
304
        ss += self._info_spec_pl()
305
        ss += self._info_spec_ecpl()
306
307
        return ss
308
309
    def _info_spec_pl(self):
310
        d = self.data
311
        ss = "{:<20s} : {:.2f}\n".format("Pivot energy", d["Energy_Spec_PL_Pivot"])
312
313
        val = d["Flux_Spec_PL_Diff_Pivot"].value
314
        err = d["Flux_Spec_PL_Diff_Pivot_Err"].value
315
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 cm^-2 s^-1 TeV^-1  = ({:.2f} +/- {:.2f}) % Crab\n".format(  # noqa: E501
316
            "Flux at pivot energy",
317
            val / FF,
318
            err / FF,
319
            val * FLUX_TO_CRAB,
320
            err * FLUX_TO_CRAB_DIFF,
321
        )
322
323
        val = d["Flux_Spec_PL_Int_1TeV"].value
324
        err = d["Flux_Spec_PL_Int_1TeV_Err"].value
325
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 cm^-2 s^-1  = ({:.2f} +/- {:.2f}) % Crab\n".format(  # noqa: E501
326
            "PL   Flux(> 1 TeV)",
327
            val / FF,
328
            err / FF,
329
            val * FLUX_TO_CRAB,
330
            err * FLUX_TO_CRAB,
331
        )
332
333
        val = d["Flux_Spec_PL_Diff_1TeV"].value
334
        err = d["Flux_Spec_PL_Diff_1TeV_Err"].value
335
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 cm^-2 s^-1 TeV^-1  = ({:.2f} +/- {:.2f}) % Crab\n".format(  # noqa: E501
336
            "PL   Flux(@ 1 TeV)",
337
            val / FF,
338
            err / FF,
339
            val * FLUX_TO_CRAB,
340
            err * FLUX_TO_CRAB_DIFF,
341
        )
342
343
        val = d["Index_Spec_PL"]
344
        err = d["Index_Spec_PL_Err"]
345
        ss += "{:<20s} : {:.2f} +/- {:.2f}\n".format("PL   Index", val, err)
346
347
        return ss
348
349
    def _info_spec_ecpl(self):
350
        d = self.data
351
        ss = ""
352
        # ss = '{:<20s} : {:.1f}\n'.format('Pivot energy', d['Energy_Spec_ECPL_Pivot'])
353
354
        val = d["Flux_Spec_ECPL_Diff_1TeV"].value
355
        err = d["Flux_Spec_ECPL_Diff_1TeV_Err"].value
356
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 cm^-2 s^-1 TeV^-1  = ({:.2f} +/- {:.2f}) % Crab\n".format(  # noqa: E501
357
            "ECPL   Flux(@ 1 TeV)",
358
            val / FF,
359
            err / FF,
360
            val * FLUX_TO_CRAB,
361
            err * FLUX_TO_CRAB_DIFF,
362
        )
363
364
        val = d["Flux_Spec_ECPL_Int_1TeV"].value
365
        err = d["Flux_Spec_ECPL_Int_1TeV_Err"].value
366
        ss += "{:<20s} : ({:.3f} +/- {:.3f}) x 10^-12 cm^-2 s^-1  = ({:.2f} +/- {:.2f}) % Crab\n".format(  # noqa: E501
367
            "ECPL   Flux(> 1 TeV)",
368
            val / FF,
369
            err / FF,
370
            val * FLUX_TO_CRAB,
371
            err * FLUX_TO_CRAB,
372
        )
373
374
        val = d["Index_Spec_ECPL"]
375
        err = d["Index_Spec_ECPL_Err"]
376
        ss += "{:<20s} : {:.2f} +/- {:.2f}\n".format("ECPL Index", val, err)
377
378
        val = d["Lambda_Spec_ECPL"].value
379
        err = d["Lambda_Spec_ECPL_Err"].value
380
        ss += "{:<20s} : {:.3f} +/- {:.3f} TeV^-1\n".format("ECPL Lambda", val, err)
381
382
        # Use Gaussian analytical error propagation,
383
        # tested against the uncertainties package
384
        err = err / val**2
385
        val = 1.0 / val
386
387
        ss += "{:<20s} : {:.2f} +/- {:.2f} TeV\n".format("ECPL E_cut", val, err)
388
389
        return ss
390
391
    def _info_flux_points(self):
392
        """Print flux point results"""
393
        d = self.data
394
        ss = "\n*** Flux points info ***\n\n"
395
        ss += "Number of flux points: {}\n".format(d["N_Flux_Points"])
396
        ss += "Flux points table: \n\n"
397
        lines = format_flux_points_table(self.flux_points_table).pformat(
398
            max_width=-1, max_lines=-1
399
        )
400
        ss += "\n".join(lines)
401
        return ss + "\n"
402
403
    def _info_components(self):
404
        """Print info about the components."""
405
        ss = "\n*** Gaussian component info ***\n\n"
406
        ss += "Number of components: {}\n".format(len(self.components))
407
        ss += "{:<20s} : {}\n\n".format("Spatial components", self.data["Components"])
408
409
        for component in self.components:
410
            ss += str(component)
411
            ss += "\n\n"
412
413
        return ss
414
415
    @property
416
    def energy_range(self):
417
        """Spectral model energy range (`~astropy.units.Quantity` with length 2)."""
418
        energy_min, energy_max = (
419
            self.data["Energy_Range_Spec_Min"],
420
            self.data["Energy_Range_Spec_Max"],
421
        )
422
423
        if np.isnan(energy_min):
424
            energy_min = u.Quantity(0.2, "TeV")
425
426
        if np.isnan(energy_max):
427
            energy_max = u.Quantity(50, "TeV")
428
429
        return u.Quantity([energy_min, energy_max], "TeV")
430
431
    def spectral_model(self, which="best"):
432
        """Spectral model (`~gammapy.modeling.models.SpectralModel`).
433
434
        One of the following models (given by ``Spectral_Model`` in the catalog):
435
436
        - ``PL`` : `~gammapy.modeling.models.PowerLawSpectralModel`
437
        - ``ECPL`` : `~gammapy.modeling.models.ExpCutoffPowerLawSpectralModel`
438
439
        Parameters
440
        ----------
441
        which : {'best', 'pl', 'ecpl'}
442
            Which spectral model
443
        """
444
        data = self.data
445
446
        if which == "best":
447
            spec_type = self.data["Spectral_Model"].strip().lower()
448
        elif which in {"pl", "ecpl"}:
449
            spec_type = which
450
        else:
451
            raise ValueError(f"Invalid selection: which = {which!r}")
452
453 View Code Duplication
        if spec_type == "pl":
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
454
            tag = "PowerLawSpectralModel"
455
            pars = {
456
                "index": data["Index_Spec_PL"],
457
                "amplitude": data["Flux_Spec_PL_Diff_Pivot"],
458
                "reference": data["Energy_Spec_PL_Pivot"],
459
            }
460
            errs = {
461
                "amplitude": data["Flux_Spec_PL_Diff_Pivot_Err"],
462
                "index": data["Index_Spec_PL_Err"],
463
            }
464
        elif spec_type == "ecpl":
465
            tag = "ExpCutoffPowerLawSpectralModel"
466
            pars = {
467
                "index": data["Index_Spec_ECPL"],
468
                "amplitude": data["Flux_Spec_ECPL_Diff_Pivot"],
469
                "reference": data["Energy_Spec_ECPL_Pivot"],
470
                "lambda_": data["Lambda_Spec_ECPL"],
471
            }
472
            errs = {
473
                "index": data["Index_Spec_ECPL_Err"],
474
                "amplitude": data["Flux_Spec_ECPL_Diff_Pivot_Err"],
475
                "lambda_": data["Lambda_Spec_ECPL_Err"],
476
            }
477
        else:
478
            raise ValueError(f"Invalid spec_type: {spec_type}")
479
480
        model = Model.create(tag, "spectral", **pars)
481
        errs["reference"] = 0 * u.TeV
482
483
        for name, value in errs.items():
484
            model.parameters[name].error = value
485
486
        return model
487
488
    def spatial_model(self):
489
        """Spatial model (`~gammapy.modeling.models.SpatialModel`).
490
491
        One of the following models (given by ``Spatial_Model`` in the catalog):
492
493
        - ``Point-Like`` or has a size upper limit : `~gammapy.modeling.models.PointSpatialModel`
494
        - ``Gaussian``: `~gammapy.modeling.models.GaussianSpatialModel`
495
        - ``2-Gaussian`` or ``3-Gaussian``: composite model (using ``+`` with Gaussians)
496
        - ``Shell``: `~gammapy.modeling.models.ShellSpatialModel`
497
        """
498
        d = self.data
499
        pars = {"lon_0": d["GLON"], "lat_0": d["GLAT"], "frame": "galactic"}
500
        errs = {"lon_0": d["GLON_Err"], "lat_0": d["GLAT_Err"]}
501
502
        spatial_type = self.data["Spatial_Model"]
503
504
        if spatial_type == "Point-Like":
505
            tag = "PointSpatialModel"
506
        elif spatial_type == "Gaussian":
507
            tag = "GaussianSpatialModel"
508
            pars["sigma"] = d["Size"]
509
            errs["sigma"] = d["Size_Err"]
510
        elif spatial_type in {"2-Gaussian", "3-Gaussian"}:
511
            raise ValueError("For Gaussian or Multi-Gaussian models, use sky_model()!")
512
        elif spatial_type == "Shell":
513
            # HGPS contains no information on shell width
514
            # Here we assume a 5% shell width for all shells.
515
            tag = "ShellSpatialModel"
516
            pars["radius"] = 0.95 * d["Size"]
517
            pars["width"] = d["Size"] - pars["radius"]
518
            errs["radius"] = d["Size_Err"]
519
        else:
520
            raise ValueError(f"Invalid spatial_type: {spatial_type}")
521
522
        model = Model.create(tag, "spatial", **pars)
523
        for name, value in errs.items():
524
            model.parameters[name].error = value
525
        return model
526
527
    def sky_model(self, which="best"):
528
        """Source sky model.
529
530
        Parameters
531
        ----------
532
        which : {'best', 'pl', 'ecpl'}
533
            Which spectral model
534
535
        Returns
536
        -------
537
        sky_model : `~gammapy.modeling.models.Models`
538
           Models of the catalog object.
539
        """
540
541
        models = self.components_models(which=which)
542
        if len(models) > 1:
543
            geom = self._get_components_geom(models)
544
            return models.to_template_sky_model(geom=geom, name=self.name)
545
        else:
546
            return models[0]
547
548
    def components_models(self, which="best", linked=False):
549
        """Models of the source components.
550
551
        Parameters
552
        ----------
553
        which : {'best', 'pl', 'ecpl'}
554
            Which spectral model
555
556
        linked : bool
557
             Each sub-component of a source is given as a different `SkyModel`
558
             If True the spectral parameters except the mormalisation are linked.
559
             Default is False
560
561
        Returns
562
        -------
563
        sky_model : `~gammapy.modeling.models.Models`
564
           Models of the catalog object.
565
        """
566
567
        spatial_type = self.data["Spatial_Model"]
568
        missing_size = (
569
            spatial_type == "Gaussian" and self.spatial_model().sigma.value == 0
570
        )
571
        if spatial_type in {"2-Gaussian", "3-Gaussian"} or missing_size:
572
            models = []
573
            spectral_model = self.spectral_model(which=which)
574
            for component in self.components:
575
                spec_component = spectral_model.copy()
576
                weight = component.data["Flux_Map"] / self.data["Flux_Map"]
577
                spec_component.parameters["amplitude"].value *= weight
578
                if linked:
579
                    for name in spec_component.parameters.names:
580
                        if name not in ["norm", "amplitude"]:
581
                            spec_component.__dict__[name] = spectral_model.parameters[
582
                                name
583
                            ]
584
                model = SkyModel(
585
                    spatial_model=component.spatial_model(),
586
                    spectral_model=spec_component,
587
                    name=component.name,
588
                )
589
                models.append(model)
590
        else:
591
            models = [
592
                SkyModel(
593
                    spatial_model=self.spatial_model(),
594
                    spectral_model=self.spectral_model(which=which),
595
                    name=self.name,
596
                )
597
            ]
598
        return Models(models)
599
600
    @staticmethod
601
    def _get_components_geom(models):
602
        energy_axis = MapAxis.from_energy_bounds(
603
            "100 GeV", "100 TeV", nbin=10, per_decade=True, name="energy_true"
604
        )
605
        regions = [m.spatial_model.evaluation_region for m in models]
606
        geom = RegionGeom.from_regions(
607
            regions, binsz_wcs="0.02 deg", axes=[energy_axis]
608
        )
609
        return geom.to_wcs_geom()
610
611
    @property
612
    def flux_points_table(self):
613
        """Flux points table (`~astropy.table.Table`)."""
614
        table = Table()
615
        table.meta["sed_type_init"] = "dnde"
616
        table.meta["n_sigma_ul"] = 2
617
        table.meta["n_sigma"] = 1
618
        table.meta["sqrt_ts_threshold_ul"] = 1
619
        mask = ~np.isnan(self.data["Flux_Points_Energy"])
620
621
        table["e_ref"] = self.data["Flux_Points_Energy"][mask]
622
        table["e_min"] = self.data["Flux_Points_Energy_Min"][mask]
623
        table["e_max"] = self.data["Flux_Points_Energy_Max"][mask]
624
625
        table["dnde"] = self.data["Flux_Points_Flux"][mask]
626
        table["dnde_errn"] = self.data["Flux_Points_Flux_Err_Lo"][mask]
627
        table["dnde_errp"] = self.data["Flux_Points_Flux_Err_Hi"][mask]
628
        table["dnde_ul"] = self.data["Flux_Points_Flux_UL"][mask]
629
        table["is_ul"] = self.data["Flux_Points_Flux_Is_UL"][mask].astype("bool")
630
        return table
631
632
633
class SourceCatalogHGPS(SourceCatalog):
634
    """HESS Galactic plane survey (HGPS) source catalog.
635
636
    Reference: https://www.mpi-hd.mpg.de/hfm/HESS/hgps/
637
638
    One source is represented by `SourceCatalogObjectHGPS`.
639
640
    Examples
641
    --------
642
    Let's assume you have downloaded the HGPS catalog FITS file, e.g. via:
643
644
    .. code-block:: bash
645
646
        curl -O https://www.mpi-hd.mpg.de/hfm/HESS/hgps/data/hgps_catalog_v1.fits.gz
647
648
    Then you can load it up like this:
649
650
    >>> import matplotlib.pyplot as plt
651
    >>> from gammapy.catalog import SourceCatalogHGPS
652
    >>> filename = '$GAMMAPY_DATA/catalogs/hgps_catalog_v1.fits.gz'
653
    >>> cat = SourceCatalogHGPS(filename)
654
655
    Access a source by name:
656
657
    >>> source = cat['HESS J1843-033']
658
    >>> print(source)
659
    <BLANKLINE>
660
    *** Basic info ***
661
    <BLANKLINE>
662
    Catalog row index (zero-based) : 64
663
    Source name          : HESS J1843-033
664
    Analysis reference   : HGPS
665
    Source class         : Unid
666
    Identified object    : --
667
    Gamma-Cat id         : 126
668
    <BLANKLINE>
669
    <BLANKLINE>
670
    *** Info from map analysis ***
671
    <BLANKLINE>
672
    RA                   :  280.952 deg = 18h43m48s
673
    DEC                  :   -3.554 deg = -3d33m15s
674
    GLON                 :   28.899 +/- 0.072 deg
675
    GLAT                 :    0.075 +/- 0.036 deg
676
    Position Error (68%) : 0.122 deg
677
    Position Error (95%) : 0.197 deg
678
    ROI number           : 3
679
    Spatial model        : 2-Gaussian
680
    Spatial components   : HGPSC 083, HGPSC 084
681
    TS                   : 256.6
682
    sqrt(TS)             : 16.0
683
    Size                 : 0.239 +/- 0.063 (UL: 0.000) deg
684
    R70                  : 0.376 deg
685
    RSpec                : 0.376 deg
686
    Total model excess   : 979.5
687
    Excess in RSpec      : 775.6
688
    Model Excess in RSpec : 690.2
689
    Background in RSpec  : 1742.4
690
    Livetime             : 41.8 hours
691
    Energy threshold     : 0.38 TeV
692
    Source flux (>1 TeV) : (2.882 +/- 0.305) x 10^-12 cm^-2 s^-1 = (12.75 +/- 1.35) % Crab
693
    <BLANKLINE>
694
    Fluxes in RSpec (> 1 TeV):
695
    Map measurement                : 2.267 x 10^-12 cm^-2 s^-1 = 10.03 % Crab
696
    Source model                   : 2.018 x 10^-12 cm^-2 s^-1 =  8.93 % Crab
697
    Other component model          : 0.004 x 10^-12 cm^-2 s^-1 =  0.02 % Crab
698
    Large scale component model    : 0.361 x 10^-12 cm^-2 s^-1 =  1.60 % Crab
699
    Total model                    : 2.383 x 10^-12 cm^-2 s^-1 = 10.54 % Crab
700
    Containment in RSpec                :  70.0 %
701
    Contamination in RSpec              :  15.3 %
702
    Flux correction (RSpec -> Total)    : 121.0 %
703
    Flux correction (Total -> RSpec)    :  82.7 %
704
    <BLANKLINE>
705
    *** Info from spectral analysis ***
706
    <BLANKLINE>
707
    Livetime             : 16.8 hours
708
    Energy range:        : 0.22 to 61.90 TeV
709
    Background           : 5126.9
710
    Excess               : 980.1
711
    Spectral model       : PL
712
    TS ECPL over PL      : --
713
    Best-fit model flux(> 1 TeV) : (3.043 +/- 0.196) x 10^-12 cm^-2 s^-1  = (13.47 +/- 0.87) % Crab
714
    Best-fit model energy flux(1 to 10 TeV) : (10.918 +/- 0.733) x 10^-12 erg cm^-2 s^-1
715
    Pivot energy         : 1.87 TeV
716
    Flux at pivot energy : (0.914 +/- 0.058) x 10^-12 cm^-2 s^-1 TeV^-1  = (4.04 +/- 0.17) % Crab
717
    PL   Flux(> 1 TeV)   : (3.043 +/- 0.196) x 10^-12 cm^-2 s^-1  = (13.47 +/- 0.87) % Crab
718
    PL   Flux(@ 1 TeV)   : (3.505 +/- 0.247) x 10^-12 cm^-2 s^-1 TeV^-1  = (15.51 +/- 0.70) % Crab
719
    PL   Index           : 2.15 +/- 0.05
720
    ECPL   Flux(@ 1 TeV) : (0.000 +/- 0.000) x 10^-12 cm^-2 s^-1 TeV^-1  = (0.00 +/- 0.00) % Crab
721
    ECPL   Flux(> 1 TeV) : (0.000 +/- 0.000) x 10^-12 cm^-2 s^-1  = (0.00 +/- 0.00) % Crab
722
    ECPL Index           : -- +/- --
723
    ECPL Lambda          : 0.000 +/- 0.000 TeV^-1
724
    ECPL E_cut           : inf +/- nan TeV
725
    <BLANKLINE>
726
    *** Flux points info ***
727
    <BLANKLINE>
728
    Number of flux points: 6
729
    Flux points table:
730
    <BLANKLINE>
731
    e_ref  e_min  e_max        dnde         dnde_errn       dnde_errp        dnde_ul     is_ul
732
     TeV    TeV    TeV   1 / (cm2 s TeV) 1 / (cm2 s TeV) 1 / (cm2 s TeV) 1 / (cm2 s TeV)
733
    ------ ------ ------ --------------- --------------- --------------- --------------- -----
734
     0.332  0.215  0.511       3.048e-11       6.890e-12       7.010e-12       4.455e-11 False
735
     0.787  0.511  1.212       5.383e-12       6.655e-13       6.843e-13       6.739e-12 False
736
     1.957  1.212  3.162       9.160e-13       9.732e-14       1.002e-13       1.120e-12 False
737
     4.870  3.162  7.499       1.630e-13       2.001e-14       2.097e-14       2.054e-13 False
738
    12.115  7.499 19.573       1.648e-14       3.124e-15       3.348e-15       2.344e-14 False
739
    30.142 19.573 46.416       7.777e-16       4.468e-16       5.116e-16       1.883e-15 False
740
    <BLANKLINE>
741
    *** Gaussian component info ***
742
    <BLANKLINE>
743
    Number of components: 2
744
    Spatial components   : HGPSC 083, HGPSC 084
745
    <BLANKLINE>
746
    Component HGPSC 083:
747
    GLON                 :   29.047 +/- 0.026 deg
748
    GLAT                 :    0.244 +/- 0.027 deg
749
    Size                 : 0.125 +/- 0.021 deg
750
    Flux (>1 TeV)        : (1.34 +/- 0.36) x 10^-12 cm^-2 s^-1 = (5.9 +/- 1.6) % Crab
751
    <BLANKLINE>
752
    Component HGPSC 084:
753
    GLON                 :   28.770 +/- 0.059 deg
754
    GLAT                 :   -0.073 +/- 0.069 deg
755
    Size                 : 0.229 +/- 0.046 deg
756
    Flux (>1 TeV)        : (1.54 +/- 0.47) x 10^-12 cm^-2 s^-1 = (6.8 +/- 2.1) % Crab
757
    <BLANKLINE>
758
    <BLANKLINE>
759
    *** Source associations info ***
760
    <BLANKLINE>
761
      Source_Name    Association_Catalog    Association_Name   Separation
762
                                                                  deg
763
    ---------------- ------------------- --------------------- ----------
764
      HESS J1843-033                3FGL     3FGL J1843.7-0322   0.178442
765
      HESS J1843-033                3FGL     3FGL J1844.3-0344   0.242835
766
      HESS J1843-033                 SNR             G28.6-0.1   0.330376
767
    <BLANKLINE>
768
769
    Access source spectral data and plot it:
770
771
    >>> ax = plt.subplot()
772
    >>> source.spectral_model().plot(source.energy_range, ax=ax) #doctest:+ELLIPSIS
773
    <AxesSubplot:...xlabel='Energy [TeV]', ylabel='dnde [1 / (cm2 s TeV)]'>
774
    >>> source.spectral_model().plot_error(source.energy_range, ax=ax) #doctest:+ELLIPSIS
775
    <AxesSubplot:...xlabel='Energy [TeV]', ylabel='dnde [1 / (cm2 s TeV)]'>
776
    >>> source.flux_points.plot(ax=ax) #doctest:+ELLIPSIS
777
    <AxesSubplot:...xlabel='Energy [TeV]', ylabel='dnde [1 / (cm2 s TeV)]'>
778
779
    Gaussian component information can be accessed as well,
780
    either via the source, or via the catalog:
781
782
    >>> source.components
783
    [SourceCatalogObjectHGPSComponent('HGPSC 083'), SourceCatalogObjectHGPSComponent('HGPSC 084')]
784
785
    >>> cat.gaussian_component(83)
786
    SourceCatalogObjectHGPSComponent('HGPSC 084')
787
    """
788
789
    tag = "hgps"
790
    """Source catalog name (str)."""
791
792
    description = "H.E.S.S. Galactic plane survey (HGPS) source catalog"
793
    """Source catalog description (str)."""
794
795
    source_object_class = SourceCatalogObjectHGPS
796
797
    def __init__(
798
        self,
799
        filename="$GAMMAPY_DATA/catalogs/hgps_catalog_v1.fits.gz",
800
        hdu="HGPS_SOURCES",
801
    ):
802
        filename = make_path(filename)
803
        table = Table.read(filename, hdu=hdu)
804
805
        source_name_alias = ("Identified_Object",)
806
        super().__init__(table=table, source_name_alias=source_name_alias)
807
808
        self._table_components = Table.read(filename, hdu="HGPS_GAUSS_COMPONENTS")
809
        self._table_associations = Table.read(filename, hdu="HGPS_ASSOCIATIONS")
810
        self._table_associations["Separation"].format = ".6f"
811
        self._table_identifications = Table.read(filename, hdu="HGPS_IDENTIFICATIONS")
812
        self._table_large_scale_component = Table.read(
813
            filename, hdu="HGPS_LARGE_SCALE_COMPONENT"
814
        )
815
816
    @property
817
    def table_components(self):
818
        """Gaussian component table (`~astropy.table.Table`)"""
819
        return self._table_components
820
821
    @property
822
    def table_associations(self):
823
        """Source association table (`~astropy.table.Table`)"""
824
        return self._table_associations
825
826
    @property
827
    def table_identifications(self):
828
        """Source identification table (`~astropy.table.Table`)"""
829
        return self._table_identifications
830
831
    @property
832
    def table_large_scale_component(self):
833
        """Large scale component table (`~astropy.table.Table`)"""
834
        return self._table_large_scale_component
835
836
    @property
837
    def large_scale_component(self):
838
        """Large scale component model (`~gammapy.catalog.SourceCatalogLargeScaleHGPS`)."""
839
        return SourceCatalogLargeScaleHGPS(self.table_large_scale_component)
840
841
    def _make_source_object(self, index):
842
        """Make `SourceCatalogObject` for given row index"""
843
        source = super()._make_source_object(index)
844
845
        if source.data["Components"] != "":
846
            source.components = list(self._get_gaussian_components(source))
847
848
        self._attach_association_info(source)
849
850
        if source.data["Source_Class"] != "Unid":
851
            self._attach_identification_info(source)
852
853
        return source
854
855
    def _get_gaussian_components(self, source):
856
        for name in source.data["Components"].split(", "):
857
            row_index = int(name.split()[-1]) - 1
858
            yield self.gaussian_component(row_index)
859
860
    def _attach_association_info(self, source):
861
        t = self.table_associations
862
        mask = source.data["Source_Name"] == t["Source_Name"]
863
        source.associations = t[mask]
864
865
    def _attach_identification_info(self, source):
866
        t = self._table_identifications
867
        idx = np.nonzero(source.name == t["Source_Name"])[0][0]
868
        source.identification_info = table_row_to_dict(t[idx])
869
870
    def gaussian_component(self, row_idx):
871
        """Gaussian component (`SourceCatalogObjectHGPSComponent`)."""
872
        data = table_row_to_dict(self.table_components[row_idx])
873
        data[SourceCatalogObject._row_index_key] = row_idx
874
        return SourceCatalogObjectHGPSComponent(data=data)
875
876
    def to_models(self, which="best", components_status="independent"):
877
        """Create Models object from catalogue
878
879
        Parameters
880
        ----------
881
        which : {'best', 'pl', 'ecpl'}
882
            Which spectral model
883
884
        components_status : {'independent', 'linked', 'merged'}
885
            Relation between the sources components:
886
                'independent' : each sub-component of a source is given as
887
                                a different `SkyModel` (Default)
888
                'linked' : each sub-component of a source is given as
889
                           a different `SkyModel` but the spectral parameters
890
                           except the mormalisation are linked.
891
                'merged' : the sub-components are merged into a single `SkyModel`
892
                           given as a `~gammapy.modeling.models.TemplateSpatialModel`
893
                           with a `~gammapy.modeling.models.PowerLawNormSpectralModel`.
894
                           In that case the relavtie weights between the components
895
                           cannot be adjusted.
896
897
        Returns
898
        -------
899
        models : `~gammapy.modeling.models.Models`
900
            Models of the catalog.
901
        """
902
903
        models = []
904
        for source in self:
905
            if components_status == "merged":
906
                m = [source.sky_model(which=which)]
907
            else:
908
                m = source.components_models(
909
                    which=which, linked=components_status == "linked"
910
                )
911
            models.extend(m)
912
        return Models(models)
913
914
915
class SourceCatalogLargeScaleHGPS:
916
    """Gaussian band model.
917
918
    This 2-dimensional model is Gaussian in ``y`` for a given ``x``,
919
    and the Gaussian parameters can vary in ``x``.
920
921
    One application of this model is the diffuse emission along the
922
    Galactic plane, i.e. ``x = GLON`` and ``y = GLAT``.
923
924
    Parameters
925
    ----------
926
    table : `~astropy.table.Table`
927
        Table of Gaussian parameters.
928
        ``x``, ``amplitude``, ``mean``, ``stddev``.
929
    interp_kwargs : dict
930
        Keyword arguments passed to `ScaledRegularGridInterpolator`
931
    """
932
933
    def __init__(self, table, interp_kwargs=None):
934
        interp_kwargs = interp_kwargs or {}
935
        interp_kwargs.setdefault("values_scale", "lin")
936
937
        self.table = table
938
        glon = Angle(self.table["GLON"]).wrap_at("180d")
939
940
        interps = {}
941
942
        for column in table.colnames:
943
            values = self.table[column].quantity
944
            interp = ScaledRegularGridInterpolator((glon,), values, **interp_kwargs)
945
            interps[column] = interp
946
947
        self._interp = interps
948
949
    def _interpolate_parameter(self, parname, glon):
950
        glon = glon.wrap_at("180d")
951
        return self._interp[parname]((np.asanyarray(glon),), clip=False)
952
953
    def peak_brightness(self, glon):
954
        """Peak brightness at a given longitude.
955
956
        Parameters
957
        ----------
958
        glon : `~astropy.coordinates.Angle`
959
            Galactic Longitude.
960
        """
961
        return self._interpolate_parameter("Surface_Brightness", glon)
962
963
    def peak_brightness_error(self, glon):
964
        """Peak brightness error at a given longitude.
965
966
        Parameters
967
        ----------
968
        glon : `~astropy.coordinates.Angle`
969
            Galactic Longitude.
970
        """
971
        return self._interpolate_parameter("Surface_Brightness_Err", glon)
972
973
    def width(self, glon):
974
        """Width at a given longitude.
975
976
        Parameters
977
        ----------
978
        glon : `~astropy.coordinates.Angle`
979
            Galactic Longitude.
980
        """
981
        return self._interpolate_parameter("Width", glon)
982
983
    def width_error(self, glon):
984
        """Width error at a given longitude.
985
986
        Parameters
987
        ----------
988
        glon : `~astropy.coordinates.Angle`
989
            Galactic Longitude.
990
        """
991
        return self._interpolate_parameter("Width_Err", glon)
992
993
    def peak_latitude(self, glon):
994
        """Peak position at a given longitude.
995
996
        Parameters
997
        ----------
998
        glon : `~astropy.coordinates.Angle`
999
            Galactic Longitude.
1000
        """
1001
        return self._interpolate_parameter("GLAT", glon)
1002
1003
    def peak_latitude_error(self, glon):
1004
        """Peak position error at a given longitude.
1005
1006
        Parameters
1007
        ----------
1008
        glon : `~astropy.coordinates.Angle`
1009
            Galactic Longitude.
1010
        """
1011
        return self._interpolate_parameter("GLAT_Err", glon)
1012
1013
    def evaluate(self, position):
1014
        """Evaluate model at a given position.
1015
1016
        Parameters
1017
        ----------
1018
        position : `~astropy.coordinates.SkyCoord`
1019
            Position on the sky.
1020
        """
1021
        glon, glat = position.galactic.l, position.galactic.b
1022
        width = self.width(glon)
1023
        amplitude = self.peak_brightness(glon)
1024
        mean = self.peak_latitude(glon)
1025
        return Gaussian1D.evaluate(glat, amplitude=amplitude, mean=mean, stddev=width)
1026