예제 #1
0
def calculate_polarization_vector(launch_vector, shower_axis):
    """ calculates the polarization vector in spherical coordinates (eR, eTheta, ePhi)
	"""
    polarization_direction = np.cross(launch_vector,
                                      np.cross(shower_axis, launch_vector))
    polarization_direction /= np.linalg.norm(polarization_direction)
    cs = cstrans.cstrafo(*hp.cartesian_to_spherical(*launch_vector))
    return cs.transform_from_ground_to_onsky(polarization_direction)
예제 #2
0
def make_sim_shower(corsika, observer=None, detector=None, station_id=None):
    sim_shower = NuRadioReco.framework.radio_shower.RadioShower()

    zenith, azimuth, magnetic_field_vector = get_angles(corsika)
    sim_shower.set_parameter(shp.zenith, zenith)
    sim_shower.set_parameter(shp.azimuth, azimuth)
    sim_shower.set_parameter(shp.magnetic_field_vector, magnetic_field_vector)

    energy = corsika['inputs'].attrs["ERANGE"][0] * units.GeV
    sim_shower.set_parameter(shp.energy, energy)
    # We can only set the shower core relative to the station if we know its position
    if observer is not None and detector is not None and station_id is not None:
        station_position = detector.get_absolute_position(station_id)
        position = observer.attrs['position']
        cs = coordinatesystems.cstrafo(zenith, azimuth, magnetic_field_vector=magnetic_field_vector)
        observer_position = np.zeros(3)
        observer_position[0], observer_position[1], observer_position[2] = -position[1] * units.cm, position[0] * units.cm, position[2] * units.cm
        observer_position = cs.transform_from_magnetic_to_geographic(observer_position)
        core_position = (-observer_position + station_position)
        core_position[2] = 0
        sim_shower.set_parameter(shp.core, core_position)

    sim_shower.set_parameter(shp.shower_maximum, corsika['CoREAS'].attrs['DepthOfShowerMaximum'] * units.g / units.cm2)
    sim_shower.set_parameter(shp.refractive_index_at_ground, corsika['CoREAS'].attrs["GroundLevelRefractiveIndex"])
    sim_shower.set_parameter(shp.magnetic_field_rotation,
                             corsika['CoREAS'].attrs["RotationAngleForMagfieldDeclination"] * units.degree)
    sim_shower.set_parameter(shp.distance_shower_maximum_geometric,
                             corsika['CoREAS'].attrs["DistanceOfShowerMaximum"] * units.cm)

    sim_shower.set_parameter(shp.observation_level, corsika["inputs"].attrs["OBSLEV"] * units.cm)
    sim_shower.set_parameter(shp.primary_particle, corsika["inputs"].attrs["PRMPAR"])
    if 'ATMOD' in corsika['inputs'].attrs.keys():
        sim_shower.set_parameter(shp.atmospheric_model, corsika["inputs"].attrs["ATMOD"])

    try:
        sim_shower.set_parameter(shp.electromagnetic_energy, corsika["highlevel"].attrs["Eem"] * units.eV)
    except:
        global warning_printed_coreas_py
        if(not warning_printed_coreas_py):
            logger.warning("No high-level quantities in HDF5 file, not setting EM energy, this warning will be only printed once")
            warning_printed_coreas_py = True

    return sim_shower
    def run(self, detector, output_mode=0):
        """
        Read in a random sample of stations from a CoREAS file.
        For each position the closest observer is selected and a simulated
        event is created for that observer.

        Parameters
        ----------
        detector: Detector object
            Detector description of the detector that shall be simulated
        output_mode: integer (default 0)
            0: only the event object is returned
            1: the function reuturns the event object, the current inputfilename, the distance between the choosen station and the requested core position,
               and the area in which the core positions are randomly distributed


        """
        while (self.__current_input_file < len(self.__input_files)):
            t = time.time()
            t_per_event = time.time()
            filesize = os.path.getsize(
                self.__input_files[self.__current_input_file])
            if (
                    filesize < 18456 * 2
            ):  # based on the observation that a file with such a small filesize is corrupt
                self.logger.warning(
                    "file {} seems to be corrupt, skipping to next file".
                    format(self.__input_files[self.__current_input_file]))
                self.__current_input_file += 1
                continue
            corsika = h5py.File(self.__input_files[self.__current_input_file],
                                "r")
            self.logger.info(
                "using coreas simulation {} with E={:2g} theta = {:.0f}".
                format(self.__input_files[self.__current_input_file],
                       corsika['inputs'].attrs["ERANGE"][0] * units.GeV,
                       corsika['inputs'].attrs["THETAP"][0]))
            positions = []
            for i, observer in enumerate(
                    corsika['CoREAS']['observers'].values()):
                position = observer.attrs['position']
                positions.append(
                    np.array([-position[1], position[0], 0]) * units.cm)
#                 self.logger.debug("({:.0f}, {:.0f})".format(positions[i][0], positions[i][1]))
            positions = np.array(positions)

            zenith, azimuth, magnetic_field_vector = coreas.get_angles(corsika)
            cs = cstrafo.cstrafo(zenith, azimuth, magnetic_field_vector)
            positions_vBvvB = cs.transform_from_magnetic_to_geographic(
                positions.T)
            positions_vBvvB = cs.transform_to_vxB_vxvxB(positions_vBvvB).T
            #             for i, pos in enumerate(positions_vBvvB):
            #                 self.logger.debug("star shape")
            #                 self.logger.debug("({:.0f}, {:.0f}); ({:.0f}, {:.0f})".format(positions[i, 0], positions[i, 1], pos[0], pos[1]))

            dd = (positions_vBvvB[:, 0]**2 + positions_vBvvB[:, 1]**2)**0.5
            ddmax = dd.max()
            self.logger.info("star shape from: {} - {}".format(
                -dd.max(), dd.max()))

            # generate core positions randomly within a rectangle
            cores = np.array([
                self.__random_generator.uniform(self.__area[0], self.__area[1],
                                                self.__n_cores),
                self.__random_generator.uniform(self.__area[2], self.__area[3],
                                                self.__n_cores),
                np.zeros(self.__n_cores)
            ]).T

            self.__t_per_event += time.time() - t_per_event
            self.__t += time.time() - t

            station_ids = detector.get_station_ids()
            for iCore, core in enumerate(cores):
                t = time.time()
                evt = NuRadioReco.framework.event.Event(
                    self.__current_input_file, iCore)  # create empty event
                sim_shower = coreas.make_sim_shower(corsika)
                evt.add_sim_shower(sim_shower)
                rd_shower = NuRadioReco.framework.radio_shower.RadioShower(
                    station_ids=station_ids)
                evt.add_shower(rd_shower)

                for station_id in station_ids:
                    # convert into vxvxB frame to calculate closests simulated station to detecor station
                    det_station_position = detector.get_absolute_position(
                        station_id)
                    det_station_position[2] = 0
                    core_rel_to_station = core - det_station_position
                    #             core_rel_to_station_vBvvB = cs.transform_from_magnetic_to_geographic(core_rel_to_station)
                    core_rel_to_station_vBvvB = cs.transform_to_vxB_vxvxB(
                        core_rel_to_station)
                    dcore = (core_rel_to_station_vBvvB[0]**2 +
                             core_rel_to_station_vBvvB[1]**2)**0.5
                    #                     print(f"{core_rel_to_station}, {core_rel_to_station_vBvvB} -> {dcore}")
                    if (dcore > ddmax):
                        # station is outside of the star shape pattern, create empty station
                        station = NuRadioReco.framework.station.Station(
                            station_id)
                        channel_ids = detector.get_channel_ids(station_id)
                        sim_station = coreas.make_sim_station(
                            station_id, corsika, None, channel_ids)
                        station.set_sim_station(sim_station)
                        evt.set_station(station)
                        self.logger.debug(
                            f"station {station_id} is outside of star shape, channel_ids {channel_ids}"
                        )
                    else:
                        distances = np.linalg.norm(
                            core_rel_to_station_vBvvB[:2] -
                            positions_vBvvB[:, :2],
                            axis=1)
                        index = np.argmin(distances)
                        distance = distances[index]
                        key = list(
                            corsika['CoREAS']['observers'].keys())[index]
                        self.logger.debug(
                            "generating core at ground ({:.0f}, {:.0f}), rel to station ({:.0f}, {:.0f}) vBvvB({:.0f}, {:.0f}), nearest simulated station is {:.0f}m away at ground ({:.0f}, {:.0f}), vBvvB({:.0f}, {:.0f})"
                            .format(cores[iCore][0], cores[iCore][1],
                                    core_rel_to_station[0],
                                    core_rel_to_station[1],
                                    core_rel_to_station_vBvvB[0],
                                    core_rel_to_station_vBvvB[1],
                                    distance / units.m, positions[index][0],
                                    positions[index][1],
                                    positions_vBvvB[index][0],
                                    positions_vBvvB[index][1]))
                        t_event_structure = time.time()
                        observer = corsika['CoREAS']['observers'].get(key)

                        station = NuRadioReco.framework.station.Station(
                            station_id)
                        channel_ids = detector.get_channel_ids(station_id)
                        sim_station = coreas.make_sim_station(
                            station_id, corsika, observer, channel_ids)
                        station.set_sim_station(sim_station)
                        evt.set_station(station)
                if (output_mode == 0):
                    self.__t += time.time() - t
                    yield evt
                elif (output_mode == 1):
                    self.__t += time.time() - t
                    self.__t_event_structure += time.time() - t_event_structure
                    yield evt, self.__current_input_file
                else:
                    self.logger.debug("output mode > 1 not implemented")
                    raise NotImplementedError

            self.__current_input_file += 1
