This is a fixed-text formatted version of a Jupyter notebook

Light curve estimation

Prerequisites

Context

This tutorial presents how light curve extraction is performed in gammapy, i.e. how to measure the flux of a source in different time bins.

Cherenkov telescopes usually work with observing runs and distribute data according to this basic time interval. A typical use case is to look for variability of a source on various time binnings: observation run-wise binning, nightly, weekly etc.

Objective: The Crab nebula is not known to be variable at TeV energies, so we expect constant brightness within statistical and systematic errors. Compute per-observation and nightly fluxes of the four Crab nebula observations from theH.E.S.S. first public test data releaseto check it.

Proposed approach

We will demonstrate how to compute a gammapy.estimators.LightCurve from 3D reduced datasets (gammapy.datasets.MapDataset) as well as 1D ON-OFF spectral datasets (gammapy.datasets.SpectrumDatasetOnOff).

The data reduction will be performed with the high level interface for the data reduction. Then we will use the gammapy.estimators.LightCurveEstimator class, which is able to extract a light curve independently of the dataset type.

Setup

As usual, we’ll start with some general imports…

[1]:
%matplotlib inline
import matplotlib.pyplot as plt

import astropy.units as u
from astropy.coordinates import SkyCoord
import logging

from astropy.time import Time

log = logging.getLogger(__name__)

Now let’s import gammapy specific classes and functions

[2]:
from gammapy.modeling.models import PowerLawSpectralModel
from gammapy.modeling.models import PointSpatialModel
from gammapy.modeling.models import SkyModel, Models
from gammapy.estimators import LightCurveEstimator
from gammapy.analysis import Analysis, AnalysisConfig

Analysis configuration

For the 1D and 3D extraction, we will use the same CrabNebula configuration than in the notebook analysis_1.ipynb using the high level interface of Gammapy.

From the high level interface, the data reduction for those observations is performed as followed

Building the 3D analysis configuration

[3]:
conf_3d = AnalysisConfig()

Definition of the data selection

Here we use the Crab runs from the HESS DL3 data release 1

[4]:
conf_3d.observations.obs_ids = [23523, 23526, 23559, 23592]

Definition of the dataset geometry

[5]:
# We want a 3D analysis
conf_3d.datasets.type = "3d"

# We want to extract the data by observation and therefore to not stack them
conf_3d.datasets.stack = False

# Here is the WCS geometry of the Maps
conf_3d.datasets.geom.wcs.skydir = dict(
    frame="icrs", lon=83.63308 * u.deg, lat=22.01450 * u.deg
)
conf_3d.datasets.geom.wcs.binsize = 0.02 * u.deg
conf_3d.datasets.geom.wcs.fov = dict(width=1 * u.deg, height=1 * u.deg)

# We define a value for the IRF Maps binsize
conf_3d.datasets.geom.wcs.binsize_irf = 0.2 * u.deg

# Define energy binning for the Maps
conf_3d.datasets.geom.axes.energy = dict(
    min=0.7 * u.TeV, max=10 * u.TeV, nbins=5
)
conf_3d.datasets.geom.axes.energy_true = dict(
    min=0.3 * u.TeV, max=20 * u.TeV, nbins=20
)

Run the 3D data reduction

[6]:
analysis_3d = Analysis(conf_3d)
analysis_3d.get_observations()
analysis_3d.get_datasets()
Setting logging config: {'level': 'INFO', 'filename': None, 'filemode': None, 'format': None, 'datefmt': None}
Fetching observations.
Number of selected observations: 4
Creating geometry.
Creating datasets.
No background maker set for 3d analysis. Check configuration.
Processing observation 23523
Processing observation 23526
Processing observation 23559
Processing observation 23592

Define the model to be used

Here we don’t try to fit the model parameters to the whole dataset, but we use predefined values instead.

[7]:
target_position = SkyCoord(ra=83.63308, dec=22.01450, unit="deg")
spatial_model = PointSpatialModel(
    lon_0=target_position.ra, lat_0=target_position.dec, frame="icrs"
)

spectral_model = PowerLawSpectralModel(
    index=2.702,
    amplitude=4.712e-11 * u.Unit("1 / (cm2 s TeV)"),
    reference=1 * u.TeV,
)

