Пример #1
0
def test_ground_frame_roundtrip():
    """test transform from sky to ground roundtrip"""
    from ctapipe.coordinates import GroundFrame, TiltedGroundFrame

    normal = SkyCoord(alt=70 * u.deg, az=0 * u.deg, frame=AltAz())
    coord = SkyCoord(x=0, y=10, z=5, unit=u.m, frame=GroundFrame())
    tilted = coord.transform_to(TiltedGroundFrame(pointing_direction=normal))

    back = tilted.transform_to(GroundFrame())

    assert u.isclose(coord.x, back.x, atol=1e-12 * u.m)
    assert u.isclose(coord.y, back.y, atol=1e-12 * u.m)
    assert u.isclose(coord.z, back.z, atol=1e-12 * u.m)
Пример #2
0
    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)
Пример #3
0
def test_ground_to_tilt():
    from ctapipe.coordinates import GroundFrame, TiltedGroundFrame

    # define ground coordinate
    grd_coord = GroundFrame(x=1 * u.m, y=2 * u.m, z=0 * u.m)
    pointing_direction = SkyCoord(alt=90 * u.deg, az=0 * u.deg, frame=AltAz())

    # Convert to tilted frame at zenith (should be the same)
    tilt_coord = grd_coord.transform_to(
        TiltedGroundFrame(pointing_direction=pointing_direction)
    )
    assert tilt_coord.separation_3d(grd_coord) == 0 * u.m

    # Check 180 degree rotation reverses y coordinate
    pointing_direction = SkyCoord(alt=90 * u.deg, az=180 * u.deg, frame=AltAz())
    tilt_coord = grd_coord.transform_to(
        TiltedGroundFrame(pointing_direction=pointing_direction)
    )
    assert np.abs(tilt_coord.y + 2.0 * u.m) < 1e-5 * u.m

    # Check that if we look at horizon the x coordinate is 0
    pointing_direction = SkyCoord(alt=0 * u.deg, az=0 * u.deg, frame=AltAz())
    tilt_coord = grd_coord.transform_to(
        TiltedGroundFrame(pointing_direction=pointing_direction)
    )
    assert np.abs(tilt_coord.x) < 1e-5 * u.m
Пример #4
0
def grd_to_tilt():
    grd_coord = GroundFrame(x=1 * u.m, y=2 * u.m, z=0 * u.m)
    tilt_coord = grd_coord.transform_to(
        TiltedGroundFrame(
            pointing_direction=AltAz(alt=90 * u.deg, az=180 * u.deg)))
    print(project_to_ground(tilt_coord))
    print("Tilted Coordinate", tilt_coord)
Пример #5
0
    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
Пример #6
0
    def get_prediction(self, tel_id, shower_reco, energy_reco):

        horizon_seed = HorizonFrame(az=shower_reco.az, alt=shower_reco.alt)
        nominal_seed = horizon_seed.transform_to(
            NominalFrame(array_direction=horizon_seed))
        source_x = nominal_seed.x.to(u.rad).value
        source_y = nominal_seed.y.to(u.rad).value

        ground = GroundFrame(x=shower_reco.core_x,
                             y=shower_reco.core_y,
                             z=0 * u.m)
        tilted = ground.transform_to(
            TiltedGroundFrame(pointing_direction=self.array_direction))
        tilt_x = tilted.x.to(u.m).value
        tilt_y = tilted.y.to(u.m).value

        zenith = 90 * u.deg - self.array_direction.alt

        x_max = shower_reco.h_max / np.cos(zenith)

        # Calculate expected Xmax given this energy
        x_max_exp = guess_shower_depth(energy_reco.energy)

        # Convert to binning of Xmax, addition of 100 can probably be removed
        x_max_bin = x_max - x_max_exp

        # Check for range
        if x_max_bin > 250 * (u.g * u.cm**-2):
            x_max_bin = 250 * (u.g * u.cm**-2)
        if x_max_bin < -250 * (u.g * u.cm**-2):
            x_max_bin = -250 * (u.g * u.cm**-2)

        x_max_bin = x_max_bin.value

        impact = np.sqrt(
            pow(self.tel_pos_x[tel_id] - tilt_x, 2) +
            pow(self.tel_pos_y[tel_id] - tilt_y, 2))

        phi = np.arctan2((self.tel_pos_y[tel_id] - tilt_y),
                         (self.tel_pos_x[tel_id] - tilt_x))

        pix_x_rot, pix_y_rot = self.rotate_translate(self.pixel_x[tel_id] * -1,
                                                     self.pixel_y[tel_id],
                                                     source_x, source_y, phi)

        prediction = self.image_prediction(self.type[tel_id],
                                           (90 * u.deg) - shower_reco.alt,
                                           shower_reco.az,
                                           energy_reco.energy.value, impact,
                                           x_max_bin,
                                           pix_x_rot * (180 / math.pi),
                                           pix_y_rot * (180 / math.pi))

        prediction *= self.scale[self.type[tel_id]]
        # prediction *= self.pixel_area[tel_id]

        prediction[prediction < 0] = 0
        prediction[np.isnan(prediction)] = 0

        return prediction
