예제 #1
0
class NeighborPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak defined by the wavefroms in neighboring pixels.
    """

    window_width = IntTelescopeParameter(
        default_value=7, help="Define the width of the integration window"
    ).tag(config=True)
    window_shift = IntTelescopeParameter(
        default_value=3,
        help="Define the shift of the integration window "
        "from the peak_index (peak_index - shift)",
    ).tag(config=True)
    lwt = IntTelescopeParameter(
        default_value=0,
        help="Weight of the local pixel (0: peak from neighbors only, "
        "1: local pixel counts as much as any neighbor)",
    ).tag(config=True)

    def __call__(self, waveforms, telid=None):
        neighbors = self.subarray.tel[telid].camera.neighbor_matrix_where
        average_wfs = neighbor_average_waveform(
            waveforms, neighbors, self.lwt.tel[telid]
        )
        peak_index = average_wfs.argmax(axis=-1)
        charge, pulse_time = extract_around_peak(
            waveforms,
            peak_index,
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )
        return charge, pulse_time
예제 #2
0
class FixedWindowSum(ImageExtractor):
    """
    Extractor that sums within a fixed window defined by the user.
    """

    window_start = IntTelescopeParameter(
        default_value=0, help="Define the start position for the integration window"
    ).tag(config=True)
    window_width = IntTelescopeParameter(
        default_value=7, help="Define the width of the integration window"
    ).tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        """
        Assuming the pulse is centered in the manually defined integration
        window, the integration_correction with a shift=0 is correct
        """
        readout = self.subarray.tel[telid].camera.readout
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value('ns'),
            (1/readout.sampling_rate).to_value('ns'),
            self.window_width.tel[telid],
            0,
        )

    def __call__(self, waveforms, telid, selected_gain_channel):
        charge, pulse_time = extract_around_peak(
            waveforms, self.window_start.tel[telid], self.window_width.tel[telid], 0,
            self.sampling_rate[telid]
        )
        correction = self._calculate_correction(telid=telid)[selected_gain_channel]
        return charge * correction, pulse_time
예제 #3
0
class LocalPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak in each pixel's waveform.
    """

    window_width = IntTelescopeParameter(
        default_value=7,
        help="Define the width of the integration window").tag(config=True)
    window_shift = IntTelescopeParameter(
        default_value=3,
        help="Define the shift of the integration window"
        "from the peak_index (peak_index - shift)",
    ).tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        readout = self.subarray.tel[telid].camera.readout
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value('ns'),
            (1 / readout.sampling_rate).to_value('ns'),
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )

    def __call__(self, waveforms, telid, selected_gain_channel):
        peak_index = waveforms.argmax(axis=-1).astype(np.int)
        charge, pulse_time = extract_around_peak(waveforms, peak_index,
                                                 self.window_width.tel[telid],
                                                 self.window_shift.tel[telid],
                                                 self.sampling_rate[telid])
        correction = self._calculate_correction(
            telid=telid)[selected_gain_channel]
        return charge * correction, pulse_time
예제 #4
0
class FixedWindowSum(ImageExtractor):
    """
    Extractor that sums within a fixed window defined by the user.
    """

    peak_index = IntTelescopeParameter(
        default_value=0,
        help="Manually select index where the peak is located").tag(
            config=True)
    window_width = IntTelescopeParameter(
        default_value=7,
        help="Define the width of the integration window").tag(config=True)
    window_shift = IntTelescopeParameter(
        default_value=0,
        help="Define the shift of the integration window from the peak_index "
        "(peak_index - shift)",
    ).tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        """
        Calculate the correction for the extracted change such that the value
        returned would equal 1 for a noise-less unit pulse.

        This method is decorated with @lru_cache to ensure it is only
        calculated once per telescope.

        Parameters
        ----------
        telid : int

        Returns
        -------
        correction : ndarray
        The correction to apply to an extracted charge using this ImageExtractor
        Has size n_channels, as a different correction value might be required
        for different gain channels.
        """
        readout = self.subarray.tel[telid].camera.readout
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value("ns"),
            (1 / readout.sampling_rate).to_value("ns"),
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )

    def __call__(self, waveforms, telid, selected_gain_channel):
        charge, peak_time = extract_around_peak(
            waveforms,
            self.peak_index.tel[telid],
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
            self.sampling_rate[telid],
        )
        charge *= self._calculate_correction(
            telid=telid)[selected_gain_channel]
        return charge, peak_time
예제 #5
0
class NeighborPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak defined by the wavefroms in neighboring pixels.
    """

    window_width = IntTelescopeParameter(
        default_value=7, help="Define the width of the integration window"
    ).tag(config=True)
    window_shift = IntTelescopeParameter(
        default_value=3,
        help="Define the shift of the integration window "
        "from the peak_index (peak_index - shift)",
    ).tag(config=True)
    lwt = IntTelescopeParameter(
        default_value=0,
        help="Weight of the local pixel (0: peak from neighbors only, "
        "1: local pixel counts as much as any neighbor)",
    ).tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        readout = self.subarray.tel[telid].camera.readout
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value('ns'),
            (1/readout.sampling_rate).to_value('ns'),
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )

    def __call__(self, waveforms, telid, selected_gain_channel):
        neighbors = self.subarray.tel[telid].camera.geometry.neighbor_matrix_where
        average_wfs = neighbor_average_waveform(
            waveforms, neighbors, self.lwt.tel[telid]
        )
        peak_index = average_wfs.argmax(axis=-1)
        charge, pulse_time = extract_around_peak(
            waveforms,
            peak_index,
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
            self.sampling_rate[telid]
        )
        correction = self._calculate_correction(telid=telid)[selected_gain_channel]
        return charge * correction, pulse_time
예제 #6
0
class FixedWindowSum(ImageExtractor):
    """
    Extractor that sums within a fixed window defined by the user.
    """

    window_start = IntTelescopeParameter(
        default_value=0, help="Define the start position for the integration window"
    ).tag(config=True)
    window_width = IntTelescopeParameter(
        default_value=7, help="Define the width of the integration window"
    ).tag(config=True)

    def __call__(self, waveforms, telid=None):
        charge, pulse_time = extract_around_peak(
            waveforms, self.window_start.tel[telid], self.window_width.tel[telid], 0
        )
        return charge, pulse_time
예제 #7
0
class LocalPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak in each pixel's waveform.
    """
    window_width = IntTelescopeParameter(
        default_value=7,
        help='Define the width of the integration window').tag(config=True)
    window_shift = IntTelescopeParameter(
        default_value=3,
        help='Define the shift of the integration window'
        'from the peak_index (peak_index - shift)').tag(config=True)

    def __call__(self, waveforms, telid=None):
        peak_index = waveforms.argmax(axis=-1).astype(np.int)
        charge, pulse_time = extract_around_peak(waveforms, peak_index,
                                                 self.window_width[telid],
                                                 self.window_shift[telid])
        return charge, pulse_time
