def front_end(self, signal): """ Apply front-end processes to a signal and return the output. The front-end consists of amplification according to data taken from NuRadioReco and signal clipping. Parameters ---------- signal : Signal ``Signal`` object on which to apply the front-end processes. Returns ------- Signal Signal processed by the antenna front end. """ copy = Signal(signal.times, signal.values) copy.filter_frequencies(self.interpolate_filter, force_real=True) clipped_values = np.clip(copy.values * self.amplification, a_min=-self.amplifier_clipping, a_max=self.amplifier_clipping) return Signal(signal.times, clipped_values, value_type=signal.value_type)
def front_end(self, signal): """ Apply front-end processes to a signal and return the output. The front-end consists of the full ARA electronics chain (including amplification) and signal clipping. Parameters ---------- signal : Signal ``Signal`` object on which to apply the front-end processes. Returns ------- Signal Signal processed by the antenna front end. """ copy = Signal(signal.times, signal.values) copy.filter_frequencies(self.interpolate_filter, force_real=True) # sqrt(2) for 3dB splitter for TURF, SURF clipped_values = np.clip(copy.values / np.sqrt(2) * self.amplification, a_min=-self.amplifier_clipping, a_max=self.amplifier_clipping) return Signal(signal.times, clipped_values, value_type=signal.value_type)
def receive(self, signal, origin=None, polarization=None): """Process incoming signal according to the filter function and store it to the signals list. Subclasses may extend this fuction, but should end with super().receive(signal).""" copy = Signal(signal.times, signal.values, value_type=Signal.ValueTypes.voltage) copy.filter_frequencies(self.response) if origin is None: d_gain = 1 else: # Calculate theta and phi relative to the orientation r, theta, phi = self._convert_to_antenna_coordinates(origin) d_gain = self.directional_gain(theta=theta, phi=phi) if polarization is None: p_gain = 1 else: p_gain = self.polarization_gain(normalize(polarization)) signal_factor = d_gain * p_gain * self.efficiency if signal.value_type==Signal.ValueTypes.voltage: pass elif signal.value_type==Signal.ValueTypes.field: signal_factor /= self.antenna_factor else: raise ValueError("Signal's value type must be either " +"voltage or field. Given "+str(signal.value_type)) copy.values *= signal_factor self.signals.append(copy)
def receive(self, signal, direction=None, polarization=None, force_real=True): """Process incoming signal according to the filter function and store it to the signals list. Subclasses may extend this fuction, but should end with super().receive(signal).""" copy = Signal(signal.times, signal.values, value_type=Signal.ValueTypes.voltage) copy.filter_frequencies(self.response, force_real=force_real) if direction is not None: # Calculate theta and phi relative to the orientation origin = self.position - normalize(direction) r, theta, phi = self._convert_to_antenna_coordinates(origin) freq_data, gain_data, phase_data = self.generate_directionality_gains( theta, phi) def interpolate_directionality(frequencies): interp_gains = np.interp(frequencies, freq_data, gain_data, left=0, right=0) interp_phases = np.interp(frequencies, freq_data, phase_data, left=0, right=0) return interp_gains * np.exp(1j * interp_phases) copy.filter_frequencies(interpolate_directionality, force_real=force_real) if polarization is None: p_gain = 1 else: p_gain = self.polarization_gain(normalize(polarization)) signal_factor = p_gain * self.efficiency if signal.value_type == Signal.ValueTypes.voltage: pass elif signal.value_type == Signal.ValueTypes.field: signal_factor /= self.antenna_factor else: raise ValueError("Signal's value type must be either " + "voltage or field. Given " + str(signal.value_type)) copy.values *= signal_factor self.signals.append(copy)
def front_end(self, signal): """Apply the front-end processing of the antenna signal, including electronics chain filters/amplification and clipping.""" copy = Signal(signal.times, signal.values) copy.filter_frequencies(self.antenna.interpolate_filter, force_real=True) clipped_values = np.clip(copy.values, a_min=-self.amplifier_clipping, a_max=self.amplifier_clipping) return Signal(signal.times, clipped_values, value_type=signal.value_type)
def test_filter_frequencies_force_real(self, signal): """Test that the filter_frequencies force_real option works""" resp = lambda f: int(f==0.2) copy = Signal(signal.times, signal.values) expected = Signal(signal.times, [-0.05,0.0190983,0.0618034,0.0190983,-0.05], value_type=signal.value_type) copy.filter_frequencies(resp, force_real=False) for i in range(5): assert copy.values[i] == pytest.approx(expected.values[i]) expected = Signal(signal.times, [-0.1,0.0381966,0.1236068,0.0381966,-0.1], value_type=signal.value_type) signal.filter_frequencies(resp, force_real=True) for i in range(5): assert signal.values[i] == pytest.approx(expected.values[i])
def propagate(self, signal=None, polarization=None): """ Propagate the signal with optional polarization along the ray path. Applies the frequency-dependent signal attenuation along the ray path and shifts the times according to the ray time of flight. Additionally provides the s and p polarization directions. Parameters ---------- signal : Signal, optional ``Signal`` object to propagate. polarization : array_like, optional Vector representing the linear polarization of the `signal`. Returns ------- tuple of Signal Tuple of ``Signal`` objects representing the s and p polarizations of the original `signal` attenuated along the ray path. Only returned if `signal` was not ``None``. tuple of ndarray Tuple of polarization vectors representing the s and p polarization directions of the `signal` at the end of the ray path. Only returned if `polarization` was not ``None``. See Also -------- pyrex.Signal : Base class for time-domain signals. """ if polarization is None: if signal is None: return else: copy = Signal(signal.times+self.tof, signal.values, value_type=signal.value_type) copy.filter_frequencies(self.attenuation) return copy else: # Unit vectors perpendicular and parallel to plane of incidence # at the launching point u_s0 = normalize(np.cross(self.emitted_direction, [0, 0, 1])) u_p0 = normalize(np.cross(u_s0, self.emitted_direction)) # Unit vector parallel to plane of incidence at the receiving point # (perpendicular vector stays the same) u_p1 = normalize(np.cross(u_s0, self.received_direction)) if signal is None: return (u_s0, u_p1) else: # Amplitudes of s and p components pol_s = np.dot(polarization, u_s0) pol_p = np.dot(polarization, u_p0) # Fresnel reflectances of s and p components f_s, f_p = self.fresnel # Apply fresnel s and p coefficients in addition to attenuation attenuation_s = lambda freqs: self.attenuation(freqs) * f_s attenuation_p = lambda freqs: self.attenuation(freqs) * f_p signal_s = Signal(signal.times+self.tof, signal.values*pol_s, value_type=signal.value_type) signal_p = Signal(signal.times+self.tof, signal.values*pol_p, value_type=signal.value_type) signal_s.filter_frequencies(attenuation_s, force_real=True) signal_p.filter_frequencies(attenuation_p, force_real=True) return (signal_s, signal_p), (u_s0, u_p1)
def apply_response(self, signal, direction=None, polarization=None, force_real=True): """ Process the complete antenna response for an incoming signal. Processes the incoming signal according to the frequency response of the antenna, the efficiency, and the antenna factor. May also apply the directionality and the polarization gain depending on the provided parameters. Subclasses may wish to overwrite this function if the full antenna response cannot be divided nicely into the described pieces. Parameters ---------- signal : Signal Incoming ``Signal`` object to process. direction : array_like, optional Vector denoting the direction of travel of the signal as it reaches the antenna. If ``None`` no directional response will be applied. polarization : array_like, optional Vector denoting the signal's polarization direction. If ``None`` no polarization gain will be applied. force_real : boolean, optional Whether or not the frequency response should be redefined in the negative-frequency domain to keep the values of the filtered signal real. Returns ------- Signal Processed ``Signal`` object after the complete antenna response has been applied. Should have a ``value_type`` of ``voltage``. Raises ------ ValueError If the given `signal` does not have a ``value_type`` of ``voltage`` or ``field``. See Also -------- pyrex.Signal : Base class for time-domain signals. """ copy = Signal(signal.times, signal.values, value_type=Signal.Type.voltage) copy.filter_frequencies(self.frequency_response, force_real=force_real) if direction is not None and polarization is not None: # Calculate theta and phi relative to the orientation origin = self.position - normalize(direction) r, theta, phi = self._convert_to_antenna_coordinates(origin) # Calculate polarization vector in the antenna coordinates y_axis = np.cross(self.z_axis, self.x_axis) transformation = np.array([self.x_axis, y_axis, self.z_axis]) ant_pol = np.dot(transformation, normalize(polarization)) # Calculate directional response as a function of frequency directive_response = self.directional_response(theta, phi, ant_pol) copy.filter_frequencies(directive_response, force_real=force_real) elif (direction is not None and polarization is None or direction is None and polarization is not None): raise ValueError( "Direction and polarization must be specified together") signal_factor = self.efficiency if signal.value_type == Signal.Type.voltage: pass elif signal.value_type == Signal.Type.field: signal_factor /= self.antenna_factor else: raise ValueError("Signal's value type must be either " + "voltage or field. Given " + str(signal.value_type)) copy *= signal_factor return copy