Skip to content

Commit 91f5def

Browse files
authored
Merge pull request #2943 from cta-observatory/sim_calib_gain_select
allow gain selection when skipping R1 calibration in SimTelEventSourve
2 parents 408aab0 + 1520df1 commit 91f5def

3 files changed

Lines changed: 92 additions & 32 deletions

File tree

docs/changes/2943.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow to select gain for calibration events in SimTelEventSource, when skipping R1 calibration.

src/ctapipe/io/simteleventsource.py

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import warnings
44
from contextlib import nullcontext
55
from enum import Enum, IntFlag, auto, unique
6+
from functools import lru_cache
67
from gzip import GzipFile
78
from io import BufferedReader
89

@@ -119,6 +120,11 @@ class SimTelTriggerMask(IntFlag):
119120
RANDOM_MONO = auto()
120121

121122

123+
@lru_cache()
124+
def _get_pixel_index(n_pixels):
125+
return np.arange(n_pixels)
126+
127+
122128
def _trigger_mask_to_event_type(trigger_mask):
123129
trigger_mask = SimTelTriggerMask(int(trigger_mask))
124130

@@ -333,11 +339,10 @@ def _telescope_from_meta(telescope_meta, mirror_area):
333339

334340

335341
def apply_simtel_r1_calibration(
336-
r0_waveforms, pedestal, factor, gain_selector, calib_scale=1.0, calib_shift=0.0
342+
r0_waveforms, pedestal, factor, calib_scale=1.0, calib_shift=0.0
337343
):
338344
"""
339345
Perform the R1 calibration for R0 simtel waveforms. This includes:
340-
- Gain selection
341346
- Pedestal subtraction
342347
- Conversion of samples into units proportional to photoelectrons
343348
(If the full signal in the waveform was integrated, then the resulting
@@ -356,7 +361,6 @@ def apply_simtel_r1_calibration(
356361
Conversion factor between R0 waveform samples and ~p.e., stored in the
357362
simtel file for each gain channel
358363
Shape: (n_channels, n_pixels)
359-
gain_selector : ctapipe.calib.camera.gainselection.GainSelector
360364
calib_scale : float
361365
Extra global scale factor for calibration.
362366
Conversion factor to transform the integrated charges
@@ -369,27 +373,47 @@ def apply_simtel_r1_calibration(
369373
Returns
370374
-------
371375
r1_waveforms : ndarray
372-
Calibrated waveforms
376+
Calibrated waveforms not gain-selected, in units of photoelectrons (p.e.).
373377
Shape: (n_channels, n_pixels, n_samples)
374-
selected_gain_channel : ndarray
375-
The gain channel selected for each pixel
376-
Shape: (n_pixels)
377378
"""
378-
n_pixels = r0_waveforms.shape[-2]
379379
ped = pedestal[..., np.newaxis]
380380
factor = factor[..., np.newaxis]
381381
gain = factor * calib_scale
382382

383383
r1_waveforms = (r0_waveforms - ped) * gain + calib_shift
384384

385-
if gain_selector is not None:
386-
selected_gain_channel = gain_selector(r0_waveforms)
387-
r1_waveforms = r1_waveforms[
388-
np.newaxis, selected_gain_channel, np.arange(n_pixels)
389-
]
390-
else:
391-
selected_gain_channel = None
385+
return r1_waveforms
392386

387+
388+
def apply_gain_selection(r0_waveforms, r1_waveforms, gain_selector):
389+
"""
390+
Apply gain selection to the R1 waveform.
391+
392+
Parameters
393+
----------
394+
r0_waveforms : ndarray
395+
Raw ADC waveforms from a simtel file. All gain channels available.
396+
Shape: (n_channels, n_pixels, n_samples)
397+
r1_waveforms : ndarray
398+
Calibrated waveforms not gain-selected, in units of photoelectrons (p.e.).
399+
Shape: (n_channels, n_pixels, n_samples)
400+
gain_selector : ctapipe.calib.camera.gainselection.GainSelector
401+
The GainSelector to use for selecting the gain channel.
402+
403+
Returns
404+
-------
405+
r1_waveforms : ndarray
406+
Calibrated waveforms after gain selection, in units of photoelectrons (p.e.).
407+
Shape: (1, n_pixels, n_samples)
408+
selected_gain_channel : ndarray
409+
The gain channel selected for each pixel. Shape: (n_pixels,)
410+
"""
411+
selected_gain_channel = gain_selector(r0_waveforms)
412+
r1_waveforms = r1_waveforms[
413+
np.newaxis,
414+
selected_gain_channel,
415+
_get_pixel_index(r0_waveforms.shape[-2]),
416+
]
393417
return r1_waveforms, selected_gain_channel
394418

395419

@@ -1060,29 +1084,30 @@ def _generate_events(self):
10601084
factor = array_event["laser_calibrations"][tel_id]["calib"]
10611085
disabled_pixel_mask = self._get_disabled_pixel_mask(tel_id)
10621086

