def test_shower_impact_distance(): """test several boundary cases using the function that takes a Subarray and Container """ sub = SubarrayDescription( name="test", tel_positions={ 1: [0, 0, 0] * u.m, 2: [0, 1, 0] * u.m, 3: [0, 2, 0] * u.m }, tel_descriptions={ 1: None, 2: None, 3: None }, ) # coming from zenith to the center of the array, the impact distance should # be the cartesian distance shower_geom = ReconstructedGeometryContainer(core_x=0 * u.m, core_y=0 * u.m, alt=90 * u.deg, az=0 * u.deg) impact_distances = shower_impact_distance(shower_geom=shower_geom, subarray=sub) assert np.allclose(impact_distances, [0, 1, 2] * u.m) # alt=0 az=0 should be aligned to x-axis (north in ground frame) # therefore the distances should also be just the y-offset of the telecope shower_geom = ReconstructedGeometryContainer(core_x=0 * u.m, core_y=0 * u.m, alt=0 * u.deg, az=0 * u.deg) impact_distances = shower_impact_distance(shower_geom=shower_geom, subarray=sub) assert np.allclose(impact_distances, [0, 1, 2] * u.m) # alt=0 az=90 should be aligned to y-axis (east in ground frame) # therefore the distances should also be just the x-offset of the telecope shower_geom = ReconstructedGeometryContainer(core_x=0 * u.m, core_y=0 * u.m, alt=0 * u.deg, az=90 * u.deg) impact_distances = shower_impact_distance(shower_geom=shower_geom, subarray=sub) assert np.allclose(impact_distances, [0, 0, 0] * u.m)
def test_image_prediction(self): pixel_x = np.array([0]) * u.deg pixel_y = np.array([0]) * u.deg image = np.array([1]) pixel_area = np.array([1]) * u.deg * u.deg self.impact_reco.set_event_properties( {1: image}, {1: pixel_x}, {1: pixel_y}, {1: pixel_area}, {1: "CHEC"}, {1: 0 * u.m}, {1: 0 * u.m}, array_direction=[0 * u.deg, 0 * u.deg], ) """First check image prediction by directly accessing the function""" pred = self.impact_reco.image_prediction( "CHEC", zenith=0, azimuth=0, energy=1, impact=50, x_max=0, pix_x=pixel_x, pix_y=pixel_y, ) assert np.sum(pred) != 0 """Then check helper function gives the same answer""" shower = ReconstructedGeometryContainer() shower.is_valid = True shower.alt = 0 * u.deg shower.az = 0 * u.deg shower.core_x = 0 * u.m shower.core_y = 100 * u.m shower.h_max = 300 + 93 * np.log10(1) energy = ReconstructedEnergyContainer() energy.is_valid = True energy.energy = 1 * u.TeV pred2 = self.impact_reco.get_prediction(1, shower_reco=shower, energy_reco=energy) print(pred, pred2) assert pred.all() == pred2.all()
def __call__(self, event: ArrayEventContainer): """overwrite this method with your favourite direction reconstruction algorithm Parameters ---------- tels_dict : dict general dictionary containing all triggered telescopes data Returns ------- `~ctapipe.containers.ReconstructedGeometryContainer` """ return ReconstructedGeometryContainer()
def __call__(self, event): """ Perform the full shower geometry reconstruction on the input event. Parameters ---------- event : container `ctapipe.containers.ArrayEventContainer` """ # Read only valid HillasContainers hillas_dict = { tel_id: dl1.parameters.hillas for tel_id, dl1 in event.dl1.tel.items() if np.isfinite(dl1.parameters.hillas.intensity) } # Due to tracking the pointing of the array will never be a constant array_pointing = SkyCoord( az=event.pointing.array_azimuth, alt=event.pointing.array_altitude, frame=AltAz(), ) telescope_pointings = { tel_id: SkyCoord( alt=event.pointing.tel[tel_id].altitude, az=event.pointing.tel[tel_id].azimuth, frame=AltAz(), ) for tel_id in event.dl1.tel.keys() } try: result = self._predict( event, hillas_dict, self.subarray, array_pointing, telescope_pointings ) except (TooFewTelescopesException, InvalidWidthException): result = ReconstructedGeometryContainer() event.dl2.stereo.geometry["HillasReconstructor"] = result
def test_compare_3d_and_frame_impact_distance( example_subarray: SubarrayDescription, ): """Test another (slower) way of computing the impact distance, using Frames and compare to the implemented method. """ for alt in [90, 0, 50, 60] * u.deg: for az in [0, 90, 45, 270, 360] * u.deg: shower_geom = ReconstructedGeometryContainer(core_x=0 * u.m, core_y=0 * u.m, alt=alt, az=az) impact_distances_3d = shower_impact_distance( shower_geom=shower_geom, subarray=example_subarray) impact_distances_frame = shower_impact_distance_with_frames( shower_geom=shower_geom, subarray=example_subarray) assert np.allclose(impact_distances_frame, impact_distances_3d), f"failed at {alt=} {az=}"
def predict(self, hillas_dict, subarray, array_pointing, telescopes_pointings=None): """ The function you want to call for the reconstruction of the event. It takes care of setting up the event and consecutively calls the functions for the direction and core position reconstruction. Shower parameters not reconstructed by this class are set to np.nan Parameters ----------- hillas_dict: dict dictionary with telescope IDs as key and HillasParametersContainer instances as values 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 Raises ------ TooFewTelescopesException if len(hillas_dict) < 2 InvalidWidthException if any width is np.nan or 0 """ # 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" ) # use the single telescope pointing also for parallel pointing: code is more general if telescopes_pointings is None: telescopes_pointings = { tel_id: array_pointing for tel_id in hillas_dict.keys() } else: self.divergent_mode = True self.corrected_angle_dict = {} self.initialize_hillas_planes( hillas_dict, subarray, telescopes_pointings, array_pointing ) # algebraic direction estimate direction, err_est_dir = self.estimate_direction() # array pointing is needed to define the tilted frame core_pos = self.estimate_core_position(hillas_dict, array_pointing) # container class for reconstructed showers _, lat, lon = cartesian_to_spherical(*direction) # estimate max height of shower h_max = self.estimate_h_max() # astropy's coordinates system rotates counter-clockwise. # Apparently we assume it to be clockwise. # that's why lon get's a sign result = ReconstructedGeometryContainer( alt=lat, az=-lon, core_x=core_pos[0], core_y=core_pos[1], tel_ids=[h for h in hillas_dict.keys()], average_intensity=np.mean([h.intensity for h in hillas_dict.values()]), is_valid=True, alt_uncert=err_est_dir, h_max=h_max, ) return result
def predict(self, hillas_dict, subarray, array_pointing, telescopes_pointings=None): """ Parameters ---------- hillas_dict: dict Dictionary containing Hillas parameters for all telescopes in reconstruction inst : ctapipe.io.InstrumentContainer instrumental description array_pointing: SkyCoord[AltAz] pointing direction of the array telescopes_pointings: dict[SkyCoord[AltAz]] dictionary of pointing direction per each telescope Returns ------- ReconstructedShowerContainer: """ # filter warnings for missing obs time. this is needed because MC data has no obs time warnings.filterwarnings(action="ignore", category=MissingFrameAttributeWarning) # stereoscopy needs at least two telescopes if len(hillas_dict) < 2: raise TooFewTelescopesException( "need at least two telescopes, have {}".format(len(hillas_dict)) ) # check for np.nan or 0 width's as these screw up weights if any([np.isnan(hillas_dict[tel]["width"].value) for tel in hillas_dict]): raise InvalidWidthException( "A HillasContainer contains an ellipse of width==np.nan" ) if any([hillas_dict[tel]["width"].value == 0 for tel in hillas_dict]): raise InvalidWidthException( "A HillasContainer contains an ellipse of width==0" ) if telescopes_pointings is None: telescopes_pointings = { tel_id: array_pointing for tel_id in hillas_dict.keys() } tilted_frame = TiltedGroundFrame(pointing_direction=array_pointing) grd_coord = subarray.tel_coords tilt_coord = grd_coord.transform_to(tilted_frame) tel_ids = list(hillas_dict.keys()) tel_indices = subarray.tel_ids_to_indices(tel_ids) tel_x = { tel_id: tilt_coord.x[tel_index] for tel_id, tel_index in zip(tel_ids, tel_indices) } tel_y = { tel_id: tilt_coord.y[tel_index] for tel_id, tel_index in zip(tel_ids, tel_indices) } nom_frame = NominalFrame(origin=array_pointing) hillas_dict_mod = {} for tel_id, hillas in hillas_dict.items(): if isinstance(hillas, CameraHillasParametersContainer): focal_length = subarray.tel[tel_id].optics.equivalent_focal_length camera_frame = CameraFrame( telescope_pointing=telescopes_pointings[tel_id], focal_length=focal_length, ) cog_coords = SkyCoord(x=hillas.x, y=hillas.y, frame=camera_frame) cog_coords_nom = cog_coords.transform_to(nom_frame) else: telescope_frame = TelescopeFrame( telescope_pointing=telescopes_pointings[tel_id] ) cog_coords = SkyCoord( fov_lon=hillas.fov_lon, fov_lat=hillas.fov_lat, frame=telescope_frame, ) cog_coords_nom = cog_coords.transform_to(nom_frame) hillas_dict_mod[tel_id] = HillasParametersContainer( fov_lon=cog_coords_nom.fov_lon, fov_lat=cog_coords_nom.fov_lat, psi=hillas.psi, width=hillas.width, length=hillas.length, intensity=hillas.intensity, ) src_fov_lon, src_fov_lat, err_fov_lon, err_fov_lat = self.reconstruct_nominal( hillas_dict_mod ) core_x, core_y, core_err_x, core_err_y = self.reconstruct_tilted( hillas_dict_mod, tel_x, tel_y ) err_fov_lon *= u.rad err_fov_lat *= u.rad nom = SkyCoord( fov_lon=src_fov_lon * u.rad, fov_lat=src_fov_lat * u.rad, frame=nom_frame ) sky_pos = nom.transform_to(array_pointing.frame) tilt = SkyCoord(x=core_x * u.m, y=core_y * u.m, frame=tilted_frame) grd = project_to_ground(tilt) x_max = self.reconstruct_xmax( nom.fov_lon, nom.fov_lat, tilt.x, tilt.y, hillas_dict_mod, tel_x, tel_y, 90 * u.deg - array_pointing.alt, ) src_error = np.sqrt(err_fov_lon ** 2 + err_fov_lat ** 2) result = ReconstructedGeometryContainer( alt=sky_pos.altaz.alt.to(u.rad), az=sky_pos.altaz.az.to(u.rad), core_x=grd.x, core_y=grd.y, core_uncert=u.Quantity(np.sqrt(core_err_x ** 2 + core_err_y ** 2), u.m), tel_ids=[h for h in hillas_dict_mod.keys()], average_intensity=np.mean([h.intensity for h in hillas_dict_mod.values()]), is_valid=True, alt_uncert=src_error.to(u.rad), az_uncert=src_error.to(u.rad), h_max=x_max, h_max_uncert=u.Quantity(np.nan * x_max.unit), goodness_of_fit=np.nan, ) return result
def 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
from astropy.coordinates import SkyCoord, AltAz from ctapipe.coordinates import ( NominalFrame, CameraFrame, TelescopeFrame, TiltedGroundFrame, project_to_ground, MissingFrameAttributeWarning, ) import warnings from ctapipe.core import traits __all__ = ["HillasIntersection"] INVALID = ReconstructedGeometryContainer(tel_ids=[]) class HillasIntersection(Reconstructor): """ This class is a simple re-implementation of Hillas parameter based event reconstruction. e.g. https://arxiv.org/abs/astro-ph/0607333 In this case the Hillas parameters are all constructed in the shared angular (Nominal) system. Direction reconstruction is performed by extrapolation of the major axes of the Hillas parameters in the nominal system and the weighted average of the crossing points is taken. Core reconstruction is performed by performing the same procedure in the tilted ground system. The height of maximum is reconstructed by the projection os the image
def _predict(self, event, hillas_dict, array_pointing, telescopes_pointings): """ The function you want to call for the reconstruction of the event. It takes care of setting up the event and consecutively calls the functions for the direction and core position reconstruction. Shower parameters not reconstructed by this class are set to np.nan Parameters ---------- hillas_dict: dict dictionary with telescope IDs as key and HillasParametersContainer instances as values 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 Raises ------ InvalidWidthException if any width is np.nan or 0 """ # filter warnings for missing obs time. this is needed because MC data has no obs time warnings.filterwarnings(action="ignore", category=MissingFrameAttributeWarning) # Here we perform some basic quality checks BEFORE applying reconstruction # This should be substituted by a DL1 QualityQuery specific to this # reconstructor hillas_planes, psi_core_dict = self.initialize_hillas_planes( hillas_dict, self.subarray, telescopes_pointings, array_pointing) # algebraic direction estimate direction, err_est_dir = self.estimate_direction(hillas_planes) # array pointing is needed to define the tilted frame core_pos_ground, core_pos_tilted = self.estimate_core_position( event, hillas_dict, array_pointing, psi_core_dict, hillas_planes) # container class for reconstructed showers _, lat, lon = cartesian_to_spherical(*direction) # estimate max height of shower h_max = self.estimate_h_max(hillas_planes) # astropy's coordinates system rotates counter-clockwise. # Apparently we assume it to be clockwise. # that's why lon get's a sign result = ReconstructedGeometryContainer( alt=lat, az=-lon, core_x=core_pos_ground.x, core_y=core_pos_ground.y, core_tilted_x=core_pos_tilted.x, core_tilted_y=core_pos_tilted.y, tel_ids=[h for h in hillas_dict.keys()], average_intensity=np.mean( [h.intensity for h in hillas_dict.values()]), is_valid=True, alt_uncert=err_est_dir, az_uncert=err_est_dir, h_max=h_max, ) return result
""" High level processing of showers. This processor will be able to process a shower/event in 3 steps: - shower geometry - estimation of energy (optional, currently unavailable) - estimation of classification (optional, currently unavailable) """ from ctapipe.core import Component, QualityQuery from ctapipe.core.traits import List from ctapipe.containers import ArrayEventContainer, ReconstructedGeometryContainer from ctapipe.instrument import SubarrayDescription from ctapipe.reco import HillasReconstructor DEFAULT_SHOWER_PARAMETERS = ReconstructedGeometryContainer(tel_ids=[]) class ShowerQualityQuery(QualityQuery): """Configuring shower-wise data checks.""" quality_criteria = List( default_value=[ ("> 50 phe", "lambda p: p.hillas.intensity > 50"), ("Positive width", "lambda p: p.hillas.width.value > 0"), ("> 3 pixels", "lambda p: p.morphology.num_pixels > 3"), ], help=QualityQuery.quality_criteria.help, ).tag(config=True)