class CalibrationCalculator(Component):
    """
    Parent class for the camera calibration calculators.
    Fills the MonitoringCameraContainer on the base of calibration events

    Parameters
    ----------

    flatfield_calculator: lstchain.calib.camera.flatfield
         The flatfield to use. If None, then FlatFieldCalculator
            will be used by default.

    pedestal_calculator: lstchain.calib.camera.pedestal
         The pedestal to use. If None, then
           PedestalCalculator will be used by default.

    kwargs

    """
    squared_excess_noise_factor = Float(
        1.222,
        help='Excess noise factor squared: 1+ Var(gain)/Mean(Gain)**2').tag(
            config=True)

    pedestal_product = traits.create_class_enum_trait(
        PedestalCalculator, default_value='PedestalIntegrator')

    flatfield_product = traits.create_class_enum_trait(
        FlatFieldCalculator, default_value='FlasherFlatFieldCalculator')

    classes = List([FlatFieldCalculator, PedestalCalculator] +
                   traits.classes_with_traits(FlatFieldCalculator) +
                   traits.classes_with_traits(PedestalCalculator))

    def __init__(self, subarray, parent=None, config=None, **kwargs):
        """
        Parent class for the camera calibration calculators.
        Fills the MonitoringCameraContainer on the base of calibration events

        Parameters
        ----------

        flatfield_calculator: lstchain.calib.camera.flatfield
             The flatfield to use. If None, then FlatFieldCalculator
                will be used by default.

        pedestal_calculator: lstchain.calib.camera.pedestal
             The pedestal to use. If None, then
               PedestalCalculator will be used by default.

        """

        super().__init__(parent=parent, config=config, **kwargs)

        self.flatfield = FlatFieldCalculator.from_name(self.flatfield_product,
                                                       parent=self,
                                                       subarray=subarray)
        self.pedestal = PedestalCalculator.from_name(self.pedestal_product,
                                                     parent=self,
                                                     subarray=subarray)

        msg = "tel_id not the same for all calibration components"
        if self.pedestal.tel_id != self.flatfield.tel_id:
            raise ValueError(msg)

        self.tel_id = self.flatfield.tel_id

        self.log.debug(f"{self.pedestal}")
        self.log.debug(f"{self.flatfield}")
Beispiel #2
0
class DataBinning(Component):
    """
    Collects information on generating energy and angular bins for
    generating IRFs as per pyIRF requirements.
    """

    true_energy_min = Float(
        help="Minimum value for True Energy bins in TeV units",
        default_value=0.01,
    ).tag(config=True)

    true_energy_max = Float(
        help="Maximum value for True Energy bins in TeV units",
        default_value=100,
    ).tag(config=True)

    true_energy_n_bins_per_decade = Float(
        help="Number of edges per decade for True Energy bins",
        default_value=5.5,
    ).tag(config=True)

    reco_energy_min = Float(
        help="Minimum value for Reco Energy bins in TeV units",
        default_value=0.01,
    ).tag(config=True)

    reco_energy_max = Float(
        help="Maximum value for Reco Energy bins in TeV units",
        default_value=100,
    ).tag(config=True)

    reco_energy_n_bins_per_decade = Float(
        help="Number of edges per decade for Reco Energy bins",
        default_value=5.5,
    ).tag(config=True)

    energy_migration_min = Float(
        help="Minimum value of Energy Migration matrix",
        default_value=0.2,
    ).tag(config=True)

    energy_migration_max = Float(
        help="Maximum value of Energy Migration matrix",
        default_value=5,
    ).tag(config=True)

    energy_migration_n_bins = Int(
        help="Number of bins in log scale for Energy Migration matrix",
        default_value=31,
    ).tag(config=True)

    fov_offset_min = Float(
        help="Minimum value for FoV Offset bins",
        default_value=0.1,
    ).tag(config=True)

    fov_offset_max = Float(
        help="Maximum value for FoV offset bins",
        default_value=1.1,
    ).tag(config=True)

    fov_offset_n_edges = Int(
        help="Number of edges for FoV offset bins",
        default_value=9,
    ).tag(config=True)

    bkg_fov_offset_min = Float(
        help="Minimum value for FoV offset bins for Background IRF",
        default_value=0,
    ).tag(config=True)

    bkg_fov_offset_max = Float(
        help="Maximum value for FoV offset bins for Background IRF",
        default_value=10,
    ).tag(config=True)

    bkg_fov_offset_n_edges = Int(
        help="Number of edges for FoV offset bins for Background IRF",
        default_value=21,
    ).tag(config=True)

    source_offset_min = Float(
        help="Minimum value for Source offset for PSF IRF",
        default_value=0.0001,
    ).tag(config=True)

    source_offset_max = Float(
        help="Maximum value for Source offset for PSF IRF",
        default_value=1.0001,
    ).tag(config=True)

    source_offset_n_edges = Int(
        help="Number of edges for Source offset for PSF IRF",
        default_value=1000,
    ).tag(config=True)

    def true_energy_bins(self):
        """
        Creates bins per decade for true MC energy using pyirf function.

        The overflow binning added is not needed at the current stage
        It can be used as - add_overflow_bins(***)[1:-1]
        """
        true_energy = create_bins_per_decade(
            self.true_energy_min * u.TeV,
            self.true_energy_max * u.TeV,
            self.true_energy_n_bins_per_decade,
        )
        return true_energy

    def reco_energy_bins(self):
        """
        Creates bins per decade for reconstructed MC energy using pyirf function.

        The overflow binning added is not needed at the current stage
        It can be used as - add_overflow_bins(***)[1:-1]
        """
        reco_energy = create_bins_per_decade(
            self.reco_energy_min * u.TeV,
            self.reco_energy_max * u.TeV,
            self.reco_energy_n_bins_per_decade,
        )
        return reco_energy

    def energy_migration_bins(self):
        """
        Creates bins for energy migration.
        """
        energy_migration = np.geomspace(
            self.energy_migration_min,
            self.energy_migration_max,
            self.energy_migration_n_bins,
        )
        return energy_migration

    def fov_offset_bins(self):
        """
        Creates bins for single/multiple FoV offset
        """
        fov_offset = (
            np.linspace(
                self.fov_offset_min,
                self.fov_offset_max,
                self.fov_offset_n_edges,
            )
            * u.deg
        )
        return fov_offset

    def bkg_fov_offset_bins(self):
        """
        Creates bins for FoV offset for Background IRF,
        Using the same binning as in pyirf example.
        """
        background_offset = (
            np.linspace(
                self.bkg_fov_offset_min,
                self.bkg_fov_offset_max,
                self.bkg_fov_offset_n_edges,
            )
            * u.deg
        )
        return background_offset

    def source_offset_bins(self):
        """
        Creates bins for source offset for generating PSF IRF.
        Using the same binning as in pyirf example.
        """

        source_offset = (
            np.linspace(
                self.source_offset_min,
                self.source_offset_max,
                self.source_offset_n_edges,
            )
            * u.deg
        )
        return source_offset
class SingleTelEventDisplay(Tool):
    name = "ctapipe-display-televents"
    description = Unicode(__doc__)

    infile = Path(help="input file to read", exists=True,
                  directory_ok=False).tag(config=True)
    tel = Int(help="Telescope ID to display", default_value=0).tag(config=True)
    write = Bool(help="Write out images to PNG files",
                 default_value=False).tag(config=True)
    clean = Bool(help="Apply image cleaning",
                 default_value=False).tag(config=True)
    hillas = Bool(help="Apply and display Hillas parametrization",
                  default_value=False).tag(config=True)
    samples = Bool(help="Show each sample",
                   default_value=False).tag(config=True)
    display = Bool(help="Display results in interactive window",
                   default_value=True).tag(config=True)
    delay = Float(help="delay between events in s",
                  default_value=0.01,
                  min=0.001).tag(config=True)
    progress = Bool(help="display progress bar",
                    default_value=True).tag(config=True)

    aliases = Dict({
        "infile": "SingleTelEventDisplay.infile",
        "tel": "SingleTelEventDisplay.tel",
        "max-events": "EventSource.max_events",
        "write": "SingleTelEventDisplay.write",
        "clean": "SingleTelEventDisplay.clean",
        "hillas": "SingleTelEventDisplay.hillas",
        "samples": "SingleTelEventDisplay.samples",
        "display": "SingleTelEventDisplay.display",
        "delay": "SingleTelEventDisplay.delay",
        "progress": "SingleTelEventDisplay.progress",
    })

    classes = List([EventSource, CameraCalibrator])

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def setup(self):
        print("TOLLES INFILE", self.infile)
        self.event_source = EventSource.from_url(self.infile, parent=self)
        self.event_source.allowed_tels = {self.tel}

        self.calibrator = CameraCalibrator(parent=self,
                                           subarray=self.event_source.subarray)
        self.log.info(f"SELECTING EVENTS FROM TELESCOPE {self.tel}")

    def start(self):

        disp = None

        for event in tqdm(
                self.event_source,
                desc=f"Tel{self.tel}",
                total=self.event_source.max_events,
                disable=~self.progress,
        ):

            self.log.debug(event.trigger)
            self.log.debug(f"Energy: {event.simulation.shower.energy}")

            self.calibrator(event)

            if disp is None:
                geom = self.event_source.subarray.tel[self.tel].camera.geometry
                self.log.info(geom)
                disp = CameraDisplay(geom)
                # disp.enable_pixel_picker()
                disp.add_colorbar()
                if self.display:
                    plt.show(block=False)

            # display the event
            disp.axes.set_title("CT{:03d} ({}), event {:06d}".format(
                self.tel, geom.camera_name, event.index.event_id))

            if self.samples:
                # display time-varying event
                data = event.dl0.tel[self.tel].waveform
                for ii in range(data.shape[1]):
                    disp.image = data[:, ii]
                    disp.set_limits_percent(70)
                    plt.suptitle(f"Sample {ii:03d}")
                    if self.display:
                        plt.pause(self.delay)
                    if self.write:
                        plt.savefig(
                            f"CT{self.tel:03d}_EV{event.index.event_id:10d}"
                            f"_S{ii:02d}.png")
            else:
                # display integrated event:
                im = event.dl1.tel[self.tel].image

                if self.clean:
                    mask = tailcuts_clean(geom,
                                          im,
                                          picture_thresh=10,
                                          boundary_thresh=7)
                    im[~mask] = 0.0

                disp.image = im

                if self.hillas:
                    try:
                        ellipses = disp.axes.findobj(Ellipse)
                        if len(ellipses) > 0:
                            ellipses[0].remove()

                        params = hillas_parameters(geom, image=im)
                        disp.overlay_moments(params,
                                             color="pink",
                                             lw=3,
                                             with_label=False)
                    except HillasParameterizationError:
                        pass

                if self.display:
                    plt.pause(self.delay)
                if self.write:
                    plt.savefig(
                        f"CT{self.tel:03d}_EV{event.index.event_id:010d}.png")

        self.log.info("FINISHED READING DATA FILE")

        if disp is None:
            self.log.warning(
                "No events for tel {} were found in {}. Try a "
                "different EventIO file or another telescope".format(
                    self.tel, self.infile))
