def __call__(self, image_2d, verbose=False): """Apply the pre-selection cuts on ``image_2d``. Parameters ---------- image_2d : array_like The image to evaluate. Returns ------- bool Returns ``True`` if the image **does not** fulfill the pre-selection cuts (i.e. returns ``True`` if the image should be rejected). Return ``False`` if the image satisfy the pre-selection cuts (i.e. returns ``False`` if the image should be kept). """ image_1d = geometry_converter.image_2d_to_1d(image_2d, self.cam_id) hillas_params = self.hillas_parameters(image_1d) npe_contained = self.min_npe < np.nansum(image_1d) < self.max_npe ellipticity_contained = self.min_ellipticity < self.hillas_ellipticity( hillas_params) < self.max_ellipticity radius_contained = self.min_radius < self.hillas_centroid_dist( hillas_params) < self.max_radius num_pixels_contained = self.min_num_pixels <= np.sum(image_1d > 0) if verbose: print( "npe_contained: {} ; ellipticity_contained: {} ; radius_contained: {} ; num_pixels_contained: {}" .format(npe_contained, ellipticity_contained, radius_contained, num_pixels_contained)) return not (npe_contained and ellipticity_contained and radius_contained and num_pixels_contained)
def clean_image(self, img, geom): """Clean image according to configuration Parameter --------- img: array Calibrated image geom: `~ctapipe.XXX` Camera geometry Returns ------- new_img: `np.array` Cleaned image """ new_img = np.copy(img) cam_id = geom.cam_id if self.mode in "tail": mask = self.cleaners[cam_id](new_img, geom, self.clean_opts[cam_id]) new_img[~mask] = 0 return new_img if self.mode in "wave": image_2d = geometry_converter.image_1d_to_2d(new_img, cam_id) image_2d = self.cleaners[cam_id](image_2d, self.clean_opts[cam_id]) new_img = geometry_converter.image_2d_to_1d(image_2d, cam_id) return new_img
def clean_image(self, img, geom): """Clean image according to configuration Parameter --------- img: array Calibrated image geom: `~ctapipe.XXX` Camera geometry Returns ------- new_img: `np.array` Cleaned image mask: `np.array` Boolean array which corresponds to the cleaned pixels """ new_img = np.copy(img) cam_id = geom.cam_id if self.mode in "tail": mask = self.cleaners[cam_id](new_img, geom, self.clean_opts[cam_id]) new_img[~mask] = 0 # this is still the full camera, but the non-surviving pixels are # set to 0: this is not a problem for the number_of_islands # ctapipe function used later return new_img, mask if self.mode in "wave": image_2d = geometry_converter.image_1d_to_2d(new_img, cam_id) image_2d = self.cleaners[cam_id](image_2d, self.clean_opts[cam_id]) new_img = geometry_converter.image_2d_to_1d(image_2d, cam_id) return new_img, mask
def prepare_event(self, source, return_stub=True, save_images=False, debug=False): """ Calibrate, clean and reconstruct the direction of an event. Parameters ---------- source : ctapipe.io.EventSource A container of selected showers from a simtel file. geom_cam_tel: dict Dictionary of of MyCameraGeometry objects for each camera in the file return_stub : bool If True, yield also images from events that won't be reconstructed. This feature is not currently available. save_images : bool If True, save photoelectron images from reconstructed events. debug : bool If True, print some debugging information (to be expanded). Yields ------ PreparedEvent: dict Dictionary containing event-image information to be written. """ # ============================================================= # TRANSFORMED CAMERA GEOMETRIES # ============================================================= # These are the camera geometries were the Hillas parametrization will # be performed. # They are transformed to TelescopeFrame using the effective focal # lengths # These geometries could be used also to performe the image cleaning, # but for the moment we still do that in the CameraFrame geom_cam_tel = {} for camera in source.subarray.camera_types: # Original geometry of each camera geom = camera.geometry # Same but with focal length as an attribute # This is planned to disappear and be imported by ctapipe focal_length = effective_focal_lengths(camera.camera_name) geom_cam_tel[camera.camera_name] = MyCameraGeometry( camera_name=camera.camera_name, pix_type=geom.pix_type, pix_id=geom.pix_id, pix_x=geom.pix_x, pix_y=geom.pix_y, pix_area=geom.pix_area, cam_rotation=geom.cam_rotation, pix_rotation=geom.pix_rotation, frame=CameraFrame(focal_length=focal_length), ).transform_to(TelescopeFrame()) # ============================================================= ievt = 0 for event in source: # Display event counts if debug: print( bcolors.BOLD + f"EVENT #{event.count}, EVENT_ID #{event.index.event_id}" + bcolors.ENDC) print(bcolors.BOLD + f"has triggered telescopes {event.r1.tel.keys()}" + bcolors.ENDC) ievt += 1 # if (ievt < 10) or (ievt % 10 == 0): # print(ievt) self.event_cutflow.count("noCuts") # LST stereo condition # whenever there is only 1 LST in an event, we remove that telescope # if the remaining telescopes are less than min_tel we remove the event lst_tel_ids = set( source.subarray.get_tel_ids_for_type("LST_LST_LSTCam")) triggered_LSTs = set(event.r0.tel.keys()).intersection(lst_tel_ids) n_triggered_LSTs = len(triggered_LSTs) n_triggered_non_LSTs = len(event.r0.tel.keys()) - n_triggered_LSTs bad_LST_stereo = False if self.LST_stereo and self.event_cutflow.cut( "no-LST-stereo + <2 other types", n_triggered_LSTs, n_triggered_non_LSTs): bad_LST_stereo = True if return_stub: print( bcolors.WARNING + "WARNING: LST_stereo is set to 'True'\n" + f"This event has < {self.min_ntel_LST} triggered LSTs\n" + "and < 2 triggered telescopes from other telescope types.\n" + "The event will be processed up to DL1b." + bcolors.ENDC) # we show this, but we proceed to analyze the event up to # DL1a/b for the associated benchmarks # this checks for < 2 triggered telescopes of ANY type if self.event_cutflow.cut("min2Tels trig", len(event.r1.tel.keys())): if return_stub: print( bcolors.WARNING + f"WARNING : < {self.min_ntel} triggered telescopes!" + bcolors.ENDC) # we show this, but we proceed to analyze it # ============================================================= # CALIBRATION # ============================================================= if debug: print(bcolors.OKBLUE + "Extracting all calibrated images..." + bcolors.ENDC) self.calib(event) # Calibrate the event # ============================================================= # BEGINNING OF LOOP OVER TELESCOPES # ============================================================= dl1_phe_image = {} dl1_phe_image_mask_reco = {} dl1_phe_image_mask_clusters = {} mc_phe_image = {} max_signals = {} n_pixel_dict = {} hillas_dict_reco = {} # for direction reconstruction hillas_dict = {} # for discrimination leakage_dict = {} concentration_dict = {} n_tels = { "Triggered": len(event.r1.tel.keys()), "LST_LST_LSTCam": 0, "MST_MST_NectarCam": 0, "MST_MST_FlashCam": 0, "MST_SCT_SCTCam": 0, "SST_1M_DigiCam": 0, "SST_ASTRI_ASTRICam": 0, "SST_GCT_CHEC": 0, } n_cluster_dict = {} impact_dict_reco = {} # impact distance measured in tilt system point_azimuth_dict = {} point_altitude_dict = {} good_for_reco = {} # 1 = success, 0 = fail # Array pointing in AltAz frame az = event.pointing.array_azimuth alt = event.pointing.array_altitude array_pointing = SkyCoord(az, alt, frame=AltAz()) ground_frame = GroundFrame() tilted_frame = TiltedGroundFrame(pointing_direction=array_pointing) for tel_id in event.r1.tel.keys(): point_azimuth_dict[tel_id] = event.pointing.tel[tel_id].azimuth point_altitude_dict[tel_id] = event.pointing.tel[ tel_id].altitude if debug: print(bcolors.OKBLUE + f"Working on telescope #{tel_id}..." + bcolors.ENDC) self.image_cutflow.count("noCuts") camera = source.subarray.tel[tel_id].camera.geometry # count the current telescope according to its type tel_type = str(source.subarray.tel[tel_id]) n_tels[tel_type] += 1 # use ctapipe's functionality to get the calibrated image # and scale the reconstructed values if required pmt_signal = event.dl1.tel[tel_id].image / self.calibscale # If required... if save_images is True: # Save the simulated and reconstructed image of the event dl1_phe_image[tel_id] = pmt_signal mc_phe_image[tel_id] = event.simulation.tel[ tel_id].true_image # We now ASSUME that the event will be good good_for_reco[tel_id] = 1 # later we change to 0 if any condition is NOT satisfied if self.cleaner_reco.mode == "tail": # tail uses only ctapipe # Cleaning used for direction reconstruction image_biggest, mask_reco = self.cleaner_reco.clean_image( pmt_signal, camera) # calculate the leakage (before filtering) leakages = {} # this is needed by both cleanings # The check on SIZE shouldn't be here, but for the moment # I prefer to sacrifice elegancy... if np.sum(image_biggest[mask_reco]) != 0.0: leakage_biggest = leakage_parameters( camera, image_biggest, mask_reco) leakages["leak1_reco"] = leakage_biggest[ "intensity_width_1"] leakages["leak2_reco"] = leakage_biggest[ "intensity_width_2"] else: leakages["leak1_reco"] = 0.0 leakages["leak2_reco"] = 0.0 # find all islands using this cleaning num_islands, labels = number_of_islands(camera, mask_reco) if num_islands == 1: # if only ONE islands is left ... # ...use directly the old mask and reduce dimensions # to make Hillas parametrization faster camera_biggest = camera[mask_reco] image_biggest = image_biggest[mask_reco] if save_images is True: dl1_phe_image_mask_reco[tel_id] = mask_reco elif num_islands > 1: # if more islands survived.. # ...find the biggest one mask_biggest = largest_island(labels) # and also reduce dimensions camera_biggest = camera[mask_biggest] image_biggest = image_biggest[mask_biggest] if save_images is True: dl1_phe_image_mask_reco[tel_id] = mask_biggest else: # if no islands survived use old camera and image camera_biggest = camera dl1_phe_image_mask_reco[tel_id] = mask_reco # Cleaning used for score/energy estimation image_extended, mask_extended = self.cleaner_extended.clean_image( pmt_signal, camera) dl1_phe_image_mask_clusters[tel_id] = mask_extended # calculate the leakage (before filtering) # this part is not well coded, but for the moment it works if np.sum(image_extended[mask_extended]) != 0.0: leakage_extended = leakage_parameters( camera, image_extended, mask_extended) leakages["leak1"] = leakage_extended[ "intensity_width_1"] leakages["leak2"] = leakage_extended[ "intensity_width_2"] else: leakages["leak1"] = 0.0 leakages["leak2"] = 0.0 # find all islands with this cleaning # we will also register how many have been found n_cluster_dict[tel_id], labels = number_of_islands( camera, mask_extended) # NOTE: the next check shouldn't be necessary if we keep # all the isolated pixel clusters, but for now the # two cleanings are set the same in analysis.yml because # the performance of the extended one has never been really # studied in model estimation. # if some islands survived if n_cluster_dict[tel_id] > 0: # keep all of them and reduce dimensions camera_extended = camera[mask_extended] image_extended = image_extended[mask_extended] else: # otherwise continue with the old camera and image camera_extended = camera # could this go into `hillas_parameters` ...? # this is basically the charge of ALL islands # not calculated later by the Hillas parametrization! max_signals[tel_id] = np.max(image_extended) else: # for wavelets we stick to old pywi-cta code try: # "try except FileNotFoundError" not clear to me, but for now it stays... with warnings.catch_warnings(): # Image with biggest cluster (reco cleaning) image_biggest, mask_reco = self.cleaner_reco.clean_image( pmt_signal, camera) image_biggest2d = geometry_converter.image_1d_to_2d( image_biggest, camera.camera_name) image_biggest2d = filter_pixels_clusters( image_biggest2d) image_biggest = geometry_converter.image_2d_to_1d( image_biggest2d, camera.camera_name) # Image for score/energy estimation (with clusters) ( image_extended, mask_extended, ) = self.cleaner_extended.clean_image( pmt_signal, camera) # This last part was outside the pywi-cta block # before, but is indeed part of it because it uses # pywi-cta functions in the "extended" case # For cluster counts image_2d = geometry_converter.image_1d_to_2d( image_extended, camera.camera_name) n_cluster_dict[ tel_id] = pixel_clusters.number_of_pixels_clusters( array=image_2d, threshold=0) # could this go into `hillas_parameters` ...? max_signals[tel_id] = np.max(image_extended) except FileNotFoundError as e: print(e) continue # ============================================================= # PRELIMINARY IMAGE SELECTION # ============================================================= cleaned_image_is_good = True # we assume this if self.image_selection_source == "extended": cleaned_image_to_use = image_extended elif self.image_selection_source == "biggest": cleaned_image_to_use = image_biggest else: raise ValueError( "Only supported cleanings are 'biggest' or 'extended'." ) # Apply some selection if self.image_cutflow.cut("min pixel", cleaned_image_to_use): if debug: print(bcolors.WARNING + "WARNING : not enough pixels!" + bcolors.ENDC) good_for_reco[tel_id] = 0 # we record it as BAD cleaned_image_is_good = False if self.image_cutflow.cut("min charge", np.sum(cleaned_image_to_use)): if debug: print(bcolors.WARNING + "WARNING : not enough charge!" + bcolors.ENDC) good_for_reco[tel_id] = 0 # we record it as BAD cleaned_image_is_good = False if debug and (not cleaned_image_is_good): # BAD image quality print(bcolors.WARNING + "WARNING : The cleaned image didn't pass" + " preliminary cuts.\n" + "An attempt to parametrize it will be made," + " but the image will NOT be used for" + " direction reconstruction." + bcolors.ENDC) # ============================================================= # IMAGE PARAMETRIZATION # ============================================================= with np.errstate(invalid="raise", divide="raise"): try: # Filter the cameras in TelescopeFrame with the same # cleaning masks camera_biggest_tel = geom_cam_tel[camera.camera_name][ camera_biggest.pix_id] camera_extended_tel = geom_cam_tel[camera.camera_name][ camera_extended.pix_id] # Parametrize the image in the TelescopeFrame moments_reco = hillas_parameters( camera_biggest_tel, image_biggest) # for geometry (eg direction) moments = hillas_parameters( camera_extended_tel, image_extended ) # for discrimination and energy reconstruction if debug: print( "Image parameters from main cluster cleaning:") print(moments_reco) print( "Image parameters from all-clusters cleaning:") print(moments) # Add concentration parameters concentrations = {} concentrations_extended = concentration_parameters( camera_extended_tel, image_extended, moments) concentrations[ "concentration_cog"] = concentrations_extended[ "cog"] concentrations[ "concentration_core"] = concentrations_extended[ "core"] concentrations[ "concentration_pixel"] = concentrations_extended[ "pixel"] # =================================================== # PARAMETRIZED IMAGE SELECTION # =================================================== if self.image_selection_source == "extended": moments_to_use = moments else: moments_to_use = moments_reco # if width and/or length are zero (e.g. when there is # only only one pixel or when all pixel are exactly # in one row), the parametrisation # won't be very useful: skip if self.image_cutflow.cut("poor moments", moments_to_use): if debug: print(bcolors.WARNING + "WARNING : poor moments!" + bcolors.ENDC) good_for_reco[tel_id] = 0 # we record it as BAD if self.image_cutflow.cut("close to the edge", moments_to_use, camera.camera_name): if debug: print( bcolors.WARNING + "WARNING : out of containment radius!\n" + f"Camera radius = {self.camera_radius[camera.camera_name]}\n" + f"COG radius = {moments_to_use.r}" + bcolors.ENDC) good_for_reco[tel_id] = 0 if self.image_cutflow.cut("bad ellipticity", moments_to_use): if debug: print(bcolors.WARNING + "WARNING : bad ellipticity" + bcolors.ENDC) good_for_reco[tel_id] = 0 if debug and good_for_reco[tel_id] == 1: print( bcolors.OKGREEN + "Image survived and correctly parametrized." # + "\nIt will be used for direction reconstruction!" + bcolors.ENDC) elif debug and good_for_reco[tel_id] == 0: print( bcolors.WARNING + "Image not survived or " + "not good enough for parametrization." # + "\nIt will be NOT used for direction reconstruction, " # + "BUT it's information will be recorded." + bcolors.ENDC) hillas_dict[tel_id] = moments hillas_dict_reco[tel_id] = moments_reco n_pixel_dict[tel_id] = len( np.where(image_extended > 0)[0]) leakage_dict[tel_id] = leakages concentration_dict[tel_id] = concentrations except ( FloatingPointError, HillasParameterizationError, ValueError, ) as e: if debug: print(bcolors.FAIL + "Parametrization error: " + f"{e}\n" + "Dummy parameters recorded." + bcolors.ENDC) good_for_reco[tel_id] = 0 hillas_dict[ tel_id] = HillasParametersTelescopeFrameContainer( ) hillas_dict_reco[ tel_id] = HillasParametersTelescopeFrameContainer( ) n_pixel_dict[tel_id] = len( np.where(image_extended > 0)[0]) leakage_dict[tel_id] = leakages concentration_dict[tel_id] = concentrations # END OF THE CYCLE OVER THE TELESCOPES # ============================================================= # DIRECTION RECONSTRUCTION # ============================================================= if bad_LST_stereo: if debug: print( bcolors.WARNING + "WARNING: This event was triggered with 1 LST image and <2 images from other telescope types." + "\nWARNING : direction reconstruction will not be performed." + bcolors.ENDC) # Set all the involved images as NOT good for recosntruction # even though they might have been # but this is because of the LST stereo trigger.... for tel_id in event.r0.tel.keys(): good_for_reco[tel_id] = 0 # and set the number of good and bad images accordingly n_tels["GOOD images"] = 0 n_tels[ "BAD images"] = n_tels["Triggered"] - n_tels["GOOD images"] # create a dummy container for direction reconstruction reco_result = ReconstructedShowerContainer() if return_stub: # if saving all events (default) if debug: print(bcolors.OKBLUE + "Recording event..." + bcolors.ENDC) print( bcolors.WARNING + "WARNING: This is event shall NOT be used further along the pipeline." + bcolors.ENDC) yield stub( # record event with dummy info event, mc_phe_image, dl1_phe_image, dl1_phe_image_mask_reco, dl1_phe_image_mask_clusters, good_for_reco, hillas_dict, hillas_dict_reco, n_tels, leakage_dict, concentration_dict) continue else: continue # Now in case the only triggered telescopes were # - < self.min_ntel_LST LST, # - >=2 any other telescope type, # we remove the single-LST image and continue reconstruction with # the images from the other telescope types if self.LST_stereo and (n_triggered_LSTs < self.min_ntel_LST) and ( n_triggered_LSTs != 0) and (n_triggered_non_LSTs >= 2): if debug: print( bcolors.WARNING + f"WARNING: LST stereo trigger condition is active.\n" + f"This event triggered < {self.min_ntel_LST} LSTs " + f"and {n_triggered_non_LSTs} images from other telescope types.\n" + bcolors.ENDC) for tel_id in triggered_LSTs: # in case we test for min_ntel_LST>2 if good_for_reco[tel_id]: # we don't use it for reconstruction good_for_reco[tel_id] = 0 print( bcolors.WARNING + f"WARNING: LST image #{tel_id} removed, even though it passed quality cuts." + bcolors.ENDC) # TODO: book-keeping of this kind of events doesn't seem easy # convert dictionary in numpy array to get a "mask" images_status = np.asarray(list(good_for_reco.values())) # record how many images will be used for reconstruction n_tels["GOOD images"] = len( np.extract(images_status == 1, images_status)) n_tels["BAD images"] = n_tels["Triggered"] - n_tels["GOOD images"] if self.event_cutflow.cut("min2Tels reco", n_tels["GOOD images"]): if debug: print( bcolors.FAIL + f"WARNING: < {self.min_ntel} ({n_tels['GOOD images']}) images remaining!" + "\nWARNING : direction reconstruction is not possible!" + bcolors.ENDC) # create a dummy container for direction reconstruction reco_result = ReconstructedShowerContainer() if return_stub: # if saving all events (default) if debug: print(bcolors.OKBLUE + "Recording event..." + bcolors.ENDC) print( bcolors.WARNING + "WARNING: This is event shall NOT be used further along the pipeline." + bcolors.ENDC) yield stub( # record event with dummy info event, mc_phe_image, dl1_phe_image, dl1_phe_image_mask_reco, dl1_phe_image_mask_clusters, good_for_reco, hillas_dict, hillas_dict_reco, n_tels, leakage_dict, concentration_dict) continue else: continue if debug: print(bcolors.OKBLUE + "Starting direction reconstruction..." + bcolors.ENDC) try: with warnings.catch_warnings(): warnings.simplefilter("ignore") if self.image_selection_source == "extended": hillas_dict_to_use = hillas_dict else: hillas_dict_to_use = hillas_dict_reco # use only the successfully parametrized images # to reconstruct the direction of this event successfull_hillas = np.where(images_status == 1)[0] all_images = np.asarray(list(good_for_reco.keys())) good_images = set(all_images[successfull_hillas]) good_hillas_dict = { k: v for k, v in hillas_dict_to_use.items() if k in good_images } if debug: print( bcolors.PURPLE + f"{len(good_hillas_dict)} images " + f"(from telescopes #{list(good_hillas_dict.keys())}) will be " + "used to recover the shower's direction..." + bcolors.ENDC) # Reconstruction results reco_result = self.shower_reco.predict( good_hillas_dict, source.subarray, SkyCoord(alt=alt, az=az, frame="altaz"), None, # use the array direction ) # Impact parameter for telescope-wise energy estimation subarray = source.subarray for tel_id in hillas_dict_to_use.keys(): pos = subarray.positions[tel_id] tel_ground = SkyCoord(pos[0], pos[1], pos[2], frame=ground_frame) core_ground = SkyCoord( reco_result.core_x, reco_result.core_y, 0 * u.m, frame=ground_frame, ) # Go back to the tilted frame # this should be the same... tel_tilted = tel_ground.transform_to(tilted_frame) # but this not core_tilted = SkyCoord(x=core_ground.x, y=core_ground.y, frame=tilted_frame) impact_dict_reco[tel_id] = np.sqrt( (core_tilted.x - tel_tilted.x)**2 + (core_tilted.y - tel_tilted.y)**2) except (Exception, TooFewTelescopesException, InvalidWidthException) as e: if debug: print("exception in reconstruction:", e) raise if return_stub: if debug: print( bcolors.FAIL + "Shower could NOT be correctly reconstructed! " + "Recording event..." + "WARNING: This is event shall NOT be used further along the pipeline." + bcolors.ENDC) yield stub( # record event with dummy info event, mc_phe_image, dl1_phe_image, dl1_phe_image_mask_reco, dl1_phe_image_mask_clusters, good_for_reco, hillas_dict, hillas_dict_reco, n_tels, leakage_dict, concentration_dict) else: continue if self.event_cutflow.cut("direction nan", reco_result): if debug: print(bcolors.WARNING + "WARNING: undefined direction!" + bcolors.ENDC) if return_stub: if debug: print( bcolors.FAIL + "Shower could NOT be correctly reconstructed! " + "Recording event..." + "WARNING: This is event shall NOT be used further along the pipeline." + bcolors.ENDC) yield stub( # record event with dummy info event, mc_phe_image, dl1_phe_image, dl1_phe_image_mask_reco, dl1_phe_image_mask_clusters, good_for_reco, hillas_dict, hillas_dict_reco, n_tels, leakage_dict, concentration_dict) else: continue if debug: print(bcolors.BOLDGREEN + "Shower correctly reconstructed! " + "Recording event..." + bcolors.ENDC) yield PreparedEvent( event=event, dl1_phe_image=dl1_phe_image, dl1_phe_image_mask_reco=dl1_phe_image_mask_reco, dl1_phe_image_mask_clusters=dl1_phe_image_mask_clusters, mc_phe_image=mc_phe_image, n_pixel_dict=n_pixel_dict, hillas_dict=hillas_dict, hillas_dict_reco=hillas_dict_reco, leakage_dict=leakage_dict, concentration_dict=concentration_dict, n_tels=n_tels, max_signals=max_signals, n_cluster_dict=n_cluster_dict, reco_result=reco_result, impact_dict=impact_dict_reco, good_event=True, good_for_reco=good_for_reco, )
# Cleaning the image with Wavelets transform filtering. #TMP_DIR = "/Volumes/ramdisk" TMP_DIR = "." wavelet = WaveletTransform() cleaned_image = wavelet.clean_image( image_2d, type_of_filtering='hard_filtering', filter_thresholds=[8, 2], # <- TODO last_scale_treatment="mask", detect_only_positive_structures=False, kill_isolated_pixels=False, noise_distribution=noise_distribution, tmp_files_directory=TMP_DIR) ############################################################################### # Plot the cleaned image. plt.imshow(cleaned_image) plt.show() cleaned_image_1d = geometry_converter.image_2d_to_1d(cleaned_image, image.meta['cam_id']) plot_ctapipe_image(cleaned_image_1d, geom=geom1d, plot_axis=False, title=title_str) plt.show()
def run( self, cleaning_function_params, input_file_or_dir_path_list, benchmark_method, output_file_path, plot=False, saveplot=None, ref_img_as_input=False, # A hack to easily produce CSV files... max_num_img=None, tel_id=None, event_id=None, cam_id=None, debug=False, rejection_criteria=None): """A convenient optional wrapper to simplify the image cleaning analysis. Apply the image cleaning analysis on `input_file_or_dir_path_list`, apply some pre-processing and post-processing procedures, collect and return results, intermediate values and metadata. Parameters ---------- cleaning_function_params A dictionary containing the parameters required for the image cleaning method. input_file_or_dir_path_list A list of file to clean. Can be a list of simtel files, fits files or directories containing such files. benchmark_method The list of estimators to use to assess the image cleaning. If `None`, images are cleaned but nothing is returned (can be used with e.g. the `plot` and/or `saveplot` options). output_file_path The result file path (a JSON file). plot The result of each cleaning is plot if `True`. saveplot The result of each cleaning is saved if `True`. ref_img_as_input This option is a hack to easily produce a "flatten" CSV results files. max_num_img The number of images to process among the input set (`input_file_or_dir_path_list`). debug Stop the execution and print the full traceback when an exception is encountered if this parameter is `True`. Report exceptions and continue with the next input image if this parameter is `False`. Returns ------- dict Results, intermediate values and metadata. """ launch_time = time.perf_counter() if benchmark_method is not None: io_list = [] # The list of returned dictionaries if tel_id is not None: tel_id = [tel_id] if event_id is not None: event_id = [event_id] if cam_id is None: raise ValueError('cam_id is now mandatory.') if cam_id == "ASTRICam": integrator_window_width = 1 integrator_window_shift = 1 elif cam_id == "CHEC": integrator_window_width = 10 integrator_window_shift = 5 elif cam_id == "DigiCam": integrator_window_width = 5 integrator_window_shift = 2 elif cam_id == "FlashCam": integrator_window_width = 6 integrator_window_shift = 3 elif cam_id == "NectarCam": integrator_window_width = 5 integrator_window_shift = 2 elif cam_id == "LSTCam": integrator_window_width = 5 integrator_window_shift = 2 else: raise ValueError('Unknown cam_id "{}"'.format(cam_id)) for image in image_generator( input_file_or_dir_path_list, max_num_images=max_num_img, tel_filter_list=tel_id, ev_filter_list=event_id, cam_filter_list=[cam_id], mc_rejection_criteria=rejection_criteria, ctapipe_format=False, integrator='LocalPeakIntegrator', integrator_window_width=integrator_window_width, integrator_window_shift=integrator_window_shift, integration_correction=False, mix_channels=True): input_file_path = image.meta['file_path'] if self.verbose: print(input_file_path) # `image_dict` contains metadata (to be returned) on the current image image_dict = {"input_file_path": input_file_path} try: # READ THE INPUT FILE ##################################### if self.verbose: print("TEL{}_EV{}".format(image.meta["tel_id"], image.meta["event_id"])) reference_img = image.reference_image pixels_position = image.pixels_position if ref_img_as_input: # This option is a hack to easily produce CSV files with # the "null_ref" "cleaning" module... input_img = copy.deepcopy(reference_img) else: input_img = image.input_image image_dict.update(image.meta) # Make the original 1D geom (required for the 2D to 1D geometry # conversion, for Tailcut and for Hillas) cam_id = image.meta['cam_id'] cleaning_function_params["cam_id"] = cam_id geom1d = geometry_converter.get_geom1d(cam_id) if benchmark_method is not None: # FETCH ADDITIONAL IMAGE METADATA ##################### image_dict["img_ref_signal_to_border"] = signal_to_border( reference_img) # TODO: NaN image_dict[ "img_ref_signal_to_border_distance"] = signal_to_border_distance( reference_img) # TODO: NaN image_dict["img_ref_pemax_on_border"] = pemax_on_border( reference_img) # TODO: NaN delta_pe, delta_abs_pe, delta_num_pixels = filter_pixels_clusters_stats( reference_img) # TODO: NaN num_islands = number_of_pixels_clusters( reference_img) # TODO: NaN image_dict["img_ref_islands_delta_pe"] = delta_pe image_dict["img_ref_islands_delta_abs_pe"] = delta_abs_pe image_dict[ "img_ref_islands_delta_num_pixels"] = delta_num_pixels image_dict["img_ref_num_islands"] = num_islands image_dict["img_ref_sum_pe"] = float( np.nansum(reference_img)) image_dict["img_ref_min_pe"] = float( np.nanmin(reference_img)) image_dict["img_ref_max_pe"] = float( np.nanmax(reference_img)) image_dict["img_ref_num_pix"] = int( (reference_img[np.isfinite(reference_img)] > 0).sum()) image_dict["img_in_sum_pe"] = float(np.nansum(input_img)) image_dict["img_in_min_pe"] = float(np.nanmin(input_img)) image_dict["img_in_max_pe"] = float(np.nanmax(input_img)) image_dict["img_in_num_pix"] = int( (input_img[np.isfinite(input_img)] > 0).sum()) reference_img1d = geometry_converter.image_2d_to_1d( reference_img, cam_id) try: hillas_params_2_ref_img = get_hillas_parameters( geom1d, reference_img1d, HILLAS_IMPLEMENTATION) # TODO GEOM except Exception as e: hillas_params_2_ref_img = float('nan') print(e) #traceback.print_tb(e.__traceback__, file=sys.stdout) image_dict["img_ref_hillas_2_size"] = float( hillas_params_2_ref_img.intensity) image_dict[ "img_ref_hillas_2_cen_x"] = hillas_params_2_ref_img.x.value image_dict[ "img_ref_hillas_2_cen_y"] = hillas_params_2_ref_img.y.value image_dict[ "img_ref_hillas_2_length"] = hillas_params_2_ref_img.length.value image_dict[ "img_ref_hillas_2_width"] = hillas_params_2_ref_img.width.value image_dict[ "img_ref_hillas_2_r"] = hillas_params_2_ref_img.r.value image_dict[ "img_ref_hillas_2_phi"] = hillas_params_2_ref_img.phi.to( u.rad).value image_dict[ "img_ref_hillas_2_psi"] = hillas_params_2_ref_img.psi.to( u.rad).value try: image_dict["img_ref_hillas_2_miss"] = float( hillas_params_2_ref_img.miss.value) except: image_dict["img_ref_hillas_2_miss"] = None image_dict[ "img_ref_hillas_2_kurtosis"] = hillas_params_2_ref_img.kurtosis image_dict[ "img_ref_hillas_2_skewness"] = hillas_params_2_ref_img.skewness # CLEAN THE INPUT IMAGE ################################### # Copy the image (otherwise some cleaning functions like Tailcut may change it) #input_img_copy = copy.deepcopy(input_img) input_img_copy = input_img.astype('float64', copy=True) cleaning_function_params["output_data_dict"] = {} initial_time = time.perf_counter() cleaned_img = self.clean_image( input_img_copy, **cleaning_function_params) # TODO: NaN full_clean_execution_time_sec = time.perf_counter( ) - initial_time if benchmark_method is not None: image_dict.update( cleaning_function_params["output_data_dict"]) del cleaning_function_params["output_data_dict"] # ASSESS OR PRINT THE CLEANED IMAGE ####################### if benchmark_method is not None: # ASSESS THE CLEANING ################################# kwargs = { 'geom': geom1d, 'hillas_implementation': HILLAS_IMPLEMENTATION } # TODO GEOM score_tuple, score_name_tuple = assess.assess_image_cleaning( input_img, cleaned_img, reference_img, benchmark_method, **kwargs) image_dict[ "img_cleaned_signal_to_border"] = signal_to_border( cleaned_img) image_dict[ "img_cleaned_signal_to_border_distance"] = signal_to_border_distance( cleaned_img) image_dict[ "img_cleaned_pemax_on_border"] = pemax_on_border( cleaned_img) image_dict["score"] = score_tuple image_dict["score_name"] = score_name_tuple image_dict[ "full_clean_execution_time_sec"] = full_clean_execution_time_sec image_dict["img_cleaned_sum_pe"] = float( np.nansum(cleaned_img)) image_dict["img_cleaned_min_pe"] = float( np.nanmin(cleaned_img)) image_dict["img_cleaned_max_pe"] = float( np.nanmax(cleaned_img)) image_dict["img_cleaned_num_pix"] = int( (cleaned_img[np.isfinite(cleaned_img)] > 0).sum()) cleaned_img1d = geometry_converter.image_2d_to_1d( cleaned_img, cam_id) try: hillas_params_2_cleaned_img = get_hillas_parameters( geom1d, cleaned_img1d, HILLAS_IMPLEMENTATION) # GEOM except Exception as e: hillas_params_2_cleaned_img = float('nan') print(e) #traceback.print_tb(e.__traceback__, file=sys.stdout) try: image_dict["img_cleaned_hillas_2_size"] = float( hillas_params_2_cleaned_img.intensity) except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_size"] = float('nan') try: image_dict[ "img_cleaned_hillas_2_cen_x"] = hillas_params_2_cleaned_img.x.value except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_cen_x"] = float('nan') try: image_dict[ "img_cleaned_hillas_2_cen_y"] = hillas_params_2_cleaned_img.y.value except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_cen_y"] = float('nan') try: image_dict[ "img_cleaned_hillas_2_length"] = hillas_params_2_cleaned_img.length.value except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_length"] = float( 'nan') try: image_dict[ "img_cleaned_hillas_2_width"] = hillas_params_2_cleaned_img.width.value except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_width"] = float('nan') try: image_dict[ "img_cleaned_hillas_2_r"] = hillas_params_2_cleaned_img.r.value except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_r"] = float('nan') try: image_dict[ "img_cleaned_hillas_2_phi"] = hillas_params_2_cleaned_img.phi.to( u.rad).value except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_phi"] = float('nan') try: image_dict[ "img_cleaned_hillas_2_psi"] = hillas_params_2_cleaned_img.psi.to( u.rad).value except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_psi"] = float('nan') try: image_dict["img_cleaned_hillas_2_miss"] = float( hillas_params_2_cleaned_img.miss.value) except: image_dict["img_cleaned_hillas_2_miss"] = float('nan') try: image_dict[ "img_cleaned_hillas_2_kurtosis"] = hillas_params_2_cleaned_img.kurtosis except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_kurtosis"] = float( 'nan') try: image_dict[ "img_cleaned_hillas_2_skewness"] = hillas_params_2_cleaned_img.skewness except AttributeError as e: print(e) image_dict["img_cleaned_hillas_2_skewness"] = float( 'nan') # PLOT IMAGES ######################################################### if plot or (saveplot is not None): image_list = [ geometry_converter.image_2d_to_1d(input_img, cam_id), geometry_converter.image_2d_to_1d( reference_img, cam_id), geometry_converter.image_2d_to_1d(cleaned_img, cam_id) ] title_list = [ "Input image", "Reference image", "Cleaned image" ] geom_list = [geom1d, geom1d, geom1d] hillas_list = [False, True, True] if plot: pywicta.io.images.plot_list(image_list, geom_list=geom_list, title_list=title_list, hillas_list=hillas_list, metadata_dict=image.meta) if saveplot is not None: basename, extension = os.path.splitext(saveplot) plot_file_path = "{}_E{}_T{}{}".format( basename, image.meta["event_id"], image.meta["tel_id"], extension) print("Saving {}".format(plot_file_path)) pywicta.io.images.mpl_save_list( image_list, geom_list=geom_list, output_file_path=plot_file_path, title_list=title_list, hillas_list=hillas_list, metadata_dict=image.meta) except Exception as e: print("Abort image {}: {} ({})".format(input_file_path, e, type(e))) if debug: # The following line print the full trackback traceback.print_tb(e.__traceback__, file=sys.stdout) if benchmark_method is not None: # http://docs.python.org/2/library/sys.html#sys.exc_info exc_type, exc_value, exc_traceback = sys.exc_info( ) # most recent (if any) by default ''' Reason this _can_ be bad: If an (unhandled) exception happens AFTER this, or if we do not delete the labels on (not much) older versions of Py, the reference we created can linger. traceback.format_exc/print_exc do this very thing, BUT note this creates a temp scope within the function. ''' error_dict = { 'filename': exc_traceback.tb_frame.f_code.co_filename, 'lineno': exc_traceback.tb_lineno, 'name': exc_traceback.tb_frame.f_code.co_name, 'type': exc_type.__name__, #'message' : exc_value.message 'message': str(e) } del ( exc_type, exc_value, exc_traceback ) # So we don't leave our local labels/objects dangling # This still isn't "completely safe", though! #error_dict = {"type": str(type(e)), # "message": str(e)} image_dict["error"] = error_dict finally: if benchmark_method is not None: io_list.append(image_dict) if benchmark_method is not None: error_list = [ image_dict["error"] for image_dict in io_list if "error" in image_dict ] print("{} images aborted".format(len(error_list))) # GENERAL EXPERIMENT METADATA output_dict = {} output_dict["benchmark_execution_time_sec"] = str( time.perf_counter() - launch_time) output_dict["date_time"] = str(datetime.datetime.now()) output_dict["class_name"] = self.__class__.__name__ output_dict["algo_code_ref"] = str( self.__class__.clean_image.__code__) output_dict["label"] = self.label output_dict["cmd"] = " ".join(sys.argv) output_dict["algo_params"] = cleaning_function_params if "noise_distribution" in output_dict["algo_params"]: del output_dict["algo_params"][ "noise_distribution"] # not JSON serializable... output_dict["benchmark_method"] = benchmark_method output_dict["system"] = " ".join(os.uname()) output_dict["io"] = io_list if output_file_path is not None: with open(output_file_path, "w") as fd: json.dump(output_dict, fd, sort_keys=True, indent=4) # pretty print format return output_dict
def clean_image(self, input_img, high_threshold=10., low_threshold=8., pixels_clusters_filtering="off", verbose=False, cam_id=None, output_data_dict=None, **kwargs): """Apply ctapipe's tail-cut image cleaning on ``input_img``. Note ---- The main difference with :mod:`ctapipe.image.cleaning.tailcuts_clean` is that here the cleaning function takes 2D Numpy arrays. Parameters ---------- input_img : array_like The image to clean. Should be a **2D** Numpy array. high_threshold : float The *core threshold* (a.k.a. *picture threshold*). low_threshold : float The *boundary threshold*. pixels_clusters_filtering : str Defines the method used to remove isolated pixels after the tail-cut image cleaning. Accepted values are: "off", "scipy" or "mars". - "off": don't apply any filtering after the tail-cut image cleaning. - "scipy": keep only the largest cluster of pixels after the tail-cut image cleaning. See :mod:`pywi.processing.filtering.pixel_clusters` for more information. - "mars": apply the same filtering than in CTA-Mars analysis, keep only *significant core pixels* that have at least two others *significant core pixels* among its neighbors (a *significant core pixels* is a pixel above the *core threshold*). verbose : bool Print additional messages if ``True``. cam_id : str The camera ID from which ``input_img`` came from: "ASTRICam", "CHEC", "DigiCam", "FlashCam", "NectarCam" or "LSTCam". output_data_dict : dict An optional dictionary used to transmit internal information to the caller. Returns ------- array_like The ``input_img`` after tail-cut image cleaning. This is a **2D** Numpy array (i.e. it should be converted with :func:`pywicta.io.geometry_converter.image_2d_to_1d` before any usage in ctapipe). """ if cam_id is None: raise Exception("cam_id have to be defined") # TODO if not (pixels_clusters_filtering.lower() in ("off", "scipy", "mars")): raise ValueError( 'pixels_clusters_filtering = {}. Accepted values are: "off", "scipy" or "mars".' .format(pixels_clusters_filtering)) # If low_threshold > high_threshold then low_threshold = high_threshold low_threshold = min(high_threshold, low_threshold) # 2D ARRAY (FITS IMAGE) TO CTAPIPE IMAGE ############### geom_1d = geometry_converter.get_geom1d(cam_id) img_1d = geometry_converter.image_2d_to_1d(input_img, cam_id) # APPLY TAILCUT CLEANING ############################## if pixels_clusters_filtering.lower() == "mars": if verbose: print("Mars pixels clusters filtering") mask = tailcuts_clean(geom_1d, img_1d, picture_thresh=high_threshold, boundary_thresh=low_threshold, keep_isolated_pixels=False, min_number_picture_neighbors=2) else: mask = tailcuts_clean(geom_1d, img_1d, picture_thresh=high_threshold, boundary_thresh=low_threshold, keep_isolated_pixels=True) img_1d[mask == False] = 0 # CTAPIPE IMAGE TO 2D ARRAY (FITS IMAGE) ############### cleaned_img_2d = geometry_converter.image_1d_to_2d(img_1d, cam_id) # KILL ISOLATED PIXELS ################################# img_cleaned_islands_delta_pe, img_cleaned_islands_delta_abs_pe, img_cleaned_islands_delta_num_pixels = filter_pixels_clusters_stats( cleaned_img_2d) img_cleaned_num_islands = number_of_pixels_clusters(cleaned_img_2d) if output_data_dict is not None: output_data_dict[ "img_cleaned_islands_delta_pe"] = img_cleaned_islands_delta_pe output_data_dict[ "img_cleaned_islands_delta_abs_pe"] = img_cleaned_islands_delta_abs_pe output_data_dict[ "img_cleaned_islands_delta_num_pixels"] = img_cleaned_islands_delta_num_pixels output_data_dict[ "img_cleaned_num_islands"] = img_cleaned_num_islands if pixels_clusters_filtering.lower() == "scipy": if verbose: print("Scipy pixels clusters filtering") cleaned_img_2d = scipy_pixels_clusters_filtering(cleaned_img_2d) return cleaned_img_2d
def prepare_event(self, source, return_stub=False, save_images=False): for event in source: self.event_cutflow.count("noCuts") if self.event_cutflow.cut("min2Tels trig", len(event.dl0.tels_with_data)): if return_stub: yield stub(event) else: continue self.calib(event) # telescope loop tot_signal = 0 dl1_phe_image = None mc_phe_image = None max_signals = {} n_pixel_dict = {} hillas_dict_reco = {} # for direction reconstruction hillas_dict = {} # for discrimination n_tels = { "tot": len(event.dl0.tels_with_data), "LST_LST_LSTCam": 0, "MST_MST_NectarCam": 0, "SST": 0, # add later correct names when testing on Paranal } n_cluster_dict = {} impact_dict_reco = {} # impact distance measured in tilt system point_azimuth_dict = {} point_altitude_dict = {} # Compute impact parameter in tilt system run_array_direction = event.mcheader.run_array_direction az, alt = run_array_direction[0], run_array_direction[1] ground_frame = GroundFrame() for tel_id in event.dl0.tels_with_data: self.image_cutflow.count("noCuts") camera = event.inst.subarray.tel[tel_id].camera # count the current telescope according to its size tel_type = str(event.inst.subarray.tel[tel_id]) # use ctapipe's functionality to get the calibrated image pmt_signal = event.dl1.tel[tel_id].image # Save the calibrated image after the gain has been chosen # automatically by ctapipe, together with the simulated one if save_images is True: dl1_phe_image = pmt_signal mc_phe_image = event.mc.tel[tel_id].photo_electron_image if self.cleaner_reco.mode == "tail": # tail uses only ctapipe # Cleaning used for direction reconstruction image_biggest, mask_reco = self.cleaner_reco.clean_image( pmt_signal, camera) # find all islands using this cleaning num_islands, labels = number_of_islands(camera, mask_reco) if num_islands == 1: # if only ONE islands is left ... # ...use directly the old mask and reduce dimensions # to make Hillas parametrization faster camera_biggest = camera[mask_reco] image_biggest = image_biggest[mask_reco] elif num_islands > 1: # if more islands survived.. # ...find the biggest one mask_biggest = largest_island(labels) # and also reduce dimensions camera_biggest = camera[mask_biggest] image_biggest = image_biggest[mask_biggest] else: # if no islands survived use old camera and image camera_biggest = camera # Cleaning used for score/energy estimation image_extended, mask_extended = self.cleaner_extended.clean_image( pmt_signal, camera) # find all islands with this cleaning # we will also register how many have been found n_cluster_dict[tel_id], labels = number_of_islands( camera, mask_extended) # NOTE: the next check shouldn't be necessary if we keep # all the isolated pixel clusters, but for now the # two cleanings are set the same in analysis.yml because # the performance of the extended one has never been really # studied in model estimation. # (This is a nice way to ask for volunteers :P) # if some islands survived if num_islands > 0: # keep all of them and reduce dimensions camera_extended = camera[mask_extended] image_extended = image_extended[mask_extended] else: # otherwise continue with the old camera and image camera_extended = camera # could this go into `hillas_parameters` ...? # this is basically the charge of ALL islands # not calculated later by the Hillas parametrization! max_signals[tel_id] = np.max(image_extended) else: # for wavelets we stick to old pywi-cta code try: # "try except FileNotFoundError" not clear to me, but for now it stays... with warnings.catch_warnings(): # Image with biggest cluster (reco cleaning) image_biggest, mask_reco = self.cleaner_reco.clean_image( pmt_signal, camera) image_biggest2d = geometry_converter.image_1d_to_2d( image_biggest, camera.cam_id) image_biggest2d = filter_pixels_clusters( image_biggest2d) image_biggest = geometry_converter.image_2d_to_1d( image_biggest2d, camera.cam_id) # Image for score/energy estimation (with clusters) image_extended, mask_extended = self.cleaner_extended.clean_image( pmt_signal, camera) # This last part was outside the pywi-cta block # before, but is indeed part of it because it uses # pywi-cta functions in the "extended" case # For cluster counts image_2d = geometry_converter.image_1d_to_2d( image_extended, camera.cam_id) n_cluster_dict[ tel_id] = pixel_clusters.number_of_pixels_clusters( array=image_2d, threshold=0) # could this go into `hillas_parameters` ...? max_signals[tel_id] = np.max(image_extended) except FileNotFoundError as e: # JLK, WHAT? print(e) continue # ============================================================== # Apply some selection if self.image_cutflow.cut("min pixel", image_biggest): continue if self.image_cutflow.cut("min charge", np.sum(image_biggest)): continue # do the hillas reconstruction of the images # QUESTION should this change in numpy behaviour be done here # or within `hillas_parameters` itself? # JLK: make selection on biggest cluster with np.errstate(invalid="raise", divide="raise"): try: moments_reco = hillas_parameters( camera_biggest, image_biggest) # for geometry (eg direction) moments = hillas_parameters( camera_extended, image_extended ) # for discrimination and energy reconstruction # if width and/or length are zero (e.g. when there is # only only one pixel or when all pixel are exactly # in one row), the parametrisation # won't be very useful: skip if self.image_cutflow.cut("poor moments", moments_reco): continue if self.image_cutflow.cut("close to the edge", moments_reco, camera.cam_id): continue if self.image_cutflow.cut("bad ellipticity", moments_reco): continue except (FloatingPointError, hillas.HillasParameterizationError): continue point_azimuth_dict[ tel_id] = event.mc.tel[tel_id].azimuth_raw * u.rad point_altitude_dict[ tel_id] = event.mc.tel[tel_id].altitude_raw * u.rad n_tels[tel_type] += 1 hillas_dict[tel_id] = moments hillas_dict_reco[tel_id] = moments_reco n_pixel_dict[tel_id] = len(np.where(image_extended > 0)[0]) tot_signal += moments.intensity n_tels["reco"] = len(hillas_dict_reco) n_tels["discri"] = len(hillas_dict) if self.event_cutflow.cut("min2Tels reco", n_tels["reco"]): if return_stub: yield stub(event) else: continue try: with warnings.catch_warnings(): warnings.simplefilter("ignore") # Reconstruction results reco_result = self.shower_reco.predict( hillas_dict_reco, event.inst, SkyCoord(alt=alt, az=az, frame="altaz"), { tel_id: SkyCoord( alt=point_altitude_dict[tel_id], az=point_azimuth_dict[tel_id], frame="altaz", ) # cycle only on tels which still have an image for tel_id in point_altitude_dict.keys() }, ) # Impact parameter for energy estimation (/ tel) subarray = event.inst.subarray for tel_id in hillas_dict.keys(): pos = subarray.positions[tel_id] tel_ground = SkyCoord(pos[0], pos[1], pos[2], frame=ground_frame) core_ground = SkyCoord( reco_result.core_x, reco_result.core_y, 0 * u.m, frame=ground_frame, ) # Should be better handled (tilted frame) impact_dict_reco[tel_id] = np.sqrt( (core_ground.x - tel_ground.x)**2 + (core_ground.y - tel_ground.y)**2) except Exception as e: print("exception in reconstruction:", e) raise if return_stub: yield stub(event) else: continue if self.event_cutflow.cut("direction nan", reco_result): if return_stub: yield stub(event) else: continue yield PreparedEvent( event=event, dl1_phe_image=dl1_phe_image, mc_phe_image=mc_phe_image, n_pixel_dict=n_pixel_dict, hillas_dict=hillas_dict, hillas_dict_reco=hillas_dict_reco, n_tels=n_tels, tot_signal=tot_signal, max_signals=max_signals, n_cluster_dict=n_cluster_dict, reco_result=reco_result, impact_dict=impact_dict_reco, )
def prepare_event(self, source, return_stub=False): # configuration for the camera calibrator # modifies the integration window to be more like in MARS # JLK, only for LST!!!! # Option for integration correction is done above cfg = Config() cfg["ChargeExtractorFactory"]["window_width"] = 5 cfg["ChargeExtractorFactory"]["window_shift"] = 2 self.calib = CameraCalibrator( config=cfg, extractor_product="LocalPeakIntegrator", eventsource=source, tool=None, ) for event in source: self.event_cutflow.count("noCuts") if self.event_cutflow.cut("min2Tels trig", len(event.dl0.tels_with_data)): if return_stub: yield stub(event) else: continue # calibrate the event self.calib.calibrate(event) # telescope loop tot_signal = 0 max_signals = {} n_pixel_dict = {} hillas_dict_reco = {} # for geometry hillas_dict = {} # for discrimination n_tels = { "tot": len(event.dl0.tels_with_data), "LST": 0, "MST": 0, "SST": 0, } n_cluster_dict = {} impact_dict_reco = {} # impact distance measured in tilt system point_azimuth_dict = {} point_altitude_dict = {} # To compute impact parameter in tilt system run_array_direction = event.mcheader.run_array_direction az, alt = run_array_direction[0], run_array_direction[1] ground_frame = GroundFrame() for tel_id in event.dl0.tels_with_data: self.image_cutflow.count("noCuts") camera = event.inst.subarray.tel[tel_id].camera # count the current telescope according to its size tel_type = event.inst.subarray.tel[tel_id].optics.tel_type # JLK, N telescopes before cut selection are not really interesting for # discrimination, too much fluctuations # n_tels[tel_type] += 1 # the camera image as a 1D array and stuff needed for calibration # Choose gain according to pywicta's procedure image_1d = simtel_event_to_images(event=event, tel_id=tel_id, ctapipe_format=True) pmt_signal = image_1d.input_image # calibrated image # clean the image try: with warnings.catch_warnings(): # Image with biggest cluster (reco cleaning) image_biggest = self.cleaner_reco.clean_image( pmt_signal, camera) image_biggest2d = geometry_converter.image_1d_to_2d( image_biggest, camera.cam_id) image_biggest2d = filter_pixels_clusters( image_biggest2d) image_biggest = geometry_converter.image_2d_to_1d( image_biggest2d, camera.cam_id) # Image for score/energy estimation (with clusters) image_extended = self.cleaner_extended.clean_image( pmt_signal, camera) except FileNotFoundError as e: # JLK, WHAT? print(e) continue # Apply some selection if self.image_cutflow.cut("min pixel", image_biggest): continue if self.image_cutflow.cut("min charge", np.sum(image_biggest)): continue # For cluster counts image_2d = geometry_converter.image_1d_to_2d( image_extended, camera.cam_id) n_cluster_dict[ tel_id] = pixel_clusters.number_of_pixels_clusters( array=image_2d, threshold=0) # could this go into `hillas_parameters` ...? max_signals[tel_id] = np.max(image_extended) # do the hillas reconstruction of the images # QUESTION should this change in numpy behaviour be done here # or within `hillas_parameters` itself? # JLK: make selection on biggest cluster with np.errstate(invalid="raise", divide="raise"): try: moments_reco = hillas_parameters( camera, image_biggest) # for geometry (eg direction) moments = hillas_parameters( camera, image_extended ) # for discrimination and energy reconstruction # if width and/or length are zero (e.g. when there is only only one # pixel or when all pixel are exactly in one row), the # parametrisation won't be very useful: skip if self.image_cutflow.cut("poor moments", moments_reco): # print('poor moments') continue if self.image_cutflow.cut("close to the edge", moments_reco, camera.cam_id): # print('close to the edge') continue if self.image_cutflow.cut("bad ellipticity", moments_reco): # print('bad ellipticity: w={}, l={}'.format(moments_reco.width, moments_reco.length)) continue except (FloatingPointError, hillas.HillasParameterizationError): continue point_azimuth_dict[ tel_id] = event.mc.tel[tel_id].azimuth_raw * u.rad point_altitude_dict[ tel_id] = event.mc.tel[tel_id].altitude_raw * u.rad n_tels[tel_type] += 1 hillas_dict[tel_id] = moments hillas_dict_reco[tel_id] = moments_reco n_pixel_dict[tel_id] = len(np.where(image_extended > 0)[0]) tot_signal += moments.intensity n_tels["reco"] = len(hillas_dict_reco) n_tels["discri"] = len(hillas_dict) if self.event_cutflow.cut("min2Tels reco", n_tels["reco"]): if return_stub: yield stub(event) else: continue try: with warnings.catch_warnings(): warnings.simplefilter("ignore") # Reconstruction results reco_result = self.shower_reco.predict( hillas_dict_reco, event.inst, point_altitude_dict, point_azimuth_dict, ) # shower_sys = TiltedGroundFrame(pointing_direction=HorizonFrame( # az=reco_result.az, # alt=reco_result.alt # )) # Impact parameter for energy estimation (/ tel) subarray = event.inst.subarray for tel_id in hillas_dict.keys(): pos = subarray.positions[tel_id] tel_ground = SkyCoord(pos[0], pos[1], pos[2], frame=ground_frame) # tel_tilt = tel_ground.transform_to(shower_sys) core_ground = SkyCoord( reco_result.core_x, reco_result.core_y, 0 * u.m, frame=ground_frame, ) # core_tilt = core_ground.transform_to(shower_sys) # Should be better handled (tilted frame) impact_dict_reco[tel_id] = np.sqrt( (core_ground.x - tel_ground.x)**2 + (core_ground.y - tel_ground.y)**2) except Exception as e: print("exception in reconstruction:", e) raise if return_stub: yield stub(event) else: continue if self.event_cutflow.cut("direction nan", reco_result): if return_stub: yield stub(event) else: continue yield PreparedEvent( event=event, n_pixel_dict=n_pixel_dict, hillas_dict=hillas_dict, hillas_dict_reco=hillas_dict_reco, n_tels=n_tels, tot_signal=tot_signal, max_signals=max_signals, n_cluster_dict=n_cluster_dict, reco_result=reco_result, impact_dict=impact_dict_reco, )