sky_model = SkyModel(
    spatial_model=spatial_model, spectral_model=spectral_model, name="crab"
)
# Now we freeze these parameters that we don't want the light curve estimator to change
sky_model.parameters["index"].frozen = True
sky_model.parameters["lon_0"].frozen = True
sky_model.parameters["lat_0"].frozen = True

We assign them the model to be fitted to each dataset

[8]:
models = Models([sky_model])
analysis_3d.set_models(models)
Reading model.
Models

Component 0: SkyModel

  Name                      : crab
  Datasets names            : None
  Spectral model type       : PowerLawSpectralModel
  Spatial  model type       : PointSpatialModel
  Temporal model type       :
  Parameters:
    index        (frozen)   :   2.702
    amplitude               :   4.71e-11  1 / (cm2 s TeV)
    reference    (frozen)   :   1.000  TeV
    lon_0        (frozen)   :  83.633  deg
    lat_0        (frozen)   :  22.015  deg

Component 1: FoVBackgroundModel

  Name                      : RjmLjX-f-bkg
  Datasets names            : ['RjmLjX-f']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV

Component 2: FoVBackgroundModel

  Name                      : eYljoNDR-bkg
  Datasets names            : ['eYljoNDR']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV

Component 3: FoVBackgroundModel

  Name                      : wFM3iTby-bkg
  Datasets names            : ['wFM3iTby']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV

Component 4: FoVBackgroundModel

  Name                      : hNYYRmcl-bkg
  Datasets names            : ['hNYYRmcl']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV


Light Curve estimation: by observation

We can now create the light curve estimator.

We pass it the list of datasets and the name of the model component for which we want to build the light curve. We can optionally ask for parameters reoptimization during fit, that is most of the time to fit background normalization in each time bin.

If we don’t set any time interval, the ~gammapy.time.LightCurveEstimator is determines the flux of each dataset and places it at the corresponding time in the light curve. Here one dataset equals to one observing run.

[9]:
lc_maker_3d = LightCurveEstimator(
    energy_edges=[1, 10] * u.TeV, source="crab", reoptimize=False
)
lc_3d = lc_maker_3d.run(analysis_3d.datasets)

The LightCurve object contains a table which we can explore.

[10]:
lc_3d.table["time_min", "time_max", "e_min", "e_max", "flux", "flux_err"]
[10]:
Table length=4
time_mintime_maxe_min [1]e_max [1]flux [1]flux_err [1]
TeVTeV1 / (cm2 s)1 / (cm2 s)
float64float64float64float64float64float64
53343.9223400925953343.941865555551.19145716144943710.0000000000000022.0044854222687593e-112.0855480821587682e-12
53343.9542150925953343.973694259261.19145716144943710.0000000000000022.034368084890059e-112.112697965713362e-12
53345.9619812962953345.981495185181.19145716144943710.0000000000000022.1489523572686956e-112.7480983271873764e-12
53347.9131965740753347.9327104629561.19145716144943710.0000000000000022.475608406609803e-112.8758307941587e-12

Running the light curve extraction in 1D

Building the 1D analysis configuration

[11]:
conf_1d = AnalysisConfig()

Definition of the data selection

Here we use the Crab runs from the HESS DL3 data release 1

[12]:
conf_1d.observations.obs_ids = [23523, 23526, 23559, 23592]

Definition of the dataset geometry

[13]:
# We want a 1D analysis
conf_1d.datasets.type = "1d"

# We want to extract the data by observation and therefore to not stack them
conf_1d.datasets.stack = False

# Here we define the ON region and make sure that PSF leakage is corrected
conf_1d.datasets.on_region = dict(
    frame="icrs",
    lon=83.63308 * u.deg,
    lat=22.01450 * u.deg,
    radius=0.1 * u.deg,
)
conf_1d.datasets.containment_correction = True

# Finally we define the energy binning for the spectra
conf_1d.datasets.geom.axes.energy = dict(
    min=0.7 * u.TeV, max=10 * u.TeV, nbins=5
)
conf_1d.datasets.geom.axes.energy_true = dict(
    min=0.3 * u.TeV, max=20 * u.TeV, nbins=20
)

Run the 1D data reduction

