Correcting coma aberration bias

LST-1 study of the coma aberration bias correction
notes
cta
Author

Thomas Vuillaume

Published

June 9, 2022

Code
from lstchain.io.io import dl2_params_lstcam_key
from ctapipe.io import read_table
import matplotlib.pyplot as plt
import numpy as np
import ctaplot
import astropy.units as u
from astropy.visualization import quantity_support
from copy import deepcopy

import warnings
warnings.filterwarnings("ignore")

ctaplot.set_style('notebook')
Code
# Reading DL2 parameters

filename = 'dl2_gamma-diffuse_20deg_180deg_20220215_v0.9.1_prod5_trans_80_local_tailcut_8_4_testing.h5'
params = read_table(filename, path=dl2_params_lstcam_key)
Code
params[:4]
Table length=4
obs_idevent_idintensitylog_intensityxyrphilengthlength_uncertaintywidthwidth_uncertaintypsiskewnesskurtosistime_gradientinterceptleakage_intensity_width_1leakage_intensity_width_2leakage_pixels_width_1leakage_pixels_width_2n_pixelsconcentration_cogconcentration_coreconcentration_pixeln_islandsalt_telaz_telcalibration_idmc_energylog_mc_energymc_altmc_azmc_core_xmc_core_ymc_h_first_intmc_typemc_az_telmc_alt_telmc_x_maxmc_core_distancewltel_idtel_pos_xtel_pos_ytel_pos_ztrigger_typetrigger_timeevent_typedisp_dxdisp_dydisp_normdisp_angledisp_signsrc_xsrc_ylog_reco_energyreco_energyreco_disp_normreco_disp_signreco_disp_dxreco_disp_dyreco_src_xreco_src_ysigned_time_gradientsigned_skewnessreco_altreco_azreco_typegammaness
int32int64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float32float32float64float64int64float64float64float64int64float32float32int64float64float64float64float64float64float64float64int64float32float32float64float64float64int64float32float32float32int64float64int64float32float32float32float32float32float32float32float64float64float64float32float64float64float64float64float64float64float64float64int64float64
1001709204.691669464111332.311100168117379-0.9429552882748465-0.0269576669362539020.9433405490554008-3.11301194919339340.234438603433601370.0078422699881002010.049864477210902860.002820204535803679-0.4176405820038234-0.076179366145008951.91617966015168583.158349431562738712.5877506351235180.100735320.23272440.0010781671159029650.0026954177897574125170.232599608790322180.38632659203980240.1231618798467566111.22173053.1415927-10.10125340521335602-0.99459036228117161.17378842830657963.155841827392578117.61976623535156-120.9898757934570326547.11523437503.14159271.2217305290.8888854980469200.75099945937570.212697382088891661-70.93-52.0743.0321606861253.33423132-0.39838670.181398140.43774107-0.427279-1.0-1.3413420.15444046-1.14141249823460030.072208363388064910.46018360223014054-1.0-0.42063011995620920.18665275231598732-1.36358540823105570.159695085379733433.1583494315627387-0.076179366145008951.17299157911605233.15629815008605251010.4760000000000001
100114702217.46422433853152.3373878201397390.460728340928599430.363365243998680140.58677500345621410.66779763331754360.116625116825143440.0048114807151058070.0537388680402243950.002788135479311323-1.0265033637403687-0.0383849316998664262.48053926335876662.748325297976986512.3493308386246630.00.00.00.0120.53900221839869670.39936006652944170.221835966629967411.22173053.1415927-10.0735081136226654-1.13366472204401661.22850549221038823.214990139007568442.88823699951172-197.0823059082031218002.34960937503.14159271.2217305219.3333282470703184.345219890478660.460782972854769551-70.93-52.0743.0321606861265.15868932-0.247248650.325900260.40907562-0.92177355-1.00.21347970.6892655-1.22318082100537070.0598162494811730960.30984948357431924-1.0-0.16044420044510360.26507425566207620.300284140483495830.62843949966075632.7483252979769865-0.0383849316998664261.23174262488649783.209116810285751010.3741666666666666
100115008165.506139755249022.218814109364702-0.9169175343015934-0.40885725896150971.0039432368989956-2.7221501232647110.077661954272045640.0033586928472081870.069267845379087150.0031922758184949524-1.477054216302340.58120401348858492.2382191567652474-3.026177055962925512.3968614725557430.127804590.59622810.00161725067385444750.0037735849056603774100.52844817890861140.273803418517280350.2546447834400062411.22173053.1415927-10.06741833686828613-1.1712219652210831.18636918067932133.1234266757965090.5827245712280273-149.9759216308593821047.28320312503.14159271.2217305188.18182373046875121.242069305143560.89191478669781621-70.93-52.0743.0321606861266.30892832-0.071568220.217989340.22943705-1.253573-1.0-0.98848575-0.19086792-1.1691163702078060.067745995633085850.21682144939570283-1.0-0.020295544965148090.21586947855734717-0.9372130792667415-0.19298778040416253-3.02617705596292550.58120401348858491.18819881175506333.1231402459886931010.0455
1001240065026.3798446655273.7012553052292714-0.46367241536546555-0.082357610007603550.4709298086732346-2.9658057346182480.322248640715590730.00355458230495727050.078745762415324360.0012247855781627778-0.04766358667702001-0.89759372661622663.446251471675243-3.336247707080466415.7476675263774340.00.00.00.0640.186266628290241380.449535054129446170.0933368727446329721.22173053.1415927-11.00427675247192380.0018534095311007061.22196137905120853.1297266483306885-205.5702667236328-40.5134811401367224701.789062503.14159271.2217305335.6097412109375135.13531907809270.244363365631148051-70.93-52.0743.0321606861279.03483320.4707722-0.0312043430.47180524-0.0661864951.00.0070998-0.11356195-0.0187872323993603370.95766313040243230.46634585480762541.00.46581622875101053-0.0222193008090054430.0021438133855449792-0.1045769108166093.33624770708046640.89759372661622661.2217878633150463.13067074037698400.96
Code
def add_params(events):
    """
    add some parameters and units
    """
    for pos in ['reco_src_x', 'reco_src_y', 'src_x', 'src_y', 'reco_disp_dx', 'reco_disp_dy', 'x', 'y']:
        events[pos].unit = u.m
    
    for ang in ['reco_alt', 'reco_az', 'mc_alt', 'mc_az', 'alt_tel', 'az_tel']:
        events[ang].unit = u.rad
        
    for ene in ['reco_energy', 'mc_energy']:
        events[ene].unit = u.TeV
    
    events['diff_x'] = events['reco_src_x'] - events['src_x']
    events['diff_y'] = events['reco_src_y'] - events['src_y']
    events['diff_alt'] = events['reco_alt'] - events['mc_alt']
    events['diff_az'] = events['reco_az'] - events['mc_az']
    events['diff'] = np.sqrt(events['diff_x']**2 + events['diff_y']**2)
    events['offset'] = ctaplot.ana.ana.angular_separation_altaz(events['alt_tel'].quantity, events['az_tel'].quantity,
                                                                events['mc_alt'].quantity, events['mc_az'].quantity)
    events['alpha'] = np.arctan2(events['src_y'], events['src_x'])
