FluxPoints.to_table()   F
last analyzed

Complexity

Conditions 22

Size

Total Lines 153
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
eloc 75
nop 4
dl 0
loc 153
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like gammapy.estimators.points.core.FluxPoints.to_table() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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