Sky scan

This tutorial shows how to produce a Sky scan.

For analysis setup details check the Fitting a steady point-source with the public 14-year IceCube track data tutorial.

[1]:
import numpy as np

import skyllh
from skyllh.analyses.i3.publicdata_ps.time_integrated_ps import create_analysis
from skyllh.core.config import Config
from skyllh.core.source_model import PointLikeSource

cfg = Config()
datasets = skyllh.create_datasets('IceTracks-DR2', cfg=cfg)

src_ra = 40.67  # degrees
src_dec = -0.01  # degrees

source = PointLikeSource(ra=np.radians(src_ra), dec=np.radians(src_dec))

ana = create_analysis(cfg=cfg, datasets=datasets, source=source)
100%|██████████| 33/33 [00:00<00:00, 597.16it/s]
100%|██████████| 33/33 [00:00<00:00, 586.56it/s]
100%|██████████| 33/33 [00:00<00:00, 564.73it/s]
100%|██████████| 33/33 [00:00<00:00, 585.47it/s]
100%|██████████| 4/4 [00:10<00:00,  2.75s/it]
100%|██████████| 136/136 [00:00<00:00, 11812.49it/s]

Unblinding the data

After creating the analysis instance we can unblind the data for the chosen source. Hence, we initialize the analysis with a trial of the experimental data, maximize the log-likelihood ratio function for all given experimental data events, and calculate the test-statistic value. The analysis instance has the method unblind that can be used for that. This method requires a RandomStateService instance in case the minimizer does not succeed and a new set of initial values for the fit parameters need to get generated.

[2]:
from skyllh.core.random import RandomStateService

rss = RandomStateService(3090)
ts, fitparam_values, status = ana.unblind(rss)
ns_bf = fitparam_values['ns']
gamma_bf = fitparam_values['gamma']
print(f'Unblinded results: TS={ts:.2f}, ns={ns_bf:.2f}, gamma={gamma_bf:.2f}')
print(f'Minimizer status: {status}')
Unblinded results: TS=29.15, ns=80.09, gamma=3.21
Minimizer status: {'grad': array([-3.62370079e-06, -2.31360859e-04]), 'task': 'CONVERGENCE: RELATIVE REDUCTION OF F <= FACTR*EPSMCH', 'funcalls': 13, 'nit': 10, 'warnflag': 0, 'skyllh_minimizer_n_reps': 0, 'n_llhratio_func_calls': 13}

Producing a Sky scan near the source

Here we scan a patch of the sky around the source coordinates. We therefore define a grid of points in R.A. and Dec. and unblind the data to get a TS, ns, and gamma value at each grid point.

The change_source property of the Analysis instance allows us to change the source hypothesis of the analysis without re-initializing the analysis instance itself.

Compute time note: Despite this optimization, running the following cell can take 2-3 minutes on a laptop.

[3]:
# Produce a little sky scan around the source location

src_ra = 40.67  # degrees
src_dec = -0.01  # degrees

ra_min = src_ra - 1.0
ra_max = src_ra + 1.0
dec_min = src_dec - 1.0
dec_max = src_dec + 1.0

nbins = 50
edges_x = np.linspace(ra_min, ra_max, nbins + 1)
edges_y = np.linspace(dec_min, dec_max, nbins + 1)
centers_x = 0.5 * (edges_x[1:] + edges_x[:-1])
centers_y = 0.5 * (edges_y[1:] + edges_y[:-1])

# Initialize the results arrays
results_ts = np.zeros((nbins, nbins), dtype=float)
results_p = np.zeros((nbins, nbins), dtype=float)
results_ns = np.zeros((nbins, nbins), dtype=float)
results_g = np.zeros((nbins, nbins), dtype=float)

for i, ra in enumerate(centers_x):
    for j, dec in enumerate(centers_y):
        # Change source position to evaluate the likelihood
        src_dec = np.radians(dec)
        src_ra = np.radians(ra)
        source = PointLikeSource(src_ra, src_dec)
        ana.change_source(source, update_detsigyield_service=False)

        # Unblind the new position
        rss = RandomStateService(3090)
        ts, fitparam_values, status = ana.unblind(rss)
        results_ts[i, j] = ts
        results_ns[i, j] = fitparam_values['ns']
        results_g[i, j] = fitparam_values['gamma']

The contours are computed assuming that Wilks’ theorem holds true, that is the TS distribution follows a \(\chi^2\) distribution with 2 degrees of freedom in this example where we fit the flux normalization and spectral index of a powerlaw flux.

[4]:
import matplotlib.pyplot as plt
from scipy.stats import chi2

ts_max = results_ts.max()
chi2_68_quantile = ts_max - chi2.ppf(0.68, df=2)
chi2_90_quantile = ts_max - chi2.ppf(0.90, df=2)
chi2_95_quantile = ts_max - chi2.ppf(0.95, df=2)

idx = np.unravel_index(results_ts.argmax(), results_ts.shape)
best_dec = centers_y[idx[0]]
best_ra = centers_x[idx[1]]

X, Y = np.meshgrid(centers_x, centers_y)

src_ra = 40.67  # degrees
src_dec = -0.01

fig, ax = plt.subplots()

im = ax.pcolormesh(centers_x, centers_y, results_ts, cmap='GnBu')
ax.invert_xaxis()
contour_68 = ax.contour(
    centers_x, centers_y, results_ts, levels=[chi2_68_quantile], colors='w', linestyles='-', linewidths=2
)
contour_90 = ax.contour(
    centers_x, centers_y, results_ts, levels=[chi2_90_quantile], colors='w', linestyles='--', linewidths=2
)
contour_95 = ax.contour(
    centers_x, centers_y, results_ts, levels=[chi2_95_quantile], colors='w', linestyles=':', linewidths=2
)
ax.scatter([src_ra], [src_dec], marker='*', color='gold', s=120, label='Source position')
ax.scatter([best_ra], [best_dec], marker='x', color='w', s=120, label='best fit')

ax.set_aspect('equal')
ax.set_xlim(40, 41.2)
ax.set_ylim(-0.5, 0.75)
cbar = plt.colorbar(im)
cbar.set_label('Test Statistic')
ax.set_xlabel('R.A. (Deg)')
ax.set_ylabel('Dec. (Deg)')

ax.clabel(
    contour_68, fmt={chi2_68_quantile: '68% CL'}, fontsize=10, inline=False, inline_spacing=5, manual=[(40.5, 0.2)]
)
ax.clabel(
    contour_90, fmt={chi2_90_quantile: '90% CL'}, fontsize=10, inline=False, inline_spacing=5, manual=[(40.1, -0.3)]
)
ax.clabel(
    contour_95, fmt={chi2_95_quantile: '95% CL'}, fontsize=10, inline=False, inline_spacing=5, manual=[(40.6, 0.4)]
)

ax.invert_xaxis()
plt.legend()
plt.show()
../_images/tutorials_sky_scan_10_0.png