예제 #8
0
class GlobalPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak from the global average waveform.
    """

    window_width = IntTelescopeParameter(
        default_value=7, help="Define the width of the integration window"
    ).tag(config=True)
    window_shift = IntTelescopeParameter(
        default_value=3,
        help="Define the shift of the integration window from the peak_index "
        "(peak_index - shift)",
    ).tag(config=True)

    def __call__(self, waveforms, telid=None):
        peak_index = waveforms.mean(axis=-2).argmax(axis=-1)
        charge, pulse_time = extract_around_peak(
            waveforms,
            peak_index,
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )
        return charge, pulse_time
예제 #9
0
    class SomeComponent(Component):
        tel_param1 = IntTelescopeParameter(
            default_value=[("type", "*", 10), ("type", "LST*", 100)])

        tel_param2 = FloatTelescopeParameter(default_value=[
            ("type", "*", 10.0),
            ("type", "LST_LST_LSTCam", 100.0),
            ("id", 3, 200.0),
        ])

        tel_param3 = FloatTelescopeParameter(default_value=[
            ("type", "*", 10.0),
            ("type", "LST_LST_LSTCam", 100.0),
            ("type", "*", 200.0),  # should overwrite everything with 200.0
            ("id", 100, 300.0),
        ])
예제 #10
0
class TailCutsDataVolumeReducer(DataVolumeReducer):
    """
    Reduce the time integrated shower image in 3 Steps:

    1) Select pixels with tailcuts_clean.
    2) Add iteratively all pixels with Signal S >= boundary_thresh
       with ctapipe module dilate until no new pixels were added.
    3) Adding new pixels with dilate to get more conservative.
    """

    n_end_dilates = IntTelescopeParameter(
        default_value=1,
        help="Number of how many times to dilate at the end.").tag(config=True)
    do_boundary_dilation = BoolTelescopeParameter(
        default_value=True,
        help="If set to 'False', the iteration steps in 2) are skipped and"
        "normal TailcutCleaning is used.",
    ).tag(config=True)

    def select_pixels(self, waveforms, telid=None, selected_gain_channel=None):
        camera_geom = self.subarray.tel[telid].camera.geometry
        # Pulse-integrate waveforms
        charge, _ = self.image_extractor(
            waveforms,
            telid=telid,
            selected_gain_channel=selected_gain_channel)

        # 1) Step: TailcutCleaning at first
        mask = self.cleaner(telid, charge)
        pixels_above_boundary_thresh = (
            charge >= self.cleaner.boundary_threshold_pe.tel[telid])
        mask_in_loop = np.array([])
        # 2) Step: Add iteratively all pixels with Signal
        #          S > boundary_thresh with ctapipe module
        #          'dilate' until no new pixels were added.
        while (not np.array_equal(mask, mask_in_loop)
               and self.do_boundary_dilation.tel[telid]):
            mask_in_loop = mask
            mask = dilate(camera_geom, mask) & pixels_above_boundary_thresh

        # 3) Step: Adding Pixels with 'dilate' to get more conservative.
        for _ in range(self.n_end_dilates.tel[telid]):
            mask = dilate(camera_geom, mask)

        return mask
예제 #11
0
 class SomeComponentInt(Component):
     tel_param = IntTelescopeParameter(default_value=1)
예제 #12
0
 class SomeComponent(Component):
     tel_param = TelescopeParameter()
     tel_param_int = IntTelescopeParameter()
예제 #13
0
class TwoPassWindowSum(ImageExtractor):
    """Extractor based on [1]_ which integrates the waveform a second time using
    a time-gradient linear fit. This is in particular the version implemented
    in the CTA-MARS analysis pipeline [2]_.

    Notes
    -----

    #. slide a 3-samples window through the waveform, finding max counts sum;
       the range of the sliding is the one allowing extension from 3 to 5;
       add 1 sample on each side and integrate charge in the 5-sample window;
       time is obtained as a charge-weighted average of the sample numbers;
       No information from neighboouring pixels is used.
    #. Preliminary image cleaning via simple tailcut with minimum number
       of core neighbours set at 1,
    #. Only the biggest cluster of pixels is kept.
    #. Parametrize following Hillas approach only if the resulting image has 3
       or more pixels.
    #. Do a linear fit of pulse time vs. distance along major image axis
       (CTA-MARS uses ROOT "robust" fit option,
       aka Least Trimmed Squares, to get rid of far outliers - this should
       be implemented in 'timing_parameters', e.g scipy.stats.siegelslopes).
    #. For all pixels except the core ones in the main island, integrate
       the waveform once more, in a fixed window of 5 samples set at the time
       "predicted" by the linear time fit.
       If the predicted time for a pixel leads to a window outside the readout
       window, then integrate the last (or first) 5 samples.
    #. The result is an image with main-island core pixels calibrated with a
       1st pass and non-core pixels re-calibrated with a 2nd pass.

    References
    ----------
    .. [1] J. Holder et al., Astroparticle Physics, 25, 6, 391 (2006)
    .. [2] https://forge.in2p3.fr/projects/step-by-step-reference-mars-analysis/wiki

    """

    # Get thresholds for core-pixels depending on telescope type.
    # WARNING: default values are not yet optimized
    core_threshold = FloatTelescopeParameter(
        default_value=[
            ("type", "*", 6.0),
            ("type", "LST*", 6.0),
            ("type", "MST*", 8.0),
            ("type", "SST*", 4.0),
        ],
        help="Picture threshold for internal tail-cuts pass",
    ).tag(config=True)

    disable_second_pass = Bool(
        default_value=False,
        help="only run the first pass of the extractor, for debugging purposes",
    ).tag(config=True)

    peak_finding_window_width = IntTelescopeParameter(
        default_value=3, help="width of sliding window used to do peak detection"
    ).tag(config=True)

    @lru_cache(maxsize=4096)
    def _calculate_correction(self, telid, width, shift):
        """Obtain the correction for the integration window specified for each
        pixel.

        The TwoPassWindowSum image extractor applies potentially different
        parameters for the integration window to each pixel, depending on the
        position of the peak. It has been decided to apply gain selection
        directly here. For basic definitions look at the documentation of
        `integration_correction`.

        Parameters
        ----------
        telid : int
            Index of the telescope in use.
        width : int
            Width of the integration window in samples
        shift : int
            Window shift to the left of the pulse peak in samples

        Returns
        -------
        correction : ndarray
            Value of the pixel-wise gain-selected integration correction.

        """
        readout = self.subarray.tel[telid].camera.readout
        # Calculate correction of first pixel for both channels
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value("ns"),
            (1 / readout.sampling_rate).to_value("ns"),
            width,
            shift,
        )

    def _apply_first_pass(
        self, waveforms, telid
    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Execute step 1.

        Parameters
        ----------
        waveforms : array of size (N_pixels, N_samples)
            DL0-level waveforms of one event.
        telid : int
            Index of the telescope.

        Returns
        -------
        charge : array_like
            Integrated charge per pixel.
            Shape: (n_pix)
        pulse_time : array_like
            Samples in which the waveform peak has been recognized.
            Shape: (n_pix)
        """
        # STEP 1

        # Starting from DL0, the channel is already selected (if more than one)
        # event.dl0.tel[tel_id].waveform object has shape (N_pixels, N_samples)

        # For each pixel, we slide a 3-samples window through the
        # waveform without touching the extremes (so later we can increase it
        # to 5), summing each time the ADC counts contained within it.

        # 'width' could be configurable in a generalized version
        # Right now this image extractor is optimized for LSTCam and NectarCam
        width = self.peak_finding_window_width.tel[telid]
        sums = convolve1d(waveforms, np.ones(width), axis=1, mode="nearest")
        # Note that the input waveforms are clipped at the extremes because
        # we want to extend this 3-samples window to 5 samples
        # 'sums' has now the shape of (N_pixels, N_samples-4)

        # For each pixel, in each of the (N_samples - 4) positions, we check
        # where the window encountered the maximum number of ADC counts
        start_windows = np.argmax(sums, axis=1)
        # Now startWindows has the shape of (N_pixels).
        # Note that the index values stored in startWindows come from 'sums'
        # of which the first index (0) corresponds of index 1 of each waveform
        # since we clipped them before.

        # Since we have to add 1 sample on each side, window_shift will always
        # be (-)1, while window_width will always be window1_width + 1
        # so we the final 5-samples window will be 1+3+1
        window_width = width + 2
        window_shift = 1

        # the 'peak_index' argument of 'extract_around_peak' has a different
        # meaning here: it's the start of the 3-samples window.
        # Since since the "sums" arrays started from index 1 of each waveform,
        # then each peak index has to be increased by one
        charge_1stpass, pulse_time_1stpass = extract_around_peak(
            waveforms,
            start_windows + 1,
            window_width,
            window_shift,
            self.sampling_rate[telid],
        )

        # Get integration correction factors
        correction = self._calculate_correction(telid, window_width, window_shift)

        return charge_1stpass, pulse_time_1stpass, correction

    def _apply_second_pass(
        self,
        waveforms,
        telid,
        selected_gain_channel,
        charge_1stpass_uncorrected,
        pulse_time_1stpass,
        correction,
    ) -> Tuple[np.ndarray, np.ndarray]:
        """
        Follow steps from 2 to 7.

        Parameters
        ----------
        waveforms : array of shape (N_pixels, N_samples)
            DL0-level waveforms of one event.
        telid : int
            Index of the telescope.
        selected_gain_channel: array of shape (N_channels, N_pixels)
            Array containing the index of the selected gain channel for each
            pixel (0 for low gain, 1 for high gain).
        charge_1stpass_uncorrected : array of shape N_pixels
            Pixel charges reconstructed with the 1st pass, but not corrected.
        pulse_time_1stpass : array of shape N_pixels
            Pixel-wise pulse times reconstructed with the 1st pass.
        correction: array of shape N_pixels
            Charge correction from 1st pass.

        Returns
        -------
        charge : array_like
            Integrated charge per pixel.
            Note that in the case of a very bright full-camera image this can
            coincide the 1st pass information.
            Also in the case of very dim images the 1st pass will be recycled,
            but in this case the resulting image should be discarded
            from further analysis.
            Shape: (n_pix)
        pulse_time : array_like
            Samples in which the waveform peak has been recognized.
            Same specifications as above.
            Shape: (n_pix)
        """
        # STEP 2

        # Apply correction to 1st pass charges
        charge_1stpass = charge_1stpass_uncorrected * correction[selected_gain_channel]

        # Set thresholds for core-pixels depending on telescope
        core_th = self.core_threshold.tel[telid]
        # Boundary thresholds will be half of core thresholds.

        # Preliminary image cleaning with simple two-level tail-cut
        camera_geometry = self.subarray.tel[telid].camera.geometry
        mask_1 = tailcuts_clean(
            camera_geometry,
            charge_1stpass,
            picture_thresh=core_th,
            boundary_thresh=core_th / 2,
            keep_isolated_pixels=False,
            min_number_picture_neighbors=1,
        )
        image_1 = charge_1stpass.copy()
        image_1[~mask_1] = 0

        # STEP 3

        # find all islands using this cleaning
        num_islands, labels = number_of_islands(camera_geometry, mask_1)
        if num_islands == 0:
            image_2 = image_1.copy()  # no islands = image unchanged
        else:
            # ...find the biggest one
            mask_biggest = largest_island(labels)
            image_2 = image_1.copy()
            image_2[~mask_biggest] = 0

        # Indexes of pixels that will need the 2nd pass
        non_core_pixels_ids = np.where(image_2 < core_th)[0]
        non_core_pixels_mask = image_2 < core_th

        # STEP 4

        # if the resulting image has less then 3 pixels
        # or there are more than 3 pixels but all contain a number of
        # photoelectrons above the core threshold
        if np.count_nonzero(image_2) < 3:
            # we return the 1st pass information
            # NOTE: In this case, the image was not bright enough!
            # We should label it as "bad and NOT use it"
            return charge_1stpass, pulse_time_1stpass
        elif len(non_core_pixels_ids) == 0:
            # Since all reconstructed charges are above the core threshold,
            # there is no need to perform the 2nd pass.
            # We return the 1st pass information.
            # NOTE: In this case, even if this is 1st pass information,
            # the image is actually very bright! We should label it as "good"!
            return charge_1stpass, pulse_time_1stpass

        # otherwise we proceed by parametrizing the image
        hillas = hillas_parameters(camera_geometry, image_2)

        # STEP 5

        # linear fit of pulse time vs. distance along major image axis
        # using only the main island surviving the preliminary
        # image cleaning
        # WARNING: in case of outliers, the fit can perform better if
        # it is a robust algorithm.
        timing = timing_parameters(camera_geometry, image_2, pulse_time_1stpass, hillas)

        # get projected distances along main image axis
        long, _ = camera_to_shower_coordinates(
            camera_geometry.pix_x, camera_geometry.pix_y, hillas.x, hillas.y, hillas.psi
        )

        # get the predicted times as a linear relation
        predicted_pulse_times = (
            timing.slope * long[non_core_pixels_ids] + timing.intercept
        )

        predicted_peaks = np.zeros(len(predicted_pulse_times))

        # Convert time in ns to sample index using the sampling rate from
        # the readout.
        # Approximate the value obtained to nearest integer, then cast to
        # int64 otherwise 'extract_around_peak' complains.
        sampling_rate = self.sampling_rate[telid]
        np.rint(predicted_pulse_times.value * sampling_rate, predicted_peaks)
        predicted_peaks = predicted_peaks.astype(np.int64)

        # Due to the fit these peak indexes can now be also outside of the
        # readout window, so later we check for this.

        # STEP 6

        # select only the waveforms correspondent to the non-core pixels
        # of the main island survived from the 1st pass image cleaning
        non_core_waveforms = waveforms[non_core_pixels_ids]

        # Build 'width' and 'shift' arrays that adapt on the position of the
        # window along each waveform

        # Now the definition of peak_index is really the peak.
        # We have to add 2 samples each side, so the shift will always
        # be (-)2, while width will always end 4 samples to the right.
        # This "always" refers to a 5-samples window of course
        window_width_default = 5
        window_shift_default = 2

        # now let's deal with some edge cases: the predicted peak falls before
        # or after the readout window:
        peak_before_window = predicted_peaks < 0
        peak_after_window = predicted_peaks > (non_core_waveforms.shape[1] - 1)

        # BUT, if the resulting 5-samples window falls outside of the readout
        # window then we take the first (or last) 5 samples
        window_width_before = 5
        window_shift_before = 0

        # in the case where the window is after, shift backward
        window_width_after = 5
        window_shift_after = 5

        # and put them together:
        window_widths = np.full(non_core_waveforms.shape[0], window_width_default)
        window_widths[peak_before_window] = window_width_before
        window_widths[peak_after_window] = window_width_after
        window_shifts = np.full(non_core_waveforms.shape[0], window_shift_default)
        window_shifts[peak_before_window] = window_shift_before
        window_shifts[peak_after_window] = window_shift_after

        # Now we can also (re)define the patological predicted times
        # because (we needed them to define the corrispective widths
        # and shifts)
        # set sample to 0 (beginning of the waveform) if predicted time
        # falls before
        predicted_peaks[predicted_peaks < 0] = 0
        # set sample to max-1 (first sample has index 0)
        # if predicted time falls after
        predicted_peaks[predicted_peaks > (waveforms.shape[1] - 1)] = (
            waveforms.shape[1] - 1
        )

        # re-calibrate non-core pixels using the fixed 5-samples window
        charge_no_core, pulse_times_no_core = extract_around_peak(
            non_core_waveforms,
            predicted_peaks,
            window_widths,
            window_shifts,
            self.sampling_rate[telid],
        )

        # Modify integration correction factors only for non-core pixels
        # now we compute 3 corrections for the default, before, and after cases:
        correction = self._calculate_correction(
            telid, window_width_default, window_shift_default
        )[selected_gain_channel][non_core_pixels_mask]

        correction_before = self._calculate_correction(
            telid, window_width_before, window_shift_before
        )[selected_gain_channel][non_core_pixels_mask]

        correction_after = self._calculate_correction(
            telid, window_width_after, window_shift_after
        )[selected_gain_channel][non_core_pixels_mask]

        correction[peak_before_window] = correction_before[peak_before_window]
        correction[peak_after_window] = correction_after[peak_after_window]

        charge_no_core *= correction

        # STEP 7

        # Combine core and non-core pixels in the final output

        # this is the biggest cluster from the cleaned image
        # it contains the core pixels (which we leave untouched)
        # plus possibly some non-core pixels
        charge_2ndpass = image_2.copy()
        # Now we overwrite the charges of all non-core pixels in the camera
        # plus all those pixels which didn't survive the preliminary
        # cleaning.
        # We apply also their corrections.
        charge_2ndpass[non_core_pixels_mask] = charge_no_core

        # Same approach for the pulse times
        pulse_time_2ndpass = pulse_time_1stpass  # core + non-core pixels
        pulse_time_2ndpass[
            non_core_pixels_mask
        ] = pulse_times_no_core  # non-core pixels

        return charge_2ndpass, pulse_time_2ndpass

    def __call__(self, waveforms, telid, selected_gain_channel):
        """
        Call this ImageExtractor.

        Parameters
        ----------
        waveforms : array of shape (N_pixels, N_samples)
            DL0-level waveforms of one event.
        telid : int
            Index of the telescope.
        selected_gain_channel: array of shape (N_channels, N_pixels)
            Array containing the index of the selected gain channel for each
            pixel (0 for low gain, 1 for high gain).

        Returns
        -------
        charge : array_like
            Integrated charge per pixel.
            Shape: (n_pix)
        pulse_time : array_like
            Samples in which the waveform peak has been recognized.
            Shape: (n_pix)
        """

        charge1, pulse_time1, correction1 = self._apply_first_pass(waveforms, telid)

        # FIXME: properly make sure that output is 32Bit instead of downcasting here
        if self.disable_second_pass:
            return (
                (charge1 * correction1[selected_gain_channel]).astype("float32"),
                pulse_time1.astype("float32"),
            )

        charge2, pulse_time2 = self._apply_second_pass(
            waveforms, telid, selected_gain_channel, charge1, pulse_time1, correction1
        )
        # FIXME: properly make sure that output is 32Bit instead of downcasting here
        return charge2.astype("float32"), pulse_time2.astype("float32")
