class RoachHeterodyne(RoachInterface): initial_values_for_writeable_registers = { 'chans': -1, # this isn't a register, but this will make the read table invalid 'dacctrl': 0, 'debug': 0, 'dout_ctrl': 0, 'dram_bank': 0, 'dram_mask': 0, 'dram_rst': 0, 'fftout_ctrl': 0, 'fftshift': 0, 'gpioa': 0, 'gpiob': 0, 'i0_ctrl': 0, 'q0_ctrl': 0, 'streamid': 0, 'sync': 0, } def __init__(self, roach=None, wafer=0, roachip='roach', adc_valon=None, host_ip=None, initialize=False, nfs_root='/srv/roach_boot/etch', lo_valon=None, attenuator=None, use_config=True): """ Class to represent the heterodyne readout system (high-frequency (1.5 GHz), IQ mixers) roach: an FpgaClient instance for communicating with the ROACH. If not specified, will try to instantiate one connected to *roachip* wafer: 0 Not used for heterodyne system roachip: (optional). Network address of the ROACH if you don't want to provide an FpgaClient adc_valon: a Valon class, a string, or None Provide access to the Valon class which controls the Valon synthesizer which provides the ADC and DAC sampling clock. The default None value will use the valon.find_valon function to locate a synthesizer and create a Valon class for you. You can alternatively pass a string such as '/dev/ttyUSB0' to specify the port for the synthesizer, which will then be used for creating a Valon class. Finally, for test suites, you can directly pass a Valon class or a class with the same interface. """ super(RoachHeterodyne,self).__init__(roach=roach, roachip=roachip, adc_valon=adc_valon, host_ip=host_ip, nfs_root=nfs_root, lo_valon=lo_valon) self.lo_frequency = 0.0 self.heterodyne = True #self.boffile = 'iq2xpfb14mcr7_2015_Nov_25_0907.bof' #self.boffile = 'iq2xpfb14mcr10_2016_Jun_29_1532.bof' self.boffile = 'iq2xpfb14mcr11_2016_Jun_30_1301.bof' self.iq_delay = 0 self.channel_selection_offset=3 self.wafer = wafer self.raw_adc_ns = 2 ** 12 # number of samples in the raw ADC buffer self.nfft = 2 ** 14 self.fpga_cycles_per_filterbank_frame = 2**13 self._fpga_output_buffer = 'ppout%d' % wafer self._general_setup() self.demodulator = Demodulator(hardware_delay_samples=self.hardware_delay_estimate * self.fs * 1e6) self.attenuator = attenuator if initialize: self.initialize(use_config=use_config) def get_raw_adc(self): """ Grab raw ADC samples returns: s0,s1 s0 and s1 are the samples from adc 0 and adc 1 respectively Each sample is a 12 bit signed integer (cast to a numpy float) """ if self._using_mock_roach: return np.random.randint(-2048,2047,size=self.raw_adc_ns),np.random.randint(-2048,2047,size=self.raw_adc_ns) self.r.write_int('adc_snap_ctrl', 0) self.r.write_int('adc_snap_ctrl', 5) s0 = (np.fromstring(self.r.read('adc_snap_bram', self.raw_adc_ns * 2 * 2), dtype='>i2')) sb = s0.view('>i4') i = sb[::2].copy().view('>i2') / 16. q = sb[1::2].copy().view('>i2') / 16. return i, q def find_best_iq_delay(self,iq_delay_range=np.arange(-4,5),set_tones=True,make_plot=False): if set_tones: self.set_tone_baseband_freqs(np.hstack((np.linspace(-220,-10,8),np.linspace(10,220,8)+2)),nsamp=2**16) best_delay,best_rejection = find_best_iq_delay_adc(self,iq_delay_range=iq_delay_range,make_plot=make_plot) if best_rejection < 15: logger.warning("Best image rejection was only %.1f dB at iq_delay=%d, which is suspiciously low.\nCheck " "connections and " "try running with make_plot=True to diagnose" % (best_rejection,best_rejection)) self.iq_delay = best_delay logger.debug("iq_delay set to %d" % best_delay) return best_delay,best_rejection def set_loopback(self,enable): self.loopback = enable if enable: self.r.write_int('sync',2) else: self.r.write_int('sync',0) def load_waveforms(self, i_wave, q_wave, fast=True, start_offset=0): """ Load waveforms for the two DACs i_wave,q_wave : arrays of 16-bit (dtype='i2') integers with waveforms for the two DACs fast : boolean decide what method for loading the dram """ data = np.zeros((2 * i_wave.shape[0],), dtype='>i2') data[0::4] = i_wave[::2] data[1::4] = i_wave[1::2] data[2::4] = q_wave[::2] data[3::4] = q_wave[1::2] self._load_dram(data, fast=fast, start_offset=start_offset*data.shape[0]) def set_tone_freqs(self, freqs, nsamp, amps=None, preset_norm=True, **kwargs): baseband_freqs = freqs-self.lo_frequency actual_baseband_freqs = self.set_tone_baseband_freqs(baseband_freqs,nsamp,amps=amps,preset_norm=preset_norm, **kwargs) return actual_baseband_freqs + self.lo_frequency def set_tone_baseband_freqs(self, freqs, nsamp, amps=None, preset_norm=True, **kwargs): """ Set the stimulus tones to generate freqs : array of frequencies in MHz For Heterodyne system, these can be positive or negative to produce tones above and below the local oscillator frequency. nsamp : int, must be power of 2 number of samples in the playback buffer. Frequency resolution will be fs/nsamp amps : optional array of floats, same length as freqs array specify the relative amplitude of each tone. Can set to zero to read out a portion of the spectrum with no stimulus tone. returns: actual_freqs : array of the actual frequencies after quantization based on nsamp """ bins = np.round((freqs / self.fs) * nsamp).astype('int') actual_freqs = self.fs * bins / float(nsamp) bins[bins < 0] = nsamp + bins[bins < 0] #self.set_tone_bins(bins, nsamp, amps=amps, phases=self.phases, **kwargs) self.set_tone_bins(bins, nsamp, amps=amps, preset_norm=preset_norm, **kwargs) self.fft_bins = self.calc_fft_bins(bins, nsamp) readout_selection = range(self.fft_bins.shape[1]) self.select_bank(0) self.select_fft_bins(readout_selection) self.save_state() return actual_freqs @property def tone_baseband_frequencies(self): actual_freqs = self.fs * self.tone_bins / float(self.tone_nsamp) actual_freqs[actual_freqs>self.fs/2.0] = actual_freqs[actual_freqs>self.fs/2.0] - self.fs return actual_freqs @property def tone_frequencies(self): return self.tone_baseband_frequencies + self.lo_frequency def add_tone_freqs(self, freqs, amps=None, overwrite_last=False): baseband_freqs = freqs-self.lo_frequency actual_baseband_freqs = self.add_tone_baseband_freqs(baseband_freqs, amps=amps, overwrite_last=overwrite_last) return actual_baseband_freqs + self.lo_frequency def add_tone_baseband_freqs(self, freqs, amps=None, overwrite_last=False): if freqs.shape[0] != self.tone_bins.shape[1]: raise ValueError("freqs array must contain same number of tones as original waveforms") # This is a hack that doesn't handle bank selection at all and may have additional problems. if overwrite_last: # Delete the last waveform and readout selection entry. self.tone_bins = self.tone_bins[:-1, :] self.fft_bins = self.fft_bins[:-1, :] nsamp = self.tone_nsamp bins = np.round((freqs / self.fs) * nsamp).astype('int') actual_freqs = self.fs * bins / float(nsamp) bins[bins < 0] = nsamp + bins[bins < 0] self.add_tone_bins(bins, amps=amps) self.fft_bins = np.vstack((self.fft_bins, self.calc_fft_bins(bins, nsamp))) self.save_state() return actual_freqs def set_tone_bins(self, bins, nsamp, amps=None, load=True, normfact=None, phases=None, preset_norm=True): """ Set the stimulus tones by specific integer bins bins : array of bins at which tones should be placed For Heterodyne system, negative frequencies should be placed in cannonical FFT order If 2d, interpret as (nwaves,ntones) nsamp : int, must be power of 2 number of samples in the playback buffer. Frequency resolution will be fs/nsamp amps : optional array of floats, same length as bins array specify the relative amplitude of each tone. Can set to zero to read out a portion of the spectrum with no stimulus tone. load : bool (debug only). If false, don't actually load the waveform, just calculate it. """ if bins.ndim == 1: bins.shape = (1, bins.shape[0]) nwaves = bins.shape[0] spec = np.zeros((nwaves, nsamp), dtype='complex') self.tone_bins = bins.copy() self.tone_nsamp = nsamp #this is to make sure phases are correct shape since we are reusing phases if phases is None or phases.shape[0] != bins.shape[1]: phases = np.random.random(bins.shape[1]) * 2 * np.pi self.phases = phases.copy() if amps is None: amps = 1.0 self.amps = amps for k in range(nwaves): spec[k, bins[k, :]] = amps * np.exp(1j * phases) wave = np.fft.ifft(spec, axis=1) if preset_norm: self.wavenorm = calc_wavenorm(bins.shape[1], nsamp) else: self.wavenorm = np.abs(wave).max() if normfact is not None: wn = (2.0 / normfact) * len(bins) / float(nsamp) print "ratio of current wavenorm to optimal:", self.wavenorm / wn self.wavenorm = wn q_rwave = np.round((wave.real / self.wavenorm) * (2 ** 15 - 1024)).astype('>i2') q_iwave = np.round((wave.imag / self.wavenorm) * (2 ** 15 - 1024)).astype('>i2') q_iwave = np.roll(q_iwave, self.iq_delay, axis=1) q_rwave.shape = (q_rwave.shape[0] * q_rwave.shape[1],) q_iwave.shape = (q_iwave.shape[0] * q_iwave.shape[1],) self.q_rwave = q_rwave self.q_iwave = q_iwave if load: self.load_waveforms(q_rwave,q_iwave) self.save_state() def add_tone_bins(self, bins, amps=None, preset_norm=True): nsamp = self.tone_nsamp spec = np.zeros((nsamp,), dtype='complex') self.tone_bins = np.vstack((self.tone_bins, bins)) phases = self.phases if amps is None: amps = 1.0 # self.amps = amps # TODO: Need to figure out how to deal with this spec[bins] = amps * np.exp(1j * phases) wave = np.fft.ifft(spec) if preset_norm: self.wavenorm = calc_wavenorm(self.tone_bins.shape[1], nsamp) else: self.wavenorm = np.abs(wave).max() q_rwave = np.round((wave.real / self.wavenorm) * (2 ** 15 - 1024)).astype('>i2') q_iwave = np.round((wave.imag / self.wavenorm) * (2 ** 15 - 1024)).astype('>i2') q_iwave = np.roll(q_iwave, self.iq_delay, axis=0) start_offset = self.tone_bins.shape[0] - 1 self.load_waveforms(q_rwave, q_iwave, start_offset=start_offset) self.save_state() def calc_fft_bins(self, tone_bins, nsamp): """ Calculate the FFT bins in which the tones will fall tone_bins: array of integers the tone bins (0 to nsamp - 1) which contain tones nsamp : length of the playback bufffer returns: fft_bins, array of integers. """ tone_bins_per_fft_bin = nsamp / float(self.nfft) fft_bins = np.round(tone_bins / tone_bins_per_fft_bin).astype('int') return fft_bins def fft_bin_to_index(self, bins): """ Convert FFT bins to FPGA indexes """ idx = bins.copy() return idx def select_fft_bins(self, readout_selection, sync=True): """ Select which subset of the available FFT bins to read out Initially we can only read out from a subset of the FFT bins, so this function selects which bins to read out right now This also takes care of writing the selection to the FPGA with the appropriate tweaks The readout selection is stored to self.readout_selection The FPGA readout indexes is stored in self.fpga_fft_readout_indexes The bins that we are reading out is stored in self.readout_fft_bins readout_selection : array of ints indexes into the self.fft_bins array to specify the bins to read out """ idxs = self.fft_bin_to_index(self.fft_bins[self.bank,readout_selection]) order = idxs.argsort() idxs = idxs[order] if np.any(np.diff(idxs//2)==0): failures = np.flatnonzero(np.diff(idxs//2)==0) raise ValueError("Selected filterbank channels are too close together.\nChannels 2*k and 2*k+1 cannot be" "read out together.\n" "The requested channel indexes are: %s\n" "and the failing channel indexes are: %s" % (str(idxs), '; '.join([('%d,%d'% (x,x+1)) for x in idxs[failures]]))) self.readout_selection = np.array(readout_selection)[order] self.fpga_fft_readout_indexes = idxs self.readout_fft_bins = self.fft_bins[self.bank, self.readout_selection] binsel = np.zeros((self.nfft//2,), dtype='u1') # values in this array are 1 for even channels and 3 for odd channels evenodd = np.mod(self.fpga_fft_readout_indexes,2)*2+1 binsel_index = np.mod(self.fpga_fft_readout_indexes//2-self.channel_selection_offset,self.nfft//2) binsel[binsel_index] = evenodd self.r.write('chans', binsel.tostring()) if sync: self._sync() def demodulate_data(self,data,seq_nos=None): bank = self.bank demod = np.zeros_like(data) for n, ich in enumerate(self.readout_selection): demod[:,n] = self.demodulator.demodulate(data[:,n], tone_bin=self.tone_bins[bank,ich], tone_num_samples=self.tone_nsamp, tone_phase=self.phases[ich], fft_bin=self.fft_bins[bank,ich], nchan=self.readout_selection.shape[0], seq_nos=seq_nos) return demod*self.wavenorm def get_stream_demodulator(self): return StreamDemodulator(tone_bins=self.tone_bins[self.bank,:], phases=self.phases, tone_nsamp=self.tone_nsamp, fft_bins=self.fft_bins[self.bank,:], nfft=self.nfft, num_taps=self.demodulator.num_taps, window=self.demodulator.window_function, hardware_delay_samples=self.demodulator.hardware_delay_samples) def demodulate_data_original(self, data): """ Demodulate the data from the FFT bin This function assumes that self.select_fft_bins was called to set up the necessary class attributes data : array of complex data returns : demodulated data in an array of the same shape and dtype as *data* """ bank = self.bank demod = np.zeros_like(data) t = np.arange(data.shape[0]) for n, ich in enumerate(self.readout_selection): phi0 = self.phases[ich] k = self.tone_bins[bank,ich] m = self.fft_bins[bank,ich] if m >= self.nfft / 2: sign = -1.0 doconj = True else: sign = -1.0 doconj = False nfft = self.nfft ns = self.tone_nsamp foffs = (k * nfft - m * ns) / float(ns) demod[:, n] = np.exp(sign * 1j * (2 * np.pi * foffs * t + phi0)) * data[:, n] if doconj: demod[:, n] = np.conjugate(demod[:, n]) return demod*self.wavenorm @property def blocks_per_second_per_channel(self): chan_rate = self.fs * 1e6 / (self.nfft) # samples per second for one tone_index samples_per_channel_per_block = 4096 return chan_rate / samples_per_channel_per_block def get_data(self, nread=2, demod=True): # TODO This is a temporary hack until we get the system simulation code in place if self._using_mock_roach: data = (np.random.standard_normal((nread * 4096, self.num_tones)) + 1j * np.random.standard_normal((nread * 4096, self.num_tones))) if self.r.sleep_for_fake_data: time.sleep(nread / self.blocks_per_second) seqnos = np.arange(data.shape[0]) return data, seqnos else: return self.get_data_udp(nread=nread, demod=demod) def get_data_udp(self, nread=2, demod=True): chan_offset = 1 nch = self.fpga_fft_readout_indexes.shape[0] udp_channel = (self.fpga_fft_readout_indexes//2 + chan_offset) % (self.nfft//2) data, seqnos = kid_readout.roach.udp_catcher.get_udp_data(self, npkts=nread * 16, streamid=1, chans=udp_channel, nfft=self.nfft//2, addr=(self.host_ip, 12345)) # , stream_reg, addr) if demod: data = self.demodulate_data(data) return data, seqnos def get_data_katcp(self, nread=10, demod=True): """ Get a chunk of data nread: number of 4096 sample frames to read demod: should the data be demodulated before returning? Default, yes returns dout,addrs dout: complex data stream. Real and imaginary parts are each 16 bit signed integers (but cast to numpy complex) addrs: counter values when each frame was read. Can be used to check that frames are contiguous """ print "getting data" bufname = 'ppout%d' % self.wafer chan_offset = 2 draw, addr, ch = self._read_data(nread, bufname) if not np.all(ch == ch[0]): print "all channel registers not the same; this case not yet supported" return draw, addr, ch if not np.all(np.diff(addr) < 8192): print "address skip!" nch = self.readout_selection.shape[0] dout = draw.reshape((-1, nch)) shift = np.flatnonzero(self.fpga_fft_readout_indexes / 2 == (ch[0] - chan_offset))[0] - (nch - 1) print shift dout = np.roll(dout, shift, axis=1) if demod: dout = self.demodulate_data(dout) return dout, addr def set_lo(self, lomhz=1200.0, chan_spacing=2.0, modulator_lo_power=5, demodulator_lo_power=5): """ Set the local oscillator frequency for the IQ mixers lomhz: float, frequency in MHz lo_level: LO power level on the valon. options are [-4, -1, 2, 5] """ #TODO: Fix this after valon is updated if self.lo_valon is None: self.adc_valon.set_rf_level(8,2) self.adc_valon.set_frequency_b(lomhz, chan_spacing=chan_spacing) self.lo_frequency = lomhz self.save_state() else: #out1 goes to demod at 0dBm #out2 goes to mod at 5dBm power_settings = [-4, -1, 2, 5] if demodulator_lo_power in power_settings: self.lo_valon.set_rf_level(0,demodulator_lo_power) else: print "demodulator_lo_level not available, using full power" self.lo_valon.set_rf_level(0,5) if modulator_lo_power in power_settings: self.lo_valon.set_rf_level(8,modulator_lo_power) else: print "modulator_lo_level not available, using full power" self.lo_valon.set_rf_level(8,5) self.lo_valon.set_frequency_a(lomhz, chan_spacing=chan_spacing) self.lo_valon.set_frequency_b(lomhz, chan_spacing=chan_spacing) self.lo_frequency = lomhz self.save_state() def set_dac_attenuator(self, attendb): if self.attenuator is None: if attendb < 0 or attendb > 63: raise ValueError("DAC Attenuator must be between 0 and 63 dB. Value given was: %s" % str(attendb)) if attendb > 31.5: attena = 31.5 attenb = attendb - attena else: attena = attendb attenb = 0 self.set_attenuator(attena, le_bit=0x01) self.set_attenuator(attenb, le_bit=0x80) self.dac_atten = int(attendb * 2) / 2.0 else : self.attenuator.set_att(attendb) self.dac_atten = int(attendb * 2) / 2.0 logger.info("Set DAC attenuator to {:.1f} dB.".format(self.dac_atten)) def set_adc_attenuator(self, attendb): if attendb < 0 or attendb > 31.5: raise ValueError("ADC Attenuator must be between 0 and 31.5 dB. Value given was: %s" % str(attendb)) self.set_attenuator(attendb, le_bit=0x02) self.adc_atten = int(attendb * 2) / 2.0 def get_fftout_snap(self): self.r.wselfte_int('fftout_ctrl',0) self.r.wselfte_int('fftout_ctrl',1) self._sync() while self.r.read_int('fftout_status') != 0x8000: pass return np.fromstselfng(self.r.read('fftout_bram',2**15),dtype='>i2').astype('float').view('complex')
class RoachHeterodyne(RoachInterface): MEMORY_SIZE_BYTES = 2**28 # 256 MB initial_values_for_writeable_registers = { 'chans': -1, # this isn't a register, but this will make the read table invalid 'dacctrl': 0, 'debug': 0, 'dout_ctrl': 0, 'dram_bank': 0, 'dram_mask': 0, 'dram_rst': 0, 'fftout_ctrl': 0, 'fftshift': 0, 'gpioa': 0, 'gpiob': 0, 'i0_ctrl': 0, 'q0_ctrl': 0, 'streamid': 0, 'sync': 0, } def __init__(self, roach=None, wafer=0, roachip='roach', adc_valon=None, host_ip=None, initialize=False, nfs_root='/srv/roach_boot/etch', lo_valon=None, attenuator=None, use_config=True): """ Class to represent the heterodyne readout system (high-frequency with IQ mixers) roach: an FpgaClient instance for communicating with the ROACH. If not specified, will try to instantiate one connected to *roachip* wafer: 0 Not used for heterodyne system roachip: (optional). Network address of the ROACH if you don't want to provide an FpgaClient adc_valon: a Valon class, a string, or None Provide access to the Valon class which controls the Valon synthesizer which provides the ADC and DAC sampling clock. The default None value will use the valon.find_valon function to locate a synthesizer and create a Valon class for you. You can alternatively pass a string such as '/dev/ttyUSB0' to specify the port for the synthesizer, which will then be used for creating a Valon class. Finally, for test suites, you can directly pass a Valon class or a class with the same interface. """ super(RoachHeterodyne, self).__init__(roach=roach, roachip=roachip, adc_valon=adc_valon, host_ip=host_ip, nfs_root=nfs_root, lo_valon=lo_valon) self.lo_frequency = 0.0 self.heterodyne = True #self.boffile = 'iq2xpfb14mcr7_2015_Nov_25_0907.bof' #self.boffile = 'iq2xpfb14mcr10_2016_Jun_29_1532.bof' self.boffile = 'iq2xpfb14mcr11_2016_Jun_30_1301.bof' self.iq_delay = 0 self.channel_selection_offset = 3 self.wafer = wafer self.raw_adc_ns = 2**12 # number of samples in the raw ADC buffer self.nfft = 2**14 self.fpga_cycles_per_filterbank_frame = 2**13 self._fpga_output_buffer = 'ppout%d' % wafer self._general_setup() self.demodulator = Demodulator( hardware_delay_samples=self.hardware_delay_estimate * self.fs * 1e6) self.attenuator = attenuator if initialize: self.initialize(use_config=use_config) def get_raw_adc(self): """ Grab raw ADC samples returns: s0,s1 s0 and s1 are the samples from adc 0 and adc 1 respectively Each sample is a 12 bit signed integer (cast to a numpy float) """ if self._using_mock_roach: return np.random.randint(-2048, 2047, size=self.raw_adc_ns), np.random.randint( -2048, 2047, size=self.raw_adc_ns) self.r.write_int('adc_snap_ctrl', 0) self.r.write_int('adc_snap_ctrl', 5) s0 = (np.fromstring(self.r.read('adc_snap_bram', self.raw_adc_ns * 2 * 2), dtype='>i2')) sb = s0.view('>i4') i = sb[::2].copy().view('>i2') / 16. q = sb[1::2].copy().view('>i2') / 16. return i, q def find_best_iq_delay(self, iq_delay_range=np.arange(-4, 5), set_tones=True, make_plot=False): if set_tones: self.set_tone_baseband_freqs(np.hstack( (np.linspace(-220, -10, 8), np.linspace(10, 220, 8) + 2)), nsamp=2**16) best_delay, best_rejection = find_best_iq_delay_adc( self, iq_delay_range=iq_delay_range, make_plot=make_plot) if best_rejection < 15: logger.warning( "Best image rejection was only %.1f dB at iq_delay=%d, which is suspiciously low.\nCheck " "connections and " "try running with make_plot=True to diagnose" % (best_rejection, best_rejection)) self.iq_delay = best_delay logger.debug("iq_delay set to %d" % best_delay) return best_delay, best_rejection def set_loopback(self, enable): self.loopback = enable if enable: self.r.write_int('sync', 2) else: self.r.write_int('sync', 0) def load_waveforms(self, i_wave, q_wave, fast=True, start_offset=0): """ Load waveforms for the two DACs i_wave,q_wave : arrays of 16-bit (dtype='i2') integers with waveforms for the two DACs fast : boolean decide what method for loading the dram """ data = np.zeros((2 * i_wave.shape[0], ), dtype='>i2') data[0::4] = i_wave[::2] data[1::4] = i_wave[1::2] data[2::4] = q_wave[::2] data[3::4] = q_wave[1::2] self._load_dram(data, fast=fast, start_offset=start_offset * data.shape[0]) def set_tone_freqs(self, freqs, nsamp, amps=None, preset_norm=True, **kwargs): baseband_freqs = freqs - self.lo_frequency actual_baseband_freqs = self.set_tone_baseband_freqs( baseband_freqs, nsamp, amps=amps, preset_norm=preset_norm, **kwargs) return actual_baseband_freqs + self.lo_frequency def set_tone_baseband_freqs(self, freqs, nsamp, amps=None, preset_norm=True, **kwargs): """ Set the stimulus tones to generate freqs : array of frequencies in MHz For Heterodyne system, these can be positive or negative to produce tones above and below the local oscillator frequency. nsamp : int, must be power of 2 number of samples in the playback buffer. Frequency resolution will be fs/nsamp amps : optional array of floats, same length as freqs array specify the relative amplitude of each tone. Can set to zero to read out a portion of the spectrum with no stimulus tone. returns: actual_freqs : array of the actual frequencies after quantization based on nsamp """ bins = np.round((freqs / self.fs) * nsamp).astype('int') actual_freqs = self.fs * bins / float(nsamp) bins[bins < 0] = nsamp + bins[bins < 0] #self.set_tone_bins(bins, nsamp, amps=amps, phases=self.phases, **kwargs) self.set_tone_bins(bins, nsamp, amps=amps, preset_norm=preset_norm, **kwargs) self.fft_bins = self.calc_fft_bins(bins, nsamp) readout_selection = range(self.fft_bins.shape[1]) self.select_bank(0) self.select_fft_bins(readout_selection) self.save_state() return actual_freqs @property def tone_baseband_frequencies(self): actual_freqs = self.fs * self.tone_bins / float(self.tone_nsamp) actual_freqs[ actual_freqs > self.fs / 2.0] = actual_freqs[actual_freqs > self.fs / 2.0] - self.fs return actual_freqs @property def tone_frequencies(self): return self.tone_baseband_frequencies + self.lo_frequency def add_tone_freqs(self, freqs, amps=None, overwrite_last=False): baseband_freqs = freqs - self.lo_frequency actual_baseband_freqs = self.add_tone_baseband_freqs( baseband_freqs, amps=amps, overwrite_last=overwrite_last) return actual_baseband_freqs + self.lo_frequency def add_tone_baseband_freqs(self, freqs, amps=None, overwrite_last=False): if freqs.shape[0] != self.tone_bins.shape[1]: raise ValueError( "freqs array must contain same number of tones as original waveforms" ) # This is a hack that doesn't handle bank selection at all and may have additional problems. if overwrite_last: # Delete the last waveform and readout selection entry. self.tone_bins = self.tone_bins[:-1, :] self.fft_bins = self.fft_bins[:-1, :] nsamp = self.tone_nsamp bins = np.round((freqs / self.fs) * nsamp).astype('int') actual_freqs = self.fs * bins / float(nsamp) bins[bins < 0] = nsamp + bins[bins < 0] self.add_tone_bins(bins, amps=amps) self.fft_bins = np.vstack( (self.fft_bins, self.calc_fft_bins(bins, nsamp))) self.save_state() return actual_freqs def set_tone_bins(self, bins, nsamp, amps=None, load=True, normfact=None, phases=None, preset_norm=True): """ Set the stimulus tones by specific integer bins bins : array of bins at which tones should be placed For Heterodyne system, negative frequencies should be placed in cannonical FFT order If 2d, interpret as (nwaves,ntones) nsamp : int, must be power of 2 number of samples in the playback buffer. Frequency resolution will be fs/nsamp amps : optional array of floats, same length as bins array specify the relative amplitude of each tone. Can set to zero to read out a portion of the spectrum with no stimulus tone. load : bool (debug only). If false, don't actually load the waveform, just calculate it. """ if self.BYTES_PER_SAMPLE * nsamp > self.MEMORY_SIZE_BYTES: message = "Requested tone size ({:d} bytes) exceeds available memory ({:d} bytes)" raise ValueError( message.format(self.BYTES_PER_SAMPLE * nsamp, self.MEMORY_SIZE_BYTES)) if bins.ndim == 1: bins.shape = (1, bins.shape[0]) nwaves = bins.shape[0] spec = np.zeros((nwaves, nsamp), dtype='complex') self.tone_bins = bins.copy() self.tone_nsamp = nsamp #this is to make sure phases are correct shape since we are reusing phases if phases is None or phases.shape[0] != bins.shape[1]: phases = np.random.random(bins.shape[1]) * 2 * np.pi self.phases = phases.copy() if amps is None: amps = 1.0 self.amps = amps for k in range(nwaves): spec[k, bins[k, :]] = amps * np.exp(1j * phases) wave = np.fft.ifft(spec, axis=1) if preset_norm: self.wavenorm = calc_wavenorm(bins.shape[1], nsamp) else: self.wavenorm = np.abs(wave).max() if normfact is not None: wn = (2.0 / normfact) * len(bins) / float(nsamp) print "ratio of current wavenorm to optimal:", self.wavenorm / wn self.wavenorm = wn q_rwave = np.round( (wave.real / self.wavenorm) * (2**15 - 1024)).astype('>i2') q_iwave = np.round( (wave.imag / self.wavenorm) * (2**15 - 1024)).astype('>i2') q_iwave = np.roll(q_iwave, self.iq_delay, axis=1) q_rwave.shape = (q_rwave.shape[0] * q_rwave.shape[1], ) q_iwave.shape = (q_iwave.shape[0] * q_iwave.shape[1], ) self.q_rwave = q_rwave self.q_iwave = q_iwave if load: self.load_waveforms(q_rwave, q_iwave) self.save_state() def add_tone_bins(self, bins, amps=None, preset_norm=True): nsamp = self.tone_nsamp spec = np.zeros((nsamp, ), dtype='complex') self.tone_bins = np.vstack((self.tone_bins, bins)) phases = self.phases if amps is None: amps = 1.0 # self.amps = amps # TODO: Need to figure out how to deal with this spec[bins] = amps * np.exp(1j * phases) wave = np.fft.ifft(spec) if preset_norm: self.wavenorm = calc_wavenorm(self.tone_bins.shape[1], nsamp) else: self.wavenorm = np.abs(wave).max() q_rwave = np.round( (wave.real / self.wavenorm) * (2**15 - 1024)).astype('>i2') q_iwave = np.round( (wave.imag / self.wavenorm) * (2**15 - 1024)).astype('>i2') q_iwave = np.roll(q_iwave, self.iq_delay, axis=0) start_offset = self.tone_bins.shape[0] - 1 self.load_waveforms(q_rwave, q_iwave, start_offset=start_offset) self.save_state() def calc_fft_bins(self, tone_bins, nsamp): """ Calculate the FFT bins in which the tones will fall tone_bins: array of integers the tone bins (0 to nsamp - 1) which contain tones nsamp : length of the playback bufffer returns: fft_bins, array of integers. """ tone_bins_per_fft_bin = nsamp / float(self.nfft) fft_bins = np.round(tone_bins / tone_bins_per_fft_bin).astype('int') return fft_bins def fft_bin_to_index(self, bins): """ Convert FFT bins to FPGA indexes """ idx = bins.copy() return idx def select_fft_bins(self, readout_selection, sync=True): """ Select which subset of the available FFT bins to read out Initially we can only read out from a subset of the FFT bins, so this function selects which bins to read out right now This also takes care of writing the selection to the FPGA with the appropriate tweaks The readout selection is stored to self.readout_selection The FPGA readout indexes is stored in self.fpga_fft_readout_indexes The bins that we are reading out is stored in self.readout_fft_bins readout_selection : array of ints indexes into the self.fft_bins array to specify the bins to read out """ idxs = self.fft_bin_to_index(self.fft_bins[self.bank, readout_selection]) order = idxs.argsort() idxs = idxs[order] if np.any(np.diff(idxs // 2) == 0): failures = np.flatnonzero(np.diff(idxs // 2) == 0) raise ValueError( "Selected filterbank channels are too close together.\nChannels 2*k and 2*k+1 cannot be" "read out together.\n" "The requested channel indexes are: %s\n" "and the failing channel indexes are: %s" % (str(idxs), '; '.join([('%d,%d' % (x, x + 1)) for x in idxs[failures]]))) self.readout_selection = np.array(readout_selection)[order] self.fpga_fft_readout_indexes = idxs self.readout_fft_bins = self.fft_bins[self.bank, self.readout_selection] binsel = np.zeros((self.nfft // 2, ), dtype='u1') # values in this array are 1 for even channels and 3 for odd channels evenodd = np.mod(self.fpga_fft_readout_indexes, 2) * 2 + 1 binsel_index = np.mod( self.fpga_fft_readout_indexes // 2 - self.channel_selection_offset, self.nfft // 2) binsel[binsel_index] = evenodd self.r.write('chans', binsel.tostring()) if sync: self._sync() def demodulate_data(self, data, seq_nos=None): bank = self.bank demod = np.zeros_like(data) for n, ich in enumerate(self.readout_selection): demod[:, n] = self.demodulator.demodulate( data[:, n], tone_bin=self.tone_bins[bank, ich], tone_num_samples=self.tone_nsamp, tone_phase=self.phases[ich], fft_bin=self.fft_bins[bank, ich], nchan=self.readout_selection.shape[0], seq_nos=seq_nos) return demod * self.wavenorm def get_stream_demodulator(self): return StreamDemodulator( tone_bins=self.tone_bins[self.bank, :], phases=self.phases, tone_nsamp=self.tone_nsamp, fft_bins=self.fft_bins[self.bank, :], nfft=self.nfft, num_taps=self.demodulator.num_taps, window=self.demodulator.window_function, hardware_delay_samples=self.demodulator.hardware_delay_samples) def demodulate_data_original(self, data): """ Demodulate the data from the FFT bin This function assumes that self.select_fft_bins was called to set up the necessary class attributes data : array of complex data returns : demodulated data in an array of the same shape and dtype as *data* """ bank = self.bank demod = np.zeros_like(data) t = np.arange(data.shape[0]) for n, ich in enumerate(self.readout_selection): phi0 = self.phases[ich] k = self.tone_bins[bank, ich] m = self.fft_bins[bank, ich] if m >= self.nfft / 2: sign = -1.0 doconj = True else: sign = -1.0 doconj = False nfft = self.nfft ns = self.tone_nsamp foffs = (k * nfft - m * ns) / float(ns) demod[:, n] = np.exp(sign * 1j * (2 * np.pi * foffs * t + phi0)) * data[:, n] if doconj: demod[:, n] = np.conjugate(demod[:, n]) return demod * self.wavenorm @property def blocks_per_second_per_channel(self): chan_rate = self.fs * 1e6 / (self.nfft ) # samples per second for one tone_index samples_per_channel_per_block = 4096 return chan_rate / samples_per_channel_per_block def get_data(self, nread=2, demod=True): # TODO This is a temporary hack until we get the system simulation code in place if self._using_mock_roach: data = (np.random.standard_normal((nread * 4096, self.num_tones)) + 1j * np.random.standard_normal( (nread * 4096, self.num_tones))) if self.r.sleep_for_fake_data: time.sleep(nread / self.blocks_per_second) seqnos = np.arange(data.shape[0]) return data, seqnos else: return self.get_data_udp(nread=nread, demod=demod) def get_data_udp(self, nread=2, demod=True): chan_offset = 1 nch = self.fpga_fft_readout_indexes.shape[0] udp_channel = (self.fpga_fft_readout_indexes // 2 + chan_offset) % (self.nfft // 2) data, seqnos = kid_readout.roach.udp_catcher.get_udp_data( self, npkts=nread * 16, streamid=1, chans=udp_channel, nfft=self.nfft // 2, addr=(self.host_ip, 12345)) # , stream_reg, addr) if demod: data = self.demodulate_data(data) return data, seqnos def get_data_katcp(self, nread=10, demod=True): """ Get a chunk of data nread: number of 4096 sample frames to read demod: should the data be demodulated before returning? Default, yes returns dout,addrs dout: complex data stream. Real and imaginary parts are each 16 bit signed integers (but cast to numpy complex) addrs: counter values when each frame was read. Can be used to check that frames are contiguous """ print "getting data" bufname = 'ppout%d' % self.wafer chan_offset = 2 draw, addr, ch = self._read_data(nread, bufname) if not np.all(ch == ch[0]): print "all channel registers not the same; this case not yet supported" return draw, addr, ch if not np.all(np.diff(addr) < 8192): print "address skip!" nch = self.readout_selection.shape[0] dout = draw.reshape((-1, nch)) shift = np.flatnonzero(self.fpga_fft_readout_indexes / 2 == (ch[0] - chan_offset))[0] - (nch - 1) print shift dout = np.roll(dout, shift, axis=1) if demod: dout = self.demodulate_data(dout) return dout, addr def set_lo(self, lomhz=1200.0, chan_spacing=2.0, modulator_lo_power=5, demodulator_lo_power=5): """ Set the local oscillator frequency for the IQ mixers lomhz: float, frequency in MHz lo_level: LO power level on the valon. options are [-4, -1, 2, 5] """ #TODO: Fix this after valon is updated # ToDo: move logging to ValonSynth if self.lo_valon is None: self.adc_valon.set_rf_level(8, 2) self.adc_valon.set_frequency_b(lomhz, chan_spacing=chan_spacing) logger.info("Set ADC Valon frequency B to {:.4f} MHz " "with channel spacing {:.4f} MHz".format( lomhz, chan_spacing)) self.lo_frequency = lomhz self.save_state() else: #out1 goes to demod at 0dBm #out2 goes to mod at 5dBm power_settings = [-4, -1, 2, 5] if demodulator_lo_power in power_settings: self.lo_valon.set_rf_level(0, demodulator_lo_power) else: print "demodulator_lo_level not available, using full power" self.lo_valon.set_rf_level(0, 5) if modulator_lo_power in power_settings: self.lo_valon.set_rf_level(8, modulator_lo_power) else: print "modulator_lo_level not available, using full power" self.lo_valon.set_rf_level(8, 5) self.lo_valon.set_frequency_a(lomhz, chan_spacing=chan_spacing) self.lo_valon.set_frequency_b(lomhz, chan_spacing=chan_spacing) logger.info("Set LO Valon frequencies A and B to {:.4f} MHz" " with channel spacing {:.4f} MHz".format( lomhz, chan_spacing)) self.lo_frequency = lomhz self.save_state() def set_dac_attenuator(self, attendb): if self.attenuator is None: if attendb < 0 or attendb > 63: raise ValueError( "DAC Attenuator must be between 0 and 63 dB. Value given was: %s" % str(attendb)) if attendb > 31.5: attena = 31.5 attenb = attendb - attena else: attena = attendb attenb = 0 self.set_attenuator(attena, le_bit=0x01) self.set_attenuator(attenb, le_bit=0x80) self.dac_atten = int(attendb * 2) / 2.0 else: self.attenuator.set_att(attendb) self.dac_atten = int(attendb * 2) / 2.0 logger.info("Set DAC attenuator to {:.1f} dB.".format(self.dac_atten)) def set_adc_attenuator(self, attendb): if attendb < 0 or attendb > 31.5: raise ValueError( "ADC Attenuator must be between 0 and 31.5 dB. Value given was: %s" % str(attendb)) self.set_attenuator(attendb, le_bit=0x02) self.adc_atten = int(attendb * 2) / 2.0 def get_fftout_snap(self): self.r.wselfte_int('fftout_ctrl', 0) self.r.wselfte_int('fftout_ctrl', 1) self._sync() while self.r.read_int('fftout_status') != 0x8000: pass return np.fromstselfng(self.r.read('fftout_bram', 2**15), dtype='>i2').astype('float').view('complex')