class SingleTelEventDisplay(Tool):
    name = "ctapipe-display-televents"
    description = Unicode(__doc__)

    infile = Unicode(help="input file to read", default='').tag(config=True)
    tel = Int(help='Telescope ID to display', default=0).tag(config=True)
    channel = Integer(help="channel number to display", min=0,
                      max=1).tag(config=True)
    write = Bool(help="Write out images to PNG files",
                 default=False).tag(config=True)
    clean = Bool(help="Apply image cleaning", default=False).tag(config=True)
    hillas = Bool(help="Apply and display Hillas parametrization",
                  default=False).tag(config=True)
    samples = Bool(help="Show each sample", default=False).tag(config=True)
    display = Bool(help="Display results in interactive window",
                   default_value=True).tag(config=True)
    delay = Float(help='delay between events in s',
                  default_value=0.01,
                  min=0.001).tag(config=True)
    progress = Bool(help='display progress bar',
                    default_value=True).tag(config=True)

    aliases = Dict({
        'infile': 'SingleTelEventDisplay.infile',
        'tel': 'SingleTelEventDisplay.tel',
        'max-events': 'EventSource.max_events',
        'channel': 'SingleTelEventDisplay.channel',
        'write': 'SingleTelEventDisplay.write',
        'clean': 'SingleTelEventDisplay.clean',
        'hillas': 'SingleTelEventDisplay.hillas',
        'samples': 'SingleTelEventDisplay.samples',
        'display': 'SingleTelEventDisplay.display',
        'delay': 'SingleTelEventDisplay.delay',
        'progress': 'SingleTelEventDisplay.progress'
    })

    classes = List([EventSource, CameraCalibrator])

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def setup(self):
        print('TOLLES INFILE', self.infile)
        self.event_source = EventSource.from_url(self.infile, parent=self)
        self.event_source.allowed_tels = {
            self.tel,
        }

        self.calibrator = CameraCalibrator(parent=self)

        self.log.info(f'SELECTING EVENTS FROM TELESCOPE {self.tel}')

    def start(self):

        disp = None

        for event in tqdm(self.event_source,
                          desc=f'Tel{self.tel}',
                          total=self.event_source.max_events,
                          disable=~self.progress):

            self.log.debug(event.trig)
            self.log.debug(f"Energy: {event.mc.energy}")

            self.calibrator(event)

            if disp is None:
                geom = event.inst.subarray.tel[self.tel].camera
                self.log.info(geom)
                disp = CameraDisplay(geom)
                # disp.enable_pixel_picker()
                disp.add_colorbar()
                if self.display:
                    plt.show(block=False)

            # display the event
            disp.axes.set_title('CT{:03d} ({}), event {:06d}'.format(
                self.tel, geom.cam_id, event.r0.event_id))

            if self.samples:
                # display time-varying event
                data = event.dl0.tel[self.tel].waveform[self.channel]
                for ii in range(data.shape[1]):
                    disp.image = data[:, ii]
                    disp.set_limits_percent(70)
                    plt.suptitle(f"Sample {ii:03d}")
                    if self.display:
                        plt.pause(self.delay)
                    if self.write:
                        plt.savefig(
                            f'CT{self.tel:03d}_EV{event.r0.event_id:10d}'
                            f'_S{ii:02d}.png')
            else:
                # display integrated event:
                im = event.dl1.tel[self.tel].image[self.channel]

                if self.clean:
                    mask = tailcuts_clean(geom,
                                          im,
                                          picture_thresh=10,
                                          boundary_thresh=7)
                    im[~mask] = 0.0

                disp.image = im

                if self.hillas:
                    try:
                        ellipses = disp.axes.findobj(Ellipse)
                        if len(ellipses) > 0:
                            ellipses[0].remove()

                        params = hillas_parameters(geom, image=im)
                        disp.overlay_moments(params,
                                             color='pink',
                                             lw=3,
                                             with_label=False)
                    except HillasParameterizationError:
                        pass

                if self.display:
                    plt.pause(self.delay)
                if self.write:
                    plt.savefig(
                        f'CT{self.tel:03d}_EV{event.r0.event_id:010d}.png')

        self.log.info("FINISHED READING DATA FILE")

        if disp is None:
            self.log.warning(
                'No events for tel {} were found in {}. Try a '
                'different EventIO file or another telescope'.format(
                    self.tel, self.infile), )
Beispiel #5
0
class CalibrationCalculator(Component):
    """
    Parent class for the camera calibration calculators.
    Fills the MonitoringCameraContainer on the base of calibration events

    Parameters
    ----------

    flatfield_calculator: lstchain.calib.camera.flatfield
         The flatfield to use. If None, then FlatFieldCalculator
            will be used by default.

    pedestal_calculator: lstchain.calib.camera.pedestal
         The pedestal to use. If None, then
           PedestalCalculator will be used by default.

    kwargs

    """

    systematic_correction_path = Path(
        default_value=None,
        allow_none=True,
        exists=True,
        directory_ok=False,
        help='Path to systematic correction file ',
    ).tag(config=True)

    squared_excess_noise_factor = Float(
        1.222,
        help='Excess noise factor squared: 1+ Var(gain)/Mean(Gain)**2').tag(
            config=True)

    pedestal_product = traits.create_class_enum_trait(
        PedestalCalculator, default_value='PedestalIntegrator')

    flatfield_product = traits.create_class_enum_trait(
        FlatFieldCalculator, default_value='FlasherFlatFieldCalculator')

    classes = ([FlatFieldCalculator, PedestalCalculator] +
               traits.classes_with_traits(FlatFieldCalculator) +
               traits.classes_with_traits(PedestalCalculator))

    def __init__(self, subarray, parent=None, config=None, **kwargs):
        """
        Parent class for the camera calibration calculators.
        Fills the MonitoringCameraContainer on the base of calibration events

        Parameters
        ----------

        flatfield_calculator: lstchain.calib.camera.flatfield
             The flatfield to use. If None, then FlatFieldCalculator
                will be used by default.

        pedestal_calculator: lstchain.calib.camera.pedestal
             The pedestal to use. If None, then
               PedestalCalculator will be used by default.

        """

        super().__init__(parent=parent, config=config, **kwargs)

        if self.squared_excess_noise_factor <= 0:
            msg = "Argument squared_excess_noise_factor must have a positive value"
            raise ValueError(msg)

        self.flatfield = FlatFieldCalculator.from_name(self.flatfield_product,
                                                       parent=self,
                                                       subarray=subarray)
        self.pedestal = PedestalCalculator.from_name(self.pedestal_product,
                                                     parent=self,
                                                     subarray=subarray)

        msg = "tel_id not the same for all calibration components"
        if self.pedestal.tel_id != self.flatfield.tel_id:
            raise ValueError(msg)

        self.tel_id = self.flatfield.tel_id

        # load systematic correction term B
        self.quadratic_term = 0
        if self.systematic_correction_path is not None:
            try:
                with h5py.File(self.systematic_correction_path, 'r') as hf:
                    self.quadratic_term = np.array(hf['B_term'])

            except:
                raise IOError(
                    f"Problem in reading quadratic term file {self.systematic_correction_path}"
                )
        self.log.debug(f"{self.pedestal}")
        self.log.debug(f"{self.flatfield}")
Beispiel #6
0
 class SomeComponent(TelescopeComponent):
     path = TelescopeParameter(Path(), default_value=None).tag(config=True)
     val = TelescopeParameter(Float(), default_value=1.0).tag(config=True)
class TimeCorrectionCalculate(Component):
    """
        The TimeCorrectionCalculate class to create h5py
        file with coefficients for time correction curve
        of chip DRS4.
        Description of this method: "Analysis techniques
        and performance of the Domino Ring Sampler version 4
        based readout for the MAGIC telescopes [arxiv:1305.1007]
    """

    minimum_charge = Float(
        200, help='Cut on charge. Default 200 ADC').tag(config=True)

    tel_id = Int(1, help='Id of the telescope to calibrate').tag(config=True)

    n_combine = Int(
        8, help='How many capacitors are combines in a single bin. Default 8'
    ).tag(config=True)

    n_harmonics = Int(
        16, help='Number of harmonic for Fourier series expansion. Default 16'
    ).tag(config=True)

    n_capacitors = Int(
        1024, help='Number of capacitors (1024 or 4096). Default 1024.').tag(
            config=True)

    charge_product = Unicode(
        'LocalPeakWindowSum',
        help='Name of the charge extractor to be used').tag(config=True)

    calib_file_path = Unicode(
        '', allow_none=True,
        help='Path to the time calibration file').tag(config=True)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.n_bins = int(self.n_capacitors / self.n_combine)

        self.mean_values_per_bin = np.zeros((n_gain, n_pixels, self.n_bins))
        self.entries_per_bin = np.zeros((n_gain, n_pixels, self.n_bins))

        self.first_cap_array = np.zeros((n_modules, n_gain, n_channel))

        # load the waveform charge extractor
        self.extractor = ImageExtractor.from_name(self.charge_product,
                                                  config=self.config)

        self.log.info(f"extractor {self.extractor}")
        self.sum_events = 0

    def calibrate_pulse_time(self, event):
        """
        Fill bins using time pulse from LocalPeakWindowSum.
        Parameters
        ----------
        event : `ctapipe` event-container
        """
        if event.r1.tel[self.tel_id].trigger_type == 1:
            for nr_module in prange(0, n_modules):
                self.first_cap_array[
                    nr_module, :, :] = self.get_first_capacitor(
                        event, nr_module)

            pixel_ids = event.lst.tel[self.tel_id].svc.pixel_ids
            charge, pulse_time = self.extractor(
                event.r1.tel[self.tel_id].waveform)
            self.calib_pulse_time_jit(charge,
                                      pulse_time,
                                      pixel_ids,
                                      self.first_cap_array,
                                      self.mean_values_per_bin,
                                      self.entries_per_bin,
                                      n_cap=self.n_capacitors,
                                      n_combine=self.n_combine,
                                      min_charge=self.minimum_charge)
            self.sum_events += 1

    @jit(parallel=True)
    def calib_pulse_time_jit(self, charge, pulse_time, pixel_ids,
                             first_cap_array, mean_values_per_bin,
                             entries_per_bin, n_cap, n_combine, min_charge):
        """
        Numba function for calibration pulse time.

        Parameters
        ----------
        pulse : ndarray
            Pulse time stored in a numpy array of shape
            (n_gain, n_pixels).
        charge : ndarray
            Charge in each pixel.
            (n_gain, n_pixels).
        pixel_ids: ndarray
            Array stored expected pixel id
            (n_pixels).
        first_cap_array : ndarray
            Value of first capacitor stored in a numpy array of shape
            (n_clus, n_gain, n_pix).
        mean_values_per_bin : ndarray
            Array to fill using pulse time
            stored in a numpy array of shape
            (n_gain, n_pixels, n_bins).
        entries_per_bin : ndarray
            Array to store number of entries per bin
            stored in a numpy array of shape
            (n_gain, n_pixels, n_bins).
        n_cap : int
            Number of capacitors
        n_combine : int
            Number of combine capacitors in a single bin

        """

        for nr_module in prange(0, n_modules):
            for gain in prange(0, n_gain):
                for pix in prange(0, n_channel):
                    pixel = pixel_ids[nr_module * 7 + pix]
                    if charge[gain, pixel] > min_charge:  # cut change
                        fc = first_cap_array[nr_module, :, :]
                        first_cap = (fc[gain, pix]) % n_cap
                        bin = int(first_cap / n_combine)
                        mean_values_per_bin[gain, pixel,
                                            bin] += pulse_time[gain, pixel]
                        entries_per_bin[gain, pixel, bin] += 1

    def finalize(self):
        if np.sum(self.entries_per_bin == 0) > 0:
            raise RuntimeError(
                "Not enough events to coverage all capacitor. "
                "Please use more events to time calibration file.")
        else:
            self.mean_values_per_bin = self.mean_values_per_bin / self.entries_per_bin
            self.save_to_hdf5_file()

    def fit(self, pixel_id, gain):
        """
            Fit data bins using Fourier series expansion
            Parameters
            ----------
            pixel_id : ndarray
            Array stored expected pixel id of shape
            (n_pixels).
            gain: int
            0 for high gain, 1 for low gain
        """
        self.pos = np.zeros(self.n_bins)
        for i in range(0, self.n_bins):
            self.pos[i] = (i + 0.5) * self.n_combine

        self.fan = np.zeros(self.n_harmonics)  # cos coeff
        self.fbn = np.zeros(self.n_harmonics)  # sin coeff

        for n in range(0, self.n_harmonics):
            self.integrate_with_trig(self.pos,
                                     self.mean_values_per_bin[gain, pixel_id],
                                     n, self.fan, self.fbn)

    def integrate_with_trig(self, x, y, n, an, bn):
        """
            Function to expanding into Fourier series
            Parameters
            ----------
            x : ndarray
            Array stored position in DRS4 ring of shape
            (n_bins).
            y: ndarray
            Array stored mean pulse time per bin of shape
            (n_bins)
            n : int
            n harmonic
            an: ndarray
            Array to fill with cos coeff of shape
            (n_harmonics)
            bn: ndarray
            Array to fill with sin coeff of shape
            (n_harmonics)
        """
        suma = 0
        sumb = 0

        for i in range(0, self.n_bins):
            suma += y[i] * self.n_combine * np.cos(
                2 * np.pi * n * (x[i] / float(self.n_capacitors)))
            sumb += y[i] * self.n_combine * np.sin(
                2 * np.pi * n * (x[i] / float(self.n_capacitors)))

        an[n] = suma * (2. / (self.n_bins * self.n_combine))
        bn[n] = sumb * (2. / (self.n_bins * self.n_combine))

    def get_first_capacitor(self, event, nr):
        fc = np.zeros((n_gain, n_channel))
        first_cap = event.lst.tel[
            self.tel_id].evt.first_capacitor_id[nr * 8:(nr + 1) * 8]
        # First capacitor order according Dragon v5 board data format
        for i, j in zip([0, 1, 2, 3, 4, 5, 6], [0, 0, 1, 1, 2, 2, 3]):
            fc[high_gain, i] = first_cap[j]
        for i, j in zip([0, 1, 2, 3, 4, 5, 6], [4, 4, 5, 5, 6, 6, 7]):
            fc[low_gain, i] = first_cap[j]
        return fc

    def save_to_hdf5_file(self):
        """
            Function to save Fourier series expansion coeff into hdf5 file
        """
        fan_array = np.zeros((n_gain, n_pixels, self.n_harmonics))
        fbn_array = np.zeros((n_gain, n_pixels, self.n_harmonics))
        for pix_id in range(0, n_pixels):
            self.fit(pix_id, gain=high_gain)
            fan_array[high_gain, pix_id, :] = self.fan
            fbn_array[high_gain, pix_id, :] = self.fbn

            self.fit(pix_id, gain=low_gain)
            fan_array[low_gain, pix_id, :] = self.fan
            fbn_array[low_gain, pix_id, :] = self.fbn

        try:
            hf = h5py.File(self.calib_file_path, 'w')
            hf.create_dataset('fan', data=fan_array)
            hf.create_dataset('fbn', data=fbn_array)
            hf.attrs['n_events'] = self.sum_events
            hf.attrs['n_harm'] = self.n_harmonics

        except Exception as err:
            print("FAILED!", err)
        hf.close()