1063-
select_gain = self.select_gain is True or (
1064-
self.select_gain is None
1065-
and trigger.event_type is EventType.SUBARRAY
1066-
)
1067-
if select_gain:
1068-
gain_selector = self.gain_selector
1069-
else:
1070-
gain_selector = None
1071-
10721087
if self.skip_r1_calibration:
10731088
# Skip the simtel R1 calibration
10741089
r1_waveform = adc_samples.astype(np.float32)
1075-
selected_gain_channel = None
10761090
else:
1077-
# Apply the simtel R1 calibration and the gain selector if selected
1078-
r1_waveform, selected_gain_channel = apply_simtel_r1_calibration(
1091+
# Apply the simtel R1 calibration to get waveforms in units of photoelectrons (p.e.)
1092+
r1_waveform = apply_simtel_r1_calibration(
10791093
adc_samples,
10801094
pedestal,
10811095
factor,
1082-
gain_selector,
10831096
self.calib_scale,
10841097
self.calib_shift,
10851098
)
1099+
# Perform the gain selection if requested.
1100+
# By default, the cosmic events will be gain-selected, not for calibration events.
1101+
select_gain = self.select_gain is True or (
1102+
self.select_gain is None
1103+
and trigger.event_type is EventType.SUBARRAY
1104+
)
1105+
if select_gain:
1106+
r1_waveform, selected_gain_channel = apply_gain_selection(
1107+
adc_samples, r1_waveform, self.gain_selector
1108+
)
1109+
else:
1110+
selected_gain_channel = None
10861111

10871112
pixel_status = self._get_r1_pixel_status(
10881113
tel_id=tel_id,

src/ctapipe/io/tests/test_simteleventsource.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ctapipe.io.simteleventsource import (
2020
AtmosphereProfileKind,
2121
SimTelEventSource,
22+
apply_gain_selection,
2223
apply_simtel_r1_calibration,
2324
read_atmosphere_profile_from_simtel,
2425
)
@@ -270,6 +271,37 @@ def test_skip_r1_calibration():
270271
assert n_processed == n_expected
271272

272273

274+
def test_skip_r1_calibration_with_gain_selection():
275+
n_expected = 1
276+
with SimTelEventSource(
277+
input_url=calib_events_path,
278+
max_events=n_expected,
279+
skip_calibration_events=False,
280+
select_gain=True,
281+
focal_length_choice="EQUIVALENT",
282+
) as reader:
283+
n_processed = 0
284+
for event in reader:
285+
n_processed += 1
286+
# R0 should have 2 channels (high gain and low gain)
287+
assert event.r0.tel[1].waveform.ndim == 3
288+
assert event.r0.tel[1].waveform.shape[0] == 2, (
289+
f"R0 waveforms should have 2 channels, got {event.r0.tel[1].waveform.shape[0]}"
290+
)
291+
# R1 should have 1 channel after gain selection
292+
assert event.r1.tel[1].waveform.ndim == 3
293+
assert event.r1.tel[1].waveform.shape[0] == 1, (
294+
f"R1 waveforms should have 1 channel after gain selection, got {event.r1.tel[1].waveform.shape[0]}"
295+
)
296+
# Check that selected_gain_channel exists and has correct shape
297+
assert event.r1.tel[1].selected_gain_channel is not None
298+
assert (
299+
event.r1.tel[1].selected_gain_channel.shape[0]
300+
== event.r1.tel[1].waveform.shape[1]
301+
), "selected_gain_channel should have one entry per pixel"
302+
assert n_processed == n_expected
303+
304+
273305
def test_time_shift():
274306
source = SimTelEventSource(
275307
input_url=calib_events_path,
@@ -326,8 +358,9 @@ def test_apply_simtel_r1_calibration_1_channel():
326358
dc_to_pe = np.full((n_channels, n_pixels), 0.5)
327359

328360
gain_selector = ThresholdGainSelector(threshold=90)
329-
r1_waveforms, selected_gain_channel = apply_simtel_r1_calibration(
330-
r0_waveforms, pedestal, dc_to_pe, gain_selector
361+
r1_waveforms = apply_simtel_r1_calibration(r0_waveforms, pedestal, dc_to_pe)
362+
r1_waveforms, selected_gain_channel = apply_gain_selection(
363+
r0_waveforms, r1_waveforms, gain_selector
331364
)
332365

333366
assert (selected_gain_channel == 0).all()
@@ -357,8 +390,9 @@ def test_apply_simtel_r1_calibration_2_channel():
357390
dc_to_pe[1] = 0.1
358391

359392
gain_selector = ThresholdGainSelector(threshold=90)
360-
r1_waveforms, selected_gain_channel = apply_simtel_r1_calibration(
361-
r0_waveforms, pedestal, dc_to_pe, gain_selector
393+
r1_waveforms = apply_simtel_r1_calibration(r0_waveforms, pedestal, dc_to_pe)
394+
r1_waveforms, selected_gain_channel = apply_gain_selection(
395+
r0_waveforms, r1_waveforms, gain_selector
362396
)
363397

364398
assert selected_gain_channel[0] == 1

0 commit comments

Comments
 (0)