Code
add_params(params)
Code
def plot_true_pos(events, ax=None):
    ax = plt.gca() if ax is None else ax
    ax.scatter(events['src_x'], events['src_y'], s=1);
    ax.grid(True)
    ax.axis('equal')
    ax.set_xlabel('src_x')
    ax.set_ylabel('src_y')
    ax.set_title('events position')
    return ax

plot_true_pos(params);

Code
def plot_psf(events, ax=None):
    ax = plt.gca() if ax is None else ax
    rx = 1.6
    ry = 0.4
    # ax.hist2d(events['diff_x'], events['diff_y'], bins=100, range=[[-r,r],[-r,r]]);
    diff_az = events['reco_az'] - events['mc_az']
    diff_alt = events['reco_alt'] - events['mc_alt']
    with quantity_support():
        ax.hist2d(diff_az.to(u.deg), diff_alt.to(u.deg), 
              bins=120, range=[[-rx,rx],[-ry,ry]], cmap='hot')
    ax.grid(True)
    # ax.axis('equal')
    ax.set_xlabel('diff_az [rad]')
    ax.set_ylabel('diff_alt [rad]')
    ax.set_title('PSF')
    return ax

plot_psf(params);

Code
def plot_theta2(events, ax=None, bias_correction=True):
    ax = plt.gca() if ax is None else ax
    opt = dict(bins=100, range=(0, 0.05), density=True, histtype='step', bias_correction=False, lw=2)
    ax=ctaplot.plots.plot_theta2(events['mc_alt'].quantity,  
                                 events['reco_alt'].quantity, 
                                 events['mc_az'].quantity, 
                                 events['reco_az'].quantity,
                                 ax=ax, 
                                 label='no bias correction',
                                 **opt)
    
    if bias_correction:
        opt['bias_correction'] = True
        ax=ctaplot.plots.plot_theta2(events['mc_alt'].quantity,
                                     events['reco_alt'].quantity,
                                     events['mc_az'].quantity,
                                     events['reco_az'].quantity,
                                     ax=ax, label='bias corrected', **opt)
        ax.legend()
    return ax