예제 #14
0
class SlidingWindowMaxSum(ImageExtractor):
    """
    Sliding window extractor that maximizes the signal in window_width consecutive slices.
    """

    window_width = IntTelescopeParameter(
        default_value=7, help="Define the width of the integration window"
    ).tag(config=True)

    apply_integration_correction = BoolTelescopeParameter(
        default_value=True, help="Apply the integration window correction"
    ).tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        """
        Calculate the correction for the extracted charge such that the value
        returned would equal 1 for a noise-less unit pulse.

        This method is decorated with @lru_cache to ensure it is only
        calculated once per telescope.

        The same procedure as for the actual SlidingWindowMaxSum extractor is used, but
        on the reference pulse_shape (that is also more finely binned)

        Parameters
        ----------
        telid : int

        Returns
        -------
        correction : ndarray
        The correction to apply to an extracted charge using this ImageExtractor
        Has size n_channels, as a different correction value might be required
        for different gain channels.
        """

        readout = self.subarray.tel[telid].camera.readout

        # compute the number of slices to integrate in the pulse template
        width_shape = int(
            round(
                (
                    self.window_width.tel[telid]
                    / readout.sampling_rate
                    / readout.reference_pulse_sample_width
                )
                .to("")
                .value
            )
        )

        n_channels = len(readout.reference_pulse_shape)
        correction = np.ones(n_channels, dtype=np.float)
        for ichannel, pulse_shape in enumerate(readout.reference_pulse_shape):

            # apply the same method as sliding window to find the highest sum
            cwf = np.cumsum(pulse_shape)
            # add zero at the begining so it is easier to substract the two arrays later
            cwf = np.concatenate((np.zeros(1), cwf))
            sums = cwf[width_shape:] - cwf[:-width_shape]
            maxsum = np.max(sums)
            correction[ichannel] = np.sum(pulse_shape) / maxsum

        return correction

    def __call__(self, waveforms, telid, selected_gain_channel):
        charge, peak_time = extract_sliding_window(
            waveforms, self.window_width.tel[telid], self.sampling_rate_ghz[telid]
        )
        if self.apply_integration_correction.tel[telid]:
            charge *= self._calculate_correction(telid=telid)[selected_gain_channel]
        return charge, peak_time
