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 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
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