예제 #4
0
    def get_time_trace(self, shower_energy, theta, N, dt, shower_type, n_index, R, shift_for_xmax=False,
                       same_shower=False, iN=None, output_mode='trace', maximum_angle=20 * units.deg):
        """
        calculates the electric-field Askaryan pulse from a charge-excess profile

        Parameters
        ----------
        shower_energy: float
            the energy of the shower
        theta: float
            viewing angle, i.e., the angle between shower axis and launch angle of the signal (the ray path)
        N: int
            number of samples in the time domain
        dt: float
            size of one time bin in units of time
        profile_depth: array of floats
            shower depth values of the charge excess profile
        profile_ce: array of floats
            charge-excess values of the charge excess profile
        shower_type: string (default "HAD")
            type of shower, either "HAD" (hadronic), "EM" (electromagnetic) or "TAU" (tau lepton induced)
        n_index: float (default 1.78)
            index of refraction where the shower development takes place
        R: float (default 1km)
            observation distance, the signal amplitude will be scaled according to 1/R
        interp_factor: int (default 10)
            interpolation factor of charge-excess profile. Results in a more precise numerical integration which might be beneficial
            for small vertex distances but also slows down the calculation proportional to the interpolation factor.
        shift_for_xmax: bool (default True)
            if True the observer position is placed relative to the position of the shower maximum, if False it is placed
            with respect to (0,0,0) which is the start of the charge-excess profile
        same_shower: bool (default False)
            if False, for each request a new random shower realization is choosen.
            if True, the shower from the last request of the same shower type is used. This is needed to get the Askaryan
            signal for both ray tracing solutions from the same shower.
        iN: int or None (default None)
            specify shower number
        output_mode: string
            * 'trace' (default): return only the electric field trace
            * 'Xmax': return trace and position of xmax in units of length
            * 'full' return trace, depth and charge_excess profile
        maximum_angle: float
            Maximum angular difference allowed between the observer angle and the Cherenkov angle.
            If the difference is greater, the function returns an empty trace.

        Returns: array of floats
            array of electric-field time trace in 'on-sky' coordinate system eR, eTheta, ePhi
        """
        if not shower_type in self._library.keys():
            raise KeyError("shower type {} not present in library. Available shower types are {}".format(shower_type, *self._library.keys()))

        # Due to the oscillatory nature of the ARZ integral, some numerical instabilities arise
        #  for angles near the axis and near 90 degrees. This creates some waveforms with large
        # spikes due to numerical errors, while the real electric field should be much smaller
        # than near the Cherenkov cone due to the loss of coherence. Since incoherent events
        # should not trigger, we return an empty trace for angular differences > 20 degrees.
        cherenkov_angle = np.arccos(1 / n_index)

        if np.abs(theta - cherenkov_angle) > maximum_angle:
            logger.info(f"viewing angle {theta/units.deg:.1f}deg is more than {maximum_angle/units.deg:.1f}deg away from the cherenkov cone. Returning zero trace.")
            self._random_numbers[shower_type] = None
            empty_trace = np.zeros((3, N))
            return empty_trace

        # determine closes available energy in shower library
        energies = np.array([*self._library[shower_type]])
        iE = np.argmin(np.abs(energies - shower_energy))
        rescaling_factor = shower_energy / energies[iE]
        logger.info("shower energy of {:.3g}eV requested, closest available energy is {:.3g}eV. The amplitude of the charge-excess profile will be rescaled accordingly by a factor of {:.2f}".format(shower_energy / units.eV, energies[iE] / units.eV, rescaling_factor))
        profiles = self._library[shower_type][energies[iE]]
        N_profiles = len(profiles['charge_excess'])

        if(iN is None):
            if(same_shower):
                if(shower_type in self._random_numbers):
                    iN = self._random_numbers[shower_type]
                    logger.info("using previously used shower {}/{}".format(iN, N_profiles))
                else:
                    logger.warning("no previous random number for shower type {} exists. Generating a new random number.".format(shower_type))
                    iN = self._random_generator.randint(N_profiles)
                    self._random_numbers[shower_type] = iN
                    logger.info("picking profile {}/{} randomly".format(iN, N_profiles))
            else:
                iN = self._random_generator.randint(N_profiles)
                self._random_numbers[shower_type] = iN
                logger.info("picking profile {}/{} randomly".format(iN, N_profiles))
        else:
            iN = int(iN)  # saveguard against iN being a float
            logger.info("using shower {}/{} as specified by user".format(iN, N_profiles))
            self._random_numbers[shower_type] = iN

        profile_depth = profiles['depth']
        profile_ce = profiles['charge_excess'][iN] * rescaling_factor

        xmax = profile_depth[np.argmax(profile_ce)]

        vp = self.get_vector_potential_fast(shower_energy, theta, N, dt, profile_depth, profile_ce, shower_type, n_index, R,
                                            self._interp_factor, self._interp_factor2, shift_for_xmax)
        trace = -np.diff(vp, axis=0) / dt
#         trace = -np.gradient(vp, axis=0) / dt

        # use viewing angle relative to shower maximum for rotation into spherical coordinate system (that reduced eR component)
        if shift_for_xmax:
            thetaprime = theta
        else:
            thetaprime = theta_to_thetaprime(theta, xmax, R)
        cs = cstrafo.cstrafo(zenith=thetaprime, azimuth=0)
        trace_onsky = cs.transform_from_ground_to_onsky(trace.T)
        if(output_mode == 'full'):
            return trace_onsky, profile_depth, profile_ce
        elif(output_mode == 'Xmax'):
            xmax = profile_depth[np.argmax(profile_ce)]
            Lmax = xmax / rho
            return trace_onsky, Lmax
        return trace_onsky
