def overlay_hillas(self, hillas, scale_fac=10000, draw_axes=False, **kwargs): """ Overlay hillas parameters on top of the array map Parameters ---------- hillas: dictionary Hillas moments objects to overlay scale_fac: float Scaling factor to array to hillas width and length when drawing kwargs: key=value any style keywords to pass to matplotlib Returns ------- None """ tel_x = [self.instrument.tel_pos[i][0].to(u.m).value for i in hillas] tel_y = [self.instrument.tel_pos[i][1].to(u.m).value for i in hillas] tel_z = [self.instrument.tel_pos[i][2].to(u.m).value for i in hillas] if self.system is not None: ground = GroundFrame(x=np.asarray(tel_x)*u.m, y=np.asarray(tel_y)*u.m, z=np.asarray(tel_z)*u.m) new_sys = ground.transform_to(self.system) self.array.overlay_moments(hillas, (new_sys.x, new_sys.y), scale_fac, cmap="Greys", alpha=0.5, **kwargs) if draw_axes: self.array.overlay_axis(hillas, (new_sys.x, new_sys.y)) else: self.array.overlay_moments(hillas, (tel_x, tel_y), scale_fac, alpha=0.5, cmap="Greys", **kwargs) self.hillas = hillas
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 azimuth = self.array_direction.az 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 draw_position(self, core_x, core_y, use_centre=False, **kwargs): """ Draw a marker at a position in the array plotter (for marking reconstructed positions etc) Parameters ---------- core_x: float X position of point core_y: float Y position of point use_centre: bool Centre the plotter on this position kwargs: key=value any style keywords to pass to matplotlib Returns ------- None """ ground = GroundFrame(x=np.asarray(core_x) * u.m, y=np.asarray(core_y) * u.m, z=np.asarray(0) * u.m) if self.system is not None: new_sys = ground.transform_to(self.system) else: new_sys = ground self.array.add_polygon(centroid=(new_sys.x.value,new_sys.y.value), radius=10, nsides=3, **kwargs) if use_centre: self.centre = (new_sys.x.value,new_sys.y.value)
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. * 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 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 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=HorizonFrame(alt=90 * u.deg, az=180 * u.deg) ) ) print(project_to_ground(tilt_coord)) print("Tilted Coordinate", tilt_coord)
def __init__(self, instrument, telescopes=None, system=None, ax=None): """ Parameters ---------- instrument: dictionary intrument containers for this event telescopes: list List of telescopes included system: Coordinate system Coordinate system to transform coordinates into """ self.instrument = instrument self.system = system if telescopes is None: self.telescopes = instrument.telescope_ids else: self.telescopes = telescopes type_dict = {28.0: 1, 16.0: 2, 2.1500000953674316: 3, 2.2829999923706055: 4, 5.599999904632568: 5} tel_x = [self.instrument.tel_pos[i][0].to(u.m).value for i in self.telescopes] tel_y = [self.instrument.tel_pos[i][1].to(u.m).value for i in self.telescopes] tel_z = [self.instrument.tel_pos[i][2].to(u.m).value for i in self.telescopes] self.axes = ax if ax is not None else plt.gca() tel_type = np.asarray([type_dict[self.instrument.optical_foclen[i].to(u.m).value] for i in self.telescopes]) self.tel_type = tel_type if system is not None: ground = GroundFrame(x=np.asarray(tel_x)*u.m, y=np.asarray(tel_y)*u.m, z=np.asarray(tel_z)*u.m) new_sys = ground.transform_to(system) self.tel_x = new_sys.x self.tel_y = new_sys.y else: self.tel_x = tel_x*u.m self.tel_y = tel_y*u.m self.centre = (0,0) self.array = ArrayDisplay(telx=np.asarray(self.tel_x), tely=np.asarray(self.tel_y), tel_type=tel_type, axes=self.axes) self.hillas = None
def overlay_hillas(self, hillas, scale_fac=20000, draw_axes=False, **kwargs): """ Overlay hillas parameters on top of the array map Parameters ---------- hillas: dictionary Hillas moments objects to overlay scale_fac: float Scaling factor to array to hillas width and length when drawing kwargs: key=value any style keywords to pass to matplotlib Returns ------- None """ tel_x = [self.instrument.tel_pos[i][0].to(u.m).value for i in hillas] tel_y = [self.instrument.tel_pos[i][1].to(u.m).value for i in hillas] tel_z = [self.instrument.tel_pos[i][2].to(u.m).value for i in hillas] if self.system is not None: ground = GroundFrame(x=np.asarray(tel_x) * u.m, y=np.asarray(tel_y) * u.m, z=np.asarray(tel_z) * u.m) new_sys = ground.transform_to(self.system) self.array.overlay_moments(hillas, (new_sys.x, new_sys.y), scale_fac, cmap="Viridis", alpha=0.5, **kwargs) if draw_axes: self.array.overlay_axis(hillas, (new_sys.x, new_sys.y)) else: self.array.overlay_moments(hillas, (tel_x, tel_y), scale_fac, alpha=0.5, cmap="viridis", **kwargs) self.hillas = hillas
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 test_ground_to_tilt(): from ctapipe.coordinates import GroundFrame, TiltedGroundFrame, HorizonFrame # define ground coordinate grd_coord = GroundFrame(x=1 * u.m, y=2 * u.m, z=0 * u.m) pointing_direction = HorizonFrame(alt=90 * u.deg, az=0 * u.deg) # 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 = HorizonFrame(alt=90 * u.deg, az=180 * u.deg) tilt_coord = grd_coord.transform_to( TiltedGroundFrame(pointing_direction=pointing_direction)) assert np.abs(tilt_coord.y + 2. * u.m) < 1e-5 * u.m # Check that if we look at horizon the x coordinate is 0 pointing_direction = HorizonFrame(alt=0 * u.deg, az=0 * u.deg) tilt_coord = grd_coord.transform_to( TiltedGroundFrame(pointing_direction=pointing_direction)) assert np.abs(tilt_coord.x) < 1e-5 * u.m
def ground_grid(event, tel_pos=False): """ Return the telescopes positions in the GroundFrame and plot on ground :param event: input event selected from simtel :param tel_pos: (bool) if True, plot the telescopes as spheres :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, pointing_direction=array_pointing) grid_unit = 20000 # in centimeters ground_system = union() if tel_pos: # for i in range(ground_coordinates.x.size): for i in range(50): coords = [ 100 * ground_coordinates.x[i].value, 100 * ground_coordinates.y[i].value, 100 * ground_coordinates.z[i].value ] position = translate(coords)(color([0, 0, 1])(sphere(r=800))) ground_system.add(position) grid = 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 = color([0, 0, 1, 0.5])(grid) ground_system.add(grid) # SYSTEM + ARROW ref_arr = ref_arrow_3d(8000, origin=(1000, 1000, 0), label={ 'x': "x_gnd = NORTH", 'y': "y_gnd = WEST", 'z': "z_gnd" }) ground_system = ground_system + ref_arr return ground_system
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 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.tel_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.tel_type[tel_id]] # prediction *= self.pixel_area[tel_id] prediction[prediction < 0] = 0 prediction[np.isnan(prediction)] = 0 return prediction
def __init__(self, subarray, axes=None, autoupdate=True, tel_scale=2.0, alpha=0.7, title=None, radius=None, frame=GroundFrame()): self.frame = frame self.subarray = subarray # get the telescope positions. If a new frame is set, this will # transform to the new frame. self.tel_coords = subarray.tel_coords.transform_to(frame) # set up colors per telescope type tel_types = [str(tel) for tel in subarray.tels.values()] if radius is None: # set radius to the mirror radius (so big tels appear big) radius = [ np.sqrt(tel.optics.mirror_area.to("m2").value) * tel_scale for tel in subarray.tel.values() ] if title is None: title = subarray.name # get default matplotlib color cycle (depends on the current style) color_cycle = cycle(plt.rcParams['axes.prop_cycle'].by_key()['color']) # map a color to each telescope type: tel_type_to_color = {} for tel_type in list(set(tel_types)): tel_type_to_color[tel_type] = next(color_cycle) tel_color = [tel_type_to_color[ttype] for ttype in tel_types] patches = [] for x, y, r, c in zip(list(self.tel_coords.x.value), list(self.tel_coords.y.value), list(radius), tel_color): patches.append( Circle( xy=(x, y), radius=r, fill=True, color=c, alpha=alpha, )) # build the legend: legend_elements = [] for ttype in list(set(tel_types)): color = tel_type_to_color[ttype] legend_elements.append( Line2D([0], [0], marker='o', color=color, label=ttype, markersize=10, alpha=alpha, linewidth=0)) plt.legend(handles=legend_elements) self.tel_colors = tel_color self.autoupdate = autoupdate self.telescopes = PatchCollection(patches, match_original=True) self.telescopes.set_linewidth(2.0) self.axes = axes or plt.gca() self.axes.add_collection(self.telescopes) self.axes.set_aspect(1.0) self.axes.set_title(title) self._labels = [] self._quiver = None self.axes.autoscale_view()
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(project_to_ground(tilt_coord)) print("Tilted Coordinate", tilt_coord)
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
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
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 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, )
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 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 __init__(self, instrument, telescopes=None, system=None, ax=None): """ Parameters ---------- instrument: dictionary intrument containers for this event telescopes: list List of telescopes included system: Coordinate system Coordinate system to transform coordinates into """ self.instrument = instrument self.system = system if telescopes is None: self.telescopes = instrument.telescope_ids else: self.telescopes = telescopes type_dict = { 28.0: 1, 16.0: 2, 2.1500000953674316: 3, 2.2829999923706055: 4, 5.599999904632568: 5 } tel_x = [ self.instrument.tel_pos[i][0].to(u.m).value for i in self.telescopes ] tel_y = [ self.instrument.tel_pos[i][1].to(u.m).value for i in self.telescopes ] tel_z = [ self.instrument.tel_pos[i][2].to(u.m).value for i in self.telescopes ] self.axes = ax if ax is not None else plt.gca() tel_type = np.asarray([ type_dict[self.instrument.optical_foclen[i].to(u.m).value] for i in self.telescopes ]) self.tel_type = tel_type if system is not None: ground = GroundFrame(x=np.asarray(tel_x) * u.m, y=np.asarray(tel_y) * u.m, z=np.asarray(tel_z) * u.m) new_sys = ground.transform_to(system) self.tel_x = new_sys.x self.tel_y = new_sys.y else: self.tel_x = tel_x * u.m self.tel_y = tel_y * u.m self.centre = (0, 0) self.array = ArrayDisplay(telx=np.asarray(self.tel_x), tely=np.asarray(self.tel_y), tel_type=tel_type, axes=self.axes) self.hillas = None
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 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 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 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 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 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 = SkyCoord( az=shower_seed.az, alt=shower_seed.alt, frame=HorizonFrame() ) nominal_seed = horizon_seed.transform_to( NominalFrame(origin=self.array_direction) ) source_x = nominal_seed.delta_az.to_value(u.rad) source_y = nominal_seed.delta_alt.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 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 = SkyCoord( x=fit_params[0] * u.rad, y=fit_params[1] * u.rad, frame=NominalFrame(origin=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 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 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 __init__( self, subarray, axes=None, autoupdate=True, tel_scale=2.0, alpha=0.7, title=None, radius=None, frame=GroundFrame(), ): self.frame = frame self.subarray = subarray self.axes = axes or plt.gca() # get the telescope positions. If a new frame is set, this will # transform to the new frame. self.tel_coords = subarray.tel_coords.transform_to(frame).cartesian self.unit = self.tel_coords.x.unit # set up colors per telescope type tel_types = [str(tel) for tel in subarray.tels.values()] if radius is None: # set radius to the mirror radius (so big tels appear big) radius = [ np.sqrt(tel.optics.mirror_area.to("m2").value) * tel_scale for tel in subarray.tel.values() ] self.radii = radius else: self.radii = np.ones(len(tel_types)) * radius if title is None: title = subarray.name # get default matplotlib color cycle (depends on the current style) color_cycle = cycle(plt.rcParams["axes.prop_cycle"].by_key()["color"]) # map a color to each telescope type: tel_type_to_color = {} for tel_type in list(set(tel_types)): tel_type_to_color[tel_type] = next(color_cycle) tel_color = [tel_type_to_color[ttype] for ttype in tel_types] patches = [] for x, y, r, c in zip( list(self.tel_coords.x.to_value("m")), list(self.tel_coords.y.to_value("m")), list(radius), tel_color, ): patches.append( Circle(xy=(x, y), radius=r, fill=True, color=c, alpha=alpha)) # build the legend: legend_elements = [] for ttype in list(set(tel_types)): color = tel_type_to_color[ttype] legend_elements.append( Line2D( [0], [0], marker="o", color=color, label=ttype, markersize=10, alpha=alpha, linewidth=0, )) self.axes.legend(handles=legend_elements) self.add_radial_grid() # create the plot self.tel_colors = tel_color self.autoupdate = autoupdate self.telescopes = PatchCollection(patches, match_original=True) self.telescopes.set_linewidth(2.0) self.axes.add_collection(self.telescopes) self.axes.set_aspect(1.0) self.axes.set_title(title) xunit = self.tel_coords.x.unit.to_string("latex") yunit = self.tel_coords.y.unit.to_string("latex") xname, yname, _ = frame.get_representation_component_names().keys() self.axes.set_xlabel(f"{xname} [{xunit}] $\\rightarrow$") self.axes.set_ylabel(f"{yname} [{yunit}] $\\rightarrow$") self._labels = [] self._quiver = None self.axes.autoscale_view()
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, 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