[14]:
analysis_1d = Analysis(conf_1d)
analysis_1d.get_observations()
analysis_1d.get_datasets()
Setting logging config: {'level': 'INFO', 'filename': None, 'filemode': None, 'format': None, 'datefmt': None}
Fetching observations.
Number of selected observations: 4
Reducing spectrum datasets.
No background maker set for 1d analysis. Check configuration.
Processing observation 23523
Processing observation 23526
Processing observation 23559
Processing observation 23592

Define the model to be used

Here we don’t try to fit the model parameters to the whole dataset, but we use predefined values instead.

[15]:
target_position = SkyCoord(ra=83.63308, dec=22.01450, unit="deg")
spatial_model = PointSpatialModel(
    lon_0=target_position.ra, lat_0=target_position.dec, frame="icrs"
)

spectral_model = PowerLawSpectralModel(
    index=2.702,
    amplitude=4.712e-11 * u.Unit("1 / (cm2 s TeV)"),
    reference=1 * u.TeV,
)

sky_model = SkyModel(
    spatial_model=spatial_model, spectral_model=spectral_model, name="crab"
)
# Now we freeze these parameters that we don't want the light curve estimator to change
sky_model.parameters["index"].frozen = True
sky_model.parameters["lon_0"].frozen = True
sky_model.parameters["lat_0"].frozen = True

We assign the model to be fitted to each dataset. We can use the same gammapy.modeling.models.SkyModel as before.

[16]:
models = Models([sky_model])
analysis_1d.set_models(models)
Reading model.
Models

Component 0: SkyModel

  Name                      : crab
  Datasets names            : None
  Spectral model type       : PowerLawSpectralModel
  Spatial  model type       : PointSpatialModel
  Temporal model type       :
  Parameters:
    index        (frozen)   :   2.702
    amplitude               :   4.71e-11  1 / (cm2 s TeV)
    reference    (frozen)   :   1.000  TeV
    lon_0        (frozen)   :  83.633  deg
    lat_0        (frozen)   :  22.015  deg

Component 1: FoVBackgroundModel

  Name                      : 2N87TEky-bkg
  Datasets names            : ['2N87TEky']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV

Component 2: FoVBackgroundModel

  Name                      : O5nF3EPi-bkg
  Datasets names            : ['O5nF3EPi']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV

Component 3: FoVBackgroundModel

  Name                      : 6le8ANvE-bkg
  Datasets names            : ['6le8ANvE']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV

Component 4: FoVBackgroundModel

  Name                      : lPQl_QF2-bkg
  Datasets names            : ['lPQl_QF2']
  Spectral model type       : PowerLawNormSpectralModel
  Parameters:
    norm                    :   1.000
    tilt         (frozen)   :   0.000
    reference    (frozen)   :   1.000  TeV


Extracting the light curve

