Exemplo n.º 1
0
def test_enum_trait_default_is_right():
    """ check default value of enum trait """
    from ctapipe.core.traits import create_class_enum_trait

    with pytest.raises(ValueError):
        create_class_enum_trait(ImageExtractor,
                                default_value="name_of_default_choice")
Exemplo n.º 2
0
def test_enum_trait():
    """ check that enum traits are constructable from a complex class """
    from ctapipe.core.traits import create_class_enum_trait

    trait = create_class_enum_trait(ImageExtractor,
                                    default_value="NeighborPeakWindowSum")
    assert isinstance(trait, CaselessStrEnum)
Exemplo n.º 3
0
class ShowerProcessor(Component):
    """
    Run the stereo event reconstruction on the input events.

    This is mainly needed, so that the type of reconstructor can be chosen
    via the configuration system.

    Input events must already contain dl1 parameters.
    """
    reconstructor_type = create_class_enum_trait(
        Reconstructor,
        default_value="HillasReconstructor",
        help="The stereo geometry reconstructor to be used",
    )

    def __init__(self,
                 subarray: SubarrayDescription,
                 config=None,
                 parent=None,
                 **kwargs):
        """
        Parameters
        ----------
        subarray: SubarrayDescription
            Description of the subarray. Provides information about the
            camera which are useful in calibration. Also required for
            configuring the TelescopeParameter traitlets.
        config: traitlets.loader.Config
            Configuration specified by config file or cmdline arguments.
            Used to set traitlet values.
            This is mutually exclusive with passing a ``parent``.
        parent: ctapipe.core.Component or ctapipe.core.Tool
            Parent of this component in the configuration hierarchy,
            this is mutually exclusive with passing ``config``
        """

        super().__init__(config=config, parent=parent, **kwargs)
        self.subarray = subarray
        self.reconstructor = Reconstructor.from_name(
            self.reconstructor_type,
            subarray=self.subarray,
            parent=self,
        )

    def __call__(self, event: ArrayEventContainer):
        """
        Perform the full shower geometry reconstruction on the input event.

        Afterwards, optionally perform energy estimation and/or particle
        classification (currently these two operations are not yet supported).

        Parameters
        ----------
        event : ctapipe.containers.ArrayEventContainer
            Top-level container for all event information.
        """
        k = self.reconstructor_type
        event.dl2.stereo.geometry[k] = self.reconstructor(event)