Beispiel #8
0
class DL3Cuts(Component):
    """
    Selection cuts for DL2 to DL3 conversion
    """

    global_gh_cut = Float(
        help="Global selection cut for gh_score (gammaness)",
        default_value=0.6,
    ).tag(config=True)

    gh_efficiency = Float(
        help="Gamma efficiency for optimized g/h cuts in %",
        default_value=0.95,
    ).tag(config=True)

    theta_containment = Float(
        help="Percentage containment region for theta cuts",
        default_value=0.68,
    ).tag(config=True)

    global_theta_cut = Float(
        help="Global selection cut for theta",
        default_value=0.2,
    ).tag(config=True)

    global_alpha_cut = Float(
        help="Global selection cut for alpha",
        default_value=20,
    ).tag(config=True)

    allowed_tels = List(
        help="List of allowed LST telescope ids",
        trait=Int(),
        default_value=[1],
    ).tag(config=True)

    def apply_global_gh_cut(self, data):
        """
        Applying a global gammaness cut on a given data
        """
        return data[data["gh_score"] > self.global_gh_cut]

    def energy_dependent_gh_cuts(self,
                                 data,
                                 energy_bins,
                                 min_value=0.1,
                                 max_value=0.99,
                                 smoothing=None,
                                 min_events=10):
        """
        Evaluating energy-dependent gammaness cuts, in a given
        data, with provided reco energy bins, and other parameters to
        pass to the pyirf.cuts.calculate_percentile_cut function
        """

        gh_cuts = calculate_percentile_cut(
            data["gh_score"],
            data["reco_energy"],
            bins=energy_bins,
            min_value=min_value,
            max_value=max_value,
            fill_value=data["gh_score"].max(),
            percentile=100 * (1 - self.gh_efficiency),
            smoothing=smoothing,
            min_events=min_events,
        )
        return gh_cuts

    def apply_global_alpha_cut(self, data):
        """
        Applying a global alpha cut on a given data
        """
        return data[data["alpha"].to_value(u.deg) < self.global_alpha_cut]

    def apply_energy_dependent_gh_cuts(self, data, gh_cuts):
        """
        Applying a given energy-dependent gh cuts to a data file, along the reco
        energy bins provided.
        """

        data["selected_gh"] = evaluate_binned_cut(
            data["gh_score"],
            data["reco_energy"],
            gh_cuts,
            operator.ge,
        )
        return data[data["selected_gh"]]

    def apply_global_theta_cut(self, data):
        """
        Applying a global theta cut on a given data
        """
        return data[data["theta"].to_value(u.deg) < self.global_theta_cut]

    def energy_dependent_theta_cuts(self,
                                    data,
                                    energy_bins,
                                    min_value=0.05 * u.deg,
                                    fill_value=0.32 * u.deg,
                                    max_value=0.32 * u.deg,
                                    smoothing=None,
                                    min_events=10):
        """
        Evaluating an optimized energy-dependent theta cuts, in a given
        data, with provided reco energy bins, and other parameters to
        pass to the pyirf.cuts.calculate_percentile_cut function.

        Note: Using too fine binning will result in too un-smooth cuts.
        """

        theta_cuts = calculate_percentile_cut(
            data["theta"],
            data["reco_energy"],
            bins=energy_bins,
            min_value=min_value,
            max_value=max_value,
            fill_value=fill_value,
            percentile=100 * self.theta_containment,
            smoothing=smoothing,
            min_events=min_events,
        )
        return theta_cuts

    def apply_energy_dependent_theta_cuts(self, data, theta_cuts):
        """
        Applying a given energy-dependent theta cuts to a data file, along the
        reco energy bins provided.
        """

        data["selected_theta"] = evaluate_binned_cut(
            data["theta"],
            data["reco_energy"],
            theta_cuts,
            operator.le,
        )
        return data[data["selected_theta"]]

    def allowed_tels_filter(self, data):
        """
        Applying a filter on telescopes used for observation.
        """
        mask = np.zeros(len(data), dtype=bool)
        for tel_id in self.allowed_tels:
            mask |= data["tel_id"] == tel_id
        return data[mask]
Beispiel #9
0
 class SomeComponent(TelescopeComponent):
     tel_param = TelescopeParameter(Float(default_value=0.0, allow_none=True))
     tel_param_int = IntTelescopeParameter()
Beispiel #10
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,
            )
Beispiel #11
0
class CameraDL1Calibrator(Component):
    """
    The calibrator for DL1 charge extraction. Fills the dl1 container.

    It handles the integration correction and, if required, the list of
    neighbours.

    Parameters
    ----------
    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.
    tool : ctapipe.core.Tool or None
        Tool executable that is calling this component.
        Passes the correct logger to the component.
        Set to None if no Tool to pass.
    extractor : ctapipe.calib.camera.charge_extractors.ChargeExtractor
        The extractor to use to extract the charge from the waveforms.
        By default the NeighbourPeakIntegrator with default configuration
        is used.
    cleaner : ctapipe.calib.camera.waveform_cleaners.Cleaner
        The waveform cleaner to use. By default no cleaning is
        applied to the waveforms.
    kwargs
    """

    name = 'CameraCalibrator'
    radius = Float(None,
                   allow_none=True,
                   help='Pixels within radius from a pixel are considered '
                   'neighbours to the pixel. Set to None for the default '
                   '(1.4 * min_pixel_seperation).').tag(config=True)
    clip_amplitude = Float(None,
                           allow_none=True,
                           help='Amplitude in p.e. above which the signal is '
                           'clipped. Set to None for no '
                           'clipping.').tag(config=True)

    def __init__(self, config, tool, extractor=None, cleaner=None, **kwargs):
        super().__init__(config=config, parent=tool, **kwargs)
        self.extractor = extractor
        if self.extractor is None:
            self.extractor = NeighbourPeakIntegrator(config, tool)
        self.cleaner = cleaner
        if self.cleaner is None:
            self.cleaner = NullWaveformCleaner(config, tool)
        self._dl0_empty_warn = False

    def check_dl0_exists(self, event, telid):
        """
        Check that dl0 data exists. If it does not, then do not change dl1.

        This ensures that if the containers were filled from a file containing
        dl1 data, it is not overwritten by non-existant data.

        Parameters
        ----------
        event : container
            A `ctapipe` event container
        telid : int
            The telescope id.

        Returns
        -------
        bool
            True if dl0.tel[telid].pe_samples is not None, else false.
        """
        dl0 = event.dl0.tel[telid].pe_samples
        if dl0 is not None:
            return True
        else:
            if not self._dl0_empty_warn:
                self.log.warning("Encountered an event with no DL0 data. "
                                 "DL1 is unchanged in this circumstance.")
                self._dl0_empty_warn = True
            return False

    @staticmethod
    def get_geometry(event, telid):
        """
        Obtain the geometry for this telescope.

        Parameters
        ----------
        event : container
            A `ctapipe` event container
        telid : int
            The telescope id.
            The neighbours are calculated once per telescope.

        Returns
        -------
        `CameraGeometry`
        """
        return CameraGeometry.guess(*event.inst.pixel_pos[telid],
                                    event.inst.optical_foclen[telid])

    def get_correction(self, event, telid):
        """
        Obtain the integration correction for this telescope.

        Parameters
        ----------
        event : container
            A `ctapipe` event container
        telid : int
            The telescope id.
            The integration correction is calculated once per telescope.

        Returns
        -------
        ndarray
        """
        try:
            shift = self.extractor.window_shift
            width = self.extractor.window_width
            n_chan = event.inst.num_channels[telid]
            shape = event.mc.tel[telid].reference_pulse_shape
            step = event.mc.tel[telid].meta['refstep']
            time_slice = event.mc.tel[telid].time_slice
            correction = integration_correction(n_chan, shape, step,
                                                time_slice, width, shift)
            return correction
        except (AttributeError, KeyError):
            # Don't apply correction when window_shift or window_width
            # does not exist in extractor, or when container does not have
            # a reference pulse shape
            return np.ones(event.inst.num_channels[telid])

    def calibrate(self, event):
        """
        Fill the dl1 container with the calibration data that results from the
        configuration of this calibrator.

        Parameters
        ----------
        event : container
            A `ctapipe` event container
        """
        for telid in event.dl0.tels_with_data:

            if self.check_dl0_exists(event, telid):
                waveforms = event.dl0.tel[telid].pe_samples
                n_samples = waveforms.shape[2]
                if n_samples == 1:
                    # To handle ASTRI and dst
                    corrected = waveforms[..., 0]
                    window = np.ones(waveforms.shape)
                    peakpos = np.zeros(waveforms.shape[0:2])
                    cleaned = waveforms
                else:
                    # Clean waveforms
                    cleaned = self.cleaner.apply(waveforms)

                    # Extract charge
                    if self.extractor.requires_neighbours():
                        e = self.extractor
                        g = self.get_geometry(event, telid)
                        e.neighbours = g.neighbor_matrix_where
                    extract = self.extractor.extract_charge
                    charge, peakpos, window = extract(cleaned)

                    # Apply integration correction
                    correction = self.get_correction(event, telid)[:, None]
                    corrected = charge * correction

                # Clip amplitude
                if self.clip_amplitude:
                    corrected[corrected > self.clip_amplitude] = \
                        self.clip_amplitude

                # Store into event container
                event.dl1.tel[telid].image = corrected
                event.dl1.tel[telid].extracted_samples = window
                event.dl1.tel[telid].peakpos = peakpos
                event.dl1.tel[telid].cleaned = cleaned
