Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change R1- and DL0-waveforms shape to (n_channels, n_pixels, n_samples) #2529

Merged
merged 42 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b1c862a
Initial commit
Hckjs Mar 19, 2024
fd7fc3a
Adapting CameraCalibrator
Hckjs Mar 20, 2024
2fb7f06
Enhance Toymodel
Hckjs Mar 22, 2024
f547cfb
Fix tests
Hckjs Mar 26, 2024
6268cbb
Adapting shift_waveforms test
Hckjs Mar 27, 2024
f4a2916
Fix reducer
Hckjs Mar 27, 2024
d57dedd
Change GainSelector handle new waveforms shape
Hckjs Mar 28, 2024
d7d450c
Adapt some comment
Hckjs Mar 28, 2024
1411bdd
Minor fixes
Hckjs Apr 2, 2024
9e7b2b5
Make CameraCalibrator work for not gain selected data
Hckjs Apr 3, 2024
151e9c0
Change datamodel version
Hckjs Apr 3, 2024
e93b35c
Test shift_waveforms for both use cases
Hckjs Apr 3, 2024
a693088
Test wrong ndim in GainSelector
Hckjs Apr 3, 2024
7c5a4d0
Add toymodel test for wrong gain channel
Hckjs Apr 3, 2024
7a3cf78
Update test for neighbor_average_maximum
Hckjs Apr 3, 2024
279e6bf
Fix GlobalPeakWindowSum for 2 gains
Hckjs Apr 3, 2024
9a65df2
Add Changelog
Hckjs Apr 4, 2024
7633667
Fix docs
Hckjs Apr 4, 2024
074f405
Minor beauty changes
Hckjs Apr 4, 2024
e50eb72
Delete breakpoint ...-.-
Hckjs Apr 4, 2024
89bf1dd
Keep compatibility for older datamodel versions
Hckjs Apr 5, 2024
0bad55c
Create helper function for applying integration correction
Hckjs Apr 5, 2024
a76ea8f
Keep dtype after correction
Hckjs Apr 8, 2024
3b82775
Handle broken pixels with 2 gain channels
Hckjs Apr 8, 2024
dfff566
Minor Fix
Hckjs Apr 8, 2024
fa56126
Change broken_pixels to have always shape (n_channels, n_pixels)
Hckjs Apr 8, 2024
82a956a
Adapt InvalidPixelHandler to work with 2 gain channels
Hckjs Apr 8, 2024
887eb6d
Fix reference
maxnoe Apr 9, 2024
9ebbdd5
Change charge and peak_time calibration broadcasting
Hckjs Apr 9, 2024
732c5cf
Adapting changelog
Hckjs Apr 9, 2024
a85cc58
Add test for 2 gains in InvaliPixelHandler
Hckjs Apr 9, 2024
bd59741
Add select_gain option to SimTelEventSource
maxnoe Apr 9, 2024
25c3bce
Calibration coefficients always have shape (n_channels, n_pixels)
maxnoe Apr 9, 2024
9fe0018
Test all tels in first event
maxnoe Apr 10, 2024
0a2e309
Remove unneeded differentiation between simtel and "real" data in fla…
maxnoe Apr 10, 2024
0402d1b
Improve readability
maxnoe Apr 11, 2024
4ad3edb
Cache pixel index
maxnoe Apr 11, 2024
596abb3
Adressing some minor comments
Hckjs Apr 17, 2024
9697f7b
Merge calibrator test for 1 and 2 gain channels
Hckjs Apr 17, 2024
f932630
Move _apply_correction into the ImageExtractor parent class
Hckjs Apr 17, 2024
07646b7
Remove unnecessary instantiation
Hckjs Apr 23, 2024
e37bb41
Fix wrong merge in table_writer_reader.py
Hckjs Apr 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changes/2529.datamodel.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Change R1- and DL0-waveforms datamodel shape from (n_pixels, n_samples)
to be always (n_channels, n_pixels, n_samples). ``HDF5EventSource`` was adjusted
accordingly to support also older datamodel versions.

Re-introduce also the possibility of running ``ImageExtractor``\s on data
consisting of multiple gain channels.
2 changes: 1 addition & 1 deletion examples/core/table_writer_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

"""


######################################################################
# Caveats to think about: \* vector columns in Containers *can* be
# written, but some lilbraries like Pandas can not read those (so you must
Expand Down Expand Up @@ -228,6 +227,7 @@ def create_stream(n_event):
# note that here we can still access the metadata
#


table.attrs

######################################################################
Expand Down
63 changes: 46 additions & 17 deletions src/ctapipe/calib/camera/calibrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Definition of the `CameraCalibrator` class, providing all steps needed to apply
calibration and image extraction, as well as supporting algorithms.
"""
from functools import cache

import astropy.units as u
import numpy as np
Expand All @@ -21,17 +22,27 @@
__all__ = ["CameraCalibrator"]


def _get_invalid_pixels(n_pixels, pixel_status, selected_gain_channel):
broken_pixels = np.zeros(n_pixels, dtype=bool)
index = np.arange(n_pixels)
@cache
def _get_pixel_index(n_pixels):
"""Cached version of ``np.arange(n_pixels)``"""
return np.arange(n_pixels)


