Esempio n. 1
0
    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,
        )
Esempio n. 2
0
def analyze_muon_event(subarray, event_id, image, geom,
                       equivalent_focal_length, mirror_area, plot_rings,
                       plots_path):
    """
    Analyze an event to fit a muon ring

    Paramenters
    ---------
    event_id:   `int` id of the analyzed event
    image:      `np.ndarray` number of photoelectrons in each pixel
    geom:       CameraGeometry
    equivalent_focal_length: `float` focal length of the telescope
    mirror_area: `float` mirror area of the telescope in square meters
    plot_rings: `bool` plot the muon ring
    plots_path: `string` path to store the figures

    Returns
    ---------

    muonintensityoutput MuonEfficiencyContainer
    dist_mask           ndarray, pixels used in ring intensity likelihood fit
    ring_size           float, in p.e. total intensity in ring
    size_outside_ring   float, in p.e. to check for "shower contamination"
    muonringparam       MuonParametersContainer
    good_ring           bool, it determines whether the ring can be used for
                        analysis or not
    radial_distribution dict, return of function radial_light_distribution
    mean_pixel_charge_around_ring  float, charge "just outside" ring,
                                   to check the possible signal extrator bias
    muonparameters      MuonParametersContainer

    TODO: several hard-coded quantities that can go into a configuration file
    """

    lst1_tel_id = 1
    lst1_description = subarray.tels[lst1_tel_id]

    tailcuts = [10, 5]

    cam_rad = (lst1_description.camera.geometry.guess_radius() /
               lst1_description.optics.equivalent_focal_length) * u.rad

    # some cuts for good ring selection:
    min_pix = 148  # (8%) minimum number of pixels in the ring with >0 signal
    min_pix_fraction_after_cleaning = 0.1  # minimum fraction of the ring pixels that must be above tailcuts[0]
    min_ring_radius = 0.8 * u.deg  # minimum ring radius
    max_ring_radius = 1.5 * u.deg  # maximum ring radius
    max_radial_stdev = 0.1 * u.deg  # maximum standard deviation of the light distribution along ring radius
    max_radial_excess_kurtosis = 1.  # maximum excess kurtosis
    min_impact_parameter = 0.2  # in fraction of mirror radius
    max_impact_parameter = 0.9  # in fraction of mirror radius
    ring_integration_width = 0.25  # +/- integration range along ring radius, in fraction of ring radius (was 0.4 until 20200326)
    outer_ring_width = 0.2  # in fraction of ring radius, width of ring just outside the integrated muon ring, used to check pedestal bias

    x, y = pixel_coords_to_telescope(geom, equivalent_focal_length)
    muonringparam, clean_mask, dist, image_clean = fit_muon(
        x, y, image, geom, tailcuts)

    mirror_radius = np.sqrt(mirror_area / np.pi)  # meters
    dist_mask = np.abs(dist - muonringparam.radius
                       ) < muonringparam.radius * ring_integration_width
    pix_ring = image * dist_mask
    pix_outside_ring = image * ~dist_mask

    # mask to select pixels just outside the ring that will be integrated to obtain the ring's intensity:
    dist_mask_2 = np.logical_and(
        ~dist_mask,
        np.abs(dist - muonringparam.radius) < muonringparam.radius *
        (ring_integration_width + outer_ring_width))
    pix_ring_2 = image[dist_mask_2]

    #    nom_dist = np.sqrt(np.power(muonringparam.center_x,2)
    #                    + np.power(muonringparam.center_y, 2))

    muonparameters = MuonParametersContainer()
    muonparameters.containment = ring_containment(muonringparam.radius,
                                                  muonringparam.center_x,
                                                  muonringparam.center_y,
                                                  cam_rad)

    radial_distribution = radial_light_distribution(muonringparam.center_x,
                                                    muonringparam.center_y,
                                                    x[clean_mask],
                                                    y[clean_mask],
                                                    image[clean_mask])

    # Do complicated calculations (minuit-based max likelihood ring fit) only for selected rings:
    candidate_clean_ring = all([
        radial_distribution['standard_dev'] < max_radial_stdev,
        radial_distribution['excess_kurtosis'] < max_radial_excess_kurtosis,
        (pix_ring > tailcuts[0]).sum() >
        min_pix_fraction_after_cleaning * min_pix,
        np.count_nonzero(pix_ring) > min_pix,
        muonringparam.radius < max_ring_radius,
        muonringparam.radius > min_ring_radius
    ])

    if candidate_clean_ring:
        intensity_fitter = MuonIntensityFitter(subarray)

        # Use same hard-coded value for pedestal fluctuations as the previous
        # version of ctapipe:
        pedestal_stddev = 1.1 * np.ones(len(image))

        muonintensityoutput = \
            intensity_fitter(1,
                             muonringparam.center_x,
                             muonringparam.center_y,
                             muonringparam.radius,
                             image,
                             pedestal_stddev,
                             dist_mask)

        dist_ringwidth_mask = np.abs(dist - muonringparam.radius) < \
                              muonintensityoutput.width

        # We do the calculation of the ring completeness (i.e. fraction of whole circle) using the pixels
        # within the "width" fitted using MuonIntensityFitter
        muonparameters.completeness = ring_completeness(
            x[dist_ringwidth_mask],
            y[dist_ringwidth_mask],
            image[dist_ringwidth_mask],
            muonringparam.radius,
            muonringparam.center_x,
            muonringparam.center_y,
            threshold=30,
            bins=30)

        # No longer existing in ctapipe 0.8:
        # pix_ringwidth_im = image[dist_ringwidth_mask]
        # muonintensityoutput.ring_pix_completeness =  \
        #     (pix_ringwidth_im > tailcuts[0]).sum() / len(pix_ringwidth_im)

    else:
        # just to have the default values with units:
        muonintensityoutput = MuonEfficiencyContainer()
        muonintensityoutput.width = u.Quantity(np.nan, u.deg)
        muonintensityoutput.impact = u.Quantity(np.nan, u.m)
        muonintensityoutput.impact_x = u.Quantity(np.nan, u.m)
        muonintensityoutput.impact_y = u.Quantity(np.nan, u.m)

    # muonintensityoutput.mask = dist_mask # no longer there in ctapipe 0.8
    ring_size = np.sum(pix_ring)
    size_outside_ring = np.sum(pix_outside_ring * clean_mask)

    # This is just mean charge per pixel in pixels just around the ring
    # (on the outer side):
    mean_pixel_charge_around_ring = np.sum(pix_ring_2) / len(pix_ring_2)

    if candidate_clean_ring:
        print(
            "Impact parameter={:.3f}, ring_width={:.3f}, ring radius={:.3f}, "
            "ring completeness={:.3f}".format(
                muonintensityoutput.impact,
                muonintensityoutput.width,
                muonringparam.radius,
                muonparameters.completeness,
            ))
    # Now add the conditions based on the detailed muon ring fit:
    conditions = [
        candidate_clean_ring,
        muonintensityoutput.impact < max_impact_parameter * mirror_radius,
        muonintensityoutput.impact > min_impact_parameter * mirror_radius,

        # TODO: To be applied when we have decent optics.
        # muonintensityoutput.width
        # < 0.08,
        # NOTE: inside "candidate_clean_ring" cuts there is already a cut in
        # the std dev of light distribution along ring radius, which is also
        # a measure of the ring width

        # muonintensityoutput.width
        # > 0.04
    ]

    if all(conditions):
        good_ring = True
    else:
        good_ring = False

    if (plot_rings and plots_path and good_ring):
        focal_length = equivalent_focal_length
        ring_telescope = SkyCoord(muonringparam.center_x,
                                  muonringparam.center_y, TelescopeFrame())

        ring_camcoord = ring_telescope.transform_to(
            CameraFrame(
                focal_length=focal_length,
                rotation=geom.cam_rotation,
            ))
        centroid = (ring_camcoord.x.value, ring_camcoord.y.value)
        radius = muonringparam.radius
        width = muonintensityoutput.width
        ringrad_camcoord = 2 * radius.to(u.rad) * focal_length
        ringwidthfrac = width / radius
        ringrad_inner = ringrad_camcoord * (1. - ringwidthfrac)
        ringrad_outer = ringrad_camcoord * (1. + ringwidthfrac)

        fig, ax = plt.subplots(figsize=(10, 10))
        plot_muon_event(ax, geom, image * clean_mask, centroid,
                        ringrad_camcoord, ringrad_inner, ringrad_outer,
                        event_id)

        plt.figtext(0.15, 0.20, 'radial std dev: {0:.3f}'. \
                    format(radial_distribution['standard_dev']))
        plt.figtext(0.15, 0.18, 'radial excess kurtosis: {0:.3f}'. \
                    format(radial_distribution['excess_kurtosis']))
        plt.figtext(0.15, 0.16, 'fitted ring width: {0:.3f}'.format(width))
        plt.figtext(0.15, 0.14, 'ring completeness: {0:.3f}'. \
                    format(muonparameters.completeness))

        fig.savefig('{}/Event_{}_fitted.png'.format(plots_path, event_id))

    if (plot_rings and not plots_path):
        print("You are trying to plot without giving a path!")

    return muonintensityoutput, dist_mask, ring_size, size_outside_ring, \
           muonringparam, good_ring, radial_distribution, \
           mean_pixel_charge_around_ring, muonparameters
