Coverage for src/pyhiperta/R0Reader.py: 99%
63 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-07 14:41 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-07 14:41 +0000
1# Copyright 2026 CNRS
2# This software is distributed under the terms of the CeCILL-C free software license.
4"""Implements the reading/loading of R0 data (waveforms) and service data (gains/pedestals, etc.) from R0 hdf5 files."""
6from collections.abc import Iterable
7from pathlib import Path
9import numpy as np
10import tables
13class R0HDF5Dataset:
14 """HDF5 reader class inspired by pytorch datasets.
16 The main purpose of this class is to load shower waveforms in batch from hdf5 files, allowing to
17 seamlessly iterate over batches independently of the number of files or the number of events per file.
19 Parameters
20 ----------
21 R0_files : Path or str or Iterable[Path] or Iterable[str]
22 If `R0_files` is a string or a Path, it is expected to be the path to a single ".h5" R0 file, or to
23 be the path to a folder containing the ".h5" R0 files to use.
24 If it is an Iterable, each element is expected to be a string or path to a ".h5" data file.
25 batch_size : int
26 Number of shower events to stack to create a batch.
27 nb_first_slice_to_reject : int, optionnal
28 If provided, the first `nb_first_slice_to_reject` frames of the shower videos will be discarded when
29 loading the waveforms. (This is required at the moment for LST data)
30 nb_last_slice_to_reject : int, optionnal
31 If provided, the last `nb_last_slice_to_reject` frames of the shower videos will be discarded when
32 loading the waveforms. (This is required at the moment for LST data)
33 """
35 def __init__(
36 self,
37 R0_files: Path | str | Iterable[Path] | Iterable[str],
38 batch_size: int,
39 nb_first_slice_to_reject: int = None,
40 nb_last_slice_to_reject: int = None,
41 ):
42 if isinstance(R0_files, (str, Path)): # path to folder containing R0.h5 files or to a single file.
43 R0_files = Path(R0_files)
44 if R0_files.is_file():
45 R0_files = [R0_files]
46 elif R0_files.is_dir():
47 R0_files = list(R0_files.glob("*.h5"))
48 else:
49 raise FileNotFoundError(f"{R0_files} is not a valid file or directory.")
50 elif isinstance(R0_files, Iterable): # assume R0_files is list of paths to files directly 50 ↛ 53line 50 didn't jump to line 53 because the condition on line 50 was always true
51 R0_files = [Path(p) for p in R0_files]
53 self._files = sorted(R0_files) # sort list of files for consistency among different execution.
54 # Compute the number of shower events (and cumulative) in each files
55 self._files_lengths = []
56 for f in self._files:
57 with tables.open_file(str(f), "r") as f_reader:
58 self._files_lengths.append(f_reader.root.r0.event.telescope.waveform.tel_001.shape[0])
59 self._cum_files_lengths = np.cumsum(self._files_lengths)
60 self._batch_size = batch_size
61 self._nb_first_slice_to_reject = nb_first_slice_to_reject
62 self._nb_last_slice_to_reject = nb_last_slice_to_reject
64 def read_gains(self) -> np.ndarray:
65 """Read the per-pixel gains corresponding to the waveforms data.
67 The high and low gains are stored thus read together.
69 Returns
70 -------
71 np.ndarray
72 The per-pixel gains values. gains[0, :] are the high gains, gains[1, :] the low gains.
73 Shape: (2, N_pixels).
75 Notes
76 -----
77 At the moment, the gains are constant for all events of a telescope run. Therefore the gains are
78 simply read from the first file.
79 """
80 with tables.open_file(str(self._files[0]), "r") as f_reader:
81 gains = f_reader.root.r0.monitoring.telescope.gain.tel_001[:]
82 return gains
84 def read_pedestals(self) -> np.ndarray:
85 """Read the per-pixel pedestals corresponding to the waveform data.
87 The pedestals corresponding to each channel (gain) are stored and read stacked together.
89 Returns
90 -------
91 np.ndarray
92 The per-pixel pedestal values. pedestals[0, :] are the high gain pedestals, while
93 pedestals[1, :] are the low gain pedestals. Shape: (2, N_pixels)
95 Notes
96 -----
97 At the moment, the pedestals are constant for all events of a telescope run. Therefore the
98 pedestals are simply read from the first file.
99 """
100 with tables.open_file(str(self._files[0]), "r") as f_reader:
101 pedestals = f_reader.root.r0.monitoring.telescope.pedestal.tel_001.cols.pedestal[:].squeeze()
102 return pedestals
104 def read_camera_geometry(self) -> np.ndarray:
105 """Read the camera geometry (pixel coordinates) from the first file of the dataset.
107 Returns
108 -------
109 np.ndarray
110 Camera pixel coordinates. geometry[0, :] are the x coordinates, geometry[1, :] are the y
111 coordinates. Shape (2, N_pixels)
112 """
113 with tables.open_file(str(self._files[0]), "r") as f_reader:
114 return np.stack(
115 [
116 f_reader.root.configuration.instrument.telescope.camera.geometry_LSTCam.cols.pix_x[:],
117 f_reader.root.configuration.instrument.telescope.camera.geometry_LSTCam.cols.pix_y[:],
118 ],
119 axis=0,
120 )
122 def read_reference_pulse(self) -> tuple[np.ndarray, np.ndarray]:
123 """Read the reference pulse shape arrays from the first file of the dataset.
125 Returns
126 -------
127 Tuple[np.ndarray, np.ndarray]
128 The first element are the "reference_pulse_sample_time", the second are the
129 "reference_pulse_shape_channel0" and "reference_pulse_shape_channel0", stacked in a single array.
130 reference_pulse_shape[0] si channel 0 and reference_pulse_shape[1] is channel 1.
131 """
132 with tables.open_file(str(self._files[0]), "r") as f_reader:
133 ref_pulse = f_reader.root.configuration.instrument.telescope.camera.readout_LSTCam[:]
134 return (
135 ref_pulse["reference_pulse_sample_time"],
136 np.stack(
137 [ref_pulse["reference_pulse_shape_channel0"], ref_pulse["reference_pulse_shape_channel1"]], axis=0
138 ),
139 )
141 def events_n_frames(self) -> int:
142 """Return the number of frames in an event by reading the 1st event of 1st batch."""
143 return self[0][0].shape[1]
145 def __len__(self) -> int:
146 """Return the number of batches in the dataset."""
147 return np.ceil(self._cum_files_lengths[-1] / self._batch_size).astype(np.int64)
149 def __getitem__(self, idx) -> tuple[np.ndarray, np.ndarray]:
150 """Load a batch of waveforms from the dataset.
152 The size of the batch should be `batch_size` unless there are not enough events left in the dataset
153 to load (for instance for the last batch, or if the batch size is greater than the number of events
154 in the dataset.)
156 The loaded waveforms are truncated at each ends by `nb_first_slice_to_reject`, respectively
157 `nb_last_slice_to_reject`.
159 Parameters
160 ----------
161 idx : int
162 The index of the batch to load.
164 Returns
165 -------
166 Tuple[np.ndarray, np.ndarray]
167 waveforms[0] are the high gain waveforms of the batch, waveforms[1] are the low gain waveforms.
168 Shape of the waveforms arrays: (N_batch, N_frames, N_pixels)
169 """
170 if idx >= len(self) or idx < 0:
171 raise IndexError(f"Index {idx} out of range for dataset with length {len(self)}")
172 # Get the index of the file into which the first event should be read.
173 file_idx = np.searchsorted(self._cum_files_lengths, idx * self._batch_size)
175 # Calculate the idx of the first event to load in the first file:
176 # if this is the first file in the dataset, simply subtract the number of already read events
177 # if this is another file, also subtract the number of events of the previous files
178 idx_start = (
179 idx * self._batch_size - self._cum_files_lengths[file_idx - 1] if file_idx > 0 else idx * self._batch_size
180 )
182 # Load from idx start to idx start + batch size
183 # if there are not enough events in files, we will get a array smaller than batch size
184 # and we can continue loading from the next file.
185 with tables.open_file(self._files[file_idx], "r") as f_reader:
186 waveforms_high = [
187 f_reader.root.r0.event.telescope.waveform.tel_001.cols.waveformHi[
188 idx_start : idx_start + self._batch_size
189 ]
190 ]
191 waveforms_low = [
192 f_reader.root.r0.event.telescope.waveform.tel_001.cols.waveformLo[
193 idx_start : idx_start + self._batch_size
194 ]
195 ]
197 # While we haven't batch_size events, we loop over the next files to load more
198 # if we are out of files, return the events we have.
199 cum_lengths = waveforms_high[0].shape[0]
200 file_idx += 1
201 while cum_lengths < self._batch_size and file_idx < len(self._files):
202 with tables.open_file(self._files[file_idx], "r") as f_reader:
203 waveforms_high.append(
204 f_reader.root.r0.event.telescope.waveform.tel_001.cols.waveformHi[
205 0 : self._batch_size - cum_lengths
206 ]
207 )
208 waveforms_low.append(
209 f_reader.root.r0.event.telescope.waveform.tel_001.cols.waveformLo[
210 0 : self._batch_size - cum_lengths
211 ]
212 )
213 cum_lengths = cum_lengths + waveforms_high[-1].shape[0]
214 file_idx += 1
216 # concatenate lists of batches of events into single arrays
217 waveforms_high = np.concatenate(waveforms_high, axis=0, dtype=np.float32)
218 waveforms_low = np.concatenate(waveforms_low, axis=0, dtype=np.float32)
220 # remove the unwanted samples (required for LST data) and return the high gain and low gain waveforms
221 end_frame = None if self._nb_last_slice_to_reject is None else -self._nb_last_slice_to_reject
222 return (
223 waveforms_high[:, self._nb_first_slice_to_reject : end_frame],
224 waveforms_low[:, self._nb_first_slice_to_reject : end_frame],
225 )