def simple_rfi_path(f_start, drift_rate, spread, spread_type='uniform', rfi_type='stationary'): """ A crude simulation of one style of RFI that shows up, in which the signal jumps around in frequency. This example samples the center frequency for each time sample from either a uniform or normal distribution. 'spread' defines the range for center frequency variations. Argument 'spread_type' can be either 'uniform' or 'normal'. Argument 'rfi_type' can be either 'stationary' or 'random_walk'; 'stationary' only offsets with respect to a straight-line path, but 'random_walk' accumulates frequency offsets over time. """ f_start = unit_utils.get_value(f_start, u.Hz) drift_rate = unit_utils.get_value(drift_rate, u.Hz / u.s) spread = unit_utils.get_value(spread, u.Hz) def path(t): if spread_type == 'uniform': f_offset = np.random.uniform(-spread / 2., spread / 2., size=t.shape) elif spread_type == 'normal': factor = 2 * np.sqrt(2 * np.log(2)) f_offset = np.random.normal(0, spread / factor, size=t.shape) else: sys.exit('{} is not a valid spread type!'.format(spread_type)) if rfi_type == 'random_walk': f_offset = np.cumsum(f_offset) return f_start + drift_rate * t + f_offset return path
def add_constant_signal(self, f_start, drift_rate, level, phase=0): """ Adds a drifting cosine signal (linear chirp) as a signal source function. Parameters ---------- f_start : float Starting signal frequency drift_rate : float Drift rate in Hz / s level : float Signal level or amplitude phase : float Phase, in radiations """ f_start = unit_utils.get_value(f_start, u.Hz) drift_rate = unit_utils.get_value(drift_rate, u.Hz / u.s) def signal_func(ts): # Calculate adjusted center frequencies, according to chirp chirp_phase = 2 * xp.pi * ( (f_start - self.fch1) * ts + 0.5 * drift_rate * ts**2) if not self.ascending: chirp_phase = -chirp_phase return level * xp.cos(chirp_phase + phase) self.signal_sources.append(signal_func)
def __init__(self, sample_rate=3 * u.GHz, fch1=0 * u.GHz, ascending=True, t_start=0, seed=None): """ Initialize a DataStream object with a sampling rate and frequency range. By default, :code:`setigen.voltage` does not employ heterodyne mixing and filtering to focus on a frequency bandwidth. Instead, the sensitive range is determined by these parameters; starting at the frequency `fch1` and spanning the Nyquist range `sample_rate / 2` in the increasing or decreasing frequency direction, as specified by `ascending`. Note that accordingly, the spectral response will be susceptible to aliasing, so take care that the desired frequency range is correct and that signals are injected at appropriate frequencies. Parameters ---------- sample_rate : float, optional Physical sample rate, in Hz, for collecting real voltage data fch1 : astropy.Quantity, optional Starting frequency of the first coarse channel, in Hz. If ascending=True, fch1 is the minimum frequency; if ascending=False (default), fch1 is the maximum frequency. ascending : bool, optional Specify whether frequencies should be in ascending or descending order. Default is True, for which fch1 is the minimum frequency. t_start : float, optional Start time, in seconds seed : int, optional Integer seed between 0 and 2**32. If None, the random number generator will use a random seed. """ #: Random number generator self.rng = xp.random.RandomState(seed) self.sample_rate = unit_utils.get_value(sample_rate, u.Hz) self.dt = 1 / self.sample_rate # For adjusting signal frequencies self.fch1 = unit_utils.get_value(fch1, u.Hz) self.ascending = ascending # For estimating SNR for signals self.noise_std = 0 self.bg_noise_std = 0 # Tracks start time of next sequence of data self.t_start = t_start self.start_obs = True self.ts = None self.v = None # Hold functions that generate voltage values self.noise_sources = [] self.signal_sources = []
def squared_path(f_start, drift_rate): """ Quadratic signal path; drift_rate here only refers to the starting slope. """ f_start = unit_utils.get_value(f_start, u.Hz) drift_rate = unit_utils.get_value(drift_rate, u.Hz / u.s) def path(t): return f_start + 0.5 * drift_rate * t**2 return path
def constant_path(f_start, drift_rate): """ Constant drift rate. """ f_start = unit_utils.get_value(f_start, u.Hz) drift_rate = unit_utils.get_value(drift_rate, u.Hz / u.s) def path(t): return f_start + drift_rate * t return path
def sine_path(f_start, drift_rate, period, amplitude): """ Sine path in time-frequency space. """ f_start = unit_utils.get_value(f_start, u.Hz) drift_rate = unit_utils.get_value(drift_rate, u.Hz / u.s) period = unit_utils.get_value(period, u.s) amplitude = unit_utils.get_value(amplitude, u.Hz) def path(t): return f_start + amplitude * np.sin(2*np.pi*t/period) + drift_rate * t return path
def sinc2_f_profile(width, width_mode='fwhm', trunc=True): """ Sinc squared profile; width is the FWHM of the squared normalized sinc function. The trunc parameter controls whether or not the sinc squared profile is truncated at the first root (e.g. zeroed out for more distant frequencies). Parameters ---------- width : float Signal width, in Hz width_mode : str How to interpret `width`. Can be 'fwhm' or 'crossing', for zero crossing. trunc : bool, optional Whether to truncate signal after first zero crossing """ width = unit_utils.get_value(width, u.Hz) # Using the numerical solution for the FWHM if width_mode == 'fwhm': zero_crossing = (width / 2) / 0.442946470689452 else: zero_crossing = width / 2 def f_profile(f, f_center): if trunc: return np.where( np.abs(f - f_center) < zero_crossing, np.sinc((f - f_center) / zero_crossing), 0)**2 else: return np.sinc((f - f_center) / zero_crossing)**2 return f_profile
def voigt_f_profile(g_width, l_width): """ Voigt profile; g_width and l_width are the FWHMs of the Gaussian and Lorentzian profiles. Further information here: https://en.wikipedia.org/wiki/Voigt_profile. """ g_width = unit_utils.get_value(g_width, u.Hz) factor = 2 * np.sqrt(2 * np.log(2)) sigma = g_width / factor l_width = unit_utils.get_value(l_width, u.Hz) gamma = l_width / 2 def f_profile(f, f_center): return func_utils.voigt(f, f_center, sigma, gamma) / func_utils.voigt( f_center, f_center, sigma, gamma) return f_profile
def box_f_profile(width): """ Square intensity profile in the frequency direction. """ width = unit_utils.get_value(width, u.Hz) def f_profile(f, f_center): return (np.abs(f - f_center) < width / 2).astype(int) return f_profile
def sine_t_profile(period, phase=0, amplitude=1, level=1): """ Intensity varying as a sine curve, where level is the mean intensity. """ period = unit_utils.get_value(period, u.s) def t_profile(t): return amplitude * np.sin(2 * np.pi * (t + phase) / period) + level return t_profile
def lorentzian_f_profile(width): """ Lorentzian profile; width is the FWHM of the profile. """ width = unit_utils.get_value(width, u.Hz) gamma = width / 2 def f_profile(f, f_center): return func_utils.lorentzian(f, f_center, gamma) return f_profile
def gaussian_f_profile(width): """ Gaussian profile; width is the FWHM of the profile. """ width = unit_utils.get_value(width, u.Hz) factor = 2 * np.sqrt(2 * np.log(2)) sigma = width / factor def f_profile(f, f_center): return func_utils.gaussian(f, f_center, sigma) return f_profile
def choppy_rfi_path(f_start, drift_rate, spread, spread_type='uniform'): """ A crude simulation of one style of RFI that shows up, in which the signal jumps around in frequency. This example samples the center frequency for each time sample from either a uniform or normal distribution. Argument spread_type can be either 'uniform' or 'normal'. Note: another approach could be to random walk the frequency over time. """ f_start = unit_utils.get_value(f_start, u.Hz) drift_rate = unit_utils.get_value(drift_rate, u.Hz / u.s) spread = unit_utils.get_value(spread, u.Hz) def path(t): if spread_type == 'uniform': f_offset = np.random.uniform(-spread / 2., spread / 2., t.shape) elif spread_type == 'normal': f_offset = np.random.normal(0, spread, t.shape) else: sys.exit('%s is not a valid spread type!' % spread_type) return f_start + drift_rate * t + f_offset return path
def multiple_gaussian_f_profile(width): """ Example adding multiple Gaussians in the frequency direction. """ width = unit_utils.get_value(width, u.Hz) factor = 2 * np.sqrt(2 * np.log(2)) sigma = width / factor def f_profile(f, f_center): # Offsets by 100 Hz @ a quarter intensity, absolutely arbitrarily return func_utils.gaussian(f, f_center - 100, sigma) / 4 \ + func_utils.gaussian(f, f_center, sigma) \ + func_utils.gaussian(f, f_center + 100, sigma) / 4 return f_profile
def __init__(self, sample_rate=3*u.GHz, fch1=0*u.GHz, ascending=True, num_pols=2, t_start=0, seed=None, **kwargs): """ Initialize an Antenna object, which creates DataStreams for each polarization, under Antenna.x and Antenna.y (if there is a second polarization). Parameters ---------- sample_rate : float, optional Physical sample rate, in Hz, for collecting real voltage data fch1 : astropy.Quantity, optional Starting frequency of the first coarse channel, in Hz. If ascending=True, fch1 is the minimum frequency; if ascending=False (default), fch1 is the maximum frequency. ascending : bool, optional Specify whether frequencies should be in ascending or descending order. Default is True, for which fch1 is the minimum frequency. num_pols : int, optional Number of polarizations, can be 1 or 2 t_start : float, optional Start time, in seconds seed : int, optional Integer seed between 0 and 2**32. If None, the random number generator will use a random seed. """ self.rng = xp.random.RandomState(seed) self.sample_rate = unit_utils.get_value(sample_rate, u.Hz) self.dt = 1 / self.sample_rate self.fch1 = unit_utils.get_value(fch1, u.Hz) self.ascending = ascending assert num_pols in [1, 2] self.num_pols = num_pols self.t_start = t_start self.start_obs = True self.x = data_stream.DataStream(sample_rate=self.sample_rate, fch1=self.fch1, ascending=self.ascending, t_start=self.t_start, seed=int(self.rng.randint(2**32))) self.streams = [self.x] if self.num_pols == 2: self.y = data_stream.DataStream(sample_rate=self.sample_rate, fch1=self.fch1, ascending=self.ascending, t_start=self.t_start, seed=int(self.rng.randint(2**32))) self.streams.append(self.y) self.delay = None self.bg_cache = [None, None]
def __init__(self, num_antennas, sample_rate=3*u.GHz, fch1=0*u.GHz, ascending=True, num_pols=2, delays=None, t_start=0, seed=None, **kwargs): """ Initialize a MultiAntennaArray object, which creates a list of Antenna objects, each with a specified relative integer sample delay. Also creates background DataStreams to model coherent noise present in each Antenna, subject to that Antenna's delay. Parameters ---------- num_antennas : int Number of Antennas in the array sample_rate : float, optional Physical sample rate, in Hz, for collecting real voltage data fch1 : astropy.Quantity, optional Starting frequency of the first coarse channel, in Hz. If ascending=True, fch1 is the minimum frequency; if ascending=False (default), fch1 is the maximum frequency. ascending : bool, optional Specify whether frequencies should be in ascending or descending order. Default is True, for which fch1 is the minimum frequency. num_pols : int, optional Number of polarizations, can be 1 or 2 delays : array, optional Array of integers specifying relative delay offsets per array with respect to the coherent antenna array background. If None, uses 0 delay for all Antennas. t_start : float, optional Start time, in seconds seed : int, optional Integer seed between 0 and 2**32. If None, the random number generator will use a random seed. """ self.rng = xp.random.RandomState(seed) if delays is None: self.delays = xp.zeros(num_antennas) else: assert len(delays) == num_antennas self.delays = xp.array(delays).astype(int) self.max_delay = int(xp.max(delays)) self.num_antennas = num_antennas self.sample_rate = unit_utils.get_value(sample_rate, u.Hz) self.dt = 1 / self.sample_rate self.fch1 = unit_utils.get_value(fch1, u.Hz) self.ascending = ascending assert num_pols in [1, 2] self.num_pols = num_pols self.t_start = t_start self.start_obs = True self.antennas = [] for i in range(self.num_antennas): antenna = Antenna(sample_rate=self.sample_rate, fch1=self.fch1, ascending=self.ascending, num_pols=self.num_pols, t_start=self.t_start, seed=int(self.rng.randint(2**32))) antenna.delay = delays[i] self.antennas.append(antenna) # Create background data streams and link relevant antenna data streams for tracking noise self.bg_x = data_stream.BackgroundDataStream(sample_rate=self.sample_rate, fch1=self.fch1, ascending=self.ascending, t_start=self.t_start, seed=int(self.rng.randint(2**32)), antenna_streams=[antenna.x for antenna in self.antennas]) self.bg_streams = [self.bg_x] if self.num_pols == 2: self.bg_y = data_stream.BackgroundDataStream(sample_rate=self.sample_rate, fch1=self.fch1, ascending=self.ascending, t_start=self.t_start, seed=int(self.rng.randint(2**32)), antenna_streams=[antenna.y for antenna in self.antennas]) self.bg_streams.append(self.bg_y)
def periodic_gaussian_t_profile(pulse_width, period, phase=0, pulse_offset_width=0, pulse_direction='rand', pnum=3, amplitude=1, level=1, min_level=0): """ Intensity varying as Gaussian pulses, allowing for variation in the arrival time of each pulse. `period` and `phase` give a baseline for pulse periodicity. `pulse_direction` can be 'up', 'down', or 'rand', referring to whether the intensity increases or decreases from the baseline `level`. `amplitude` is the magnitude of each pulse. `min_level` is the minimum intensity, default is 0. `pulse_offset_width` encodes the variation in the pulse period, whereas `pulse_width` is the width of individual pulses. Both are modeled as Gaussians, where 'width' refers to the FWHM of the distribution. `pnum` is the number of Gaussians pulses to consider when calculating the intensity at each timestep. The higher this number, the more accurate the intensities. """ period = unit_utils.get_value(period, u.s) factor = 2 * np.sqrt(2 * np.log(2)) pulse_offset_sigma = unit_utils.get_value(pulse_offset_width, u.s) / factor pulse_sigma = unit_utils.get_value(pulse_width, u.s) / factor def t_profile(t): # This gives an array of length len(t) center_ks = np.round((t + phase) / period - 1 / 4.) # This conditional could be written in one line, but that obfuscates # the code. Here we determine which pulse centers need to be considered # for each time sample (e.g. the closest pnum pulses) temp = pnum // 2 if pnum % 2 == 1: center_ks = np.array( [center_ks + 1 * i for i in np.arange(-temp, temp + 1)]) else: center_ks = np.array( [center_ks + 1 * i for i in np.arange(-temp + 1, temp + 1)]) # Here center_ks.shape = (pnum, len(t)), of ints centers = (4. * center_ks + 1.) / 4. * period - phase # Calculate unique offsets per pulse and add to centers of Gaussians # Each element in unique_center_ks corresponds to a distinct (tracked) # pulse unique_center_ks = np.unique(center_ks) # Apply the pulse offset to each tracked pulse offset_dict = dict( zip( unique_center_ks, np.random.normal(0, pulse_offset_sigma, unique_center_ks.shape))) get_offsets = np.vectorize(lambda x: offset_dict[x]) # Calculate the signs for each pulse sign_list = [] for c in unique_center_ks: x = np.random.uniform(0, 1) if (pulse_direction == 'up' or pulse_direction == 'rand' and x < 0.5): sign_list.append(1) elif pulse_direction == 'down' or pulse_direction == 'rand': sign_list.append(-1) else: sys.exit('Invalid pulse direction!') sign_dict = dict(zip(unique_center_ks, sign_list)) get_signs = np.vectorize(lambda x: sign_dict[x]) # Apply the previously computed variations and total to compute # intensities centers += get_offsets(center_ks) center_signs = zip(centers, get_signs(center_ks)) intensity = 0 for c, sign in center_signs: intensity += sign * amplitude * func_utils.gaussian( t, c, pulse_sigma) intensity += level return np.maximum(min_level, intensity) return t_profile