Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
    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 = []
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
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
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
    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]
Ejemplo n.º 16
0
    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)
Ejemplo n.º 17
0
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