Calibrating Hits

Hits stored in ROOT and HDF5 files are usually not calibrated, which means that they have invalid positions, directions and uncorrected hit times. This example shows how to assign the PMT position and direction to each hit and applying a time correction to them.

The KM3NeT offline format (derived from aanet) uses a single class for every hit type (regular hits, MC hits and their correspeonding calibrated and uncalibrated counter parts). In ROOT files, the actual struct/class definition is stored along the data, which means that all the attributes are accessible even when they are invalid or not instantiated. Positions and directions are also part of these attributes and they are initialised to (0, 0, 0).

The km3pipe.calib.Calibration() class can be used to load a calibration and the .apply() method to update the position, direction and correct the arrival time of hit.

# Author: Tamas Gal <tgal@km3net.de>
# License: BSD-3

import km3pipe as kp
import km3io
from km3net_testdata import data_path

The offline/km3net_offline.root contains 10 events with hit information:

f = km3io.OfflineReader(data_path("offline/km3net_offline.root"))

The corresponding calibration file is stored in detx/km3net_offline.detx:

calib = kp.calib.Calibration(filename=data_path("detx/km3net_offline.detx"))
Detector: Parsing the DETX header
Detector: Reading PMT information...
Detector: Done.

Let’s grab the hits of the event with index 5:

hits = f.events[5].hits

The positions (pos_x, pos_y, pos_z) and directions (dir_x, dir_y, dir_z) are not available for uncalibrated hits, in contrast to aanet where each field is present and initialised to some magic value (e.g. 0). km3io hides all those fields and do not occupy additional memory for those.

Here are the uncalibrated times:

n = 7  # just an arbitrary number to limit the output
uncalibrated_times = hits.t
print(uncalibrated_times[:n])
[8.19e+07, 8.19e+07, 8.19e+07, 8.19e+07, 8.19e+07, 8.19e+07, 8.19e+07]

To calibrate the hits, use the calib.apply() method which will create a km3pipe.Table, retrieve the positions and directions of the corresponding PMTs, apply the time calibration and also do the PMT time slew correction.

calibrated_hits = calib.apply(hits)

The calibrated hits are stored in a kp.Table which is a thin wrapper around a numpy.record array (a simple numpy array with named attributes):

print(calibrated_hits.dtype)
(numpy.record, [('channel_id', '<u4'), ('dir_x', '<f8'), ('dir_y', '<f8'), ('dir_z', '<f8'), ('dom_id', '<i4'), ('du', 'u1'), ('floor', 'u1'), ('pmt_id', '<i4'), ('pos_x', '<f8'), ('pos_y', '<f8'), ('pos_z', '<f8'), ('t0', '<f8'), ('time', '<f8'), ('tot', '<u4'), ('triggered', '<i4')])

The positions and directions are now showing the correct values and the time is the calibrated one:

for attr in [f"{k}_{q}" for k in ("pos", "dir") for q in "xzy"]:
    print(attr, getattr(calibrated_hits, attr)[:n])

print(calibrated_hits.time[:n])
pos_x [  9.486  -2.2    -2.366  -2.368  -2.037 -13.339 -13.503]
pos_z [116.027 182.901 183.212 183.16  106.456 192.881 192.881]
pos_y [ 5.779 -9.6   -9.604 -9.509 -9.5    6.754  6.73 ]
dir_x [-0.57   0.    -0.832 -0.838  0.816  0.304 -0.518]
dir_z [-0.555 -1.     0.555  0.295  0.295  0.555  0.555]
dir_y [-0.606 -0.    -0.02   0.458  0.497  0.774  0.651]
[82069045.722      82067743.25       82068723.247      82069201.96800001
 82068064.22299999 82068657.47600001 82068658.204     ]

The t0 field holds the time calibration correction which was automatically added to hit time (hit.time):

print(calibrated_hits.t0[:n])
[207808.962 208135.32  208137.147 208140.038 207706.953 208031.076
 208033.454]

As mentioned above, the PMT time slewing correction is also applied, which is a tiny correction of the arrival time with respect to the hit’s ToT value. We can reveal their values by subtracting the t0 from the calibrated time and compare to the uncalibrated ones. Notice that hits represented as kp.Table in km3pipe use .time instead of .t for mainly historical reasons:

slews = uncalibrated_times - (calibrated_hits.time - calibrated_hits.t0)
print(slews[:n])
[0.24, 0.07, -0.1, 0.07, -0.27, 0.6, 3.25]

Let’s compare the slews with the ones calculated with kp.calib.slew(). The values match very well, with tiny variations due to floating point arithmetic:

slew_diff = slews - kp.calib.slew(hits.tot)
print(slew_diff[:n])
[-5.36e-09, -7.15e-09, 5.96e-09, -7.15e-09, 4.17e-09, -5.96e-09, 0]

To omit the PMT slew calibration, you can pass the correct_slewing=False option the .apply():

calibrated_hits_no_slew_correction = calib.apply(hits, correct_slewing=False)

The difference between the calibration with and without slewing is obviously the slewing correction itself:

print((calibrated_hits_no_slew_correction.time - calibrated_hits.time)[:n])
[ 0.23999999  0.06999999 -0.09999999  0.06999999 -0.27        0.59999999
  3.25      ]

Total running time of the script: (0 minutes 2.390 seconds)

Gallery generated by Sphinx-Gallery