def __init__(self, stage_num, input_rate, dm_rate, filter_taps): """ Create a decimation stage with given decimation rate, input sample rate, and filter taps. :param stage_num: the index of this filter/decimate stage to all stages (beginning with stage 0) :param input_rate: the input sampling rate, in Hz. :param dm_rate: the decimation rate. Must be an integer. :param filter_taps: a list of filter taps (numeric) to be convolved with the data before the decimation is done. :raises ExperimentException: if types are not correct for signal processing module to use """ self.stage_num = stage_num self.input_rate = input_rate if not isinstance(dm_rate, int): raise ExperimentException('Decimation rate is not an integer') self.output_rate = input_rate/dm_rate self.dm_rate = dm_rate if not isinstance(filter_taps, list): errmsg = 'Filter taps {} of type {} must be a list in decimation stage {}'.format(filter_taps, type(filter_taps), stage_num) raise ExperimentException(errmsg) for x in filter_taps: if not isinstance(x, (int, float)): # TODO should complex be included here? errmsg = 'Filter tap {} is not numeric in decimation stage {}'.format(x, stage_num) raise ExperimentException(errmsg) self.filter_taps = filter_taps
def __init__(self, ave_keys, ave_slice_dict, ave_interface, transmit_metadata, slice_to_beamorder_dict, slice_to_beamdir_dict): ScanClassBase.__init__(self, ave_keys, ave_slice_dict, ave_interface, transmit_metadata) self.slice_to_beamorder = slice_to_beamorder_dict self.slice_to_beamdir = slice_to_beamdir_dict # Metadata for an AveragingPeriod: clear frequency search, integration time, number of averages goal self.clrfrqflag = False # there may be multiple slices in this averaging period at different frequencies so # we may have to search multiple ranges. self.clrfrqrange = [] for slice_id in self.slice_ids: if self.slice_dict[slice_id]['clrfrqflag']: self.clrfrqflag = True self.clrfrqrange.append(self.slice_dict[slice_id]['clrfrqrange']) # TODO: SET UP CLEAR FREQUENCY SEARCH CAPABILITY # also note for when setting this up clrfrqranges may overlap self.intt = self.slice_dict[self.slice_ids[0]]['intt'] self.intn = self.slice_dict[self.slice_ids[0]]['intn'] if self.intt is not None: # intt has priority over intn for slice_id in self.slice_ids: if self.slice_dict[slice_id]['intt'] != self.intt: errmsg = "Slices {} and {} are INTEGRATION or PULSE interfaced and do not have the" \ " same Averaging Period duration intt".format(self.slice_ids[0], slice_id) raise ExperimentException(errmsg) elif self.intn is not None: for slice_id in self.slice_ids: if self.slice_dict[slice_id]['intn'] != self.intn: errmsg = "Slices {} and {} are INTEGRATION or PULSE interfaced and do not have the" \ " same NAVE goal intn".format(self.slice_ids[0], slice_id) raise ExperimentException(errmsg) for slice_id in self.slice_ids: if len(self.slice_dict[slice_id]['beam_order']) != len(self.slice_dict[self.slice_ids[0]]['beam_order']): errmsg = "Slices {} and {} are INTEGRATION or PULSE interfaced but do not have the" \ " same number of integrations in their beam order" \ .format(self.slice_ids[0], slice_id) raise ExperimentException(errmsg) self.num_beams_in_scan = len(self.slice_dict[self.slice_ids[0]]['beam_order']) # NOTE: Do not need beam information inside the AveragingPeriod, this is in Scan. # Determine how this averaging period is made by separating out the INTEGRATION interfaced. self.nested_slice_list = self.get_sequence_slice_ids() self.sequences = [] for params in self.prep_for_nested_scan_class(): self.sequences.append(Sequence(*params)) self.one_pulse_only = False self.beam_iter = 0 # used to keep track of place in beam order.
def retrieve_experiment(experiment_module_name): """ Retrieve the experiment class from the provided module given as an argument. :param experiment_module_name: The name of the experiment module to run from the Borealis project's experiments directory. :raise ExperimentException: if the experiment module provided as an argument does not contain a single class that inherits from ExperimentPrototype class. :returns: Experiment, the experiment class, inherited from ExperimentPrototype. """ if __debug__: printing("Running the experiment: " + experiment_module_name) experiment_mod = importlib.import_module("experiments." + experiment_module_name) # find the class or classes *defined* in this module. # returns list of class name and object experiment_classes = [ (m[0], m[1]) for m in inspect.getmembers(experiment_mod, inspect.isclass) if m[1].__module__ == experiment_mod.__name__ ] # remove any classes that do not have ExperimentPrototype as parent. for (class_name, class_obj) in experiment_classes: if ExperimentPrototype not in inspect.getmro(class_obj): # an experiment must inherit from ExperimentPrototype # other utility classes might be in the file but we will ignore them. experiment_classes.remove((class_name, class_obj)) # experiment_classes should now only have classes *defined* in the module, # that have ExperimentPrototype as parent. if len(experiment_classes) == 0: errmsg = "No experiment classes are present that are built from"\ " parent class ExperimentPrototype - exiting" raise ExperimentException(errmsg) if len(experiment_classes) > 1: errmsg = "You have more than one experiment class in your " \ "experiment file - exiting" raise ExperimentException(errmsg) # this is the experiment class that we need to run. Experiment = experiment_classes[0][1] printing('Retrieving experiment: {} from module {}'.format( experiment_classes[0][0], experiment_mod)) return Experiment
def search_for_experiment(radar_control_to_exp_handler, exphan_to_radctrl_iden, status): """ Check for new experiments from the experiment handler :param radar_control_to_exp_handler: :param radctrl_to_exphan_iden: The :param status: status string (EXP_NEEDED or NO_ERROR). :returns new_experiment_received: boolean (True for new experiment received) :returns experiment: experiment instance (or None if there is no new experiment) """ try: socket_operations.send_request(radar_control_to_exp_handler, exphan_to_radctrl_iden, status) except zmq.ZMQBaseError as e: errmsg = "ZMQ ERROR" raise [ExperimentException(errmsg), e] experiment = None new_experiment_received = False try: serialized_exp = socket_operations.recv_exp( radar_control_to_exp_handler, exphan_to_radctrl_iden, rad_ctrl_print) except zmq.ZMQBaseError as e: errmsg = "ZMQ ERROR" raise [ExperimentException(errmsg), e] new_exp = pickle.loads(serialized_exp) # protocol detected automatically if isinstance(new_exp, ExperimentPrototype): experiment = new_exp new_experiment_received = True if __debug__: rad_ctrl_print("NEW EXPERIMENT FOUND") elif new_exp is not None: if __debug__: rad_ctrl_print( "RECEIVED AN EXPERIMENT NOT OF TYPE EXPERIMENT_PROTOTYPE. CANNOT RUN." ) else: if __debug__: rad_ctrl_print("The experiment was not updated - continuing.") # TODO decide what to do here. I think we need this case if someone doesn't build their experiment # properly return new_experiment_received, experiment
def set_beamdirdict(self, beamiter): """ Get a dictionary of 'slice_id' : 'beamdir(s)' for this averaging period. At a given beam iteration, this averagingperiod instance will select the beam directions that it will shift to. :param beamiter: the index into the beam_order list, or the index of an averaging period into the scan :returns: dictionary of slice to beamdir where beamdir is always a list (may be of length one though). Beamdir is azimuth angle. """ slice_to_beamdir_dict = {} try: for slice_id in self.slice_ids: beam_number = self.slice_to_beamorder[slice_id][beamiter] if isinstance(beam_number, int): beamdir = [] beamdir.append(self.slice_to_beamdir[slice_id][beam_number]) else: # is a list beamdir = [self.slice_to_beamdir[slice_id][bmnum] for bmnum in beam_number] slice_to_beamdir_dict[slice_id] = beamdir except IndexError: errmsg = 'Looking for BeamNumber or Beamdir that does not Exist at BeamIter' \ ' {}'.format(beamiter) raise ExperimentException(errmsg) return slice_to_beamdir_dict
def get_wavetables(wavetype): """ Find the wavetable to sample from for a given wavetype. If there are ever any other types of wavetypes besides 'SINE', set them up here. NOTE: The wavetables should sample a single cycle of the waveform. Note that we will have to block frequencies that could interfere with our license, which will affect the waveform. This blocking of frequencies is not currently set up, so beware. Would have to get the spectrum of the wavetable waveform and then block frequencies that when mixed with the centre frequency, result in the restricted frequencies. Also NOTE: wavetables create a fixed frequency resolution based on their length. This code is from get_samples: f_norm = wave_freq / rate sample_skip = int(f_norm * wave_table_len) # THIS MUST BE AN INT, WHICH DEFINES THE FREQUENCY RESOLUTION. actual_wave_freq = (float(sample_skip) / float(wave_table_len)) * rate :param wavetype: A string descriptor of the wavetype. :returns iwavetable: an in-phase wavetable, or None if given 'SINE' wavetype. :returns qwavetable: a quadrature wavetable, or None if given 'SINE' wavetype. """ # TODO : See docstring above. if wavetype == "SINE": iwave_table = None qwave_table = None else: iwave_table = [] qwave_table = [] errmsg = "Wavetype {} not defined".format(wavetype) raise ExperimentException(errmsg) # Example of a wavetable is below, if they were defined for SINE wavetypes. # wave_table_len=8192 # for i in range(0, wave_table_len): # iwave_table.append(math.cos(i*2*math.pi/wave_table_len)) # qwave_table.append(math.sin(i*2*math.pi/wave_table_len)) return iwave_table, qwave_table
def prep_for_nested_scan_class(self): """ Retrieve the params needed for the nested class (also with base ScanClassBase). This class reduces duplicate code by breaking down the ScanClassBase class into the separate portions for the nested instances. For Scan class, the nested class is AveragingPeriod, and we will need to break down the parameters given to the Scan instance because there may be multiple AveragingPeriods within. For AveragingPeriod, the nested class is Sequence. :returns: params for the nested class's instantiation. """ # TODO documentation make a detailed example of this and diagram nested_class_param_lists = [] if __debug__: print(self.nested_slice_list) for slice_list in self.nested_slice_list: slices_for_nested_class = {} for slice_id in slice_list: try: slices_for_nested_class[slice_id] = self.slice_dict[ slice_id] except KeyError: errmsg = 'Error with slice list - slice id {} cannot be found.'.format( slice_id) raise ExperimentException(errmsg) # now take a subset of the interface dictionary that applies to this nested object # of scan_class_base type. nested_class_interface = {} for i in itertools.combinations(slice_list, 2): # slice_list is sorted so we should have the following effect: # combinations([1, 3, 5], 2) --> [1,3], [1,5], [3,5] nested_class_interface[tuple(i)] = self.interface[tuple(i)] nested_class_param_lists.append([ slice_list, slices_for_nested_class, nested_class_interface, self.transmit_metadata ]) return nested_class_param_lists
def __init__(self, rxrate, output_sample_rate, stages=None): """ Set up the decimation scheme for the experiment. :param rxrate: sampling rate of USRP, in Hz. :param output_sample_rate: desired output rate of the data, to decimate to, in Hz. :param stages: a list of DecimationStages, or None, if they will be set up as default here. """ if stages is None: # create the default filters according to default scheme. # Currently only creating default filters if sampling rate and output rate are set # up as per original design. TODO: make this more general. if rxrate != 5.0e6 or round(output_sample_rate, 0) != 3.333e3: errmsg = 'Default filters not defined for rxrate {} and output rate {}'.format( rxrate, output_sample_rate) raise ExperimentException(errmsg) # set up defaults as per design. return(create_default_scheme()) else: options = ExperimentOptions() self.rxrate = rxrate self.output_sample_rate = output_sample_rate # check that number of stages is correct if len(stages) > options.max_number_of_filtering_stages: errmsg = 'Number of decimation stages ({}) is greater than max available {}' \ ''.format(len(stages), options.max_number_of_filtering_stages) raise ExperimentException(errmsg) self.dm_rates = [] self.output_rates = [] self.input_rates = [] self.filter_scaling_factors = [] self.stages = stages for dec_stage in self.stages: self.dm_rates.append(dec_stage.dm_rate) self.output_rates.append(dec_stage.output_rate) self.input_rates.append(dec_stage.input_rate) filter_scaling_factor = sum(dec_stage.filter_taps) self.filter_scaling_factors.append(filter_scaling_factor) # check rates are appropriate given rxrate and output_sample_rate, and # check sequentiality of stages, ie output rate transfers to input rate of next stage. if self.input_rates[0] != self.rxrate: errmsg = 'Decimation stage 0 does not have input rate {} equal to USRP sampling ' \ 'rate {}'.format(self.input_rates[0], self.rxrate) raise ExperimentException(errmsg) for stage_num in range(0, len(stages) -1): if not math.isclose(self.output_rates[stage_num], self.input_rates[stage_num + 1], abs_tol=0.001): errmsg = 'Decimation stage {} output rate {} does not equal next stage {} ' \ 'input rate {}'.format(stage_num, self.output_rates[stage_num], stage_num + 1, self.input_rates[stage_num + 1]) raise ExperimentException(errmsg) if self.output_rates[-1] != self.output_sample_rate: errmsg = 'Last decimation stage {} does not have output rate {} equal to ' \ 'requested output data rate {}'.format(len(stages) - 1, self.output_rates[-1], self.output_sample_rate) raise ExperimentException(errmsg)
def create_uncombined_pulses(pulse_list, power_divider, exp_slices, beamdir, txrate, txctrfreq, main_antenna_count, main_antenna_spacing, pulse_ramp_time, max_usrp_dac_amplitude): """ Create the samples for a given pulse_list and append those samples to the pulse_list. Creates a list of numpy arrays where each numpy array is the pulse samples for a given pulse and a given transmit antenna (index of array in list provides antenna number). Adds the list of samples to the pulse dictionary (in the pulse_list list) under the key 'samples'. If the antenna is listed in the config but is not used in the sequence, it is provided an array of zeroes to transmit. :param pulse_list: a list of dictionaries, each dict is a pulse. The list includes all pulses that will be combined together. All dictionaries in this list (all 'pulses') will be modified to include the 'samples' key which will be a list of arrays where every array is a set of samples for a specific antenna. :param power_divider: an integer for number of pulses combined (max) in the whole sequence, so we can adjust the amplitude of each uncombined pulse accordingly. :param exp_slices: slice dictionary containing all necessary slice_ids for this pulse. :param beamdir: the slice to beamdir dictionary to retrieve the phasing information for each antenna in a certain slice's pulses. :param txrate: transmit sampling rate, in Hz. :param txctrfreq: transmit mixing frequency, in kHz. :param main_antenna_count: number of main antennas in the array to transmit. :param main_antenna_spacing: spacing between main array antennas, assumed uniform. :param pulse_ramp_time: time to ramp up the pulse at the start and end of the pulse. This time counts as part of the total pulse length time (in seconds). :param max_usrp_dac_amplitude: max voltage out of the digital-analog converter on the USRP """ for pulse in pulse_list: # print exp_slices[pulse['slice_id']] if not exp_slices[pulse['slice_id']]['rxonly']: wave_freq = float(exp_slices[pulse['slice_id']] ['txfreq']) - txctrfreq # TODO error will # occur here if clrfrqrange because clrfrq search # isn't completed yet. (when clrfrq, no txfreq) phase_array = [] pulse['samples'] = [] if len( beamdir[pulse['slice_id']] ) > 1: # todo move this somwhere for each slice_id, not pulse as unnecessary repetition # we have imaging. We need to figure out the direction and amplitude to give # each antenna beamdirs_for_antennas, amps_for_antennas = \ resolve_imaging_directions(beamdir[pulse['slice_id']], main_antenna_count, main_antenna_spacing) else: # not imaging, all antennas transmitting same direction. beamdirs_for_antennas = [ beamdir[pulse['slice_id']][0] for ant in range(0, main_antenna_count) ] amps_for_antennas = [ 1.0 for ant in range(0, main_antenna_count) ] amplitude_list = [ amplitude / float(power_divider) for amplitude in amps_for_antennas ] # also adjust amplitudes for number of pulses transmitted at once. # TODO : review this as for antenna in range(0, main_antenna_count): # Get phase shifts for all channels off centre of array being phase = 0. phase_for_antenna = \ get_phshift(beamdirs_for_antennas[antenna], exp_slices[pulse['slice_id']]['txfreq'], antenna, exp_slices[pulse['slice_id']]['pulse_phase_offset'][pulse['slice_pulse_index']], main_antenna_count, main_antenna_spacing) phase_array.append(phase_for_antenna) else: # rxonly operation. pulse['samples'] = [] amplitude_list = [0.0 for ant in range(0, main_antenna_count)] wave_freq = float( exp_slices[pulse['slice_id']]['rxfreq']) - txctrfreq phase_array = [0.0 for ant in range(0, main_antenna_count)] wave_freq_hz = wave_freq * 1000 # Create samples for this frequency at this rate. Convert pulse_len to seconds and # wave_freq to Hz. basic_samples, real_freq = get_samples( txrate, wave_freq_hz, float(pulse['pulse_len']) / 1000000, pulse_ramp_time, max_usrp_dac_amplitude, exp_slices[pulse['slice_id']]['iwavetable'], exp_slices[pulse['slice_id']]['qwavetable']) if real_freq != wave_freq_hz: errmsg = 'Actual Frequency {} is Not Equal to Intended Wave Freq {}'.format( real_freq, wave_freq_hz) raise ExperimentException( errmsg) # TODO change to warning? only happens on non-SINE for antenna in range(0, main_antenna_count): if antenna in exp_slices[pulse['slice_id']]['tx_antennas']: pulse_samples = shift_samples(basic_samples, phase_array[antenna], amplitude_list[antenna]) pulse['samples'].append(pulse_samples) # pulse['samples'] is a list of numpy arrays now. else: pulse_samples = np.zeros([len(basic_samples)], dtype=np.complex64) pulse['samples'].append(pulse_samples)
def make_pulse_samples(pulse_list, power_divider, exp_slices, slice_to_beamdir_dict, txrate, txctrfreq, main_antenna_count, main_antenna_spacing, pulse_ramp_time, max_usrp_dac_amplitude, tr_window_time): """ Make all necessary samples for all antennas for this pulse. Given a pulse_list (list of dictionaries of pulses that must be combined), make and phase shift samples for all antennas, and combine pulse dictionaries into one pulse if there are multiple waveforms to combine (e.g., multiple frequencies). :param pulse_list: a list of dictionaries, each dict is a pulse. The list only contains pulses that will be sent as a single pulse (ie. have the same combined_pulse_index). :param power_divider: an integer for number of pulses combined (max) in the whole sequence, so we can adjust the amplitude of each uncombined pulse accordingly. :param exp_slices: this is the slice dictionary containing the slices necessary for the sequence. :param slice_to_beamdir_dict: a dictionary describing the beam directions for the slice_ids. :param txrate: transmit sampling rate, in Hz. :param txctrfreq: transmit mixing frequency, in kHz. :param main_antenna_count: number of main antennas in the array to transmit. :param main_antenna_spacing: spacing between main array antennas, assumed uniform. :param pulse_ramp_time: time to ramp up the pulse at the start and end of the pulse. This time counts as part of the total pulse length time (in seconds). :param max_usrp_dac_amplitude: max voltage out of the digital-analog converter on the USRP :param tr_window_time: time in seconds to add zero-samples to the transmit waveform in order to count for the transmit/receive switching time. Windows the pulse on both sides. :returns combined_samples: a list of arrays - each array corresponds to an antenna (the samples are phased). All arrays are the same length for a single pulse on that antenna. The length of the list is equal to main_antenna_count (all samples are calculated). If we are not using an antenna, that index is a numpy array of zeroes. :returns pulse_channels: The antennas to actually send the corresponding array. If not all transmit antennas, then we will know that we are transmitting zeroes on any antennas not listed in this list but available as identified in the config file. """ for pulse in pulse_list: try: assert pulse['combined_pulse_index'] == pulse_list[0][ 'combined_pulse_index'] assert pulse['pulse_timing_us'] == pulse_list[0]['pulse_timing_us'] except AssertionError: errmsg = 'Error building samples from pulse dictionaries' raise ExperimentException(errmsg, pulse, pulse_list[0]) # make the uncombined pulses create_uncombined_pulses(pulse_list, power_divider, exp_slices, slice_to_beamdir_dict, txrate, txctrfreq, main_antenna_count, main_antenna_spacing, pulse_ramp_time, max_usrp_dac_amplitude) # all pulse dictionaries in the pulse_list now have a 'samples' key which is a list of numpy # complex arrays (one for each possible tx antenna). #print type(pulse_list[0]), type(pulse_list[0]['samples']), type(pulse_list[0]['samples'][0]) #plot_samples("samples.png", pulse_list[0]['samples'][0]) # determine how long the combined pulse will be in number of samples, and add the key # 'sample_number_start' for all pulses in the pulse_list. combined_pulse_length = calculated_combined_pulse_samples_length( pulse_list, txrate) # Now we have total length so make all pulse samples same length # before combining them sample by sample. for pulse in pulse_list: # print start_samples for antenna in range(0, main_antenna_count): pulse_array = pulse['samples'][antenna] # print(combined_pulse_length, len(pulse_array), pulse['sample_number_start']) zeros_prepend = np.zeros(pulse['sample_number_start'], dtype=np.complex64) zeros_append = np.zeros((combined_pulse_length - len(pulse_array) - pulse['sample_number_start']), dtype=np.complex64) corrected_pulse_array = np.concatenate( (zeros_prepend, pulse_array, zeros_append)) pulse['samples'][antenna] = corrected_pulse_array # Sub in new array of right length for old array. # initialize to correct length combined_samples = [ np.zeros(combined_pulse_length, dtype=np.complex64) for ant in range(0, main_antenna_count) ] # This is a list of arrays (one for each antenna) with the combined # samples in it (which will be transmitted). Need to add together multiple pulses if there # are multiple frequencies, for example. for antenna in range(0, main_antenna_count): for pulse in pulse_list: try: combined_samples[antenna] += pulse['samples'][antenna] except RuntimeWarning: raise ExperimentException("RUNTIMEWARNING {}".format( len(combined_samples[antenna]))) # TODO determine if we can manage this overflow error to prevent this. tr_window_num_samps = int(math.ceil(tr_window_time * txrate)) tr_window_samples = np.zeros(tr_window_num_samps, dtype=np.complex64) combined_samples_tr = [] for cs in combined_samples: combined_samples_channel = np.concatenate( (tr_window_samples, cs, tr_window_samples)) combined_samples_tr.append(combined_samples_channel) # Now get what channels we need to transmit on for this combined # pulse. pulse_channels = [] for pulse in pulse_list: for ant in exp_slices[pulse['slice_id']]['tx_antennas']: if ant not in pulse_channels: pulse_channels.append(ant) pulse_channels.sort() return combined_samples_tr, pulse_channels
def get_samples(rate, wave_freq, pulse_len, ramp_time, max_amplitude, iwave_table=None, qwave_table=None): """ Get basic (not phase-shifted) samples for a given pulse. Find the normalized sample array given the rate (Hz), frequency (Hz), pulse length (s), and wavetables (list containing single cycle of waveform). Will shift for beam later. No need to use wavetable if just a sine wave. :param rate: tx sampling rate, in Hz. :param wave_freq: frequency offset from the centre frequency on the USRP, given in Hz. To be mixed with the centre frequency before transmitting. (ex. centre = 12 MHz, wave_freq = + 1.2 MHz, output = 13.2 MHz. :param pulse_len: length of the pulse (in seconds) :param ramp_time: ramp up and ramp down time for the pulse, in seconds. Typical 0.00001 s from config. :param max_amplitude: USRP's max DAC amplitude. N200 = 0.707 max :param iwave_table: i samples (in-phase) wavetable if a wavetable is required (ie. not a sine wave to be sampled) :param qwave_table: q samples (quadrature) wavetable if a wavetable is required (ie. not a sine wave to be sampled) :returns samples: a numpy array of complex samples, representing all samples needed for a pulse of length pulse_len sampled at a rate of rate. :returns actual_wave_freq: the frequency possible given the wavetable. If wavetype != 'SINE' (i.e. calculated wavetables were used), then actual_wave_freq may not be equal to the requested wave_freq param. """ wave_freq = float(wave_freq) rate = float(rate) if iwave_table is None and qwave_table is None: sampling_freq = 2 * math.pi * wave_freq / rate # for linear we used the below: linear_rampsampleslen = int( rate * ramp_time) # number of samples for ramp-up and ramp-down of pulse. sampleslen = int(rate * pulse_len) rads = sampling_freq * np.arange(0, sampleslen) wave_form = np.exp(rads * 1j) amplitude_ramp_up = [ ind * max_amplitude / linear_rampsampleslen for ind in np.arange(0, linear_rampsampleslen) ] amplitude_ramp_down = np.flipud(amplitude_ramp_up) # reverse amplitude = [ max_amplitude for ind in np.arange(linear_rampsampleslen, sampleslen - linear_rampsampleslen) ] linear_amps = np.concatenate( (amplitude_ramp_up, amplitude, amplitude_ramp_down)) samples = [x * y for x, y in zip(wave_form, linear_amps)] #gaussian_amps = max_amplitude * np.ones([sampleslen]) * gaussian(sampleslen, math.ceil(pulse_len/6.0)) # TODO modify ramp_time input to this function because going Gaussian (after # ... TODO: testing this) #samples = [x * y for x, y in zip(wave_form, gaussian_amps)] samples = np.array(samples) actual_wave_freq = wave_freq elif iwave_table is not None and qwave_table is not None: wave_table_len = len(iwave_table) # TODO turn this into Gaussian ramp-up not linear!! rampsampleslen = int(rate * ramp_time) # Number of samples in ramp-up, ramp-down sampleslen = int(rate * pulse_len + 2 * rampsampleslen) samples = np.empty([sampleslen], dtype=np.complex64) # sample at wave_freq with given phase shift f_norm = wave_freq / rate sample_skip = int(f_norm * wave_table_len) # This must be an int to create perfect sine, and # this int defines the frequency resolution of our generated # waveform actual_wave_freq = (float(sample_skip) / float(wave_table_len)) * rate # This is the actual frequency given the sample_skip for i in range(0, rampsampleslen): amp = max_amplitude * float(i + 1) / float( rampsampleslen) # rampup is linear if sample_skip < 0: ind = -1 * ((abs(sample_skip * i)) % wave_table_len) else: ind = (sample_skip * i) % wave_table_len samples[i] = (amp * iwave_table[ind] + amp * qwave_table[ind] * 1j) # qsamples[chi,i]=amp*qwave_table[ind] for i in range(rampsampleslen, sampleslen - rampsampleslen): amp = max_amplitude if sample_skip < 0: ind = -1 * ((abs(sample_skip * i)) % wave_table_len) else: ind = (sample_skip * i) % wave_table_len samples[i] = (amp * iwave_table[ind] + amp * qwave_table[ind] * 1j) # qsamples[chi,i]=qwave_table[ind] for i in range(sampleslen - rampsampleslen, sampleslen): amp = max_amplitude * float(sampleslen - i) / float(rampsampleslen) if sample_skip < 0: ind = -1 * ((abs(sample_skip * i)) % wave_table_len) else: ind = (sample_skip * i) % wave_table_len samples[i] = (amp * iwave_table[ind] + amp * qwave_table[ind] * 1j) # qsamples[chi,i]=amp*qwave_table[ind] else: errmsg = "Error: only one wavetable passed" raise ExperimentException(errmsg) # Samples is an array of complex samples # NOTE: phasing will be done in shift_samples function return samples, actual_wave_freq
def slice_combos_sorter(list_of_combos, all_keys): """ Sort keys of a list of combinations so that keys only appear once in the list. This function modifes the input list_of_combos so that all slices that are associated are associated in the same list. For example, if input is list_of_combos = [[0,1], [0,2], [0,4], [1,4], [2,4]] and all_keys = [0,1,2,4,5] then the output should be [[0,1,2,4], [5]]. This is used to get the slice dictionary for nested class instances. In the above example, we would then have two instances of the nested class to create: one with slices 0,1,2,4 and another with slice 5. :param list_of_combos: list of lists of length two associating two slices together. :param all_keys: list of all keys included in this object (scan, ave_period, or sequence). :return: list of combos that is sorted so that each key only appears once and the lists within the list are of however long necessary """ list_of_combos = sorted(list_of_combos) # if [2,4] and [1,4], then also must be [1,2] in the list_of_combos # Now we are going to modify the list of lists of length = 2 to be a list of length x so that if [1,2] and [2,4] # and [1,4] are in list_of_combos, we want only one list element for this scan : [1,2,4] . scan_i = 0 # TODO detailed explanation with examples. Consider changing to a graph traversal algorithm? while scan_i < len( list_of_combos ): # i: element in list_of_combos (representing one scan) slice_id_k = 0 while slice_id_k < len( list_of_combos[scan_i] ): # k: element in scan (representing a slice) scan_j = scan_i + 1 # j: iterates through the other elements of list_of_combos, to combine them into # the first, i, if they are in fact part of the same scan. while scan_j < len(list_of_combos): if list_of_combos[scan_i][slice_id_k] == list_of_combos[ scan_j][0]: # if an element (slice_id) inside # the i scan is the same as a slice_id in the j scan (somewhere further in the list_of_combos), # then we need to combine that j scan into the i scan. We only need to check the first element # of the j scan because list_of_combos has been sorted and we know the first slice_id in the scan # is less than the second slice id. add_n_slice_id = list_of_combos[scan_j][ 1] # the slice_id to add to the i scan from the j scan. list_of_combos[scan_i].append(add_n_slice_id) # Combine the indices if there are 3+ slices combining in same scan for m in range( 0, len(list_of_combos[scan_i]) - 1): # if we have added z to scan_i, such that # scan_i is now [x,y,z], we now have to remove from the list_of_combos list [x,z], and [y,z]. # If x,z existed as SCAN but y,z did not, we have an error. # Try all values in list_of_combos[i] except the last value, which is = to add_n. try: list_of_combos.remove([ list_of_combos[scan_i][m], add_n_slice_id ]) # list_of_combos[j][1] is the known last value in list_of_combos[i] except ValueError: # This error would occur if e.g. you had set [x,y] and [x,z] to PULSE but [y,z] to # SCAN. This means that we couldn't remove the scan_combo y,z from the list because it # was not added to list_of_combos because it wasn't a scan type, so the interfacing would # not make sense (conflict). errmsg = 'Interfacing not Valid: exp_slice {} and exp_slice {} are combined in-scan and do not \ interface the same with exp_slice {}'.format( list_of_combos[scan_i][m], list_of_combos[scan_i][slice_id_k], add_n_slice_id) raise ExperimentException(errmsg) scan_j = scan_j - 1 # This means that the former list_of_combos[j] has been deleted and there are new values at # index j, so decrement before incrementing in while. # The above for loop will delete more than one element of list_of_combos (min 2) but the # while scan_j < len(list_of_combos) will reevaluate the length of list_of_combos. scan_j = scan_j + 1 slice_id_k = slice_id_k + 1 # if interfacing has been properly set up, the loop will only ever find # elements to add to scan_i when slice_id_k = 0. If there were errors though (ex. x,y and y,z = PULSE # but x,z did not) then iterating through the slice_id elements will allow us to find the # error. scan_i = scan_i + 1 # At this point, all elements in the just-finished scan_i will not be found anywhere # else in list_of_combos. # Now list_of_combos is a list of lists, where a slice_id occurs only once, within the nested list. for slice_id in all_keys: for combo in list_of_combos: if slice_id in combo: break else: # no break list_of_combos.append([slice_id]) # Append the slice on its own, it is in its own object. list_of_combos = sorted(list_of_combos) return list_of_combos
def radar(): """ Run the radar with the experiment supplied by experiment_handler. Receives an instance of an experiment. Iterates through the Scans, AveragingPeriods, Sequences, and pulses of the experiment. For every pulse, samples and other control information are sent to the n200_driver. For every pulse sequence, processing information is sent to the signal processing block. After every integration time (AveragingPeriod), the experiment block is given the opportunity to change the experiment (not currently implemented). If a new experiment is sent, radar will halt the old one and begin with the new experiment. """ # Initialize driverpacket. driverpacket = DriverPacket() sigprocpacket = SigProcPacket() integration_time_packet = IntegrationTimeMetadata() # Get config options. options = ExperimentOptions() # The socket identities for radar_control, retrieved from options ids = [ options.radctrl_to_exphan_identity, options.radctrl_to_dsp_identity, options.radctrl_to_driver_identity, options.radctrl_to_brian_identity, options.radctrl_to_dw_identity ] # Setup sockets. # Socket to send pulse samples over. # TODO test: need to make sure that we know that all sockets are set up after this try...except block. # TODO test: starting the programs in different orders. try: sockets_list = socket_operations.create_sockets( ids, options.router_address) except zmq.ZMQBaseError as e: errmsg = "ZMQ ERROR Setting up sockets" raise [ExperimentException(errmsg), e] radar_control_to_exp_handler = sockets_list[0] radar_control_to_dsp = sockets_list[1] radar_control_to_driver = sockets_list[2] radar_control_to_brian = sockets_list[3] radar_control_to_dw = sockets_list[4] # seqnum is used as a identifier in all packets while # radar is running so set it up here. # seqnum will get increased by num_sequences (number of averages or sequences in the integration period) # at the end of every integration time. seqnum_start = 0 # Wait for experiment handler at the start until we have an experiment to run. new_experiment_waiting = False while not new_experiment_waiting: new_experiment_waiting, experiment = search_for_experiment( radar_control_to_exp_handler, options.exphan_to_radctrl_identity, 'EXPNEEDED') new_experiment_waiting = False new_experiment_loaded = True # Send driver initial setup data - rates and center frequency from experiment. # Wait for acknowledgment that USRP object is set up. setup_driver(driverpacket, radar_control_to_driver, options.driver_to_radctrl_identity, experiment.txctrfreq, experiment.rxctrfreq, experiment.txrate, experiment.rxrate) first_integration = True next_scan_start = None decimation_scheme = experiment.decimation_scheme while True: # This loops through all scans in an experiment, or restarts this loop if a new experiment occurs. # TODO : further documentation throughout in comments (high level) and in separate documentation. # Iterate through Scans, AveragingPeriods, Sequences, Pulses. if new_experiment_waiting: # start anew on first scan if we have a new experiment. try: experiment = new_experiment except NameError: # new_experiment does not exist, should never happen as flag only gets set when # there is a new experiment. errmsg = 'Experiment could not be found' raise ExperimentException(errmsg) new_experiment_waiting = False new_experiment = None new_experiment_loaded = True for scan_num, scan in enumerate(experiment.scan_objects): if __debug__: rad_ctrl_print("Scan number: {}".format(scan_num)) # scan iter is the iterator through the scanbound or through the number of averaging periods in the scan. scan_iter = 0 # if a new experiment was received during the last scan, it finished the integration period it was on and # returned here with new_experiment_waiting set to True. Break to load new experiment. if new_experiment_waiting: # start anew on first scan if we have a new experiment. break if scan.scanbound: if scan.align_scan_to_beamorder: for aveperiod in scan.aveperiods: aveperiod.beam_iter = 0 # always align first beam at start of scan # find the start of the next scan with a scanbound so we can # determine time remaining for end of scan next_scanbound = None next_scan_num = scan_num while next_scanbound is None: next_scan_num += 1 if next_scan_num == len(experiment.scan_objects): next_scan_num = 0 next_scanbound = experiment.scan_objects[ next_scan_num].scanbound if first_integration: # on the very first integration of Borealis starting, calculate the start minute # align scanbound reference time to find when to start now = datetime.utcnow() dt = now.replace(second=0, microsecond=0) if dt + timedelta( seconds=scan.scanbound[scan_iter]) >= now: start_minute = dt else: start_minute = round_up_time(now) else: # At the start of a scan object that has scanbound, recalculate the start # minute to the previously calculated next_scan_start start_minute = next_scan_start.replace(second=0, microsecond=0) # find the modulus of the number of aveperiod times to run in the scan and the number of AvePeriod classes. # the classes will be alternated so we can determine which class will be running at the end of the scan. index_of_last_aveperiod_in_scan = ( scan.num_aveperiods_in_scan + scan.aveperiod_iter) % len( scan.aveperiods) last_aveperiod_intt = scan.aveperiods[ index_of_last_aveperiod_in_scan].intt # a scanbound necessitates intt end_of_scan = start_minute + timedelta( seconds=scan.scanbound[-1]) + timedelta( seconds=last_aveperiod_intt * 1e-3) end_minute = end_of_scan.replace(second=0, microsecond=0) if end_minute + timedelta( seconds=next_scanbound[0]) >= end_of_scan: next_scan_start = end_minute + timedelta( seconds=next_scanbound[0]) else: next_scan_start = round_up_time(end_of_scan) + timedelta( seconds=next_scanbound[0]) while scan_iter < scan.num_aveperiods_in_scan and not new_experiment_waiting: # If there are multiple aveperiods in a scan they are alternated (INTTIME interfaced) aveperiod = scan.aveperiods[scan.aveperiod_iter] if TIME_PROFILE: time_start_of_aveperiod = datetime.utcnow() # get new experiment here, before starting a new integration. # If new_experiment_waiting is set here, implement new_experiment after this # integration period. There may be a new experiment waiting, or a new experiment. if not new_experiment_waiting and not new_experiment_loaded: new_experiment_waiting, new_experiment = search_for_experiment( radar_control_to_exp_handler, options.exphan_to_radctrl_identity, 'NOERROR') elif new_experiment_loaded: new_experiment_loaded = False if __debug__: rad_ctrl_print("New AveragingPeriod") slice_to_beamdir_dict = aveperiod.set_beamdirdict( aveperiod.beam_iter) # Build an ordered list of sequences # A sequence is a list of pulses in order # A pulse is a dictionary with all required information for that pulse. sequence_dict_list = aveperiod.build_sequences( slice_to_beamdir_dict) beam_phase_dict_list = [] for sequence_index, sequence in enumerate(aveperiod.sequences): beam_phase_dict = {} for slice_id in sequence.slice_ids: if experiment.slice_dict[slice_id]['rxonly']: receive_freq = experiment.slice_dict[slice_id][ 'rxfreq'] else: receive_freq = experiment.slice_dict[slice_id][ 'txfreq'] # TODO add clrfrqsearch result freq. beamdir = slice_to_beamdir_dict[slice_id] beam_phase_dict[slice_id] = \ rx_azimuth_to_antenna_offset(beamdir, options.main_antenna_count, options.interferometer_antenna_count, options.main_antenna_spacing, options.interferometer_antenna_spacing, options.intf_offset, receive_freq) beam_phase_dict_list.append(beam_phase_dict) # Setup debug samples if in debug mode. debug_samples = [] if __debug__: for sequence_index, sequence in enumerate( aveperiod.sequences): sequence_samples_dict = create_debug_sequence_samples( experiment.txrate, experiment.txctrfreq, sequence_dict_list[sequence_index], options.main_antenna_count, experiment.output_rx_rate, sequence.ssdelay) debug_samples.append(sequence_samples_dict) # all phases are set up for this averaging period for the beams required. if not scan.scanbound: integration_period_start_time = datetime.utcnow() # ms rad_ctrl_print("Integration start time: {}".format( integration_period_start_time)) if aveperiod.intt is not None: intt_break = True if scan.scanbound: # calculate scan start time. First beam in the sequence will likely # be ready to go if the first scan aligns directly to the minute. The # rest will need to wait until their boundary time is up. beam_scanbound = start_minute + timedelta( seconds=scan.scanbound[scan_iter]) time_diff = beam_scanbound - datetime.utcnow() if time_diff.total_seconds() > 0: if __debug__ or first_integration: msg = "{}s until averaging period {} at time {}" msg = msg.format( sm.COLOR("blue", time_diff.total_seconds()), sm.COLOR("yellow", scan_iter), sm.COLOR("red", beam_scanbound)) rad_ctrl_print(msg) # TODO: reduce sleep if we want to use GPS timestamped transmissions time.sleep(time_diff.total_seconds()) else: if __debug__: # TODO: This will be wrong if the start time is in the past. maybe use datetime.utcnow() like below # TODO: instead of beam_scanbound, or change wording to when the aveperiod should have started? msg = "starting averaging period {} at time {}" msg = msg.format( sm.COLOR("yellow", scan_iter), sm.COLOR("red", beam_scanbound)) rad_ctrl_print(msg) integration_period_start_time = datetime.utcnow() # ms msg = "Integration start time: {}" msg = msg.format( sm.COLOR("red", integration_period_start_time)) rad_ctrl_print(msg) # Here we find how much system time has elapsed to find the true amount # of time we can integrate for this scan boundary. We can then see if # we have enough time left to run the integration period. time_elapsed = integration_period_start_time - start_minute if scan_iter < len(scan.scanbound) - 1: scanbound_time = scan.scanbound[scan_iter + 1] # TODO: scanbound_time could be in the past if system has taken # too long, perhaps calculate which 'beam' (scan_iter) instead by # rewriting this code for an experiment-wide scanbound attribute instead # of individual scanbounds inside the scan objects # TODO: if scan_iter skips ahead, aveperiod.beam_iter may also need to if scan.align_to_beamorder is True bound_time_remaining = scanbound_time - time_elapsed.total_seconds( ) else: bound_time_remaining = next_scan_start - integration_period_start_time bound_time_remaining = bound_time_remaining.total_seconds( ) msg = "scan {} averaging period {}: bound_time_remaining {}s" msg = msg.format( sm.COLOR("yellow", scan_num), sm.COLOR("yellow", scan_iter), sm.COLOR("blue", round(bound_time_remaining, 6))) rad_ctrl_print(msg) if bound_time_remaining < aveperiod.intt * 1e-3: # reduce the integration period to only the time remaining # until the next scan boundary. # TODO: Check for bound_time_remaining > 0 # to be sure there is actually time to run this intt # (if bound_time_remaining < 0, we need a solution to # reset) integration_period_done_time = integration_period_start_time + \ timedelta(milliseconds=bound_time_remaining * 1e3) else: integration_period_done_time = integration_period_start_time + \ timedelta(milliseconds=aveperiod.intt) else: # no scanbound for this scan integration_period_done_time = integration_period_start_time + \ timedelta(milliseconds=aveperiod.intt) else: # intt does not exist, therefore using intn intt_break = False ending_number_of_sequences = aveperiod.intn # this will exist msg = "AvePeriod slices and beam numbers: {}".format({ x: y[aveperiod.beam_iter] for x, y in aveperiod.slice_to_beamorder.items() }) rad_ctrl_print(msg) first_sequence_out = False if TIME_PROFILE: time_to_prep_aveperiod = datetime.utcnow( ) - time_start_of_aveperiod rad_ctrl_print('Time to prep aveperiod: {}'.format( time_to_prep_aveperiod)) # Time to start averaging in the below loop num_sequences = 0 time_remains = True while time_remains: for sequence_index, sequence in enumerate( aveperiod.sequences): # Alternating sequences if there are multiple in the averaging_period. time_now = datetime.utcnow() if intt_break: if time_now >= integration_period_done_time: time_remains = False integration_period_time = ( time_now - integration_period_start_time) break else: # break at a certain number of integrations if num_sequences == ending_number_of_sequences: time_remains = False integration_period_time = time_now - integration_period_start_time break beam_phase_dict = beam_phase_dict_list[sequence_index] send_dsp_metadata( sigprocpacket, radar_control_to_dsp, options.dsp_to_radctrl_identity, radar_control_to_brian, options.brian_to_radctrl_identity, experiment.rxrate, experiment.output_rx_rate, seqnum_start + num_sequences, sequence.slice_ids, experiment.slice_dict, beam_phase_dict, sequence.seqtime, sequence.first_rx_sample_time, options.main_antenna_count, experiment.rxctrfreq, decimation_scheme) if first_integration: decimation_scheme = None first_integration = False if TIME_PROFILE: time_after_sequence_metadata = datetime.utcnow() sequence_metadata_time = time_after_sequence_metadata - time_now rad_ctrl_print('Sequence Metadata time: {}'.format( sequence_metadata_time)) # beam_phase_dict is slice_id : list of beamdirs, where beamdir = list # of antenna phase offsets for all antennas for that direction ordered # [0 ... main_antenna_count, 0 ... interferometer_antenna_count] # SEND ALL PULSES IN SEQUENCE. # If we have sent first sequence already and there is only one unique # pulse in the averaging period, send all repeats until end of the # averaging period. if first_sequence_out and aveperiod.one_pulse_only: for pulse_index, pulse_dict in \ enumerate(sequence_dict_list[sequence_index]): data_to_driver( driverpacket, radar_control_to_driver, options.driver_to_radctrl_identity, pulse_dict['samples_array'], experiment.txctrfreq, experiment.rxctrfreq, experiment.txrate, experiment.rxrate, sequence.numberofreceivesamples, sequence.seqtime, pulse_dict['startofburst'], pulse_dict['endofburst'], pulse_dict['timing'], seqnum_start + num_sequences, repeat=True) else: for pulse_index, pulse_dict in \ enumerate(sequence_dict_list[sequence_index]): data_to_driver( driverpacket, radar_control_to_driver, options.driver_to_radctrl_identity, pulse_dict['samples_array'], experiment.txctrfreq, experiment.rxctrfreq, experiment.txrate, experiment.rxrate, sequence.numberofreceivesamples, sequence.seqtime, pulse_dict['startofburst'], pulse_dict['endofburst'], pulse_dict['timing'], seqnum_start + num_sequences, repeat=pulse_dict['isarepeat']) first_sequence_out = True # Sequence is done num_sequences += 1 if __debug__: time.sleep(1) if TIME_PROFILE: pulses_to_driver_time = datetime.utcnow( ) - time_after_sequence_metadata rad_ctrl_print( 'Time for pulses to driver: {}'.format( pulses_to_driver_time)) if TIME_PROFILE: time_at_end_aveperiod = datetime.utcnow() msg = "Number of sequences: {}" msg = msg.format(sm.COLOR("magenta", num_sequences)) rad_ctrl_print(msg) if scan.aveperiod_iter == 0 and aveperiod.beam_iter == 0: # This is the first integration time in the scan object. # if scanbound is aligned to beamorder, the scan_iter will also = 0 at this point. scan_flag = True else: scan_flag = False last_sequence_num = seqnum_start + num_sequences - 1 send_datawrite_metadata( integration_time_packet, radar_control_to_dw, options.dw_to_radctrl_identity, last_sequence_num, num_sequences, scan_flag, integration_period_time, aveperiod.sequences, slice_to_beamdir_dict, experiment.cpid, experiment.experiment_name, experiment.scheduling_mode, experiment.output_rx_rate, experiment.comment_string, experiment.decimation_scheme.filter_scaling_factors, experiment.rxctrfreq, debug_samples=debug_samples) # end of the averaging period loop - move onto the next averaging period. # Increment the sequence number by the number of sequences that were in this # averaging period. seqnum_start += num_sequences if TIME_PROFILE: time_to_finish_aveperiod = datetime.utcnow( ) - time_at_end_aveperiod rad_ctrl_print('Time to finish aveperiod: {}'.format( time_to_finish_aveperiod)) aveperiod.beam_iter += 1 if aveperiod.beam_iter == aveperiod.num_beams_in_scan: aveperiod.beam_iter = 0 scan_iter += 1 scan.aveperiod_iter += 1 if scan.aveperiod_iter == len(scan.aveperiods): scan.aveperiod_iter = 0
def __init__(self): """ Create an object of necessary hardware and site parameters for use in the experiment. """ try: with open(config_file) as config_data: config = json.load(config_data) except IOError: errmsg = 'Cannot open config file at {}'.format(config_file) raise ExperimentException(errmsg) try: self._main_antenna_count = int(config['main_antenna_count']) self._interferometer_antenna_count = int( config['interferometer_antenna_count']) self._main_antenna_spacing = float(config['main_antenna_spacing']) self._interferometer_antenna_spacing = float( config['interferometer_antenna_spacing']) self._max_tx_sample_rate = float(config['max_tx_sample_rate']) self._max_rx_sample_rate = float(config['max_rx_sample_rate']) self._max_usrp_dac_amplitude = float( config['max_usrp_dac_amplitude']) self._pulse_ramp_time = float( config['pulse_ramp_time']) # in seconds self._tr_window_time = float(config['tr_window_time']) self._max_output_sample_rate = float( config['max_output_sample_rate'] ) # should use to check iqdata samples # when adjusting the experiment during operations. self._max_number_of_filtering_stages = int( config['max_number_of_filtering_stages']) self._max_number_of_filter_taps_per_stage = int( config['max_number_of_filter_taps_per_stage']) self._site_id = config['site_id'] self._max_freq = float(config['max_freq']) # Hz self._min_freq = float(config['min_freq']) # Hz self._minimum_pulse_length = float( config['minimum_pulse_length']) # us self._minimum_tau_spacing_length = float( config['minimum_tau_spacing_length']) # us # Minimum pulse separation is the minimum before the experiment treats it as a single # pulse (transmitting zeroes or no receiving between the pulses) # 125 us is approx two TX/RX times self._minimum_pulse_separation = float( config['minimum_pulse_separation']) # us self._usrp_master_clock_rate = float( config['usrp_master_clock_rate']) # Hz self._router_address = config['router_address'] self._radctrl_to_exphan_identity = str( config["radctrl_to_exphan_identity"]) self._radctrl_to_dsp_identity = str( config["radctrl_to_dsp_identity"]) self._radctrl_to_driver_identity = str( config["radctrl_to_driver_identity"]) self._radctrl_to_brian_identity = str( config["radctrl_to_brian_identity"]) self._radctrl_to_dw_identity = str( config["radctrl_to_dw_identity"]) self._driver_to_radctrl_identity = str( config["driver_to_radctrl_identity"]) self._driver_to_dsp_identity = str( config["driver_to_dsp_identity"]) self._driver_to_brian_identity = str( config["driver_to_brian_identity"]) self._exphan_to_radctrl_identity = str( config["exphan_to_radctrl_identity"]) self._exphan_to_dsp_identity = str( config["exphan_to_dsp_identity"]) self._dsp_to_radctrl_identity = str( config["dsp_to_radctrl_identity"]) self._dsp_to_driver_identity = str( config["dsp_to_driver_identity"]) self._dsp_to_exphan_identity = str( config["dsp_to_exphan_identity"]) self._dsp_to_dw_identity = str(config["dsp_to_dw_identity"]) self._dspbegin_to_brian_identity = str( config["dspbegin_to_brian_identity"]) self._dspend_to_brian_identity = str( config["dspend_to_brian_identity"]) self._dw_to_dsp_identity = str(config["dw_to_dsp_identity"]) self._dw_to_radctrl_identity = str( config["dw_to_radctrl_identity"]) self._brian_to_radctrl_identity = str( config["brian_to_radctrl_identity"]) self._brian_to_driver_identity = str( config["brian_to_driver_identity"]) self._brian_to_dspbegin_identity = str( config["brian_to_dspbegin_identity"]) self._brian_to_dspend_identity = str( config["brian_to_dspend_identity"]) # TODO add appropriate signal process maximum time here after timing is changed - can # use to check for pulse spacing minimums, pace the driver except ValueError as e: # TODO: error raise e today = datetime.datetime.today() year_start = datetime.datetime(today.year, 1, 1, 0, 0, 0, 0) # start of the year year_timedelta = today - year_start try: with open(hdw_dat_file + self.site_id) as hdwdata: lines = hdwdata.readlines() except IOError: errmsg = 'Cannot open hdw.dat.{} file at {}'.format( self.site_id, (hdw_dat_file + self.site_id)) raise ExperimentException(errmsg) lines[:] = [line for line in lines if line[0] != "#"] # remove comments lines[:] = [line for line in lines if len(line.split()) != 0] # remove blanks lines[:] = [ line for line in lines if int(line.split()[1]) > today.year or ( int(line.split()[1]) == today.year and float(line.split()[2]) > year_timedelta.total_seconds()) ] # only take present & future hdw data # there should only be one line left, however if there are more we will take the # one that expires first. if len(lines) > 1: times = [[line.split()[1], line.split()[2]] for line in lines] min_year = times[0][0] min_yrsec = times[0][1] hdw_index = 0 for i in range(len(times)): year = times[i][0] yrsec = times[i][1] if year < min_year: hdw_index = i elif year == min_year: if yrsec < min_yrsec: hdw_index = i hdw = lines[hdw_index] else: try: hdw = lines[0] except IndexError: errmsg = 'Cannot find any valid lines for this time period in the hardware file ' \ '{}'.format((hdw_dat_file + self.site_id)) raise ExperimentException(errmsg) # we now have the correct line of data. params = hdw.split() if len(params) != 19: errmsg = 'Found {} parameters in hardware file, expected 19'.format( len(params)) raise ExperimentException(errmsg) self._geo_lat = params[3] # decimal degrees, S = negative self._geo_long = params[4] # decimal degrees, W = negative self._altitude = params[5] # metres self._boresight = params[ 6] # degrees from geographic north, CCW = negative. self._beam_sep = params[ 7] # degrees TODO is this necessary, or is this a min. - for # post-processing software in RST? check with others. self._velocity_sign = params[8] # +1.0 or -1.0 self._analog_rx_attenuator = params[9] # dB self._tdiff = params[10] # ns self._phase_sign = params[11] self._intf_offset = [ float(params[12]), float(params[13]), float(params[14]) ] # interferometer offset from # midpoint of main, metres [x, y, z] where x is along line of antennas, y is along array # normal and z is altitude difference, in m. self._analog_rx_rise = params[15] # us self._analog_atten_stages = params[16] # number of stages self._max_range_gates = params[17] self._max_beams = params[ 18] # so a beam number always points in a certain direction # TODO Is this last one necessary - why don't we specify directions in angle. - also for post-processing so check if it applies to Borealis try: with open(restricted_freq_file + self.site_id) as restricted_freq_data: restricted = restricted_freq_data.readlines() except IOError: errmsg = 'Cannot open restrict.dat.{} file at {}'.format( self.site_id, (restricted_freq_file + self.site_id)) raise ExperimentException(errmsg) restricted[:] = [line for line in restricted if line[0] != "#"] # remove comments restricted[:] = [ line for line in restricted if len(line.split()) != 0 ] # remove blanks for line in restricted: splitup = line.split("=") if len(splitup) == 2: if splitup[0] == 'default' or splitup[0] == 'default ': self.__default_freq = int(splitup[1]) # kHz restricted.remove(line) break else: #no break raise Exception('No Default Frequency Found in Restrict.dat') self.__restricted_ranges = [] for line in restricted: splitup = line.split() if len(splitup) != 2: raise Exception( 'Problem with Restricted Frequency: A Range Len != 2') try: splitup = [int(float(freq)) for freq in splitup] # convert to ints except ValueError: raise ValueError( 'Error parsing Restrict.Dat Frequency Ranges, Invalid Literal' ) restricted_range = tuple(splitup) self.__restricted_ranges.append(restricted_range)