class LSTCalibrationCalculator(CalibrationCalculator):
    """
    Calibration calculator for LST camera
    Fills the MonitoringCameraContainer on the base of calibration events

    Parameters:
    ----------
    minimum_hg_charge_median :
              Temporary cut on HG charge till the calibox TIB do not work
             (default for filter 5.2)

    maximum_lg_charge_std
             Temporary cut on LG std against Lidar events till the calibox TIB do not work
            (default for filter 5.2)

    time_calibration_path:
            Path with the drs4 time calibration corrections
    """

    minimum_hg_charge_median = Float(
        5000,
        help=
        'Temporary cut on HG charge till the calibox TIB do not work (default for filter 5.2)'
    ).tag(config=True)

    maximum_lg_charge_std = Float(
        300,
        help=
        'Temporary cut on LG std against Lidar events till the calibox TIB do not work (default for filter 5.2) '
    ).tag(config=True)

    def calculate_calibration_coefficients(self, event):
        """
        Calculate calibration coefficients from flatfield and pedestal statistics
        associated to the present event

        Parameters
        ----------
        event: EventAndMonDataContainer

        """

        ped_data = event.mon.tel[self.tel_id].pedestal
        ff_data = event.mon.tel[self.tel_id].flatfield
        status_data = event.mon.tel[self.tel_id].pixel_status
        calib_data = event.mon.tel[self.tel_id].calibration

        # mask from pedestal and flat-field data
        monitoring_unusable_pixels = np.logical_or(
            status_data.pedestal_failing_pixels,
            status_data.flatfield_failing_pixels)
        # calibration unusable pixels are an OR of all masks
        calib_data.unusable_pixels = np.logical_or(
            monitoring_unusable_pixels, status_data.hardware_failing_pixels)

        # Extract calibration coefficients with F-factor method
        # Assume fixed excess noise factor must be known from elsewhere

        # calculate photon-electrons
        numerator = self.squared_excess_noise_factor * (
            ff_data.charge_median - ped_data.charge_median)**2
        denominator = ff_data.charge_std**2 - ped_data.charge_std**2
        n_pe = np.divide(numerator,
                         denominator,
                         out=np.zeros_like(numerator),
                         where=denominator != 0)

        # fill WaveformCalibrationContainer
        calib_data.time = ff_data.sample_time
        calib_data.time_range = ff_data.sample_time_range
        calib_data.n_pe = n_pe

        # find signal median of good pixels
        masked_npe = np.ma.array(n_pe, mask=calib_data.unusable_pixels)
        npe_signal_median = np.ma.median(masked_npe, axis=1)

        # Flat field factor
        numerator = npe_signal_median[:, np.newaxis]
        denominator = n_pe
        ff = np.divide(numerator,
                       denominator,
                       out=np.zeros_like(denominator),
                       where=denominator != 0)

        # calibration coefficients
        numerator = n_pe * ff
        denominator = (ff_data.charge_median - ped_data.charge_median)
        calib_data.dc_to_pe = np.divide(numerator,
                                        denominator,
                                        out=np.zeros_like(numerator),
                                        where=denominator != 0)

        # put the time around zero
        camera_time_median = np.median(ff_data.time_median, axis=1)
        calib_data.time_correction = -ff_data.relative_time_median - camera_time_median[:,
                                                                                        np
                                                                                        .
                                                                                        newaxis]

        calib_data.pedestal_per_sample = ped_data.charge_median / self.pedestal.extractor.window_width

        # put to zero unusable pixels
        calib_data.dc_to_pe[calib_data.unusable_pixels] = 0
        calib_data.pedestal_per_sample[calib_data.unusable_pixels] = 0

        # eliminate inf values id any (still necessary?)
        calib_data.dc_to_pe[np.isinf(calib_data.dc_to_pe)] = 0

    def process_interleaved(self, event):
        """
        Process interleaved calibration events (pedestals and FF)
        Parameters
        ----------
        """
        new_ped = False
        new_ff = False

        # if pedestal event
        if LSTEventType.is_pedestal(event.r1.tel[self.tel_id].trigger_type):

            new_ped = self.pedestal.calculate_pedestals(event)

        # if flat-field event: no calibration  TIB for the moment,
        # use a cut on the charge for ff events and on std for rejecting Magic Lidar events
        elif LSTEventType.is_calibration(
                event.r1.tel[self.tel_id].trigger_type
        ) or (np.median(np.sum(event.r1.tel[self.tel_id].waveform[0],
                               axis=1)) > self.minimum_hg_charge_median
              and np.std(np.sum(event.r1.tel[self.tel_id].waveform[1],
                                axis=1)) < self.maximum_lg_charge_std):

            new_ff = self.flatfield.calculate_relative_gain(event)

            # if new ff, calculate new calibration coefficients
            if new_ff:
                self.calculate_calibration_coefficients(event)

        return new_ped, new_ff

    def output_interleaved_results(self, event):
        """
        Output interleaved results on request

        """
        new_ped = False
        new_ff = False

        # store results
        if self.pedestal.num_events_seen > 0:
            self.pedestal.store_results(event)
            new_ped = True

            if self.flatfield.num_events_seen > 0:
                self.flatfield.store_results(event)

                # calculates calibration values
                self.calculate_calibration_coefficients(event)
                new_ff = True

        return new_ped, new_ff
Beispiel #13
0
class LSTEventSource(EventSource):
    """
    EventSource for LST R0 data.
    """
    multi_streams = Bool(True,
                         help='Read in parallel all streams ').tag(config=True)

    min_flatfield_adc = Float(
        default_value=3000.0,
        help=
        ('Events with that have more than ``min_flatfield_pixel_fraction``'
         ' of the pixels inside [``min_flatfield_adc``, ``max_flatfield_adc``]'
         ' get tagged as EventType.FLATFIELD'),
    ).tag(config=True)

    max_flatfield_adc = Float(
        default_value=12000.0,
        help=
        ('Events with that have more than ``min_flatfield_pixel_fraction``'
         ' of the pixels inside [``min_flatfield_adc``, ``max_flatfield_adc``]'
         ' get tagged as EventType.FLATFIELD'),
    ).tag(config=True)

    min_flatfield_pixel_fraction = Float(
        default_value=0.8,
        help=(
            'Events with that have more than ``min_flatfield_pixel_fraction``'
            ' of the pixels inside [``min_flatfield_pe``, ``max_flatfield_pe``]'
            ' get tagged as EventType.FLATFIELD'),
    ).tag(config=True)

    default_trigger_type = Enum(
        ['ucts', 'tib'],
        default_value='ucts',
        help=
        ('Default source for trigger type information.'
         ' For older data, tib might be the better choice but for data newer'
         ' than 2020-06-25, ucts is the preferred option. The source will still'
         ' fallback to the other device if the chosen default device is not '
         ' available')).tag(config=True)

    use_flatfield_heuristic = Bool(
        default_value=True,
        help=('If true, try to identify flat field events independent of the'
              ' trigger type in the event. See option ``min_flatfield_adc``'),
    ).tag(config=True)

    calibrate_flatfields_and_pedestals = Bool(
        default_value=True,
        help='If True, flat field and pedestal events are also calibrated.'
    ).tag(config=True)

    apply_drs4_corrections = Bool(
        default_value=True,
        help=(
            'Apply DRS4 corrections.'
            ' If True, this will fill R1 waveforms with the corrections applied'
            ' Use the options for the LSTR0Corrections to configure which'
            ' corrections are applied'),
    ).tag(config=True)

    trigger_information = Bool(
        default_value=True, help='Fill trigger information.').tag(config=True)
    pointing_information = Bool(
        default_value=True,
        help=('Fill pointing information.'
              ' Requires specifying `PointingSource.drive_report_path`'),
    ).tag(config=True)

    classes = [PointingSource, EventTimeCalculator, LSTR0Corrections]

    def __init__(self, input_url=None, **kwargs):
        '''
        Create a new LSTEventSource.

        Parameters
        ----------
        input_url: Path
            Path to or url understood by ``ctapipe.core.traits.Path``.
            If ``multi_streams`` is ``True``, the source will try to read all
            streams matching the given ``input_url``
        **kwargs:
            Any of the traitlets. See ``LSTEventSource.class_print_help``
        '''
        super().__init__(input_url=input_url, **kwargs)

        if self.multi_streams:
            # test how many streams are there:
            # file name must be [stream name]Run[all the rest]
            # All the files with the same [all the rest] are opened

            path, name = os.path.split(os.path.abspath(self.input_url))
            if 'Run' in name:
                _, run = name.split('Run', 1)
            else:
                run = name

            ls = listdir(path)
            self.file_list = []

            for file_name in ls:
                if run in file_name:
                    full_name = os.path.join(path, file_name)
                    self.file_list.append(full_name)

        else:
            self.file_list = [self.input_url]

        self.multi_file = MultiFiles(self.file_list)
        self.geometry_version = 4

        self.camera_config = self.multi_file.camera_config
        self.log.info("Read {} input files".format(
            self.multi_file.num_inputs()))
        self.tel_id = self.camera_config.telescope_id
        self._subarray = self.create_subarray(self.geometry_version,
                                              self.tel_id)
        self.r0_r1_calibrator = LSTR0Corrections(subarray=self._subarray,
                                                 parent=self)
        self.time_calculator = EventTimeCalculator(
            subarray=self.subarray,
            run_id=self.camera_config.configuration_id,
            expected_modules_id=self.camera_config.lstcam.expected_modules_id,
            parent=self,
        )
        self.pointing_source = PointingSource(subarray=self.subarray,
                                              parent=self)
        self.lst_service = self.fill_lst_service_container(
            self.tel_id, self.camera_config)

    @property
    def subarray(self):
        return self._subarray

    @property
    def is_simulation(self):
        return False

    @property
    def obs_ids(self):
        # currently no obs id is available from the input files
        return [
            self.camera_config.configuration_id,
        ]

    @property
    def datalevels(self):
        if self.r0_r1_calibrator.calibration_path is not None:
            return (DataLevel.R0, DataLevel.R1)
        return (DataLevel.R0, )

    def rewind(self):
        self.multi_file.rewind()

    @staticmethod
    def create_subarray(geometry_version, tel_id=1):
        """
        Obtain the subarray from the EventSource
        Returns
        -------
        ctapipe.instrument.SubarrayDescription
        """

        # camera info from LSTCam-[geometry_version].camgeom.fits.gz file
        camera_geom = load_camera_geometry(version=geometry_version)

        # get info on the camera readout:
        daq_time_per_sample, pulse_shape_time_step, pulse_shapes = read_pulse_shapes(
        )

        camera_readout = CameraReadout(
            'LSTCam',
            1 / daq_time_per_sample,
            pulse_shapes,
            pulse_shape_time_step,
        )

        camera = CameraDescription('LSTCam', camera_geom, camera_readout)

        lst_tel_descr = TelescopeDescription(name='LST',
                                             tel_type='LST',
                                             optics=OPTICS,
                                             camera=camera)

        tel_descriptions = {tel_id: lst_tel_descr}

        # LSTs telescope position taken from MC from the moment
        tel_positions = {tel_id: [50., 50., 16] * u.m}

        subarray = SubarrayDescription(
            name=f"LST-{tel_id} subarray",
            tel_descriptions=tel_descriptions,
            tel_positions=tel_positions,
        )

        return subarray

    def _generator(self):

        # container for LST data
        array_event = LSTArrayEventContainer()
        array_event.meta['input_url'] = self.input_url
        array_event.meta['max_events'] = self.max_events
        array_event.meta['origin'] = 'LSTCAM'

        # also add service container to the event section
        array_event.lst.tel[self.tel_id].svc = self.lst_service

        # initialize general monitoring container
        self.initialize_mon_container(array_event)

        # loop on events
        for count, zfits_event in enumerate(self.multi_file):
            array_event.count = count
            array_event.index.event_id = zfits_event.event_id
            array_event.index.obs_id = self.obs_ids[0]

            # Skip "empty" events that occur at the end of some runs
            if zfits_event.event_id == 0:
                self.log.warning('Event with event_id=0 found, skipping')
                continue

            self.fill_r0r1_container(array_event, zfits_event)
            self.fill_lst_event_container(array_event, zfits_event)
            if self.trigger_information:
                self.fill_trigger_info(array_event)

            self.fill_mon_container(array_event, zfits_event)

            if self.pointing_information:
                self.fill_pointing_info(array_event)

            # apply low level corrections
            if self.apply_drs4_corrections:
                self.r0_r1_calibrator.apply_drs4_corrections(array_event)

                # flat field tagging is performed on r1 data, so can only
                # be done after the drs4 corrections are applied
                if self.use_flatfield_heuristic:
                    self.tag_flatfield_events(array_event)

            # gain select and calibrate to pe
            if self.r0_r1_calibrator.calibration_path is not None:
                # skip flatfield and pedestal events if asked
                if (self.calibrate_flatfields_and_pedestals
                        or array_event.trigger.event_type
                        not in {EventType.FLATFIELD, EventType.SKY_PEDESTAL}):
                    self.r0_r1_calibrator.calibrate(array_event)

            yield array_event

    @staticmethod
    def is_compatible(file_path):
        from astropy.io import fits
        try:
            # The file contains two tables:
            #  1: CameraConfig
            #  2: Events
            h = fits.open(file_path)[2].header
            ttypes = [h[x] for x in h.keys() if 'TTYPE' in x]
        except OSError:
            # not even a fits file
            return False

        except IndexError:
            # A fits file of a different format
            return False

        is_protobuf_zfits_file = ((h['XTENSION'] == 'BINTABLE')
                                  and (h['EXTNAME'] == 'Events')
                                  and (h['ZTABLE'] is True)
                                  and (h['ORIGIN'] == 'CTA')
                                  and (h['PBFHEAD'] == 'R1.CameraEvent'))

        is_lst_file = 'lstcam_counters' in ttypes
        return is_protobuf_zfits_file & is_lst_file

    @staticmethod
    def fill_lst_service_container(tel_id, camera_config):
        """
        Fill LSTServiceContainer with specific LST service data data
        (from the CameraConfig table of zfit file)

        """
        return LSTServiceContainer(
            telescope_id=tel_id,
            cs_serial=camera_config.cs_serial,
            configuration_id=camera_config.configuration_id,
            date=camera_config.date,
            num_pixels=camera_config.num_pixels,
            num_samples=camera_config.num_samples,
            pixel_ids=camera_config.expected_pixels_id,
            data_model_version=camera_config.data_model_version,
            num_modules=camera_config.lstcam.num_modules,
            module_ids=camera_config.lstcam.expected_modules_id,
            idaq_version=camera_config.lstcam.idaq_version,
            cdhs_version=camera_config.lstcam.cdhs_version,
            algorithms=camera_config.lstcam.algorithms,
            pre_proc_algorithms=camera_config.lstcam.pre_proc_algorithms,
        )

    def fill_lst_event_container(self, array_event, zfits_event):
        """
        Fill LSTEventContainer with specific LST service data
        (from the Event table of zfit file)

        """
        tel_id = self.tel_id

        lst_evt = array_event.lst.tel[tel_id].evt

        lst_evt.configuration_id = zfits_event.configuration_id
        lst_evt.event_id = zfits_event.event_id
        lst_evt.tel_event_id = zfits_event.tel_event_id
        lst_evt.pixel_status = zfits_event.pixel_status
        lst_evt.ped_id = zfits_event.ped_id
        lst_evt.module_status = zfits_event.lstcam.module_status
        lst_evt.extdevices_presence = zfits_event.lstcam.extdevices_presence

        # if TIB data are there
        if lst_evt.extdevices_presence & 1:
            tib = zfits_event.lstcam.tib_data.view(TIB_DTYPE)[0]
            lst_evt.tib_event_counter = tib['event_counter']
            lst_evt.tib_pps_counter = tib['pps_counter']
            lst_evt.tib_tenMHz_counter = tib['tenMHz_counter']
            lst_evt.tib_stereo_pattern = tib['stereo_pattern']
            lst_evt.tib_masked_trigger = tib['masked_trigger']

        # if UCTS data are there
        if lst_evt.extdevices_presence & 2:
            if int(array_event.lst.tel[tel_id].svc.idaq_version) > 37201:
                cdts = zfits_event.lstcam.cdts_data.view(
                    CDTS_AFTER_37201_DTYPE)[0]
                lst_evt.ucts_timestamp = cdts[0]
                lst_evt.ucts_address = cdts[1]  # new
                lst_evt.ucts_event_counter = cdts[2]
                lst_evt.ucts_busy_counter = cdts[3]  # new
                lst_evt.ucts_pps_counter = cdts[4]
                lst_evt.ucts_clock_counter = cdts[5]
                lst_evt.ucts_trigger_type = cdts[6]
                lst_evt.ucts_white_rabbit_status = cdts[7]
                lst_evt.ucts_stereo_pattern = cdts[8]  # new
                lst_evt.ucts_num_in_bunch = cdts[9]  # new
                lst_evt.ucts_cdts_version = cdts[10]  # new

            else:
                # unpack UCTS-CDTS data (old version)
                cdts = zfits_event.lstcam.cdts_data.view(
                    CDTS_BEFORE_37201_DTYPE)[0]
                lst_evt.ucts_event_counter = cdts[0]
                lst_evt.ucts_pps_counter = cdts[1]
                lst_evt.ucts_clock_counter = cdts[2]
                lst_evt.ucts_timestamp = cdts[3]
                lst_evt.ucts_camera_timestamp = cdts[4]
                lst_evt.ucts_trigger_type = cdts[5]
                lst_evt.ucts_white_rabbit_status = cdts[6]

        # if SWAT data are there
        if lst_evt.extdevices_presence & 4:
            # unpack SWAT data
            unpacked_swat = zfits_event.lstcam.swat_data.view(SWAT_DTYPE)[0]
            lst_evt.swat_timestamp = unpacked_swat[0]
            lst_evt.swat_counter1 = unpacked_swat[1]
            lst_evt.swat_counter2 = unpacked_swat[2]
            lst_evt.swat_event_type = unpacked_swat[3]
            lst_evt.swat_camera_flag = unpacked_swat[4]
            lst_evt.swat_camera_event_num = unpacked_swat[5]
            lst_evt.swat_array_flag = unpacked_swat[6]
            lst_evt.swat_array_event_num = unpacked_swat[7]

        # unpack Dragon counters
        counters = zfits_event.lstcam.counters.view(DRAGON_COUNTERS_DTYPE)
        lst_evt.pps_counter = counters['pps_counter']
        lst_evt.tenMHz_counter = counters['tenMHz_counter']
        lst_evt.event_counter = counters['event_counter']
        lst_evt.trigger_counter = counters['trigger_counter']
        lst_evt.local_clock_counter = counters['local_clock_counter']

        lst_evt.chips_flags = zfits_event.lstcam.chips_flags
        lst_evt.first_capacitor_id = zfits_event.lstcam.first_capacitor_id
        lst_evt.drs_tag_status = zfits_event.lstcam.drs_tag_status
        lst_evt.drs_tag = zfits_event.lstcam.drs_tag

        lst_evt.ucts_jump = False

    def fill_trigger_info(self, array_event):
        tel_id = self.tel_id

        trigger = array_event.trigger
        trigger.time = self.time_calculator(tel_id, array_event)
        trigger.tels_with_trigger = [tel_id]
        trigger.tel[tel_id].time = trigger.time

        lst = array_event.lst.tel[tel_id]
        tib_available = lst.evt.extdevices_presence & 1
        ucts_available = lst.evt.extdevices_presence & 2

        # decide which source to use, if both are available,
        # the option decides, if not, fallback to the avilable source
        # if no source available, warn and do not fill trigger info
        if tib_available and ucts_available:
            if self.default_trigger_type == 'ucts':
                trigger_bits = lst.evt.ucts_trigger_type
            else:
                trigger_bits = lst.evt.tib_masked_trigger

        elif tib_available:
            trigger_bits = lst.evt.tib_masked_trigger

        elif ucts_available:
            trigger_bits = lst.evt.ucts_trigger_type

        else:
            self.log.warning('No trigger info available.')
            trigger.event_type = EventType.UNKNOWN
            return

        if (ucts_available and lst.evt.ucts_trigger_type == 42
                and self.default_trigger_type == "ucts"):
            self.log.warning(
                'Event with UCTS trigger_type 42 found.'
                ' Probably means unreliable or shifted UCTS data.'
                ' Consider switching to TIB using `default_trigger_type="tib"`'
            )

        # first bit mono trigger, second stereo.
        # If *only* those two are set, we assume it's a physics event
        # for all other we only check if the flag is present
        if (trigger_bits & TriggerBits.PHYSICS) and not (trigger_bits
                                                         & TriggerBits.OTHER):
            trigger.event_type = EventType.SUBARRAY
        elif trigger_bits & TriggerBits.CALIBRATION:
            trigger.event_type = EventType.FLATFIELD
        elif trigger_bits & TriggerBits.PEDESTAL:
            trigger.event_type = EventType.SKY_PEDESTAL
        elif trigger_bits & TriggerBits.SINGLE_PE:
            trigger.event_type = EventType.SINGLE_PE
        else:
            self.log.warning(
                f'Event {array_event.index.event_id} has unknown event type, trigger: {trigger_bits:08b}'
            )
            trigger.event_type = EventType.UNKNOWN

    def tag_flatfield_events(self, array_event):
        '''
        Use a heuristic based on R1 waveforms to recognize flat field events

        Currently, tagging of flat field events does not work,
        they are reported as physics events, here a heuristic identifies
        those events. Since trigger types might be wrong due to ucts errors,
        we try to identify flat field events in all trigger types.

        DRS4 corrections but not the p.e. calibration must be applied
        '''
        tel_id = self.tel_id
        waveform = array_event.r1.tel[tel_id].waveform

        # needs to work for gain already selected or not
        if waveform.ndim == 3:
            image = waveform[HIGH_GAIN].sum(axis=1)
        else:
            image = waveform.sum(axis=1)

        in_range = (image >= self.min_flatfield_adc) & (image <=
                                                        self.max_flatfield_adc)
        n_in_range = np.count_nonzero(in_range)

        looks_like_ff = n_in_range >= self.min_flatfield_pixel_fraction * image.size
        if looks_like_ff:
            array_event.trigger.event_type = EventType.FLATFIELD
            self.log.debug('Setting event type of event'
                           f' {array_event.index.event_id} to FLATFIELD')
        elif array_event.trigger.event_type == EventType.FLATFIELD:
            self.log.warning(
                'Found FF event that does not fulfill FF criteria: %d',
                array_event.index.event_id,
            )
            array_event.trigger.event_type = EventType.UNKNOWN

    def fill_pointing_info(self, array_event):
        tel_id = self.tel_id
        pointing = self.pointing_source.get_pointing_position_altaz(
            tel_id,
            array_event.trigger.time,
        )
        array_event.pointing.tel[tel_id] = pointing
        array_event.pointing.array_altitude = pointing.altitude
        array_event.pointing.array_azimuth = pointing.azimuth

        ra, dec = self.pointing_source.get_pointing_position_icrs(
            tel_id, array_event.trigger.time)
        array_event.pointing.array_ra = ra
        array_event.pointing.array_dec = dec

    def fill_r0r1_camera_container(self, zfits_event):
        """
        Fill the r0 or r1 container, depending on whether gain
        selection has already happened (r1) or not (r0)

        This will create waveforms of shape (N_GAINS, N_PIXELS, N_SAMPLES),
        or (N_PIXELS, N_SAMPLES) respectively regardless of the n_pixels, n_samples
        in the file.

        Missing or broken pixels are filled using maxval of the waveform dtype.
        """
        n_pixels = self.camera_config.num_pixels
        n_samples = self.camera_config.num_samples
        expected_pixels = self.camera_config.expected_pixels_id

        has_low_gain = (zfits_event.pixel_status
                        & PixelStatus.LOW_GAIN_STORED).astype(bool)
        has_high_gain = (zfits_event.pixel_status
                         & PixelStatus.HIGH_GAIN_STORED).astype(bool)
        not_broken = (has_low_gain | has_high_gain).astype(bool)

        # broken pixels have both false, so gain selected means checking
        # if there are any pixels where exactly one of high or low gain is stored
        gain_selected = np.any(has_low_gain != has_high_gain)

        # fill value for broken pixels
        dtype = zfits_event.waveform.dtype
        fill = np.iinfo(dtype).max
        # we assume that either all pixels are gain selected or none
        # only broken pixels are allowed to be missing completely
        if gain_selected:
            selected_gain = np.where(has_high_gain, 0, 1)
            waveform = np.full((n_pixels, n_samples), fill, dtype=dtype)
            waveform[not_broken] = zfits_event.waveform.reshape(
                (-1, n_samples))

            reordered_waveform = np.full((N_PIXELS, N_SAMPLES),
                                         fill,
                                         dtype=dtype)
            reordered_waveform[expected_pixels] = waveform

            reordered_selected_gain = np.full(N_PIXELS, -1, dtype=np.int8)
            reordered_selected_gain[expected_pixels] = selected_gain

            r0 = R0CameraContainer()
            r1 = R1CameraContainer(
                waveform=reordered_waveform,
                selected_gain_channel=reordered_selected_gain,
            )
        else:
            reshaped_waveform = zfits_event.waveform.reshape(
                N_GAINS, n_pixels, n_samples)
            # re-order the waveform following the expected_pixels_id values
            #  could also just do waveform = reshaped_waveform[np.argsort(expected_ids)]
            reordered_waveform = np.full((N_GAINS, N_PIXELS, N_SAMPLES),
                                         fill,
                                         dtype=dtype)
            reordered_waveform[:, expected_pixels, :] = reshaped_waveform
            r0 = R0CameraContainer(waveform=reordered_waveform)
            r1 = R1CameraContainer()

        return r0, r1

    def fill_r0r1_container(self, array_event, zfits_event):
        """
        Fill with R0Container

        """
        r0, r1 = self.fill_r0r1_camera_container(zfits_event)
        array_event.r0.tel[self.tel_id] = r0
        array_event.r1.tel[self.tel_id] = r1

    def initialize_mon_container(self, array_event):
        """
        Fill with MonitoringContainer.
        For the moment, initialize only the PixelStatusContainer

        """
        container = array_event.mon
        mon_camera_container = container.tel[self.tel_id]

        shape = (N_GAINS, N_PIXELS)
        # all pixels broken by default
        status_container = PixelStatusContainer(
            hardware_failing_pixels=np.ones(shape, dtype=bool),
            pedestal_failing_pixels=np.zeros(shape, dtype=bool),
            flatfield_failing_pixels=np.zeros(shape, dtype=bool),
        )
        mon_camera_container.pixel_status = status_container

    def fill_mon_container(self, array_event, zfits_event):
        """
        Fill with MonitoringContainer.
        For the moment, initialize only the PixelStatusContainer

        """
        status_container = array_event.mon.tel[self.tel_id].pixel_status

        # reorder the array
        expected_pixels_id = self.camera_config.expected_pixels_id

        reordered_pixel_status = np.zeros(N_PIXELS,
                                          dtype=zfits_event.pixel_status.dtype)
        reordered_pixel_status[expected_pixels_id] = zfits_event.pixel_status

        channel_info = get_channel_info(reordered_pixel_status)
        status_container.hardware_failing_pixels[:] = channel_info == 0

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def __len__(self):
        if self.max_events is not None:
            return min(self.max_events, len(self.multi_file))
        return len(self.multi_file)

    def close(self):
        self.multi_file.close()
class TimeCorrectionCalculate(Component):
    """
        The TimeCorrectionCalculate class to create h5py
        file with coefficients for time correction curve
        of chip DRS4.
        Description of this method: "Analysis techniques
        and performance of the Domino Ring Sampler version 4
        based readout for the MAGIC telescopes [arxiv:1305.1007]
    """

    minimum_charge = Float(
        200, help='Cut on charge. Default 200 ADC').tag(config=True)

    tel_id = Int(1, help='Id of the telescope to calibrate').tag(config=True)

    n_combine = Int(
        8, help='How many capacitors are combines in a single bin. Default 8'
    ).tag(config=True)

    n_harmonics = Int(
        16, help='Number of harmonic for Fourier series expansion. Default 16'
    ).tag(config=True)

    n_capacitors = Int(
        1024, help='Number of capacitors (1024 or 4096). Default 1024.').tag(
            config=True)

    charge_product = Unicode(
        'LocalPeakWindowSum',
        help='Name of the charge extractor to be used').tag(config=True)

    calib_file_path = Unicode(
        '', allow_none=True,
        help='Path to the time calibration file').tag(config=True)

    def __init__(self, subarray, **kwargs):
        """
        The TimeCorrectionCalculate class to create h5py
        file with coefficients for time correction curve of chip DRS4.
        Description of this method: "Analysis techniques and performance
        of the Domino Ring Sampler version 4 based readout
        for the MAGIC telescopes [arxiv:1305.1007]

        Parameters
        ----------
        subarray: ctapipe.instrument.SubarrayDescription
            Description of the subarray. Provides information about the
            camera which are useful in charge extraction, such as reference
            pulse shape, sampling rate, neighboring pixels. Also required for
            configuring the TelescopeParameter traitlets.
        kwargs
        """
        super().__init__(**kwargs)

        self.n_bins = int(self.n_capacitors / self.n_combine)

        self.mean_values_per_bin = np.zeros((n_gain, n_pixels, self.n_bins))
        self.entries_per_bin = np.zeros((n_gain, n_pixels, self.n_bins))

        self.first_cap_array = np.zeros((n_modules, n_gain, n_channel))

        # load the waveform charge extractor
        self.extractor = ImageExtractor.from_name(self.charge_product,
                                                  config=self.config,
                                                  subarray=subarray)

        self.log.info(f"extractor {self.extractor}")
        self.sum_events = 0

    def calibrate_peak_time(self, event):
        """
        Fill bins using time pulse from LocalPeakWindowSum.
        Parameters
        ----------
        event : `ctapipe` event-container
        """

        if event.trigger.event_type == EventType.FLATFIELD:
            for nr_module in prange(0, n_modules):
                self.first_cap_array[
                    nr_module, :, :] = self.get_first_capacitor(
                        event, nr_module)

            pixel_ids = event.lst.tel[self.tel_id].svc.pixel_ids
            waveforms = event.r1.tel[self.tel_id].waveform
            no_gain_selection = np.zeros(
                (waveforms.shape[0], waveforms.shape[1]), dtype=np.int64)
            # select both gain
            charge, peak_time = self.extractor(
                event.r1.tel[self.tel_id].waveform[:, :, :], self.tel_id,
                no_gain_selection)
            self.calib_peak_time_jit(charge,
                                     peak_time,
                                     pixel_ids,
                                     self.first_cap_array,
                                     self.mean_values_per_bin,
                                     self.entries_per_bin,
                                     n_cap=self.n_capacitors,
                                     n_combine=self.n_combine,
                                     min_charge=self.minimum_charge)
            self.sum_events += 1

    @staticmethod
    @njit(parallel=True)
    def calib_peak_time_jit(charge, peak_time, pixel_ids, first_cap_array,
                            mean_values_per_bin, entries_per_bin, n_cap,
                            n_combine, min_charge):
        """
        Numba function for calibration pulse time.

        Parameters
        ----------
        pulse : ndarray
            Pulse time stored in a numpy array of shape
            (n_gain, n_pixels).
        charge : ndarray
            Charge in each pixel.
            (n_gain, n_pixels).
        pixel_ids: ndarray
            Array stored expected pixel id
            (n_pixels).
        first_cap_array : ndarray
            Value of first capacitor stored in a numpy array of shape
            (n_clus, n_gain, n_pix).
        mean_values_per_bin : ndarray
            Array to fill using pulse time
            stored in a numpy array of shape
            (n_gain, n_pixels, n_bins).
        entries_per_bin : ndarray
            Array to store number of entries per bin
            stored in a numpy array of shape
            (n_gain, n_pixels, n_bins).
        n_cap : int
            Number of capacitors
        n_combine : int
            Number of combine capacitors in a single bin

        """

        for nr_module in prange(n_modules):
            for gain in prange(n_gain):
                for pix in prange(n_channel):
                    pixel = pixel_ids[nr_module * 7 + pix]
                    if charge[gain, pixel] > min_charge:  # cut change
                        fc = first_cap_array[nr_module, :, :]
                        first_cap = (fc[gain, pix]) % n_cap
                        bin = int(first_cap / n_combine)
                        mean_values_per_bin[gain, pixel,
                                            bin] += peak_time[gain, pixel]
                        entries_per_bin[gain, pixel, bin] += 1

    def finalize(self):
        n_total = self.entries_per_bin.size
        n_available = np.count_nonzero(self.entries_per_bin)
        if n_available < n_total:
            raise RuntimeError(
                "No data available for some capacitors. "
                "It might help to use more events to create the calibration file. "
                f"Available: {n_available / n_total:.3%}, Missing: {n_total - n_available}"
            )
        else:
            self.mean_values_per_bin = self.mean_values_per_bin / self.entries_per_bin
            self.save_to_hdf5_file()

    def fit(self, pixel_id, gain):
        """
            Fit data bins using Fourier series expansion
            Parameters
            ----------
            pixel_id : ndarray
            Array stored expected pixel id of shape
            (n_pixels).
            gain: int
            0 for high gain, 1 for low gain
        """
        self.pos = np.zeros(self.n_bins)
        for i in range(0, self.n_bins):
            self.pos[i] = (i + 0.5) * self.n_combine

        self.fan = np.zeros(self.n_harmonics)  # cos coeff
        self.fbn = np.zeros(self.n_harmonics)  # sin coeff

        for n in range(0, self.n_harmonics):
            self.integrate_with_trig(self.pos,
                                     self.mean_values_per_bin[gain, pixel_id],
                                     n, self.fan, self.fbn)

    def integrate_with_trig(self, x, y, n, an, bn):
        """
            Function to expanding into Fourier series
            Parameters
            ----------
            x : ndarray
            Array stored position in DRS4 ring of shape
            (n_bins).
            y: ndarray
            Array stored mean pulse time per bin of shape
            (n_bins)
            n : int
            n harmonic
            an: ndarray
            Array to fill with cos coeff of shape
            (n_harmonics)
            bn: ndarray
            Array to fill with sin coeff of shape
            (n_harmonics)
        """
        suma = 0
        sumb = 0

        for i in range(0, self.n_bins):
            suma += y[i] * self.n_combine * np.cos(
                2 * np.pi * n * (x[i] / float(self.n_capacitors)))
            sumb += y[i] * self.n_combine * np.sin(
                2 * np.pi * n * (x[i] / float(self.n_capacitors)))

        an[n] = suma * (2. / (self.n_bins * self.n_combine))
        bn[n] = sumb * (2. / (self.n_bins * self.n_combine))

    def get_first_capacitor(self, event, nr):
        fc = np.zeros((n_gain, n_channel))
        first_cap = event.lst.tel[
            self.tel_id].evt.first_capacitor_id[nr * 8:(nr + 1) * 8]
        # First capacitor order according Dragon v5 board data format
        for i, j in zip([0, 1, 2, 3, 4, 5, 6], [0, 0, 1, 1, 2, 2, 3]):
            fc[high_gain, i] = first_cap[j]
        for i, j in zip([0, 1, 2, 3, 4, 5, 6], [4, 4, 5, 5, 6, 6, 7]):
            fc[low_gain, i] = first_cap[j]
        return fc

    def save_to_hdf5_file(self):
        """
            Function to save Fourier series expansion coeff into hdf5 file
        """
        fan_array = np.zeros((n_gain, n_pixels, self.n_harmonics))
        fbn_array = np.zeros((n_gain, n_pixels, self.n_harmonics))

        for pix_id in range(n_pixels):
            self.fit(pix_id, gain=high_gain)
            fan_array[high_gain, pix_id, :] = self.fan
            fbn_array[high_gain, pix_id, :] = self.fbn

            self.fit(pix_id, gain=low_gain)
            fan_array[low_gain, pix_id, :] = self.fan
            fbn_array[low_gain, pix_id, :] = self.fbn

        try:
            with h5py.File(self.calib_file_path, 'w') as hf:
                hf.create_dataset('fan', data=fan_array)
                hf.create_dataset('fbn', data=fbn_array)
                hf.attrs['n_events'] = self.sum_events
                hf.attrs['n_harm'] = self.n_harmonics
                # need pytables and time calib container
                # to use lstchain.io.add_config_metadata
                hf.attrs['config'] = str(self.config)

            metadata = global_metadata()
            write_metadata(metadata, self.calib_file_path)

        except Exception:
            raise IOError(f"Failed to create the file {self.calib_file_path}")