Пример #7
0
    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
Пример #8
0
def test_ground_to_eastnorth_roundtrip():
    """Check Ground to EastingNorthing and the round-trip"""
    from ctapipe.coordinates import GroundFrame, EastingNorthingFrame

    ground = SkyCoord(
        x=[1, 2, 3] * u.m, y=[-2, 5, 2] * u.m, z=[1, -1, 2] * u.m, frame=GroundFrame()
    )
    eastnorth = ground.transform_to(EastingNorthingFrame())
    ground2 = eastnorth.transform_to(GroundFrame())

    assert u.isclose(eastnorth.easting, [2, -5, -2] * u.m).all()
    assert u.isclose(eastnorth.northing, [1, 2, 3] * u.m).all()
    assert u.isclose(eastnorth.height, [1, -1, 2] * u.m).all()

    assert u.isclose(ground.x, ground2.x).all()
    assert u.isclose(ground.y, ground2.y).all()
    assert u.isclose(ground.z, ground2.z).all()
Пример #9
0
    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
Пример #10
0
def test_ground_to_tilt_many_to_many():
    from ctapipe.coordinates import GroundFrame, TiltedGroundFrame

    # define ground coordinate
    grd_coord = GroundFrame(x=[1, 1] * u.m, y=[2, 2] * u.m, z=[0, 0] * u.m)
    pointing_direction = SkyCoord(
        alt=[90, 90, 90], az=[0, 0, 90], frame=AltAz(), unit=u.deg
    )

    with raises(ValueError):
        # there will be a shape mismatch in matrix multiplication
        grd_coord.transform_to(TiltedGroundFrame(pointing_direction=pointing_direction))
Пример #11
0
    def add_impact_point(self,
                         label=None,
                         status="mc",
                         frame="ground",
                         gnd_reco_pos=None):

        if label is None:
            label = ""
        if frame == "ground":
            if status == "mc":
                core_x_pos = self.event.mc.core_x.to_value(u.m)
                core_y_pos = self.event.mc.core_y.to_value(u.m)
                core_z_pos = 0
            elif status == "reco":
                core_x_pos = gnd_reco_pos.x.to_value(u.m)
                core_y_pos = gnd_reco_pos.y.to_value(u.m)
                core_z_pos = 0

        elif frame == "tilted":
            tilted_frame = TiltedGroundFrame(
                pointing_direction=self.array_pointing)

            if status == "mc":
                gnd_core_mc = GroundFrame(x=self.event.mc.core_x,
                                          y=self.event.mc.core_y,
                                          z=0.0 * u.m)
                tilted_core_mc = gnd_core_mc.transform_to(tilted_frame)
            elif status == "reco":
                tilted_core_mc = gnd_reco_pos.transform_to(tilted_frame)

            core_x_pos = tilted_core_mc.x.to_value(u.m)
            core_y_pos = tilted_core_mc.y.to_value(u.m)
            core_z_pos = 0

        tel_label = create_extruded_text(text=label)
        tel_label = scale_object(tel_label, 20)
        # those "magic numbers" are needed to have the center of the first letter
        # of the label in the exact position. Depends on the fontsize.
        tel_label = translate_object(
            tel_label, center=[core_x_pos - 12, core_y_pos - 9, core_z_pos])

        mapper = vtk.vtkPolyDataMapper()
        mapper.SetInputConnection(tel_label.GetOutputPort())

        actor = vtk.vtkActor()
        actor.SetMapper(mapper)
        actor.GetProperty().SetColor([0, 0, 255])
        if frame == "tilted":
            actor.RotateZ(-self.array_pointing.az.value)
            actor.RotateY(90 - self.array_pointing.alt.value)
        self.ren.AddActor(actor)
Пример #12
0
    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
Пример #13
0
def test_ground_to_tilt_many_to_one():
    from ctapipe.coordinates import GroundFrame, TiltedGroundFrame

    # define ground coordinate
    grd_coord = GroundFrame(x=[1, 1] * u.m, y=[2, 2] * u.m, z=[0, 0] * u.m)
    pointing_direction = SkyCoord(alt=90, az=0, frame=AltAz(), unit=u.deg)

    # Convert to tilted frame at zenith (should be the same)
    tilt_coord = grd_coord.transform_to(
        TiltedGroundFrame(pointing_direction=pointing_direction)
    )

    # We do a one-to-one conversion
    assert len(tilt_coord.data) == 2
Пример #14
0
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
Пример #15
0
    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
Пример #16
0
    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()
Пример #17
0
    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,
            )
Пример #18
0
    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,
            )
Пример #19
0
    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
Пример #20
0
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
Пример #21
0
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)
Пример #22
0
    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,
            )
Пример #23
0
    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
Пример #24
0
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)
Пример #25
0
    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
Пример #26
0
    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
Пример #27
0
    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()
Пример #28
0
    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
Пример #29
0
    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
Пример #30
0
    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