class CalibrationHDF5Writer(Tool):
    """
     Tool that generates a HDF5 file with camera calibration coefficients.
     This is just an example on how the monitoring containers can be filled using
     the calibration Components in calib/camera.
     This example is based on an input file with pedestal and flat-field events

     For getting help run:
     python calc_camera_calibration.py --help

     """

    name = "CalibrationHDF5Writer"
    description = "Generate a HDF5 file with camera calibration coefficients"

    one_event = Bool(
        False, help='Stop after first calibration event').tag(config=True)

    output_file = Unicode('calibration.hdf5',
                          help='Name of the output file').tag(config=True)

    calibration_product = traits.create_class_enum_trait(
        CalibrationCalculator, default_value='LSTCalibrationCalculator')

    events_to_skip = Int(
        1000,
        help='Number of first events to skip due to bad DRS4 pedestal correction'
    ).tag(config=True)

    mc_min_flatfield_adc = Float(
        2000,
        help=
        'Minimum high-gain camera median charge per pixel (ADC) for flatfield MC events'
    ).tag(config=True)

    mc_max_pedestal_adc = Float(
        300,
        help=
        'Maximum high-gain camera median charge per pixel (ADC) for pedestal MC events'
    ).tag(config=True)

    aliases = {
        ("i", "input_file"): 'EventSource.input_url',
        ("m", "max_events"): 'EventSource.max_events',
        ("o", "output_file"): 'CalibrationHDF5Writer.output_file',
        ("p", "pedestal_file"):
        "LSTEventSource.LSTR0Corrections.drs4_pedestal_path",
        ("s", "systematics_file"):
        "LSTCalibrationCalculator.systematic_correction_path",
        ("r", "run_summary_file"):
        "LSTEventSource.EventTimeCalculator.run_summary_path",
        ("t", "time_calibration_file"):
        "LSTEventSource.LSTR0Corrections.drs4_time_calibration_path",
        "events_to_skip": 'CalibrationHDF5Writer.events_to_skip',
    }

    flags = {
        **traits.flag(
            "flatfield-heuristic",
            "LSTEventSource.use_flatfield_heuristic",
            "Use flatfield heuristic",
            "Do not use flatfield heuristic",
        )
    }

    classes = ([EventSource, CalibrationCalculator] +
               traits.classes_with_traits(CalibrationCalculator) +
               traits.classes_with_traits(EventSource))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        """
        Tool that generates a HDF5 file with camera calibration coefficients.
        Input file must contain interleaved pedestal and flat-field events

        For getting help run:
            lstchain_create_calibration_file --help
        """
        self.eventsource = None
        self.processor = None
        self.writer = None
        self.tot_events = 0
        self.simulation = False

    def setup(self):

        self.log.debug("Opening file")
        self.eventsource = EventSource.from_config(parent=self)

        self.processor = CalibrationCalculator.from_name(
            self.calibration_product,
            parent=self,
            subarray=self.eventsource.subarray)

        tel_id = self.processor.tel_id

        # if real data
        if isinstance(self.eventsource, LSTEventSource):
            if tel_id != self.eventsource.lst_service.telescope_id:
                raise ValueError(
                    f"Events telescope_id {self.eventsource.lst_service.telescope_id} "
                    f"different than CalibrationCalculator telescope_id {tel_id}"
                )

            if self.eventsource.r0_r1_calibrator.drs4_pedestal_path.tel[
                    tel_id] is None:
                raise IOError(
                    "Missing (mandatory) drs4 pedestal file in trailets")

            # remember how many events in the files
            self.tot_events = len(self.eventsource.multi_file)
            self.log.debug(f"Input file has file {self.tot_events} events")
        else:
            self.tot_events = self.eventsource.max_events
            self.simulation = True

        group_name = 'tel_' + str(tel_id)

        self.log.debug(f"Open output file {self.output_file}")

        self.writer = HDF5TableWriter(filename=self.output_file,
                                      group_name=group_name,
                                      overwrite=True)

    def start(self):
        '''Calibration coefficient calculator'''

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

        tel_id = self.processor.tel_id
        new_ped = False
        new_ff = False
        end_of_file = False

        try:
            self.log.debug("Start loop")
            self.log.debug(
                f"If not simulation, skip first {self.events_to_skip} events")
            for count, event in enumerate(self.eventsource):

                # if simulation use not calibrated and not gain selected R0 waveform
                if self.simulation:
                    # estimate offset of each channel from the camera median pedestal value
                    offset = np.median(
                        event.mon.tel[tel_id].calibration.pedestal_per_sample,
                        axis=1).round()
                    event.r1.tel[tel_id].waveform = event.r0.tel[
                        tel_id].waveform.astype(
                            np.float32) - offset[:, np.newaxis, np.newaxis]

                if count % 1000 == 0 and count > self.events_to_skip:
                    self.log.debug(f"Event {count}")

                # if last event write results
                max_events_reached = (self.eventsource.max_events is not None
                                      and count
                                      == self.eventsource.max_events - 1)
                if count == self.tot_events - 1 or max_events_reached:
                    self.log.debug(f"Last event, count = {count}")
                    end_of_file = True

                # save the config, to be retrieved as data.meta['config']
                if count == 0:

                    if self.simulation:
                        initialize_pixel_status(
                            event.mon.tel[tel_id],
                            event.r1.tel[tel_id].waveform.shape)

                    ped_data = event.mon.tel[tel_id].pedestal
                    add_config_metadata(ped_data, self.config)
                    add_global_metadata(ped_data, metadata)

                    ff_data = event.mon.tel[tel_id].flatfield
                    add_config_metadata(ff_data, self.config)
                    add_global_metadata(ff_data, metadata)

                    status_data = event.mon.tel[tel_id].pixel_status
                    add_config_metadata(status_data, self.config)
                    add_global_metadata(status_data, metadata)

                    calib_data = event.mon.tel[tel_id].calibration
                    add_config_metadata(calib_data, self.config)
                    add_global_metadata(calib_data, metadata)

                # skip first events which are badly drs4 corrected
                if not self.simulation and count < self.events_to_skip:
                    continue

                # if pedestal event
                # use a cut on the charge for MC events if trigger not defined
                if event.trigger.event_type == EventType.SKY_PEDESTAL or (
                        self.simulation and np.median(
                            np.sum(event.r1.tel[tel_id].waveform[0],
                                   axis=1)) < self.mc_max_pedestal_adc):

                    new_ped = self.processor.pedestal.calculate_pedestals(
                        event)

                # if flat-field event
                # use a cut on the charge for MC events if trigger not defined
                elif event.trigger.event_type == EventType.FLATFIELD or (
                        self.simulation and np.median(
                            np.sum(event.r1.tel[tel_id].waveform[0],
                                   axis=1)) > self.mc_min_flatfield_adc):

                    new_ff = self.processor.flatfield.calculate_relative_gain(
                        event)

                # write pedestal results when enough statistics or end of file
                if new_ped or end_of_file:

                    # update the monitoring container with the last statistics
                    if end_of_file:
                        self.processor.pedestal.store_results(event)

                    # write the event
                    self.log.debug(
                        f"Write pedestal data at event n. {count+1}, id {event.index.event_id} "
                        f"stat = {ped_data.n_events} events")

                    # write on file
                    self.writer.write('pedestal', ped_data)

                    new_ped = False

                # write flatfield results when enough statistics (also for pedestals) or end of file
                if (new_ff and ped_data.n_events > 0) or end_of_file:

                    # update the monitoring container with the last statistics
                    if end_of_file:
                        self.processor.flatfield.store_results(event)

                    self.log.debug(
                        f"Write flatfield data at event n. {count+1}, id {event.index.event_id} "
                        f"stat = {ff_data.n_events} events")

                    # write on file
                    self.writer.write('flatfield', ff_data)

                    new_ff = False

                    # Then, calculate calibration coefficients
                    self.processor.calculate_calibration_coefficients(event)

                    # write calib and pixel status
                    self.log.debug("Write pixel_status data")
                    self.writer.write('pixel_status', status_data)

                    self.log.debug("Write calibration data")
                    self.writer.write('calibration', calib_data)
                    if self.one_event:
                        break

        except ValueError as e:
            self.log.error(e)

    def finish(self):
        Provenance().add_output_file(self.output_file,
                                     role='mon.tel.calibration')
        self.writer.close()
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}")
Exemplo n.º 6
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}")
Exemplo n.º 7
0
class TailCutsDataVolumeReducer(DataVolumeReducer):
    """
    Reduce the time integrated shower image in 3 Steps:

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

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

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

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

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

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

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

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

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

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

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

        return mask
Exemplo n.º 8
0
class BokehFileViewer(Tool):
    name = "BokehFileViewer"
    description = ("Interactively explore an event file using the bokeh "
                   "visualisation package")

    port = Int(5006, help="Port to open bokeh server onto").tag(config=True)
    disable_server = Bool(False,
                          help="Do not start the bokeh server "
                          "(useful for testing)").tag(config=True)

    default_url = get_dataset_path("gamma_test_large.simtel.gz")
    EventSource.input_url.default_value = default_url

    extractor_product = traits.create_class_enum_trait(
        ImageExtractor, default_value="NeighborPeakWindowSum")

    aliases = Dict(
        dict(
            port="BokehFileViewer.port",
            disable_server="BokehFileViewer.disable_server",
            f="EventSource.input_url",
            max_events="EventSource.max_events",
            extractor="BokehFileViewer.extractor_product",
        ))

    classes = [EventSource] + traits.classes_with_traits(ImageExtractor)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._event = None
        self._event_index = None
        self._event_id = None
        self._telid = None
        self._channel = None

        self.w_next_event = None
        self.w_previous_event = None
        self.w_event_index = None
        self.w_event_id = None
        self.w_goto_event_index = None
        self.w_goto_event_id = None
        self.w_telid = None
        self.w_channel = None
        self.w_dl1_dict = None
        self.wb_extractor = None
        self.layout = None

        self.reader = None
        self.seeker = None
        self.extractor = None
        self.calibrator = None
        self.viewer = None

        self._updating_dl1 = False
        # make sure, gzip files are seekable
        self.config.SimTelEventSource.back_seekable = True

    def setup(self):
        self.log_format = "%(levelname)s: %(message)s [%(name)s.%(funcName)s]"

        self.reader = EventSource(parent=self)
        self.seeker = EventSeeker(self.reader, parent=self)

        self.extractor = ImageExtractor.from_name(
            self.extractor_product, parent=self, subarray=self.reader.subarray)
        self.calibrator = CameraCalibrator(subarray=self.reader.subarray,
                                           parent=self,
                                           image_extractor=self.extractor)

        self.viewer = BokehEventViewer(parent=self,
                                       subarray=self.reader.subarray)

        # Setup widgets
        self.viewer.create()
        self.viewer.enable_automatic_index_increment()
        self.create_previous_event_widget()
        self.create_next_event_widget()
        self.create_event_index_widget()
        self.create_goto_event_index_widget()
        self.create_event_id_widget()
        self.create_goto_event_id_widget()
        self.create_telid_widget()
        self.create_channel_widget()
        self.create_dl1_widgets()
        self.update_dl1_widget_values()

        # Setup layout
        self.layout = layout([
            [self.viewer.layout],
            [
                self.w_previous_event,
                self.w_next_event,
                self.w_goto_event_index,
                self.w_goto_event_id,
            ],
            [self.w_event_index, self.w_event_id],
            [self.w_telid, self.w_channel],
            [self.wb_extractor],
        ])

    def start(self):
        self.event_index = 0

    def finish(self):
        if not self.disable_server:

            def modify_doc(doc):
                doc.add_root(self.layout)
                doc.title = self.name

                directory = os.path.abspath(os.path.dirname(__file__))
                theme_path = os.path.join(directory, "theme.yaml")
                template_path = os.path.join(directory, "templates")
                doc.theme = Theme(filename=theme_path)
                env = jinja2.Environment(
                    loader=jinja2.FileSystemLoader(template_path))
                doc.template = env.get_template("index.html")

            self.log.info("Opening Bokeh application on "
                          "http://localhost:{}/".format(self.port))
            server = Server({"/": modify_doc}, num_procs=1, port=self.port)
            server.start()
            server.io_loop.add_callback(server.show, "/")
            server.io_loop.start()

    @property
    def event_index(self):
        return self._event_index

    @event_index.setter
    def event_index(self, val):
        try:
            self.event = self.seeker.get_event_index(val)
        except IndexError:
            self.log.warning(f"Event Index {val} does not exist")

    @property
    def event_id(self):
        return self._event_id

    @event_id.setter
    def event_id(self, val):
        try:
            self.event = self.seeker.get_event_id(val)
        except IndexError:
            self.log.warning(f"Event ID {val} does not exist")

    @property
    def telid(self):
        return self._telid

    @telid.setter
    def telid(self, val):
        self.channel = 0
        tels = list(self.event.r0.tel.keys())
        if val not in tels:
            val = tels[0]
        self._telid = val
        self.viewer.telid = val
        self.update_telid_widget()

    @property
    def channel(self):
        return self._channel

    @channel.setter
    def channel(self, val):
        self._channel = val
        self.viewer.channel = val
        self.update_channel_widget()

    @property
    def event(self):
        return self._event

    @event.setter
    def event(self, val):
        self.calibrator(val)

        self._event = val

        self.viewer.event = val

        self._event_index = val.count
        self._event_id = val.index.event_id
        self.update_event_index_widget()
        self.update_event_id_widget()

        self._telid = self.viewer.telid
        self.update_telid_widget()

        self._channel = self.viewer.channel
        self.update_channel_widget()

    def update_dl1_calibrator(self, extractor=None):
        """
        Recreate the dl1 calibrator with the specified extractor and cleaner

        Parameters
        ----------
        extractor : ctapipe.image.extractor.ImageExtractor
        """
        if extractor is None:
            extractor = self.calibrator.image_extractor

        self.extractor = extractor

        self.calibrator = CameraCalibrator(subarray=self.reader.subarray,
                                           parent=self,
                                           image_extractor=self.extractor)
        self.viewer.refresh()

    def create_next_event_widget(self):
        self.w_next_event = Button(label=">", button_type="default", width=50)
        self.w_next_event.on_click(self.on_next_event_widget_click)

    def on_next_event_widget_click(self):
        self.event_index += 1

    def create_previous_event_widget(self):
        self.w_previous_event = Button(label="<",
                                       button_type="default",
                                       width=50)
        self.w_previous_event.on_click(self.on_previous_event_widget_click)

    def on_previous_event_widget_click(self):
        self.event_index -= 1

    def create_event_index_widget(self):
        self.w_event_index = TextInput(title="Event Index:", value="")

    def update_event_index_widget(self):
        if self.w_event_index:
            self.w_event_index.value = str(self.event_index)

    def create_event_id_widget(self):
        self.w_event_id = TextInput(title="Event ID:", value="")

    def update_event_id_widget(self):
        if self.w_event_id:
            self.w_event_id.value = str(self.event_id)

    def create_goto_event_index_widget(self):
        self.w_goto_event_index = Button(label="GOTO Index",
                                         button_type="default",
                                         width=100)
        self.w_goto_event_index.on_click(self.on_goto_event_index_widget_click)

    def on_goto_event_index_widget_click(self):
        self.event_index = int(self.w_event_index.value)

    def create_goto_event_id_widget(self):
        self.w_goto_event_id = Button(label="GOTO ID",
                                      button_type="default",
                                      width=70)
        self.w_goto_event_id.on_click(self.on_goto_event_id_widget_click)

    def on_goto_event_id_widget_click(self):
        self.event_id = int(self.w_event_id.value)

    def create_telid_widget(self):
        self.w_telid = Select(title="Telescope:", value="", options=[])
        self.w_telid.on_change("value", self.on_telid_widget_change)

    def update_telid_widget(self):
        if self.w_telid:
            tels = [str(t) for t in self.event.r0.tel.keys()]
            self.w_telid.options = tels
            self.w_telid.value = str(self.telid)

    def on_telid_widget_change(self, _, __, ___):
        if self.telid != int(self.w_telid.value):
            self.telid = int(self.w_telid.value)

    def create_channel_widget(self):
        self.w_channel = Select(title="Channel:", value="", options=[])
        self.w_channel.on_change("value", self.on_channel_widget_change)

    def update_channel_widget(self):
        if self.w_channel:
            try:
                n_chan = self.event.r0.tel[self.telid].waveform.shape[0]
            except AttributeError:
                n_chan = 1
            channels = [str(c) for c in range(n_chan)]
            self.w_channel.options = channels
            self.w_channel.value = str(self.channel)

    def on_channel_widget_change(self, _, __, ___):
        if self.channel != int(self.w_channel.value):
            self.channel = int(self.w_channel.value)

    def create_dl1_widgets(self):
        self.w_dl1_dict = dict(
            extractor=Select(
                title="Extractor:",
                value="",
                width=5,
                options=BokehFileViewer.extractor_product.values,
            ),
            extractor_window_start=TextInput(title="Window Start:", value=""),
            extractor_window_width=TextInput(title="Window Width:", value=""),
            extractor_window_shift=TextInput(title="Window Shift:", value=""),
            extractor_lwt=TextInput(title="Local Pixel Weight:", value=""),
        )

        for val in self.w_dl1_dict.values():
            val.on_change("value", self.on_dl1_widget_change)

        self.wb_extractor = widgetbox(
            PreText(text="Charge Extractor Configuration"),
            self.w_dl1_dict["extractor"],
            self.w_dl1_dict["extractor_window_start"],
            self.w_dl1_dict["extractor_window_width"],
            self.w_dl1_dict["extractor_window_shift"],
            self.w_dl1_dict["extractor_lwt"],
        )

    def update_dl1_widget_values(self):
        if self.w_dl1_dict:
            for key, val in self.w_dl1_dict.items():
                if "extractor" in key:
                    if key == "extractor":
                        val.value = self.extractor.__class__.__name__
                    else:
                        key = key.replace("extractor_", "")
                        try:
                            val.value = str(getattr(self.extractor, key))
                        except AttributeError:
                            val.value = ""

    def on_dl1_widget_change(self, _, __, ___):
        if self.event:
            if not self._updating_dl1:
                self._updating_dl1 = True
                cmdline = []
                for key, val in self.w_dl1_dict.items():
                    k = key.replace("extractor_", "ImageExtractor.")
                    if val.value:
                        cmdline.append(f"--{k}={val.value}")
                self.parse_command_line(cmdline)
                extractor = ImageExtractor.from_name(self.extractor_product,
                                                     parent=self)
                self.update_dl1_calibrator(extractor)
                self.update_dl1_widget_values()
                self._updating_dl1 = False
class CalibrationHDF5Writer(Tool):
    """
     Tool that generates a HDF5 file with camera calibration coefficients.
     This is just an example on how the monitoring containers can be filled using
     the calibration Components in calib/camera.
     This example is based on an input file with pedestal and flat-field events

     For getting help run:
     python calc_camera_calibration.py --help

     """

    name = "CalibrationHDF5Writer"
    description = "Generate a HDF5 file with camera calibration coefficients"

    one_event = Bool(
        False, help='Stop after first calibration event').tag(config=True)

    output_file = Unicode('calibration.hdf5',
                          help='Name of the output file').tag(config=True)

    log_file = Unicode('None', help='Name of the log file').tag(config=True)

    calibration_product = traits.create_class_enum_trait(
        CalibrationCalculator, default_value='LSTCalibrationCalculator')

    r0calibrator_product = traits.create_class_enum_trait(
        CameraR0Calibrator, default_value='NullR0Calibrator')

    aliases = Dict(
        dict(
            input_file='EventSource.input_url',
            output_file='CalibrationHDF5Writer.output_file',
            log_file='CalibrationHDF5Writer.log_file',
            max_events='EventSource.max_events',
            pedestal_file='LSTR0Corrections.pedestal_path',
            calibration_product='CalibrationHDF5Writer.calibration_product',
            r0calibrator_product='CalibrationHDF5Writer.r0calibrator_product',
        ))

    classes = List([EventSource, CalibrationCalculator] +
                   traits.classes_with_traits(CameraR0Calibrator) +
                   traits.classes_with_traits(CalibrationCalculator))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        """
         Tool that generates a HDF5 file with camera calibration coefficients.
         Input file must contain interleaved pedestal and flat-field events  

         For getting help run:
         python calc_camera_calibration.py --help
         
        """
        self.eventsource = None
        self.processor = None
        self.container = None
        self.writer = None
        self.r0calibrator = None
        self.tot_events = 0
        self.simulation = False

    def setup(self):

        self.log.debug(f"Open  file")
        self.eventsource = EventSource.from_config(parent=self)

        # if data remember how many event in the files
        if "LSTEventSource" in str(type(self.eventsource)):
            self.tot_events = len(self.eventsource.multi_file)
            self.log.debug(f"Input file has file {self.tot_events} events")
        else:
            self.tot_events = self.eventsource.max_events
            self.simulation = True

        self.processor = CalibrationCalculator.from_name(
            self.calibration_product,
            parent=self,
            subarray=self.eventsource.subarray)

        if self.r0calibrator_product:
            self.r0calibrator = CameraR0Calibrator.from_name(
                self.r0calibrator_product, parent=self)

        group_name = 'tel_' + str(self.processor.tel_id)

        self.log.debug(f"Open output file {self.output_file}")

        self.writer = HDF5TableWriter(filename=self.output_file,
                                      group_name=group_name,
                                      overwrite=True)

    def start(self):
        '''Calibration coefficient calculator'''

        tel_id = self.processor.tel_id
        new_ped = False
        new_ff = False
        end_of_file = False

        # skip the first events which are badly drs4 corrected
        events_to_skip = 1000

        try:
            self.log.debug(f"Start loop")
            for count, event in enumerate(self.eventsource):

                if count % 100 == 0:
                    self.log.debug(f"Event {count}")

                # if last event write results
                max_events_reached = (self.eventsource.max_events is not None
                                      and count
                                      == self.eventsource.max_events - 1)
                if count == self.tot_events - 1 or max_events_reached:
                    self.log.debug(f"Last event, count = {count}")
                    end_of_file = True

                # save the config, to be retrieved as data.meta['config']
                if count == 0:

                    if self.simulation:
                        initialize_pixel_status(
                            event.mon.tel[tel_id],
                            event.r1.tel[tel_id].waveform.shape)

                    ped_data = event.mon.tel[tel_id].pedestal
                    ped_data.meta['config'] = self.config

                    ff_data = event.mon.tel[tel_id].flatfield
                    ff_data.meta['config'] = self.config

                    status_data = event.mon.tel[tel_id].pixel_status
                    status_data.meta['config'] = self.config

                    calib_data = event.mon.tel[tel_id].calibration
                    calib_data.meta['config'] = self.config

                # correct for low level calibration
                self.r0calibrator.calibrate(event)

                # skip first events which are badly drs4 corrected
                if not self.simulation and count < events_to_skip:
                    continue

                # reject event without trigger type
                if LSTEventType.is_unknown(event.r1.tel[tel_id].trigger_type):
                    continue

                # if pedestal event
                if LSTEventType.is_pedestal(
                        event.r1.tel[tel_id].trigger_type
                ) or (self.simulation and np.median(
                        np.sum(event.r1.tel[tel_id].waveform[0], axis=1)) <
                      self.processor.minimum_hg_charge_median):
                    new_ped = self.processor.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[tel_id].trigger_type
                ) or (np.median(
                        np.sum(event.r1.tel[tel_id].waveform[0], axis=1)) >
                      self.processor.minimum_hg_charge_median and
                      np.std(np.sum(event.r1.tel[tel_id].waveform[1], axis=1))
                      < self.processor.maximum_lg_charge_std):

                    new_ff = self.processor.flatfield.calculate_relative_gain(
                        event)
                # write pedestal results when enough statistics or end of file
                if new_ped or end_of_file:

                    # update the monitoring container with the last statistics
                    if end_of_file:
                        self.processor.pedestal.store_results(event)

                    # write the event
                    self.log.debug(
                        f"Write pedestal data at event n. {count+1}, id {event.index.event_id} "
                        f"stat = {ped_data.n_events} events")

                    # write on file
                    self.writer.write('pedestal', ped_data)

                    new_ped = False

                # write flatfield results when enough statistics (also for pedestals) or end of file
                if (new_ff and ped_data.n_events > 0) or end_of_file:

                    # update the monitoring container with the last statistics
                    if end_of_file:
                        self.processor.flatfield.store_results(event)

                    self.log.debug(
                        f"Write flatfield data at event n. {count+1}, id {event.index.event_id} "
                        f"stat = {ff_data.n_events} events")

                    # write on file
                    self.writer.write('flatfield', ff_data)

                    new_ff = False

                    # Then, calculate calibration coefficients
                    self.processor.calculate_calibration_coefficients(event)

                    # write calib and pixel status
                    self.log.debug(f"Write pixel_status data")
                    self.writer.write('pixel_status', status_data)

                    self.log.debug(f"Write calibration data")
                    self.writer.write('calibration', calib_data)
                    if self.one_event:
                        break

                    #self.writer.write('mon', event.mon.tel[tel_id])
        except ValueError as e:
            self.log.error(e)

    def finish(self):
        Provenance().add_output_file(self.output_file,
                                     role='mon.tel.calibration')
        self.writer.close()
Exemplo n.º 10
0
class CameraCalibrator(TelescopeComponent):
    """
    Calibrator to handle the full camera calibration chain, in order to fill
    the DL1 data level in the event container.

    Attributes
    ----------
    data_volume_reducer_type: str
        The name of the DataVolumeReducer subclass to be used
        for data volume reduction

    image_extractor_type: str
        The name of the ImageExtractor subclass to be used for image extraction
    """

    data_volume_reducer_type = create_class_enum_trait(
        DataVolumeReducer,
        default_value="NullDataVolumeReducer").tag(config=True)

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

    apply_waveform_time_shift = BoolTelescopeParameter(
        default_value=False,
        help=("Apply waveform time shift corrections."
              " The minimal integer shift to synchronize waveforms is applied"
              " before peak extraction if this option is True"),
    ).tag(config=True)

    apply_peak_time_shift = BoolTelescopeParameter(
        default_value=True,
        help=
        ("Apply peak time shift corrections."
         " Apply the remaining absolute and fractional time shift corrections"
         " to the peak time after pulse extraction."
         " If `apply_waveform_time_shift` is False, this will apply the full time shift"
         ),
    ).tag(config=True)

    def __init__(
        self,
        subarray,
        config=None,
        parent=None,
        image_extractor=None,
        data_volume_reducer=None,
        **kwargs,
    ):
        """
        Parameters
        ----------
        subarray: ctapipe.instrument.SubarrayDescription
            Description of the subarray. Provides information about the
            camera which are useful in calibration. Also required for
            configuring the TelescopeParameter traitlets.
        config: traitlets.loader.Config
            Configuration specified by config file or cmdline arguments.
            Used to set traitlet values.
            This is mutually exclusive with passing a ``parent``.
        parent: ctapipe.core.Component or ctapipe.core.Tool
            Parent of this component in the configuration hierarchy,
            this is mutually exclusive with passing ``config``
        data_volume_reducer: ctapipe.image.reducer.DataVolumeReducer
            The DataVolumeReducer to use.
            This is used to override the options from the config system
            and to enable passing a preconfigured reducer.
        image_extractor: ctapipe.image.extractor.ImageExtractor
            The ImageExtractor to use. If None, the default via the
            configuration system will be constructed.
        """
        super().__init__(subarray=subarray,
                         config=config,
                         parent=parent,
                         **kwargs)
        self.subarray = subarray

        self._r1_empty_warn = False
        self._dl0_empty_warn = False

        self.image_extractors = {}

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

        if data_volume_reducer is None:
            self.data_volume_reducer = DataVolumeReducer.from_name(
                self.data_volume_reducer_type,
                subarray=self.subarray,
                parent=self)
        else:
            self.data_volume_reducer = data_volume_reducer

    def _check_r1_empty(self, waveforms):
        if waveforms is None:
            if not self._r1_empty_warn:
                warnings.warn("Encountered an event with no R1 data. "
                              "DL0 is unchanged in this circumstance.")
                self._r1_empty_warn = True
            return True
        else:
            return False

    def _check_dl0_empty(self, waveforms):
        if waveforms is None:
            if not self._dl0_empty_warn:
                warnings.warn("Encountered an event with no DL0 data. "
                              "DL1 is unchanged in this circumstance.")
                self._dl0_empty_warn = True
            return True
        else:
            return False

    def _calibrate_dl0(self, event, telid):
        waveforms = event.r1.tel[telid].waveform
        selected_gain_channel = event.r1.tel[telid].selected_gain_channel
        if self._check_r1_empty(waveforms):
            return

        reduced_waveforms_mask = self.data_volume_reducer(
            waveforms,
            telid=telid,
            selected_gain_channel=selected_gain_channel)

        waveforms_copy = waveforms.copy()
        waveforms_copy[~reduced_waveforms_mask] = 0
        event.dl0.tel[telid].waveform = waveforms_copy
        event.dl0.tel[telid].selected_gain_channel = selected_gain_channel

    def _calibrate_dl1(self, event, telid):
        waveforms = event.dl0.tel[telid].waveform
        selected_gain_channel = event.dl0.tel[telid].selected_gain_channel
        dl1_calib = event.calibration.tel[telid].dl1

        if self._check_dl0_empty(waveforms):
            return

        selected_gain_channel = event.r1.tel[telid].selected_gain_channel
        time_shift = event.calibration.tel[telid].dl1.time_shift
        readout = self.subarray.tel[telid].camera.readout
        n_pixels, n_samples = waveforms.shape

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

        if n_samples == 1:
            # To handle ASTRI and dst
            # TODO: Improved handling of ASTRI and dst
            #   - dst with custom EventSource?
            #   - Read into dl1 container directly?
            #   - Don't do anything if dl1 container already filled
            #   - Update on SST review decision
            charge = waveforms[..., 0].astype(np.float32)
            peak_time = np.zeros(n_pixels, dtype=np.float32)
        else:

            # shift waveforms if time_shift calibration is available
            if time_shift is not None:
                if self.apply_waveform_time_shift.tel[telid]:
                    sampling_rate = readout.sampling_rate.to_value(u.GHz)
                    time_shift_samples = time_shift * sampling_rate
                    waveforms, remaining_shift = shift_waveforms(
                        waveforms, time_shift_samples)
                    remaining_shift /= sampling_rate
                else:
                    remaining_shift = time_shift

            extractor = self.image_extractors[
                self.image_extractor_type.tel[telid]]
            charge, peak_time = extractor(
                waveforms,
                telid=telid,
                selected_gain_channel=selected_gain_channel)

            # correct non-integer remainder of the shift if given
            if self.apply_peak_time_shift.tel[telid] and time_shift is not None:
                peak_time -= remaining_shift

        # Calibrate extracted charge
        charge *= dl1_calib.relative_factor / dl1_calib.absolute_factor

        event.dl1.tel[telid].image = charge
        event.dl1.tel[telid].peak_time = peak_time

    def __call__(self, event):
        """
        Perform the full camera calibration from R1 to DL1. Any calibration
        relating to data levels before the data level the file is read into
        will be skipped.

        Parameters
        ----------
        event : container
            A `~ctapipe.containers.ArrayEventContainer` event container
        """
        # TODO: How to handle different calibrations depending on telid?
        tel = event.r1.tel or event.dl0.tel or event.dl1.tel
        for telid in tel.keys():
            self._calibrate_dl0(event, telid)
            self._calibrate_dl1(event, telid)
Exemplo n.º 11
0
class ChargeResolutionGenerator(Tool):
    name = "ChargeResolutionGenerator"
    description = ("Calculate the Charge Resolution from a sim_telarray "
                   "simulation and store within a HDF5 file.")

    telescopes = List(
        Int(),
        None,
        allow_none=True,
        help=
        "Telescopes to include from the event file. Default = All telescopes",
    ).tag(config=True)
    output_path = Path(
        default_value="charge_resolution.h5",
        directory_ok=False,
        help="Path to store the output HDF5 file",
    ).tag(config=True)
    extractor_product = traits.create_class_enum_trait(
        ImageExtractor, default_value="NeighborPeakWindowSum")

    aliases = Dict(
        dict(
            f="SimTelEventSource.input_url",
            max_events="SimTelEventSource.max_events",
            T="SimTelEventSource.allowed_tels",
            extractor="ChargeResolutionGenerator.extractor_product",
            O="ChargeResolutionGenerator.output_path",
        ))

    classes = List([SimTelEventSource] +
                   traits.classes_with_traits(ImageExtractor))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.eventsource = None
        self.calibrator = None
        self.calculator = None

    def setup(self):
        self.log_format = "%(levelname)s: %(message)s [%(name)s.%(funcName)s]"

        self.eventsource = self.add_component(SimTelEventSource(parent=self))

        extractor = self.add_component(
            ImageExtractor.from_name(
                self.extractor_product,
                parent=self,
                subarray=self.eventsource.subarray,
            ))

        self.calibrator = self.add_component(
            CameraCalibrator(
                parent=self,
                image_extractor=extractor,
                subarray=self.eventsource.subarray,
            ))
        self.calculator = ChargeResolutionCalculator()

    def start(self):
        desc = "Extracting Charge Resolution"
        for event in tqdm(self.eventsource, desc=desc):
            self.calibrator(event)

            # Check events have true charge included
            if event.count == 0:
                try:
                    pe = list(event.mc.tel.values())[0].true_image
                    if np.all(pe == 0):
                        raise KeyError
                except KeyError:
                    self.log.exception("Source does not contain true charge!")
                    raise

            for mc, dl1 in zip(event.mc.tel.values(), event.dl1.tel.values()):
                true_charge = mc.true_image
                measured_charge = dl1.image
                pixels = np.arange(measured_charge.size)
                self.calculator.add(pixels, true_charge, measured_charge)

    def finish(self):
        df_p, df_c = self.calculator.finish()

        output_directory = os.path.dirname(self.output_path)
        if not os.path.exists(output_directory):
            self.log.info(f"Creating directory: {output_directory}")
            os.makedirs(output_directory)

        with pd.HDFStore(self.output_path, "w") as store:
            store["charge_resolution_pixel"] = df_p
            store["charge_resolution_camera"] = df_c

        self.log.info("Created charge resolution file: {}".format(
            self.output_path))
        Provenance().add_output_file(self.output_path)
Exemplo n.º 12
0
class DisplayIntegrator(Tool):
    name = "ctapipe-display-integration"
    description = __doc__

    event_index = Int(0, help="Event index to view.").tag(config=True)
    use_event_id = Bool(
        False,
        help=
        "event_index will obtain an event using event_id instead of index.",
    ).tag(config=True)
    telescope = Int(
        None,
        allow_none=True,
        help="Telescope to view. Set to None to display the first"
        "telescope with data.",
    ).tag(config=True)
    channel = Enum([0, 1], 0, help="Channel to view").tag(config=True)

    extractor_product = traits.create_class_enum_trait(
        ImageExtractor, default_value="NeighborPeakWindowSum")

    aliases = Dict(
        dict(
            f="EventSource.input_url",
            max_events="EventSource.max_events",
            extractor="DisplayIntegrator.extractor_product",
            E="DisplayIntegrator.event_index",
            T="DisplayIntegrator.telescope",
            C="DisplayIntegrator.channel",
        ))
    flags = Dict(
        dict(id=(
            {
                "DisplayDL1Calib": {
                    "use_event_index": True
                }
            },
            "event_index will obtain an event using event_id instead of index.",
        )))
    classes = List([EventSource] + traits.classes_with_traits(ImageExtractor))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # make sure gzip files are seekable
        self.config.SimTelEventSource.back_seekable = True
        self.eventseeker = None
        self.extractor = None
        self.calibrator = None

    def setup(self):
        self.log_format = "%(levelname)s: %(message)s [%(name)s.%(funcName)s]"

        event_source = self.add_component(EventSource.from_config(parent=self))
        self.subarray = event_source.subarray
        self.eventseeker = self.add_component(
            EventSeeker(event_source, parent=self))
        self.extractor = self.add_component(
            ImageExtractor.from_name(self.extractor_product,
                                     parent=self,
                                     subarray=self.subarray))
        self.calibrate = self.add_component(
            CameraCalibrator(parent=self,
                             image_extractor=self.extractor,
                             subarray=self.subarray))

    def start(self):
        event_num = self.event_index
        if self.use_event_id:
            event_num = str(event_num)
        event = self.eventseeker[event_num]

        # Calibrate
        self.calibrate(event)

        # Select telescope
        tels = list(event.r0.tels_with_data)
        telid = self.telescope
        if telid is None:
            telid = tels[0]
        if telid not in tels:
            self.log.error("[event] please specify one of the following "
                           "telescopes for this event: {}".format(tels))
            exit()

        extractor_name = self.extractor.__class__.__name__

        plot(self.subarray, event, telid, self.channel, extractor_name)

    def finish(self):
        pass
Exemplo n.º 13
0
class MuonAnalysis(Tool):
    """
    Detect and extract muon ring parameters, and write the muon ring and
    intensity parameters to an output table.

    The resulting output can be read e.g. using for example
    `pandas.read_hdf(filename, 'dl1/event/telescope/parameters/muon')`
    """
    name = 'ctapipe-reconstruct-muons'
    description = traits.Unicode(__doc__)

    output = traits.Path(directory_ok=False,
                         help='HDF5 output file name').tag(config=True)

    completeness_threshold = traits.FloatTelescopeParameter(
        default_value=30.0,
        help='Threshold for calculating the ``ring_completeness``',
    ).tag(config=True)

    ratio_width = traits.FloatTelescopeParameter(
        default_value=1.5,
        help=('Ring width for intensity ratio'
              ' computation as multiple of pixel diameter')).tag(config=True)

    overwrite = traits.Bool(
        default_value=False,
        help='If true, overwrite outputfile without asking').tag(config=True)

    min_pixels = traits.IntTelescopeParameter(
        help=('Minimum number of pixels after cleaning and ring finding'
              'required to process an event'),
        default_value=100,
    ).tag(config=True)

    pedestal = traits.FloatTelescopeParameter(
        help='Pedestal noise rms',
        default_value=1.1,
    ).tag(config=True)

    extractor_name = traits.create_class_enum_trait(
        ImageExtractor,
        default_value='GlobalPeakWindowSum',
    ).tag(config=True)

    classes = [
        CameraCalibrator,
        TailcutsImageCleaner,
        EventSource,
        MuonRingFitter,
        MuonIntensityFitter,
    ] + traits.classes_with_traits(ImageExtractor)

    aliases = {
        'i': 'EventSource.input_url',
        'input': 'EventSource.input_url',
        'o': 'MuonAnalysis.output',
        'output': 'MuonAnalysis.output',
        'max-events': 'EventSource.max_events',
        'allowed-tels': 'EventSource.allowed_tels',
    }

    flags = {
        'overwrite': ({
            'MuonAnalysis': {
                'overwrite': True
            }
        }, 'overwrite output file')
    }

    def setup(self):
        if self.output is None:
            raise ToolConfigurationError(
                'You need to provide an --output file')

        if self.output.exists() and not self.overwrite:
            raise ToolConfigurationError(
                'Outputfile {self.output} already exists, use `--overwrite` to overwrite'
            )

        self.source = self.add_component(EventSource.from_config(parent=self))
        self.extractor = self.add_component(
            ImageExtractor.from_name(self.extractor_name,
                                     parent=self,
                                     subarray=self.source.subarray))
        self.calib = self.add_component(
            CameraCalibrator(
                subarray=self.source.subarray,
                parent=self,
                image_extractor=self.extractor,
            ))
        self.ring_fitter = self.add_component(MuonRingFitter(parent=self, ))
        self.intensity_fitter = self.add_component(
            MuonIntensityFitter(
                subarray=self.source.subarray,
                parent=self,
            ))
        self.cleaning = self.add_component(
            TailcutsImageCleaner(
                parent=self,
                subarray=self.source.subarray,
            ))
        self.writer = self.add_component(
            HDF5TableWriter(
                self.output,
                "",
                add_prefix=True,
                parent=self,
                mode='w',
            ))
        self.pixels_in_tel_frame = {}
        self.field_of_view = {}
        self.pixel_widths = {}

        for p in [
                'min_pixels', 'pedestal', 'ratio_width',
                'completeness_threshold'
        ]:
            getattr(self, p).attach_subarray(self.source.subarray)

    def start(self):
        for event in tqdm(self.source, desc='Processing events: '):
            self.process_array_event(event)

    def process_array_event(self, event):
        self.calib(event)

        for tel_id, dl1 in event.dl1.tel.items():
            self.process_telescope_event(event.index, tel_id, dl1)

        self.writer.write('sim/event/subarray/shower', [event.index, event.mc])

    def process_telescope_event(self, event_index, tel_id, dl1):
        event_id = event_index.event_id

        if self.source.subarray.tel[tel_id].optics.num_mirrors != 1:
            self.log.warn(f'Skipping non-single mirror telescope {tel_id}'
                          ' set --allowed_tels to get rid of this warning')
            return

        self.log.debug(f'Processing event {event_id}, telescope {tel_id}')
        image = dl1.image
        clean_mask = self.cleaning(tel_id, image)

        if np.count_nonzero(clean_mask) <= self.min_pixels.tel[tel_id]:
            self.log.debug(
                f'Skipping event {event_id}-{tel_id}:'
                f' has less then {self.min_pixels.tel[tel_id]} pixels after cleaning'
            )
            return

        x, y = self.get_pixel_coords(tel_id)

        # iterative ring fit.
        # First use cleaning pixels, then only pixels close to the ring
        # three iterations seems to be enough for most rings
        mask = clean_mask
        for i in range(3):
            ring = self.ring_fitter(x, y, image, mask)
            dist = np.sqrt((x - ring.center_x)**2 + (y - ring.center_y)**2)
            mask = np.abs(dist - ring.radius) / ring.radius < 0.4

        if np.count_nonzero(mask) <= self.min_pixels.tel[tel_id]:
            self.log.debug(
                f'Skipping event {event_id}-{tel_id}:'
                f' Less then {self.min_pixels.tel[tel_id]} pixels on ring')
            return

        if np.isnan(
            [ring.radius.value, ring.center_x.value,
             ring.center_y.value]).any():
            self.log.debug(
                f'Skipping event {event_id}-{tel_id}: Ring fit did not succeed'
            )
            return

        parameters = self.calculate_muon_parameters(tel_id, image, clean_mask,
                                                    ring)

        # intensity_fitter does not support a mask yet, set ignored pixels to 0
        image[~mask] = 0

        result = self.intensity_fitter(
            tel_id,
            ring.center_x,
            ring.center_y,
            ring.radius,
            image,
            pedestal=self.pedestal.tel[tel_id],
        )

        self.log.info(
            f'Muon fit: r={ring.radius:.2f}'
            f', width={result.width:.4f}'
            f', efficiency={result.optical_efficiency:.2%}', )

        tel_event_index = TelEventIndexContainer(
            **event_index,
            tel_id=tel_id,
        )

        self.writer.write('dl1/event/telescope/parameters/muons',
                          [tel_event_index, ring, parameters, result])

    def calculate_muon_parameters(self, tel_id, image, clean_mask, ring):
        fov_radius = self.get_fov(tel_id)
        x, y = self.get_pixel_coords(tel_id)

        # add ring containment, not filled in fit
        containment = ring_containment(
            ring.radius,
            ring.center_x,
            ring.center_y,
            fov_radius,
        )

        completeness = ring_completeness(
            x,
            y,
            image,
            ring.radius,
            ring.center_x,
            ring.center_y,
            threshold=self.completeness_threshold.tel[tel_id],
        )

        pixel_width = self.get_pixel_width(tel_id)
        intensity_ratio = intensity_ratio_inside_ring(
            x[clean_mask],
            y[clean_mask],
            image[clean_mask],
            ring.radius,
            ring.center_x,
            ring.center_y,
            width=self.ratio_width.tel[tel_id] * pixel_width,
        )

        mse = mean_squared_error(x[clean_mask], y[clean_mask],
                                 image[clean_mask], ring.radius, ring.center_x,
                                 ring.center_y)

        return MuonParametersContainer(
            containment=containment,
            completeness=completeness,
            intensity_ratio=intensity_ratio,
            mean_squared_error=mse,
        )

    def get_fov(self, tel_id):
        '''Guesstimate fov radius for telescope with id `tel_id`'''
        # memoize fov calculation
        if tel_id not in self.field_of_view:
            cam = self.source.subarray.tel[tel_id].camera.geometry
            border = cam.get_border_pixel_mask()

            x, y = self.get_pixel_coords(tel_id)
            self.field_of_view[tel_id] = np.sqrt(x[border]**2 +
                                                 y[border]**2).mean()

        return self.field_of_view[tel_id]

    def get_pixel_width(self, tel_id):
        '''Guesstimate fov radius for telescope with id `tel_id`'''
        # memoize fov calculation
        if tel_id not in self.pixel_widths:
            x, y = self.get_pixel_coords(tel_id)
            self.pixel_widths[tel_id] = CameraGeometry.guess_pixel_width(x, y)

        return self.pixel_widths[tel_id]

    def get_pixel_coords(self, tel_id):
        '''Get pixel coords in telescope frame for telescope with id `tel_id`'''
        # memoize transformation
        if tel_id not in self.pixels_in_tel_frame:
            telescope = self.source.subarray.tel[tel_id]
            cam = telescope.camera.geometry
            camera_frame = CameraFrame(
                focal_length=telescope.optics.equivalent_focal_length,
                rotation=cam.cam_rotation,
            )
            cam_coords = SkyCoord(x=cam.pix_x, y=cam.pix_y, frame=camera_frame)
            tel_coord = cam_coords.transform_to(TelescopeFrame())
            self.pixels_in_tel_frame[tel_id] = tel_coord

        coords = self.pixels_in_tel_frame[tel_id]
        return coords.fov_lon, coords.fov_lat

    def finish(self):
        Provenance().add_output_file(
            self.output,
            role='muon_efficiency_parameters',
        )
        self.writer.close()
Exemplo n.º 14
0
class DisplayDL1Calib(Tool):
    name = "ctapipe-display-dl1"
    description = __doc__

    telescope = Int(
        None,
        allow_none=True,
        help="Telescope to view. Set to None to display all telescopes.",
    ).tag(config=True)

    extractor_product = traits.create_class_enum_trait(
        ImageExtractor, default_value="NeighborPeakWindowSum")

    aliases = Dict(
        dict(
            input="EventSource.input_url",
            max_events="EventSource.max_events",
            extractor="DisplayDL1Calib.extractor_product",
            T="DisplayDL1Calib.telescope",
            O="ImagePlotter.output_path",
        ))
    flags = Dict(
        dict(D=(
            {
                "ImagePlotter": {
                    "display": True
                }
            },
            "Display the photo-electron images on-screen as they are produced.",
        )))
    classes = List([EventSource, ImagePlotter] +
                   traits.classes_with_traits(ImageExtractor))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.config.EventSource.input_url = get_dataset_path(
            "gamma_test_large.simtel.gz")
        self.eventsource = None
        self.calibrator = None
        self.plotter = None

    def setup(self):
        self.eventsource = self.add_component(
            EventSource.from_config(parent=self))

        self.calibrator = self.add_component(
            CameraCalibrator(parent=self, subarray=self.eventsource.subarray))
        self.plotter = self.add_component(
            ImagePlotter(subarray=self.eventsource.subarray, parent=self))

    def start(self):
        for event in self.eventsource:
            self.calibrator(event)

            tel_list = event.r0.tels_with_data

            if self.telescope:
                if self.telescope not in tel_list:
                    continue
                tel_list = [self.telescope]
            for telid in tel_list:
                self.plotter.plot(event, telid)

    def finish(self):
        self.plotter.finish()