Esempio n. 3
0
def analyze_muon_event(subarray, tel_id, event_id, image, good_ring_config,
                       plot_rings, plots_path):
    """
    Analyze an event to fit a muon ring

    Parameters
    ----------
    subarray: `ctapipe.instrument.subarray.SubarrayDescription`
        Telescopes subarray
    tel_id : `int`
        Id of the telescope used
    event_id : `int`
        Id of the analyzed event
    image : `np.ndarray`
        Number of photoelectrons in each pixel
    good_ring_config : `dict` or None
        Set of parameters used to identify good muon rings to update LST-1 defaults
    plot_rings : `bool`
        Plot the muon ring
    plots_path : `string`
        Path to store the figures

    Returns
    -------
    muonintensityoutput : `MuonEfficiencyContainer`
    dist_mask : `ndarray`
        Pixels used in ring intensity likelihood fit
    ring_size : `float`
        Total intensity in ring in photoelectrons
    size_outside_ring : `float`
        Intensity outside the muon ring in photoelectrons
        to check for "shower contamination"
    muonringparam : `MuonRingContainer`
    good_ring : `bool`
        It determines whether the ring can be used for analysis or not
    radial_distribution : `dict`
        Return of function radial_light_distribution
    mean_pixel_charge_around_ring : float
        Charge "just outside" ring, to check the possible signal extractor bias
    muonparameters : `MuonParametersContainer`
    """

    tel_description = subarray.tels[tel_id]

    cam_rad = (tel_description.camera.geometry.guess_radius() /
               tel_description.optics.equivalent_focal_length) * u.rad
    geom = tel_description.camera.geometry
    equivalent_focal_length = tel_description.optics.equivalent_focal_length
    mirror_area = tel_description.optics.mirror_area

    # some parameters for analysis and cuts for good ring selection:
    params = update_parameters(good_ring_config, geom.n_pixels)

    x, y = pixel_coords_to_telescope(geom, equivalent_focal_length)
    muonringparam, clean_mask, dist, image_clean = fit_muon(
        x, y, image, geom, params['tailcuts'])

    mirror_radius = np.sqrt(mirror_area / np.pi)  # meters
    dist_mask = np.abs(
        dist - muonringparam.radius
    ) < muonringparam.radius * params['ring_integration_width']
    pix_ring = image * dist_mask
    pix_outside_ring = image * ~dist_mask

    # mask to select pixels just outside the ring that will be integrated to obtain the ring's intensity:
    dist_mask_2 = np.logical_and(
        ~dist_mask,
        np.abs(dist - muonringparam.radius) < muonringparam.radius *
        (params['ring_integration_width'] + params['outer_ring_width']))
    pix_ring_2 = image[dist_mask_2]

    #    nom_dist = np.sqrt(np.power(muonringparam.center_x,2)
    #                    + np.power(muonringparam.center_y, 2))

    muonparameters = MuonParametersContainer()
    muonparameters.containment = ring_containment(muonringparam.radius,
                                                  muonringparam.center_x,
                                                  muonringparam.center_y,
                                                  cam_rad)

    radial_distribution = radial_light_distribution(muonringparam.center_x,
                                                    muonringparam.center_y,
                                                    x[clean_mask],
                                                    y[clean_mask],
                                                    image[clean_mask])

    # Do complicated calculations (minuit-based max likelihood ring fit) only for selected rings:
    candidate_clean_ring = all([
        radial_distribution['standard_dev'] < params['max_radial_stdev'],
        radial_distribution['excess_kurtosis']
        < params['max_radial_excess_kurtosis'],
        (pix_ring > params['tailcuts'][0]).sum() >
        params['min_pix_fraction_after_cleaning'] * params['min_pix'],
        np.count_nonzero(pix_ring) > params['min_pix'],
        muonringparam.radius < params['max_ring_radius'],
        muonringparam.radius > params['min_ring_radius']
    ])

    if candidate_clean_ring:
        intensity_fitter = MuonIntensityFitter(subarray, hole_radius_m=0.308)

        # Use same hard-coded value for pedestal fluctuations as the previous
        # version of ctapipe:
        pedestal_stddev = 1.1 * np.ones(len(image))

        muonintensityoutput = \
            intensity_fitter(tel_id,
                             muonringparam.center_x,
                             muonringparam.center_y,
                             muonringparam.radius,
                             image,
                             pedestal_stddev,
                             dist_mask)

        dist_ringwidth_mask = np.abs(dist - muonringparam.radius) < \
                              muonintensityoutput.width

        # We do the calculation of the ring completeness (i.e. fraction of whole circle) using the pixels
        # within the "width" fitted using MuonIntensityFitter
        muonparameters.completeness = ring_completeness(
            x[dist_ringwidth_mask],
            y[dist_ringwidth_mask],
            image[dist_ringwidth_mask],
            muonringparam.radius,
            muonringparam.center_x,
            muonringparam.center_y,
            threshold=params['ring_completeness_threshold'],
            bins=30)

        # No longer existing in ctapipe 0.8:
        # pix_ringwidth_im = image[dist_ringwidth_mask]
        # muonintensityoutput.ring_pix_completeness =  \
        #     (pix_ringwidth_im > tailcuts[0]).sum() / len(pix_ringwidth_im)

    else:
        # just to have the default values with units:
        muonintensityoutput = MuonEfficiencyContainer()
        muonintensityoutput.width = u.Quantity(np.nan, u.deg)
        muonintensityoutput.impact = u.Quantity(np.nan, u.m)
        muonintensityoutput.impact_x = u.Quantity(np.nan, u.m)
        muonintensityoutput.impact_y = u.Quantity(np.nan, u.m)

    # muonintensityoutput.mask = dist_mask # no longer there in ctapipe 0.8
    ring_size = np.sum(pix_ring)
    size_outside_ring = np.sum(pix_outside_ring * clean_mask)

    # This is just mean charge per pixel in pixels just around the ring
    # (on the outer side):
    mean_pixel_charge_around_ring = np.sum(pix_ring_2) / len(pix_ring_2)

    if candidate_clean_ring:
        print(
            "Impact parameter={:.3f}, ring_width={:.3f}, ring radius={:.3f}, "
            "ring completeness={:.3f}".format(
                muonintensityoutput.impact,
                muonintensityoutput.width,
                muonringparam.radius,
                muonparameters.completeness,
            ))
    # Now add the conditions based on the detailed muon ring fit:
    conditions = [
        candidate_clean_ring,
        muonintensityoutput.impact <
        params['max_impact_parameter'] * mirror_radius,
        muonintensityoutput.impact >
        params['min_impact_parameter'] * mirror_radius,

        # TODO: To be applied when we have decent optics.
        # muonintensityoutput.width
        # < 0.08,
        # NOTE: inside "candidate_clean_ring" cuts there is already a cut in
        # the std dev of light distribution along ring radius, which is also
        # a measure of the ring width

        # muonintensityoutput.width
        # > 0.04
    ]

    if all(conditions):
        good_ring = True
    else:
        good_ring = False

    if (plot_rings and plots_path and good_ring):
        focal_length = equivalent_focal_length
        ring_telescope = SkyCoord(muonringparam.center_x,
                                  muonringparam.center_y, TelescopeFrame())

        ring_camcoord = ring_telescope.transform_to(
            CameraFrame(
                focal_length=focal_length,
                rotation=geom.cam_rotation,
            ))
        centroid = (ring_camcoord.x.value, ring_camcoord.y.value)
        radius = muonringparam.radius
        width = muonintensityoutput.width
        ringrad_camcoord = 2 * radius.to(u.rad) * focal_length
        ringwidthfrac = width / radius
        ringrad_inner = ringrad_camcoord * (1. - ringwidthfrac)
        ringrad_outer = ringrad_camcoord * (1. + ringwidthfrac)

        fig, ax = plt.subplots(figsize=(10, 10))
        plot_muon_event(ax, geom, image * clean_mask, centroid,
                        ringrad_camcoord, ringrad_inner, ringrad_outer,
                        event_id)

        plt.figtext(0.15, 0.20, 'radial std dev: {0:.3f}'. \
                    format(radial_distribution['standard_dev']))
        plt.figtext(0.15, 0.18, 'radial excess kurtosis: {0:.3f}'. \
                    format(radial_distribution['excess_kurtosis']))
        plt.figtext(0.15, 0.16, 'fitted ring width: {0:.3f}'.format(width))
        plt.figtext(0.15, 0.14, 'ring completeness: {0:.3f}'. \
                    format(muonparameters.completeness))

        fig.savefig('{}/Event_{}_fitted.png'.format(plots_path, event_id))

    if (plot_rings and not plots_path):
        print("You are trying to plot without giving a path!")

    return muonintensityoutput, dist_mask, ring_size, size_outside_ring, \
           muonringparam, good_ring, radial_distribution, \
           mean_pixel_charge_around_ring, muonparameters