def ll_regular_station_fft(angles, corr_02_fft, corr_13_fft, sampling_rate, positions, trace_start_times): """ Likelihood function for a four antenna ARIANNA station, using FFT convolution Using FFT convolution, has built-in wrap around, but ARIANNA signals are too short for it to be accurate will show problems at zero time delay """ zenith = angles[0] azimuth = angles[1] times = [] for pos in positions: tmp = [geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[0], n=n_index) * sampling_rate, geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[1], n=n_index) * sampling_rate] times.append(tmp) delta_t_02 = (times[0][1] + trace_start_times[0][1] * sampling_rate) - (times[0][0] + trace_start_times[0][0] * sampling_rate) delta_t_13 = (times[1][1] + trace_start_times[1][1] * sampling_rate) - (times[1][0] + trace_start_times[1][0] * sampling_rate) if delta_t_02 < 0: pos_02 = int(delta_t_02 + corr_02_fft.shape[0]) else: pos_02 = int(delta_t_02) if delta_t_13 < 0: pos_13 = int(delta_t_13 + corr_13_fft.shape[0]) else: pos_13 = int(delta_t_13) weight_02 = np.sum(np.abs(corr_02_fft)) # Normalize crosscorrelation weight_13 = np.sum(np.abs(corr_13_fft)) likelihood = -1 * (np.abs(corr_02_fft[pos_02]) ** 2 / weight_02 + np.abs(corr_13[pos_13]) ** 2 / weight_13) return likelihood
def ll_regular_station(angles, corr_02, corr_13, sampling_rate, positions, trace_start_times): """ Likelihood function for a four antenna ARIANNA station, using correction. Using correlation, has no built in wrap around, pulse needs to be in the middle """ zenith = angles[0] azimuth = angles[1] times = [] for pos in positions: tmp = [geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[0], n=n_index), geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[1], n=n_index)] times.append(tmp) delta_t_02 = times[0][1] - times[0][0] delta_t_13 = times[1][1] - times[1][0] # take different trace start times into account delta_t_02 -= (trace_start_times[0][1] - trace_start_times[0][0]) delta_t_13 -= (trace_start_times[1][1] - trace_start_times[1][0]) delta_t_02 *= sampling_rate delta_t_13 *= sampling_rate pos_02 = int(corr_02.shape[0] / 2 - delta_t_02) pos_13 = int(corr_13.shape[0] / 2 - delta_t_13) # weight_02 = np.sum(corr_02 ** 2) # Normalize crosscorrelation # weight_13 = np.sum(corr_13 ** 2) # # likelihood = -1 * (corr_02[pos_02] ** 2 / weight_02 + corr_13[pos_13] ** 2 / weight_13) # After deliberating a bit, I don't think we should use the square because anti-correlating # pulses would be wrong, given that it is not a continous waveform weight_02 = np.sum(np.abs(corr_02)) # Normalize crosscorrelation weight_13 = np.sum(np.abs(corr_13)) likelihood = -1 * (corr_02[pos_02] / weight_02 + corr_13[pos_13] / weight_13) return likelihood
sim_station, det, passband, filter_type='butter', order=10) eventTypeIdentifier.run(evt, station, "forced", 'cosmic_ray') # The time differences between channels have to be stored in the channels # We can just calculate it from the CR direction. for channel in station.iter_channels(): channel.set_parameter(chp.signal_receiving_zenith, sim_station.get_parameter(stnp.zenith)) channel.set_parameter(chp.signal_receiving_azimuth, sim_station.get_parameter(stnp.azimuth)) time_offset = geometryUtilities.get_time_delay_from_direction( sim_station.get_parameter(stnp.zenith), sim_station.get_parameter(stnp.azimuth), det.get_relative_position(station.get_id(), channel.get_id()), 1.) channel.set_parameter(chp.signal_time_offset, time_offset) ift_efield_reconstructor.run(evt, station, det, [0, 1, 2, 3], False, use_sim=False) channelResampler.run(evt, station, det, sampling_rate=1 * units.GHz) electricFieldResampler.run(evt, station, det,
def run(self, evt, station, det, n_index=None, ZenLim=None, AziLim=None, channel_pairs=((0, 2), (1, 3)), use_envelope=False): """ reconstruct signal arrival direction for all events Parameters ---------- evt: Event The event to run the module on station: Station The station to run the module on det: Detector The detector description n_index: float the index of refraction ZenLim: 2-dim array/list of floats (default: [0 * units.deg, 90 * units.deg]) the zenith angle limits for the fit AziLim: 2-dim array/list of floats (default: [0 * units.deg, 360 * units.deg]) the azimuth angle limits for the fit channel_pairs: pair of pair of integers specify the two channel pairs to use, default ((0, 2), (1, 3)) use_envelope: bool (default False) if True, the hilbert envelope of the traces is used """ if ZenLim is None: ZenLim = [0 * units.deg, 90 * units.deg] if AziLim is None: AziLim = [0 * units.deg, 360 * units.deg] use_correlation = True def ll_regular_station(angles, corr_02, corr_13, sampling_rate, positions, trace_start_times): """ Likelihood function for a four antenna ARIANNA station, using correction. Using correlation, has no built in wrap around, pulse needs to be in the middle """ zenith = angles[0] azimuth = angles[1] times = [] for pos in positions: tmp = [geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[0], n=n_index), geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[1], n=n_index)] times.append(tmp) delta_t_02 = times[0][1] - times[0][0] delta_t_13 = times[1][1] - times[1][0] # take different trace start times into account delta_t_02 -= (trace_start_times[0][1] - trace_start_times[0][0]) delta_t_13 -= (trace_start_times[1][1] - trace_start_times[1][0]) delta_t_02 *= sampling_rate delta_t_13 *= sampling_rate pos_02 = int(corr_02.shape[0] / 2 - delta_t_02) pos_13 = int(corr_13.shape[0] / 2 - delta_t_13) # weight_02 = np.sum(corr_02 ** 2) # Normalize crosscorrelation # weight_13 = np.sum(corr_13 ** 2) # # likelihood = -1 * (corr_02[pos_02] ** 2 / weight_02 + corr_13[pos_13] ** 2 / weight_13) # After deliberating a bit, I don't think we should use the square because anti-correlating # pulses would be wrong, given that it is not a continous waveform weight_02 = np.sum(np.abs(corr_02)) # Normalize crosscorrelation weight_13 = np.sum(np.abs(corr_13)) likelihood = -1 * (corr_02[pos_02] / weight_02 + corr_13[pos_13] / weight_13) return likelihood def ll_regular_station_fft(angles, corr_02_fft, corr_13_fft, sampling_rate, positions, trace_start_times): """ Likelihood function for a four antenna ARIANNA station, using FFT convolution Using FFT convolution, has built-in wrap around, but ARIANNA signals are too short for it to be accurate will show problems at zero time delay """ zenith = angles[0] azimuth = angles[1] times = [] for pos in positions: tmp = [geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[0], n=n_index) * sampling_rate, geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[1], n=n_index) * sampling_rate] times.append(tmp) delta_t_02 = (times[0][1] + trace_start_times[0][1] * sampling_rate) - (times[0][0] + trace_start_times[0][0] * sampling_rate) delta_t_13 = (times[1][1] + trace_start_times[1][1] * sampling_rate) - (times[1][0] + trace_start_times[1][0] * sampling_rate) if delta_t_02 < 0: pos_02 = int(delta_t_02 + corr_02_fft.shape[0]) else: pos_02 = int(delta_t_02) if delta_t_13 < 0: pos_13 = int(delta_t_13 + corr_13_fft.shape[0]) else: pos_13 = int(delta_t_13) weight_02 = np.sum(np.abs(corr_02_fft)) # Normalize crosscorrelation weight_13 = np.sum(np.abs(corr_13_fft)) likelihood = -1 * (np.abs(corr_02_fft[pos_02]) ** 2 / weight_02 + np.abs(corr_13[pos_13]) ** 2 / weight_13) return likelihood station_id = station.get_id() positions_pairs = [[det.get_relative_position(station_id, channel_pairs[0][0]), det.get_relative_position(station_id, channel_pairs[0][1])], [det.get_relative_position(station_id, channel_pairs[1][0]), det.get_relative_position(station_id, channel_pairs[1][1])]] sampling_rate = station.get_channel(0).get_sampling_rate() # assume that channels have the same sampling rate trace_start_time_pairs = [[station.get_channel(channel_pairs[0][0]).get_trace_start_time(), station.get_channel(channel_pairs[0][1]).get_trace_start_time()], [station.get_channel(channel_pairs[1][0]).get_trace_start_time(), station.get_channel(channel_pairs[1][1]).get_trace_start_time()]] # determine automatically if one channel has an inverted waveform with respect to the other signs = [1., 1.] for iPair, pair in enumerate(channel_pairs): antenna_type = det.get_antenna_type(station_id, pair[0]) if("LPDA" in antenna_type): otheta, ophi, rot_theta, rot_azimuth = det.get_antenna_orientation(station_id, pair[0]) otheta2, ophi2, rot_theta2, rot_azimuth2 = det.get_antenna_orientation(station_id, pair[1]) if(np.isclose(np.abs(rot_azimuth - rot_azimuth2), 180 * units.deg, atol=1 * units.deg)): signs[iPair] = -1 if use_correlation: # Correlation if not use_envelope: corr_02 = signal.correlate(station.get_channel(channel_pairs[0][0]).get_trace(), signs[0] * station.get_channel(channel_pairs[0][1]).get_trace()) corr_13 = signal.correlate(station.get_channel(channel_pairs[1][0]).get_trace(), signs[1] * station.get_channel(channel_pairs[1][1]).get_trace()) else: corr_02 = signal.correlate(np.abs(signal.hilbert(station.get_channel(channel_pairs[0][0]).get_trace())), np.abs(signal.hilbert(station.get_channel(channel_pairs[0][1]).get_trace()))) corr_13 = signal.correlate(np.abs(signal.hilbert(station.get_channel(channel_pairs[1][0]).get_trace())), np.abs(signal.hilbert(station.get_channel(channel_pairs[1][1]).get_trace()))) else: # FFT convolution corr_02_fft = fftpack.ifft(-1 * fftpack.fft(station.get_channel(channel_pairs[0][0]).get_trace()).conjugate() * fftpack.fft(station.get_channel(channel_pairs[0][1]).get_trace())) corr_13_fft = fftpack.ifft(-1 * fftpack.fft(station.get_channel(channel_pairs[1][0]).get_trace()).conjugate() * fftpack.fft(station.get_channel(channel_pairs[1][1]).get_trace())) if use_correlation: # Using correlation ll = opt.brute( ll_regular_station, ranges=(slice(ZenLim[0], ZenLim[1], 0.01), slice(AziLim[0], AziLim[1], 0.01)), args=(corr_02, corr_13, sampling_rate, positions_pairs, trace_start_time_pairs), full_output=True, finish=opt.fmin) # slow but does the trick else: ll = opt.brute(ll_regular_station_fft, ranges=(slice(ZenLim[0], ZenLim[1], 0.05), slice(AziLim[0], AziLim[1], 0.05)), args=(corr_02_fft, corr_13_fft, sampling_rate, positions_pairs, trace_start_time_pairs), full_output=True, finish=opt.fmin) # slow but does the trick if self.__debug: import peakutils zenith = ll[0][0] azimuth = ll[0][1] times = [] for pos in positions_pairs: tmp = [geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[0], n=n_index), geo_utl.get_time_delay_from_direction(zenith, azimuth, pos[1], n=n_index)] times.append(tmp) delta_t_02 = times[0][1] - times[0][0] delta_t_13 = times[1][1] - times[1][0] # take different trace start times into account delta_t_02 -= (trace_start_time_pairs[0][1] - trace_start_time_pairs[0][0]) delta_t_13 -= (trace_start_time_pairs[1][1] - trace_start_time_pairs[1][0]) delta_t_02 *= sampling_rate delta_t_13 *= sampling_rate toffset = -(np.arange(0, corr_02.shape[0]) - corr_02.shape[0] / 2) / sampling_rate fig, (ax, ax2) = plt.subplots(2, 1, sharex=True) ax.plot(toffset, corr_02) ax.axvline(delta_t_02 / sampling_rate, label='time', c='k') indices = peakutils.indexes(corr_02, thres=0.8, min_dist=5) ax.plot(toffset[indices], corr_02[indices], 'o') imax = np.argmax(corr_02[indices]) self.logger.debug("offset 02= {:.3f}".format(toffset[indices[imax]] - (delta_t_02 / sampling_rate))) ax2.plot(toffset, corr_13) indices = peakutils.indexes(corr_13, thres=0.8, min_dist=5) ax2.plot(toffset[indices], corr_13[indices], 'o') ax2.axvline(delta_t_13 / sampling_rate, label='time', c='k') ax2.set_xlabel("time") ax2.set_ylabel("Correlation Ch 1/ Ch3", fontsize='small') ax.set_ylabel("Correlation Ch 0/ Ch2", fontsize='small') plt.tight_layout() # plt.close("all") station[stnp.zenith] = max(ZenLim[0], min(ZenLim[1], ll[0][0])) station[stnp.azimuth] = ll[0][1] output_str = "reconstucted angles theta = {:.1f}, phi = {:.1f}".format(station[stnp.zenith] / units.deg, station[stnp.azimuth] / units.deg) if station.has_sim_station(): sim_zen = None sim_az = None if(station.get_sim_station().is_cosmic_ray()): sim_zen = station.get_sim_station()[stnp.zenith] sim_az = station.get_sim_station()[stnp.azimuth] elif(station.get_sim_station().is_neutrino()): # in case of a neutrino simulation, each channel has a slightly different arrival direction -> compute the average sim_zen = [] sim_az = [] for efield in station.get_sim_station().get_electric_fields_for_channels(ray_path_type='direct'): sim_zen.append(efield[efp.zenith]) sim_az.append(efield[efp.azimuth]) sim_zen = np.array(sim_zen) sim_az = hp.get_normalized_angle(np.array(sim_az)) ops = "average incident zenith {:.1f} +- {:.1f}".format(np.mean(sim_zen) / units.deg, np.std(sim_zen) / units.deg) ops += " (individual: " for x in sim_zen: ops += "{:.1f}, ".format(x / units.deg) ops += ")" self.logger.debug(ops) ops = "average incident azimuth {:.1f} +- {:.1f}".format(np.mean(sim_az) / units.deg, np.std(sim_az) / units.deg) ops += " (individual: " for x in sim_az: ops += "{:.1f}, ".format(x / units.deg) ops += ")" self.logger.debug(ops) sim_zen = np.mean(np.array(sim_zen)) sim_az = np.mean(np.array(sim_az)) if(sim_zen is not None): dOmega = hp.get_angle(hp.spherical_to_cartesian(sim_zen, sim_az), hp.spherical_to_cartesian(station[stnp.zenith], station[stnp.azimuth])) output_str += " MC theta = {:.2f}, phi = {:.2f}, dOmega = {:.2f}, dZen = {:.1f}, dAz = {:.1f}".format(sim_zen / units.deg, hp.get_normalized_angle(sim_az) / units.deg, dOmega / units.deg, (station[stnp.zenith] - sim_zen) / units.deg, (station[stnp.azimuth] - hp.get_normalized_angle(sim_az)) / units.deg) self.__zenith.append(sim_zen) self.__azimuth.append(sim_az) self.__delta_zenith.append(station[stnp.zenith] - sim_zen) self.__delta_azimuth.append(station[stnp.azimuth] - hp.get_normalized_angle(sim_az)) self.logger.info(output_str) # Still have to add fit quality parameter to output if self.__debug: import peakutils # access simulated efield and high level parameters sim_present = False if(station.has_sim_station()): if(station.get_sim_station().has_parameter(stnp.zenith)): sim_station = station.get_sim_station() azimuth_orig = sim_station[stnp.azimuth] zenith_orig = sim_station[stnp.zenith] sim_present = True self.logger.debug("True CoREAS zenith {0}, azimuth {1}".format(zenith_orig, azimuth_orig)) self.logger.debug("Result of direction fitting: [zenith, azimuth] {}".format(np.rad2deg(ll[0]))) # Show fit space zen = np.arange(ZenLim[0], ZenLim[1], 1 * units.deg) az = np.arange(AziLim[0], AziLim[1], 2 * units.deg) x_plot = np.zeros(zen.shape[0] * az.shape[0]) y_plot = np.zeros(zen.shape[0] * az.shape[0]) z_plot = np.zeros(zen.shape[0] * az.shape[0]) i = 0 for a in az: for z in zen: # Evaluate fit function for grid if use_correlation: z_plot[i] = ll_regular_station([z, a], corr_02, corr_13, sampling_rate, positions_pairs, trace_start_time_pairs) else: z_plot[i] = ll_regular_station_fft([z, a], corr_02_fft, corr_13_fft, sampling_rate, positions_pairs, trace_start_time_pairs) x_plot[i] = a y_plot[i] = z i += 1 fig, ax = plt.subplots(1, 1) ax.scatter(np.rad2deg(x_plot), np.rad2deg(y_plot), c=z_plot, cmap='gnuplot2_r', lw=0) # ax.imshow(z_plot, cmap='gnuplot2_r', extent=(0, 360, 90, 180)) if sim_present: ax.plot(np.rad2deg(hp.get_normalized_angle(azimuth_orig)), np.rad2deg(zenith_orig), marker='d', c='g', label="True") ax.scatter(np.rad2deg(ll[0][1]), np.rad2deg(ll[0][0]), marker='o', c='k', label='Fit') # ax.colorbar(label='Fit parameter') ax.set_ylabel('Zenith [rad]') ax.set_xlabel('Azimuth [rad]') plt.tight_layout() # plot allowed solution separately for each pair of channels toffset = -(np.arange(0, corr_02.shape[0]) - corr_02.shape[0] / 2.) / sampling_rate indices = peakutils.indexes(corr_02, thres=0.8, min_dist=5) t02s = toffset[indices][np.argsort(corr_02[indices])[::-1]] + (trace_start_time_pairs[0][1] - trace_start_time_pairs[0][0]) toffset = -(np.arange(0, corr_13.shape[0]) - corr_13.shape[0] / 2.) / sampling_rate indices = peakutils.indexes(corr_13, thres=0.8, min_dist=5) t13s = toffset[indices][np.argsort(corr_13[indices])[::-1]] + (trace_start_time_pairs[1][1] - trace_start_time_pairs[1][0]) from scipy import constants c = constants.c * units.m / units.s dx = -6 * units.m def get_deltat13(dt, phi): t = -1. * dt * c / (dx * np.cos(phi) * n_index) t[t < 0] = np.nan return np.arcsin(t) def get_deltat02(dt, phi): t = -1 * dt * c / (dx * np.sin(phi) * n_index) t[t < 0] = np.nan return np.arcsin(t) def getDeltaTCone(r, dt): dist = np.linalg.norm(r) t0 = -dist * n_index / c Phic = np.arccos(dt / t0) # cone angle for allowable solutions self.logger.debug('dist = {}, dt = {}, t0 = {}, phic = {}'.format(dist, dt, t0, Phic)) nr = r / dist # normalize p = np.cross([0, 0, 1], nr) # create a perpendicular normal vector to r p = p / np.linalg.norm(p) q = np.cross(nr, p) # nr, p, and q form an orthonormal basis self.logger.debug('nr = {}\np = {}\nq = {}\n'.format(nr, p, q)) ThetaC = np.linspace(0, 2 * np.pi, 1000) Phis = np.zeros(len(ThetaC)) Thetas = np.zeros(len(ThetaC)) for i, thetac in enumerate(ThetaC): # create a set of vectors that point along the cone defined by r and PhiC rc = nr + np.tan(Phic) * (np.sin(thetac) * p + np.cos(thetac) * q) nrc = rc / np.linalg.norm(rc) theta = np.arccos(nrc[2]) phi = np.arctan2(nrc[1], nrc[0]) Phis[i] = phi Thetas[i] = theta return Phis, Thetas # phis = np.deg2rad(np.linspace(0, 360, 10000)) r0_2 = positions_pairs[0][1] - positions_pairs[0][0] # vector pointing from Ch2 to Ch0 r1_3 = positions_pairs[1][1] - positions_pairs[1][0] # vector pointing from Ch3 to Ch1 self.logger.debug('r02 {}\nr13 {}'.format(r0_2, r1_3)) linestyles = ['-', '--', ':', '-.'] for i, t02 in enumerate(t02s): # theta02 = get_deltat02(t02, phis) phi02, theta02 = getDeltaTCone(r0_2, t02) theta02[theta02 < 0] += np.pi phi02[phi02 < 0] += 2 * np.pi jumppos02 = np.where(np.abs(np.diff(phi02)) >= 5.0) for j, pos in enumerate(jumppos02): phi02 = np.insert(phi02, pos + 1 + j, np.nan) theta02 = np.insert(theta02, pos + 1 + j, np.nan) # mask02 = ~np.isnan(theta02) ax.plot(np.rad2deg(phi02), np.rad2deg(theta02), '{}C3'.format(linestyles[i % 4]), label='c 0+2 dt = {}'.format(t02)) for i, t13 in enumerate(t13s): # theta13 = get_deltat13(t13, phis) phi13, theta13 = getDeltaTCone(r1_3, t13) theta13[theta13 < 0] += np.pi phi13[phi13 < 0] += 2 * np.pi jumppos13 = np.where(np.abs(np.diff(phi13)) >= 5.0) for j, pos in enumerate(jumppos13): phi13 = np.insert(phi13, pos + 1 + j, np.nan) theta13 = np.insert(theta13, pos + 1 + j, np.nan) # mask13 = ~np.isnan(theta13) ax.plot(np.rad2deg(phi13), np.rad2deg(theta13), '{}C2'.format(linestyles[i % 4]), label='c 1+3 dt = {}'.format(t13)) ax.legend(fontsize='small') ax.set_ylim(ZenLim[0] / units.deg, ZenLim[1] / units.deg) ax.set_xlim(AziLim[0] / units.deg, AziLim[1] / units.deg)
def get_array_of_channels(station, det, zenith, azimuth, polarization): """ Returns an array of the channel traces that is cut to the physical overlapping time Parameters ---------- station: Station Station from which to take the channels det: Detector The detector description zenith: float Arrival zenith angle at antenna azimuth: float Arrival azimuth angle at antenna polarization: int 0: eTheta 1: ePhi """ time_shifts = np.zeros(8) t_geos = np.zeros(8) sampling_rate = station.get_channel(0).get_sampling_rate() station_id = station.get_id() site = det.get_site(station_id) for iCh, channel in enumerate(station.get_electric_fields()): channel_id = channel.get_channel_ids()[0] antenna_position = det.get_relative_position(station_id, channel_id) # determine refractive index of signal propagation speed between antennas refractive_index = ice.get_refractive_index( 1, site) # if signal comes from above, in-air propagation speed if (zenith > 0.5 * np.pi): refractive_index = ice.get_refractive_index( antenna_position[2], site ) # if signal comes from below, use refractivity at antenna position refractive_index = 1.353 time_shift = -geo_utl.get_time_delay_from_direction( zenith, azimuth, antenna_position, n=refractive_index) t_geos[iCh] = time_shift time_shift += channel.get_trace_start_time() time_shifts[iCh] = time_shift delta_t = time_shifts.max() - time_shifts.min() tmin = time_shifts.min() tmax = time_shifts.max() trace_length = station.get_electric_fields()[0].get_times( )[-1] - station.get_electric_fields()[0].get_times()[0] traces = [] n_samples = None for iCh, channel in enumerate(station.get_electric_fields()): tstart = delta_t - (time_shifts[iCh] - tmin) tstop = tmax - time_shifts[iCh] - delta_t + trace_length iStart = int(round(tstart * sampling_rate)) iStop = int(round(tstop * sampling_rate)) if (n_samples is None): n_samples = iStop - iStart if (n_samples % 2): n_samples -= 1 trace = copy.copy(channel.get_trace() [polarization]) # copy to not modify data structure trace = trace[iStart:(iStart + n_samples)] base_trace = NuRadioReco.framework.base_trace.BaseTrace( ) # create base trace class to do the fft with correct normalization etc. base_trace.set_trace(trace, sampling_rate) traces.append(base_trace) return traces
def get_array_of_channels(station, use_channels, det, zenith, azimuth, antenna_pattern_provider, time_domain=False): time_shifts = np.zeros(len(use_channels)) t_cables = np.zeros(len(use_channels)) t_geos = np.zeros(len(use_channels)) station_id = station.get_id() site = det.get_site(station_id) for iCh, channel in enumerate(station.iter_channels(use_channels)): channel_id = channel.get_id() antenna_position = det.get_relative_position(station_id, channel_id) # determine refractive index of signal propagation speed between antennas refractive_index = ice.get_refractive_index( 1, site) # if signal comes from above, in-air propagation speed if station.is_cosmic_ray(): if (zenith > 0.5 * np.pi): refractive_index = ice.get_refractive_index( antenna_position[2], site ) # if signal comes from below, use refractivity at antenna position if station.is_neutrino(): refractive_index = ice.get_refractive_index( antenna_position[2], site) time_shift = -geo_utl.get_time_delay_from_direction( zenith, azimuth, antenna_position, n=refractive_index) t_geos[iCh] = time_shift t_cables[iCh] = channel.get_trace_start_time() logger.debug( "time shift channel {}: {:.2f}ns (signal prop), {:.2f}ns (trace start time)" .format(channel.get_id(), time_shift, channel.get_trace_start_time())) time_shift += channel.get_trace_start_time() time_shifts[iCh] = time_shift delta_t = time_shifts.max() - time_shifts.min() tmin = time_shifts.min() tmax = time_shifts.max() logger.debug("adding relative station time = {:.0f}ns".format( (t_cables.min() + t_geos.max()) / units.ns)) logger.debug("delta t is {:.2f}".format(delta_t / units.ns)) trace_length = station.get_channel( 0).get_times()[-1] - station.get_channel(0).get_times()[0] debug_cut = 0 if (debug_cut): fig, ax = plt.subplots(len(use_channels), 1) traces = [] n_samples = None for iCh, channel in enumerate(station.iter_channels(use_channels)): tstart = delta_t - (time_shifts[iCh] - tmin) tstop = tmax - time_shifts[iCh] - delta_t + trace_length iStart = int(round(tstart * channel.get_sampling_rate())) iStop = int(round(tstop * channel.get_sampling_rate())) if (n_samples is None): n_samples = iStop - iStart if (n_samples % 2): n_samples -= 1 trace = copy.copy( channel.get_trace()) # copy to not modify data structure trace = trace[iStart:(iStart + n_samples)] if (debug_cut): ax[iCh].plot(trace) base_trace = NuRadioReco.framework.base_trace.BaseTrace( ) # create base trace class to do the fft with correct normalization etc. base_trace.set_trace(trace, channel.get_sampling_rate()) traces.append(base_trace) times = traces[0].get_times( ) # assumes that all channels have the same sampling rate if (time_domain): # save time domain traces first to avoid extra fft V_timedomain = np.zeros((len(use_channels), len(times))) for iCh, trace in enumerate(traces): V_timedomain[iCh] = trace.get_trace() frequencies = traces[0].get_frequencies( ) # assumes that all channels have the same sampling rate V = np.zeros((len(use_channels), len(frequencies)), dtype=np.complex) for iCh, trace in enumerate(traces): V[iCh] = trace.get_frequency_spectrum() efield_antenna_factor = trace_utilities.get_efield_antenna_factor( station, frequencies, use_channels, det, zenith, azimuth, antenna_pattern_provider) if (debug_cut): plt.show() if (time_domain): return efield_antenna_factor, V, V_timedomain return efield_antenna_factor, V
def run(self, evt, station, det, use_channels=None, use_MC_direction=False, force_Polarization=''): """ run method. This function is executed for each event Parameters --------- evt station det use_channels: array of ints (default: [0, 1, 2, 3]) the channel ids to use for the electric field reconstruction use_MC_direction: bool if True uses zenith and azimuth direction from simulated station if False uses reconstructed direction from station parameters. force_Polarization: str if eTheta or ePhi, then only reconstructs chosen polarization of electric field, assuming the other is 0. Otherwise, reconstructs electric field for both eTheta and ePhi """ if use_channels is None: use_channels = [0, 1, 2, 3] station_id = station.get_id() if use_MC_direction: zenith = station.get_sim_station()[stnp.zenith] azimuth = station.get_sim_station()[stnp.azimuth] else: logger.info( "Using reconstructed (or starting) angles as no signal arrival angles are present" ) zenith = station[stnp.zenith] azimuth = station[stnp.azimuth] efield_antenna_factor, V = get_array_of_channels( station, use_channels, det, zenith, azimuth, self.antenna_provider) n_frequencies = len(V[0]) denom = (efield_antenna_factor[0][0] * efield_antenna_factor[1][1] - efield_antenna_factor[0][1] * efield_antenna_factor[1][0]) mask = np.abs(denom) != 0 # solving for electric field using just two orthorgonal antennas E1 = np.zeros_like(V[0]) E2 = np.zeros_like(V[0]) E1[mask] = (V[0] * efield_antenna_factor[1][1] - V[1] * efield_antenna_factor[0][1])[mask] / denom[mask] E2[mask] = (V[1] - efield_antenna_factor[1][0] * E1)[mask] / efield_antenna_factor[1][1][mask] denom = (efield_antenna_factor[0][0] * efield_antenna_factor[-1][1] - efield_antenna_factor[0][1] * efield_antenna_factor[-1][0]) mask = np.abs(denom) != 0 E1[mask] = (V[0] * efield_antenna_factor[-1][1] - V[-1] * efield_antenna_factor[0][1])[mask] / denom[mask] E2[mask] = (V[-1] - efield_antenna_factor[-1][0] * E1)[mask] / efield_antenna_factor[-1][1][mask] # solve it in a vectorized way efield3_f = np.zeros((2, n_frequencies), dtype=np.complex) if force_Polarization == 'eTheta': efield3_f[:1, mask] = np.moveaxis( stacked_lstsq( np.moveaxis(efield_antenna_factor[:, 0, mask], 1, 0)[:, :, np.newaxis], np.moveaxis(V[:, mask], 1, 0)), 0, 1) elif force_Polarization == 'ePhi': efield3_f[1:, mask] = np.moveaxis( stacked_lstsq( np.moveaxis(efield_antenna_factor[:, 1, mask], 1, 0)[:, :, np.newaxis], np.moveaxis(V[:, mask], 1, 0)), 0, 1) else: efield3_f[:, mask] = np.moveaxis( stacked_lstsq( np.moveaxis(efield_antenna_factor[:, :, mask], 2, 0), np.moveaxis(V[:, mask], 1, 0)), 0, 1) # add eR direction efield3_f = np.array([ np.zeros_like(efield3_f[0], dtype=np.complex), efield3_f[0], efield3_f[1] ]) electric_field = NuRadioReco.framework.electric_field.ElectricField( use_channels, [0, 0, 0]) electric_field.set_frequency_spectrum( efield3_f, station.get_channel(0).get_sampling_rate()) electric_field.set_parameter(efp.zenith, zenith) electric_field.set_parameter(efp.azimuth, azimuth) # figure out the timing of the E-field t_shifts = np.zeros(V.shape[0]) site = det.get_site(station_id) if (zenith > 0.5 * np.pi): logger.warning( "Module has not been optimized for neutrino reconstruction yet. Results may be nonsense." ) refractive_index = ice.get_refractive_index( -1, site ) # if signal comes from below, use refractivity at antenna position else: refractive_index = ice.get_refractive_index( 1, site) # if signal comes from above, in-air propagation speed for i_ch, channel_id in enumerate(use_channels): antenna_position = det.get_relative_position( station.get_id(), channel_id) t_shifts[i_ch] = station.get_channel( channel_id).get_trace_start_time( ) - geo_utl.get_time_delay_from_direction( zenith, azimuth, antenna_position, n=refractive_index) electric_field.set_trace_start_time(t_shifts.max()) station.add_electric_field(electric_field)
def run(self, evt, station, det): t = time.time() # access simulated efield and high level parameters sim_station = station.get_sim_station() if (len(sim_station.get_electric_fields()) == 0): raise LookupError(f"station {station.get_id()} has no efields") sim_station_id = sim_station.get_id() # first we determine the trace start time of all channels and correct # for different cable delays times_min = [] times_max = [] for iCh in det.get_channel_ids(sim_station_id): for electric_field in sim_station.get_electric_fields_for_channels( [iCh]): time_resolution = 1. / electric_field.get_sampling_rate() cab_delay = det.get_cable_delay(sim_station_id, iCh) t0 = electric_field.get_trace_start_time() + cab_delay # if we have a cosmic ray event, the different signal travel time to the antennas has to be taken into account if sim_station.is_cosmic_ray(): site = det.get_site(sim_station_id) antenna_position = det.get_relative_position( sim_station_id, iCh) - electric_field.get_position() if sim_station.get_parameter( stnp.zenith ) > 90 * units.deg: # signal is coming from below, so we take IOR of ice index_of_refraction = ice.get_refractive_index( antenna_position[2], site) else: # signal is coming from above, so we take IOR of air index_of_refraction = ice.get_refractive_index(1, site) # For cosmic ray events, we only have one electric field for all channels, so we have to account # for the difference in signal travel between channels. IMPORTANT: This is only accurate # if all channels have the same z coordinate travel_time_shift = geo_utl.get_time_delay_from_direction( sim_station.get_parameter(stnp.zenith), sim_station.get_parameter(stnp.azimuth), antenna_position, index_of_refraction) t0 += travel_time_shift if ( not np.isnan(t0) ): # trace start time is None if no ray tracing solution was found and channel contains only zeros times_min.append(t0) times_max.append(t0 + electric_field.get_number_of_samples() / electric_field.get_sampling_rate()) self.logger.debug( "trace start time {}, cab_delty {}, tracelength {}". format( electric_field.get_trace_start_time(), cab_delay, electric_field.get_number_of_samples() / electric_field.get_sampling_rate())) times_min = np.array(times_min) times_max = np.array(times_max) if times_min.min() < 0: times_min -= times_min.min() times_max -= times_min.min() times_min = np.array(times_min) - self.__pre_pulse_time times_max = np.array(times_max) + self.__post_pulse_time trace_length = times_max.max() - times_min.min() trace_length_samples = int(round(trace_length / time_resolution)) if trace_length_samples % 2 != 0: trace_length_samples += 1 self.logger.debug( "smallest trace start time {:.1f}, largest trace time {:.1f} -> n_samples = {:d} {:.0f}ns)" .format(times_min.min(), times_max.max(), trace_length_samples, trace_length / units.ns)) # loop over all channels for channel_id in det.get_channel_ids(station.get_id()): # one channel might contain multiple channels to store the signals from multiple ray paths, # so we loop over all simulated channels with the same id, # convolve each trace with the antenna response for the given angles # and everything up in the time domain self.logger.debug('channel id {}'.format(channel_id)) channel = NuRadioReco.framework.channel.Channel(channel_id) channel_spectrum = None trace_object = None if (self.__debug): from matplotlib import pyplot as plt fig, axes = plt.subplots(2, 1) for electric_field in sim_station.get_electric_fields_for_channels( [channel_id]): # all simulated channels have a different trace start time # in a measurement, all channels have the same physical start time # so we need to create one long trace that can hold all the different channel times # to achieve a good time resolution, we upsample the trace first. new_efield = NuRadioReco.framework.base_trace.BaseTrace( ) # create new data structure with new efield length new_efield.set_trace(copy.copy(electric_field.get_trace()), electric_field.get_sampling_rate()) new_trace = np.zeros((3, trace_length_samples)) # calculate the start bin if (not np.isnan(electric_field.get_trace_start_time())): cab_delay = det.get_cable_delay(sim_station_id, channel_id) if sim_station.is_cosmic_ray(): site = det.get_site(sim_station_id) antenna_position = det.get_relative_position( sim_station_id, channel_id) - electric_field.get_position() if sim_station.get_parameter( stnp.zenith ) > 90 * units.deg: # signal is coming from below, so we take IOR of ice index_of_refraction = ice.get_refractive_index( antenna_position[2], site) else: # signal is coming from above, so we take IOR of air index_of_refraction = ice.get_refractive_index( 1, site) travel_time_shift = geo_utl.get_time_delay_from_direction( sim_station.get_parameter(stnp.zenith), sim_station.get_parameter(stnp.azimuth), antenna_position, index_of_refraction) start_time = electric_field.get_trace_start_time( ) + cab_delay - times_min.min() + travel_time_shift start_bin = int(round(start_time / time_resolution)) time_remainder = start_time - start_bin * time_resolution else: start_time = electric_field.get_trace_start_time( ) + cab_delay - times_min.min() start_bin = int(round(start_time / time_resolution)) time_remainder = start_time - start_bin * time_resolution self.logger.debug( 'channel {}, start time {:.1f} = bin {:d}, ray solution {}' .format( channel_id, electric_field.get_trace_start_time() + cab_delay, start_bin, electric_field[efp.ray_path_type])) new_efield.apply_time_shift(time_remainder) new_trace[:, start_bin:(start_bin + new_efield.get_number_of_samples() )] = new_efield.get_trace() trace_object = NuRadioReco.framework.base_trace.BaseTrace() trace_object.set_trace(new_trace, 1. / time_resolution) trace_object.set_trace_start_time( np.min(times_min) - cab_delay) if (self.__debug): axes[0].plot(trace_object.get_times(), new_trace[1], label="eTheta {}".format( electric_field[efp.ray_path_type]), c='C0') axes[0].plot(trace_object.get_times(), new_trace[2], label="ePhi {}".format( electric_field[efp.ray_path_type]), c='C0', linestyle=':') axes[0].plot(electric_field.get_times(), electric_field.get_trace()[1], c='C1', linestyle='-', alpha=.5) axes[0].plot(electric_field.get_times(), electric_field.get_trace()[2], c='C1', linestyle=':', alpha=.5) ff = trace_object.get_frequencies() efield_fft = trace_object.get_frequency_spectrum() zenith = electric_field[efp.zenith] azimuth = electric_field[efp.azimuth] # get antenna pattern for current channel VEL = trace_utilities.get_efield_antenna_factor( sim_station, ff, [channel_id], det, zenith, azimuth, self.antenna_provider) if VEL is None: # this can happen if there is not signal path to the antenna voltage_fft = np.zeros_like( efield_fft[1]) # set voltage trace to zeros else: # Apply antenna response to electric field VEL = VEL[ 0] # we only requested the VEL for one channel, so selecting it voltage_fft = np.sum( VEL * np.array([efield_fft[1], efield_fft[2]]), axis=0) # Remove DC offset voltage_fft[np.where(ff < 5 * units.MHz)] = 0. if (self.__debug): axes[1].plot(trace_object.get_times(), fft.freq2time( voltage_fft, electric_field.get_sampling_rate()), label="{}, zen = {:.0f}deg".format( electric_field[efp.ray_path_type], zenith / units.deg)) if ('amp' in self.__uncertainty): voltage_fft *= np.random.normal( 1, self.__uncertainty['amp'][channel_id]) if ('sys_amp' in self.__uncertainty): voltage_fft *= self.__uncertainty['sys_amp'][channel_id] if (channel_spectrum is None): channel_spectrum = voltage_fft else: channel_spectrum += voltage_fft if (self.__debug): axes[0].legend(loc='upper left') axes[1].legend(loc='upper left') plt.show() if trace_object is None: # this happens if don't have any efield for this channel # set the trace to zeros channel.set_trace(np.zeros(trace_length_samples), 1. / time_resolution) else: channel.set_frequency_spectrum( channel_spectrum, trace_object.get_sampling_rate()) channel.set_trace_start_time(times_min.min()) station.add_channel(channel) self.__t += time.time() - t
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): t = time.time() # access simulated efield and high level parameters sim_station = station.get_sim_station() if(len(sim_station.get_electric_fields()) == 0): raise LookupError(f"station {station.get_id()} has no efields") for channel_id in det.get_channel_ids(station.get_id()): # one channel might contain multiple channels to store the signals from multiple ray paths and showers, # so we loop over all simulated channels with the same id, self.logger.debug('channel id {}'.format(channel_id)) for electric_field in sim_station.get_electric_fields_for_channels([channel_id]): sim_channel = NuRadioReco.framework.sim_channel.SimChannel(channel_id, shower_id=electric_field.get_shower_id(), ray_tracing_id=electric_field.get_ray_tracing_solution_id()) ff = electric_field.get_frequencies() efield_fft = electric_field.get_frequency_spectrum() zenith = electric_field[efp.zenith] azimuth = electric_field[efp.azimuth] # get antenna pattern for current channel VEL = trace_utilities.get_efield_antenna_factor(sim_station, ff, [channel_id], det, zenith, azimuth, self.antenna_provider) if VEL is None: # this can happen if there is not signal path to the antenna voltage_fft = np.zeros_like(efield_fft[1]) # set voltage trace to zeros else: # Apply antenna response to electric field VEL = VEL[0] # we only requested the VEL for one channel, so selecting it voltage_fft = np.sum(VEL * np.array([efield_fft[1], efield_fft[2]]), axis=0) # Remove DC offset voltage_fft[np.where(ff < 5 * units.MHz)] = 0. if sim_station.is_cosmic_ray(): site = det.get_site(station.get_id()) antenna_position = det.get_relative_position(station.get_id(), channel_id) - electric_field.get_position() if zenith > 90 * units.deg: # signal is coming from below, so we take IOR of ice index_of_refraction = ice.get_refractive_index(antenna_position[2], site) else: # signal is coming from above, so we take IOR of air index_of_refraction = ice.get_refractive_index(1, site) # For cosmic ray events, we only have one electric field for all channels, so we have to account # for the difference in signal travel between channels. IMPORTANT: This is only accurate # if all channels have the same z coordinate travel_time_shift = geometryUtilities.get_time_delay_from_direction( zenith, azimuth, antenna_position, index_of_refraction ) else: travel_time_shift = 0 # set the trace to zeros sim_channel.set_frequency_spectrum(voltage_fft, electric_field.get_sampling_rate()) sim_channel.set_trace_start_time(electric_field.get_trace_start_time() + travel_time_shift) sim_station.add_channel(sim_channel) self.__t += time.time() - t