예제 #5
0
    def run(self):
        """
        Reads in a CoREAS file and returns an event containing all simulated stations

        """
        while (self.__current_input_file < len(self.__input_files)):
            t = time.time()
            t_per_event = time.time()

            filesize = os.path.getsize(
                self.__input_files[self.__current_input_file])
            if (
                    filesize < 18456 * 2
            ):  # based on the observation that a file with such a small filesize is corrupt
                logger.warning(
                    "file {} seems to be corrupt, skipping to next file".
                    format(self.__input_files[self.__current_input_file]))
                self.__current_input_file += 1
                continue

            logger.info('Reading %s ...' %
                        self.__input_files[self.__current_input_file])

            corsika = h5py.File(self.__input_files[self.__current_input_file],
                                "r")
            logger.info(
                "using coreas simulation {} with E={:2g} theta = {:.0f}".
                format(self.__input_files[self.__current_input_file],
                       corsika['inputs'].attrs["ERANGE"][0] * units.GeV,
                       corsika['inputs'].attrs["THETAP"][0]))

            f_coreas = corsika["CoREAS"]

            if self.__ascending_run_and_event_number:
                evt = NuRadioReco.framework.event.Event(
                    self.__ascending_run_and_event_number,
                    self.__ascending_run_and_event_number)
                self.__ascending_run_and_event_number += 1
            else:
                evt = NuRadioReco.framework.event.Event(
                    corsika['inputs'].attrs['RUNNR'],
                    corsika['inputs'].attrs['EVTNR'])

            evt.__event_time = f_coreas.attrs["GPSSecs"]

            # create sim shower, no core is set since no external detector description is given
            sim_shower = coreas.make_sim_shower(corsika)
            sim_shower.set_parameter(
                shp.core,
                np.array([
                    0, 0, f_coreas.attrs["CoreCoordinateVertical"] / 100
                ]))  # set core
            evt.add_sim_shower(sim_shower)

            # initialize coordinate transformation
            cs = coordinatesystems.cstrafo(
                sim_shower.get_parameter(shp.zenith),
                sim_shower.get_parameter(shp.azimuth),
                magnetic_field_vector=sim_shower.get_parameter(
                    shp.magnetic_field_vector))

            # add simulated pulses as sim station
            for idx, (name,
                      observer) in enumerate(f_coreas['observers'].items()):
                station_id = antenna_id(
                    name, idx)  # returns proper station id if possible

                station = NuRadioReco.framework.station.Station(station_id)
                if self.__det is None:
                    sim_station = coreas.make_sim_station(
                        station_id, corsika, observer, channel_ids=[0, 1, 2])
                else:
                    sim_station = coreas.make_sim_station(
                        station_id,
                        corsika,
                        observer,
                        channel_ids=self.__det.get_channel_ids(
                            self.__det.get_default_station_id()))
                station.set_sim_station(sim_station)
                evt.set_station(station)
                if self.__det is not None:
                    position = observer.attrs['position']
                    antenna_position = np.zeros(3)
                    antenna_position[0], antenna_position[1], antenna_position[
                        2] = -position[1] * units.cm, position[
                            0] * units.cm, position[2] * units.cm
                    antenna_position = cs.transform_from_magnetic_to_geographic(
                        antenna_position)
                    if not self.__det.has_station(station_id):
                        self.__det.add_generic_station({
                            'station_id':
                            station_id,
                            'pos_easting':
                            antenna_position[0],
                            'pos_northing':
                            antenna_position[1],
                            'pos_altitude':
                            antenna_position[2]
                        })
                    else:
                        self.__det.add_station_properties_for_event(
                            {
                                'pos_easting': antenna_position[0],
                                'pos_northing': antenna_position[1],
                                'pos_altitude': antenna_position[2]
                            }, station_id, evt.get_run_number(), evt.get_id())

            self.__t_per_event += time.time() - t_per_event
            self.__t += time.time() - t

            self.__current_input_file += 1
            if self.__det is None:
                yield evt
            else:
                self.__det.set_event(evt.get_run_number(), evt.get_id())
                yield evt, self.__det
예제 #6
0
def make_sim_station(station_id, corsika, observer, channel_ids, weight=None):
    """
    creates an NuRadioReco sim station from the observer object of the coreas hdf5 file

    Parameters
    ----------
    station_id : station id
        the id of the station to create
    corsika : hdf5 file object
        the open hdf5 file object of the corsika hdf5 file
    observer : hdf5 observer object
    channel_ids :
    weight : weight of individual station
        weight corresponds to area covered by station

    Returns
    -------
    sim_station: sim station
        ARIANNA simulated station object
    """
    # loop over all coreas stations, rotate to ARIANNA CS and save to simulation branch
    zenith, azimuth, magnetic_field_vector = get_angles(corsika)
    if(observer is None):
        data = np.zeros((512, 4))
        data[:, 0] = np.arange(0, 512) * units.ns / units.second
    else:
        data = np.copy(observer)
        data[:, 1], data[:, 2] = -observer[:, 2], observer[:, 1]

    # convert to SI units
    data[:, 0] *= units.second
    data[:, 1] *= conversion_fieldstrength_cgs_to_SI
    data[:, 2] *= conversion_fieldstrength_cgs_to_SI
    data[:, 3] *= conversion_fieldstrength_cgs_to_SI

    cs = coordinatesystems.cstrafo(zenith, azimuth, magnetic_field_vector=magnetic_field_vector)
    efield = cs.transform_from_magnetic_to_geographic(data[:, 1:].T)
    efield = cs.transform_from_ground_to_onsky(efield)

    # prepend trace with zeros to not have the pulse directly at the start
    n_samples_prepend = efield.shape[1]
    efield2 = np.zeros((3, n_samples_prepend + efield.shape[1]))
    efield2[0] = np.append(np.zeros(n_samples_prepend), efield[0])
    efield2[1] = np.append(np.zeros(n_samples_prepend), efield[1])
    efield2[2] = np.append(np.zeros(n_samples_prepend), efield[2])

    sampling_rate = 1. / (corsika['CoREAS'].attrs['TimeResolution'] * units.second)
    sim_station = NuRadioReco.framework.sim_station.SimStation(station_id)
    electric_field = NuRadioReco.framework.electric_field.ElectricField(channel_ids)
    electric_field.set_trace(efield2, sampling_rate)
    electric_field.set_parameter(efp.ray_path_type, 'direct')
    electric_field.set_parameter(efp.zenith, zenith)
    electric_field.set_parameter(efp.azimuth, azimuth)
    sim_station.add_electric_field(electric_field)
    sim_station.set_parameter(stnp.azimuth, azimuth)
    sim_station.set_parameter(stnp.zenith, zenith)
    energy = corsika['inputs'].attrs["ERANGE"][0] * units.GeV
    sim_station.set_parameter(stnp.cr_energy, energy)
    sim_station.set_magnetic_field_vector(magnetic_field_vector)
    sim_station.set_parameter(stnp.cr_xmax, corsika['CoREAS'].attrs['DepthOfShowerMaximum'])
    try:
        sim_station.set_parameter(stnp.cr_energy_em, corsika["highlevel"].attrs["Eem"])
    except:
        global warning_printed_coreas_py
        if(not warning_printed_coreas_py):
            logger.warning("No high-level quantities in HDF5 file, not setting EM energy, this warning will be only printed once")
            warning_printed_coreas_py = True
    sim_station.set_is_cosmic_ray()
    sim_station.set_simulation_weight(weight)
    return sim_station
