openquantumhardware / qick

QICK: Quantum Instrumentation Control Kit

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Resetting ADC phase

tristanlorriaux opened this issue · comments

Hi there !

We are trying to acquire a signal at a different frequency than the drive, and to demodulate it with coherent phase. To do this we need seperate generators for the DAC and the ADC, and to reset the phase of both at the beginning of each sequence, which we apparently cannot do. From issue 120 we understand this may be due to the ADC being now controlled by the PYNQ, not the tProc, so the latter connot adjust its phase mid-flight. Is this interpretation correct, and if so, is there any cheaper solution other than sacrificing two tProc channels for ADCs? Because we sure can use more DAC channels.

We are using the ZCU216, with the basic firmware:

	Board: ZCU216
	Global clocks (MHz): tProcessor 430.080, RF reference 245.760
	7 signal generator channels:
	0:	axis_signal_gen_v4 - tProc output 1, envelope memory 65536 samples
		DAC tile 2, ch 0, 32-bit DDS, fabric=430.080 MHz, fs=6881.280 MHz
	1:	axis_signal_gen_v4 - tProc output 2, envelope memory 65536 samples
		DAC tile 2, ch 1, 32-bit DDS, fabric=430.080 MHz, fs=6881.280 MHz
	2:	axis_signal_gen_v4 - tProc output 3, envelope memory 65536 samples
		DAC tile 2, ch 2, 32-bit DDS, fabric=430.080 MHz, fs=6881.280 MHz
	3:	axis_signal_gen_v4 - tProc output 4, envelope memory 65536 samples
		DAC tile 2, ch 3, 32-bit DDS, fabric=430.080 MHz, fs=6881.280 MHz
	4:	axis_signal_gen_v4 - tProc output 5, envelope memory 65536 samples
		DAC tile 3, ch 0, 32-bit DDS, fabric=430.080 MHz, fs=6881.280 MHz
	5:	axis_signal_gen_v4 - tProc output 6, envelope memory 65536 samples
		DAC tile 3, ch 1, 32-bit DDS, fabric=430.080 MHz, fs=6881.280 MHz
	6:	axis_signal_gen_v4 - tProc output 7, envelope memory 65536 samples
		DAC tile 3, ch 2, 32-bit DDS, fabric=430.080 MHz, fs=6881.280 MHz
	2 readout channels:
	0:	axis_readout_v2 - controlled by PYNQ
		ADC tile 2, ch 0, 32-bit DDS, fabric=307.200 MHz, fs=2457.600 MHz
		maxlen 16384 (avg) 1024 (decimated), trigger bit 14, tProc input 0
	1:	axis_readout_v2 - controlled by PYNQ
		ADC tile 2, ch 2, 32-bit DDS, fabric=307.200 MHz, fs=2457.600 MHz
		maxlen 16384 (avg) 1024 (decimated), trigger bit 15, tProc input 1
	7 DACs:
		DAC tile 2, ch 0 is 0_230, on JHC3
		DAC tile 2, ch 1 is 1_230, on JHC4
		DAC tile 2, ch 2 is 2_230, on JHC3
		DAC tile 2, ch 3 is 3_230, on JHC4
		DAC tile 3, ch 0 is 0_231, on JHC3
		DAC tile 3, ch 1 is 1_231, on JHC4
		DAC tile 3, ch 2 is 2_231, on JHC3
	2 ADCs:
		ADC tile 2, ch 0 is 0_226, on JHC7
		ADC tile 2, ch 2 is 2_226, on JHC7
	4 digital output pins (tProc output 0):
	0:	PMOD0_0_LS
	1:	PMOD0_1_LS
	2:	PMOD0_2_LS
	3:	PMOD0_3_LS
	tProc: program memory 8192 words, data memory 4096 words
		external start pin: None

And below is a minimum example, where we try to measure the DAC0 loopback to ADC1, offset by 10 MHz, and get 10 traces with random phase.

