1
|
|
|
# Licensed under a 3-clause BSD style license - see LICENSE.rst |
2
|
|
|
import logging |
3
|
|
|
from copy import deepcopy |
4
|
|
|
import numpy as np |
5
|
|
|
from scipy import stats |
6
|
|
|
from astropy.io.registry import IORegistryError |
7
|
|
|
from astropy.table import Table, vstack |
8
|
|
|
from astropy.visualization import quantity_support |
9
|
|
|
import matplotlib.pyplot as plt |
10
|
|
|
from gammapy.maps import MapAxis, Maps, RegionNDMap, TimeMapAxis |
11
|
|
|
from gammapy.maps.axes import flat_if_equal |
12
|
|
|
from gammapy.modeling.models import TemplateSpectralModel |
13
|
|
|
from gammapy.modeling.models.spectral import scale_plot_flux |
14
|
|
|
from gammapy.modeling.scipy import stat_profile_ul_scipy |
15
|
|
|
from gammapy.utils.scripts import make_path |
16
|
|
|
from gammapy.utils.table import table_standardise_units_copy |
17
|
|
|
from ..map.core import DEFAULT_UNIT, FluxMaps |
18
|
|
|
|
19
|
|
|
__all__ = ["FluxPoints"] |
20
|
|
|
|
21
|
|
|
log = logging.getLogger(__name__) |
22
|
|
|
|
23
|
|
|
|
24
|
|
|
class FluxPoints(FluxMaps): |
25
|
|
|
"""Flux points container. |
26
|
|
|
|
27
|
|
|
The supported formats are described here: :ref:`gadf:flux-points` |
28
|
|
|
|
29
|
|
|
In summary, the following formats and minimum required columns are: |
30
|
|
|
|
31
|
|
|
* Format ``dnde``: columns ``e_ref`` and ``dnde`` |
32
|
|
|
* Format ``e2dnde``: columns ``e_ref``, ``e2dnde`` |
33
|
|
|
* Format ``flux``: columns ``e_min``, ``e_max``, ``flux`` |
34
|
|
|
* Format ``eflux``: columns ``e_min``, ``e_max``, ``eflux`` |
35
|
|
|
|
36
|
|
|
Parameters |
37
|
|
|
---------- |
38
|
|
|
table : `~astropy.table.Table` |
39
|
|
|
Table with flux point data |
40
|
|
|
|
41
|
|
|
Attributes |
42
|
|
|
---------- |
43
|
|
|
table : `~astropy.table.Table` |
44
|
|
|
Table with flux point data |
45
|
|
|
|
46
|
|
|
Examples |
47
|
|
|
-------- |
48
|
|
|
The `FluxPoints` object is most easily created by reading a file with |
49
|
|
|
flux points given in one of the formats documented above:: |
50
|
|
|
|
51
|
|
|
>>> from gammapy.estimators import FluxPoints |
52
|
|
|
>>> filename = '$GAMMAPY_DATA/hawc_crab/HAWC19_flux_points.fits' |
53
|
|
|
>>> flux_points = FluxPoints.read(filename) |
54
|
|
|
>>> flux_points.plot() #doctest: +SKIP |
55
|
|
|
|
56
|
|
|
An instance of `FluxPoints` can also be created by passing an instance of |
57
|
|
|
`astropy.table.Table`, which contains the required columns, such as `'e_ref'` |
58
|
|
|
and `'dnde'`. The corresponding `sed_type` has to be defined in the meta data |
59
|
|
|
of the table:: |
60
|
|
|
|
61
|
|
|
>>> import numpy as np |
62
|
|
|
>>> from astropy import units as u |
63
|
|
|
>>> from astropy.table import Table |
64
|
|
|
>>> from gammapy.estimators import FluxPoints |
65
|
|
|
>>> from gammapy.modeling.models import PowerLawSpectralModel |
66
|
|
|
>>> table = Table() |
67
|
|
|
>>> pwl = PowerLawSpectralModel() |
68
|
|
|
>>> e_ref = np.geomspace(1, 100, 7) * u.TeV |
69
|
|
|
>>> table["e_ref"] = e_ref |
70
|
|
|
>>> table["dnde"] = pwl(e_ref) |
71
|
|
|
>>> table["dnde_err"] = pwl.evaluate_error(e_ref)[0] |
72
|
|
|
>>> table.meta["SED_TYPE"] = "dnde" |
73
|
|
|
>>> flux_points = FluxPoints.from_table(table) |
74
|
|
|
>>> flux_points.plot(sed_type="flux") #doctest: +SKIP |
75
|
|
|
|
76
|
|
|
If you have flux points in a different data format, the format can be changed |
77
|
|
|
by renaming the table columns and adding meta data:: |
78
|
|
|
|
79
|
|
|
|
80
|
|
|
>>> from astropy import units as u |
81
|
|
|
>>> from astropy.table import Table |
82
|
|
|
>>> from gammapy.estimators import FluxPoints |
83
|
|
|
>>> from gammapy.utils.scripts import make_path |
84
|
|
|
|
85
|
|
|
>>> filename = make_path('$GAMMAPY_DATA/tests/spectrum/flux_points/flux_points_ctb_37b.txt') |
86
|
|
|
>>> table = Table.read(filename ,format='ascii.csv', delimiter=' ', comment='#') |
87
|
|
|
>>> table.rename_column('Differential_Flux', 'dnde') |
88
|
|
|
>>> table['dnde'].unit = 'cm-2 s-1 TeV-1' |
89
|
|
|
|
90
|
|
|
>>> table.rename_column('lower_error', 'dnde_errn') |
91
|
|
|
>>> table['dnde_errn'].unit = 'cm-2 s-1 TeV-1' |
92
|
|
|
|
93
|
|
|
>>> table.rename_column('upper_error', 'dnde_errp') |
94
|
|
|
>>> table['dnde_errp'].unit = 'cm-2 s-1 TeV-1' |
95
|
|
|
|
96
|
|
|
>>> table.rename_column('E', 'e_ref') |
97
|
|
|
>>> table['e_ref'].unit = 'TeV' |
98
|
|
|
|
99
|
|
|
>>> flux_points = FluxPoints.from_table(table, sed_type="dnde") |
100
|
|
|
>>> flux_points.plot(sed_type="e2dnde") #doctest: +SKIP |
101
|
|
|
|
102
|
|
|
|
103
|
|
|
Note: In order to reproduce the example you need the tests datasets folder. |
104
|
|
|
You may download it with the command |
105
|
|
|
``gammapy download datasets --tests --out $GAMMAPY_DATA`` |
106
|
|
|
""" |
107
|
|
|
|
108
|
|
|
@classmethod |
109
|
|
|
def read( |
110
|
|
|
cls, filename, sed_type=None, format="gadf-sed", reference_model=None, **kwargs |
111
|
|
|
): |
112
|
|
|
"""Read precomputed flux points. |
113
|
|
|
|
114
|
|
|
Parameters |
115
|
|
|
---------- |
116
|
|
|
filename : str |
117
|
|
|
Filename |
118
|
|
|
sed_type : {"dnde", "flux", "eflux", "e2dnde", "likelihood"} |
119
|
|
|
Sed type |
120
|
|
|
format : {"gadf-sed", "lightcurve"} |
121
|
|
|
Format string. |
122
|
|
|
reference_model : `SpectralModel` |
123
|
|
|
Reference spectral model |
124
|
|
|
**kwargs : dict |
125
|
|
|
Keyword arguments passed to `astropy.table.Table.read`. |
126
|
|
|
|
127
|
|
|
Returns |
128
|
|
|
------- |
129
|
|
|
flux_points : `FluxPoints` |
130
|
|
|
Flux points |
131
|
|
|
""" |
132
|
|
|
filename = make_path(filename) |
133
|
|
|
|
134
|
|
|
try: |
135
|
|
|
table = Table.read(filename, **kwargs) |
136
|
|
|
except IORegistryError: |
137
|
|
|
kwargs.setdefault("format", "ascii.ecsv") |
138
|
|
|
table = Table.read(filename, **kwargs) |
139
|
|
|
|
140
|
|
|
return cls.from_table( |
141
|
|
|
table=table, |
142
|
|
|
sed_type=sed_type, |
143
|
|
|
reference_model=reference_model, |
144
|
|
|
format=format, |
145
|
|
|
) |
146
|
|
|
|
147
|
|
|
def write(self, filename, sed_type=None, format="gadf-sed", **kwargs): |
148
|
|
|
"""Write flux points. |
149
|
|
|
|
150
|
|
|
Parameters |
151
|
|
|
---------- |
152
|
|
|
filename : str |
153
|
|
|
Filename |
154
|
|
|
sed_type : {"dnde", "flux", "eflux", "e2dnde", "likelihood"} |
155
|
|
|
Sed type |
156
|
|
|
format : {"gadf-sed", "lightcurve", "binned-time-series", "profile"} |
157
|
|
|
Format specification. The following formats are supported: |
158
|
|
|
|
159
|
|
|
* "gadf-sed": format for sed flux points see :ref:`gadf:flux-points` |
160
|
|
|
for details |
161
|
|
|
* "lightcurve": Gammapy internal format to store energy dependent |
162
|
|
|
lightcurves. Basically a generalisation of the "gadf" format, but |
163
|
|
|
currently there is no detailed documentation available. |
164
|
|
|
* "binned-time-series": table format support by Astropy's |
165
|
|
|
`~astropy.timeseries.BinnedTimeSeries`. |
166
|
|
|
* "profile": Gammapy internal format to store energy dependent |
167
|
|
|
flux profiles. Basically a generalisation of the "gadf" format, but |
168
|
|
|
currently there is no detailed documentation available. |
169
|
|
|
**kwargs : dict |
170
|
|
|
Keyword arguments passed to `astropy.table.Table.write`. |
171
|
|
|
""" |
172
|
|
|
if sed_type is None: |
173
|
|
|
sed_type = self.sed_type_init |
174
|
|
|
|
175
|
|
|
filename = make_path(filename) |
176
|
|
|
table = self.to_table(sed_type=sed_type, format=format) |
177
|
|
|
table.write(filename, **kwargs) |
178
|
|
|
|
179
|
|
|
@staticmethod |
180
|
|
|
def _convert_loglike_columns(table): |
181
|
|
|
# TODO: check sign and factor 2 here |
182
|
|
|
# https://github.com/gammapy/gammapy/pull/2546#issuecomment-554274318 |
183
|
|
|
# The idea below is to support the format here: |
184
|
|
|
# https://gamma-astro-data-formats.readthedocs.io/en/latest/spectra/flux_points/index.html#likelihood-columns |
185
|
|
|
# but internally to go to the uniform "stat" |
186
|
|
|
|
187
|
|
|
if "loglike" in table.colnames and "stat" not in table.colnames: |
188
|
|
|
table["stat"] = 2 * table["loglike"] |
189
|
|
|
|
190
|
|
|
if "loglike_null" in table.colnames and "stat_null" not in table.colnames: |
191
|
|
|
table["stat_null"] = 2 * table["loglike_null"] |
192
|
|
|
|
193
|
|
|
if "dloglike_scan" in table.colnames and "stat_scan" not in table.colnames: |
194
|
|
|
table["stat_scan"] = 2 * table["dloglike_scan"] |
195
|
|
|
|
196
|
|
|
return table |
197
|
|
|
|
198
|
|
|
@classmethod |
199
|
|
|
def from_table( |
200
|
|
|
cls, table, sed_type=None, format="gadf-sed", reference_model=None, gti=None |
201
|
|
|
): |
202
|
|
|
"""Create flux points from a table. The table column names must be consistent with the |
203
|
|
|
sed_type |
204
|
|
|
|
205
|
|
|
Parameters |
206
|
|
|
---------- |
207
|
|
|
table : `~astropy.table.Table` |
208
|
|
|
Table |
209
|
|
|
sed_type : {"dnde", "flux", "eflux", "e2dnde", "likelihood"} |
210
|
|
|
Sed type |
211
|
|
|
format : {"gadf-sed", "lightcurve", "profile"} |
212
|
|
|
Table format. |
213
|
|
|
reference_model : `SpectralModel` |
214
|
|
|
Reference spectral model |
215
|
|
|
gti : `GTI` |
216
|
|
|
Good time intervals |
217
|
|
|
meta : dict |
218
|
|
|
Meta data. |
219
|
|
|
|
220
|
|
|
Returns |
221
|
|
|
------- |
222
|
|
|
flux_points : `FluxPoints` |
223
|
|
|
Flux points |
224
|
|
|
""" |
225
|
|
|
table = table_standardise_units_copy(table) |
226
|
|
|
|
227
|
|
|
if sed_type is None: |
228
|
|
|
sed_type = table.meta.get("SED_TYPE", None) |
229
|
|
|
|
230
|
|
|
if sed_type is None: |
231
|
|
|
sed_type = cls._guess_sed_type(table.colnames) |
232
|
|
|
|
233
|
|
|
if sed_type is None: |
234
|
|
|
raise ValueError("Specifying the sed type is required") |
235
|
|
|
|
236
|
|
|
if sed_type == "likelihood": |
237
|
|
|
table = cls._convert_loglike_columns(table) |
238
|
|
|
if reference_model is None: |
239
|
|
|
reference_model = TemplateSpectralModel( |
240
|
|
|
energy=flat_if_equal(table["e_ref"].quantity), |
241
|
|
|
values=flat_if_equal(table["ref_dnde"].quantity), |
242
|
|
|
) |
243
|
|
|
|
244
|
|
|
maps = Maps() |
245
|
|
|
table.meta.setdefault("SED_TYPE", sed_type) |
246
|
|
|
|
247
|
|
|
for name in cls.all_quantities(sed_type=sed_type): |
248
|
|
|
if name in table.colnames: |
249
|
|
|
maps[name] = RegionNDMap.from_table( |
250
|
|
|
table=table, colname=name, format=format |
251
|
|
|
) |
252
|
|
|
|
253
|
|
|
meta = cls._get_meta_gadf(table) |
254
|
|
|
return cls.from_maps( |
255
|
|
|
maps=maps, |
256
|
|
|
reference_model=reference_model, |
257
|
|
|
meta=meta, |
258
|
|
|
sed_type=sed_type, |
259
|
|
|
gti=gti, |
260
|
|
|
) |
261
|
|
|
|
262
|
|
|
@staticmethod |
263
|
|
|
def _get_meta_gadf(table): |
264
|
|
|
meta = {} |
265
|
|
|
meta.update(table.meta) |
266
|
|
|
conf_ul = table.meta.get("UL_CONF") |
267
|
|
|
if conf_ul: |
268
|
|
|
n_sigma_ul = np.round(stats.norm.isf(0.5 * (1 - conf_ul)), 1) |
269
|
|
|
meta["n_sigma_ul"] = n_sigma_ul |
270
|
|
|
meta["sed_type_init"] = table.meta.get("SED_TYPE") |
271
|
|
|
return meta |
272
|
|
|
|
273
|
|
|
@staticmethod |
274
|
|
|
def _format_table(table): |
275
|
|
|
"""Format table""" |
276
|
|
|
for column in table.colnames: |
277
|
|
|
if column.startswith(("dnde", "eflux", "flux", "e2dnde", "ref")): |
278
|
|
|
table[column].format = ".3e" |
279
|
|
|
elif column.startswith( |
280
|
|
|
("e_min", "e_max", "e_ref", "sqrt_ts", "norm", "ts", "stat") |
281
|
|
|
): |
282
|
|
|
table[column].format = ".3f" |
283
|
|
|
|
284
|
|
|
return table |
285
|
|
|
|
286
|
|
|
def to_table(self, sed_type=None, format="gadf-sed", formatted=False): |
287
|
|
|
"""Create table for a given SED type. |
288
|
|
|
|
289
|
|
|
Parameters |
290
|
|
|
---------- |
291
|
|
|
sed_type : {"likelihood", "dnde", "e2dnde", "flux", "eflux"} |
292
|
|
|
Sed type to convert to. Default is `likelihood` |
293
|
|
|
format : {"gadf-sed", "lightcurve", "binned-time-series", "profile"} |
294
|
|
|
Format specification. The following formats are supported: |
295
|
|
|
|
296
|
|
|
* "gadf-sed": format for sed flux points see :ref:`gadf:flux-points` |
297
|
|
|
for details |
298
|
|
|
* "lightcurve": Gammapy internal format to store energy dependent |
299
|
|
|
lightcurves. Basically a generalisation of the "gadf" format, but |
300
|
|
|
currently there is no detailed documentation available. |
301
|
|
|
* "binned-time-series": table format support by Astropy's |
302
|
|
|
`~astropy.timeseries.BinnedTimeSeries`. |
303
|
|
|
* "profile": Gammapy internal format to store energy dependent |
304
|
|
|
flux profiles. Basically a generalisation of the "gadf" format, but |
305
|
|
|
currently there is no detailed documentation available. |
306
|
|
|
|
307
|
|
|
formatted : bool |
308
|
|
|
Formatted version with column formats applied. Numerical columns are |
309
|
|
|
formatted to .3f and .3e respectively. |
310
|
|
|
|
311
|
|
|
Returns |
312
|
|
|
------- |
313
|
|
|
table : `~astropy.table.Table` |
314
|
|
|
Flux points table |
315
|
|
|
|
316
|
|
|
Examples |
317
|
|
|
-------- |
318
|
|
|
|
319
|
|
|
This is how to read and plot example flux points: |
320
|
|
|
|
321
|
|
|
>>> from gammapy.estimators import FluxPoints |
322
|
|
|
>>> fp = FluxPoints.read("$GAMMAPY_DATA/hawc_crab/HAWC19_flux_points.fits") |
323
|
|
|
>>> table = fp.to_table(sed_type="flux", format="gadf-sed", formatted=True) |
324
|
|
|
>>> print(table[:2]) |
325
|
|
|
e_ref e_min e_max flux flux_err flux_ul ts sqrt_ts is_ul |
326
|
|
|
TeV TeV TeV 1 / (cm2 s) 1 / (cm2 s) 1 / (cm2 s) |
327
|
|
|
----- ----- ----- ----------- ----------- ----------- -------- ------- ----- |
328
|
|
|
1.334 1.000 1.780 1.423e-11 3.135e-13 nan 2734.000 52.288 False |
329
|
|
|
2.372 1.780 3.160 5.780e-12 1.082e-13 nan 4112.000 64.125 False |
330
|
|
|
""" |
331
|
|
|
if sed_type is None: |
332
|
|
|
sed_type = self.sed_type_init |
333
|
|
|
|
334
|
|
|
if format == "gadf-sed": |
335
|
|
|
# TODO: what to do with GTI info? |
336
|
|
|
if not self.geom.axes.names == ["energy"]: |
337
|
|
|
raise ValueError( |
338
|
|
|
"Only flux points with a single energy axis " |
339
|
|
|
"can be converted to 'gadf-sed'" |
340
|
|
|
) |
341
|
|
|
|
342
|
|
|
idx = (Ellipsis, 0, 0) |
343
|
|
|
table = self.energy_axis.to_table(format="gadf-sed") |
344
|
|
|
table.meta["SED_TYPE"] = sed_type |
345
|
|
|
|
346
|
|
|
if not self.is_convertible_to_flux_sed_type: |
347
|
|
|
table.remove_columns(["e_min", "e_max"]) |
348
|
|
|
|
349
|
|
|
if self.n_sigma_ul: |
350
|
|
|
table.meta["UL_CONF"] = np.round( |
351
|
|
|
1 - 2 * stats.norm.sf(self.n_sigma_ul), 7 |
352
|
|
|
) |
353
|
|
|
|
354
|
|
|
if sed_type == "likelihood": |
355
|
|
|
table["ref_dnde"] = self.dnde_ref[idx] |
356
|
|
|
table["ref_flux"] = self.flux_ref[idx] |
357
|
|
|
table["ref_eflux"] = self.eflux_ref[idx] |
358
|
|
|
|
359
|
|
|
for quantity in self.all_quantities(sed_type=sed_type): |
360
|
|
|
data = getattr(self, quantity, None) |
361
|
|
|
if data: |
362
|
|
|
table[quantity] = data.quantity[idx] |
363
|
|
|
|
364
|
|
|
if self.has_stat_profiles: |
365
|
|
|
norm_axis = self.stat_scan.geom.axes["norm"] |
366
|
|
|
table["norm_scan"] = norm_axis.center.reshape((1, -1)) |
367
|
|
|
table["stat"] = self.stat.data[idx] |
368
|
|
|
table["stat_scan"] = self.stat_scan.data[idx] |
369
|
|
|
|
370
|
|
|
table["is_ul"] = self.is_ul.data[idx] |
371
|
|
|
|
372
|
|
|
elif format == "lightcurve": |
373
|
|
|
time_axis = self.geom.axes["time"] |
374
|
|
|
|
375
|
|
|
tables = [] |
376
|
|
|
for idx, (time_min, time_max) in enumerate(time_axis.iter_by_edges): |
377
|
|
|
table_flat = Table() |
378
|
|
|
table_flat["time_min"] = [time_min.mjd] |
379
|
|
|
table_flat["time_max"] = [time_max.mjd] |
380
|
|
|
|
381
|
|
|
fp = self.slice_by_idx(slices={"time": idx}) |
382
|
|
|
table = fp.to_table(sed_type=sed_type, format="gadf-sed") |
383
|
|
|
|
384
|
|
|
for column in table.columns: |
385
|
|
|
table_flat[column] = table[column][np.newaxis] |
386
|
|
|
|
387
|
|
|
tables.append(table_flat) |
388
|
|
|
|
389
|
|
|
table = vstack(tables) |
390
|
|
|
elif format == "binned-time-series": |
391
|
|
|
message = ( |
392
|
|
|
"Format 'binned-time-series' support a single time axis " |
393
|
|
|
f"only. Got {self.geom.axes.names}" |
394
|
|
|
) |
395
|
|
|
|
396
|
|
|
if not self.geom.axes.is_unidimensional: |
397
|
|
|
raise ValueError(message) |
398
|
|
|
|
399
|
|
|
axis = self.geom.axes.primary_axis |
400
|
|
|
|
401
|
|
|
if not isinstance(axis, TimeMapAxis): |
402
|
|
|
raise ValueError(message) |
403
|
|
|
|
404
|
|
|
table = Table() |
405
|
|
|
table["time_bin_start"] = axis.time_min |
406
|
|
|
table["time_bin_size"] = axis.time_delta |
407
|
|
|
|
408
|
|
|
for quantity in self.all_quantities(sed_type=sed_type): |
409
|
|
|
data = getattr(self, quantity, None) |
410
|
|
|
if data: |
411
|
|
|
table[quantity] = data.quantity.squeeze() |
412
|
|
|
elif format == "profile": |
413
|
|
|
x_axis = self.geom.axes["projected-distance"] |
414
|
|
|
|
415
|
|
|
tables = [] |
416
|
|
|
for idx, (x_min, x_max) in enumerate(x_axis.iter_by_edges): |
417
|
|
|
table_flat = Table() |
418
|
|
|
table_flat["x_min"] = [x_min] |
419
|
|
|
table_flat["x_max"] = [x_max] |
420
|
|
|
table_flat["x_ref"] = [(x_max + x_min) / 2] |
421
|
|
|
|
422
|
|
|
fp = self.slice_by_idx(slices={"projected-distance": idx}) |
423
|
|
|
table = fp.to_table(sed_type=sed_type, format="gadf-sed") |
424
|
|
|
|
425
|
|
|
for column in table.columns: |
426
|
|
|
table_flat[column] = table[column][np.newaxis] |
427
|
|
|
|
428
|
|
|
tables.append(table_flat) |
429
|
|
|
|
430
|
|
|
table = vstack(tables) |
431
|
|
|
|
432
|
|
|
else: |
433
|
|
|
raise ValueError(f"Not a supported format {format}") |
434
|
|
|
|
435
|
|
|
if formatted: |
436
|
|
|
table = self._format_table(table=table) |
437
|
|
|
|
438
|
|
|
return table |
439
|
|
|
|
440
|
|
|
@staticmethod |
441
|
|
|
def _energy_ref_lafferty(model, energy_min, energy_max): |
442
|
|
|
"""Helper for `to_sed_type`. |
443
|
|
|
|
444
|
|
|
Compute energy_ref that the value at energy_ref corresponds |
445
|
|
|
to the mean value between energy_min and energy_max. |
446
|
|
|
""" |
447
|
|
|
flux = model.integral(energy_min, energy_max) |
448
|
|
|
dnde_mean = flux / (energy_max - energy_min) |
449
|
|
|
return model.inverse(dnde_mean) |
450
|
|
|
|
451
|
|
|
def _plot_get_flux_err(self, sed_type=None): |
452
|
|
|
"""Compute flux error for given sed type""" |
453
|
|
|
y_errn, y_errp = None, None |
454
|
|
|
|
455
|
|
|
if "norm_err" in self.available_quantities: |
456
|
|
|
# symmetric error |
457
|
|
|
y_errn = getattr(self, sed_type + "_err") |
458
|
|
|
y_errp = y_errn.copy() |
459
|
|
|
|
460
|
|
|
if "norm_errp" in self.available_quantities: |
461
|
|
|
y_errn = getattr(self, sed_type + "_errn") |
462
|
|
|
y_errp = getattr(self, sed_type + "_errp") |
463
|
|
|
|
464
|
|
|
return y_errn, y_errp |
465
|
|
|
|
466
|
|
|
def plot(self, ax=None, sed_type=None, energy_power=0, **kwargs): |
467
|
|
|
"""Plot flux points. |
468
|
|
|
|
469
|
|
|
Parameters |
470
|
|
|
---------- |
471
|
|
|
ax : `~matplotlib.axes.Axes` |
472
|
|
|
Axis object to plot on. |
473
|
|
|
sed_type : {"dnde", "flux", "eflux", "e2dnde"} |
474
|
|
|
Sed type |
475
|
|
|
energy_power : float |
476
|
|
|
Power of energy to multiply flux axis with |
477
|
|
|
**kwargs : dict |
478
|
|
|
Keyword arguments passed to `~RegionNDMap.plot` |
479
|
|
|
|
480
|
|
|
Returns |
481
|
|
|
------- |
482
|
|
|
ax : `~matplotlib.axes.Axes` |
483
|
|
|
Axis object |
484
|
|
|
""" |
485
|
|
|
if sed_type is None: |
486
|
|
|
sed_type = self.sed_type_plot_default |
487
|
|
|
|
488
|
|
|
if not self.norm.geom.is_region: |
489
|
|
|
raise ValueError("Plotting only supported for region based flux points") |
490
|
|
|
|
491
|
|
|
if ax is None: |
492
|
|
|
ax = plt.gca() |
493
|
|
|
|
494
|
|
|
flux_unit = DEFAULT_UNIT[sed_type] |
495
|
|
|
|
496
|
|
|
flux = getattr(self, sed_type) |
497
|
|
|
|
498
|
|
|
# get errors and ul |
499
|
|
|
y_errn, y_errp = self._plot_get_flux_err(sed_type=sed_type) |
500
|
|
|
is_ul = self.is_ul.data |
501
|
|
|
|
502
|
|
|
if self.has_ul and y_errn and is_ul.any(): |
503
|
|
|
flux_ul = getattr(self, sed_type + "_ul").quantity |
504
|
|
|
y_errn.data[is_ul] = np.clip( |
505
|
|
|
0.5 * flux_ul[is_ul].to_value(y_errn.unit), 0, np.inf |
506
|
|
|
) |
507
|
|
|
y_errp.data[is_ul] = 0 |
508
|
|
|
flux.data[is_ul] = flux_ul[is_ul].to_value(flux.unit) |
509
|
|
|
kwargs.setdefault("uplims", is_ul) |
510
|
|
|
|
511
|
|
|
# set flux points plotting defaults |
512
|
|
|
if y_errp and y_errn: |
513
|
|
|
y_errp = np.clip( |
514
|
|
|
scale_plot_flux(y_errp, energy_power=energy_power).quantity, 0, np.inf |
515
|
|
|
) |
516
|
|
|
y_errn = np.clip( |
517
|
|
|
scale_plot_flux(y_errn, energy_power=energy_power).quantity, 0, np.inf |
518
|
|
|
) |
519
|
|
|
kwargs.setdefault("yerr", (y_errn, y_errp)) |
520
|
|
|
else: |
521
|
|
|
kwargs.setdefault("yerr", None) |
522
|
|
|
|
523
|
|
|
flux = scale_plot_flux(flux=flux.to_unit(flux_unit), energy_power=energy_power) |
524
|
|
|
ax = flux.plot(ax=ax, **kwargs) |
525
|
|
|
ax.set_ylabel(f"{sed_type} [{ax.yaxis.units}]") |
526
|
|
|
ax.set_yscale("log") |
527
|
|
|
return ax |
528
|
|
|
|
529
|
|
|
def plot_ts_profiles( |
530
|
|
|
self, |
531
|
|
|
ax=None, |
532
|
|
|
sed_type=None, |
533
|
|
|
add_cbar=True, |
534
|
|
|
**kwargs, |
535
|
|
|
): |
536
|
|
|
"""Plot fit statistic SED profiles as a density plot. |
537
|
|
|
|
538
|
|
|
Parameters |
539
|
|
|
---------- |
540
|
|
|
ax : `~matplotlib.axes.Axes` |
541
|
|
|
Axis object to plot on. |
542
|
|
|
sed_type : {"dnde", "flux", "eflux", "e2dnde"} |
543
|
|
|
Sed type |
544
|
|
|
add_cbar : bool |
545
|
|
|
Whether to add a colorbar to the plot. |
546
|
|
|
**kwargs : dict |
547
|
|
|
Keyword arguments passed to `~matplotlib.pyplot.pcolormesh` |
548
|
|
|
|
549
|
|
|
Returns |
550
|
|
|
------- |
551
|
|
|
ax : `~matplotlib.axes.Axes` |
552
|
|
|
Axis object |
553
|
|
|
""" |
554
|
|
|
if ax is None: |
555
|
|
|
ax = plt.gca() |
556
|
|
|
|
557
|
|
|
if sed_type is None: |
558
|
|
|
sed_type = self.sed_type_plot_default |
559
|
|
|
|
560
|
|
|
if not self.norm.geom.is_region: |
561
|
|
|
raise ValueError("Plotting only supported for region based flux points") |
562
|
|
|
|
563
|
|
|
if not self.geom.axes.is_unidimensional: |
564
|
|
|
raise ValueError( |
565
|
|
|
"Profile plotting is only supported for unidimensional maps" |
566
|
|
|
) |
567
|
|
|
|
568
|
|
|
axis = self.geom.axes.primary_axis |
569
|
|
|
|
570
|
|
|
if isinstance(axis, TimeMapAxis) and not axis.is_contiguous: |
571
|
|
|
axis = axis.to_contiguous() |
572
|
|
|
|
573
|
|
|
if ax.yaxis.units is None: |
574
|
|
|
yunits = DEFAULT_UNIT[sed_type] |
575
|
|
|
else: |
576
|
|
|
yunits = ax.yaxis.units |
577
|
|
|
|
578
|
|
|
ax.yaxis.set_units(yunits) |
579
|
|
|
|
580
|
|
|
flux_ref = getattr(self, sed_type + "_ref").to(yunits) |
581
|
|
|
|
582
|
|
|
ts = self.ts_scan |
583
|
|
|
|
584
|
|
|
norm_min, norm_max = ts.geom.axes["norm"].bounds.to_value("") |
585
|
|
|
|
586
|
|
|
flux = MapAxis.from_bounds( |
587
|
|
|
norm_min * flux_ref.value.min(), |
588
|
|
|
norm_max * flux_ref.value.max(), |
589
|
|
|
nbin=500, |
590
|
|
|
interp=axis.interp, |
591
|
|
|
unit=flux_ref.unit, |
592
|
|
|
) |
593
|
|
|
|
594
|
|
|
norm = flux.center / flux_ref.reshape((-1, 1)) |
595
|
|
|
|
596
|
|
|
coords = ts.geom.get_coord() |
597
|
|
|
coords["norm"] = norm |
598
|
|
|
coords[axis.name] = axis.center.reshape((-1, 1)) |
599
|
|
|
|
600
|
|
|
z = ts.interp_by_coord(coords, values_scale="stat-profile") |
601
|
|
|
|
602
|
|
|
kwargs.setdefault("vmax", 0) |
603
|
|
|
kwargs.setdefault("vmin", -4) |
604
|
|
|
kwargs.setdefault("zorder", 0) |
605
|
|
|
kwargs.setdefault("cmap", "Blues") |
606
|
|
|
kwargs.setdefault("linewidths", 0) |
607
|
|
|
kwargs.setdefault("shading", "auto") |
608
|
|
|
|
609
|
|
|
# clipped values are set to NaN so that they appear white on the plot |
610
|
|
|
z[-z < kwargs["vmin"]] = np.nan |
611
|
|
|
|
612
|
|
|
with quantity_support(): |
613
|
|
|
caxes = ax.pcolormesh(axis.as_plot_edges, flux.edges, -z.T, **kwargs) |
614
|
|
|
|
615
|
|
|
axis.format_plot_xaxis(ax=ax) |
616
|
|
|
|
617
|
|
|
ax.set_ylabel(f"{sed_type} ({ax.yaxis.units})") |
618
|
|
|
ax.set_yscale("log") |
619
|
|
|
|
620
|
|
|
if add_cbar: |
621
|
|
|
label = "Fit statistic difference" |
622
|
|
|
ax.figure.colorbar(caxes, ax=ax, label=label) |
623
|
|
|
|
624
|
|
|
return ax |
625
|
|
|
|
626
|
|
|
def recompute_ul(self, n_sigma_ul=2, **kwargs): |
627
|
|
|
"""Recompute upper limits corresponding to the given value. |
628
|
|
|
The pre-computed stat profiles must exist for the re-computation. |
629
|
|
|
|
630
|
|
|
Parameters |
631
|
|
|
---------- |
632
|
|
|
n_sigma_ul : int |
633
|
|
|
Number of sigma to use for upper limit computation. Default is 2. |
634
|
|
|
**kwargs : dict |
635
|
|
|
Keyword arguments passed to `~scipy.optimize.brentq`. |
636
|
|
|
|
637
|
|
|
Returns |
638
|
|
|
------- |
639
|
|
|
flux_points : `FluxPoints` |
640
|
|
|
A new FluxPoints object with modified upper limits |
641
|
|
|
|
642
|
|
|
Examples |
643
|
|
|
-------- |
644
|
|
|
>>> from gammapy.estimators import FluxPoints |
645
|
|
|
>>> filename = '$GAMMAPY_DATA/tests/spectrum/flux_points/binlike.fits' |
646
|
|
|
>>> flux_points = FluxPoints.read(filename) |
647
|
|
|
>>> flux_points_recomputed = flux_points.recompute_ul(n_sigma_ul=3) |
648
|
|
|
>>> print(flux_points.meta["n_sigma_ul"], flux_points.flux_ul.data[0]) |
649
|
|
|
2.0 [[3.95451985e-09]] |
650
|
|
|
>>> print(flux_points_recomputed.meta["n_sigma_ul"], flux_points_recomputed.flux_ul.data[0]) |
651
|
|
|
3 [[6.22245374e-09]] |
652
|
|
|
""" |
653
|
|
|
|
654
|
|
|
if not self.has_stat_profiles: |
655
|
|
|
raise ValueError( |
656
|
|
|
"Stat profiles not present. Upper limit computation is not possible" |
657
|
|
|
) |
658
|
|
|
|
659
|
|
|
delta_ts = n_sigma_ul**2 |
660
|
|
|
|
661
|
|
|
flux_points = deepcopy(self) |
662
|
|
|
|
663
|
|
|
value_scan = self.stat_scan.geom.axes["norm"].center |
664
|
|
|
shape_axes = self.stat_scan.geom._shape[slice(3, None)] |
665
|
|
|
for idx in np.ndindex(shape_axes): |
666
|
|
|
stat_scan = np.abs( |
667
|
|
|
self.stat_scan.data[idx].squeeze() - self.stat.data[idx].squeeze() |
668
|
|
|
) |
669
|
|
|
flux_points.norm_ul.data[idx] = stat_profile_ul_scipy( |
670
|
|
|
value_scan, stat_scan, delta_ts=delta_ts, **kwargs |
671
|
|
|
) |
672
|
|
|
flux_points.meta["n_sigma_ul"] = n_sigma_ul |
673
|
|
|
return flux_points |
674
|
|
|
|