[17]:
lc_maker_1d = LightCurveEstimator(
    energy_edges=[1, 10] * u.TeV, source="crab", reoptimize=False
)
lc_1d = lc_maker_1d.run(analysis_1d.datasets)
[18]:
lc_1d.table
[18]:
Table length=4
time_mintime_maxcounts [1]e_ref [1]e_min [1]e_max [1]ref_dnde [1]ref_flux [1]ref_eflux [1]ref_e2dnde [1]norm [1]stat [1]success [1]norm_err [1]ts [1]norm_errp [1]norm_errn [1]norm_ul [1]norm_scan [1,11]stat_scan [1,11]sqrt_ts [1]dnde [1]dnde_ul [1]dnde_err [1]dnde_errp [1]dnde_errn [1]flux [1]flux_ul [1]flux_err [1]flux_errp [1]flux_errn [1]
TeVTeVTeV1 / (cm2 s TeV)1 / (cm2 s)TeV / (cm2 s)TeV / (cm2 s)1 / (cm2 s TeV)1 / (cm2 s TeV)1 / (cm2 s TeV)1 / (cm2 s TeV)1 / (cm2 s TeV)1 / (cm2 s)1 / (cm2 s)1 / (cm2 s)1 / (cm2 s)1 / (cm2 s)
float64float64float64float64float64float64float64float64float64float64float64float64boolfloat64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64
53343.9223400925953343.9418655555581.03.45174906598008221.19145716144943710.0000000000000021.657384679989307e-121.9997707038808555e-114.6024352468667415e-111.9747028462498433e-111.1314281414233847-344.2582794967987True0.12571420584955773344.25827949679870.130417663538524660.121099314227694061.40183442174677890.20000000000000004 .. 5.000000000000001-196.8873194760846 .. -31.08135702280087318.55419843315251.8752116681038928e-122.32337889448478e-122.083567988320791e-132.161522375487508e-132.0070814815819124e-132.26259685076485e-112.803347408300968e-112.513995859195928e-122.6080542281293204e-122.4217086085260464e-12
53343.9542150925953343.9736942592671.03.45174906598008221.19145716144943710.0000000000000021.657384679989307e-121.9997707038808555e-114.6024352468667415e-111.9747028462498433e-110.9635779683303554-290.1475581681106True0.11435563703045191290.14755816811060.118919580505219890.109867474163032431.21066533237942830.20000000000000004 .. 5.000000000000001-179.4084999735158 .. 70.91337332566817.033718271948451.5970193626859525e-122.0065381744798268e-121.8953128088468887e-131.970954908801065e-131.8209266850693094e-131.9269349919720795e-112.4210530638965592e-112.2868505275713047e-122.3781189321213964e-122.1970975614061908e-12
53345.9619812962953345.9814951851851.03.45174906598008221.19145716144943710.0000000000000021.657384679989307e-121.9997707038808555e-114.6024352468667415e-111.9747028462498433e-111.103895504711545-194.54801386828967True0.15457625303314776194.548013868289670.1618814750354410.147437790176811381.44256458502635440.20000000000000004 .. 5.000000000000001-103.82267487209911 .. 11.36988387172065613.948046955337141.8295794978179784e-122.390884443117812e-122.5619231366728974e-132.682998766978114e-132.443611346905251e-132.2075378904679187e-112.884798395591747e-113.091170623313631e-123.23725831276895e-122.9484177344052e-12
53347.9131965740753347.93271046295659.03.45174906598008221.19145716144943710.0000000000000021.657384679989307e-121.9997707038808555e-114.6024352468667415e-111.9747028462498433e-111.269167746126186-226.72586038069366True0.16523152789708936226.725860380693660.172485857485143360.15813441280894741.62893447618278550.20000000000000004 .. 5.000000000000001-108.08941745406358 .. -41.6457395750171815.0574187821383812.103499178766099e-122.6997710455317554e-122.738522029878617e-132.8587541771069554e-132.6208955316865427e-132.538044477013642e-113.2574954440118416e-113.304251688460716e-123.4493216463255808e-123.162325660107345e-12

Compare results

Finally we compare the result for the 1D and 3D lightcurve in a single figure:

[19]:
ax = lc_1d.plot(marker="o", label="1D")
lc_3d.plot(ax=ax, marker="o", label="3D")
plt.legend()
[19]:
<matplotlib.legend.Legend at 0x12015def0>
../_images/tutorials_light_curve_40_1.png

Night-wise LC estimation

Here we want to extract a night curve per night. We define the time intervals that cover the three nights.

[20]:
time_intervals = [
    Time([53343.5, 53344.5], format="mjd", scale="utc"),
    Time([53345.5, 53346.5], format="mjd", scale="utc"),
    Time([53347.5, 53348.5], format="mjd", scale="utc"),
]

To compute the LC on the time intervals defined above, we pass the LightCurveEstimator the list of time intervals.

Internally, datasets are grouped per time interval and a flux extraction is performed for each group.

[21]:
lc_maker_1d = LightCurveEstimator(
    energy_edges=[1, 10] * u.TeV,
    time_intervals=time_intervals,
    source="crab",
    reoptimize=False,
)

nightwise_lc = lc_maker_1d.run(analysis_1d.datasets)

nightwise_lc.plot()
No handles with labels found to put in legend.
[21]:
<matplotlib.axes._subplots.AxesSubplot at 0x11fe2aa20>
../_images/tutorials_light_curve_44_2.png

What next?

When sources are bight enough to look for variability at small time scales, the per-observation time binning is no longer relevant. One can easily extend the light curve estimation approach presented above to any time binning. This is demonstrated in the following tutorial which shows the extraction of the lightcurve of an AGN flare.

[ ]: