Passed
Pull Request — master (#1898)
by
unknown
04:26
created

gammapy/catalog/fermi.py (1 issue)

1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
0 ignored issues
show
Too many lines in module (1282/1000)
Loading history...
2
"""Fermi catalog and source classes.
3
"""
4
from __future__ import absolute_import, division, print_function, unicode_literals
5
import warnings
6
import numpy as np
7
import astropy.units as u
8
from astropy.table import Table, Column
9
from astropy.time import Time
10
from ..utils.scripts import make_path
11
from ..utils.energy import EnergyBounds
12
from ..utils.table import table_standardise_units_inplace
13
from ..maps import Map
14
from ..spectrum import FluxPoints
15
from ..spectrum.models import (
16
    PowerLaw,
17
    PowerLaw2,
18
    ExponentialCutoffPowerLaw3FGL,
19
    PLSuperExpCutoff3FGL,
20
    LogParabola,
21
)
22
from ..image.models import SkyPointSource, SkyGaussian, SkyDisk, SkyDiffuseMap
23
from ..cube.models import SkyModel
24
from ..time import LightCurve
25
from .core import SourceCatalog, SourceCatalogObject
26
27
__all__ = [
28
    "SourceCatalogObject3FGL",
29
    "SourceCatalogObject1FHL",
30
    "SourceCatalogObject2FHL",
31
    "SourceCatalogObject3FHL",
32
    "SourceCatalog3FGL",
33
    "SourceCatalog1FHL",
34
    "SourceCatalog2FHL",
35
    "SourceCatalog3FHL",
36
]
37
38
39
def compute_flux_points_ul(quantity, quantity_errp):
40
    """Compute UL value for fermi flux points.
41
42
    See https://arxiv.org/pdf/1501.02003.pdf (page 30)
43
    """
44
    return 2 * quantity_errp + quantity
45
46
47
class SourceCatalogObject3FGL(SourceCatalogObject):
48
    """One source from the Fermi-LAT 3FGL catalog.
49
50
    Catalog is represented by `~gammapy.catalog.SourceCatalog3FGL`.
51
    """
52
53
    _ebounds = EnergyBounds([100, 300, 1000, 3000, 10000, 100000], "MeV")
54
    _ebounds_suffix = ["100_300", "300_1000", "1000_3000", "3000_10000", "10000_100000"]
55
    energy_range = u.Quantity([100, 100000], "MeV")
56
    """Energy range of the catalog.
57
58
    Paper says that analysis uses data up to 300 GeV,
59
    but results are all quoted up to 100 GeV only to
60
    be consistent with previous catalogs.
61
    """
62
63
    def __str__(self):
64
        return self.info()
65
66
    def info(self, info="all"):
67
        """Summary info string.
68
69
        Parameters
70
        ----------
71
        info : {'all', 'basic', 'position', 'spectral', 'lightcurve'}
72
            Comma separated list of options
73
        """
74
        if info == "all":
75
            info = "basic,position,spectral,lightcurve"
76
77
        ss = ""
78
        ops = info.split(",")
79
        if "basic" in ops:
80
            ss += self._info_basic()
81
        if "position" in ops:
82
            ss += self._info_position()
83
        if "spectral" in ops:
84
            ss += self._info_spectral_fit()
85
            ss += self._info_spectral_points()
86
        if "lightcurve" in ops:
87
            ss += self._info_lightcurve()
88
        return ss
89
90
    def _info_basic(self):
91
        """Print basic info."""
92
        d = self.data
93
        ss = "\n*** Basic info ***\n\n"
94
        ss += "Catalog row index (zero-based) : {}\n".format(d["catalog_row_index"])
95
        ss += "{:<20s} : {}\n".format("Source name", d["Source_Name"])
96
        ss += "{:<20s} : {}\n".format("Extended name", d["Extended_Source_Name"])
97
98
        def get_nonentry_keys(keys):
99
            vals = [d[_].strip() for _ in keys]
100
            return ", ".join([_ for _ in vals if _ != ""])
101
102
        keys = [
103
            "ASSOC1",
104
            "ASSOC2",
105
            "ASSOC_TEV",
106
            "ASSOC_GAM1",
107
            "ASSOC_GAM2",
108
            "ASSOC_GAM3",
109
        ]
110
        associations = get_nonentry_keys(keys)
111
        ss += "{:<20s} : {}\n".format("Associations", associations)
112
113
        keys = ["0FGL_Name", "1FGL_Name", "2FGL_Name", "1FHL_Name"]
114
        other_names = get_nonentry_keys(keys)
115
        ss += "{:<20s} : {}\n".format("Other names", other_names)
116
117
        ss += "{:<20s} : {}\n".format("Class", d["CLASS1"])
118
119
        tevcat_flag = d["TEVCAT_FLAG"]
120
        if tevcat_flag == "N":
121
            tevcat_message = "No TeV association"
122
        elif tevcat_flag == "P":
123
            tevcat_message = "Small TeV source"
124
        elif tevcat_flag == "E":
125
            tevcat_message = "Extended TeV source (diameter > 40 arcmins)"
126
        else:
127
            tevcat_message = "N/A"
128
        ss += "{:<20s} : {}\n".format("TeVCat flag", tevcat_message)
129
130
        flag_message = {
131
            0: "None",
132
            1: "Source with TS > 35 which went to TS < 25 when changing the diffuse model. Note that sources with TS < "
133
            "35 are not flagged with this bit because normal statistical fluctuations can push them to TS < 25.",
134
            3: "Flux (> 1 GeV) or energy flux (> 100 MeV) changed by more than 3 sigma when changing the diffuse model."
135
            " Requires also that the flux change by more than 35% (to not flag strong sources).",
136
            4: "Source-to-background ratio less than 10% in highest band in which TS > 25. Background is integrated "
137
            "over the 68%-confidence area (pi*r_682) or 1 square degree, whichever is smaller.",
138
            5: "Closer than theta_ref from a brighter neighbor, where theta_ref is defined in the highest band in which"
139
            " source TS > 25, or the band with highest TS if all are < 25. theta_ref is set to 2.17 degrees (FWHM)"
140
            " below 300 MeV, 1.38 degrees between 300 MeV and 1 GeV, 0.87 degrees between 1 GeV and 3 GeV, 0.67"
141
            " degrees between 3 and 10 GeV and 0.45 degrees about 10 GeV (2*r_68).",
142
            6: "On top of an interstellar gas clump or small-scale defect in the model of diffuse emission. This flag "
143
            'is equivalent to the "c" suffix in the source name.',
144
            7: "Unstable position determination; result from gtfindsrc outside the 95% ellipse from pointlike.",
145
            9: "Localization Quality > 8 in pointlike (see Section 3.1 in catalog paper) or long axis of 95% ellipse >"
146
            " 0.25.",
147
            10: "Spectral Fit Quality > 16.3 (see Equation 3 in 2FGL catalog paper).",
148
            11: "Possibly due to the Sun (see Section 3.6 in catalog paper).",
149
            12: "Highly curved spectrum; LogParabola beta fixed to 1 or PLExpCutoff Spectral Index fixed to 0 (see "
150
            "Section 3.3 in catalog paper).",
151
        }
152
        ss += "{:<20s} : {}\n".format(
153
            "Other flags", flag_message.get(d["Flags"], "N/A")
154
        )
155
156
        return ss
157
158
    def _info_position(self):
159
        """Print position info."""
160
        d = self.data
161
        ss = "\n*** Position info ***\n\n"
162
        ss += "{:<20s} : {:.3f}\n".format("RA", d["RAJ2000"])
163
        ss += "{:<20s} : {:.3f}\n".format("DEC", d["DEJ2000"])
164
        ss += "{:<20s} : {:.3f}\n".format("GLON", d["GLON"])
165
        ss += "{:<20s} : {:.3f}\n".format("GLAT", d["GLAT"])
166
167
        ss += "\n"
168
        ss += "{:<20s} : {:.4f}\n".format("Semimajor (68%)", d["Conf_68_SemiMajor"])
169
        ss += "{:<20s} : {:.4f}\n".format("Semiminor (68%)", d["Conf_68_SemiMinor"])
170
        ss += "{:<20s} : {:.2f}\n".format("Position angle (68%)", d["Conf_68_PosAng"])
171
        ss += "{:<20s} : {:.4f}\n".format("Semimajor (95%)", d["Conf_95_SemiMajor"])
172
        ss += "{:<20s} : {:.4f}\n".format("Semiminor (95%)", d["Conf_95_SemiMinor"])
173
        ss += "{:<20s} : {:.2f}\n".format("Position angle (95%)", d["Conf_95_PosAng"])
174
        ss += "{:<20s} : {:.0f}\n".format("ROI number", d["ROI_num"])
175
176
        return ss
177
178
    def _info_spectral_fit(self):
179
        """Print spectral info."""
180
        d = self.data
181
        spec_type = d["SpectrumType"].strip()
182
183
        ss = "\n*** Spectral info ***\n\n"
184
185
        ss += "{:<45s} : {}\n".format("Spectrum type", d["SpectrumType"])
186
        fmt = "{:<45s} : {:.3f}\n"
187
        ss += fmt.format("Detection significance (100 MeV - 300 GeV)", d["Signif_Avg"])
188
        ss += "{:<45s} : {:.1f}\n".format("Significance curvature", d["Signif_Curve"])
189
190
        if spec_type == "PowerLaw":
191
            pass
192
        elif spec_type == "LogParabola":
193
            ss += "{:<45s} : {} +- {}\n".format("beta", d["beta"], d["Unc_beta"])
194
        elif spec_type in ["PLExpCutoff", "PlSuperExpCutoff"]:
195
            fmt = "{:<45s} : {:.0f} +- {:.0f} {}\n"
196
            ss += fmt.format(
197
                "Cutoff energy",
198
                d["Cutoff"].value,
199
                d["Unc_Cutoff"].value,
200
                d["Cutoff"].unit,
201
            )
202
        elif spec_type == "PLSuperExpCutoff":
203
            ss += "{:<45s} : {} +- {}\n".format(
204
                "Super-exponential cutoff index", d["Exp_Index"], d["Unc_Exp_Index"]
205
            )
206
        else:
207
            raise ValueError("Invalid spec_type")
208
209
        ss += "{:<45s} : {:.0f} {}\n".format(
210
            "Pivot energy", d["Pivot_Energy"].value, d["Pivot_Energy"].unit
211
        )
212
213
        ss += "{:<45s} : {:.3f}\n".format(
214
            "Power law spectral index", d["PowerLaw_Index"]
215
        )
216
217
        fmt = "{:<45s} : {:.3f} +- {:.3f}\n"
218
        ss += fmt.format("Spectral index", d["Spectral_Index"], d["Unc_Spectral_Index"])
219
220
        fmt = "{:<45s} : {:.3} +- {:.3} {}\n"
221
        ss += fmt.format(
222
            "Flux Density at pivot energy",
223
            d["Flux_Density"].value,
224
            d["Unc_Flux_Density"].value,
225
            "cm-2 MeV-1 s-1",
226
        )
227
228
        fmt = "{:<45s} : {:.3} +- {:.3} {}\n"
229
        ss += fmt.format(
230
            "Integral flux (1 - 100 GeV)",
231
            d["Flux1000"].value,
232
            d["Unc_Flux1000"].value,
233
            "cm-2 s-1",
234
        )
235
236
        fmt = "{:<45s} : {:.3} +- {:.3} {}\n"
237
        ss += fmt.format(
238
            "Energy flux (100 MeV - 100 GeV)",
239
            d["Energy_Flux100"].value,
240
            d["Unc_Energy_Flux100"].value,
241
            "erg cm-2 s-1",
242
        )
243
244
        return ss
245
246
    def _info_spectral_points(self):
247
        """Print spectral points."""
248
        ss = "\n*** Spectral points ***\n\n"
249
        lines = self._flux_points_table_formatted.pformat(max_width=-1, max_lines=-1)
250
        ss += "\n".join(lines)
251
252
        return ss + "\n"
253
254
    def _info_lightcurve(self):
255
        """Print lightcurve info."""
256
        d = self.data
257
        ss = "\n*** Lightcurve info ***\n\n"
258
        ss += "Lightcurve measured in the energy band: 100 MeV - 100 GeV\n\n"
259
260
        ss += "{:<15s} : {:.3f}\n".format("Variability index", d["Variability_Index"])
261
262
        if d["Signif_Peak"] == np.nan:
263
            ss += "{:<40s} : {:.3f}\n".format(
264
                "Significance peak (100 MeV - 100 GeV)", d["Signif_Peak"]
265
            )
266
267
            fmt = "{:<40s} : {:.3} +- {:.3} cm^-2 s^-1\n"
268
            ss += fmt.format(
269
                "Integral flux peak (100 MeV - 100 GeV)",
270
                d["Flux_Peak"],
271
                d["Unc_Flux_Peak"],
272
            )
273
274
            # TODO: give time as UTC string, not MET
275
            ss += "{:<40s} : {:.3} s (Mission elapsed time)\n".format(
276
                "Time peak", d["Time_Peak"]
277
            )
278
            peak_interval = d["Peak_Interval"].to_value("day")
279
            ss += "{:<40s} : {:.3} day\n".format("Peak interval", peak_interval)
280
        else:
281
            ss += "\nNo peak measured for this source.\n"
282
283
        # TODO: Add a lightcurve table with d['Flux_History'] and d['Unc_Flux_History']
284
285
        return ss
286
287
    @property
288
    def spectral_model(self):
289
        """Best fit spectral model (`~gammapy.spectrum.models.SpectralModel`)."""
290
        spec_type = self.data["SpectrumType"].strip()
291
292
        pars, errs = {}, {}
293
        pars["amplitude"] = self.data["Flux_Density"]
294
        errs["amplitude"] = self.data["Unc_Flux_Density"]
295
        pars["reference"] = self.data["Pivot_Energy"]
296
297
        if spec_type == "PowerLaw":
298
            pars["index"] = self.data["Spectral_Index"]
299
            errs["index"] = self.data["Unc_Spectral_Index"]
300
            model = PowerLaw(**pars)
301
        elif spec_type == "PLExpCutoff":
302
            pars["index"] = self.data["Spectral_Index"]
303
            pars["ecut"] = self.data["Cutoff"]
304
            errs["index"] = self.data["Unc_Spectral_Index"]
305
            errs["ecut"] = self.data["Unc_Cutoff"]
306
            model = ExponentialCutoffPowerLaw3FGL(**pars)
307
        elif spec_type == "LogParabola":
308
            pars["alpha"] = self.data["Spectral_Index"]
309
            pars["beta"] = self.data["beta"]
310
            errs["alpha"] = self.data["Unc_Spectral_Index"]
311
            errs["beta"] = self.data["Unc_beta"]
312
            model = LogParabola(**pars)
313
        elif spec_type == "PLSuperExpCutoff":
314
            # TODO: why convert to GeV here? Remove?
315
            pars["reference"] = pars["reference"].to("GeV")
316
            pars["index_1"] = self.data["Spectral_Index"]
317
            pars["index_2"] = self.data["Exp_Index"]
318
            pars["ecut"] = self.data["Cutoff"].to("GeV")
319
            errs["index_1"] = self.data["Unc_Spectral_Index"]
320
            errs["index_2"] = self.data["Unc_Exp_Index"]
321
            errs["ecut"] = self.data["Unc_Cutoff"].to("GeV")
322
            model = PLSuperExpCutoff3FGL(**pars)
323
        else:
324
            raise ValueError("Invalid spec_type: {!r}".format(spec_type))
325
326
        model.parameters.set_parameter_errors(errs)
327
        return model
328
329 View Code Duplication
    @property
330
    def spatial_model(self):
331
        """
332
        Source spatial model (`~gammapy.image.models.SkySpatialModel`).
333
        """
334
        d = self.data
335
336
        pars = {}
337
        glon = d["GLON"]
338
        glat = d["GLAT"]
339
340
        if self.is_pointlike:
341
            pars["lon_0"] = glon
342
            pars["lat_0"] = glat
343
            return SkyPointSource(lon_0=glon, lat_0=glat)
344
        else:
345
            de = self.data_extended
346
            morph_type = de["Model_Form"].strip()
347
348
            if morph_type == "Disk":
349
                r_0 = de["Model_SemiMajor"].to("deg")
350
                return SkyDisk(lon_0=glon, lat_0=glat, r_0=r_0)
351
            elif morph_type in ["Map", "Ring", "2D Gaussian x2"]:
352
                filename = de["Spatial_Filename"].strip()
353
                path = make_path(
354
                    "$GAMMAPY_DATA/catalogs/fermi/Extended_archive_v15/Templates/"
355
                )
356
                return SkyDiffuseMap.read(path / filename)
357
            elif morph_type == "2D Gaussian":
358
                # TODO: fill elongation info as soon as model supports it
359
                sigma = de["Model_SemiMajor"].to("deg")
360
                return SkyGaussian(lon_0=glon, lat_0=glat, sigma=sigma)
361
            else:
362
                raise ValueError("Invalid spatial model: {!r}".format(morph_type))
363
364
    @property
365
    def sky_model(self):
366
        """Source sky model (`~gammapy.cube.models.SkyModel`)."""
367
        spatial_model = self.spatial_model
368
        spectral_model = self.spectral_model
369
        return SkyModel(spatial_model, spectral_model)
370
371
    @property
372
    def is_pointlike(self):
373
        return self.data["Extended_Source_Name"].strip() == ""
374
375 View Code Duplication
    @property
376
    def _flux_points_table_formatted(self):
377
        """Returns formatted version of self.flux_points.table"""
378
        table = self.flux_points.table.copy()
379
        flux_cols = [
380
            "flux",
381
            "flux_errn",
382
            "flux_errp",
383
            "e2dnde",
384
            "e2dnde_errn",
385
            "e2dnde_errp",
386
            "flux_ul",
387
            "e2dnde_ul",
388
            "dnde",
389
        ]
390
        table["sqrt_TS"].format = ".1f"
391
        table["e_ref"].format = ".1f"
392
        for _ in flux_cols:
393
            table[_].format = ".3"
394
395
        return table
396
397
    @property
398
    def flux_points(self):
399
        """Flux points (`~gammapy.spectrum.FluxPoints`)."""
400
        table = Table()
401
        table.meta["SED_TYPE"] = "flux"
402
        e_ref = self._ebounds.log_centers
403
        table["e_ref"] = e_ref
404
        table["e_min"] = self._ebounds.lower_bounds
405
        table["e_max"] = self._ebounds.upper_bounds
406
407
        flux = self._get_flux_values("Flux")
408
        flux_err = self._get_flux_values("Unc_Flux")
409
        table["flux"] = flux
410
        table["flux_errn"] = np.abs(flux_err[:, 0])
411
        table["flux_errp"] = flux_err[:, 1]
412
413
        nuFnu = self._get_flux_values("nuFnu", "erg cm-2 s-1")
414
        table["e2dnde"] = nuFnu
415
        table["e2dnde_errn"] = np.abs(nuFnu * flux_err[:, 0] / flux)
416
        table["e2dnde_errp"] = nuFnu * flux_err[:, 1] / flux
417
418
        is_ul = np.isnan(table["flux_errn"])
419
        table["is_ul"] = is_ul
420
421
        # handle upper limits
422
        table["flux_ul"] = np.nan * flux_err.unit
423
        flux_ul = compute_flux_points_ul(table["flux"], table["flux_errp"])
424
        table["flux_ul"][is_ul] = flux_ul[is_ul]
425
426
        # handle upper limits
427
        table["e2dnde_ul"] = np.nan * nuFnu.unit
428
        e2dnde_ul = compute_flux_points_ul(table["e2dnde"], table["e2dnde_errp"])
429
        table["e2dnde_ul"][is_ul] = e2dnde_ul[is_ul]
430
431
        # Square root of test statistic
432
        table["sqrt_TS"] = [self.data["Sqrt_TS" + _] for _ in self._ebounds_suffix]
433
434
        table["dnde"] = (nuFnu * e_ref ** -2).to("TeV-1 cm-2 s-1")
435
        return FluxPoints(table)
436
437
    def _get_flux_values(self, prefix, unit="cm-2 s-1"):
438
        values = [self.data[prefix + _] for _ in self._ebounds_suffix]
439
        return u.Quantity(values, unit)
440
441
    @property
442
    def lightcurve(self):
443
        """Lightcurve (`~gammapy.time.LightCurve`).
444
445
        Examples
446
        --------
447
448
        >>> from gammapy.catalog import source_catalogs
449
        >>> source = source_catalogs['3fgl']['3FGL J0349.9-2102']
450
        >>> lc = source.lightcurve
451
        >>> lc.plot()
452
        """
453
        flux = self.data["Flux_History"]
454
455
        # Flux error is given as asymmetric high/low
456
        flux_errn = -self.data["Unc_Flux_History"][:, 0]
457
        flux_errp = self.data["Unc_Flux_History"][:, 1]
458
459
        # Really the time binning is stored in a separate HDU in the FITS
460
        # catalog file called `Hist_Start`, with a single column `Hist_Start`
461
        # giving the time binning in MET (mission elapsed time)
462
        # This is not available here for now.
463
        # TODO: read that info in `SourceCatalog3FGL` and pass it down to the
464
        # `SourceCatalogObject3FGL` object somehow.
465
466
        # For now, we just hard-code the start and stop time and assume
467
        # equally-spaced time intervals. This is roughly correct,
468
        # for plotting the difference doesn't matter, only for analysis
469
        time_start = Time("2008-08-02T00:33:19")
470
        time_end = Time("2012-07-31T22:45:47")
471
        n_points = len(flux)
472
        time_step = (time_end - time_start) / n_points
473
        time_bounds = time_start + np.arange(n_points + 1) * time_step
474
475
        table = Table(
476
            [
477
                Column(time_bounds[:-1].utc.mjd, "time_min"),
478
                Column(time_bounds[1:].utc.mjd, "time_max"),
479
                Column(flux, "flux"),
480
                Column(flux_errp, "flux_errp"),
481
                Column(flux_errn, "flux_errn"),
482
            ]
483
        )
484
        return LightCurve(table)
485
486
487 View Code Duplication
class SourceCatalogObject1FHL(SourceCatalogObject):
488
    """One source from the Fermi-LAT 1FHL catalog.
489
490
    Catalog is represented by `~gammapy.catalog.SourceCatalog1FHL`.
491
    """
492
493
    _ebounds = EnergyBounds([10, 30, 100, 500], "GeV")
494
    _ebounds_suffix = ["10_30", "30_100", "100_500"]
495
    energy_range = u.Quantity([0.01, 0.5], "TeV")
496
    """Energy range of the Fermi 1FHL source catalog"""
497
498
    def __str__(self):
499
        return self.info()
500
501
    def info(self):
502
        """Print summary info."""
503
        # TODO: can we share code with 3FGL summary function?
504
        d = self.data
505
506
        ss = "Source: {}\n".format(d["Source_Name"])
507
        ss += "\n"
508
509
        ss += "RA (J2000)  : {}\n".format(d["RAJ2000"])
510
        ss += "Dec (J2000) : {}\n".format(d["DEJ2000"])
511
        ss += "GLON        : {}\n".format(d["GLON"])
512
        ss += "GLAT        : {}\n".format(d["GLAT"])
513
        ss += "\n"
514
515
        # val, err = d['Energy_Flux100'], d['Unc_Energy_Flux100']
516
        # ss += 'Energy flux (100 MeV - 100 GeV) : {} +- {} erg cm^-2 s^-1\n'.format(val, err)
517
        # ss += 'Detection significance : {}\n'.format(d['Signif_Avg'])
518
519
        return ss
520
521
    def _get_flux_values(self, prefix, unit="cm-2 s-1"):
522
        values = [self.data[prefix + _ + "GeV"] for _ in self._ebounds_suffix]
523
        return u.Quantity(values, unit)
524
525
    @property
526
    def flux_points(self):
527
        """Integral flux points (`~gammapy.spectrum.FluxPoints`)."""
528
        table = Table()
529
        table.meta["SED_TYPE"] = "flux"
530
        table["e_min"] = self._ebounds.lower_bounds
531
        table["e_max"] = self._ebounds.upper_bounds
532
        table["flux"] = self._get_flux_values("Flux")
533
        flux_err = self._get_flux_values("Unc_Flux")
534
        table["flux_errn"] = np.abs(flux_err[:, 0])
535
        table["flux_errp"] = flux_err[:, 1]
536
537
        # handle upper limits
538
        is_ul = np.isnan(table["flux_errn"])
539
        table["is_ul"] = is_ul
540
        table["flux_ul"] = np.nan * flux_err.unit
541
        flux_ul = compute_flux_points_ul(table["flux"], table["flux_errp"])
542
        table["flux_ul"][is_ul] = flux_ul[is_ul]
543
544
        flux_points = FluxPoints(table)
545
546
        # TODO: change this and leave it up to the caller to convert to dnde
547
        # See https://github.com/gammapy/gammapy/issues/1034
548
        return flux_points.to_sed_type("dnde", model=self.spectral_model)
549
550
    @property
551
    def spectral_model(self):
552
        """Best fit spectral model `~gammapy.spectrum.models.SpectralModel`."""
553
        pars, errs = {}, {}
554
        pars["amplitude"] = self.data["Flux"]
555
        pars["emin"], pars["emax"] = self.energy_range
556
        pars["index"] = self.data["Spectral_Index"]
557
        errs["amplitude"] = self.data["Unc_Flux"]
558
        errs["index"] = self.data["Unc_Spectral_Index"]
559
        model = PowerLaw2(**pars)
560
        model.parameters.set_parameter_errors(errs)
561
        return model
562
563
564 View Code Duplication
class SourceCatalogObject2FHL(SourceCatalogObject):
565
    """One source from the Fermi-LAT 2FHL catalog.
566
567
    Catalog is represented by `~gammapy.catalog.SourceCatalog2FHL`.
568
    """
569
570
    _ebounds = EnergyBounds([50, 171, 585, 2000], "GeV")
571
    _ebounds_suffix = ["50_171", "171_585", "585_2000"]
572
    energy_range = u.Quantity([0.05, 2], "TeV")
573
    """Energy range of the Fermi 2FHL source catalog"""
574
575
    def __str__(self):
576
        return self.info()
577
578
    def info(self):
579
        """Print summary info."""
580
        # TODO: can we share code with 3FGL summary funtion?
581
        d = self.data
582
583
        ss = "Source: {}\n".format(d["Source_Name"])
584
        ss += "\n"
585
586
        ss += "RA (J2000)  : {}\n".format(d["RAJ2000"])
587
        ss += "Dec (J2000) : {}\n".format(d["DEJ2000"])
588
        ss += "GLON        : {}\n".format(d["GLON"])
589
        ss += "GLAT        : {}\n".format(d["GLAT"])
590
        ss += "\n"
591
592
        # val, err = d['Energy_Flux100'], d['Unc_Energy_Flux100']
593
        # ss += 'Energy flux (100 MeV - 100 GeV) : {} +- {} erg cm^-2 s^-1\n'.format(val, err)
594
        # ss += 'Detection significance : {}\n'.format(d['Signif_Avg'])
595
596
        return ss
597
598
    def _get_flux_values(self, prefix, unit="cm-2 s-1"):
599
        values = [self.data[prefix + _ + "GeV"] for _ in self._ebounds_suffix]
600
        return u.Quantity(values, unit)
601
602
    @property
603
    def flux_points(self):
604
        """Integral flux points (`~gammapy.spectrum.FluxPoints`)."""
605
        table = Table()
606
        table.meta["SED_TYPE"] = "flux"
607
        table["e_min"] = self._ebounds.lower_bounds
608
        table["e_max"] = self._ebounds.upper_bounds
609
        table["flux"] = self._get_flux_values("Flux")
610
        flux_err = self._get_flux_values("Unc_Flux")
611
        table["flux_errn"] = np.abs(flux_err[:, 0])
612
        table["flux_errp"] = flux_err[:, 1]
613
614
        # handle upper limits
615
        is_ul = np.isnan(table["flux_errn"])
616
        table["is_ul"] = is_ul
617
        table["flux_ul"] = np.nan * flux_err.unit
618
        flux_ul = compute_flux_points_ul(table["flux"], table["flux_errp"])
619
        table["flux_ul"][is_ul] = flux_ul[is_ul]
620
621
        flux_points = FluxPoints(table)
622
623
        # TODO: change this and leave it up to the caller to convert to dnde
624
        # See https://github.com/gammapy/gammapy/issues/1034
625
        return flux_points.to_sed_type("dnde", model=self.spectral_model)
626
627
    @property
628
    def spectral_model(self):
629
        """Best fit spectral model (`~gammapy.spectrum.models.SpectralModel`)."""
630
        pars, errs = {}, {}
631
        pars["amplitude"] = self.data["Flux50"]
632
        pars["emin"], pars["emax"] = self.energy_range
633
        pars["index"] = self.data["Spectral_Index"]
634
635
        errs["amplitude"] = self.data["Unc_Flux50"]
636
        errs["index"] = self.data["Unc_Spectral_Index"]
637
638
        model = PowerLaw2(**pars)
639
        model.parameters.set_parameter_errors(errs)
640
        return model
641
642
643
class SourceCatalogObject3FHL(SourceCatalogObject):
644
    """One source from the Fermi-LAT 3FHL catalog.
645
646
    Catalog is represented by `~gammapy.catalog.SourceCatalog3FHL`.
647
    """
648
649
    energy_range = u.Quantity([0.01, 2], "TeV")
650
    """Energy range of the Fermi 1FHL source catalog"""
651
652
    _ebounds = EnergyBounds([10, 20, 50, 150, 500, 2000], "GeV")
653
654
    def __str__(self):
655
        return self.info()
656
657
    def info(self, info="all"):
658
        """Summary info string.
659
660
        Parameters
661
        ----------
662
        info : {'all', 'basic', 'position', 'spectral'}
663
            Comma separated list of options
664
        """
665
        if info == "all":
666
            info = "basic,position,spectral,other"
667
668
        ss = ""
669
        ops = info.split(",")
670
        if "basic" in ops:
671
            ss += self._info_basic()
672
        if "position" in ops:
673
            ss += self._info_position()
674
            if not self.is_pointlike:
675
                ss += self._info_morphology()
676
        if "spectral" in ops:
677
            ss += self._info_spectral_fit()
678
            ss += self._info_spectral_points()
679
        if "other" in ops:
680
            ss += self._info_other()
681
682
        return ss
683
684
    def _info_basic(self):
685
        """Print basic info."""
686
        d = self.data
687
        ss = "\n*** Basic info ***\n\n"
688
        ss += "Catalog row index (zero-based) : {}\n".format(d["catalog_row_index"])
689
        ss += "{:<20s} : {}\n".format("Source name", d["Source_Name"])
690
        ss += "{:<20s} : {}\n".format("Extended name", d["Extended_Source_Name"])
691
692
        def get_nonentry_keys(keys):
693
            vals = [d[_].strip() for _ in keys]
694
            return ", ".join([_ for _ in vals if _ != ""])
695
696
        keys = ["ASSOC1", "ASSOC2", "ASSOC_TEV", "ASSOC_GAM"]
697
        associations = get_nonentry_keys(keys)
698
        ss += "{:<16s} : {}\n".format("Associations", associations)
699
        ss += "{:<16s} : {:.3f}\n".format("ASSOC_PROB_BAY", d["ASSOC_PROB_BAY"])
700
        ss += "{:<16s} : {:.3f}\n".format("ASSOC_PROB_LR", d["ASSOC_PROB_LR"])
701
702
        ss += "{:<16s} : {}\n".format("Class", d["CLASS"])
703
704
        tevcat_flag = d["TEVCAT_FLAG"]
705
        if tevcat_flag == "N":
706
            tevcat_message = "No TeV association"
707
        elif tevcat_flag == "P":
708
            tevcat_message = "Small TeV source"
709
        elif tevcat_flag == "E":
710
            tevcat_message = "Extended TeV source (diameter > 40 arcmins)"
711
        else:
712
            tevcat_message = "N/A"
713
        ss += "{:<16s} : {}\n".format("TeVCat flag", tevcat_message)
714
715
        fmt = "\n{:<32s} : {:.3f}\n"
716
        ss += fmt.format("Significance (10 GeV - 2 TeV)", d["Signif_Avg"])
717
        ss += "{:<32s} : {:.1f}\n".format("Npred", d["Npred"])
718
719
        return ss
720
721
    def _info_position(self):
722
        """Print position info."""
723
        d = self.data
724
        ss = "\n*** Position info ***\n\n"
725
        ss += "{:<20s} : {:.3f}\n".format("RA", d["RAJ2000"])
726
        ss += "{:<20s} : {:.3f}\n".format("DEC", d["DEJ2000"])
727
        ss += "{:<20s} : {:.3f}\n".format("GLON", d["GLON"])
728
        ss += "{:<20s} : {:.3f}\n".format("GLAT", d["GLAT"])
729
730
        # TODO: All sources are non-elliptical; just give one number for radius?
731
        ss += "\n"
732
        ss += "{:<20s} : {:.4f}\n".format("Semimajor (95%)", d["Conf_95_SemiMajor"])
733
        ss += "{:<20s} : {:.4f}\n".format("Semiminor (95%)", d["Conf_95_SemiMinor"])
734
        ss += "{:<20s} : {:.2f}\n".format("Position angle (95%)", d["Conf_95_PosAng"])
735
        ss += "{:<20s} : {:.0f}\n".format("ROI number", d["ROI_num"])
736
737
        return ss
738
739
    def _info_morphology(self):
740
        e = self.data_extended
741
        ss = "*** Extended source information ***\n"
742
        ss += "{:<16s} : {}\n".format("Model form", e["Model_Form"])
743
        ss += "{:<16s} : {:.4f}\n".format("Model semimajor", e["Model_SemiMajor"])
744
        ss += "{:<16s} : {:.4f}\n".format("Model semiminor", e["Model_SemiMinor"])
745
        ss += "{:<16s} : {:.4f}\n".format("Position angle", e["Model_PosAng"])
746
        ss += "{:<16s} : {}\n".format("Spatial function", e["Spatial_Function"])
747
        ss += "{:<16s} : {}\n\n".format("Spatial filename", e["Spatial_Filename"])
748
        return ss
749
750
    def _info_spectral_fit(self):
751
        """Print model data."""
752
        d = self.data
753
        spec_type = d["SpectrumType"].strip()
754
755
        ss = "\n*** Spectral fit info ***\n\n"
756
757
        ss += "{:<32s} : {}\n".format("Spectrum type", d["SpectrumType"])
758
        ss += "{:<32s} : {:.1f}\n".format("Significance curvature", d["Signif_Curve"])
759
760
        # Power-law parameters are always given; give in any case
761
        fmt = "{:<32s} : {:.3f} +- {:.3f}\n"
762
        ss += fmt.format(
763
            "Power-law spectral index", d["PowerLaw_Index"], d["Unc_PowerLaw_Index"]
764
        )
765
766
        if spec_type == "PowerLaw":
767
            pass
768
        elif spec_type == "LogParabola":
769
            fmt = "{:<32s} : {:.3f} +- {:.3f}\n"
770
            ss += fmt.format(
771
                "LogParabola spectral index",
772
                d["Spectral_Index"],
773
                d["Unc_Spectral_Index"],
774
            )
775
776
            ss += "{:<32s} : {:.3f} +- {:.3f}\n".format(
777
                "LogParabola beta", d["beta"], d["Unc_beta"]
778
            )
779
        else:
780
            raise ValueError("Invalid spec_type")
781
782
        ss += "{:<32s} : {:.1f} {}\n".format(
783
            "Pivot energy", d["Pivot_Energy"].value, d["Pivot_Energy"].unit
784
        )
785
786
        ss += "{:<32s} : {:.3} +- {:.3} {}\n".format(
787
            "Flux Density at pivot energy",
788
            d["Flux_Density"].value,
789
            d["Unc_Flux_Density"].value,
790
            "cm-2 GeV-1 s-1",
791
        )
792
793
        ss += "{:<32s} : {:.3} +- {:.3} {}\n".format(
794
            "Integral flux (10 GeV - 1 TeV)",
795
            d["Flux"].value,
796
            d["Unc_Flux"].value,
797
            "cm-2 s-1",
798
        )
799
800
        ss += "{:<32s} : {:.3} +- {:.3} {}\n".format(
801
            "Energy flux (10 GeV - TeV)",
802
            d["Energy_Flux"].value,
803
            d["Unc_Energy_Flux"].value,
804
            "erg cm-2 s-1",
805
        )
806
807
        return ss
808
809
    def _info_spectral_points(self):
810
        """Print spectral points."""
811
        ss = "\n*** Spectral points ***\n\n"
812
        lines = self._flux_points_table_formatted.pformat(max_width=-1, max_lines=-1)
813
        ss += "\n".join(lines)
814
        return ss + "\n"
815
816
    def _info_other(self):
817
        """Print other info."""
818
        d = self.data
819
        ss = "\n*** Other info ***\n\n"
820
        ss += "{:<16s} : {:.3f} {}\n".format(
821
            "HEP Energy", d["HEP_Energy"].value, d["HEP_Energy"].unit
822
        )
823
        ss += "{:<16s} : {:.3f}\n".format("HEP Probability", d["HEP_Prob"])
824
825
        # This is the number of Bayesian blocks for most sources,
826
        # except -1 means "could not be tested"
827
        msg = d["Variability_BayesBlocks"]
828
        if msg == 1:
829
            msg = "1 (not variable)"
830
        elif msg == -1:
831
            msg = "Could not be tested"
832
        ss += "{:<16s} : {}\n".format("Bayesian Blocks", msg)
833
834
        ss += "{:<16s} : {:.3f}\n".format("Redshift", d["Redshift"])
835
        ss += "{:<16s} : {:.3} {}\n".format(
836
            "NuPeak_obs", d["NuPeak_obs"].value, d["NuPeak_obs"].unit
837
        )
838
839
        return ss
840
841
    @property
842
    def spectral_model(self):
843
        """Best fit spectral model (`~gammapy.spectrum.models.SpectralModel`)."""
844
        d = self.data
845
        spec_type = self.data["SpectrumType"].strip()
846
847
        pars, errs = {}, {}
848
        pars["amplitude"] = d["Flux_Density"]
849
        errs["amplitude"] = d["Unc_Flux_Density"]
850
        pars["reference"] = d["Pivot_Energy"]
851
852
        if spec_type == "PowerLaw":
853
            pars["index"] = d["PowerLaw_Index"]
854
            errs["index"] = d["Unc_PowerLaw_Index"]
855
            model = PowerLaw(**pars)
856
        elif spec_type == "LogParabola":
857
            pars["alpha"] = d["Spectral_Index"]
858
            pars["beta"] = d["beta"]
859
            errs["alpha"] = d["Unc_Spectral_Index"]
860
            errs["beta"] = d["Unc_beta"]
861
            model = LogParabola(**pars)
862
        else:
863
            raise ValueError("Invalid spec_type: {!r}".format(spec_type))
864
865
        model.parameters.set_parameter_errors(errs)
866
        return model
867
868 View Code Duplication
    @property
869
    def _flux_points_table_formatted(self):
870
        """Returns formatted version of self.flux_points.table"""
871
        table = self.flux_points.table.copy()
872
        flux_cols = [
873
            "flux",
874
            "flux_errn",
875
            "flux_errp",
876
            "e2dnde",
877
            "e2dnde_errn",
878
            "e2dnde_errp",
879
            "flux_ul",
880
            "e2dnde_ul",
881
            "dnde",
882
        ]
883
        table["sqrt_ts"].format = ".1f"
884
        table["e_ref"].format = ".1f"
885
        for _ in flux_cols:
886
            table[_].format = ".3"
887
888
        return table
889
890
    @property
891
    def flux_points(self):
892
        """Flux points (`~gammapy.spectrum.FluxPoints`)."""
893
        table = Table()
894
        table.meta["SED_TYPE"] = "flux"
895
        e_ref = self._ebounds.log_centers
896
        table["e_ref"] = e_ref
897
        table["e_min"] = self._ebounds.lower_bounds
898
        table["e_max"] = self._ebounds.upper_bounds
899
900
        flux = self.data["Flux_Band"]
901
        flux_err = self.data["Unc_Flux_Band"]
902
        e2dnde = self.data["nuFnu"]
903
904
        table["flux"] = flux
905
        table["flux_errn"] = np.abs(flux_err[:, 0])
906
        table["flux_errp"] = flux_err[:, 1]
907
908
        table["e2dnde"] = e2dnde
909
        table["e2dnde_errn"] = np.abs(e2dnde * flux_err[:, 0] / flux)
910
        table["e2dnde_errp"] = e2dnde * flux_err[:, 1] / flux
911
912
        is_ul = np.isnan(table["flux_errn"])
913
        table["is_ul"] = is_ul
914
915
        # handle upper limits
916
        table["flux_ul"] = np.nan * flux_err.unit
917
        flux_ul = compute_flux_points_ul(table["flux"], table["flux_errp"])
918
        table["flux_ul"][is_ul] = flux_ul[is_ul]
919
920
        table["e2dnde_ul"] = np.nan * e2dnde.unit
921
        e2dnde_ul = compute_flux_points_ul(table["e2dnde"], table["e2dnde_errp"])
922
        table["e2dnde_ul"][is_ul] = e2dnde_ul[is_ul]
923
924
        # Square root of test statistic
925
        table["sqrt_ts"] = self.data["Sqrt_TS_Band"]
926
927
        # TODO: remove this computation here.
928
        # # Instead provide a method on the FluxPoints class like `to_dnde()` or something.
929
        table["dnde"] = (e2dnde * e_ref ** -2).to("cm-2 s-1 TeV-1")
930
931
        return FluxPoints(table)
932
933 View Code Duplication
    @property
934
    def spatial_model(self):
935
        """Source spatial model (`~gammapy.image.models.SkySpatialModel`)."""
936
        d = self.data
937
938
        pars = {}
939
        glon = d["GLON"]
940
        glat = d["GLAT"]
941
942
        if self.is_pointlike:
943
            pars["lon_0"] = glon
944
            pars["lat_0"] = glat
945
            return SkyPointSource(lon_0=glon, lat_0=glat)
946
        else:
947
            de = self.data_extended
948
            morph_type = de["Spatial_Function"].strip()
949
950
            if morph_type == "RadialDisk":
951
                r_0 = de["Model_SemiMajor"].to("deg")
952
                return SkyDisk(lon_0=glon, lat_0=glat, r_0=r_0)
953
            elif morph_type in ["SpatialMap"]:
954
                filename = de["Spatial_Filename"].strip()
955
                path = make_path(
956
                    "$GAMMAPY_DATA/catalogs/fermi/Extended_archive_v18/Templates/"
957
                )
958
                return SkyDiffuseMap.read(path / filename)
959
            elif morph_type == "RadialGauss":
960
                # TODO: fill elongation info as soon as model supports it
961
                sigma = de["Model_SemiMajor"].to("deg")
962
                return SkyGaussian(lon_0=glon, lat_0=glat, sigma=sigma)
963
            else:
964
                raise ValueError("Invalid morph_type: {!r}".format(morph_type))
965
966
    @property
967
    def sky_model(self):
968
        """Source sky model (`~gammapy.cube.models.SkyModel`)."""
969
        spatial_model = self.spatial_model
970
        spectral_model = self.spectral_model
971
        return SkyModel(spatial_model, spectral_model)
972
973
    @property
974
    def is_pointlike(self):
975
        return self.data["Extended_Source_Name"].strip() == ""
976
977
978
class SourceCatalog3FGL(SourceCatalog):
979
    """Fermi-LAT 3FGL source catalog.
980
981
    Reference: https://ui.adsabs.harvard.edu/#abs/2015ApJS..218...23A
982
983
    One source is represented by `~gammapy.catalog.SourceCatalogObject3FGL`.
984
    """
985
986
    name = "3fgl"
987
    description = "LAT 4-year point source catalog"
988
    source_object_class = SourceCatalogObject3FGL
989
    source_categories = {
990
        "galactic": ["psr", "pwn", "snr", "spp", "glc"],
991
        "extra-galactic": [
992
            "css",
993
            "bll",
994
            "fsrq",
995
            "agn",
996
            "nlsy1",
997
            "rdg",
998
            "sey",
999
            "bcu",
1000
            "gal",
1001
            "sbg",
1002
            "ssrq",
1003
        ],
1004
        "GALACTIC": ["PSR", "PWN", "SNR", "HMB", "BIN", "NOV", "SFR"],
1005
        "EXTRA-GALACTIC": [
1006
            "CSS",
1007
            "BLL",
1008
            "FSRQ",
1009
            "AGN",
1010
            "NLSY1",
1011
            "RDG",
1012
            "SEY",
1013
            "BCU",
1014
            "GAL",
1015
            "SBG",
1016
            "SSRQ",
1017
        ],
1018
        "unassociated": [""],
1019
    }
1020
1021 View Code Duplication
    def __init__(self, filename="$GAMMAPY_DATA/catalogs/fermi/gll_psc_v16.fit.gz"):
1022
        filename = str(make_path(filename))
1023
1024
        with warnings.catch_warnings():  # ignore FITS units warnings
1025
            warnings.simplefilter("ignore", u.UnitsWarning)
1026
            table = Table.read(filename, hdu="LAT_Point_Source_Catalog")
1027
1028
        table_standardise_units_inplace(table)
1029
1030
        source_name_key = "Source_Name"
1031
        source_name_alias = (
1032
            "Extended_Source_Name",
1033
            "0FGL_Name",
1034
            "1FGL_Name",
1035
            "2FGL_Name",
1036
            "1FHL_Name",
1037
            "ASSOC_TEV",
1038
            "ASSOC1",
1039
            "ASSOC2",
1040
        )
1041
        super(SourceCatalog3FGL, self).__init__(
1042
            table=table,
1043
            source_name_key=source_name_key,
1044
            source_name_alias=source_name_alias,
1045
        )
1046
1047
        self.extended_sources_table = Table.read(filename, hdu="ExtendedSources")
1048
1049 View Code Duplication
    def is_source_class(self, source_class):
1050
        """
1051
        Check if source belongs to a given source class.
1052
1053
        The classes are described in Table 3 of the 3FGL paper:
1054
1055
        http://adsabs.harvard.edu/abs/2015arXiv150102003T
1056
1057
        Parameters
1058
        ----------
1059
        source_class : str
1060
            Source class designator as defined in Table 3. There are a few extra
1061
            selections available:
1062
1063
            - 'ALL': all identified objects
1064
            - 'all': all objects with associations
1065
            - 'galactic': all sources with an associated galactic object
1066
            - 'GALACTIC': all identified galactic sources
1067
            - 'extra-galactic': all sources with an associated extra-galactic object
1068
            - 'EXTRA-GALACTIC': all identified extra-galactic sources
1069
            - 'unassociated': all unassociated objects
1070
1071
        Returns
1072
        -------
1073
        selection : `~numpy.ndarray`
1074
            Selection mask.
1075
        """
1076
        source_class_info = np.array([_.strip() for _ in self.table["CLASS1"]])
1077
1078
        cats = self.source_categories
1079
        if source_class in cats:
1080
            category = set(cats[source_class])
1081
        elif source_class == "ALL":
1082
            category = set(cats["EXTRA-GALACTIC"] + cats["GALACTIC"])
1083
        elif source_class == "all":
1084
            category = set(cats["extra-galactic"] + cats["galactic"])
1085
        elif source_class in np.unique(source_class_info):
1086
            category = set([source_class])
1087
        else:
1088
            raise ValueError("Invalid source_class: {!r}".format(source_class))
1089
1090
        return np.array([_ in category for _ in source_class_info])
1091
1092
    def select_source_class(self, source_class):
1093
        """
1094
        Select all sources of a given source class.
1095
1096
        See `SourceCatalog3FHL.is_source_class` for further documentation
1097
1098
        Parameters
1099
        ----------
1100
        source_class : str
1101
            Source class designator.
1102
1103
        Returns
1104
        -------
1105
        selection : `SourceCatalog3FHL`
1106
            Subset of the 3FHL catalog containing only the selected source class.
1107
        """
1108
        catalog = self.copy()
1109
        selection = self.is_source_class(source_class)
1110
        catalog.table = catalog.table[selection]
1111
        return catalog
1112
1113
1114
class SourceCatalog1FHL(SourceCatalog):
1115
    """Fermi-LAT 1FHL source catalog.
1116
1117
    Reference: http://adsabs.harvard.edu/abs/2013ApJS..209...34A
1118
1119
    One source is represented by `~gammapy.catalog.SourceCatalogObject1FHL`.
1120
    """
1121
1122
    name = "1fhl"
1123
    description = "First Fermi-LAT Catalog of Sources above 10 GeV"
1124
    source_object_class = SourceCatalogObject1FHL
1125
1126 View Code Duplication
    def __init__(self, filename="$GAMMAPY_DATA/catalogs/fermi/gll_psch_v07.fit.gz"):
1127
        filename = str(make_path(filename))
1128
1129
        with warnings.catch_warnings():  # ignore FITS units warnings
1130
            warnings.simplefilter("ignore", u.UnitsWarning)
1131
            table = Table.read(filename, hdu="LAT_Point_Source_Catalog")
1132
1133
        table_standardise_units_inplace(table)
1134
1135
        source_name_key = "Source_Name"
1136
        source_name_alias = ("ASSOC1", "ASSOC2", "ASSOC_TEV", "ASSOC_GAM")
1137
        super(SourceCatalog1FHL, self).__init__(
1138
            table=table,
1139
            source_name_key=source_name_key,
1140
            source_name_alias=source_name_alias,
1141
        )
1142
1143
        self.extended_sources_table = Table.read(filename, hdu="ExtendedSources")
1144
1145
1146
class SourceCatalog2FHL(SourceCatalog):
1147
    """Fermi-LAT 2FHL source catalog.
1148
1149
    Reference: http://adsabs.harvard.edu/abs/2016ApJS..222....5A
1150
1151
    One source is represented by `~gammapy.catalog.SourceCatalogObject2FHL`.
1152
    """
1153
1154
    name = "2fhl"
1155
    description = "LAT second high-energy source catalog"
1156
    source_object_class = SourceCatalogObject2FHL
1157
1158
    def __init__(self, filename="$GAMMAPY_DATA/catalogs/fermi/gll_psch_v08.fit.gz"):
1159
        filename = str(make_path(filename))
1160
1161
        with warnings.catch_warnings():  # ignore FITS units warnings
1162
            warnings.simplefilter("ignore", u.UnitsWarning)
1163
            table = Table.read(filename, hdu="2FHL Source Catalog")
1164
1165
        table_standardise_units_inplace(table)
1166
1167
        source_name_key = "Source_Name"
1168
        source_name_alias = ("ASSOC", "3FGL_Name", "1FHL_Name", "TeVCat_Name")
1169
        super(SourceCatalog2FHL, self).__init__(
1170
            table=table,
1171
            source_name_key=source_name_key,
1172
            source_name_alias=source_name_alias,
1173
        )
1174
1175
        self.counts_image = Map.read(filename, hdu="Count Map")
1176
        self.extended_sources_table = Table.read(filename, hdu="Extended Sources")
1177
        self.rois = Table.read(filename, hdu="ROIs")
1178
1179
1180
class SourceCatalog3FHL(SourceCatalog):
1181
    """Fermi-LAT 3FHL source catalog.
1182
1183
    Reference: http://adsabs.harvard.edu/abs/2017ApJS..232...18A
1184
1185
    One source is represented by `~gammapy.catalog.SourceCatalogObject3FHL`.
1186
    """
1187
1188
    name = "3fhl"
1189
    description = "LAT third high-energy source catalog"
1190
    source_object_class = SourceCatalogObject3FHL
1191
    source_categories = {
1192
        "galactic": ["glc", "hmb", "psr", "pwn", "sfr", "snr", "spp"],
1193
        "extra-galactic": ["agn", "bcu", "bll", "fsrq", "rdg", "sbg"],
1194
        "GALACTIC": ["BIN", "HMB", "PSR", "PWN", "SFR", "SNR"],
1195
        "EXTRA-GALACTIC": ["BLL", "FSRQ", "NLSY1", "RDG"],
1196
        "unassociated": [""],
1197
    }
1198
1199
    def __init__(self, filename="$GAMMAPY_DATA/catalogs/fermi/gll_psch_v13.fit.gz"):
1200
        filename = str(make_path(filename))
1201
1202
        with warnings.catch_warnings():  # ignore FITS units warnings
1203
            warnings.simplefilter("ignore", u.UnitsWarning)
1204
            table = Table.read(filename, hdu="LAT_Point_Source_Catalog")
1205
1206
        table_standardise_units_inplace(table)
1207
1208
        source_name_key = "Source_Name"
1209
        source_name_alias = ("ASSOC1", "ASSOC2", "ASSOC_TEV", "ASSOC_GAM")
1210
        super(SourceCatalog3FHL, self).__init__(
1211
            table=table,
1212
            source_name_key=source_name_key,
1213
            source_name_alias=source_name_alias,
1214
        )
1215
1216
        self.extended_sources_table = Table.read(filename, hdu="ExtendedSources")
1217
        self.rois = Table.read(filename, hdu="ROIs")
1218
        self.energy_bounds_table = Table.read(filename, hdu="EnergyBounds")
1219
1220 View Code Duplication
    def is_source_class(self, source_class):
1221
        """
1222
        Check if source belongs to a given source class.
1223
1224
        The classes are described in Table 3 of the 3FGL paper:
1225
1226
        http://adsabs.harvard.edu/abs/2015arXiv150102003T
1227
1228
        Parameters
1229
        ----------
1230
        source_class : str
1231
            Source class designator as defined in Table 3. There are a few extra
1232
            selections available:
1233
1234
            - 'ALL': all identified objects
1235
            - 'all': all objects with associations
1236
            - 'galactic': all sources with an associated galactic object
1237
            - 'GALACTIC': all identified galactic sources
1238
            - 'extra-galactic': all sources with an associated extra-galactic object
1239
            - 'EXTRA-GALACTIC': all identified extra-galactic sources
1240
            - 'unassociated': all unassociated objects
1241
1242
        Returns
1243
        -------
1244
        selection : `~numpy.ndarray`
1245
            Selection mask.
1246
        """
1247
        source_class_info = np.array([_.strip() for _ in self.table["CLASS"]])
1248
1249
        cats = self.source_categories
1250
        if source_class in cats:
1251
            category = set(cats[source_class])
1252
        elif source_class == "ALL":
1253
            category = set(cats["EXTRA-GALACTIC"] + cats["GALACTIC"])
1254
        elif source_class == "all":
1255
            category = set(cats["extra-galactic"] + cats["galactic"])
1256
        elif source_class in np.unique(source_class_info):
1257
            category = set([source_class])
1258
        else:
1259
            raise ValueError("Invalid source_class: {!r}".format(source_class))
1260
1261
        return np.array([_ in category for _ in source_class_info])
1262
1263
    def select_source_class(self, source_class):
1264
        """
1265
        Select all sources of a given source class.
1266
1267
        See `SourceCatalog3FHL.is_source_class` for further documentation
1268
1269
        Parameters
1270
        ----------
1271
        source_class : str
1272
            Source class designator.
1273
1274
        Returns
1275
        -------
1276
        selection : `SourceCatalog3FHL`
1277
            Subset of the 3FHL catalog containing only the selected source class.
1278
        """
1279
        catalog = self.copy()
1280
        selection = self.is_source_class(source_class)
1281
        catalog.table = catalog.table[selection]
1282
        return catalog
1283