예제 #7
0
    def run(self, detector, output_mode=0):
        """
        Read in a random sample of stations from a CoREAS file.
        A number of random positions is selected within a certain radius.
        For each position the closest observer is selected and a simulated
        event is created for that observer.

        Parameters
        ----------
        detector: Detector object
            Detector description of the detector that shall be simulated
        output_mode: integer (default 0)
            0: only the event object is returned
            1: the function reuturns the event object, the current inputfilename, the distance between the choosen station and the requested core position,
               and the area in which the core positions are randomly distributed


        """
        while (self.__current_input_file < len(self.__input_files)):
            t = time.time()
            t_per_event = time.time()
            filesize = os.path.getsize(self.__input_files[self.__current_input_file])
            if(filesize < 18456 * 2):  # based on the observation that a file with such a small filesize is corrupt
                self.logger.warning("file {} seems to be corrupt, skipping to next file".format(self.__input_files[self.__current_input_file]))
                self.__current_input_file += 1
                continue
            corsika = h5py.File(self.__input_files[self.__current_input_file], "r")
            self.logger.info(
                "using coreas simulation {} with E={:2g} theta = {:.0f}".format(
                    self.__input_files[self.__current_input_file],
                    corsika['inputs'].attrs["ERANGE"][0] * units.GeV,
                    corsika['inputs'].attrs["THETAP"][0]
                )
            )
            positions = []
            for i, observer in enumerate(corsika['CoREAS']['observers'].values()):
                position = observer.attrs['position']
                positions.append(np.array([-position[1], position[0], 0]) * units.cm)
                self.logger.debug("({:.0f}, {:.0f})".format(position[0], position[1]))
            positions = np.array(positions)

            max_distance = self.__max_distace
            if(max_distance is None):
                max_distance = np.max(np.abs(positions[:, 0:2]))
            area = np.pi * max_distance ** 2

            if(output_mode == 0):
                n_cores = self.__n_cores * 100  # for output mode 1 we want always n_cores in star pattern. Therefore we generate more core positions to be able to select n_cores in the star pattern afterwards
            elif(output_mode == 1):
                n_cores = self.__n_cores
            else:
                raise ValueError('output mode {} not defined.'.format(output_mode))
            theta = self.__random_generator.rand(n_cores) * 2 * np.pi
            r = (self.__random_generator.rand(n_cores)) ** 0.5 * max_distance
            cores = np.array([r * np.cos(theta), r * np.sin(theta), np.zeros(n_cores)]).T

            zenith, azimuth, magnetic_field_vector = coreas.get_angles(corsika)
            cs = cstrafo.cstrafo(zenith, azimuth, magnetic_field_vector)
            positions_vBvvB = cs.transform_from_magnetic_to_geographic(positions.T)
            positions_vBvvB = cs.transform_to_vxB_vxvxB(positions_vBvvB).T
            dd = (positions_vBvvB[:, 0] ** 2 + positions_vBvvB[:, 1] ** 2) ** 0.5
            ddmax = dd.max()
            self.logger.info("star shape from: {} - {}".format(-dd.max(), dd.max()))

            cores_vBvvB = cs.transform_from_magnetic_to_geographic(cores.T)
            cores_vBvvB = cs.transform_to_vxB_vxvxB(cores_vBvvB).T
            dcores = (cores_vBvvB[:, 0] ** 2 + cores_vBvvB[:, 1] ** 2) ** 0.5
            mask_cores_in_starpattern = dcores <= ddmax

            if((not np.sum(mask_cores_in_starpattern)) and (output_mode == 1)):  # handle special case of no core position being generated within star pattern
                observer = corsika['CoREAS']['observers'].values()[0]

                evt = NuRadioReco.framework.event.Event(corsika['inputs'].attrs['RUNNR'], corsika['inputs'].attrs['EVTNR'])  # create empty event
                station = NuRadioReco.framework.station.Station(self.__station_id)
                sim_station = coreas.make_sim_station(self.__station_id, corsika, observer, detector.get_channel_ids(self.__station_id))

                station.set_sim_station(sim_station)
                evt.set_station(station)
                yield evt, self.__current_input_file, None, area

            cores_to_iterate = cores_vBvvB[mask_cores_in_starpattern]
            if(output_mode == 0):  # select first n_cores that are in star pattern
                if(np.sum(mask_cores_in_starpattern) < self.__n_cores):
                    self.logger.warning("only {0} cores contained in star pattern, returning {0} cores instead of {1} cores that were requested".format(np.sum(mask_cores_in_starpattern), self.__n_cores))
                else:
                    cores_to_iterate = cores_vBvvB[mask_cores_in_starpattern][:self.__n_cores]

            self.__t_per_event += time.time() - t_per_event
            self.__t += time.time() - t

            for iCore, core in enumerate(cores_to_iterate):
                t = time.time()
                # check if out of bounds

                distances = np.linalg.norm(core[:2] - positions_vBvvB[:, :2], axis=1)
                index = np.argmin(distances)
                distance = distances[index]
                key = list(corsika['CoREAS']['observers'].keys())[index]
                self.logger.info(
                    "generating core at ground ({:.0f}, {:.0f}), vBvvB({:.0f}, {:.0f}), nearest simulated station is {:.0f}m away at ground ({:.0f}, {:.0f}), vBvvB({:.0f}, {:.0f})".format(
                        cores[iCore][0],
                        cores[iCore][1],
                        core[0],
                        core[1],
                        distance / units.m,
                        positions[index][0],
                        positions[index][1],
                        positions_vBvvB[index][0],
                        positions_vBvvB[index][1]
                    )
                )
                t_event_structure = time.time()
                observer = corsika['CoREAS']['observers'].get(key)

                evt = NuRadioReco.framework.event.Event(self.__current_input_file, iCore)  # create empty event
                station = NuRadioReco.framework.station.Station(self.__station_id)
                channel_ids = detector.get_channel_ids(self.__station_id)
                sim_station = coreas.make_sim_station(self.__station_id, corsika, observer, channel_ids)
                station.set_sim_station(sim_station)
                evt.set_station(station)
                sim_shower = coreas.make_sim_shower(corsika, observer, detector, self.__station_id)
                evt.add_sim_shower(sim_shower)
                rd_shower = NuRadioReco.framework.radio_shower.RadioShower(station_ids=[station.get_id()])
                evt.add_shower(rd_shower)
                if(output_mode == 0):
                    self.__t += time.time() - t
                    self.__t_event_structure += time.time() - t_event_structure
                    yield evt
                elif(output_mode == 1):
                    self.__t += time.time() - t
                    self.__t_event_structure += time.time() - t_event_structure
                    yield evt, self.__current_input_file, distance, area
                else:
                    self.logger.debug("output mode > 1 not implemented")
                    raise NotImplementedError

            self.__current_input_file += 1
    def run(self, evt, station, det, debug=False, debug_plotpath=None,
            use_channels=None,
            bandpass=None,
            use_MC_direction=False):
        """
        run method. This function is executed for each event

        Parameters
        ---------
        evt
        station
        det
        debug: bool
            if True debug plotting is enables
        debug_plotpath: string or None
            if not None plots will be saved to a file rather then shown. Plots will
            be save into the `debug_plotpath` directory
        use_channels: array of ints (default: [0, 1, 2, 3])
            the channel ids to use for the electric field reconstruction
            default: 0 - 3
        bandpass: [float, float] (default: [100 * units.MHz, 500 * units.MHz])
            the lower and upper frequecy for which the analytic pulse is calculated.
            A butterworth filter of 10th order and a rectangular filter is applied.
            default 100 - 500 MHz
        use_MC_direction: bool
            use simulated direction instead of reconstructed direction
        """
        if use_channels is None:
            use_channels = [0, 1, 2, 3]
        if bandpass is None:
            bandpass = [100 * units.MHz, 500 * units.MHz]
        self.__counter += 1
        station_id = station.get_id()
        logger.info("event {}, station {}".format(evt.get_id(), station_id))
        if use_MC_direction and (station.get_sim_station() is not None):
            zenith = station.get_sim_station()[stnp.zenith]
            azimuth = station.get_sim_station()[stnp.azimuth]
            sim_present = True
        else:
            logger.warning("Using reconstructed angles as no simulation present")
            zenith = station[stnp.zenith]
            azimuth = station[stnp.azimuth]
            sim_present = False

        efield_antenna_factor, V, V_timedomain = get_array_of_channels(station, use_channels,
                                                                       det, zenith, azimuth, self.antenna_provider,
                                                                       time_domain=True)
        sampling_rate = station.get_channel(0).get_sampling_rate()
        n_samples_time = V_timedomain.shape[1]

        noise_RMS = det.get_noise_RMS(station.get_id(), 0)

        def obj_xcorr(params):
            if(len(params) == 3):
                slope, ratio2, phase2 = params
                ratio = (np.arctan(ratio2) + np.pi * 0.5) / np.pi  # project -inf..inf on 0..1
            elif(len(params) == 2):
                slope, ratio2 = params
                phase2 = 0
                ratio = (np.arctan(ratio2) + np.pi * 0.5) / np.pi  # project -inf..inf on 0..1
            elif(len(params) == 1):
                phase2 = 0
                ratio = 0
                slope = params[0]
            phase = np.arctan(phase2)  # project -inf..+inf to -0.5 pi..0.5 pi

            analytic_pulse_theta = pulse.get_analytic_pulse_freq(ratio, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
            analytic_pulse_phi = pulse.get_analytic_pulse_freq(1 - ratio, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
            chi2 = 0

            n_channels = len(V_timedomain)
            analytic_traces = np.zeros((n_channels, n_samples_time))
            positions = np.zeros(n_channels, dtype=np.int)
            max_xcorrs = np.zeros(n_channels)
            # first determine the position with the larges xcorr
            for iCh, trace in enumerate(V_timedomain):
                analytic_trace_fft = np.sum(efield_antenna_factor[iCh] * np.array([analytic_pulse_theta, analytic_pulse_phi]), axis=0)
                analytic_traces[iCh] = fft.freq2time(analytic_trace_fft, sampling_rate)
                xcorr = np.abs(hp.get_normalized_xcorr(trace, analytic_traces[iCh]))
                positions[iCh] = np.argmax(np.abs(xcorr)) + 1
                max_xcorrs[iCh] = xcorr.max()
                chi2 -= xcorr.max()
            logger.debug("ratio = {:.2f}, slope = {:.4g}, phase = {:.0f} ({:.4f}), chi2 = {:.4g}".format(ratio, slope, phase / units.deg, phase2, chi2))
            return chi2

        def obj_amplitude(params, slope, phase, pos, debug_obj=0):
            if(len(params) == 2):
                ampPhi, ampTheta = params
            elif(len(params) == 1):
                ampPhi = params[0]
                ampTheta = 0
            analytic_pulse_theta = pulse.get_analytic_pulse_freq(ampTheta, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
            analytic_pulse_phi = pulse.get_analytic_pulse_freq(ampPhi, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
            chi2 = 0

            if(debug_obj):
                fig, ax = plt.subplots(4, 2, sharex=True)

            n_channels = len(V_timedomain)
            analytic_traces = np.zeros((n_channels, n_samples_time))
            # first determine the position with the larges xcorr
            for iCh, trace in enumerate(V_timedomain):
                analytic_trace_fft = np.sum(efield_antenna_factor[iCh] * np.array([analytic_pulse_theta, analytic_pulse_phi]), axis=0)
                analytic_traces[iCh] = fft.freq2time(analytic_trace_fft, sampling_rate)

                argmax = np.argmax(np.abs(trace))
                imin = np.int(argmax - 30 * sampling_rate)
                imax = np.int(argmax + 50 * sampling_rate)

                tmp = np.sum(np.abs(trace[imin:imax] - np.roll(analytic_traces[iCh], pos)[imin:imax]) / noise_RMS)
                chi2 += tmp ** 2
                if(debug_obj):
                    ax[iCh][0].plot(trace, label='measurement')
                    ax[iCh][0].plot(np.roll(analytic_traces[iCh], pos), '--', label='fit')
                    ax[iCh][1].plot(trace - np.roll(analytic_traces[iCh], pos), label='delta')
                    ax[iCh][1].set_xlim(imin, imax)
            logger.debug("amp phi = {:.4g}, amp theta = {:.4g} , chi2 = {:.2g}".format(ampPhi, ampTheta, chi2))
            if(debug_obj):
                fig.suptitle("amp phi = {:.4g}, amp theta = {:.4g} , chi2 = {:.2g}".format(ampPhi, ampTheta, chi2))
                fig.tight_layout()
                plt.show()
            return chi2

        def obj_amplitude_slope(params, phase, pos, compare='hilbert', debug_obj=0):
            ampPhi, ampTheta, slope = params
            analytic_pulse_theta = pulse.get_analytic_pulse_freq(ampTheta, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
            analytic_pulse_phi = pulse.get_analytic_pulse_freq(ampPhi, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
            chi2 = 0
            if(debug_obj and self.i_slope_fit_iterations % 25 == 0):
                fig, ax = plt.subplots(4, 2, sharex=False, figsize=(20, 10))

            n_channels = len(V_timedomain)
            analytic_traces = np.zeros((n_channels, n_samples_time))
            # first determine the position with the larges xcorr
            channel_max = 0
            for trace in V_timedomain:
                if np.max(np.abs(trace)) > channel_max:
                    channel_max = np.max(np.abs(trace))
                    argmax = np.argmax(np.abs(trace))
                    imin = np.int(max(argmax - 50 * sampling_rate, 0))
                    imax = np.int(argmax + 50 * sampling_rate)
            for iCh, trace in enumerate(V_timedomain):
                analytic_trace_fft = np.sum(efield_antenna_factor[iCh] * np.array([analytic_pulse_theta, analytic_pulse_phi]), axis=0)
                analytic_traces[iCh] = fft.freq2time(analytic_trace_fft, sampling_rate)
                if compare == 'trace':
                    tmp = np.sum(np.abs(trace[imin:imax] - np.roll(analytic_traces[iCh], pos)[imin:imax]) ** 2) / noise_RMS ** 2
                elif compare == 'abs':
                    tmp = np.sum(np.abs(np.abs(trace[imin:imax]) - np.abs(np.roll(analytic_traces[iCh], pos)[imin:imax])) ** 2) / noise_RMS ** 2
                elif compare == 'hilbert':
                    tmp = np.sum(np.abs(np.abs(signal.hilbert(trace[imin:imax])) - np.abs(signal.hilbert(np.roll(analytic_traces[iCh], pos)[imin:imax]))) ** 2) / noise_RMS ** 2
                else:
                    raise NameError('Unsupported value for parameter "compare": {}. Value must be "trace", "abs" or "hilbert".'.format(compare))
                chi2 += tmp
                if(debug_obj and self.i_slope_fit_iterations % 25 == 0):
                    ax[iCh][1].plot(np.array(trace) / units.mV, label='measurement', color='blue')
                    ax[iCh][1].plot(np.roll(analytic_traces[iCh], pos) / units.mV, '--', label='fit', color='orange')
                    ax[iCh][1].plot(np.abs(signal.hilbert(trace)) / units.mV, linestyle=':', color='blue')
                    ax[iCh][1].plot(np.abs(signal.hilbert(np.roll(analytic_traces[iCh], pos))) / units.mV, ':', label='fit', color='orange')
                    # ax[iCh][1].plot(trace - np.roll(analytic_traces[iCh], pos), label='delta')
                    ax[iCh][0].plot(np.abs(fft.time2freq(trace, sampling_rate)) / units.mV, label='measurement')
                    ax[iCh][0].plot(np.abs(fft.time2freq(np.roll(analytic_traces[iCh], pos), sampling_rate)) / units.mV, '--', label='fit')
                    ax[iCh][0].set_xlim([0, 600])
                    ax[iCh][1].set_xlim([imin - 500, imax + 500])
                    ax[iCh][1].axvline(imin, linestyle='--', alpha=.8)
                    ax[iCh][1].axvline(imax, linestyle='--', alpha=.8)
            logger.debug("amp phi = {:.4g}, amp theta = {:.4g}, slope = {:.4g} chi2 = {:.8g}".format(ampPhi, ampTheta, slope, chi2))
            if(debug_obj and self.i_slope_fit_iterations % 25 == 0):
                fig.tight_layout()
                plt.show()
                self.i_slope_fit_iterations = 0
            self.i_slope_fit_iterations += 1
            return chi2

        def obj_amplitude_second_order(params, slope, phase, pos, compare='hilbert', debug_obj=0):
            ampPhi, ampTheta, second_order = params
            analytic_pulse_theta = pulse.get_analytic_pulse_freq(ampTheta, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass, quadratic_term=second_order, quadratic_term_offset=bandpass[0])
            analytic_pulse_phi = pulse.get_analytic_pulse_freq(ampPhi, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass, quadratic_term=second_order, quadratic_term_offset=bandpass[0])
            chi2 = 0
            if(debug_obj and self.i_slope_fit_iterations % 50 == 0):
                fig, ax = plt.subplots(5, 2, sharex=False, figsize=(20, 10))

            n_channels = len(V_timedomain)
            analytic_traces = np.zeros((n_channels, n_samples_time))
            # first determine the position with the larges xcorr
            channel_max = 0
            for trace in V_timedomain:
                if np.max(np.abs(trace)) > channel_max:
                    channel_max = np.max(np.abs(trace))
                    argmax = np.argmax(np.abs(trace))
                    imin = np.int(max(argmax - 50 * sampling_rate, 0))
                    imax = np.int(argmax + 50 * sampling_rate)
            for iCh, trace in enumerate(V_timedomain):
                analytic_trace_fft = np.sum(efield_antenna_factor[iCh] * np.array([analytic_pulse_theta, analytic_pulse_phi]), axis=0)
                analytic_traces[iCh] = fft.freq2time(analytic_trace_fft, sampling_rate)
                if compare == 'trace':
                    tmp = np.sum(np.abs(trace[imin:imax] - np.roll(analytic_traces[iCh], pos)[imin:imax]) ** 2) / noise_RMS ** 2
                elif compare == 'abs':
                    tmp = np.sum(np.abs(np.abs(trace[imin:imax]) - np.abs(np.roll(analytic_traces[iCh], pos)[imin:imax])) ** 2) / noise_RMS ** 2
                elif compare == 'hilbert':
                    tmp = np.sum(np.abs(np.abs(signal.hilbert(trace[imin:imax])) - np.abs(signal.hilbert(np.roll(analytic_traces[iCh], pos)[imin:imax]))) ** 2) / noise_RMS ** 2
                else:
                    raise NameError('Unsupported value for parameter "compare": {}. Value must be "trace", "abs" or "hilbert".'.format(compare))
                chi2 += tmp
                if(debug_obj and self.i_slope_fit_iterations % 50 == 0):
                    ax[iCh][1].plot(np.array(trace) / units.mV, label='measurement', color='blue')
                    ax[iCh][1].plot(np.roll(analytic_traces[iCh], pos) / units.mV, '--', label='fit', color='orange')
                    ax[iCh][1].plot(np.abs(signal.hilbert(trace)) / units.mV, linestyle=':', color='blue')
                    ax[iCh][1].plot(np.abs(signal.hilbert(np.roll(analytic_traces[iCh], pos))) / units.mV, ':', label='fit', color='orange')
                    ax[iCh][0].plot(np.abs(fft.time2freq(trace, sampling_rate)) / units.mV, label='measurement')
                    ax[iCh][0].plot(np.abs(fft.time2freq(np.roll(analytic_traces[iCh], pos), sampling_rate)) / units.mV, '--', label='fit')
                    ax[iCh][0].set_xlim([0, 600])
                    ax[iCh][1].set_xlim([imin - 500, imax + 500])
                    ax[iCh][1].axvline(imin, linestyle='--', alpha=.8)
                    ax[iCh][1].axvline(imax, linestyle='--', alpha=.8)
            if(debug_obj and self.i_slope_fit_iterations % 50 == 0):
                sim_channel = station.get_sim_station().get_channel(0)[0]
                ax[4][0].plot(sim_channel.get_frequencies() / units.MHz, np.abs(pulse.get_analytic_pulse_freq(ampTheta, slope, phase, len(sim_channel.get_times()), sim_channel.get_sampling_rate(), bandpass=bandpass, quadratic_term=second_order)), '--', color='orange')
                ax[4][0].plot(sim_channel.get_frequencies() / units.MHz, np.abs(station.get_sim_station().get_channel(0)[0].get_frequency_spectrum()[1]), color='blue')
                ax[4][1].plot(sim_channel.get_frequencies() / units.MHz, np.abs(pulse.get_analytic_pulse_freq(ampPhi, slope, phase, len(sim_channel.get_times()), sim_channel.get_sampling_rate(), bandpass=bandpass, quadratic_term=second_order)), '--', color='orange')
                ax[4][1].plot(sim_channel.get_frequencies() / units.MHz, np.abs(sim_channel.get_frequency_spectrum()[2]), color='blue')
                ax[4][0].set_xlim([20, 500])
                ax[4][1].set_xlim([20, 500])
            logger.debug("amp phi = {:.4g}, amp theta = {:.4g}, slope = {:.4g} chi2 = {:.8g}".format(ampPhi, ampTheta, slope, chi2))
            if(debug_obj and self.i_slope_fit_iterations % 50 == 0):
                fig.tight_layout()
                plt.show()
                self.i_slope_fit_iterations = 0
            self.i_slope_fit_iterations += 1
            return chi2

        method = "Nelder-Mead"
        options = {'maxiter': 1000,
                   'disp': True}

        res = opt.minimize(obj_xcorr, x0=[-1], method=method, options=options)
        logger.info("slope xcorr fit, slope = {:.3g} with fmin = {:.3f}".format(res.x[0], res.fun))
        # plot objective function
        phase = 0
        ratio = 0
        slope = res.x[0]
        if slope > 0 or slope < -50:  # sanity check
            slope = -1.9
        analytic_pulse_theta_freq = pulse.get_analytic_pulse_freq(ratio, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
        analytic_pulse_phi_freq = pulse.get_analytic_pulse_freq(1 - ratio, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)

        n_channels = len(V_timedomain)
        analytic_traces = np.zeros((n_channels, n_samples_time))
        positions = np.zeros(n_channels, dtype=np.int)
        max_xcorrs = np.zeros(n_channels)
        for iCh, trace in enumerate(V_timedomain):
            analytic_trace_fft = np.sum(efield_antenna_factor[iCh] * np.array([analytic_pulse_theta_freq, analytic_pulse_phi_freq]), axis=0)
            analytic_traces[iCh] = fft.freq2time(analytic_trace_fft, sampling_rate)
            xcorr = np.abs(hp.get_normalized_xcorr(trace, analytic_traces[iCh]))
            positions[iCh] = np.argmax(np.abs(xcorr)) + 1
            max_xcorrs[iCh] = xcorr.max()
        pos = positions[np.argmax(max_xcorrs)]
        for iCh, trace in enumerate(V_timedomain):
            analytic_traces[iCh] = np.roll(analytic_traces[iCh], pos)

        res_amp = opt.minimize(obj_amplitude, x0=[1.], args=(slope, phase, pos, 0), method=method, options=options)
        logger.info("amplitude fit, Aphi = {:.3g} with fmin = {:.5e}".format(res_amp.x[0], res_amp.fun))
        res_amp = opt.minimize(obj_amplitude, x0=[res_amp.x[0], 0], args=(slope, phase, pos, 0), method=method, options=options)
        logger.info("amplitude fit, Aphi = {:.3g} Atheta = {:.3g} with fmin = {:.5e}".format(res_amp.x[0], res_amp.x[1], res_amp.fun))
        # counts number of iterations in the slope fit. Used so we do not need to show the plots every iteration
        self.i_slope_fit_iterations = 0
        res_amp_slope = opt.minimize(obj_amplitude_slope, x0=[res_amp.x[0], res_amp.x[1], slope], args=(phase, pos, 'hilbert', False),
                                     method=method, options=options)

        # calculate uncertainties
        def Wrapper(params):
            return obj_amplitude_slope(params, phase, pos, 0)

        try:
            cov = covariance(Wrapper, res_amp_slope.x, 0.5, fast=True)
        except:
            cov = np.zeros((3, 3))
        logger.info("slope fit, Aphi = {:.3g}+-{:.3g} Atheta = {:.3g}+-{:.3g}, slope = {:.3g}+-{:.3g} with fmin = {:.5e}".format(
            res_amp_slope.x[0],
            cov[0, 0] ** 0.5,
            res_amp_slope.x[1], cov[1, 1] ** 0.5,
            res_amp_slope.x[2], cov[2, 2] ** 0.5,
            res_amp_slope.fun)
        )
        logger.info("covariance matrix \n{}".format(cov))
        if(cov[0, 0] > 0 and cov[1, 1] > 0 and cov[2, 2] > 0):
            logger.info("correlation matrix \n{}".format(hp.covariance_to_correlation(cov)))
        Aphi = res_amp_slope.x[0]
        Atheta = res_amp_slope.x[1]
        slope = res_amp_slope.x[2]
        Aphi_error = cov[0, 0] ** 0.5
        Atheta_error = cov[1, 1] ** 0.5

        # plot objective function
        if 0:
            fo, ao = plt.subplots(1, 1)
            ss = np.linspace(-6, -0, 100)
            oos = [obj_amplitude_slope([res_amp_slope.x[0], res_amp_slope.x[1], s], phase, pos) for s in ss]
            ao.plot(ss, oos)

            n = 10
            x = np.linspace(res_amp_slope.x[0] * 0.6, res_amp_slope.x[0] * 1.4, n)
            y = np.linspace(-5, -1, n)
            X, Y = np.meshgrid(x, y)
            Z = np.zeros((n, n))
            for i in range(n):
                for j in range(n):
                    Z[i, j] = obj_amplitude_slope([X[i, j], X[i, j] * res_amp_slope.x[1] / res_amp_slope.x[0], Y[i, j]], phase, pos)

            fig, ax = plt.subplots(1, 1)
            ax.pcolor(X, Y, Z, cmap='viridis_r', vmin=res_amp_slope.fun, vmax=res_amp_slope.fun * 2)

        analytic_pulse_theta = pulse.get_analytic_pulse(Atheta, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
        analytic_pulse_phi = pulse.get_analytic_pulse(Aphi, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
        analytic_pulse_theta_freq = pulse.get_analytic_pulse_freq(Atheta, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)
        analytic_pulse_phi_freq = pulse.get_analytic_pulse_freq(Aphi, slope, phase, n_samples_time, sampling_rate, bandpass=bandpass)

        analytic_pulse_theta = np.roll(analytic_pulse_theta, pos)
        analytic_pulse_phi = np.roll(analytic_pulse_phi, pos)
        station_trace = np.array([np.zeros_like(analytic_pulse_theta), analytic_pulse_theta, analytic_pulse_phi])

        electric_field = NuRadioReco.framework.electric_field.ElectricField(use_channels)
        electric_field.set_trace(station_trace, sampling_rate)
        energy_fluence = trace_utilities.get_electric_field_energy_fluence(electric_field.get_trace(), electric_field.get_times())
        electric_field.set_parameter(efp.signal_energy_fluence, energy_fluence)
        electric_field.set_parameter_error(efp.signal_energy_fluence, np.array([0, Atheta_error, Aphi_error]))
        electric_field.set_parameter(efp.cr_spectrum_slope, slope)
        electric_field.set_parameter(efp.zenith, zenith)
        electric_field.set_parameter(efp.azimuth, azimuth)
        # calculate high level parameters
        x = np.sign(Atheta) * np.abs(Atheta) ** 0.5
        y = np.sign(Aphi) * np.abs(Aphi) ** 0.5
        sx = Atheta_error * 0.5
        sy = Aphi_error * 0.5
        pol_angle = np.arctan2(abs(y), abs(x))
        pol_angle_error = 1. / (x ** 2 + y ** 2) * (y ** 2 * sx ** 2 + x ** 2 + sy ** 2) ** 0.5  # gaussian error propagation
        logger.info("polarization angle = {:.1f} +- {:.1f}".format(pol_angle / units.deg, pol_angle_error / units.deg))
        electric_field.set_parameter(efp.polarization_angle, pol_angle)
        electric_field.set_parameter_error(efp.polarization_angle, pol_angle_error)

        # compute expeted polarization
        site = det.get_site(station.get_id())
        exp_efield = hp.get_lorentzforce_vector(zenith, azimuth, hp.get_magnetic_field_vector(site))
        cs = coordinatesystems.cstrafo(zenith, azimuth, site=site)
        exp_efield_onsky = cs.transform_from_ground_to_onsky(exp_efield)
        exp_pol_angle = np.arctan2(exp_efield_onsky[2], exp_efield_onsky[1])
        logger.info("expected polarization angle = {:.1f}".format(exp_pol_angle / units.deg))
        electric_field.set_parameter(efp.polarization_angle_expectation, exp_pol_angle)
        res_amp_second_order = opt.minimize(
            obj_amplitude_second_order,
            x0=[res_amp_slope.x[0], res_amp_slope.x[1], 0],
            args=(slope, phase, pos, 'hilbert', False),
            method=method,
            options=options
        )
        second_order_correction = res_amp_second_order.x[2]
        electric_field.set_parameter(efp.cr_spectrum_quadratic_term, second_order_correction)
        # figure out the timing of the electric field
        voltages_from_efield = trace_utilities.get_channel_voltage_from_efield(station, electric_field, use_channels, det, zenith, azimuth, self.antenna_provider, False)
        correlation = np.zeros(voltages_from_efield.shape[1] + station.get_channel(use_channels[0]).get_trace().shape[0] - 1)
        channel_trace_start_times = []
        for channel_id in use_channels:
            channel_trace_start_times.append(station.get_channel(channel_id).get_trace_start_time())
        average_trace_start_time = np.average(channel_trace_start_times)
        for i_trace, v_trace in enumerate(voltages_from_efield):
            channel = station.get_channel(use_channels[i_trace])
            time_shift = geo_utl.get_time_delay_from_direction(zenith, azimuth, det.get_relative_position(station.get_id(), use_channels[i_trace])) - (channel.get_trace_start_time() - average_trace_start_time)
            voltage_trace = np.roll(np.copy(v_trace), int(time_shift * electric_field.get_sampling_rate()))
            correlation += signal.correlate(voltage_trace, channel.get_trace())
        toffset = (np.arange(0, correlation.shape[0]) - channel.get_trace().shape[0]) / electric_field.get_sampling_rate()
        electric_field.set_trace_start_time(-toffset[np.argmax(correlation)] + average_trace_start_time)
        station.add_electric_field(electric_field)

        if debug:
            analytic_traces = np.zeros((n_channels, n_samples_time))
            for iCh, trace in enumerate(V_timedomain):
                analytic_trace_fft = np.sum(efield_antenna_factor[iCh] * np.array([analytic_pulse_theta_freq, analytic_pulse_phi_freq]), axis=0)
                analytic_traces[iCh] = fft.freq2time(analytic_trace_fft, electric_field.get_sampling_rate())
                analytic_traces[iCh] = np.roll(analytic_traces[iCh], pos)
            fig, (ax2, ax2f) = plt.subplots(2, 1, figsize=(10, 8))
            lw = 2

            times = station.get_times() / units.ns
            ax2.plot(times, station.get_trace()[1] / units.mV * units.m, "-C0", label="analytic eTheta", lw=lw)
            ax2.plot(times, station.get_trace()[2] / units.mV * units.m, "-C1", label="analytic ePhi", lw=lw)
            tmax = times[np.argmax(station.get_trace()[2])]
            ax2.set_xlim(tmax - 40, tmax + 50)

            ff = station.get_frequencies() / units.MHz
            df = ff[1] - ff[0]
            ax2f.plot(ff[ff < 600], np.abs(station.get_frequency_spectrum()[1][ff < 600]) / df / units.mV * units.m, "-C0", label="analytic eTheta", lw=lw)
            ax2f.plot(ff[ff < 600], np.abs(station.get_frequency_spectrum()[2][ff < 600]) / df / units.mV * units.m, "-C1", label="analytic ePhi", lw=lw)

            if station.has_sim_station():
                sim_station = station.get_sim_station()
                logger.debug("station start time {:.1f}ns, relativ sim station time = {:.1f}".format(station.get_trace_start_time(), sim_station.get_trace_start_time()))
                df = (sim_station.get_frequencies()[1] - sim_station.get_frequencies()[0]) / units.MHz
                c = 1.
                ffsim = sim_station.get_frequencies()
                mask = (ffsim > 100 * units.MHz) & (ffsim < 500 * units.MHz)
                result = poly.polyfit(ffsim[mask], np.log10(np.abs(sim_station.get_frequency_spectrum()[2][mask]) / df / units.mV * units.m), 1, full=True)
                logger.info("polyfit result = {:.2g}  {:.2g}".format(*result[0]))
                ax2.plot(sim_station.get_times() / units.ns, sim_station.get_trace()[1] / units.mV * units.m * c, "--C0", label="simulation eTheta", lw=lw)
                ax2.plot(sim_station.get_times() / units.ns, sim_station.get_trace()[2] / units.mV * units.m * c, "--C1", label="simulation ePhi", lw=lw)
                ax2f.plot(sim_station.get_frequencies() / units.MHz, np.abs(sim_station.get_frequency_spectrum()[1]) / df / units.mV * units.m * c, "--C0", label="simulation eTheta", lw=lw)
                ax2f.plot(sim_station.get_frequencies() / units.MHz, np.abs(sim_station.get_frequency_spectrum()[2]) / df / units.mV * units.m * c, "--C1", label="simulation ePhi", lw=lw)

                ax2f.plot(ffsim / units.MHz, 10 ** (result[0][0] + result[0][1] * ffsim), "C3:")

            ax2.legend(fontsize="xx-small")
            ax2.set_xlabel("time [ns]")
            ax2.set_ylabel("electric-field [mV/m]")
            ax2f.set_ylim(1e-3, 5)
            ax2f.set_xlabel("Frequency [MHz]")
            ax2f.set_xlim(100, 500)
            ax2f.semilogy(True)
            if sim_present:
                sim = station.get_sim_station()
                fig.suptitle("Simulation: Zenith {:.1f}, Azimuth {:.1f}".format(np.rad2deg(sim[stnp.zenith]), np.rad2deg(sim[stnp.azimuth])))
            else:
                fig.suptitle("Data: reconstructed zenith {:.1f}, azimuth {:.1f}".format(np.rad2deg(zenith), np.rad2deg(azimuth)))
            fig.tight_layout()
            fig.subplots_adjust(top=0.95)
            if(debug_plotpath is not None):
                fig.savefig(os.path.join(debug_plotpath, 'run_{:05d}_event_{:06d}_efield.png'.format(evt.get_run_number(), evt.get_id())))
                plt.close(fig)

            # plot antenna response and channels
            fig, ax = plt.subplots(len(V), 3, sharex='col', sharey='col')
            for iCh in range(len(V)):
                mask = ff > 100
                ax[iCh, 0].plot(ff[mask], np.abs(efield_antenna_factor[iCh][0])[mask], label="theta, channel {}".format(use_channels[iCh]), lw=lw)
                ax[iCh, 0].plot(ff[mask], np.abs(efield_antenna_factor[iCh][1])[mask], label="phi, channel {}".format(use_channels[iCh]), lw=lw)
                ax[iCh, 0].legend(fontsize='xx-small')
                ax[iCh, 0].set_xlim(100, 500)
                ax[iCh, 1].set_xlim(400, 600)
                ax[iCh, 2].set_xlim(400, 600)
                ax[iCh, 1].plot(times, V_timedomain[iCh] / units.micro / units.V, lw=lw)
                ax[iCh, 1].plot(times, analytic_traces[iCh] / units.micro / units.V, '--', lw=lw)
                ax[iCh, 2].plot(times, (V_timedomain[iCh] - analytic_traces[iCh]) / units.micro / units.V, '-', lw=lw)
                ax[iCh, 0].set_ylabel("H [m]")
                ax[iCh, 1].set_ylabel(r"V [$\mu$V]")
                ax[iCh, 2].set_ylabel(r"$\Delta$V [$\mu$V]")
                RMS = det.get_noise_RMS(station.get_id(), 0)
                ax[iCh, 1].text(0.6, 0.8, 'S/N={:.1f}'.format(np.max(np.abs(V_timedomain[iCh])) / RMS), transform=ax[iCh, 1].transAxes)
            ax[0][2].set_ylim(ax[0][1].get_ylim())
            ax[-1, 1].set_xlabel("time [ns]")
            ax[-1, 2].set_xlabel("time [ns]")
            ax[-1, 0].set_xlabel("frequency [MHz]")
            fig.tight_layout()
            if(debug_plotpath is not None):
                fig.savefig(os.path.join(debug_plotpath, 'run_{:05d}_event_{:06d}_channels.png'.format(evt.get_run_number(), evt.get_id())))
                plt.close(fig)
    def run(self, evt, station, det, debug=False):
        """
        reconstructs quantities for electric field

        Parameters
        ----------
        evt: event

        station: station

        det: detector

        debug: bool
            set debug

        """
        for electric_field in station.get_electric_fields():
            trace_copy = copy.copy(electric_field.get_trace())

            # calculate hilbert envelope
            envelope = np.abs(signal.hilbert(trace_copy))
            envelope_mag = np.linalg.norm(envelope, axis=0)
            signal_time_bin = np.argmax(envelope_mag)
            signal_time = electric_field.get_times()[signal_time_bin]
            electric_field[efp.signal_time] = signal_time

    #
            low_pos = np.int(130 * units.ns * electric_field.get_sampling_rate())
            up_pos = np.int(210 * units.ns * electric_field.get_sampling_rate())
            if(debug):
                fig, ax = plt.subplots(1, 1)
                sc = ax.scatter(trace_copy[1, low_pos:up_pos], trace_copy[2, low_pos:up_pos], c=electric_field.get_times()[low_pos:up_pos], s=5)
                fig.colorbar(sc, ax=ax)
                ax.set_aspect('equal')
                ax.set_xlabel("eTheta")
                ax.set_ylabel("ePhi")
                fig.tight_layout()

            low_pos, up_pos = hp.get_interval(envelope_mag, scale=0.5)
            v_start = trace_copy[:, signal_time_bin]
            v_avg = np.zeros(3)
            for i in range(low_pos, up_pos + 1):
                v = trace_copy[:, i]
                alpha = hp.get_angle(v_start, v)
                if(alpha > 0.5 * np.pi):
                    v *= -1
                v_avg += v
            pole = np.arctan2(np.abs(v_avg[2]), np.abs(v_avg[1]))
            electric_field[efp.polarization_angle] = pole
            logger.info("average e-field vector = {:.4g}, {:.4g}, {:.4g} -> polarization = {:.1f}deg".format(v_avg[0], v_avg[1], v_avg[2], pole / units.deg))
            trace = electric_field.get_trace()

            if(debug):
                fig, ax = plt.subplots(1, 1)
                tt = electric_field.get_times()
                dt = 1. / electric_field.get_sampling_rate()
                ax.plot(tt / units.ns, trace[1] / units.mV * units.m)
                ax.plot(tt / units.ns, trace[2] / units.mV * units.m)
                ax.plot(tt / units.ns, envelope_mag / units.mV * units.m)
                ax.vlines([low_pos * dt, up_pos * dt], 0, envelope_mag.max() / units.mV * units.m)
                ax.vlines([signal_time - self.__signal_window_pre, signal_time + self.__signal_window_post], 0, envelope_mag.max() / units.mV * units.m, linestyles='dashed')

            times = electric_field.get_times()
            mask_signal_window = (times > (signal_time - self.__signal_window_pre)) & (times < (signal_time + self.__signal_window_post))
            mask_noise_window = np.zeros_like(mask_signal_window, dtype=np.bool)
            if(self.__noise_window > 0):
                mask_noise_window[np.int(np.round((-self.__noise_window - 141.) * electric_field.get_sampling_rate())):np.int(np.round(-141. * electric_field.get_sampling_rate()))] = np.ones(np.int(np.round(self.__noise_window * electric_field.get_sampling_rate())), dtype=np.bool)  # the last n bins

            signal_energy_fluence = trace_utilities.get_electric_field_energy_fluence(trace, times, mask_signal_window, mask_noise_window)
            dt = times[1] - times[0]
            signal_energy_fluence_error = np.zeros(3)
            if(np.sum(mask_noise_window)):
                RMSNoise = np.sqrt(np.mean(trace[:, mask_noise_window] ** 2, axis=1))
                signal_energy_fluence_error = (4 * np.abs(signal_energy_fluence / self.__conversion_factor_integrated_signal) * RMSNoise ** 2 * dt + 2 * (self.__signal_window_pre + self.__signal_window_post) * RMSNoise ** 4 * dt) ** 0.5
            signal_energy_fluence_error *= self.__conversion_factor_integrated_signal
            electric_field.set_parameter(efp.signal_energy_fluence, signal_energy_fluence)
            electric_field.set_parameter_error(efp.signal_energy_fluence, signal_energy_fluence_error)

            logger.info("f = {} +- {}".format(signal_energy_fluence / units.eV * units.m2, signal_energy_fluence_error / units.eV * units.m2))

            # calculate polarization angle from energy fluence
            x = np.abs(signal_energy_fluence[1]) ** 0.5
            y = np.abs(signal_energy_fluence[2]) ** 0.5
            sx = signal_energy_fluence_error[1] * 0.5
            sy = signal_energy_fluence_error[2] * 0.5
            pol_angle = np.arctan2(y, x)
            pol_angle_error = 1. / (x ** 2 + y ** 2) * (y ** 2 * sx ** 2 + x ** 2 * sy ** 2) ** 0.5  # gaussian error propagation
            logger.info("polarization angle = {:.1f} +- {:.1f}".format(pol_angle / units.deg, pol_angle_error / units.deg))
            electric_field.set_parameter(efp.polarization_angle, pol_angle)
            electric_field.set_parameter_error(efp.polarization_angle, pol_angle_error)

            # compute expeted polarization
            site = det.get_site(station.get_id())
            exp_efield = hp.get_lorentzforce_vector(electric_field[efp.zenith], electric_field[efp.azimuth], hp.get_magnetic_field_vector(site))
            cs = coordinatesystems.cstrafo(electric_field[efp.zenith], electric_field[efp.azimuth], site=site)
            exp_efield_onsky = cs.transform_from_ground_to_onsky(exp_efield)
            exp_pol_angle = np.arctan2(np.abs(exp_efield_onsky[2]), np.abs(exp_efield_onsky[1]))
            logger.info("expected polarization angle = {:.1f}".format(exp_pol_angle / units.deg))
            electric_field.set_parameter(efp.polarization_angle_expectation, exp_pol_angle)