예제 #15
0
 class SomeComponent(TelescopeComponent):
     tel_param = TelescopeParameter(Float(default_value=0.0, allow_none=True))
     tel_param_int = IntTelescopeParameter()
예제 #16
0
class LocalPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak in each pixel's waveform.
    """

    window_width = IntTelescopeParameter(
        default_value=7,
        help="Define the width of the integration window").tag(config=True)

    window_shift = IntTelescopeParameter(
        default_value=3,
        help="Define the shift of the integration window"
        "from the peak_index (peak_index - shift)",
    ).tag(config=True)

    apply_integration_correction = BoolTelescopeParameter(
        default_value=True,
        help="Apply the integration window correction").tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        """
        Calculate the correction for the extracted change such that the value
        returned would equal 1 for a noise-less unit pulse.

        This method is decorated with @lru_cache to ensure it is only
        calculated once per telescope.

        Parameters
        ----------
        telid : int

        Returns
        -------
        correction : ndarray
        The correction to apply to an extracted charge using this ImageExtractor
        Has size n_channels, as a different correction value might be required
        for different gain channels.
        """
        readout = self.subarray.tel[telid].camera.readout
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value("ns"),
            (1 / readout.sampling_rate).to_value("ns"),
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )

    def __call__(self, waveforms, telid, selected_gain_channel):
        peak_index = waveforms.argmax(axis=-1).astype(np.int)
        charge, peak_time = extract_around_peak(
            waveforms,
            peak_index,
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
            self.sampling_rate[telid],
        )
        if self.apply_integration_correction.tel[telid]:
            charge *= self._calculate_correction(
                telid=telid)[selected_gain_channel]
        return charge, peak_time
예제 #17
0
class LSTR0Corrections(TelescopeComponent):
    """
    The base R0-level calibrator. Changes the r0 container.

    The R0 calibrator performs the camera-specific R0 calibration that is
    usually performed on the raw data by the camera server.
    This calibrator exists in lstchain for testing and prototyping purposes.
    """
    offset = IntTelescopeParameter(
        default_value=0,
        help=(
            'Define offset to be subtracted from the waveform *additionally*'
            ' to the drs4 pedestal offset. This only needs to be given when'
            ' the drs4 pedestal calibration is not applied or the offset of the'
            ' drs4 run is different from the data run'
        )
    ).tag(config=True)

    r1_sample_start = IntTelescopeParameter(
        default_value=3,
        help='Start sample for r1 waveform',
        allow_none=True,
    ).tag(config=True)

    r1_sample_end = IntTelescopeParameter(
        default_value=39,
        help='End sample for r1 waveform',
        allow_none=True,
    ).tag(config=True)

    drs4_pedestal_path = TelescopeParameter(
        trait=Path(exists=True, directory_ok=False),
        allow_none=True,
        default_value=None,
        help=(
            'Path to the LST pedestal file'
            ', required when `apply_drs4_pedestal_correction=True`'
        ),
    ).tag(config=True)

    calibration_path = Path(
        exists=True, directory_ok=False,
        help='Path to LST calibration file',
    ).tag(config=True)

    drs4_time_calibration_path = TelescopeParameter(
        trait=Path(exists=True, directory_ok=False),
        help='Path to the time calibration file',
        default_value=None,
        allow_none=True,
    ).tag(config=True)

    calib_scale_high_gain = FloatTelescopeParameter(
        default_value=1.0,
        help='High gain waveform is multiplied by this number'
    ).tag(config=True)

    calib_scale_low_gain = FloatTelescopeParameter(
        default_value=1.0,
        help='Low gain waveform is multiplied by this number'
    ).tag(config=True)

    select_gain = Bool(
        default_value=True,
        help='Set to False to keep both gains.'
    ).tag(config=True)

    apply_drs4_pedestal_correction = Bool(
        default_value=True,
        help=(
            'Set to False to disable drs4 pedestal correction.'
            ' Providing the drs4_pedestal_path is required to perform this calibration'
        ),
    ).tag(config=True)

    apply_timelapse_correction = Bool(
        default_value=True,
        help='Set to False to disable drs4 timelapse correction'
    ).tag(config=True)

    apply_spike_correction = Bool(
        default_value=True,
        help='Set to False to disable drs4 spike correction'
    ).tag(config=True)

    add_calibration_timeshift = Bool(
        default_value=True,
        help=(
            'If true, time correction from the calibration'
            ' file is added to calibration.dl1.time'
        ),
    ).tag(config=True)

    gain_selection_threshold = Float(
        default_value=3500,
        help='Threshold for the ThresholdGainSelector.'
    ).tag(config=True)

    def __init__(self, subarray, config=None, parent=None, **kwargs):
        """
        The R0 calibrator for LST data.
        Fill the r1 container.

        Parameters
        ----------
        """
        super().__init__(
            subarray=subarray, config=config, parent=parent, **kwargs
        )

        self.mon_data = None
        self.last_readout_time = {}
        self.first_cap = {}
        self.first_cap_old = {}
        self.fbn = {}
        self.fan = {}

        for tel_id in self.subarray.tel:
            shape = (N_GAINS, N_PIXELS, N_CAPACITORS_PIXEL)
            self.last_readout_time[tel_id] = np.zeros(shape, dtype='uint64')

            shape = (N_GAINS, N_PIXELS)
            self.first_cap[tel_id] = np.zeros(shape, dtype=int)
            self.first_cap_old[tel_id] = np.zeros(shape, dtype=int)

        if self.select_gain:
            self.gain_selector = ThresholdGainSelector(
                threshold=self.gain_selection_threshold,
                parent=self
            )
        else:
            self.gain_selector = None

        if self.calibration_path is not None:
            self.mon_data = self._read_calibration_file(self.calibration_path)

    def apply_drs4_corrections(self, event: LSTArrayEventContainer):
        self.update_first_capacitors(event)

        for tel_id, r0 in event.r0.tel.items():
            r1 = event.r1.tel[tel_id]
            # If r1 was not yet filled, copy of r0 converted
            if r1.waveform is None:
                r1.waveform = r0.waveform

            # float32 can represent all values of uint16 exactly,
            # so this does not loose precision.
            r1.waveform = r1.waveform.astype(np.float32, copy=False)

            # apply drs4 corrections
            if self.apply_drs4_pedestal_correction:
                self.subtract_pedestal(event, tel_id)

            if self.apply_timelapse_correction:
                self.time_lapse_corr(event, tel_id)

            if self.apply_spike_correction:
                self.interpolate_spikes(event, tel_id)

            # remove samples at beginning / end of waveform
            start = self.r1_sample_start.tel[tel_id]
            end = self.r1_sample_end.tel[tel_id]
            r1.waveform = r1.waveform[..., start:end]

            if self.offset.tel[tel_id] != 0:
                r1.waveform -= self.offset.tel[tel_id]

            mon = event.mon.tel[tel_id]
            if r1.selected_gain_channel is None:
                r1.waveform[mon.pixel_status.hardware_failing_pixels] = 0.0
            else:
                broken = mon.pixel_status.hardware_failing_pixels[r1.selected_gain_channel, PIXEL_INDEX]
                r1.waveform[broken] = 0.0


    def update_first_capacitors(self, event: LSTArrayEventContainer):
        for tel_id, lst in event.lst.tel.items():
            self.first_cap_old[tel_id] = self.first_cap[tel_id]
            self.first_cap[tel_id] = get_first_capacitors_for_pixels(
                lst.evt.first_capacitor_id,
                lst.svc.pixel_ids,
            )

    def calibrate(self, event: LSTArrayEventContainer):
        for tel_id in event.r0.tel:
            r1 = event.r1.tel[tel_id]
            # if `apply_drs4_corrections` is False, we did not fill in the
            # waveform yet.
            if r1.waveform is None:
                r1.waveform = event.r0.tel[tel_id].waveform

            r1.waveform = r1.waveform.astype(np.float32, copy=False)

            # do gain selection before converting to pe
            # like eventbuilder will do
            if self.select_gain and r1.selected_gain_channel is None:
                r1.selected_gain_channel = self.gain_selector(r1.waveform)
                r1.waveform = r1.waveform[r1.selected_gain_channel, PIXEL_INDEX]

            # apply monitoring data corrections,
            # subtract pedestal and convert to pe
            if self.mon_data is not None:
                calibration = self.mon_data.tel[tel_id].calibration
                convert_to_pe(
                    waveform=r1.waveform,
                    calibration=calibration,
                    selected_gain_channel=r1.selected_gain_channel
                )

            broken_pixels = event.mon.tel[tel_id].pixel_status.hardware_failing_pixels
            if r1.selected_gain_channel is None:
                r1.waveform[broken_pixels] = 0.0
            else:
                r1.waveform[broken_pixels[r1.selected_gain_channel, PIXEL_INDEX]] = 0.0

            # store calibration data needed for dl1 calibration in ctapipe
            # first drs4 time shift (zeros if no calib file was given)
            time_shift = self.get_drs4_time_correction(
                tel_id, self.first_cap[tel_id],
                selected_gain_channel=r1.selected_gain_channel,
            )

            # time shift from flat fielding
            if self.mon_data is not None and self.add_calibration_timeshift:
                time_corr = self.mon_data.tel[tel_id].calibration.time_correction
                # time_shift is subtracted in ctapipe,
                # but time_correction should be added
                if r1.selected_gain_channel is not None:
                    time_shift -= time_corr[r1.selected_gain_channel, PIXEL_INDEX].to_value(u.ns)
                else:
                    time_shift -= time_corr.to_value(u.ns)

            event.calibration.tel[tel_id].dl1.time_shift = time_shift

            # needed for charge scaling in ctpaipe dl1 calib
            if r1.selected_gain_channel is not None:
                relative_factor = np.empty(N_PIXELS)
                relative_factor[r1.selected_gain_channel == HIGH_GAIN] = self.calib_scale_high_gain.tel[tel_id]
                relative_factor[r1.selected_gain_channel == LOW_GAIN] = self.calib_scale_low_gain.tel[tel_id]
            else:
                relative_factor = np.empty((N_GAINS, N_PIXELS))
                relative_factor[HIGH_GAIN] = self.calib_scale_high_gain.tel[tel_id]
                relative_factor[LOW_GAIN] = self.calib_scale_low_gain.tel[tel_id]

            event.calibration.tel[tel_id].dl1.relative_factor = relative_factor

    @staticmethod
    def _read_calibration_file(path):
        """
        Read the correction from hdf5 calibration file
        """
        mon = MonitoringContainer()

        with tables.open_file(path) as f:
            tel_ids = [
                int(key[4:]) for key in f.root._v_children.keys()
                if key.startswith('tel_')
            ]

        for tel_id in tel_ids:
            with HDF5TableReader(path) as h5_table:
                base = f'/tel_{tel_id}'
                # read the calibration data
                table = base + '/calibration'
                next(h5_table.read(table, mon.tel[tel_id].calibration))

                # read pedestal data
                table = base + '/pedestal'
                next(h5_table.read(table, mon.tel[tel_id].pedestal))

                # read flat-field data
                table = base + '/flatfield'
                next(h5_table.read(table, mon.tel[tel_id].flatfield))

                # read the pixel_status container
                table = base + '/pixel_status'
                next(h5_table.read(table, mon.tel[tel_id].pixel_status))

        return mon

    @staticmethod
    def load_drs4_time_calibration_file(path):
        """
        Function to load calibration file.
        """
        with tables.open_file(path, 'r') as f:
            fan = f.root.fan[:]
            fbn = f.root.fbn[:]

        return fan, fbn

    def load_drs4_time_calibration_file_for_tel(self, tel_id):
        self.fan[tel_id], self.fbn[tel_id] = self.load_drs4_time_calibration_file(
            self.drs4_time_calibration_path.tel[tel_id]
        )

    def get_drs4_time_correction(self, tel_id, first_capacitors, selected_gain_channel=None):
        """
        Return pulse time after time correction.
        """

        if self.drs4_time_calibration_path.tel[tel_id] is None:
            if selected_gain_channel is None:
                return np.zeros((N_GAINS, N_PIXELS))
            else:
                return np.zeros(N_PIXELS)

        # load calib file if not already done
        if tel_id not in self.fan:
            self.load_drs4_time_calibration_file_for_tel(tel_id)

        if selected_gain_channel is not None:
            return calc_drs4_time_correction_gain_selected(
                first_capacitors,
                selected_gain_channel,
                self.fan[tel_id],
                self.fbn[tel_id],
            )
        else:
            return calc_drs4_time_correction_both_gains(
                first_capacitors,
                self.fan[tel_id],
                self.fbn[tel_id],
            )

    @staticmethod
    @lru_cache(maxsize=4)
    def _get_drs4_pedestal_data(path):
        """
        Function to load pedestal file.

        To make boundary conditions unnecessary,
        the first N_SAMPLES values are repeated at the end of the array

        The result is cached so we can repeatedly call this method
        using the configured path without reading it each time.
        """
        if path is None:
            raise ValueError(
                "DRS4 pedestal correction requested"
                " but no file provided for telescope"
            )

        pedestal_data = np.empty(
            (N_GAINS, N_PIXELS_MODULE * N_MODULES, N_CAPACITORS_PIXEL + N_SAMPLES),
            dtype=np.int16
        )
        with fits.open(path) as f:
            pedestal_data[:, :, :N_CAPACITORS_PIXEL] = f[1].data

        pedestal_data[:, :, N_CAPACITORS_PIXEL:] = pedestal_data[:, :, :N_SAMPLES]

        return pedestal_data

    def subtract_pedestal(self, event, tel_id):
        """
        Subtract cell offset using pedestal file.
        Fill the R1 container.
        Parameters
        ----------
        event : `ctapipe` event-container
        tel_id : id of the telescope
        """
        pedestal = self._get_drs4_pedestal_data(
            self.drs4_pedestal_path.tel[tel_id]
        )

        if event.r1.tel[tel_id].selected_gain_channel is None:
            subtract_pedestal(
                event.r1.tel[tel_id].waveform,
                self.first_cap[tel_id],
                pedestal,
            )
        else:
            subtract_pedestal_gain_selected(
                event.r1.tel[tel_id].waveform,
                self.first_cap[tel_id],
                pedestal,
                event.r1.tel[tel_id].selected_gain_channel,
            )


    def time_lapse_corr(self, event, tel_id):
        """
        Perform time lapse baseline corrections.
        Fill the R1 container or modifies R0 container.
        Parameters
        ----------
        event : `ctapipe` event-container
        tel_id : id of the telescope
        """
        lst = event.lst.tel[tel_id]

        # If R1 container exists, update it inplace
        if isinstance(event.r1.tel[tel_id].waveform, np.ndarray):
            container = event.r1.tel[tel_id]
        else:
            # Modify R0 container. This is to create pedestal files.
            container = event.r0.tel[tel_id]

        waveform = container.waveform.copy()

        # We have 2 functions: one for data from 2018/10/10 to 2019/11/04 and
        # one for data from 2019/11/05 (from Run 1574) after update firmware.
        # The old readout (before 2019/11/05) is shifted by 1 cell.
        run_id = event.lst.tel[tel_id].svc.configuration_id

        # not yet gain selected
        if event.r1.tel[tel_id].selected_gain_channel is None:
            apply_timelapse_correction(
                waveform=waveform,
                local_clock_counter=lst.evt.local_clock_counter,
                first_capacitors=self.first_cap[tel_id],
                last_readout_time=self.last_readout_time[tel_id],
                expected_pixels_id=lst.svc.pixel_ids,
                run_id=run_id,
            )
        else:
            apply_timelapse_correction_gain_selected(
                waveform=waveform,
                local_clock_counter=lst.evt.local_clock_counter,
                first_capacitors=self.first_cap[tel_id],
                last_readout_time=self.last_readout_time[tel_id],
                expected_pixels_id=lst.svc.pixel_ids,
                selected_gain_channel=event.r1.tel[tel_id].selected_gain_channel,
                run_id=run_id,
            )

        container.waveform = waveform

    def interpolate_spikes(self, event, tel_id):
        """
        Interpolates spike A & B.
        Fill the R1 container.
        Parameters
        ----------
        event : `ctapipe` event-container
        tel_id : id of the telescope
        """
        run_id = event.lst.tel[tel_id].svc.configuration_id

        r1 = event.r1.tel[tel_id]
        if r1.selected_gain_channel is None:
            interpolate_spikes(
                waveform=r1.waveform,
                first_capacitors=self.first_cap[tel_id],
                previous_first_capacitors=self.first_cap_old[tel_id],
                run_id=run_id,
            )
        else:
            interpolate_spikes_gain_selected(
                waveform=r1.waveform,
                first_capacitors=self.first_cap[tel_id],
                previous_first_capacitors=self.first_cap_old[tel_id],
                selected_gain_channel=r1.selected_gain_channel,
                run_id=run_id,
            )
예제 #18
0
class MuonIntensityFitter(TelescopeComponent):
    spe_width = FloatTelescopeParameter(
        help="Width of a single photo electron distribution", default_value=0.5
    ).tag(config=True)

    min_lambda_m = FloatTelescopeParameter(
        help="Minimum wavelength for Cherenkov light in m", default_value=300e-9,
    ).tag(config=True)

    max_lambda_m = FloatTelescopeParameter(
        help="Minimum wavelength for Cherenkov light in m", default_value=600e-9,
    ).tag(config=True)

    hole_radius_m = FloatTelescopeParameter(
        help="Hole radius of the reflector in m",
        default_value=[
            ("type", "LST_*", 0.308),
        ],
    ).tag(config=True)

    oversampling = IntTelescopeParameter(
        help="Oversampling for the line integration", default_value=3
    ).tag(config=True)

    def __call__(
        self, tel_id, center_x, center_y, radius, image, pedestal, mask
    ):
        """

        Parameters
        ----------
        center_x: Angle quantity
            Initial guess for muon ring center in telescope frame
        center_y: Angle quantity
            Initial guess for muon ring center in telescope frame
        radius: Angle quantity
            Radius of muon ring from circle fitting
        pixel_x: ndarray
            X position of pixels in image from circle fitting
        pixel_y: ndarray
            Y position of pixel in image from circle fitting
        image: ndarray
            Amplitude of image pixels
        pedestal: ndarray
            pedestal RMS
        mask: ndarray
            mask marking pixels to be used in the likelihood fit

        Returns
        -------
        MuonEfficiencyContainer
        """
        telescope = self.subarray.tel[tel_id]
        if telescope.optics.num_mirrors != 1:
            raise NotImplementedError(
                "Currently only single mirror telescopes"
                f" are supported in {self.__class__.__name__}"
            )

        negative_log_likelihood = build_negative_log_likelihood(
            image,
            telescope,
            mask,
            oversampling=self.oversampling.tel[tel_id],
            min_lambda=self.min_lambda_m.tel[tel_id] * u.m,
            max_lambda=self.max_lambda_m.tel[tel_id] * u.m,
            spe_width=self.spe_width.tel[tel_id],
            pedestal=pedestal,
            hole_radius=self.hole_radius_m.tel[tel_id] * u.m,
        )

        initial_guess = create_initial_guess(center_x, center_y, radius, telescope,)

        step_sizes = {}
        step_sizes["error_impact_parameter"] = 0.5
        step_sizes["error_phi"] = np.deg2rad(0.5)
        step_sizes["error_ring_width"] = 0.001 * radius.to_value(u.rad)
        step_sizes["error_optical_efficiency_muon"] = 0.05

        constraints = {}
        constraints["limit_impact_parameter"] = (0, None)
        constraints["limit_phi"] = (-np.pi, np.pi)
        constraints["fix_radius"] = True
        constraints["fix_center_x"] = True
        constraints["fix_center_y"] = True
        constraints["limit_ring_width"] = (0.0, None)
        constraints["limit_optical_efficiency_muon"] = (0.0, None)

        # Create Minuit object with first guesses at parameters
        # strip away the units as Minuit doesnt like them

        minuit = Minuit(
            negative_log_likelihood,
            # forced_parameters=parameter_names,
            **initial_guess,
            **step_sizes,
            **constraints,
            errordef=0.5,
            print_level=0,
            pedantic=True,
        )

        # Perform minimisation
        minuit.migrad()

        # Get fitted values
        result = minuit.values

        return MuonEfficiencyContainer(
            impact=result["impact_parameter"] * u.m,
            impact_x=result["impact_parameter"] * np.cos(result["phi"]) * u.m,
            impact_y=result["impact_parameter"] * np.sin(result["phi"]) * u.m,
            width=u.Quantity(np.rad2deg(result["ring_width"]), u.deg),
            optical_efficiency=result["optical_efficiency_muon"],
        )
예제 #19
0
 class SomeComponent(Component):
     tel_param1 = IntTelescopeParameter(
         default_value=[("type", "*", 10), ("type", "LST*", 100)])
예제 #20
0
파일: reducer.py 프로젝트: watsonjj/ctapipe
class TailCutsDataVolumeReducer(DataVolumeReducer):
    """
    Reduce the time integrated shower image in 3 Steps:

    1) Select pixels with tailcuts_clean.
    2) Add iteratively all pixels with Signal S >= boundary_thresh
       with ctapipe module dilate until no new pixels were added.
    3) Adding new pixels with dilate to get more conservative.

    Attributes
    ----------
    image_extractor_type: String
        Name of the image_extractor to be used.
    n_end_dilates: IntTelescopeParameter
        Number of how many times to dilate at the end.
    do_boundary_dilation: BoolTelescopeParameter
        If set to 'False', the iteration steps in 2) are skipped and
        normal TailcutCleaning is used.
    """

    image_extractor_type = TelescopeParameter(
        trait=create_class_enum_trait(ImageExtractor,
                                      default_value="NeighborPeakWindowSum"),
        default_value="NeighborPeakWindowSum",
        help="Name of the ImageExtractor subclass to be used.",
    ).tag(config=True)

    n_end_dilates = IntTelescopeParameter(
        default_value=1,
        help="Number of how many times to dilate at the end.").tag(config=True)

    do_boundary_dilation = BoolTelescopeParameter(
        default_value=True,
        help="If set to 'False', the iteration steps in 2) are skipped and"
        "normal TailcutCleaning is used.",
    ).tag(config=True)

    def __init__(
        self,
        subarray,
        config=None,
        parent=None,
        cleaner=None,
        image_extractor=None,
        **kwargs,
    ):
        """
        Parameters
        ----------
        subarray: ctapipe.instrument.SubarrayDescription
            Description of the subarray
        config: traitlets.loader.Config
            Configuration specified by config file or cmdline arguments.
            Used to set traitlet values.
            Set to None if no configuration to pass.
        kwargs
        """
        super().__init__(config=config,
                         parent=parent,
                         subarray=subarray,
                         **kwargs)

        if cleaner is None:
            self.cleaner = TailcutsImageCleaner(parent=self,
                                                subarray=self.subarray)
        else:
            self.cleaner = cleaner

        self.image_extractors = {}
        if image_extractor is None:
            for (_, _, name) in self.image_extractor_type:
                self.image_extractors[name] = ImageExtractor.from_name(
                    name, subarray=self.subarray, parent=self)
        else:
            name = image_extractor.__class__.__name__
            self.image_extractor_type = [("type", "*", name)]
            self.image_extractors[name] = image_extractor

    def select_pixels(self, waveforms, telid=None, selected_gain_channel=None):
        camera_geom = self.subarray.tel[telid].camera.geometry
        # Pulse-integrate waveforms
        extractor = self.image_extractors[self.image_extractor_type.tel[telid]]
        charge, _ = extractor(waveforms,
                              telid=telid,
                              selected_gain_channel=selected_gain_channel)

        # 1) Step: TailcutCleaning at first
        mask = self.cleaner(telid, charge)
        pixels_above_boundary_thresh = (
            charge >= self.cleaner.boundary_threshold_pe.tel[telid])
        mask_in_loop = np.array([])
        # 2) Step: Add iteratively all pixels with Signal
        #          S > boundary_thresh with ctapipe module
        #          'dilate' until no new pixels were added.
        while (not np.array_equal(mask, mask_in_loop)
               and self.do_boundary_dilation.tel[telid]):
            mask_in_loop = mask
            mask = dilate(camera_geom, mask) & pixels_above_boundary_thresh

        # 3) Step: Adding Pixels with 'dilate' to get more conservative.
        for _ in range(self.n_end_dilates.tel[telid]):
            mask = dilate(camera_geom, mask)

        return mask
예제 #21
0
class NeighborPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak defined by the wavefroms in neighboring pixels.
    """

    window_width = IntTelescopeParameter(
        default_value=7,
        help="Define the width of the integration window").tag(config=True)

    window_shift = IntTelescopeParameter(
        default_value=3,
        help="Define the shift of the integration window "
        "from the peak_index (peak_index - shift)",
    ).tag(config=True)

    lwt = IntTelescopeParameter(
        default_value=0,
        help="Weight of the local pixel (0: peak from neighbors only, "
        "1: local pixel counts as much as any neighbor)",
    ).tag(config=True)

    apply_integration_correction = BoolTelescopeParameter(
        default_value=True,
        help="Apply the integration window correction").tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        """
        Calculate the correction for the extracted change such that the value
        returned would equal 1 for a noise-less unit pulse.

        This method is decorated with @lru_cache to ensure it is only
        calculated once per telescope.

        Parameters
        ----------
        telid : int

        Returns
        -------
        correction : ndarray
        The correction to apply to an extracted charge using this ImageExtractor
        Has size n_channels, as a different correction value might be required
        for different gain channels.
        """
        readout = self.subarray.tel[telid].camera.readout
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value("ns"),
            (1 / readout.sampling_rate).to_value("ns"),
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )

    def __call__(self, waveforms, telid, selected_gain_channel):
        neighbors = self.subarray.tel[
            telid].camera.geometry.neighbor_matrix_sparse
        average_wfs = neighbor_average_waveform(
            waveforms,
            neighbors_indices=neighbors.indices,
            neighbors_indptr=neighbors.indptr,
            lwt=self.lwt.tel[telid],
        )
        peak_index = average_wfs.argmax(axis=-1)
        charge, peak_time = extract_around_peak(
            waveforms,
            peak_index,
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
            self.sampling_rate[telid],
        )
        if self.apply_integration_correction.tel[telid]:
            charge *= self._calculate_correction(
                telid=telid)[selected_gain_channel]
        return charge, peak_time