plot_theta2(params, bias_correction=True);

Code
def plot_bias_per_offset(events, ax=None, **kwargs):
    offset, bias_alt = ctaplot.ana.bias_per_bin(events['mc_alt'], events['reco_alt'], events['offset'], bins=20)
    offset, bias_az = ctaplot.ana.bias_per_bin(events['mc_az'], events['reco_az'], events['offset'], bins=20)
    ax = plt.gca() if ax is None else ax
    ax.stairs(np.rad2deg(bias_alt), edges=np.rad2deg(offset), label='bias in alt', **kwargs)
    ax.stairs(np.rad2deg(bias_az), edges=np.rad2deg(offset), label='bias in az', **kwargs)
    ax.set_xlabel('offset in degrees')
    ax.set_ylabel('bias in degrees')
    ax.legend()
    return ax

def plot_bias_per_alpha(events, ax=None, **kwargs):
    alpha, bias_alt = ctaplot.ana.bias_per_bin(events['mc_alt'], events['reco_alt'], events['alpha'], bins=20)
    alpha, bias_az = ctaplot.ana.bias_per_bin(events['mc_az'], events['reco_az'], events['alpha'], bins=20)
    ax = plt.gca() if ax is None else ax
    ax.stairs(np.rad2deg(bias_alt), edges=np.rad2deg(alpha), label='bias in alt', **kwargs)
    ax.stairs(np.rad2deg(bias_az), edges=np.rad2deg(alpha), label='bias in az', **kwargs)
    ax.set_xlabel('alpha in degrees')
    ax.set_ylabel('bias in degrees')
    ax.legend()
    return ax

plot_bias_per_offset(params);
plt.show()
plot_bias_per_alpha(params);

Code
# Select bright events to see the effect

bright_events = params[(params['intensity']>500)]
# selected_events = bright_events[(bright_events['src_x']>0) & (bright_events['src_y']>0)]
selected_events = bright_events[(-np.pi/4<bright_events['alpha'])&(bright_events['alpha']<np.pi/4.)]
Code
def plot_all(events):
    fig, axes = plt.subplots(1, 5, figsize=(25,5))
    plot_true_pos(events, ax=axes[0])
    plot_psf(events, ax=axes[1])
    plot_theta2(events, ax=axes[2])
    plot_bias_per_offset(events, ax=axes[3])
    plot_bias_per_alpha(events, ax=axes[4])
    plt.tight_layout()
    plt.show()
