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

Estimation of the CTA point source sensitivity

Introduction

This notebook explains how to estimate the CTA sensitivity for a point-like IRF at a fixed zenith angle and fixed offset using the full containement IRFs distributed for the CTA 1DC. The significativity is computed for a 1D analysis (On-OFF regions) and the LiMa formula.

We use here an approximate approach with an energy dependent integration radius to take into account the variation of the PSF. We will first determine the 1D IRFs including a containment correction.

We will be using the following Gammapy class:

Setup

As usual, we’ll start with some setup …

[1]:
%matplotlib inline
import matplotlib.pyplot as plt
[2]:
import numpy as np
import astropy.units as u
from astropy.coordinates import Angle
from gammapy.irf import load_cta_irfs
from gammapy.spectrum import SensitivityEstimator, CountsSpectrum

Define analysis region and energy binning

Here we assume a source at 0.7 degree from pointing position. We perform a simple energy independent extraction for now with a radius of 0.1 degree.

[3]:
offset = Angle("0.5 deg")

energy_reco = np.logspace(-1.8, 1.5, 20) * u.TeV
energy_true = np.logspace(-2, 2, 100) * u.TeV

Load IRFs

We extract the 1D IRFs from the full 3D IRFs provided by CTA.

[4]:
filename = (
    "$GAMMAPY_DATA/cta-1dc/caldb/data/cta/1dc/bcf/South_z20_50h/irf_file.fits"
)
irfs = load_cta_irfs(filename)
arf = irfs["aeff"].to_effective_area_table(offset, energy=energy_true)
rmf = irfs["edisp"].to_energy_dispersion(
    offset, e_true=energy_true, e_reco=energy_reco
)
psf = irfs["psf"].to_energy_dependent_table_psf(theta=offset)

Determine energy dependent integration radius

Here we will determine an integration radius that varies with the energy to ensure a constant fraction of flux enclosure (e.g. 68%). We then apply the fraction to the effective area table.

By doing so we implicitly assume that energy dispersion has a neglible effect. This should be valid for large enough energy reco bins as long as the bias in the energy estimation is close to zero.

[5]:
containment = 0.68
energy = np.sqrt(energy_reco[1:] * energy_reco[:-1])
on_radii = psf.containment_radius(energy=energy, fraction=containment)
solid_angles = 2 * np.pi * (1 - np.cos(on_radii)) * u.sr
[6]:
arf.data.data *= containment

Estimate background

We now provide a workaround to estimate the background from the tabulated IRF in the energy bins we consider.

[7]:
bkg_data = irfs["bkg"].evaluate_integrate(
    fov_lon=0 * u.deg, fov_lat=offset, energy_reco=energy_reco
)
bkg = CountsSpectrum(
    energy_reco[:-1],
    energy_reco[1:],
    data=(bkg_data * solid_angles).to_value("s-1"),
    unit="s-1",
)

Compute sensitivity

We impose a minimal number of expected signal counts of 5 per bin and a minimal significance of 3 per bin. We assume an alpha of 0.2 (ratio between ON and OFF area). We then run the sensitivity estimator.

[8]:
sensitivity_estimator = SensitivityEstimator(
    arf=arf, rmf=rmf, bkg=bkg, livetime="5h", gamma_min=5, sigma=3, alpha=0.2
)
sensitivity_table = sensitivity_estimator.run()

Results

The results are given as an Astropy table. A column criterion allows to distinguish bins where the significance is limited by the signal statistical significance from bins where the sensitivity is limited by the number of signal counts. This is visible in the plot below.

[9]:
# Show the results table
sensitivity_table
[9]:
Table length=19
energye2dndeexcessbackgroundcriterion
TeVerg / (cm2 s)
float64float64float64float64str12
0.01935723.77719e-11142.5531826.68significance
0.02887531.0647e-11220.6074420.96significance
0.04307357.74208e-12180.2282938.04significance
0.06425324.22411e-12111.9851118.13significance
0.09584711.73695e-1275.5441499.549significance
0.1429761.23576e-1250.3005215.216significance
0.2132798.16227e-1331.625280.8063significance
0.318155.57441e-1320.714932.1657significance
0.4745874.1604e-1313.996313.1734significance
0.7079463.25327e-139.720055.43152significance
1.056052.64287e-137.197562.4464significance
1.575322.29178e-136.002561.44036significance
2.349921.90235e-135.14260.878003significance
3.505392.09706e-1350.522997gamma
5.229032.62358e-1350.337574gamma
7.800193.51438e-1350.161236gamma
11.63565.19109e-1350.0682205gamma
17.3576.78149e-1350.0365443gamma
25.89151.05731e-1250.0155002gamma
[10]:
# Save it to file (could use e.g. format of CSV or ECSV or FITS)
# sensitivity_table.write('sensitivity.ecsv', format='ascii.ecsv')
[11]:
# Plot the sensitivity curve
t = sensitivity_estimator.results_table

is_s = t["criterion"] == "significance"
plt.plot(
    t["energy"][is_s],
    t["e2dnde"][is_s],
    "s-",
    color="red",
    label="significance",
)

is_g = t["criterion"] == "gamma"
plt.plot(
    t["energy"][is_g], t["e2dnde"][is_g], "*-", color="blue", label="gamma"
)

plt.loglog()
plt.xlabel("Energy ({})".format(t["energy"].unit))
plt.ylabel("Sensitivity ({})".format(t["e2dnde"].unit))
plt.legend();
../_images/notebooks_cta_sensitivity_20_0.png

We add some control plots showing the expected number of background counts per bin and the ON region size cut (here the 68% containment radius of the PSF).

[12]:
# Plot expected number of counts for signal and background
fig, ax1 = plt.subplots()
# ax1.plot( t["energy"], t["excess"],"o-", color="red", label="signal")
ax1.plot(
    t["energy"], t["background"], "o-", color="black", label="blackground"
)

ax1.loglog()
ax1.set_xlabel("Energy ({})".format(t["energy"].unit))
ax1.set_ylabel("Expected number of bkg counts")

ax2 = ax1.twinx()
ax2.set_ylabel("ON region radius ({})".format(on_radii.unit), color="red")
ax2.semilogy(t["energy"], on_radii, color="red", label="PSF68")
ax2.tick_params(axis="y", labelcolor="red")
ax2.set_ylim(0.01, 0.5)
[12]:
(0.01, 0.5)
../_images/notebooks_cta_sensitivity_22_1.png

Exercises

  • Also compute the sensitivity for a 20 hour observation
  • Compare how the sensitivity differs between 5 and 20 hours by plotting the ratio as a function of energy.
[ ]: