Ejemplo n.º 1
0
class DumpInstrumentTool(Tool):
    description = Unicode(__doc__)
    name = 'ctapipe-dump-instrument'

    infile = Unicode(help='input simtelarray file').tag(config=True)
    format = Enum(['fits', 'ecsv', 'hdf5'],
                  default_value='fits',
                  help='Format '
                  'of '
                  'output '
                  'file',
                  config=True)

    aliases = Dict(
        dict(infile='DumpInstrumentTool.infile',
             format='DumpInstrumentTool.format'))

    def setup(self):
        source = hessio_event_source(self.infile)
        data = next(source)  # get one event, so the instrument table is
        # filled in
        self.inst = data.inst  # keep a pointer to the instrument stuff
        pass

    def start(self):
        self.write_camera_geometries()

    def finish(self):
        pass

    def _get_file_format_info(self, format_name, table_type, cam_name):
        """ returns file extension + dict of required parameters for 
        Table.write"""
        if format_name == 'fits':
            return 'fits.gz', dict()
        elif format_name == 'ecsv':
            return 'ecsv.txt', dict(format='ascii.ecsv')
        elif format_name == 'hdf5':
            return 'h5', dict(path="/" + table_type + "/" + cam_name)
        else:
            raise NameError("format not supported")

    def write_camera_geometries(self):
        cam_types = get_camera_types(self.inst)
        print_camera_types(self.inst, printer=self.log.info)
        for cam_name in cam_types:
            ext, args = self._get_file_format_info(self.format, 'CAMGEOM',
                                                   cam_name)
            self.log.debug("writing {}".format(cam_name))
            tel_id = cam_types[cam_name].pop()
            pix = self.inst.pixel_pos[tel_id]
            flen = self.inst.optical_foclen[tel_id]
            geom = CameraGeometry.guess(*pix, flen)
            table = geom.to_table()
            table.meta['SOURCE'] = self.infile
            table.write("{}.camgeom.{}".format(cam_name, ext), **args)
Ejemplo n.º 2
0
class DumpInstrumentTool(Tool):
    description = Unicode(__doc__)
    name = "ctapipe-dump-instrument"

    infile = Path(exists=True, help="input simtelarray file").tag(config=True)
    format = Enum(
        ["fits", "ecsv", "hdf5"],
        default_value="fits",
        help="Format of output file",
        config=True,
    )

    aliases = Dict(
        dict(infile="DumpInstrumentTool.infile",
             format="DumpInstrumentTool.format"))

    def setup(self):
        with event_source(self.infile) as source:
            self.subarray = source.subarray

    def start(self):
        if self.format == "hdf5":
            self.subarray.to_hdf("subarray.h5")
        else:
            self.write_camera_geometries()
            self.write_optics_descriptions()
            self.write_subarray_description()

    def finish(self):
        pass

    @staticmethod
    def _get_file_format_info(format_name, table_type, table_name):
        """ returns file extension + dict of required parameters for
        Table.write"""
        if format_name == "fits":
            return "fits.gz", dict()
        elif format_name == "ecsv":
            return "ecsv.txt", dict(format="ascii.ecsv")
        else:
            raise NameError(f"format {format_name} not supported")

    def write_camera_geometries(self):
        cam_types = get_camera_types(self.subarray)
        self.subarray.info(printer=self.log.info)
        for cam_name in cam_types:
            ext, args = self._get_file_format_info(self.format, "CAMGEOM",
                                                   cam_name)

            self.log.debug(f"writing {cam_name}")
            tel_id = cam_types[cam_name].pop()
            geom = self.subarray.tel[tel_id].camera.geometry
            table = geom.to_table()
            table.meta["SOURCE"] = str(self.infile)
            filename = f"{cam_name}.camgeom.{ext}"

            try:
                table.write(filename, **args)
                Provenance().add_output_file(filename, "dl0.tel.svc.camera")
            except IOError as err:
                self.log.warning(
                    "couldn't write camera definition '%s' because: %s",
                    filename, err)

    def write_optics_descriptions(self):
        sub = self.subarray
        ext, args = self._get_file_format_info(self.format, sub.name, "optics")

        tab = sub.to_table(kind="optics")
        tab.meta["SOURCE"] = str(self.infile)
        filename = f"{sub.name}.optics.{ext}"
        try:
            tab.write(filename, **args)
            Provenance().add_output_file(filename, "dl0.sub.svc.optics")
        except IOError as err:
            self.log.warning(
                "couldn't write optics description '%s' because: %s", filename,
                err)

    def write_subarray_description(self):
        sub = self.subarray
        ext, args = self._get_file_format_info(self.format, sub.name,
                                               "subarray")
        tab = sub.to_table(kind="subarray")
        tab.meta["SOURCE"] = str(self.infile)
        filename = f"{sub.name}.subarray.{ext}"
        try:
            tab.write(filename, **args)
            Provenance().add_output_file(filename, "dl0.sub.svc.subarray")
        except IOError as err:
            self.log.warning(
                "couldn't write subarray description '%s' because: %s",
                filename, err)
Ejemplo n.º 3
0
class DumpInstrumentTool(Tool):
    description = Unicode(__doc__)
    name = 'ctapipe-dump-instrument'

    infile = Unicode(help='input simtelarray file').tag(config=True)
    format = Enum(['fits', 'ecsv', 'hdf5'],
                  default_value='fits',
                  help='Format of output file',
                  config=True)

    aliases = Dict(dict(infile='DumpInstrumentTool.infile',
                        format='DumpInstrumentTool.format'))

    def setup(self):
        with event_source(self.infile) as source:
            data = next(iter(source))  # get one event, so the instrument table is there

        self.inst = data.inst  # keep a reference to the instrument stuff

    def start(self):
        self.write_camera_geometries()
        self.write_optics_descriptions()
        self.write_subarray_description()

    def finish(self):
        pass

    @staticmethod
    def _get_file_format_info(format_name, table_type, table_name):
        """ returns file extension + dict of required parameters for
        Table.write"""
        if format_name == 'fits':
            return 'fits.gz', dict()
        elif format_name == 'ecsv':
            return 'ecsv.txt', dict(format='ascii.ecsv')
        elif format_name == 'hdf5':
            return 'h5', dict(path="/" + table_type + "/" + table_name)
        else:
            raise NameError("format not supported")

    def write_camera_geometries(self):
        cam_types = get_camera_types(self.inst.subarray)
        self.inst.subarray.info(printer=self.log.info)
        for cam_name in cam_types:
            ext, args = self._get_file_format_info(self.format,
                                                   'CAMGEOM',
                                                   cam_name)

            self.log.debug("writing {}".format(cam_name))
            tel_id = cam_types[cam_name].pop()
            geom = self.inst.subarray.tel[tel_id].camera
            table = geom.to_table()
            table.meta['SOURCE'] = self.infile
            filename = "{}.camgeom.{}".format(cam_name, ext)

            try:
                table.write(filename, **args)
                Provenance().add_output_file(filename, 'dl0.tel.svc.camera')
            except IOError as err:
                self.log.warn("couldn't write camera definition '%s' because: "
                              "%s", filename, err)

    def write_optics_descriptions(self):
        sub = self.inst.subarray
        ext, args = self._get_file_format_info(self.format, sub.name, 'optics')

        tab = sub.to_table(kind='optics')
        tab.meta['SOURCE'] = self.infile
        filename = '{}.optics.{}'.format(sub.name, ext)
        try:
            tab.write(filename, **args)
            Provenance().add_output_file(filename, 'dl0.sub.svc.optics')
        except IOError as err:
            self.log.warn("couldn't write optics description '%s' because: "
                          "%s", filename, err)

    def write_subarray_description(self):
        sub = self.inst.subarray
        ext, args = self._get_file_format_info(self.format, sub.name,
                                               'subarray')
        tab = sub.to_table(kind='subarray')
        tab.meta['SOURCE'] = self.infile
        filename = '{}.subarray.{}'.format(sub.name, ext)
        try:
            tab.write(filename, **args)
            Provenance().add_output_file(filename, 'dl0.sub.svc.subarray')
        except IOError as err:
            self.log.warn("couldn't write subarray description '%s' because: "
                          "%s", filename, err)
Ejemplo n.º 4
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()