def test_ground_to_tilt(): from ctapipe.coordinates import GroundFrame, TiltedGroundFrame # define ground coordinate grd_coord = GroundFrame(x=1 * u.m, y=2 * u.m, z=0 * u.m) pointing_direction = SkyCoord(alt=90 * u.deg, az=0 * u.deg, frame=AltAz()) # Convert to tilted frame at zenith (should be the same) tilt_coord = grd_coord.transform_to( TiltedGroundFrame(pointing_direction=pointing_direction) ) assert tilt_coord.separation_3d(grd_coord) == 0 * u.m # Check 180 degree rotation reverses y coordinate pointing_direction = SkyCoord(alt=90 * u.deg, az=180 * u.deg, frame=AltAz()) tilt_coord = grd_coord.transform_to( TiltedGroundFrame(pointing_direction=pointing_direction) ) assert np.abs(tilt_coord.y + 2.0 * u.m) < 1e-5 * u.m # Check that if we look at horizon the x coordinate is 0 pointing_direction = SkyCoord(alt=0 * u.deg, az=0 * u.deg, frame=AltAz()) tilt_coord = grd_coord.transform_to( TiltedGroundFrame(pointing_direction=pointing_direction) ) assert np.abs(tilt_coord.x) < 1e-5 * u.m
def get_prediction(self, tel_id, shower_reco, energy_reco): horizon_seed = HorizonFrame(az=shower_reco.az, alt=shower_reco.alt) nominal_seed = horizon_seed.transform_to( NominalFrame(array_direction=horizon_seed)) source_x = nominal_seed.x.to(u.rad).value source_y = nominal_seed.y.to(u.rad).value ground = GroundFrame(x=shower_reco.core_x, y=shower_reco.core_y, z=0 * u.m) tilted = ground.transform_to( TiltedGroundFrame(pointing_direction=self.array_direction)) tilt_x = tilted.x.to(u.m).value tilt_y = tilted.y.to(u.m).value zenith = 90 * u.deg - self.array_direction.alt x_max = shower_reco.h_max / np.cos(zenith) # Calculate expected Xmax given this energy x_max_exp = guess_shower_depth(energy_reco.energy) # Convert to binning of Xmax, addition of 100 can probably be removed x_max_bin = x_max - x_max_exp # Check for range if x_max_bin > 250 * (u.g * u.cm**-2): x_max_bin = 250 * (u.g * u.cm**-2) if x_max_bin < -250 * (u.g * u.cm**-2): x_max_bin = -250 * (u.g * u.cm**-2) x_max_bin = x_max_bin.value impact = np.sqrt( pow(self.tel_pos_x[tel_id] - tilt_x, 2) + pow(self.tel_pos_y[tel_id] - tilt_y, 2)) phi = np.arctan2((self.tel_pos_y[tel_id] - tilt_y), (self.tel_pos_x[tel_id] - tilt_x)) pix_x_rot, pix_y_rot = self.rotate_translate(self.pixel_x[tel_id] * -1, self.pixel_y[tel_id], source_x, source_y, phi) prediction = self.image_prediction(self.type[tel_id], (90 * u.deg) - shower_reco.alt, shower_reco.az, energy_reco.energy.value, impact, x_max_bin, pix_x_rot * (180 / math.pi), pix_y_rot * (180 / math.pi)) prediction *= self.scale[self.type[tel_id]] # prediction *= self.pixel_area[tel_id] prediction[prediction < 0] = 0 prediction[np.isnan(prediction)] = 0 return prediction
def grd_to_tilt(): grd_coord = GroundFrame(x=1 * u.m, y=2 * u.m, z=0 * u.m) tilt_coord = grd_coord.transform_to( TiltedGroundFrame( pointing_direction=AltAz(alt=90 * u.deg, az=180 * u.deg))) print(project_to_ground(tilt_coord)) print("Tilted Coordinate", tilt_coord)
def draw_tilted_surface(self, shower_seed, energy_seed, bins=50, core_range=100 * u.m): """ Simple reconstruction for evaluating the likelihood in a grid across the nominal system, fixing all values but the core position of the gamma rays. Useful for checking the reconstruction performance of the algorithm Parameters ---------- shower_seed: ReconstructedShowerContainer Best fit ImPACT shower geometry energy_seed: ReconstructedEnergyContainer Best fit ImPACT energy bins: int Number of bins in surface evaluation nominal_range: Quantity Range over which to create likelihood surface Returns ------- ndarray, ndarray, ndarray: Bin centres in X and Y coordinates and the values of the likelihood at each position """ horizon_seed = HorizonFrame(az=shower_seed.az, alt=shower_seed.alt) nominal_seed = horizon_seed.transform_to( NominalFrame(array_direction=self.array_direction)) source_x = nominal_seed.x[0].to(u.rad).value source_y = nominal_seed.y[0].to(u.rad).value ground = GroundFrame(x=shower_seed.core_x, y=shower_seed.core_y, z=0 * u.m) tilted = ground.transform_to( TiltedGroundFrame(pointing_direction=self.array_direction) ) tilt_x = tilted.x.to(u.m) tilt_y = tilted.y.to(u.m) x_ground_list = np.linspace(tilt_x - core_range, tilt_x + core_range, num=bins) y_ground_list = np.linspace(tilt_y - core_range, tilt_y + core_range, num=bins) w = np.zeros([bins, bins]) zenith = 90*u.deg - self.array_direction.alt for xb in range(bins): for yb in range(bins): x_max_scale = shower_seed.h_max / \ self.get_shower_max(source_x, source_y, x_ground_list[xb].value, y_ground_list[yb].value, zenith.to(u.rad).value) w[xb][yb] = self.get_likelihood(source_x, source_y, x_ground_list[xb].value, y_ground_list[yb].value, energy_seed.energy.value, x_max_scale) return x_ground_list, y_ground_list, w
def get_prediction(self, tel_id, shower_reco, energy_reco): horizon_seed = HorizonFrame(az=shower_reco.az, alt=shower_reco.alt) nominal_seed = horizon_seed.transform_to( NominalFrame(array_direction=horizon_seed)) source_x = nominal_seed.x.to(u.rad).value source_y = nominal_seed.y.to(u.rad).value print(self.array_direction[0]) ground = GroundFrame(x=shower_reco.core_x, y=shower_reco.core_y, z=0 * u.m) tilted = ground.transform_to( TiltedGroundFrame(pointing_direction=HorizonFrame( alt=self.array_direction[0], az=self.array_direction[1]))) tilt_x = tilted.x.to(u.m).value tilt_y = tilted.y.to(u.m).value zenith = 90 * u.deg - self.array_direction[0] azimuth = self.array_direction[1] x_max_exp = 300 + 93 * np.log10(energy_reco.energy.value) x_max = shower_reco.h_max / np.cos(zenith) # Convert to binning of Xmax, addition of 100 can probably be removed x_max_bin = x_max.value - x_max_exp if x_max_bin > 100: x_max_bin = 100 if x_max_bin < -100: x_max_bin = -100 impact = np.sqrt( pow(self.tel_pos_x[tel_id] - tilt_x, 2) + pow(self.tel_pos_y[tel_id] - tilt_y, 2)) phi = np.arctan2((self.tel_pos_y[tel_id] - tilt_y), (self.tel_pos_x[tel_id] - tilt_x)) pix_x_rot, pix_y_rot = self.rotate_translate(self.pixel_x[tel_id] * -1, self.pixel_y[tel_id], source_x, source_y, phi) prediction = self.image_prediction(self.type[tel_id], 20 * u.deg, 0 * u.deg, energy_reco.energy.value, impact, x_max_bin, pix_x_rot * (180 / math.pi), pix_y_rot * (180 / math.pi)) prediction *= self.scale[self.type[tel_id]] prediction[prediction < 0] = 0 prediction[np.isnan(prediction)] = 0 return prediction
def test_ground_to_tilt_many_to_many(): from ctapipe.coordinates import GroundFrame, TiltedGroundFrame # define ground coordinate grd_coord = GroundFrame(x=[1, 1] * u.m, y=[2, 2] * u.m, z=[0, 0] * u.m) pointing_direction = SkyCoord( alt=[90, 90, 90], az=[0, 0, 90], frame=AltAz(), unit=u.deg ) with raises(ValueError): # there will be a shape mismatch in matrix multiplication grd_coord.transform_to(TiltedGroundFrame(pointing_direction=pointing_direction))
def test_ground_frame_roundtrip(): """test transform from sky to ground roundtrip""" from ctapipe.coordinates import GroundFrame, TiltedGroundFrame normal = SkyCoord(alt=70 * u.deg, az=0 * u.deg, frame=AltAz()) coord = SkyCoord(x=0, y=10, z=5, unit=u.m, frame=GroundFrame()) tilted = coord.transform_to(TiltedGroundFrame(pointing_direction=normal)) back = tilted.transform_to(GroundFrame()) assert u.isclose(coord.x, back.x, atol=1e-12 * u.m) assert u.isclose(coord.y, back.y, atol=1e-12 * u.m) assert u.isclose(coord.z, back.z, atol=1e-12 * u.m)
def add_impact_point(self, label=None, status="mc", frame="ground", gnd_reco_pos=None): if label is None: label = "" if frame == "ground": if status == "mc": core_x_pos = self.event.mc.core_x.to_value(u.m) core_y_pos = self.event.mc.core_y.to_value(u.m) core_z_pos = 0 elif status == "reco": core_x_pos = gnd_reco_pos.x.to_value(u.m) core_y_pos = gnd_reco_pos.y.to_value(u.m) core_z_pos = 0 elif frame == "tilted": tilted_frame = TiltedGroundFrame( pointing_direction=self.array_pointing) if status == "mc": gnd_core_mc = GroundFrame(x=self.event.mc.core_x, y=self.event.mc.core_y, z=0.0 * u.m) tilted_core_mc = gnd_core_mc.transform_to(tilted_frame) elif status == "reco": tilted_core_mc = gnd_reco_pos.transform_to(tilted_frame) core_x_pos = tilted_core_mc.x.to_value(u.m) core_y_pos = tilted_core_mc.y.to_value(u.m) core_z_pos = 0 tel_label = create_extruded_text(text=label) tel_label = scale_object(tel_label, 20) # those "magic numbers" are needed to have the center of the first letter # of the label in the exact position. Depends on the fontsize. tel_label = translate_object( tel_label, center=[core_x_pos - 12, core_y_pos - 9, core_z_pos]) mapper = vtk.vtkPolyDataMapper() mapper.SetInputConnection(tel_label.GetOutputPort()) actor = vtk.vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetColor([0, 0, 255]) if frame == "tilted": actor.RotateZ(-self.array_pointing.az.value) actor.RotateY(90 - self.array_pointing.alt.value) self.ren.AddActor(actor)
def test_ground_to_tilt_many_to_one(): from ctapipe.coordinates import GroundFrame, TiltedGroundFrame # define ground coordinate grd_coord = GroundFrame(x=[1, 1] * u.m, y=[2, 2] * u.m, z=[0, 0] * u.m) pointing_direction = SkyCoord(alt=90, az=0, frame=AltAz(), unit=u.deg) # Convert to tilted frame at zenith (should be the same) tilt_coord = grd_coord.transform_to( TiltedGroundFrame(pointing_direction=pointing_direction) ) # We do a one-to-one conversion assert len(tilt_coord.data) == 2
def estimate_core_position(self, hillas_dict, array_pointing): """ Estimate the core position by intersection the major ellipse lines of each telescope. Parameters ----------- hillas_dict: dict[HillasContainer] dictionary of hillas moments array_pointing: SkyCoord[HorizonFrame] Pointing direction of the array Returns ----------- core_x: u.Quantity estimated x position of impact core_y: u.Quantity estimated y position of impact """ if self.divergent_mode: psi = u.Quantity(list(self.corrected_angle_dict.values())) else: psi = u.Quantity([h.psi for h in hillas_dict.values()]) z = np.zeros(len(psi)) uvw_vectors = np.column_stack([np.cos(psi).value, np.sin(psi).value, z]) tilted_frame = TiltedGroundFrame(pointing_direction=array_pointing) ground_frame = GroundFrame() positions = [ ( SkyCoord(*plane.pos, frame=ground_frame) .transform_to(tilted_frame) .cartesian.xyz ) for plane in self.hillas_planes.values() ] core_position = line_line_intersection_3d(uvw_vectors, positions) core_pos_tilted = SkyCoord( x=core_position[0] * u.m, y=core_position[1] * u.m, frame=tilted_frame ) core_pos = project_to_ground(core_pos_tilted) return core_pos.x, core_pos.y
def __init__(self, event, telescopes_ids=None): self.event = event if telescopes_ids is None: self.tel_ids = list(event.r0.tels_with_data) else: self.tel_ids = telescopes_ids # dictionary to save the actor per each telescope self.tel_id = {} self.tel_coords = {} # event.inst.subarray.tel_coords self.subarray = self.event.inst.subarray for tel_id in self.tel_ids: coords = self.subarray.tel_coords[ self.subarray.tel_indices[tel_id]] self.tel_coords[tel_id] = coords self.ren = vtk.vtkRenderer() self.ren.SetBackground(0.9, 0.9, 0.9) try: self.array_pointing = SkyCoord( alt=np.rad2deg(event.mcheader.run_array_direction[1]), az=np.rad2deg(event.mcheader.run_array_direction[0]), frame=AltAz()) except ValueError: self.array_pointing = SkyCoord(alt=event.mc.alt, az=event.mc.az, frame=AltAz()) self.tilted_frame = TiltedGroundFrame( pointing_direction=self.array_pointing) self.pointing = {} for tel_id in self.tel_ids: self.tel_id[tel_id] = [] self.pointing[tel_id] = { 'alt': np.rad2deg(event.mc.tel[tel_id].altitude_raw), 'az': np.rad2deg(event.mc.tel[tel_id].azimuth_raw) }
def add_tilted_tels(self, tel_labels=False): tilted_positions = {} tel_coords_gnd = self.subarray.tel_coords tilted_system = TiltedGroundFrame( pointing_direction=self.array_pointing) tilt_tel_pos = tel_coords_gnd.transform_to(tilted_system) for tel_id in self.tel_ids: tel_x_pos = tilt_tel_pos[ self.subarray.tel_indices[tel_id]].x.to_value(u.m) tel_y_pos = tilt_tel_pos[ self.subarray.tel_indices[tel_id]].y.to_value(u.m) tel_z_pos = 0 tilted_positions[tel_id] = [tel_x_pos, tel_y_pos, tel_z_pos] if tel_labels is True: self.tel_labels(frame="tilted", tilted_pos=tilted_positions) self.ren.AddActor( add_tilted_positions(tilted_positions, array_pointing=self.array_pointing))
def grd_to_tilt(): grd_coord = GroundFrame(x=1 * u.m, y=2 * u.m, z=0 * u.m) tilt_coord = grd_coord.transform_to( TiltedGroundFrame(pointing_direction=[90 * u.deg, 180 * u.deg])) print("Tilted Coordinate", tilt_coord)
def predict(self, shower_seed, energy_seed): """ Parameters ---------- shower_seed: ReconstructedShowerContainer Seed shower geometry to be used in the fit energy_seed: ReconstructedEnergyContainer Seed energy to be used in fit Returns ------- ReconstructedShowerContainer, ReconstructedEnergyContainer: Reconstructed ImPACT shower geometry and energy """ horizon_seed = HorizonFrame(az=shower_seed.az, alt=shower_seed.alt) nominal_seed = horizon_seed.transform_to( NominalFrame(array_direction=self.array_direction)) print(nominal_seed) print(horizon_seed) print(self.array_direction) source_x = nominal_seed.x[0].to(u.rad).value source_y = nominal_seed.y[0].to(u.rad).value ground = GroundFrame(x=shower_seed.core_x, y=shower_seed.core_y, z=0 * u.m) tilted = ground.transform_to( TiltedGroundFrame(pointing_direction=self.array_direction)) tilt_x = tilted.x.to(u.m).value tilt_y = tilted.y.to(u.m).value lower_en_limit = energy_seed.energy * 0.5 en_seed = energy_seed.energy if lower_en_limit < 0.04 * u.TeV: lower_en_limit = 0.04 * u.TeV en_seed = 0.041 * u.TeV seed = (source_x, source_y, tilt_x, tilt_y, en_seed.value, 0.8) step = (0.001, 0.001, 10, 10, en_seed.value * 0.1, 0.1) limits = ((source_x - 0.01, source_x + 0.01), (source_y - 0.01, source_y + 0.01), (tilt_x - 100, tilt_x + 100), (tilt_y - 100, tilt_y + 100), (lower_en_limit.value, en_seed.value * 2), (0.5, 2)) fit_params, errors = self.minimise(params=seed, step=step, limits=limits, minimiser_name=self.minimiser_name) # container class for reconstructed showers ''' shower_result = ReconstructedShowerContainer() nominal = NominalFrame(x=fit_params[0] * u.rad, y=fit_params[1] * u.rad, array_direction=self.array_direction) horizon = nominal.transform_to(HorizonFrame()) shower_result.alt, shower_result.az = horizon.alt, horizon.az tilted = TiltedGroundFrame(x=fit_params[2] * u.m, y=fit_params[3] * u.m, pointing_direction=self.array_direction) ground = project_to_ground(tilted) shower_result.core_x = ground.x shower_result.core_y = ground.y shower_result.is_valid = True shower_result.alt_uncert = np.nan shower_result.az_uncert = np.nan shower_result.core_uncert = np.nan zenith = 90 * u.deg - self.array_direction.alt shower_result.h_max = fit_params[5] * \ self.get_shower_max(fit_params[0], fit_params[1], fit_params[2], fit_params[3], zenith.to(u.rad).value) shower_result.h_max_uncert = errors[5] * shower_result.h_max shower_result.goodness_of_fit = np.nan shower_result.tel_ids = list(self.image.keys()) energy_result = ReconstructedEnergyContainer() energy_result.energy = fit_params[4] * u.TeV energy_result.energy_uncert = errors[4] * u.TeV energy_result.is_valid = True energy_result.tel_ids = list(self.image.keys()) # Return interesting stuff return shower_result, energy_result
def estimate_core_position(self, event, hillas_dict, array_pointing, corrected_angle_dict, hillas_planes): """ Estimate the core position by intersection the major ellipse lines of each telescope. Parameters ----------- hillas_dict: dict[HillasContainer] dictionary of hillas moments array_pointing: SkyCoord[HorizonFrame] Pointing direction of the array Returns ----------- core_x: u.Quantity estimated x position of impact core_y: u.Quantity estimated y position of impact Notes ----- The part of the algorithm taking into account divergent pointing mode and the usage of a corrected psi angle is explained in [gasparetto]_ section 7.1.4. """ # Since psi has been recalculated in the fake CameraFrame # it doesn't need any further corrections because it is now independent # of both pointing and cleaning/parametrization frame. # This angle will be used to visualize the telescope-wise directions of # the shower core the ground. psi_core = corrected_angle_dict # Record these values for tel_id in hillas_dict.keys(): event.dl1.tel[tel_id].parameters.core.psi = psi_core[tel_id] # Transform them for numpy psi = u.Quantity(list(psi_core.values())) # Estimate the position of the shower's core # from the TiltedFram to the GroundFrame z = np.zeros(len(psi)) uvw_vectors = np.column_stack( [np.cos(psi).value, np.sin(psi).value, z]) tilted_frame = TiltedGroundFrame(pointing_direction=array_pointing) ground_frame = GroundFrame() positions = [(SkyCoord( *plane.pos, frame=ground_frame).transform_to(tilted_frame).cartesian.xyz) for plane in hillas_planes.values()] core_position = line_line_intersection_3d(uvw_vectors, positions) core_pos_tilted = SkyCoord(x=core_position[0] * u.m, y=core_position[1] * u.m, frame=tilted_frame) core_pos = project_to_ground(core_pos_tilted) return core_pos.x, core_pos.y
def predict(self, hillas_dict, subarray, array_pointing, telescopes_pointings=None): """ Parameters ---------- hillas_dict: dict Dictionary containing Hillas parameters for all telescopes in reconstruction inst : ctapipe.io.InstrumentContainer instrumental description array_pointing: SkyCoord[AltAz] pointing direction of the array telescopes_pointings: dict[SkyCoord[AltAz]] dictionary of pointing direction per each telescope Returns ------- ReconstructedShowerContainer: """ # filter warnings for missing obs time. this is needed because MC data has no obs time warnings.filterwarnings(action="ignore", category=MissingFrameAttributeWarning) # stereoscopy needs at least two telescopes if len(hillas_dict) < 2: raise TooFewTelescopesException( "need at least two telescopes, have {}".format(len(hillas_dict)) ) # check for np.nan or 0 width's as these screw up weights if any([np.isnan(hillas_dict[tel]["width"].value) for tel in hillas_dict]): raise InvalidWidthException( "A HillasContainer contains an ellipse of width==np.nan" ) if any([hillas_dict[tel]["width"].value == 0 for tel in hillas_dict]): raise InvalidWidthException( "A HillasContainer contains an ellipse of width==0" ) if telescopes_pointings is None: telescopes_pointings = { tel_id: array_pointing for tel_id in hillas_dict.keys() } tilted_frame = TiltedGroundFrame(pointing_direction=array_pointing) grd_coord = subarray.tel_coords tilt_coord = grd_coord.transform_to(tilted_frame) tel_ids = list(hillas_dict.keys()) tel_indices = subarray.tel_ids_to_indices(tel_ids) tel_x = { tel_id: tilt_coord.x[tel_index] for tel_id, tel_index in zip(tel_ids, tel_indices) } tel_y = { tel_id: tilt_coord.y[tel_index] for tel_id, tel_index in zip(tel_ids, tel_indices) } nom_frame = NominalFrame(origin=array_pointing) hillas_dict_mod = {} for tel_id, hillas in hillas_dict.items(): if isinstance(hillas, CameraHillasParametersContainer): focal_length = subarray.tel[tel_id].optics.equivalent_focal_length camera_frame = CameraFrame( telescope_pointing=telescopes_pointings[tel_id], focal_length=focal_length, ) cog_coords = SkyCoord(x=hillas.x, y=hillas.y, frame=camera_frame) cog_coords_nom = cog_coords.transform_to(nom_frame) else: telescope_frame = TelescopeFrame( telescope_pointing=telescopes_pointings[tel_id] ) cog_coords = SkyCoord( fov_lon=hillas.fov_lon, fov_lat=hillas.fov_lat, frame=telescope_frame, ) cog_coords_nom = cog_coords.transform_to(nom_frame) hillas_dict_mod[tel_id] = HillasParametersContainer( fov_lon=cog_coords_nom.fov_lon, fov_lat=cog_coords_nom.fov_lat, psi=hillas.psi, width=hillas.width, length=hillas.length, intensity=hillas.intensity, ) src_fov_lon, src_fov_lat, err_fov_lon, err_fov_lat = self.reconstruct_nominal( hillas_dict_mod ) core_x, core_y, core_err_x, core_err_y = self.reconstruct_tilted( hillas_dict_mod, tel_x, tel_y ) err_fov_lon *= u.rad err_fov_lat *= u.rad nom = SkyCoord( fov_lon=src_fov_lon * u.rad, fov_lat=src_fov_lat * u.rad, frame=nom_frame ) sky_pos = nom.transform_to(array_pointing.frame) tilt = SkyCoord(x=core_x * u.m, y=core_y * u.m, frame=tilted_frame) grd = project_to_ground(tilt) x_max = self.reconstruct_xmax( nom.fov_lon, nom.fov_lat, tilt.x, tilt.y, hillas_dict_mod, tel_x, tel_y, 90 * u.deg - array_pointing.alt, ) src_error = np.sqrt(err_fov_lon ** 2 + err_fov_lat ** 2) result = ReconstructedGeometryContainer( alt=sky_pos.altaz.alt.to(u.rad), az=sky_pos.altaz.az.to(u.rad), core_x=grd.x, core_y=grd.y, core_uncert=u.Quantity(np.sqrt(core_err_x ** 2 + core_err_y ** 2), u.m), tel_ids=[h for h in hillas_dict_mod.keys()], average_intensity=np.mean([h.intensity for h in hillas_dict_mod.values()]), is_valid=True, alt_uncert=src_error.to(u.rad), az_uncert=src_error.to(u.rad), h_max=x_max, h_max_uncert=u.Quantity(np.nan * x_max.unit), goodness_of_fit=np.nan, ) return result
def tilted_grid(event, tel_pos=False, zen_az_arrows=False): """ Return the telescopes positions in the TiltedGroundFrame and plot them according to azimuth and zenith of simulation :param event: event selected from simtel :param tel_pos: (bool) If True, plot the telescopes as spheres :param zen_az_arrows: plot curved arrows for ZEN and AZ in titled ref frame :return: """ alt = event.mcheader.run_array_direction[1] az = event.mcheader.run_array_direction[0] array_pointing = HorizonFrame(alt=alt, az=az) ground_coordinates = GroundFrame( x=event.inst.subarray.tel_coords.x, y=event.inst.subarray.tel_coords.y, z=event.inst.subarray.tel_coords.z, # *0+15.0*u.m, pointing_direction=array_pointing) tilted_system = TiltedGroundFrame(pointing_direction=array_pointing) tilted = ground_coordinates.transform_to(tilted_system) grid_unit = 20000 # in centimeters tilted_system = union() # ADD TELESCOPES AS SPHERES if tel_pos: # for i in range(tilted.x.size): for i in range(50): coords = [100 * tilted.x[i].value, 100 * tilted.y[i].value] position = translate(coords)(color([1, 0, 0])(sphere(r=800))) tilted_system.add(position) # add GRID grid_tilted = grid_plane( grid_unit=grid_unit, count=2 * int(100 * np.max(np.abs(ground_coordinates.x.value)) / grid_unit), line_weight=200, plane='xy') grid_tilted = color([1, 0, 0, 0.5])(grid_tilted) grid_tilted = grid_tilted + ref_arrow_2d( 8000, label={ 'x': "x_tilted", 'y': "y_tilted" }, origin=(0, 0)) tilted_system = rotate([0, 90 - alt.to('deg').value, az.to('deg').value])(tilted_system) tilted_system.add(grid_tilted) arr_curved_az = color([1, 1, 0])(rot_arrow(8000, az.to('deg').value, 0, label='AZ')) tilted_system.add(arr_curved_az) arr_curved_alt = color([1, 0, 1])(rot_arrow(8000, 0, 90 - alt.to('deg').value, label='ZEN')) arr_curved_alt = rotate([90, 0, 0])(arr_curved_alt) tilted_system.add(arr_curved_alt) return tilted_system
def predict(self, shower_seed, energy_seed): """ Parameters ---------- source_x: float Initial guess of source position in the nominal frame source_y: float Initial guess of source position in the nominal frame core_x: float Initial guess of the core position in the tilted system core_y: float Initial guess of the core position in the tilted system energy: float Initial guess of energy Returns ------- Shower object with fit results """ horizon_seed = HorizonFrame(az=shower_seed.az, alt=shower_seed.alt) nominal_seed = horizon_seed.transform_to( NominalFrame(array_direction=self.array_direction)) source_x = nominal_seed.x.to(u.rad).value source_y = nominal_seed.y.to(u.rad).value ground = GroundFrame(x=shower_seed.core_x, y=shower_seed.core_y, z=0 * u.m) tilted = ground.transform_to( TiltedGroundFrame(pointing_direction=self.array_direction)) tilt_x = tilted.x.to(u.m).value tilt_y = tilted.y.to(u.m).value lower_en_limit = energy_seed.energy * 0.1 if lower_en_limit < 0.04 * u.TeV: lower_en_limit = 0.04 * u.TeV # Create Minuit object with first guesses at parameters, strip away the # units as Minuit doesnt like them min = Minuit(self.get_likelihood, print_level=1, source_x=source_x, error_source_x=0.01 / 57.3, fix_source_x=False, limit_source_x=(source_x - 0.5 / 57.3, source_x + 0.5 / 57.3), source_y=source_y, error_source_y=0.01 / 57.3, fix_source_y=False, limit_source_y=(source_y - 0.5 / 57.3, source_y + 0.5 / 57.3), core_x=tilt_x, error_core_x=10, limit_core_x=(tilt_x - 200, tilt_x + 200), core_y=tilt_y, error_core_y=10, limit_core_y=(tilt_y - 200, tilt_y + 200), energy=energy_seed.energy.value, error_energy=energy_seed.energy.value * 0.05, limit_energy=(lower_en_limit.value, energy_seed.energy.value * 10.), x_max_scale=1, error_x_max_scale=0.1, limit_x_max_scale=(0.5, 2), fix_x_max_scale=False, errordef=1) min.tol *= 1000 min.strategy = 0 # Perform minimisation migrad = min.migrad() fit_params = min.values errors = min.errors # print(migrad) # print(min.minos()) # container class for reconstructed showers ''' shower_result = ReconstructedShowerContainer() nominal = NominalFrame(x=fit_params["source_x"] * u.rad, y=fit_params["source_y"] * u.rad, array_direction=self.array_direction) horizon = nominal.transform_to(HorizonFrame()) shower_result.alt, shower_result.az = horizon.alt, horizon.az tilted = TiltedGroundFrame(x=fit_params["core_x"] * u.m, y=fit_params["core_y"] * u.m, pointing_direction=self.array_direction) ground = project_to_ground(tilted) shower_result.core_x = ground.x shower_result.core_y = ground.y shower_result.is_valid = True shower_result.alt_uncert = np.nan shower_result.az_uncert = np.nan shower_result.core_uncert = np.nan zenith = 90 * u.deg - self.array_direction[0] shower_result.h_max = fit_params["x_max_scale"] * \ self.get_shower_max(fit_params["source_x"], fit_params["source_y"], fit_params["core_x"], fit_params["core_y"], zenith.to(u.rad).value) shower_result.h_max_uncert = errors["x_max_scale"] * shower_result.h_max shower_result.goodness_of_fit = np.nan shower_result.tel_ids = list(self.image.keys()) energy_result = ReconstructedEnergyContainer() energy_result.energy = fit_params["energy"] * u.TeV energy_result.energy_uncert = errors["energy"] * u.TeV energy_result.is_valid = True energy_result.tel_ids = list(self.image.keys()) # Return interesting stuff return shower_result, energy_result
continue # calibrating the event calib(event) hillas_dict = {} timing_dict = {} # plot the core position, which must be transformed from the tilted # system to the system that the ArrayDisplay is in (default # GroundFrame) point_dir = SkyCoord( az=event.pointing.array_azimuth, alt=event.pointing.array_altitude, frame=AltAz(), ) tiltedframe = TiltedGroundFrame(pointing_direction=point_dir) if markers: for marker in markers: marker.remove() core_coord = SkyCoord( x=event.simulation.shower.core_x, y=event.simulation.shower.core_y, frame=tiltedframe, ).transform_to(array_disp.frame) markers = ax.plot([core_coord.x.value], [core_coord.y.value], "r+", markersize=10) # plot the hit pattern (triggered tels).
def read_templates(self, filename, max_events=1e9): """ This is a pretty standard ctapipe event loop that calibrates events, rotates them into a common frame and then stores the pixel values in a list :param filename: str Location of input :param max_events: int Maximum number of events to include in the loop :return: tuple Return 3 lists of amplitude and rotated x,y positions of all pixels in all events """ # Create dictionaries to contain our output templates = dict() # Pixel amplitude templates_xb = dict() # Rotated X position templates_yb = dict() # Rotated Y positions # Create a dummy time for our AltAz objects dummy_time = Time('2010-01-01T00:00:00', format='isot', scale='utc') if self.verbose: print("Reading", filename.strip()) calibrator = CameraCalibrator() with event_source(input_url=filename.strip()) as source: grd_tel = None num = 0 # Event counter for event in tqdm(source): alt = event.mcheader.run_array_direction[1] if alt > 90. * u.deg: alt = 90. * u.deg point = SkyCoord(alt=alt, az=event.mcheader.run_array_direction[0], frame=AltAz(obstime=dummy_time)) mc = event.mc # Create coordinate objects for source position src = SkyCoord(alt=mc.alt.value * u.rad, az=mc.az.value * u.rad, frame=AltAz(obstime=dummy_time)) if point.separation(point) > self.maximum_offset: continue # And transform into nominal system (where we store our templates) source_direction = src.transform_to(NominalFrame(origin=point)) # Perform calibration of images try: calibrator(event) except ZeroDivisionError: print( "ZeroDivisionError in calibrator, skipping this event") continue # Store simulated event energy energy = mc.energy # Store ground position of all telescopes # We only want to do this once, but has to be done in event loop if grd_tel is None: grd_tel = event.inst.subarray.tel_coords # Convert to tilted system tilt_tel = grd_tel.transform_to( TiltedGroundFrame(pointing_direction=point)) # Calculate core position in tilted system grd_core_true = SkyCoord(x=np.asarray(mc.core_x) * u.m, y=np.asarray(mc.core_y) * u.m, z=np.asarray(0) * u.m, frame=GroundFrame()) tilt_core_true = grd_core_true.transform_to( TiltedGroundFrame(pointing_direction=point)) # Loop over triggered telescopes for tel_id in event.dl0.tels_with_data: # Get pixel signal pmt_signal = event.dl1.tel[tel_id].image # Get pixel coordinates and convert to the nominal system geom = event.inst.subarray.tel[tel_id].camera fl = event.inst.subarray.tel[tel_id].optics.equivalent_focal_length * \ self.eff_fl camera_coord = SkyCoord(x=geom.pix_x, y=geom.pix_y, frame=CameraFrame( focal_length=fl, telescope_pointing=point)) nom_coord = camera_coord.transform_to( NominalFrame(origin=point)) y = nom_coord.delta_az.to(u.deg) x = nom_coord.delta_alt.to(u.deg) # Calculate expected rotation angle of the image phi = np.arctan2((tilt_tel.y[tel_id - 1] - tilt_core_true.y), (tilt_tel.x[tel_id - 1] - tilt_core_true.x)) + \ 180 * u.deg phi += self.rotation_angle # And the impact distance of the shower impact = np.sqrt(np.power(tilt_tel.x[tel_id - 1] - tilt_core_true.x, 2) + np.power(tilt_tel.y[tel_id - 1] - tilt_core_true.y, 2)). \ to(u.m).value # now rotate and translate our images such that they lie on top of one # another x, y = \ ImPACTReconstructor.rotate_translate(x, y, source_direction.delta_alt, source_direction.delta_az, phi) x *= -1 # We only want to keep pixels that fall within the bounds of our # final template mask = np.logical_and(x > self.bounds[0][0] * u.deg, x < self.bounds[0][1] * u.deg) mask = np.logical_and(mask, y < self.bounds[1][1] * u.deg) mask = np.logical_and(mask, y > self.bounds[1][0] * u.deg) # Make sure everythin is 32 bit x = x[mask].astype(np.float32) y = y[mask].astype(np.float32) image = pmt_signal[mask].astype(np.float32) zen = 90 - mc.alt.to(u.deg).value # Store simulated Xmax mc_xmax = event.mc.x_max.value / np.cos(np.deg2rad(zen)) # Calc difference from expected Xmax (for gammas) exp_xmax = 300 + 93 * np.log10(energy.value) x_diff = mc_xmax - exp_xmax x_diff_bin = find_nearest_bin(self.xmax_bins, x_diff) zen = 90 - point.alt.to(u.deg).value az = point.az.to(u.deg).value # Now fill up our output with the X, Y and amplitude of our pixels if (zen, az, energy.value, int(impact), x_diff_bin) in templates.keys(): # Extend the list if an entry already exists templates[(zen, az, energy.value, int(impact), x_diff_bin)].\ extend(image) templates_xb[(zen, az, energy.value, int(impact), x_diff_bin)].\ extend(x.to(u.deg).value) templates_yb[(zen, az, energy.value, int(impact), x_diff_bin)].\ extend(y.to(u.deg).value) else: templates[(zen, az, energy.value, int(impact), x_diff_bin)] = \ image.tolist() templates_xb[(zen, az, energy.value, int(impact), x_diff_bin)] = \ x.value.tolist() templates_yb[(zen, az, energy.value, int(impact), x_diff_bin)] = \ y.value.tolist() if num > max_events: return templates, templates_xb, templates_yb num += 1 return templates, templates_xb, templates_yb
def predict(self, shower_seed, energy_seed): """ Parameters ---------- shower_seed: ReconstructedShowerContainer Seed shower geometry to be used in the fit energy_seed: ReconstructedEnergyContainer Seed energy to be used in fit Returns ------- ReconstructedShowerContainer, ReconstructedEnergyContainer: Reconstructed ImPACT shower geometry and energy """ horizon_seed = HorizonFrame(az=shower_seed.az, alt=shower_seed.alt) nominal_seed = horizon_seed.transform_to(NominalFrame( array_direction=self.array_direction)) source_x = nominal_seed.x.to(u.rad).value source_y = nominal_seed.y.to(u.rad).value ground = GroundFrame(x=shower_seed.core_x, y=shower_seed.core_y, z=0 * u.m) tilted = ground.transform_to( TiltedGroundFrame(pointing_direction=self.array_direction) ) tilt_x = tilted.x.to(u.m).value tilt_y = tilted.y.to(u.m).value zenith = 90 * u.deg - self.array_direction.alt if len(self.hillas_parameters) > 3: shift = [1] else: shift = [1.5, 1, 0.5, 0, -0.5, -1, -1.5] seed_list = spread_line_seed(self.hillas_parameters, self.tel_pos_x, self.tel_pos_y, source_x[0], source_y[0], tilt_x, tilt_y, energy_seed.energy.value, shift_frac = shift) chosen_seed = self.choose_seed(seed_list) # Perform maximum likelihood fit fit_params, errors, like = self.minimise(params=chosen_seed[0], step=chosen_seed[1], limits=chosen_seed[2], minimiser_name=self.minimiser_name) # Create a container class for reconstructed shower shower_result = ReconstructedShowerContainer() # Convert the best fits direction and core to Horizon and ground systems and # copy to the shower container nominal = NominalFrame(x=fit_params[0] * u.rad, y=fit_params[1] * u.rad, array_direction=self.array_direction) horizon = nominal.transform_to(HorizonFrame()) shower_result.alt, shower_result.az = horizon.alt, horizon.az tilted = TiltedGroundFrame(x=fit_params[2] * u.m, y=fit_params[3] * u.m, pointing_direction=self.array_direction) ground = project_to_ground(tilted) shower_result.core_x = ground.x shower_result.core_y = ground.y shower_result.is_valid = True # Currently no errors not availible to copy NaN shower_result.alt_uncert = np.nan shower_result.az_uncert = np.nan shower_result.core_uncert = np.nan # Copy reconstructed Xmax shower_result.h_max = fit_params[5] * self.get_shower_max(fit_params[0], fit_params[1], fit_params[2], fit_params[3], zenith.to(u.rad).value) shower_result.h_max *= np.cos(zenith) shower_result.h_max_uncert = errors[5] * shower_result.h_max shower_result.goodness_of_fit = like # Create a container class for reconstructed energy energy_result = ReconstructedEnergyContainer() # Fill with results energy_result.energy = fit_params[4] * u.TeV energy_result.energy_uncert = errors[4] * u.TeV energy_result.is_valid = True return shower_result, energy_result
def predict(self, shower_seed, energy_seed): """Predict method for the ImPACT reconstructor. Used to calculate the reconstructed ImPACT shower geometry and energy. Parameters ---------- shower_seed: ReconstructedShowerContainer Seed shower geometry to be used in the fit energy_seed: ReconstructedEnergyContainer Seed energy to be used in fit Returns ------- ReconstructedShowerContainer, ReconstructedEnergyContainer: """ self.reset_interpolator() horizon_seed = SkyCoord(az=shower_seed.az, alt=shower_seed.alt, frame=AltAz()) nominal_seed = horizon_seed.transform_to(self.nominal_frame) source_x = nominal_seed.fov_lon.to_value(u.rad) source_y = nominal_seed.fov_lat.to_value(u.rad) ground = GroundFrame(x=shower_seed.core_x, y=shower_seed.core_y, z=0 * u.m) tilted = ground.transform_to( TiltedGroundFrame(pointing_direction=self.array_direction)) tilt_x = tilted.x.to(u.m).value tilt_y = tilted.y.to(u.m).value zenith = 90 * u.deg - self.array_direction.alt seeds = spread_line_seed( self.hillas_parameters, self.tel_pos_x, self.tel_pos_y, source_x, source_y, tilt_x, tilt_y, energy_seed.energy.value, shift_frac=[1], )[0] # Perform maximum likelihood fit fit_params, errors, like = self.minimise( params=seeds[0], step=seeds[1], limits=seeds[2], minimiser_name=self.minimiser_name, ) # Create a container class for reconstructed shower shower_result = ReconstructedGeometryContainer() # Convert the best fits direction and core to Horizon and ground systems and # copy to the shower container nominal = SkyCoord( fov_lon=fit_params[0] * u.rad, fov_lat=fit_params[1] * u.rad, frame=self.nominal_frame, ) horizon = nominal.transform_to(AltAz()) shower_result.alt, shower_result.az = horizon.alt, horizon.az tilted = TiltedGroundFrame( x=fit_params[2] * u.m, y=fit_params[3] * u.m, pointing_direction=self.array_direction, ) ground = project_to_ground(tilted) shower_result.core_x = ground.x shower_result.core_y = ground.y shower_result.is_valid = True # Currently no errors not available to copy NaN shower_result.alt_uncert = np.nan shower_result.az_uncert = np.nan shower_result.core_uncert = np.nan # Copy reconstructed Xmax shower_result.h_max = fit_params[5] * self.get_shower_max( fit_params[0], fit_params[1], fit_params[2], fit_params[3], zenith.to(u.rad).value, ) shower_result.h_max *= np.cos(zenith) shower_result.h_max_uncert = errors[5] * shower_result.h_max shower_result.goodness_of_fit = like # Create a container class for reconstructed energy energy_result = ReconstructedEnergyContainer() # Fill with results energy_result.energy = fit_params[4] * u.TeV energy_result.energy_uncert = errors[4] * u.TeV energy_result.is_valid = True return shower_result, energy_result
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, )
def predict(self, hillas_parameters, tel_x, tel_y, array_direction): """ Parameters ---------- hillas_parameters: dict Dictionary containing Hillas parameters for all telescopes in reconstruction tel_x: dict Dictionary containing telescope position on ground for all telescopes in reconstruction tel_y: dict Dictionary containing telescope position on ground for all telescopes in reconstruction array_direction: AltAz Pointing direction of the array Returns ------- ReconstructedShowerContainer: """ src_x, src_y, err_x, err_y = self.reconstruct_nominal( hillas_parameters) core_x, core_y, core_err_x, core_err_y = self.reconstruct_tilted( hillas_parameters, tel_x, tel_y) err_x *= u.rad err_y *= u.rad nom = SkyCoord(x=src_x * u.rad, y=src_y * u.rad, frame=NominalFrame(array_direction=array_direction)) horiz = nom.transform_to(AltAz()) result = ReconstructedShowerContainer() result.alt, result.az = horiz.alt, horiz.az tilt = SkyCoord( x=core_x * u.m, y=core_y * u.m, frame=TiltedGroundFrame(pointing_direction=array_direction), ) grd = project_to_ground(tilt) result.core_x = grd.x result.core_y = grd.y x_max = self.reconstruct_xmax( nom.x, nom.y, tilt.x, tilt.y, hillas_parameters, tel_x, tel_y, 90 * u.deg - array_direction.alt, ) result.core_uncert = np.sqrt(core_err_x**2 + core_err_y**2) * u.m result.tel_ids = [h for h in hillas_parameters.keys()] result.average_intensity = np.mean( [h.intensity for h in hillas_parameters.values()]) result.is_valid = True src_error = np.sqrt(err_x**2 + err_y**2) result.alt_uncert = src_error.to(u.deg) result.az_uncert = src_error.to(u.deg) result.h_max = x_max result.h_max_uncert = np.nan result.goodness_of_fit = np.nan return result
def plot_array_sideview(event, result, reco): cams = [ event.inst.subarray.tels[i].camera for i in event.r0.tels_with_data ] cams = [c for c in cams if c.cam_id in allowed_cameras] pos = [] ids = event.inst.subarray.tel.keys() for i in list(ids): if event.inst.subarray.tels[i].camera.cam_id in allowed_cameras: pos.append(event.inst.subarray.positions[i]) pos = np.array(pos) plt.plot(pos[:, 0], pos[:, 2], '.', c='gray') sst = [] mst = [] lst = [] ids = event.r0.tels_with_data for i in list(ids): if event.inst.subarray.tels[i].camera.cam_id in allowed_cameras: alt = event.mc.tel[i].altitude_raw direction = [1 * np.cos(alt), 1 * np.sin(alt)] pos = event.inst.subarray.positions[i] if event.inst.subarray.tels[i].camera.cam_id == 'DigiCam': sst.append(pos) plt.quiver(*pos[[0, 2]], *direction, color='blue', scale=1.5, width=0.0015, alpha=0.2) if event.inst.subarray.tels[i].camera.cam_id == 'NectarCam': mst.append(pos) plt.quiver(*pos[[0, 2]], *direction, color='red', scale=1.5, width=0.0015, alpha=0.2) if event.inst.subarray.tels[i].camera.cam_id == 'LSTCam': lst.append(pos) plt.quiver(*pos[[0, 2]], *direction, color='green', scale=1.5, width=0.0015, alpha=0.2) if sst: sst = np.array(sst) plt.scatter(sst[:, 0], sst[:, 2], label='SST', color='blue', s=60) if mst: mst = np.array(mst) plt.scatter(mst[:, 0], mst[:, 2], label='MST', color='red', s=60) if lst: lst = np.array(lst) plt.scatter(lst[:, 0], lst[:, 2], label='LST', color='green', s=60) point_dir = SkyCoord(*(event.mcheader.run_array_direction), frame='altaz') tiltedframe = TiltedGroundFrame(pointing_direction=point_dir) core_coord = SkyCoord(x=event.mc.core_x, y=event.mc.core_y, frame=tiltedframe).transform_to(GroundFrame()) plt.scatter(core_coord.x.value, 0, s=150, marker='+', label='impact point', color='black') plt.scatter(result.core_x.value, 0, s=150, marker='+', label='impact point estimated', color='orange') for c in reco.hillas_planes.values(): plt.quiver(*c.pos.value[[0, 2]], *c.a[[0, 2]], scale=1.5, width=0.002, color='gray') plt.quiver(*c.pos.value[[0, 2]], *c.b[[0, 2]], scale=1.5, width=0.002, color='silver') direction = [ 1 * np.cos(event.mc.alt.value), 1 * np.sin(event.mc.alt.value) ] plt.quiver(event.mc.core_x.value, 0, *direction, scale=1.5, width=0.002, color='black', alpha=0.5, label='true direction') direction = [ 1 * np.cos(result.alt.to('rad').value), 1 * np.sin(result.alt.to('rad').value) ] plt.quiver(result.core_x.value, 0, *direction, scale=1.5, width=0.002, color='orange', label='estimated direction') plt.plot([-1500, 1500], [0, 0], color='#987a48', lw=3, ls='--') plt.ylim(-5, 40) plt.xlim(-1500, 1500)
def predict(self, hillas_dict, inst, array_pointing, telescopes_pointings=None): """ Parameters ---------- hillas_dict: dict Dictionary containing Hillas parameters for all telescopes in reconstruction inst : ctapipe.io.InstrumentContainer instrumental description array_pointing: SkyCoord[AltAz] pointing direction of the array telescopes_pointings: dict[SkyCoord[AltAz]] dictionary of pointing direction per each telescope Returns ------- ReconstructedShowerContainer: """ # filter warnings for missing obs time. this is needed because MC data has no obs time warnings.filterwarnings(action='ignore', category=MissingFrameAttributeWarning) # stereoscopy needs at least two telescopes if len(hillas_dict) < 2: raise TooFewTelescopesException( "need at least two telescopes, have {}".format( len(hillas_dict))) # check for np.nan or 0 width's as these screw up weights if any( [np.isnan(hillas_dict[tel]['width'].value) for tel in hillas_dict]): raise InvalidWidthException( "A HillasContainer contains an ellipse of width==np.nan") if any([hillas_dict[tel]['width'].value == 0 for tel in hillas_dict]): raise InvalidWidthException( "A HillasContainer contains an ellipse of width==0") if telescopes_pointings is None: telescopes_pointings = { tel_id: array_pointing for tel_id in hillas_dict.keys() } tilted_frame = TiltedGroundFrame(pointing_direction=array_pointing) ground_positions = inst.subarray.tel_coords grd_coord = GroundFrame(x=ground_positions.x, y=ground_positions.y, z=ground_positions.z) tilt_coord = grd_coord.transform_to(tilted_frame) tel_x = { tel_id: tilt_coord.x[tel_id - 1] for tel_id in list(hillas_dict.keys()) } tel_y = { tel_id: tilt_coord.y[tel_id - 1] for tel_id in list(hillas_dict.keys()) } nom_frame = NominalFrame(origin=array_pointing) hillas_dict_mod = copy.deepcopy(hillas_dict) for tel_id, hillas in hillas_dict_mod.items(): # prevent from using rads instead of meters as inputs assert hillas.x.to(u.m).unit == u.Unit('m') focal_length = inst.subarray.tel[ tel_id].optics.equivalent_focal_length camera_frame = CameraFrame( telescope_pointing=telescopes_pointings[tel_id], focal_length=focal_length, ) cog_coords = SkyCoord(x=hillas.x, y=hillas.y, frame=camera_frame) cog_coords_nom = cog_coords.transform_to(nom_frame) hillas.x = cog_coords_nom.delta_alt hillas.y = cog_coords_nom.delta_az src_x, src_y, err_x, err_y = self.reconstruct_nominal(hillas_dict_mod) core_x, core_y, core_err_x, core_err_y = self.reconstruct_tilted( hillas_dict_mod, tel_x, tel_y) err_x *= u.rad err_y *= u.rad nom = SkyCoord(delta_az=src_x * u.rad, delta_alt=src_y * u.rad, frame=nom_frame) # nom = sky_pos.transform_to(nom_frame) sky_pos = nom.transform_to(array_pointing.frame) result = ReconstructedShowerContainer() result.alt = sky_pos.altaz.alt.to(u.rad) result.az = sky_pos.altaz.az.to(u.rad) tilt = SkyCoord( x=core_x * u.m, y=core_y * u.m, frame=tilted_frame, ) grd = project_to_ground(tilt) result.core_x = grd.x result.core_y = grd.y x_max = self.reconstruct_xmax( nom.delta_az, nom.delta_alt, tilt.x, tilt.y, hillas_dict_mod, tel_x, tel_y, 90 * u.deg - array_pointing.alt, ) result.core_uncert = np.sqrt(core_err_x**2 + core_err_y**2) * u.m result.tel_ids = [h for h in hillas_dict_mod.keys()] result.average_intensity = np.mean( [h.intensity for h in hillas_dict_mod.values()]) result.is_valid = True src_error = np.sqrt(err_x**2 + err_y**2) result.alt_uncert = src_error.to(u.rad) result.az_uncert = src_error.to(u.rad) result.h_max = x_max result.h_max_uncert = np.nan result.goodness_of_fit = np.nan return result
def read_templates(self, filename, max_events=1e9, fill_correction=False): """ This is a pretty standard ctapipe event loop that calibrates events, rotates them into a common frame and then stores the pixel values in a list :param filename: str Location of input :param max_events: int Maximum number of events to include in the loop :param fill_correction: bool Fill correction factor table :return: tuple Return 3 lists of amplitude and rotated x,y positions of all pixels in all events """ # Create dictionaries to contain our output # Create a dummy time for our AltAz objects dummy_time = Time('2010-01-01T00:00:00', format='isot', scale='utc') if self.verbose: print("Reading", filename.strip()) # calibrator = CameraCalibrator(image_extractor=FullWaveformSum()) calibrator = None with event_source(input_url=filename.strip()) as source: if calibrator is None: calibrator = CameraCalibrator(source.subarray, image_extractor=FullWaveformSum(source.subarray)) grd_tel = None num = 0 # Event counter for event in tqdm(source): alt = event.mcheader.run_array_direction[1] if alt > 90. * u.deg: alt = 90. * u.deg point = SkyCoord(alt=alt, az=event.mcheader.run_array_direction[0], frame=AltAz(obstime=dummy_time)) mc = event.mc # Create coordinate objects for source position src = SkyCoord(alt=mc.alt.value * u.rad, az=mc.az.value * u.rad, frame=AltAz(obstime=dummy_time)) #print("here1", point.separation(src), self.maximum_offset) if point.separation(src) > self.maximum_offset: continue #print("here2") # And transform into nominal system (where we store our templates) source_direction = src.transform_to(NominalFrame(origin=point)) # Perform calibration of images try: calibrator(event) except ZeroDivisionError: print("ZeroDivisionError in calibrator, skipping this event") continue # Store simulated event energy energy = mc.energy # Store ground position of all telescopes # We only want to do this once, but has to be done in event loop if grd_tel is None: grd_tel = source.subarray.tel_coords # Convert to tilted system tilt_tel = grd_tel.transform_to( TiltedGroundFrame(pointing_direction=point)) # Calculate core position in tilted system grd_core_true = SkyCoord(x=np.asarray(mc.core_x) * u.m, y=np.asarray(mc.core_y) * u.m, z=np.asarray(0) * u.m, frame=GroundFrame()) tilt_core_true = grd_core_true.transform_to(TiltedGroundFrame( pointing_direction=point)) # Loop over triggered telescopes for tel_id in event.dl0.tels_with_data: # Get pixel signal, Why like this? You have already calibrated the image # try: # hg, lg =np.sum(event.r1.tel[tel_id].waveform, axis=2) # pmt_signal = hg # pmt_signal[pmt_signal>150] = lg[pmt_signal>150] # except ValueError: # pmt_signal = np.sum(event.r1.tel[tel_id].waveform, axis=-1)[0] pmt_signal = event.dl1.tel[tel_id].image # Get pixel coordinates and convert to the nominal system geom = source.subarray.tel[tel_id].camera.geometry fl = source.subarray.tel[tel_id].optics.equivalent_focal_length * \ self.eff_fl camera_coord = SkyCoord(x=geom.pix_x, y=geom.pix_y, frame=CameraFrame(focal_length=fl, telescope_pointing=point)) nom_coord = camera_coord.transform_to( NominalFrame(origin=point)) x = nom_coord.fov_lat.to(u.deg) y = nom_coord.fov_lon.to(u.deg) # Calculate expected rotation angle of the image phi = np.arctan2((tilt_tel.y[tel_id - 1] - tilt_core_true.y), (tilt_tel.x[tel_id - 1] - tilt_core_true.x)) + \ 180 * u.deg phi += self.rotation_angle # And the impact distance of the shower impact = np.sqrt(np.power(tilt_tel.x[tel_id - 1] - tilt_core_true.x, 2) + np.power(tilt_tel.y[tel_id - 1] - tilt_core_true.y, 2)). \ to(u.m).value # now rotate and translate our images such that they lie on top of one # another x, y = \ ImPACTReconstructor.rotate_translate(x, y, source_direction.fov_lat, source_direction.fov_lon, phi) x *= -1 # We only want to keep pixels that fall within the bounds of our # final template mask = np.logical_and(x > self.bounds[0][0] * u.deg, x < self.bounds[0][1] * u.deg) mask = np.logical_and(mask, y < self.bounds[1][1] * u.deg) mask = np.logical_and(mask, y > self.bounds[1][0] * u.deg) mask510 = tailcuts_clean(geom, pmt_signal, picture_thresh=self.tailcuts[0], boundary_thresh=self.tailcuts[1], min_number_picture_neighbors=1) amp_sum = np.sum(pmt_signal[mask510]) if fill_correction: mask = np.logical_and(mask, mask510) # if self.training_library == "kde" and fill_correction==False: # for i in range(4): # mask510 = dilate(geom, mask510) # mask = np.logical_and(mask, mask510) if amp_sum < self.min_amp: continue #mask510 = dilate(geom, mask510) #mask510 = dilate(geom, mask510) #if fill_correction: # mask = np.logical_and(mask, mask510) # Make sure everything is 32 bit x = x[mask].astype(np.float32) y = y[mask].astype(np.float32) image = pmt_signal[mask].astype(np.float32) zen = 90 - mc.alt.to(u.deg).value # Store simulated Xmax mc_xmax = event.mc.x_max.value / np.cos(np.deg2rad(zen)) # Calc difference from expected Xmax (for gammas) exp_xmax = 300 + 93 * np.log10(energy.value) x_diff = mc_xmax - exp_xmax x_diff_bin = find_nearest_bin(self.xmax_bins, x_diff) zen = 90 - point.alt.to(u.deg).value az = point.az.to(u.deg).value # Now fill up our output with the X, Y and amplitude of our pixels if fill_correction: from scipy.interpolate import RegularGridInterpolator xb = np.linspace(self.bounds[0][0], self.bounds[0][1], self.bins[0]) yb = np.linspace(self.bounds[1][0], self.bounds[1][1], self.bins[1]) key = zen, az, energy.value, int(impact), x_diff_bin if key in self.template_fit.keys(): interp = RegularGridInterpolator((yb, xb), self.template_fit[key], bounds_error=False) pred = interp(np.array([y.to(u.deg).value, x.to(u.deg).value]).T) if key in self.correction.keys(): self.correction[key] = np.append(self.correction[key], np.sum(image)/np.sum(pred)) else: self.correction[key] = (np.sum(image)/np.sum(pred)) else: if (zen, az, energy.value, int(impact), x_diff_bin) in self.templates.keys(): # Extend the list if an entry already exists self.templates[(zen, az, energy.value, int(impact), x_diff_bin)].\ extend(image) self.templates_xb[(zen, az, energy.value, int(impact), x_diff_bin)].\ extend(x.to(u.deg).value) self.templates_yb[(zen, az, energy.value, int(impact), x_diff_bin)].\ extend(y.to(u.deg).value) self.count[(zen, az, energy.value, int(impact), x_diff_bin)] = self.count[(zen, az, energy.value, int(impact), x_diff_bin)] + 1 else: self.templates[(zen, az, energy.value, int(impact), x_diff_bin)] = \ image.tolist() self.templates_xb[(zen, az, energy.value, int(impact), x_diff_bin)] = \ x.value.tolist() self.templates_yb[(zen, az, energy.value, int(impact), x_diff_bin)] = \ y.value.tolist() self.count[(zen, az, energy.value, int(impact), x_diff_bin)] = 1 if num > max_events: return self.templates, self.templates_xb, self.templates_yb num += 1 return self.templates, self.templates_xb, self.templates_yb