Code
print("Bright events")
plot_all(bright_events)
Bright events

Bright events in quarters

Code
edges = np.histogram_bin_edges(bright_events['alpha'].value, bins=5)
for ii, low in enumerate(edges[:-1]):
    selected_events = bright_events[(low<bright_events['alpha'])&(bright_events['alpha']<edges[ii+1])]
    plot_all(selected_events)

Correcting the bias

We can reconstruct the source position in the sky with a different focal length. To visualize the bias, we can plot it as a function of alpha, the angle between the event position and the X-axis

Code
from lstchain.reco.utils import reco_source_position_sky
Code
def patch_events(events, true_focal_length=29.04*u.m, plot=True):
    
    raltaz = reco_source_position_sky(events['x'], events['y'],
                                  events['reco_disp_dx'], events['reco_disp_dy'],
                                  true_focal_length,
                                  events['alt_tel'], 
                                  events['az_tel'])
    
    events_corr = deepcopy(events)
    events_corr['reco_alt'] = raltaz.alt.to(u.rad)
    events_corr['reco_az'] = raltaz.az.to(u.rad)
    
    if plot:
        plt.figure(figsize=(10,6))
        opt = dict(bins=20)
        ctaplot.plots.plot_binned_bias(events['mc_alt'], 
                                   events['reco_alt'],
                                   events['alpha'],
                                   **opt,
                                   label='alt no corr'
                                  )

        ctaplot.plots.plot_binned_bias(events['mc_az'], 
                                       events['reco_az'],
                                       events['alpha'],
                                       **opt,
                                       label='az no corr'
                                      )

        x = np.linspace(-np.pi, np.pi, 100)
        plt.plot(x, np.cos(x)*0.0008, label='alt model')
        plt.plot(x, np.sin(x)*0.0028
                 , label='az model')

        ctaplot.plots.plot_binned_bias(events_corr['mc_alt'], 
                                   events_corr['reco_alt'],
                                   events_corr['alpha'],
                                   **opt,
                                   label='alt bias corrected',
                                  )

        ctaplot.plots.plot_binned_bias(events_corr['mc_az'], 
                                       events_corr['reco_az'],
                                       events_corr['alpha'],
                                       **opt,
                                       label='az bias corrected',
                                      )
        plt.xlabel('alpha [rad]')

        plt.legend()
        plt.show()
        plot_all(events_corr)
    
    return events_corr
    
Code
params_patched = patch_events(bright_events);

We can see that a bias is still present.
Let’s find the effective focal length that minimize the bias.

Code
focal_lengths = np.linspace(28.2, 29.2, num=10) * u.m
bias_alt = []
bias_az = [] 
for fl in focal_lengths:
    print(fl)
    pe = patch_events(bright_events, true_focal_length=fl);
    _, bias = ctaplot.ana.bias_per_bin(pe['mc_alt'], pe['reco_alt'], pe['alpha'])
    bias_alt.append(np.rad2deg(np.mean(np.abs(bias))))
    _, bias = ctaplot.ana.bias_per_bin(pe['mc_az'], pe['reco_az'], pe['alpha'])
    bias_az.append(np.rad2deg(np.mean(np.abs(bias))))
28.2 m
28.31111111111111 m
28.42222222222222 m
28.53333333333333 m
28.644444444444442 m
28.755555555555556 m
28.866666666666667 m
28.977777777777778 m
29.08888888888889 m
29.2 m

We can find the effective focal length that minimize the bias:

Code
plt.plot(focal_lengths, bias_alt, label='alt')
plt.plot(focal_lengths, bias_az, label='az')
plt.title('Minimizing the bias')
plt.xlabel('effective focal length [m]')
plt.ylabel('bias [degrees]')
plt.legend()

assert np.argmin(bias_alt) == np.argmin(bias_az)
print(f"The focal length that minimize the bias: {focal_lengths[np.argmin(bias_alt)]:.3f}")
The focal length that minimize the bias: 28.756 m