class loopBack(AveragerProgram):
    def initialize(self):
        cfg=self.cfg
        # define the oscillator
        self.declare_gen(ch=0, nqz=1) #Readout
        f_res=self.freq2reg(210, gen_ch=0, ro_ch=1) # conver f_res to dac register value
        f_ro = soc.adcfreq(200, 0, 1)
        self.set_pulse_registers(ch=0, style="const", freq=f_res, phase=0, gain=10000,
                                 length=self.us2cycles(40))
        self.declare_readout(ch=1, length=100, freq=f_ro)
        self.synci(200)
    def body(self):
        cfg=self.cfg
        self.reset_phase(gen_ch=[0])
        # self.reset_phase(gen_ch=[0], ro_ch=[1])
        # measure the DAC0 drive in ADC1
        self.measure(pulse_ch=[0], adcs=[1], wait=True, syncdelay=100, adc_trig_offset=150)config={"reps": 10,
        "res_gain": 10000,  # 30000, # [DAC units]
        "readout_length": 100, # [Clock ticks]
        "soft_avgs": 1,# 100000
       }
prog = loopBack(soccfg, config)
adc1, = prog.acquire_decimated(soc, load_pulses=True, progress=True, debug=False)
subplot(111, title=f"Averages = {config['soft_avgs']}", xlabel="Clock ticks", ylabel="Transmission (adc levels)")
plot(np.transpose(adc1[:, 0]), label="I value; ADC 1")

Output of loopBack:
Figure 1

Your understanding is correct.

If I understand correctly what you want to do, it's solvable without phase reset. You want phase-coherent upconversion/downconversion in the generator and readout, so you should frequency-match them and not change their phase. You want to play a tone with a frequency offset: you can do this by applying the frequency offset to the envelope.

class FreqShiftProgram(AveragerProgram):
    def initialize(self):
        cfg = self.cfg
        style = cfg['style']
        for iCh, ch in enumerate(cfg["gen_chs"]):  # configure the pulse lengths and upconversion frequencies
            length_gen = self.us2cycles(cfg['length'], gen_ch=ch)
            self.declare_gen(ch=ch, nqz=cfg['nqz'][iCh], ro_ch=cfg["ro_chs"][0])
            delta_freq = cfg['delta_freq']
            
            self.default_pulse_registers(ch=ch, 
                         gain=cfg['pulse_gain'],
                         phase=0)

            iqdata = 32000*np.exp(2j*pi*self.cycles2us(np.arange(length_gen*16), gen_ch=ch)/16*delta_freq)
            idata = np.real(iqdata).astype(np.int16)
            qdata = np.imag(iqdata).astype(np.int16)
            self.add_pulse(ch=ch, name="measure", idata=idata, qdata=qdata)
            self.set_pulse_registers(ch=ch, style="arb", waveform="measure",
                                     freq=self.freq2reg(cfg['pulse_freq'], ro_ch=cfg["ro_chs"][0]))

        for iCh, ch in enumerate(cfg["ro_chs"]):  # configure the readout lengths and downconversion frequencies
            length_ro = self.us2cycles(cfg['length']+cfg['readout_padding'], ro_ch=ch)
            
            self.declare_readout(ch=ch, freq=cfg["pulse_freq"],
                                 length=length_ro,
                                 sel=cfg['ro_sel'],
                                 gen_ch=cfg["gen_chs"][0])

        self.synci(200)  # give processor some time to configure pulses

    def body(self):
        self.measure(pulse_ch=self.cfg["gen_chs"],
                     adcs=self.ro_chs,
                     pins=[0], 
                     adc_trig_offset=self.us2cycles(self.cfg["adc_trig_offset"]),
                     wait=True,
                     syncdelay=self.us2cycles(self.cfg["relax_delay"]))

config = {
    'gen_chs': [4],
    'ro_chs': [1],
    'nqz': [1],
    'ro_sel': 'product',
    'style': 'arb',
    'pulse_gain': 30000,    # a.u.
    'pulse_freq': 400,       # MHz
    'delta_freq': 1,
    'adc_trig_offset': 0.4, # us
    'length': 1.0,          # us
    'readout_padding': 0.1, # us
    'relax_delay': 2,       # us
    'reps': 1,
#     'soft_avgs': 1
    'soft_avgs': 100
}
prog = FreqShiftProgram(soccfg, config)
iq_list = prog.acquire_decimated(soc, progress=True)
# Plot results.
plt.plot(iq_list[0][0])
plt.plot(iq_list[0][1])

gives the following in loopback, which is coherent over any number of averages:
image

Great, this does solve our problem! Thanks a lot for the quick response and a neat solution!