Beispiel #15
0
 class SomeComponent(TelescopeComponent):
     path = TelescopeParameter(Path(allow_none=True, default_value=None),
                               default_value=None).tag(config=True)
     val = TelescopeParameter(Float(), default_value=1.0).tag(config=True)
     flag = TelescopeParameter(Bool(), default_value=True).tag(config=True)
Beispiel #16
0
class TimeWaveformFitter(TelescopeComponent):
    """
    Class used to perform event reconstruction by fitting of a model on waveforms.
    """
    sigma_s = FloatTelescopeParameter(
        default_value=1,
        help='Width of the single photo-electron peak distribution.',
        allow_none=False).tag(config=True)
    crosstalk = FloatTelescopeParameter(default_value=0,
                                        help='Average pixel crosstalk.',
                                        allow_none=False).tag(config=True)
    sigma_space = Float(
        4,
        help=
        'Size of the region on which the fit is performed relative to the image extension.',
        allow_none=False).tag(config=True)
    sigma_time = Float(
        3,
        help=
        'Time window on which the fit is performed relative to the image temporal extension.',
        allow_none=False).tag(config=True)
    time_before_shower = FloatTelescopeParameter(
        default_value=10,
        help='Additional time at the start of the fit temporal window.',
        allow_none=False).tag(config=True)
    time_after_shower = FloatTelescopeParameter(
        default_value=20,
        help='Additional time at the end of the fit temporal window.',
        allow_none=False).tag(config=True)
    use_weight = Bool(
        False,
        help=
        'If True, the brightest sample is twice as important as the dimmest pixel in the '
        'likelihood. If false all samples are equivalent.',
        allow_none=False).tag(config=True)
    no_asymmetry = Bool(
        False,
        help='If true, the asymmetry of the spatial model is fixed to 0.',
        allow_none=False).tag(config=True)
    use_interleaved = Path(
        None,
        help=
        'Location of the dl1 file used to estimate the pedestal exploiting interleaved'
        ' events.',
        allow_none=True).tag(config=True)
    n_peaks = Int(
        0,
        help=
        'Maximum brightness (p.e.) for which the full likelihood computation is used. '
        'If the Poisson term for Np.e.>n_peak is more than 1e-6 a Gaussian approximation is used.',
        allow_none=False).tag(config=True)
    bound_charge_factor = FloatTelescopeParameter(
        default_value=4,
        help='Maximum relative change to the fitted charge parameter.',
        allow_none=False).tag(config=True)
    bound_t_cm_value = FloatTelescopeParameter(
        default_value=10,
        help='Maximum change to the t_cm parameter.',
        allow_none=False).tag(config=True)
    bound_centroid_control_parameter = FloatTelescopeParameter(
        default_value=1,
        help='Maximum change of the centroid coordinated in '
        'number of seed length',
        allow_none=False).tag(config=True)
    bound_max_length_factor = FloatTelescopeParameter(
        default_value=2,
        help='Maximum relative increase to the fitted length parameter.',
        allow_none=False).tag(config=True)
    bound_length_asymmetry = FloatTelescopeParameter(
        default_value=9,
        help='Bounds for the fitted rl parameter.',
        allow_none=False).tag(config=True)
    bound_max_v_cm_factor = FloatTelescopeParameter(
        default_value=2,
        help='Maximum relative increase to the fitted v_cm parameter.',
        allow_none=False).tag(config=True)
    default_seed_t_cm = FloatTelescopeParameter(
        default_value=0,
        help='Default starting value of t_cm when the seed extraction failed.',
        allow_none=False).tag(config=True)
    default_seed_v_cm = FloatTelescopeParameter(
        default_value=40,
        help='Default starting value of v_cm when the seed extraction failed.',
        allow_none=False).tag(config=True)
    verbose = Int(
        0,
        help='4 - used for tests: create debug plots\n'
        '3 - create debug plots, wait for input after each event, increase minuit verbose level\n'
        '2 - create debug plots, increase minuit verbose level\n'
        '1 - increase minuit verbose level\n'
        '0 - silent',
        allow_none=False).tag(config=True)

    def __init__(self, subarray, config=None, parent=None, **kwargs):
        super().__init__(subarray=subarray,
                         config=config,
                         parent=parent,
                         **kwargs)
        self.subarray = subarray
        self.template_dict = {}
        self.template_time_of_max_dict = {}
        for tel_id in subarray.tel:
            self.template_dict[
                tel_id] = NormalizedPulseTemplate.load_from_eventsource(
                    subarray.tel[tel_id].camera.readout)
            self.template_time_of_max_dict[tel_id] = self.template_dict[
                tel_id].compute_time_of_max()
        poisson_peaks = np.arange(self.n_peaks + 1, dtype=int)
        poisson_peaks[0] = 1
        self.factorial = np.cumprod(poisson_peaks, dtype='u8')
        # Find the transition charge between full likelihood computation and Gaussian approximation
        # The maximum charge is selected such that each Poisson terms in the full likelihood computation
        # above the n_peaks limit account for less than (1/n_peaks)%
        transition_charges = {}
        for config_crosstalk in self.crosstalk:
            # if n_peaks is set to 0, only the Gaussian approximation is used
            transition_charges[config_crosstalk[2]] = 0.0 if self.n_peaks == 0\
                else self.find_transition_charge(config_crosstalk[2], 1e-2/self.n_peaks)
        self.transition_charges = {}
        for tel_id in subarray.tel:
            self.transition_charges[tel_id] = transition_charges[
                self.crosstalk.tel[tel_id]]

        self.start_parameters = None
        self.names_parameters = None
        self.end_parameters = None
        self.error_parameters = None
        self.bound_parameters = None
        self.fcn = None

    def call_setup(self, event, telescope_id, dl1_container):
        """
        Extract all event dependent quantities used for the fit.

        Parameters
        ----------
        event: ctapipe event container
            Current event container.
        telescope_id: int
            Id of the telescope
        dl1_container: DL1ParametersContainer
            Contains the Hillas parameters used as seed for the fit

        Returns
        -------
        focal_length: astropy.units.Quantity
            Focal length of the telescope
        fit_params: array
            Array containing all the variable needed to compute the likelihood
            during the fir excluding the model parameters
        """

        geometry = self.subarray.tel[telescope_id].camera.geometry
        unit = geometry.pix_x.unit
        pix_x = geometry.pix_x.to_value(unit)
        pix_y = geometry.pix_y.to_value(unit)
        r_max = geometry.guess_radius().to_value(unit)
        pix_radius = np.sqrt(geometry.pix_area[0].to_value(unit**2) /
                             np.pi)  # find linear size of a pixel
        readout = self.subarray.tel[telescope_id].camera.readout
        sampling_rate = readout.sampling_rate.to_value(u.GHz)
        dt = (1.0 / sampling_rate)
        template = self.template_dict[telescope_id]
        image = event.dl1.tel[telescope_id].image
        hillas_signal_pixels = event.dl1.tel[telescope_id].image_mask
        start_x_cm, start_y_cm = init_centroid(dl1_container,
                                               geometry[hillas_signal_pixels],
                                               unit,
                                               image[hillas_signal_pixels],
                                               self.no_asymmetry)

        waveform = event.r1.tel[telescope_id].waveform

        dl1_calib = event.calibration.tel[telescope_id].dl1
        time_shift = dl1_calib.time_shift
        # TODO check if this is correct here or if it is applied to r1 waveform earlier
        if dl1_calib.pedestal_offset is not None:
            waveform = waveform - dl1_calib.pedestal_offset[:, np.newaxis]

        n_pixels, n_samples = waveform.shape
        times = np.arange(0, n_samples) * dt
        selected_gains = event.r1.tel[telescope_id].selected_gain_channel
        is_high_gain = (selected_gains == 0)

        # We assume that the time gradient is given in unit of 'geometry spatial unit'/ns
        v = dl1_container.time_gradient
        psi = dl1_container.psi.to_value(u.rad)
        # We use only positive time gradients and psi is projected in [-pi,pi] from [-pi/2,pi/2]
        if v < 0:
            if psi >= 0:
                psi = psi - np.pi
            else:
                psi = psi + np.pi

        start_length = max(dl1_container.length.to_value(unit), pix_radius)
        # With current likelihood computation, order and type of the parameters are important
        start_parameters = {
            'charge':
            dl1_container.intensity,
            't_cm':
            dl1_container.intercept -
            self.template_time_of_max_dict[telescope_id],
            'x_cm':
            start_x_cm.to_value(unit),
            'y_cm':
            start_y_cm.to_value(unit),
            'length':
            start_length,
            'wl':
            max(dl1_container.wl, 0.01),
            'psi':
            psi,
            'v':
            np.abs(v),
            'rl':
            0.0
        }

        # Temporal parameters extraction fails when cleaning select only 2 pixels, we use defaults values in this case
        if np.isnan(start_parameters['t_cm']):
            start_parameters['t_cm'] = self.default_seed_t_cm.tel[telescope_id]
        if np.isnan(start_parameters['v']):
            start_parameters['v'] = self.default_seed_v_cm.tel[telescope_id]

        t_max = n_samples * dt
        v_min, v_max = 0, max(
            self.bound_max_v_cm_factor.tel[telescope_id] *
            start_parameters['v'], 50)
        rl_min, rl_max = -self.bound_length_asymmetry.tel[
            telescope_id], self.bound_length_asymmetry.tel[telescope_id]
        if self.no_asymmetry:
            rl_min, rl_max = 0.0, 0.0
        bound_centroid = self.bound_centroid_control_parameter.tel[
            telescope_id] * start_length

        bound_parameters = {
            'charge': (dl1_container.intensity /
                       self.bound_charge_factor.tel[telescope_id],
                       dl1_container.intensity *
                       self.bound_charge_factor.tel[telescope_id]),
            't_cm': (-self.bound_t_cm_value.tel[telescope_id],
                     t_max + self.bound_t_cm_value.tel[telescope_id]),
            'x_cm': (start_x_cm.to_value(unit) - bound_centroid,
                     start_x_cm.to_value(unit) + bound_centroid),
            'y_cm': (start_y_cm.to_value(unit) - bound_centroid,
                     start_y_cm.to_value(unit) + bound_centroid),
            'length':
            (pix_radius,
             min(self.bound_max_length_factor.tel[telescope_id] * start_length,
                 r_max)),
            'wl': (0.001, 1.0),
            'psi': (-np.pi * 2.0, np.pi * 2.0),
            'v': (v_min, v_max),
            'rl': (rl_min, rl_max)
        }

        mask_pixel, mask_time = self.clean_data(pix_x, pix_y, pix_radius,
                                                times, start_parameters,
                                                telescope_id)
        spatial_ones = np.ones(np.sum(mask_pixel))

        is_high_gain = is_high_gain[mask_pixel]
        sig_s = spatial_ones * self.sigma_s.tel[telescope_id]
        crosstalks = spatial_ones * self.crosstalk.tel[telescope_id]

        times = (np.arange(0, n_samples) * dt)[mask_time]
        time_shift = time_shift[mask_pixel]

        p_x = pix_x[mask_pixel]
        p_y = pix_y[mask_pixel]
        pix_area = geometry.pix_area[mask_pixel].to_value(unit**2)

        data = waveform
        error = None  # TODO include option to use calibration data

        filter_pixels = np.nonzero(~mask_pixel)
        filter_times = np.nonzero(~mask_time)

        if error is None:
            std = np.std(data[~mask_pixel])
            error = np.full(data.shape[0], std)

        data = np.delete(data, filter_pixels, axis=0)
        data = np.delete(data, filter_times, axis=1)
        error = np.delete(error, filter_pixels, axis=0)

        # Fill the set of non-fitted parameters needed to compute the likelihood. Order and type sensitive.
        fit_params = [
            data, error, is_high_gain, sig_s, crosstalks, times,
            np.float32(time_shift), p_x, p_y,
            np.float64(pix_area), template.dt, template.t0,
            template.amplitude_LG, template.amplitude_HG, self.n_peaks,
            self.transition_charges[telescope_id], self.use_weight,
            self.factorial
        ]

        self.start_parameters = start_parameters
        self.names_parameters = start_parameters.keys()
        self.bound_parameters = bound_parameters

        return unit, fit_params

    def __call__(self, event, telescope_id, dl1_container):
        # setup angle to distance conversion on the camera plane for the current telescope
        focal_length = self.subarray.tel[
            telescope_id].optics.equivalent_focal_length
        angle_dist_eq = [
            (u.rad, u.m, lambda x: np.tan(x) * focal_length.to_value(u.m),
             lambda x: np.arctan(x / focal_length.to_value(u.m))),
            (u.rad**2, u.m**2, lambda x:
             (np.tan(np.sqrt(x)) * focal_length.to_value(u.m))**2, lambda x:
             (np.arctan(np.sqrt(x) / focal_length.to_value(u.m)))**2)
        ]
        with u.set_enabled_equivalencies(angle_dist_eq):
            self.start_parameters = None
            self.names_parameters = None
            unit_cam, fit_params = self.call_setup(event, telescope_id,
                                                   dl1_container)
            self.end_parameters = None
            self.error_parameters = None
            self.fcn = None

            return self.predict(unit_cam, fit_params)

    def clean_data(self, pix_x, pix_y, pix_radius, times, start_parameters,
                   telescope_id):
        """
        Method used to select pixels and time samples used in the fitting procedure.
        The spatial selection takes pixels in an ellipsis obtained from the seed Hillas parameters extended by one pixel
        size and multiplied by a factor sigma_space.
        The temporal selection takes a time window centered on the seed time of center of mass and of duration equal to
        the time of propagation of the signal along the length of the ellipsis times a factor sigma_time.
        An additional fixed duration is also added before and after this time window through the time_before_shower and
        time_after_shower arguments.

        Parameters
        ----------
        pix_x, pix_y: array-like
            Pixels positions
        pix_radius: float
        times: array-like
            Sampling times before timeshift corrections
        start_parameters: dict
            Seed parameters derived from the Hillas parameters
        telescope_id: int

        Returns
        ----------
        mask_pixel, mask_time: array-like
            Mask used to select pixels and times for the fit

        """
        x_cm = start_parameters['x_cm']
        y_cm = start_parameters['y_cm']
        length = start_parameters['length']
        width = start_parameters['wl'] * length
        psi = start_parameters['psi']

        dx = pix_x - x_cm
        dy = pix_y - y_cm

        lon = dx * np.cos(psi) + dy * np.sin(psi)
        lat = dx * np.sin(psi) - dy * np.cos(psi)

        mask_pixel = ((lon / (length + pix_radius))**2 +
                      (lat / (width + pix_radius))**2) < self.sigma_space**2

        v = start_parameters['v']
        t_start = (start_parameters['t_cm'] -
                   (np.abs(v) * length / 2 * self.sigma_time) -
                   self.time_before_shower.tel[telescope_id])
        t_end = (start_parameters['t_cm'] +
                 (np.abs(v) * length / 2 * self.sigma_time) +
                 self.time_after_shower.tel[telescope_id])

        mask_time = (times < t_end) * (times > t_start)

        return mask_pixel, mask_time

    def find_transition_charge(self, crosstalk, poisson_proba_min=1e-2):
        """
        Find the charge below which the full likelihood computation is performed and above which a Gaussian
        approximation is used. For a given pixel crosstalk it finds the maximum charge with a Generalised Poisson term
        below poisson_proba_min for n_peaks photo-electrons. n_peaks here is the configured maximum number of
        photo-electron considered in the full likelihood computation.

        Parameters
        ----------
        crosstalk : float
            Pixels crosstalk
        poisson_proba_min: float

        Returns
        -------
        transition_charge: float32
            Model charge of transition between full and approximated likelihood

        """
        transition_charge = self.n_peaks / (1 + crosstalk)
        step = transition_charge / 100

        def poisson(mu, cross_talk):
            return (mu * pow(mu + self.n_peaks * cross_talk,
                             (self.n_peaks - 1)) /
                    self.factorial[self.n_peaks] *
                    np.exp(-mu - self.n_peaks * cross_talk))

        while poisson(transition_charge, crosstalk) > poisson_proba_min:
            transition_charge -= step
        logger.info(
            f'Transition charge between full and approximated likelihood for camera '
            f'with crosstalk = {crosstalk:.4f} is,  {transition_charge:.4f}, p.e.'
        )
        return np.float32(transition_charge)

    def fit(self, fit_params):
        """
        Performs the fitting procedure.

        Parameters
        ----------
        fit_params: array
            Parameters used to compute the likelihood but not fitted

        """
        def f(*args):
            return -2 * self.log_likelihood(*args, fit_params=fit_params)

        print_level = 2 if self.verbose in [1, 2, 3] else 0
        m = Minuit(f,
                   name=self.names_parameters,
                   *self.start_parameters.values())
        for key, val in self.bound_parameters.items():
            m.limits[key] = val
        m.print_level = print_level
        m.errordef = 0.5
        m.simplex().migrad()
        self.end_parameters = m.values.to_dict()
        self.fcn = m.fval
        self.error_parameters = m.errors.to_dict()

    def predict(self, unit_cam, fit_params):
        """
            Call the fitting procedure and fill the results.

        Parameters
        ----------
        unit_cam: astropy.units.unit
            Unit used for the camera geometry and for spatial variable in the fit
        fit_params: array
            Parameters used to compute the likelihood but not fitted

        Returns
        ----------
        container: DL1LikelihoodParametersContainer
            Filled parameter container

        """
        container = DL1LikelihoodParametersContainer(lhfit_call_status=1)
        try:
            self.fit(fit_params)
            container.lhfit_TS = self.fcn

            container.lhfit_x = (self.end_parameters['x_cm'] * unit_cam).to(
                u.m)
            container.lhfit_x_uncertainty = (self.error_parameters['x_cm'] *
                                             unit_cam).to(u.m)
            container.lhfit_y = (self.end_parameters['y_cm'] * unit_cam).to(
                u.m)
            container.lhfit_y_uncertainty = (self.error_parameters['y_cm'] *
                                             unit_cam).to(u.m)
            container.lhfit_r = np.sqrt(container.lhfit_x**2 +
                                        container.lhfit_y**2)
            container.lhfit_phi = np.arctan2(container.lhfit_y,
                                             container.lhfit_x)
            if self.end_parameters['psi'] > np.pi:
                self.end_parameters['psi'] -= 2 * np.pi
            if self.end_parameters['psi'] < -np.pi:
                self.end_parameters['psi'] += 2 * np.pi
            container.lhfit_psi = self.end_parameters['psi'] * u.rad
            container.lhfit_psi_uncertainty = self.error_parameters[
                'psi'] * u.rad
            length_asy = 1 + self.end_parameters['rl'] if self.end_parameters[
                'rl'] >= 0 else 1 / (1 - self.end_parameters['rl'])
            lhfit_length = ((
                (1.0 + length_asy) * self.end_parameters['length'] / 2.0) *
                            unit_cam).to(u.deg)
            container.lhfit_length = lhfit_length
            lhfit_length_rel_err = self.error_parameters[
                'length'] / self.end_parameters['length']
            # We assume that the relative error is the same in the fitted and saved unit
            container.lhfit_length_uncertainty = lhfit_length_rel_err * container.lhfit_length
            container.lhfit_width = self.end_parameters[
                'wl'] * container.lhfit_length

            container.lhfit_time_gradient = self.end_parameters['v']
            container.lhfit_time_gradient_uncertainty = self.error_parameters[
                'v']
            container.lhfit_ref_time = self.end_parameters['t_cm']
            container.lhfit_ref_time_uncertainty = self.error_parameters[
                't_cm']

            container.lhfit_wl = u.Quantity(self.end_parameters['wl'])
            container.lhfit_wl_uncertainty = u.Quantity(
                self.error_parameters['wl'])
            container.lhfit_intensity = self.end_parameters['charge']
            container.lhfit_intensity_uncertainty = self.error_parameters[
                'charge']
            container.lhfit_log_intensity = np.log10(container.lhfit_intensity)
            container.lhfit_t_68 = container.lhfit_length.value * container.lhfit_time_gradient
            container.lhfit_area = container.lhfit_length * container.lhfit_width
            container.lhfit_length_asymmetry = self.end_parameters['rl']
            container.lhfit_length_asymmetry_uncertainty = self.error_parameters[
                'rl']
        except ZeroDivisionError:
            # TODO Check occurrence rate and solve
            container = DL1LikelihoodParametersContainer(lhfit_call_status=-1)
            logger.error(
                'ZeroDivisionError encounter during the fitting procedure, skipping event.'
            )

        return container

    def __str__(self):
        """
            Define the print format of TimeWaveformFitter objects.

            Returns
            -------
            str: string
                Contains the starting and bound parameters used for the fit,
                and the end results with errors and associated log-likelihood
                in readable format.

        """
        s = 'Event processed\n'
        s += 'Start parameters :\n\t{}\n'.format(self.start_parameters)
        s += 'Bound parameters :\n\t{}\n'.format(self.bound_parameters)
        s += 'End parameters :\n\t{}\n'.format(self.end_parameters)
        s += 'Error parameters :\n\t{}\n'.format(self.error_parameters)
        s += '-2Log-Likelihood :\t{}'.format(self.fcn)

        return s

    @staticmethod
    def log_likelihood(*args, fit_params, **kwargs):
        """Compute the log-likelihood used in the fitting procedure."""
        llh = log_pdf(*args, *fit_params, **kwargs)
        return np.sum(llh)