def _get_invalid_pixels(n_channels, n_pixels, pixel_status, selected_gain_channel):
broken_pixels = np.zeros((n_channels, n_pixels), dtype=bool)

index = _get_pixel_index(n_pixels)
masks = (
pixel_status.hardware_failing_pixels,
pixel_status.pedestal_failing_pixels,
pixel_status.flatfield_failing_pixels,
)
for mask in masks:
if mask is not None:
broken_pixels |= mask[selected_gain_channel, index]
if selected_gain_channel is not None:
broken_pixels |= mask[selected_gain_channel, index]
else:
broken_pixels |= mask
Hckjs marked this conversation as resolved.
Show resolved Hide resolved

return broken_pixels

Expand Down Expand Up @@ -188,7 +199,7 @@ def _calibrate_dl0(self, event, tel_id):
)

dl0_waveform = r1.waveform.copy()
dl0_waveform[~signal_pixels] = 0
dl0_waveform[:, ~signal_pixels] = 0

dl0_pixel_status = r1.pixel_status.copy()
# set dvr pixel bit in pixel_status for pixels kept by DVR
Expand All @@ -211,24 +222,26 @@ def _calibrate_dl1(self, event, tel_id):
if self._check_dl0_empty(waveforms):
return

n_pixels, n_samples = waveforms.shape
n_channels, n_pixels, n_samples = waveforms.shape

selected_gain_channel = event.dl0.tel[tel_id].selected_gain_channel
broken_pixels = _get_invalid_pixels(
n_channels,
n_pixels,
event.mon.tel[tel_id].pixel_status,
selected_gain_channel,
)
pixel_index = _get_pixel_index(n_pixels)

dl1_calib = event.calibration.tel[tel_id].dl1
time_shift = event.calibration.tel[tel_id].dl1.time_shift
readout = self.subarray.tel[tel_id].camera.readout

# subtract any remaining pedestal before extraction
if dl1_calib.pedestal_offset is not None:
# this copies intentionally, we don't want to modify the dl0 data
# waveforms have shape (n_pixel, n_samples), pedestals (n_pixels, )
waveforms = waveforms - dl1_calib.pedestal_offset[:, np.newaxis]
Hckjs marked this conversation as resolved.
Show resolved Hide resolved
# waveforms have shape (n_channels, n_pixel, n_samples), pedestals (n_pixels)
waveforms = waveforms.copy()
waveforms -= np.atleast_2d(dl1_calib.pedestal_offset)[..., np.newaxis]

if n_samples == 1:
# To handle ASTRI and dst
Expand All @@ -238,13 +251,18 @@ def _calibrate_dl1(self, event, tel_id):
# - Don't do anything if dl1 container already filled
# - Update on SST review decision
dl1 = DL1CameraContainer(
image=waveforms[..., 0].astype(np.float32),
image=np.squeeze(waveforms).astype(np.float32),
peak_time=np.zeros(n_pixels, dtype=np.float32),
is_valid=True,
)
else:
# shift waveforms if time_shift calibration is available
time_shift = dl1_calib.time_shift
remaining_shift = None
if time_shift is not None:
if selected_gain_channel is not None:
time_shift = time_shift[selected_gain_channel, pixel_index]

if self.apply_waveform_time_shift.tel[tel_id]:
sampling_rate = readout.sampling_rate.to_value(u.GHz)
time_shift_samples = time_shift * sampling_rate
Expand All @@ -264,11 +282,22 @@ def _calibrate_dl1(self, event, tel_id):
)

# correct non-integer remainder of the shift if given
if self.apply_peak_time_shift.tel[tel_id] and time_shift is not None:
if self.apply_peak_time_shift.tel[tel_id] and remaining_shift is not None:
Tobychev marked this conversation as resolved.
Show resolved Hide resolved
dl1.peak_time -= remaining_shift

# Calibrate extracted charge
dl1.image *= dl1_calib.relative_factor / dl1_calib.absolute_factor
if (
dl1_calib.relative_factor is not None
and dl1_calib.absolute_factor is not None
):
if selected_gain_channel is None:
dl1.image *= dl1_calib.relative_factor / dl1_calib.absolute_factor
else:
corr = (
dl1_calib.relative_factor[selected_gain_channel, pixel_index]
/ dl1_calib.absolute_factor[selected_gain_channel, pixel_index]
)
dl1.image *= corr

# handle invalid pixels
if self.invalid_pixel_handler is not None:
Expand Down Expand Up @@ -309,21 +338,21 @@ def shift_waveforms(waveforms, time_shift_samples):

Parameters
----------
waveforms: ndarray of shape (n_pixels, n_samples)
waveforms: ndarray of shape (n_channels, n_pixels, n_samples)
The waveforms to shift
time_shift_samples: ndarray of shape (n_pixels, )
time_shift_samples: ndarray
The shift to apply in units of samples.
Waveforms are shifted to the left by the smallest integer
that minimizes inter-pixel differences.

