class Correlation: def __init__(self, fpga, comb, f_start, f_stop, logger=logging.getLogger(__name__)): """ f_start and f_stop must be in Hz """ self.logger = logger snap_name = "snap_{a}x{b}".format(a=comb[0], b=comb[1]) self.snapshot0 = Snapshot(fpga, "{name}_0".format(name = snap_name), dtype='>i8', cvalue=True, logger=self.logger.getChild("{name}_0".format(name = snap_name))) self.snapshot1 = Snapshot(fpga, "{name}_1".format(name = snap_name), dtype='>i8', cvalue=True, logger=self.logger.getChild("{name}_1".format(name = snap_name))) self.f_start = np.uint64(f_start) self.f_stop = np.uint64(f_stop) # this will change from None to an array of phase offsets for each frequency bin # if calibration gets applied at a later stage. # this is an array of phases introduced by the system. So if a value is positive, # it means that the system is introducing a phase shift between comb[0] and comb[1] # in other words comb1 is artificially delayed. self.calibration_phase_offsets = None self.calibration_cable_length_offsets = None self.arm() self.fetch_signal() self.frequency_bins = np.linspace( start = self.f_start, stop = self.f_stop, num = len(self.signal), endpoint = False) def add_frequency_bin_calibration(self, frequencies, phases): assert(len(frequencies) == len(phases)) self.calibration_phase_offsets = np.ndarray(len(self.frequency_bins)) for idx, f in enumerate(self.frequency_bins): # find index in of the frequency in frequencies which is losest to f frequencies_idx = np.argmin(np.abs(frequencies - f)) self.calibration_phase_offsets[idx] = phases[frequencies_idx] self.logger.info("Added calibration factors based on each frequency bin") def add_cable_length_calibration(self, length_a, velocity_factor_a, length_b, velocity_factor_b): """ 'a' values are for comb[0]. 'b' values are for comb[1] TODO: remove what follows in docstring A positive number will produce a positive phase correlation factor. This means that that a positive number implies that comb[1] is artificially delayed with respect to comb[0]. This will happen if comb[1]s cable is longer than comb[0]s. Hence this should be len(cable1) - len(cable0) """ self.calibration_cable_length_offsets = np.ndarray(len(self.frequency_bins)) # calculate total time delay. t_a = length_a / (scipy.constants.c * velocity_factor_a) t_b = length_b / (scipy.constants.c * velocity_factor_b) # for each frequency bin, calculate corresponding for idx, f in enumerate(self.frequency_bins): # this will produce a positive number if As length is longer than Bs phase = 2*np.pi * (t_a - t_b) * f self.calibration_cable_length_offsets[idx] = phase self.logger.info("Added calibration factors base on cable length") def arm(self): self.snapshot0.arm() self.snapshot1.arm() def apply_frequency_domain_calibrations(self): if self.calibration_phase_offsets != None: offsets = np.exp(1j*self.calibration_phase_offsets) self.signal = self.signal * np.conj(offsets) if self.calibration_cable_length_offsets != None: offsets = np.exp(1j*self.calibration_cable_length_offsets) self.signal = self.signal * offsets self.logger.debug("Applied calibration factors") def fetch_signal(self): self.snapshot0.fetch_signal() self.snapshot1.fetch_signal() # F: index the elements in column-major order, with the first index changing fastest self.signal = np.ravel( (self.snapshot0.signal, self.snapshot1.signal), order='F') def strongest_frequency(self): """ Returns the frequency in Hz which has the strongest signal. Frequency will be the centre of the bin. Excludes DC bin """ # add 1 because we're excluding the initial bin bin_number = np.argmax(np.abs(self.signal[1:])) + 1 return self.frequency_bins[bin_number] def strongest_frequency_in_range(self, f_start, f_stop): idx_start = np.searchsorted(self.frequency_bins, f_start) idx_stop= np.searchsorted(self.frequency_bins, f_stop) subsig = self.signal[idx_start:idx_stop] offset_to_max = np.argmax(np.abs(subsig)) frequency = self.frequency_bins[idx_start + offset_to_max] return frequency def phase_at_freq(self, f): """ Note: this formula may need fixing!! Check against actual data """ bin_width = self.frequency_bins[1] - self.frequency_bins[0] bin_number = int(round((f - self.f_start) / bin_width)) bin_contents = self.signal[bin_number] phase = np.angle(bin_contents) return phase
class Correlation: def __init__(self, fpga, comb, f_start, f_stop, logger=logging.getLogger(__name__)): """ f_start and f_stop must be in Hz """ self.logger = logger snap_name = "snap_{a}x{b}".format(a=comb[0], b=comb[1]) self.snapshot0 = Snapshot(fpga, "{name}_0".format(name=snap_name), dtype='>i8', cvalue=True, logger=self.logger.getChild( "{name}_0".format(name=snap_name))) self.snapshot1 = Snapshot(fpga, "{name}_1".format(name=snap_name), dtype='>i8', cvalue=True, logger=self.logger.getChild( "{name}_1".format(name=snap_name))) self.f_start = np.uint64(f_start) self.f_stop = np.uint64(f_stop) # this will change from None to an array of phase offsets for each frequency bin # if calibration gets applied at a later stage. # this is an array of phases introduced by the system. So if a value is positive, # it means that the system is introducing a phase shift between comb[0] and comb[1] # in other words comb1 is artificially delayed. self.calibration_phase_offsets = None self.calibration_cable_length_offsets = None self.arm() self.fetch_signal() self.frequency_bins = np.linspace(start=self.f_start, stop=self.f_stop, num=len(self.signal), endpoint=False) def add_frequency_bin_calibration(self, frequencies, phases): assert (len(frequencies) == len(phases)) self.calibration_phase_offsets = np.ndarray(len(self.frequency_bins)) for idx, f in enumerate(self.frequency_bins): # find index in of the frequency in frequencies which is losest to f frequencies_idx = np.argmin(np.abs(frequencies - f)) self.calibration_phase_offsets[idx] = phases[frequencies_idx] self.logger.info( "Added calibration factors based on each frequency bin") def add_cable_length_calibration(self, length_a, velocity_factor_a, length_b, velocity_factor_b): """ 'a' values are for comb[0]. 'b' values are for comb[1] TODO: remove what follows in docstring A positive number will produce a positive phase correlation factor. This means that that a positive number implies that comb[1] is artificially delayed with respect to comb[0]. This will happen if comb[1]s cable is longer than comb[0]s. Hence this should be len(cable1) - len(cable0) """ self.calibration_cable_length_offsets = np.ndarray( len(self.frequency_bins)) # calculate total time delay. t_a = length_a / (scipy.constants.c * velocity_factor_a) t_b = length_b / (scipy.constants.c * velocity_factor_b) # for each frequency bin, calculate corresponding for idx, f in enumerate(self.frequency_bins): # this will produce a positive number if As length is longer than Bs phase = 2 * np.pi * (t_a - t_b) * f self.calibration_cable_length_offsets[idx] = phase self.logger.info("Added calibration factors base on cable length") def arm(self): self.snapshot0.arm() self.snapshot1.arm() def apply_frequency_domain_calibrations(self): if self.calibration_phase_offsets != None: offsets = np.exp(1j * self.calibration_phase_offsets) self.signal = self.signal * np.conj(offsets) if self.calibration_cable_length_offsets != None: offsets = np.exp(1j * self.calibration_cable_length_offsets) self.signal = self.signal * offsets self.logger.debug("Applied calibration factors") def fetch_signal(self): self.snapshot0.fetch_signal() self.snapshot1.fetch_signal() # F: index the elements in column-major order, with the first index changing fastest self.signal = np.ravel((self.snapshot0.signal, self.snapshot1.signal), order='F') def strongest_frequency(self): """ Returns the frequency in Hz which has the strongest signal. Frequency will be the centre of the bin. Excludes DC bin """ # add 1 because we're excluding the initial bin bin_number = np.argmax(np.abs(self.signal[1:])) + 1 return self.frequency_bins[bin_number] def strongest_frequency_in_range(self, f_start, f_stop): idx_start = np.searchsorted(self.frequency_bins, f_start) idx_stop = np.searchsorted(self.frequency_bins, f_stop) subsig = self.signal[idx_start:idx_stop] offset_to_max = np.argmax(np.abs(subsig)) frequency = self.frequency_bins[idx_start + offset_to_max] return frequency def phase_at_freq(self, f): """ Note: this formula may need fixing!! Check against actual data """ bin_width = self.frequency_bins[1] - self.frequency_bins[0] bin_number = int(round((f - self.f_start) / bin_width)) bin_contents = self.signal[bin_number] phase = np.angle(bin_contents) return phase
class Correlator: def __init__(self, ip_addr='localhost', num_channels=4, fs=800e6, logger=logging.getLogger(__name__)): """The interface to a ROACH cross correlator Keyword arguments: ip_addr -- IP address (or hostname) of the ROACH. (default: localhost) num_channels -- antennas in the correlator. (default: 4) fs -- sample frequency of antennas. (default 800e6; 800 MHz) logger -- logger to use. (default: new default logger) """ self.logger = logger self.fpga = corr.katcp_wrapper.FpgaClient(ip_addr) time.sleep(0.1) self.num_channels = num_channels self.fs = np.float64(fs) self.cross_combinations = list( itertools.combinations( range(num_channels), 2)) # [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] self.control_register = ControlRegister( self.fpga, self.logger.getChild('control_reg')) self.set_accumulation_len(100) self.re_sync() self.control_register.allow_trigger( ) # necessary as Correlations auto fetch signal # only 0x0 has been implemented #self.auto_combinations = [(x, x) for x in range(num_channels)] # [(0, 0), (1, 1), (2, 2), (3, 3)] self.auto_combinations = [(0, 0)] self.frequency_correlations = {} for comb in (self.cross_combinations + self.auto_combinations): self.frequency_correlations[comb] = Correlation( fpga=self.fpga, comb=comb, f_start=0, f_stop=fs / 2, logger=self.logger.getChild("{a}x{b}".format(a=comb[0], b=comb[1]))) self.time_domain_snap = Snapshot( fpga=self.fpga, name='dram_snapshot', dtype=np.int8, cvalue=False, logger=self.logger.getChild('time_domain_snap')) self.upsample_factor = 100 self.subsignal_length_max = 2**17 self.time_domain_padding = 100 self.time_domain_calibration_values = None self.time_domain_calibration_cable_values = None self.control_register.block_trigger() def impulse_arm(self): self.control_register.pulse_impulse_arm() self.time_domain_snap.arm() def impulse_fetch(self): """ Will fetch and re-arm if an impulse has occurred. Will do nothing if no impulse. Return True if fetched (ie: an impulse happened) or False if not """ pre_delay = 256 * 4 impulse_len = self.fpga.read_uint('impulse_length') if impulse_len != 0: self.logger.info( "Got an impulse of length: {}".format(impulse_len)) time.sleep(0.1) if self.fpga.read_uint('impulse_length') != impulse_len: self.logger.warning( 'Impulse has gone on for too long. Adjust setpoint?') self.fetch_time_domain_snapshot() self.impulse_arm() return True return False def set_impulse_setpoint(self, level): self.fpga.write_int('setppoint', level) self.logger.info( "Impulse detection setpoint changed to: {}".format(level)) def get_current_impulse_level(self): level = self.fpga.read_uint('current_impulse_level') self.logger.debug("Current impulse level: {}".format(level)) return level def set_impulse_filter_len(self, length): assert (length > 5) assert (length < 1000) self.fpga.write_int('impulse_filter_len', length) self.logger.info("Impulse filter length set to: {}".format(length)) def fetch_time_domain_snapshot(self, force=False): self.time_domain_snap.fetch_signal(force) sig = self.time_domain_snap.signal # shorten to fit exactly new_length = (4 * self.num_channels) * np.floor( len(sig) / (4 * self.num_channels)) sig = sig[0:new_length] self.time_domain_signals = np.ndarray( (self.num_channels, len(sig) / self.num_channels), dtype=np.float64) sig = sig.reshape(len(sig) / self.num_channels, self.num_channels) for chan in range(self.num_channels): self.time_domain_signals[chan] = sig[ chan::self.num_channels].flatten().astype(np.float64) # remove DC offset mean = np.mean(self.time_domain_signals[chan]) self.time_domain_signals[chan] -= mean self.logger.info("After: DC offset for {chan} = {offset}".format( chan=chan, offset=np.mean(self.time_domain_signals[chan]))) self.time_domain_axis = np.linspace(0, len(self.time_domain_signals[0]) / self.fs, len(self.time_domain_signals[0]), endpoint=False) def fetch_crosses(self): """ Updates the snapshot blocks for all cross correlations """ self.crosses = self.fetch_combinations(self.cross_combinations) def fetch_autos(self): """ Reads the snapshot blocks for all auto correlations and populates Correlation objects""" self.fetch_combinations(self.auto_combinations) def fetch_all(self): """ Popules both cross correlations and auto correlations. Returns Correlation objects for crosses and autos """ self.fetch_combinations(self.cross_combinations + self.auto_combinations) def fetch_combinations(self, combinations): """ Takes an array of X correlations and returns the Correlation objects """ self.control_register.block_trigger() for comb in combinations: self.arm_combination(comb) self.control_register.allow_trigger() for comb in combinations: self.frequency_correlations[comb].fetch_signal() self.get_overflow_state() def visibilities_at_frequency(self, f): visibilities = np.ndarray(len(self.cross_combinations)) for idx, comb in enumerate(self.cross_combinations): visibilities[idx] = self.frequency_correlations[ comb].phase_at_freq(f) return visibilities def save_frequency_correlations(self, path): full_dir = "{base}/{sub}/".format(base=path, sub=time.time()) os.mkdir(full_dir) for comb in self.cross_combinations: filename = "{path}/{a}x{b}".format(path=full_dir, a=comb[0], b=comb[1]) np.save(filename, self.frequency_correlations[comb].signal) self.logger.debug( "Saved frequency combinations to {d}".format(d=full_dir)) def save_time_domain_snapshots(self, path): full_dir = "{base}/{sub}/".format(base=path, sub=time.time()) os.mkdir(full_dir) for chan in range(self.num_channels): filename = "{path}/{chan}".format(path=full_dir, chan=chan) sig = self.time_domain_signals[chan] np.save(filename, sig) self.logger.debug("Saved time domain raw to {d}".format(d=full_dir)) def do_time_domain_cross_correlation(self): # TODO: initiaise factor at initialisation from config. # TODO: min(length max, actual) should be used. Init from config. self.do_time_domain_cross_correlations_cross_first() self.time_domain_cross_correlations_peaks = {} for baseline in self.cross_combinations: self.time_domain_cross_correlations_peaks[baseline] = \ self.time_domain_correlations_times[baseline][np.argmax(self.time_domain_correlations_values[baseline])] def visibilities_from_time(self): visibilities = np.ndarray(len(self.cross_combinations)) for idx, baseline in enumerate(self.cross_combinations): visibilities[idx] = self.time_domain_cross_correlations_peaks[ baseline] return visibilities def do_time_domain_cross_correlations_cross_first(self): self.time_domain_correlations_values = {} self.time_domain_correlations_times = {} for (a_idx, b_idx) in self.cross_combinations: # NOTE: The only reason this works is that the dtype of the zeros is # float64 hence 'a' and 'b' are also float64. The signals get cast # to float64. # if they stayed as int8 the correlation would fail miserably. # investivate time impact of converting to float64 vs int32 vs int64 a = self.time_domain_signals[a_idx][0:self.subsignal_length_max] a_time = np.linspace(0, len(a) / self.fs, len(a), endpoint=False) b = np.concatenate( (np.zeros(self.time_domain_padding), self.time_domain_signals[b_idx][0:self.subsignal_length_max], np.zeros(self.time_domain_padding))) b_time = np.linspace(-(self.time_domain_padding / self.fs), (len(b) - self.time_domain_padding) / self.fs, len(b), endpoint=False) # this corresponds to sliding a over b. Ie: b gets shifted each tick correlation = np.correlate(b, a, mode='valid') correlation_time = np.linspace(b_time[0] - a_time[0], b_time[-1] - a_time[-1], len(correlation), endpoint=True) correlation_upped, correlation_time_upped = scipy.signal.resample( correlation, len(correlation) * self.upsample_factor, t=correlation_time) self.time_domain_correlations_values[(a_idx, b_idx)] = correlation_upped if self.time_domain_calibration_values != None: correlation_time_upped -= self.time_domain_calibration_values[( a_idx, b_idx)] if self.time_domain_calibration_cable_values != None: correlation_time_upped -= self.time_domain_calibration_cable_values[ (a_idx, b_idx)] self.time_domain_correlations_times[( a_idx, b_idx)] = correlation_time_upped # how much extra time we're appending each side #self.time_domain_correlation_time3 = np.linspace(-pt, pt, ((2*self.time_domain_padding)+1) * self.upsample_factor) # need to do something about the different length to compensate for # correct time stamp per sample. # perhaps contact b with zeros and then remove them? While doing the same to # the 't' axis. Or just to 't'? def do_time_domain_cross_correlation_resample_first(self): raise Exception("Need to swap A and B as done above") self.time_domain_correlations = [] # fetch here maybe? for (a_idx, b_idx) in self.cross_combinations: a = np.concatenate( (np.zeros(self.time_domain_padding), self.time_domain_signals[a_idx][0:self.subsignal_length_max], np.zeros(self.time_domain_padding))) a_time = np.linspace(-(self.time_domain_padding / self.fs), (len(a) - self.time_domain_padding) / self.fs, len(a), endpoint=False) b = self.time_domain_signals[b_idx][0:self.subsignal_length_max] b_time = np.linspace(0, len(b) / self.fs, len(b), endpoint=False) assert (len(a) == ((len(b) + 2 * self.time_domain_padding))) a_upped, a_time_upped = scipy.signal.resample(a, len(a) * self.upsample_factor, t=a_time) b_upped, b_time_upped = scipy.signal.resample(b, len(b) * self.upsample_factor, t=b_time) correlation = np.correlate(a_upped, b_upped, mode='valid') correlation_time = np.linspace(a_time_upped[0] - b_time_upped[0], a_time_upped[-1] - b_time_upped[-1], len(correlation), endpoint=True) return (correlation, correlation_time) def arm_combination(self, combination): """ Arms the snapshot block associated with the correlation combination """ self.frequency_correlations[combination].arm() def set_accumulation_len(self, acc_len): """The number of vectors which should be accumulated before being snapped. """ self.fpga.write_int('acc_len', acc_len) self.logger.info("Accumulation length set to {l}".format(l=acc_len)) self.re_sync() def set_shift_schedule(self, shift_schedule): """ Defines the FFT bit shift schedule """ self.control_register.set_shift_schedule(shift_schedule) def re_sync(self): self.control_register.pulse_sync() def reset_accumulation_counter(self): self.control_register.reset_accumulation_counter() # change this to 'get overflow states' # which reads and clears all overflow flags and returns a # hash of whether or not the various flags have been set. def get_overflow_state(self): status = self.fpga.read_uint('status') overflows = { 'adc': False, 'acc': False, 'fft': False, } if (status & 1 << 0) != 0: overflows['adc'] = True self.logger.critical("An ADC has clipped") if (status & 1 << 1) != 0: overflows['acc'] = True self.logger.critical("The accumulator has overflowed") if (status & 1 << 2) != 0: overflows['fft'] = True self.logger.critical("The FFT has overflowed") self.control_register.pulse_overflow_rst() return overflows def add_time_domain_calibration(self, filename): self.time_domain_calibration_values = {} with open(filename) as f: offsets = json.load(f) for a, b in self.cross_combinations: comb_str = "{a}x{b}".format(a=a, b=b) self.time_domain_calibration_values[(a, b)] = offsets[comb_str] def add_frequency_bin_calibrations(self, filename): with open(filename) as f: offsets = json.load(f) frequencies = offsets['axis'] for a, b in self.cross_combinations: self.frequency_correlations[(a, b)].add_frequency_bin_calibration( frequencies=frequencies, phases=offsets["{a}{b}".format(a=a, b=b)]) def add_cable_length_calibrations(self, filename): """ Filename should be a json file with cable lengths for each antenna and velocity factors { "0": { "length": 0.5, "velocity factor": 0.66 }, """ with open(filename) as f: cables = json.load(f) self.time_domain_calibration_cable_values = {} for a, b in self.cross_combinations: length_a = cables[str(a)]['length'] velocity_factor_a = cables[str(a)]['velocity factor'] length_b = cables[str(b)]['length'] velocity_factor_b = cables[str(b)]['velocity factor'] # For the frequency domain: self.frequency_correlations[(a, b)].add_cable_length_calibration( length_a=length_a, velocity_factor_a=velocity_factor_a, length_b=length_b, velocity_factor_b=velocity_factor_b) # For the time domain: t_a = length_a / (scipy.constants.c * velocity_factor_a) t_b = length_b / (scipy.constants.c * velocity_factor_b) # this is how much b is delayed from a by as a result of the cable. # b delayed from a by a positive amount will mean that the (a, b) correlation # peak will be more positive than it should be. Subtract this to compensate. self.time_domain_calibration_cable_values[(a, b)] = t_b - t_a def apply_frequency_domain_calibrations(self): for a, b in self.cross_combinations: self.frequency_correlations[( a, b)].apply_frequency_domain_calibrations()
class Correlator: def __init__(self, ip_addr='localhost', num_channels=4, fs=800e6, logger=logging.getLogger(__name__)): """The interface to a ROACH cross correlator Keyword arguments: ip_addr -- IP address (or hostname) of the ROACH. (default: localhost) num_channels -- antennas in the correlator. (default: 4) fs -- sample frequency of antennas. (default 800e6; 800 MHz) logger -- logger to use. (default: new default logger) """ self.logger = logger self.fpga = corr.katcp_wrapper.FpgaClient(ip_addr) time.sleep(0.1) self.num_channels = num_channels self.fs = np.float64(fs) self.cross_combinations = list(itertools.combinations(range(num_channels), 2)) # [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] self.control_register = ControlRegister(self.fpga, self.logger.getChild('control_reg')) self.set_accumulation_len(100) self.re_sync() self.control_register.allow_trigger() # necessary as Correlations auto fetch signal # only 0x0 has been implemented #self.auto_combinations = [(x, x) for x in range(num_channels)] # [(0, 0), (1, 1), (2, 2), (3, 3)] self.auto_combinations = [(0, 0)] self.frequency_correlations = {} for comb in (self.cross_combinations + self.auto_combinations): self.frequency_correlations[comb] = Correlation(fpga = self.fpga, comb = comb, f_start = 0, f_stop = fs/2, logger = self.logger.getChild("{a}x{b}".format(a = comb[0], b = comb[1])) ) self.time_domain_snap = Snapshot(fpga = self.fpga, name = 'dram_snapshot', dtype = np.int8, cvalue = False, logger = self.logger.getChild('time_domain_snap')) self.upsample_factor = 100 self.subsignal_length_max = 2**17 self.time_domain_padding = 100 self.time_domain_calibration_values = None self.time_domain_calibration_cable_values = None self.control_register.block_trigger() def impulse_arm(self): self.control_register.pulse_impulse_arm() self.time_domain_snap.arm() def impulse_fetch(self): """ Will fetch and re-arm if an impulse has occurred. Will do nothing if no impulse. Return True if fetched (ie: an impulse happened) or False if not """ pre_delay = 256 * 4 impulse_len = self.fpga.read_uint('impulse_length') if impulse_len != 0: self.logger.info("Got an impulse of length: {}".format(impulse_len)) time.sleep(0.1) if self.fpga.read_uint('impulse_length') != impulse_len: self.logger.warning('Impulse has gone on for too long. Adjust setpoint?') self.fetch_time_domain_snapshot() self.impulse_arm() return True return False def set_impulse_setpoint(self, level): self.fpga.write_int('setppoint', level) self.logger.info("Impulse detection setpoint changed to: {}".format(level)) def get_current_impulse_level(self): level = self.fpga.read_uint('current_impulse_level') self.logger.debug("Current impulse level: {}".format(level)) return level def set_impulse_filter_len(self, length): assert(length > 5) assert(length < 1000) self.fpga.write_int('impulse_filter_len', length) self.logger.info("Impulse filter length set to: {}".format(length)) def fetch_time_domain_snapshot(self, force=False): self.time_domain_snap.fetch_signal(force) sig = self.time_domain_snap.signal # shorten to fit exactly new_length = (4 * self.num_channels) * np.floor(len(sig) / (4 * self.num_channels)) sig = sig[0:new_length] self.time_domain_signals = np.ndarray((self.num_channels, len(sig)/self.num_channels), dtype = np.float64) sig = sig.reshape(len(sig)/self.num_channels, self.num_channels) for chan in range(self.num_channels): self.time_domain_signals[chan] = sig[chan::self.num_channels].flatten().astype(np.float64) # remove DC offset mean = np.mean(self.time_domain_signals[chan]) self.time_domain_signals[chan] -= mean self.logger.info("After: DC offset for {chan} = {offset}".format( chan = chan, offset = np.mean(self.time_domain_signals[chan]))) self.time_domain_axis = np.linspace(0, len(self.time_domain_signals[0])/self.fs, len(self.time_domain_signals[0]), endpoint = False) def fetch_crosses(self): """ Updates the snapshot blocks for all cross correlations """ self.crosses = self.fetch_combinations(self.cross_combinations) def fetch_autos(self): """ Reads the snapshot blocks for all auto correlations and populates Correlation objects""" self.fetch_combinations(self.auto_combinations) def fetch_all(self): """ Popules both cross correlations and auto correlations. Returns Correlation objects for crosses and autos """ self.fetch_combinations(self.cross_combinations + self.auto_combinations) def fetch_combinations(self, combinations): """ Takes an array of X correlations and returns the Correlation objects """ self.control_register.block_trigger() for comb in combinations: self.arm_combination(comb) self.control_register.allow_trigger() for comb in combinations: self.frequency_correlations[comb].fetch_signal() self.get_overflow_state() def visibilities_at_frequency(self, f): visibilities = np.ndarray(len(self.cross_combinations)) for idx, comb in enumerate(self.cross_combinations): visibilities[idx] = self.frequency_correlations[comb].phase_at_freq(f) return visibilities def save_frequency_correlations(self, path): full_dir = "{base}/{sub}/".format(base = path, sub = time.time()) os.mkdir(full_dir) for comb in self.cross_combinations: filename = "{path}/{a}x{b}".format(path = full_dir, a = comb[0], b = comb[1]) np.save(filename, self.frequency_correlations[comb].signal) self.logger.debug("Saved frequency combinations to {d}".format(d = full_dir)) def save_time_domain_snapshots(self, path): full_dir = "{base}/{sub}/".format(base = path, sub = time.time()) os.mkdir(full_dir) for chan in range(self.num_channels): filename = "{path}/{chan}".format(path = full_dir, chan = chan) sig = self.time_domain_signals[chan] np.save(filename, sig) self.logger.debug("Saved time domain raw to {d}".format(d = full_dir)) def do_time_domain_cross_correlation(self): # TODO: initiaise factor at initialisation from config. # TODO: min(length max, actual) should be used. Init from config. self.do_time_domain_cross_correlations_cross_first() self.time_domain_cross_correlations_peaks = {} for baseline in self.cross_combinations: self.time_domain_cross_correlations_peaks[baseline] = \ self.time_domain_correlations_times[baseline][np.argmax(self.time_domain_correlations_values[baseline])] def visibilities_from_time(self): visibilities = np.ndarray(len(self.cross_combinations)) for idx, baseline in enumerate(self.cross_combinations): visibilities[idx] = self.time_domain_cross_correlations_peaks[baseline] return visibilities def do_time_domain_cross_correlations_cross_first(self): self.time_domain_correlations_values = {} self.time_domain_correlations_times = {} for (a_idx, b_idx) in self.cross_combinations: # NOTE: The only reason this works is that the dtype of the zeros is # float64 hence 'a' and 'b' are also float64. The signals get cast # to float64. # if they stayed as int8 the correlation would fail miserably. # investivate time impact of converting to float64 vs int32 vs int64 a = self.time_domain_signals[a_idx][0:self.subsignal_length_max] a_time = np.linspace(0, len(a)/self.fs, len(a), endpoint=False) b = np.concatenate( (np.zeros(self.time_domain_padding), self.time_domain_signals[b_idx][0:self.subsignal_length_max], np.zeros(self.time_domain_padding))) b_time = np.linspace(-(self.time_domain_padding/self.fs), (len(b)-self.time_domain_padding)/self.fs, len(b), endpoint=False) # this corresponds to sliding a over b. Ie: b gets shifted each tick correlation = np.correlate(b, a, mode='valid') correlation_time = np.linspace(b_time[0] - a_time[0], b_time[-1] - a_time[-1], len(correlation), endpoint=True) correlation_upped, correlation_time_upped = scipy.signal.resample( correlation, len(correlation)*self.upsample_factor, t = correlation_time) self.time_domain_correlations_values[(a_idx, b_idx)] = correlation_upped if self.time_domain_calibration_values != None: correlation_time_upped -= self.time_domain_calibration_values[(a_idx, b_idx)] if self.time_domain_calibration_cable_values != None: correlation_time_upped -= self.time_domain_calibration_cable_values[(a_idx, b_idx)] self.time_domain_correlations_times[(a_idx, b_idx)] = correlation_time_upped # how much extra time we're appending each side #self.time_domain_correlation_time3 = np.linspace(-pt, pt, ((2*self.time_domain_padding)+1) * self.upsample_factor) # need to do something about the different length to compensate for # correct time stamp per sample. # perhaps contact b with zeros and then remove them? While doing the same to # the 't' axis. Or just to 't'? def do_time_domain_cross_correlation_resample_first(self): raise Exception("Need to swap A and B as done above") self.time_domain_correlations = [] # fetch here maybe? for (a_idx, b_idx) in self.cross_combinations: a = np.concatenate( (np.zeros(self.time_domain_padding), self.time_domain_signals[a_idx][0:self.subsignal_length_max], np.zeros(self.time_domain_padding))) a_time = np.linspace(-(self.time_domain_padding/self.fs), (len(a)-self.time_domain_padding)/self.fs, len(a), endpoint=False) b = self.time_domain_signals[b_idx][0:self.subsignal_length_max] b_time = np.linspace(0, len(b)/self.fs, len(b), endpoint=False) assert(len(a) == ((len(b) + 2*self.time_domain_padding))) a_upped, a_time_upped = scipy.signal.resample(a, len(a)*self.upsample_factor, t = a_time) b_upped, b_time_upped = scipy.signal.resample(b, len(b)*self.upsample_factor, t = b_time) correlation = np.correlate(a_upped, b_upped, mode='valid') correlation_time = np.linspace(a_time_upped[0] - b_time_upped[0], a_time_upped[-1] - b_time_upped[-1], len(correlation), endpoint=True) return (correlation, correlation_time) def arm_combination(self, combination): """ Arms the snapshot block associated with the correlation combination """ self.frequency_correlations[combination].arm() def set_accumulation_len(self, acc_len): """The number of vectors which should be accumulated before being snapped. """ self.fpga.write_int('acc_len', acc_len) self.logger.info("Accumulation length set to {l}".format(l = acc_len)) self.re_sync() def set_shift_schedule(self, shift_schedule): """ Defines the FFT bit shift schedule """ self.control_register.set_shift_schedule(shift_schedule) def re_sync(self): self.control_register.pulse_sync() def reset_accumulation_counter(self): self.control_register.reset_accumulation_counter() # change this to 'get overflow states' # which reads and clears all overflow flags and returns a # hash of whether or not the various flags have been set. def get_overflow_state(self): status = self.fpga.read_uint('status') overflows = { 'adc': False, 'acc': False, 'fft': False, } if (status & 1 << 0) != 0: overflows['adc'] = True self.logger.critical("An ADC has clipped") if (status & 1 << 1) != 0: overflows['acc'] = True self.logger.critical("The accumulator has overflowed") if (status & 1 << 2) != 0: overflows['fft'] = True self.logger.critical("The FFT has overflowed") self.control_register.pulse_overflow_rst() return overflows def add_time_domain_calibration(self, filename): self.time_domain_calibration_values = {} with open(filename) as f: offsets = json.load(f) for a, b in self.cross_combinations: comb_str = "{a}x{b}".format(a = a, b = b) self.time_domain_calibration_values[(a, b)] = offsets[comb_str] def add_frequency_bin_calibrations(self, filename): with open(filename) as f: offsets = json.load(f) frequencies = offsets['axis'] for a, b in self.cross_combinations: self.frequency_correlations[(a, b)].add_frequency_bin_calibration( frequencies = frequencies, phases = offsets["{a}{b}".format(a = a, b = b)]) def add_cable_length_calibrations(self, filename): """ Filename should be a json file with cable lengths for each antenna and velocity factors { "0": { "length": 0.5, "velocity factor": 0.66 }, """ with open(filename) as f: cables = json.load(f) self.time_domain_calibration_cable_values = {} for a, b in self.cross_combinations: length_a = cables[str(a)]['length'] velocity_factor_a = cables[str(a)]['velocity factor'] length_b = cables[str(b)]['length'] velocity_factor_b = cables[str(b)]['velocity factor'] # For the frequency domain: self.frequency_correlations[(a, b)].add_cable_length_calibration( length_a = length_a, velocity_factor_a = velocity_factor_a, length_b = length_b, velocity_factor_b = velocity_factor_b) # For the time domain: t_a = length_a / (scipy.constants.c * velocity_factor_a) t_b = length_b / (scipy.constants.c * velocity_factor_b) # this is how much b is delayed from a by as a result of the cable. # b delayed from a by a positive amount will mean that the (a, b) correlation # peak will be more positive than it should be. Subtract this to compensate. self.time_domain_calibration_cable_values[(a, b)] = t_b - t_a def apply_frequency_domain_calibrations(self): for a, b in self.cross_combinations: self.frequency_correlations[(a, b)].apply_frequency_domain_calibrations()