예제 #22
0
class GlobalPeakWindowSum(ImageExtractor):
    """
    Extractor which sums in a window about the
    peak from the global average waveform.

    To reduce the influence of noise pixels, the average can be calculated
    only on the ``pixel_fraction`` brightest pixels.
    The "brightest" pixels are determined by sorting the waveforms by their
    maximum value.
    """

    window_width = IntTelescopeParameter(
        default_value=7,
        help="Define the width of the integration window").tag(config=True)

    window_shift = IntTelescopeParameter(
        default_value=3,
        help="Define the shift of the integration window from the peak_index "
        "(peak_index - shift)",
    ).tag(config=True)

    apply_integration_correction = BoolTelescopeParameter(
        default_value=True,
        help="Apply the integration window correction").tag(config=True)

    pixel_fraction = FloatTelescopeParameter(
        default_value=1.0,
        help=
        ("Fraction of pixels to use for finding the integration window."
         " By default, the full camera is used."
         " If fraction is smaller 1, only the brightest pixels will be averaged"
         " to find the peak position"),
    ).tag(config=True)

    @lru_cache(maxsize=128)
    def _calculate_correction(self, telid):
        """
        Calculate the correction for the extracted change such that the value
        returned would equal 1 for a noise-less unit pulse.

        This method is decorated with @lru_cache to ensure it is only
        calculated once per telescope.

        Parameters
        ----------
        telid : int

        Returns
        -------
        correction : ndarray
        The correction to apply to an extracted charge using this ImageExtractor
        Has size n_channels, as a different correction value might be required
        for different gain channels.
        """
        readout = self.subarray.tel[telid].camera.readout
        return integration_correction(
            readout.reference_pulse_shape,
            readout.reference_pulse_sample_width.to_value("ns"),
            (1 / readout.sampling_rate).to_value("ns"),
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
        )

    def __call__(self, waveforms, telid, selected_gain_channel):
        if self.pixel_fraction.tel[telid] == 1.0:
            # average over pixels then argmax over samples
            peak_index = waveforms.mean(axis=-2).argmax()
        else:
            n_pixels = int(self.pixel_fraction.tel[telid] *
                           waveforms.shape[-2])
            brightest = np.argsort(waveforms.max(axis=-1))[..., -n_pixels:]

            # average over brightest pixels then argmax over samples
            peak_index = waveforms[brightest].mean(axis=-2).argmax()

        charge, peak_time = extract_around_peak(
            waveforms,
            peak_index,
            self.window_width.tel[telid],
            self.window_shift.tel[telid],
            self.sampling_rate_ghz[telid],
        )
        if self.apply_integration_correction.tel[telid]:
            charge *= self._calculate_correction(
                telid=telid)[selected_gain_channel]
        return DL1CameraContainer(image=charge,
                                  peak_time=peak_time,
                                  is_valid=True)