Returns
-------
shifted_waveforms: ndarray of shape (n_pixels, n_samples)
shifted_waveforms: ndarray of shape (n_channels, n_pixels, n_samples)
The shifted waveforms
remaining_shift: ndarray of shape (n_pixels, )
remaining_shift: ndarray
The remaining shift after applying the integer shift to the waveforms.
"""
mean_shift = time_shift_samples.mean()
mean_shift = time_shift_samples.mean(axis=-1, keepdims=True)
integer_shift = np.round(time_shift_samples - mean_shift).astype("int16")
remaining_shift = time_shift_samples - integer_shift
shifted_waveforms = _shift_waveforms_by_integer(waveforms, integer_shift)
Expand Down
25 changes: 11 additions & 14 deletions src/ctapipe/calib/camera/flatfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,11 @@ def _extract_charge(self, event) -> DL1CameraContainer:
"""

waveforms = event.r1.tel[self.tel_id].waveform
n_channels, n_pixels, _ = waveforms.shape
selected_gain_channel = event.r1.tel[self.tel_id].selected_gain_channel
broken_pixels = _get_invalid_pixels(
n_pixels=waveforms.shape[-2],
n_channels=n_channels,
n_pixels=n_pixels,
pixel_status=event.mon.tel[self.tel_id].pixel_status,
selected_gain_channel=selected_gain_channel,
)
Expand Down Expand Up @@ -215,20 +217,15 @@ def calculate_relative_gain(self, event):
if self.n_events_seen == self.sample_size:
self.n_events_seen = 0

# real data
trigger_time = event.trigger.time
if event.meta["origin"] != "hessio":
hardware_or_pedestal_mask = np.logical_or(
event.mon.tel[self.tel_id].pixel_status.hardware_failing_pixels,
event.mon.tel[self.tel_id].pixel_status.pedestal_failing_pixels,
)
pixel_mask = np.logical_or(
hardware_or_pedestal_mask,
event.mon.tel[self.tel_id].pixel_status.flatfield_failing_pixels,
)

else: # patches for MC data
pixel_mask = np.zeros(waveform.shape[1], dtype=bool)
hardware_or_pedestal_mask = np.logical_or(
event.mon.tel[self.tel_id].pixel_status.hardware_failing_pixels,
event.mon.tel[self.tel_id].pixel_status.pedestal_failing_pixels,
)
pixel_mask = np.logical_or(
hardware_or_pedestal_mask,
event.mon.tel[self.tel_id].pixel_status.flatfield_failing_pixels,
)

if self.n_events_seen == 0:
self.time_start = trigger_time
Expand Down
20 changes: 10 additions & 10 deletions src/ctapipe/calib/camera/gainselection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Algorithms to select correct gain channel
"""

from abc import abstractmethod
from enum import IntEnum

Expand Down Expand Up @@ -47,16 +48,15 @@ def __call__(self, waveforms):
Shape: n_pix
Dtype: int8
"""
if waveforms.ndim == 2: # Return None if already gain selected
return None
elif waveforms.ndim == 3:
n_channels, n_pixels, _ = waveforms.shape
if n_channels == 1: # Must be first channel if only one channel
return np.zeros(n_pixels, dtype=np.int8)
else:
return self.select_channel(waveforms)
else:
raise ValueError(f"Cannot handle waveform array of shape: {waveforms.ndim}")
if waveforms.ndim != 3:
raise ValueError(
f"Cannot handle waveform array of shape: {waveforms.shape}"
)

n_channels, n_pixels, _ = waveforms.shape
if n_channels == 1:
return np.zeros(n_pixels, dtype=np.int8)
return self.select_channel(waveforms)

@abstractmethod
def select_channel(self, waveforms):
Expand Down
10 changes: 4 additions & 6 deletions src/ctapipe/calib/camera/pedestals.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,14 @@ def _extract_charge(self, event) -> DL1CameraContainer:
DL1CameraContainer
"""
waveforms = event.r1.tel[self.tel_id].waveform
n_channels, n_pixels, _ = waveforms.shape
selected_gain_channel = event.r1.tel[self.tel_id].selected_gain_channel
broken_pixels = _get_invalid_pixels(
n_pixels=waveforms.shape[-2],
n_channels=n_channels,
n_pixels=n_pixels,
pixel_status=event.mon.tel[self.tel_id].pixel_status,
selected_gain_channel=selected_gain_channel,
)

# Extract charge and time
if self.extractor:
return self.extractor(
Expand Down Expand Up @@ -247,10 +248,7 @@ def calculate_pedestals(self, event):

# real data
trigger_time = event.trigger.time
if event.meta["origin"] != "hessio":
pixel_mask = event.mon.tel[self.tel_id].pixel_status.hardware_failing_pixels
else: # patches for MC data
pixel_mask = np.zeros(waveform.shape[1], dtype=bool)
pixel_mask = event.mon.tel[self.tel_id].pixel_status.hardware_failing_pixels

if self.n_events_seen == 0:
self.time_start = trigger_time
Expand Down
Loading