Let’s see the results with that focal length:

Code
params_patched = patch_events(bright_events, focal_lengths[np.argmin(bias_alt)], plot=False);
add_params(params_patched)
Code
edges = np.histogram_bin_edges(params_patched['alpha'].value, bins=5)
for ii, low in enumerate(edges[:-1]):
    selected_events = params_patched[(low<bright_events['alpha'])&(params_patched['alpha']<edges[ii+1])]
    plot_all(selected_events)

The alpha-dependence is now gone

Angular resolution before and after

Code
selected_events = params[(params['intensity']>100) & (params['gammaness']>0.4)]
selected_corr = patch_events(selected_events, plot=False)
Code
ctaplot.plot_angular_resolution_per_energy(selected_events['mc_alt'].quantity,
                                           selected_events['reco_alt'].quantity,
                                           selected_events['mc_az'].quantity,
                                           selected_events['reco_az'].quantity,
                                           selected_events['reco_energy'].quantity,)

ctaplot.plot_angular_resolution_per_energy(selected_corr['mc_alt'].quantity,
                                           selected_corr['reco_alt'].quantity,
                                           selected_corr['mc_az'].quantity,
                                           selected_corr['reco_az'].quantity,
                                           selected_corr['reco_energy'].quantity,)


ctaplot.plot_angular_resolution_cta_requirement('north', color='black')
plt.ylim(0, 0.4)

Testing the same correction on point source gammas

Code
filename = 'dl2_gamma_20deg_180deg_off0.4deg_20220215_v0.9.1_prod5_trans_80_local_tailcut_8_4_testing.h5'

def analyis(filename):
    params = read_table(filename, path=dl2_params_lstcam_key)
    add_params(params)
    # bright_events = params[(params['intensity']>5000)]
    bright_events = params[(params['intensity']>50) & ((params['gammaness']>0.7))]
    bright_events_corr = patch_events(bright_events, true_focal_length=29.04*u.m, plot=False)


    fig, axes = plt.subplots(1, 3, figsize=(20,5))
    
    plot_psf(bright_events_corr, ax=axes[0])

    opt=dict(bins=50,  histtype='step', range=(0, 0.1))
    ctaplot.plot_theta2(bright_events['mc_alt'].quantity,
                       bright_events['reco_alt'].quantity,
                       bright_events['mc_az'].quantity,
                       bright_events['reco_az'].quantity,
                        ax=axes[1],
                        label='no bias correction',
                        **opt
                       )
    
    ctaplot.plot_theta2(bright_events_corr['mc_alt'].quantity,
                   bright_events_corr['reco_alt'].quantity,
                   bright_events_corr['mc_az'].quantity,
                   bright_events_corr['reco_az'].quantity,
                        ax=axes[1],
                    label='bias corrected',
                        **opt
                   )
    axes[1].legend()
    
    ctaplot.plot_angular_resolution_per_energy(bright_events['mc_alt'].quantity,
                                           bright_events['reco_alt'].quantity,
                                           bright_events['mc_az'].quantity,
                                           bright_events['reco_az'].quantity,
                                           bright_events['reco_energy'].quantity,
                                                  ax=axes[-1],
                                             label='no bias correction',  
                                              )


    ctaplot.plot_angular_resolution_per_energy(bright_events_corr['mc_alt'].quantity,
                                               bright_events_corr['reco_alt'].quantity,
                                               bright_events_corr['mc_az'].quantity,
                                               bright_events_corr['reco_az'].quantity,
                                               bright_events_corr['reco_energy'].quantity,
                                               ax=axes[-1],
                                               label='bias corrected',
                                               # ls='--'
                                              )

    ctaplot.plot_angular_resolution_cta_requirement('north', color='black', ax=axes[-1])
    axes[-1].set_ylim(0, 0.4)
Code
analyis(filename)