01 - Phase coherent readout

Objective: Understand the properties of QICK’s “phase-coherent” generators and readouts, and practice calibrating their relative phase delays. Before you measure a resonance with your QICK this is the first calibration you should do. It is a calibration for the two synthesizers which belong to the QICK signal generator and the QICK readout, respectively. The two synthesizers are running at the same frequency, but there is initially a constant phase difference \(\phi\) between these two synthesizers. Doing this calibration results in you finding that phase difference \(\phi\). In your subsequent measurements, you can specify this initial phase difference \(\phi\) to compensate for it. From then on, the signal generator can synthesize any frequency and then if you read in data (doing a digital down conversion in the process), the readout will still be phase coherent with respect to the signal generator.

The angular frequency \(\omega = 2 \pi f\) .

Also, \(\phi = (\omega t) + \phi_0\).

So, $:nbsphinx-math:phi `= (2 :nbsphinx-math:pi f)*t + :nbsphinx-math:phi`_0 $.

If \(f\) goes up linearly, the phase difference will also change linearly (it will either increase or decrease, depending on whether the readout is ahead or behind of the signal generator- this is randomly determined each time the board clocks are initialized). Once the phase hits 360 degrees it cycles back to 0 again. For a readout frequency of interest \(f_i\) there is a corresponding phase difference \(\phi_i\). In this demonstration we assume \(f_i \approx 180\) MHz. You can plot \(\phi(f)\) and evaluate \(\phi(f_i)=\phi_i\).

[1]:
# Import the QICK drivers and auxiliary libraries
from qick import *
from qick.parser import load_program
%pylab inline
Populating the interactive namespace from numpy and matplotlib
[2]:
# Load bitstream with custom overlay
soc = QickSoc()
# Since we're running locally on the QICK, we don't need a separate QickConfig object.
# If running remotely, you could generate a QickConfig from the QickSoc:
#     soccfg = QickConfig(soc.get_cfg())
# or save the config to file, and load it later:
#     with open("qick_config.json", "w") as f:
#         f.write(soc.dump_cfg())
#     soccfg = QickConfig("qick_config.json")
soccfg = soc
# print(soccfg)

# Print the QICK configuration
print(soc)

# loopback and monitor channels
out_chs = [0,5,6]

QICK configuration:

        Board: ZCU111

        Global clocks (MHz): DAC fabric 384.000, ADC fabric 384.000, reference 204.800

        Sampling freqs (MHz): DAC 6144.000, ADC 3072.000

        Refclk multiplier factors: 30 (DAC), 15 (ADC)
        Frequency resolution step: 1.431 Hz

        7 signal generator channels:
        0:      tProc output 1, maxlen 65536
                DAC tile 0, ch 0, 32-bit DDS, fabric=384.000 MHz, fs=6144.000 MHz
        1:      tProc output 2, maxlen 65536
                DAC tile 0, ch 1, 32-bit DDS, fabric=384.000 MHz, fs=6144.000 MHz
        2:      tProc output 3, maxlen 65536
                DAC tile 0, ch 2, 32-bit DDS, fabric=384.000 MHz, fs=6144.000 MHz
        3:      tProc output 4, maxlen 65536
                DAC tile 1, ch 0, 32-bit DDS, fabric=384.000 MHz, fs=6144.000 MHz
        4:      tProc output 5, maxlen 65536
                DAC tile 1, ch 1, 32-bit DDS, fabric=384.000 MHz, fs=6144.000 MHz
        5:      tProc output 6, maxlen 65536
                DAC tile 1, ch 2, 32-bit DDS, fabric=384.000 MHz, fs=6144.000 MHz
        6:      tProc output 7, maxlen 65536
                DAC tile 1, ch 3, 32-bit DDS, fabric=384.000 MHz, fs=6144.000 MHz

        2 readout channels:
        0:      ADC tile 0, ch 0, 32-bit DDS, fabric=384.000 MHz, fs=3072.000 MHz
                maxlen 1024 (avg) 1024 (decimated), trigger 14, tproc input 0
        1:      ADC tile 0, ch 1, 32-bit DDS, fabric=384.000 MHz, fs=3072.000 MHz
                maxlen 1024 (avg) 1024 (decimated), trigger 15, tproc input 1

        tProc: 1048576 words program memory, 4096 words data memory
                program RAM: 65536 bytes

Hardware Configuration

tProc channel 7 : DAC 229 CH3 <-> Readout channel 0 : ADC 224 CH0

The following are optional:

  • tProc channel 6 : DAC 229 CH2 <-> Readout channel 1 : ADC 224 CH1

  • tProc channel 1 : DAC 228 CH0(P) <-> oscilloscope, through a DC-block

  • PMOD 0_0 (pin 1 on J48 on the ZCU111) <-> oscilloscope

We are going to drive the DAC outputs in periodic mode, so we only need to specify a short constant waveform and they will continue to run forever. This is useful for the phase measurement, but it’s good practice to stop the DACs afterwards using soc.reset_gens().

We define the frequency range to be tested, and some helper functions that will process and print the results. For each frequency we will print the measured phase (what we care about), and also the mean magnitude and RMS of the waveform (for debugging - the magnitude should always be much larger than the RMS).

[4]:
# Output frequency f0_v.
###################
# Try it yourself ! Change the output frequency.
###################

f0_start=100
f0_step=0.000250
expts=40
# expts=1

f0_v = np.arange(0,expts)*f0_step+f0_start

f0_v = soccfg.adcfreq(f0_v, gen_ch=6, ro_ch=0)

def calculate_phase(d):
    [xi,xq] = d
    x = xi +1j*xq

    # Average to improve calibration.
    xavg = np.mean(x)

    # Calculate calibration phase.
    fi = np.remainder(np.angle(xavg,deg=True)+360,360)
    return [fi, np.abs(xavg), np.std(x)]

def print_results(res):
    print("freq_i = %f MHz, "%(f0) +
         "phi_i = (%.2f, %.2f) deg, " % tuple([res[i][0] for i in range(2)]) +
         "mag = (%.2f, %.2f), " % tuple([res[i][1] for i in range(2)]) +
         "RMS = (%.2f, %.2f) ADU" % tuple([res[i][2] for i in range(2)]))
#     print("freq_i = %f MHz, phi_i = (%.2f, %.2f) deg, mag = (%.2f, %.2f), RMS = (%.2f, %.2f) ADU" %(f0,*out_array,*A,*xrms))

Measurement methods

We are going to make the same measurement in several ways, to demonstrate different ways of running the QICK (and hopefully gain some understanding of what’s going on under the hood). You can jump to the analysis at the end at any point to plot the data - all methods write the results to the same array.

For the first two methods, we will have a common Python method - below - that acquires the data, but doesn’t load the tProc program.

[5]:
def measure_phase_decimated(soc, f0, fi0, out_array):
    """
    Measure the phase shift for a given frequency using decimated readout, and write it into the provided array.
    Assumes that the tProc is already loaded with a program that reads the frequency and phase from memory locations 123 and 124.
    """
    readout_length = 1000

    f0_dac_int = soccfg.freq2reg(f=f0) # Frequency of signal generator

    soc.tproc.single_write(addr=123, data=f0_dac_int)

    fi0_int = soccfg.deg2reg(fi0, gen_ch=6)
    soc.tproc.single_write(addr=124, data=fi0_int)

    # set readout parameters
    ro_pars = {'freq': f0, 'gen_ch': out_chs[0]}

    # Configure readout.
    for ch in range(2):
        ro_cfg = soccfg.calc_ro_regs(soccfg['readouts'][ch], phase=0, sel='product')
        soccfg.calc_ro_freq(soccfg['readouts'][ch], ro_pars, ro_cfg, False, 0)
        soc.configure_readout(ch=ch, ro_regs=ro_cfg)
#         soc.config_avg(ch=ch, address=0, length=readout_length)
        soc.config_buf(ch=ch, address=0, length=readout_length)
        soc.enable_buf(ch=ch, enable_avg=False, enable_buf=True)

    # Start tProc.
    soc.start_tproc()

    time.sleep(0.1)

    # Get data.
    data = [soc.get_decimated(ch=ch, length=readout_length).T for ch in range(2)]
#     print(data)

    # Process data.
    res = [calculate_phase(d) for d in data]

    # Save results.
    out_array[:] = [a[0] for a in res]
    print_results(res)

Load ASM from text file

First, let’s program the tProc from an assembly language file, which needs to be located in the same directory when you run it: 01_phase_calibration.asm

[6]:
# Constant, real envelope.
# The length must be at least 16 times the nsamp parameter passed to the signal generators.
xg = np.zeros((160,2), dtype=np.int16)
xg[:,0] = 30000

# Upload waveform.
for ch in out_chs:
    # Set the DAC channels to be in 1st Nyquist zone mode
    soc.set_nyquist(ch=ch,nqz=1)
    soc.load_envelope(ch=ch, data=xg, addr=0)

# Load program.
load_program(soc, prog="01_phase_calibration.asm")

# Output phase.
fi0 = 0
fi_v = np.zeros((2,len(f0_v)))
for ii, f0 in enumerate(f0_v):
    measure_phase_decimated(soc, f0, fi0, fi_v[:,ii])

soc.reset_gens()
freq_i = 100.000000 MHz, phi_i = (323.42, 217.77) deg, mag = (637.06, 635.08), RMS = (1.87, 1.79) ADU
freq_i = 100.000249 MHz, phi_i = (302.88, 197.21) deg, mag = (637.77, 634.55), RMS = (1.84, 1.91) ADU
freq_i = 100.000500 MHz, phi_i = (282.22, 176.56) deg, mag = (637.85, 634.81), RMS = (1.84, 1.63) ADU
freq_i = 100.000750 MHz, phi_i = (261.56, 155.90) deg, mag = (637.85, 634.85), RMS = (1.77, 1.75) ADU
freq_i = 100.001000 MHz, phi_i = (240.89, 135.23) deg, mag = (637.83, 634.69), RMS = (1.84, 1.70) ADU
freq_i = 100.001249 MHz, phi_i = (220.34, 114.69) deg, mag = (637.97, 634.82), RMS = (1.84, 1.71) ADU
freq_i = 100.001500 MHz, phi_i = (199.68, 94.02) deg, mag = (637.68, 634.96), RMS = (1.89, 1.67) ADU
freq_i = 100.001750 MHz, phi_i = (179.02, 73.36) deg, mag = (637.74, 635.02), RMS = (1.93, 1.70) ADU
freq_i = 100.002000 MHz, phi_i = (158.35, 52.69) deg, mag = (637.95, 635.06), RMS = (1.82, 1.79) ADU
freq_i = 100.002251 MHz, phi_i = (137.70, 32.03) deg, mag = (637.94, 634.95), RMS = (1.84, 1.65) ADU
freq_i = 100.002500 MHz, phi_i = (117.14, 11.48) deg, mag = (637.89, 634.72), RMS = (1.80, 1.68) ADU
freq_i = 100.002750 MHz, phi_i = (96.48, 350.81) deg, mag = (637.74, 634.90), RMS = (1.79, 1.71) ADU
freq_i = 100.003000 MHz, phi_i = (75.81, 330.15) deg, mag = (637.78, 634.51), RMS = (1.90, 1.69) ADU
freq_i = 100.003251 MHz, phi_i = (55.16, 309.49) deg, mag = (637.60, 634.80), RMS = (2.11, 1.71) ADU
freq_i = 100.003500 MHz, phi_i = (34.61, 288.94) deg, mag = (637.84, 635.00), RMS = (1.91, 1.78) ADU
freq_i = 100.003750 MHz, phi_i = (13.95, 268.28) deg, mag = (637.56, 634.95), RMS = (1.96, 1.67) ADU
freq_i = 100.004000 MHz, phi_i = (353.27, 247.61) deg, mag = (637.69, 634.96), RMS = (1.80, 1.72) ADU
freq_i = 100.004251 MHz, phi_i = (332.62, 226.95) deg, mag = (637.83, 634.87), RMS = (1.94, 1.73) ADU
freq_i = 100.004499 MHz, phi_i = (312.08, 206.40) deg, mag = (637.81, 634.75), RMS = (1.87, 1.70) ADU
freq_i = 100.004750 MHz, phi_i = (291.41, 185.75) deg, mag = (637.74, 634.55), RMS = (2.02, 1.66) ADU
freq_i = 100.005000 MHz, phi_i = (270.73, 165.08) deg, mag = (637.96, 635.02), RMS = (1.77, 1.58) ADU
freq_i = 100.005250 MHz, phi_i = (250.08, 144.42) deg, mag = (637.77, 634.60), RMS = (1.96, 1.70) ADU
freq_i = 100.005499 MHz, phi_i = (229.54, 123.87) deg, mag = (637.60, 635.15), RMS = (1.93, 1.83) ADU
freq_i = 100.005750 MHz, phi_i = (208.87, 103.21) deg, mag = (637.83, 634.97), RMS = (2.08, 1.85) ADU
freq_i = 100.006000 MHz, phi_i = (188.19, 82.54) deg, mag = (637.74, 634.82), RMS = (2.19, 1.71) ADU
freq_i = 100.006250 MHz, phi_i = (167.53, 61.86) deg, mag = (638.14, 635.18), RMS = (2.22, 1.78) ADU
freq_i = 100.006499 MHz, phi_i = (146.99, 41.32) deg, mag = (637.77, 635.07), RMS = (2.11, 1.76) ADU
freq_i = 100.006750 MHz, phi_i = (126.33, 20.67) deg, mag = (638.16, 634.90), RMS = (1.83, 1.68) ADU
freq_i = 100.007000 MHz, phi_i = (105.66, 359.99) deg, mag = (637.95, 634.95), RMS = (1.99, 1.69) ADU
freq_i = 100.007250 MHz, phi_i = (85.00, 339.32) deg, mag = (638.05, 634.89), RMS = (1.93, 1.71) ADU
freq_i = 100.007501 MHz, phi_i = (64.34, 318.67) deg, mag = (638.02, 635.13), RMS = (2.20, 1.65) ADU
freq_i = 100.007750 MHz, phi_i = (43.80, 298.14) deg, mag = (637.97, 635.01), RMS = (2.07, 1.76) ADU
freq_i = 100.008000 MHz, phi_i = (23.13, 277.46) deg, mag = (638.15, 635.51), RMS = (2.02, 1.73) ADU
freq_i = 100.008250 MHz, phi_i = (2.47, 256.80) deg, mag = (637.93, 635.11), RMS = (1.94, 1.78) ADU
freq_i = 100.008501 MHz, phi_i = (341.80, 236.14) deg, mag = (637.57, 634.87), RMS = (1.93, 1.85) ADU
freq_i = 100.008749 MHz, phi_i = (321.26, 215.59) deg, mag = (637.83, 635.21), RMS = (1.94, 1.79) ADU
freq_i = 100.009000 MHz, phi_i = (300.59, 194.92) deg, mag = (638.07, 635.14), RMS = (1.82, 1.69) ADU
freq_i = 100.009250 MHz, phi_i = (279.92, 174.26) deg, mag = (638.02, 635.14), RMS = (2.15, 1.71) ADU
freq_i = 100.009501 MHz, phi_i = (259.26, 153.60) deg, mag = (638.08, 635.00), RMS = (1.91, 1.73) ADU
freq_i = 100.009749 MHz, phi_i = (238.72, 133.06) deg, mag = (638.15, 635.30), RMS = (2.09, 1.73) ADU

QickProgram

It’s usually easier to define the tProc program dynamically in Python, rather than loading it from a separate file. The low-level tool for this is QickProgram.

If you compare 01_phase_calibration.asm to the commands below, you can see the resemblance; if you convert the QickProgram to .asm format with print(prog) it is very clear.

[7]:
nsamp = 10

prog = QickProgram(soccfg)

# Constant, real envelope.
# The length must be at least 16 times the nsamp parameter passed to the signal generators.
xg = np.zeros((16*nsamp,2), dtype=np.int16)
xg[:,0] = 30000

for ch in out_chs:
    # Set the DAC channels to be in 1st Nyquist zone mode
    prog.declare_gen(ch=ch,nqz=1)
    # Upload waveform. Since this is the only waveform in this generator, it will start at address 0.
    prog.add_pulse(ch=ch, name="const", idata=xg[:,0], qdata=xg[:,1])

prog.memri(1,1,123,"freq")
prog.memri(1,2,124,"phase")
prog.regwi(1,3,32000,"gain")
prog.regwi(1,4,nsamp,"nsamp: generator will consume 16*nsamp DAC values.")
prog.regwi(1,5,0x4,"b00100 -> phrst = 0, stdysel = 0, mode = 1, outsel = 00")
prog.bitwi(1,5,5,"<<",16)
prog.bitw(1,4,4,"|",5)
prog.regwi(1,6,0,"start time")
prog.synci(1000)
prog.comment("Set trigger.")
prog.regwi(0,1,0x1,"Send a pulse on pmod 0_0 (pin 1 on J48 on the ZCU111).")
prog.seti(soccfg['readouts'][0]['trigger_port'],0,1,0,"Start the pulse.")
prog.seti(soccfg['readouts'][0]['trigger_port'],0,0,100,"End the pulse after 100 ticks (260 ns).")

prog.comment("Program signal generators in periodic mode.")
prog.comment("Channels 4, 5, 6, 7 -> DAC 229 CH0/1/2/3.")
for ch in out_chs:
    # tuse the tProc channel number for the gen
    prog.set(soccfg['gens'][ch]['tproc_ch'],1,1,2,0,3,4,6)
prog.synci(1000)
prog.comment("Set trigger.")
prog.regwi(0,1,0xc000,"Trigger both buffers.")
prog.seti(soccfg['readouts'][0]['trigger_port'],0,1,0,"Start the pulse.")
prog.seti(soccfg['readouts'][0]['trigger_port'],0,0,100,"End the pulse after 100 ticks (260 ns).")
prog.end()



print(prog)

prog.config_all(soc)



# Output phase.
fi0 = 0

fi_v = np.zeros((2,len(f0_v)))

# Load program.
for ii, f0 in enumerate(f0_v):
    measure_phase_decimated(soc, f0, fi0, fi_v[:,ii])

soc.reset_gens()

// Program

  memri 1, $1, 123;                             //freq
  memri 1, $2, 124;                             //phase
  regwi 1, $3, 32000;                           //gain
  regwi 1, $4, 10;                              //nsamp: generator will consume 16*nsamp DAC values.
  regwi 1, $5, 4;                               //b00100 -> phrst = 0, stdysel = 0, mode = 1, outsel = 00
  bitwi 1, $5, $5 << 16;
  bitw 1, $4, $4 | $5;
  regwi 1, $6, 0;                               //start time
  synci 1000;
  regwi 0, $1, 1;                               //Send a pulse on pmod 0_0 (pin 1 on J48 on the ZCU111).
  seti 0, 0, $1, 0;                             //Start the pulse.
  seti 0, 0, $0, 100;                           //End the pulse after 100 ticks (260 ns).
  set 1, 1, $1, $2, $0, $3, $4, $6;
  set 6, 1, $1, $2, $0, $3, $4, $6;
  set 7, 1, $1, $2, $0, $3, $4, $6;
  synci 1000;
  regwi 0, $1, 49152;                           //Trigger both buffers.
  seti 0, 0, $1, 0;                             //Start the pulse.
  seti 0, 0, $0, 100;                           //End the pulse after 100 ticks (260 ns).
  end ;
freq_i = 100.000000 MHz, phi_i = (323.43, 217.77) deg, mag = (637.87, 634.92), RMS = (1.88, 1.66) ADU
freq_i = 100.000249 MHz, phi_i = (302.89, 197.23) deg, mag = (638.03, 635.09), RMS = (2.21, 1.64) ADU
freq_i = 100.000500 MHz, phi_i = (282.21, 176.56) deg, mag = (638.04, 635.01), RMS = (1.86, 1.75) ADU
freq_i = 100.000750 MHz, phi_i = (261.55, 155.89) deg, mag = (638.13, 635.17), RMS = (1.81, 1.68) ADU
freq_i = 100.001000 MHz, phi_i = (240.89, 135.23) deg, mag = (637.75, 635.00), RMS = (2.14, 1.69) ADU
freq_i = 100.001249 MHz, phi_i = (220.34, 114.69) deg, mag = (638.08, 635.03), RMS = (2.01, 1.72) ADU
freq_i = 100.001500 MHz, phi_i = (199.67, 94.02) deg, mag = (638.02, 635.14), RMS = (2.16, 1.81) ADU
freq_i = 100.001750 MHz, phi_i = (179.01, 73.36) deg, mag = (638.09, 635.15), RMS = (1.92, 1.73) ADU
freq_i = 100.002000 MHz, phi_i = (158.35, 52.69) deg, mag = (637.91, 635.10), RMS = (1.91, 1.76) ADU
freq_i = 100.002251 MHz, phi_i = (137.68, 32.03) deg, mag = (638.00, 635.13), RMS = (1.90, 1.70) ADU
freq_i = 100.002500 MHz, phi_i = (117.14, 11.48) deg, mag = (637.91, 634.97), RMS = (2.03, 1.71) ADU
freq_i = 100.002750 MHz, phi_i = (96.48, 350.82) deg, mag = (638.06, 635.07), RMS = (2.01, 1.79) ADU
freq_i = 100.003000 MHz, phi_i = (75.81, 330.15) deg, mag = (638.04, 635.02), RMS = (2.28, 1.69) ADU
freq_i = 100.003251 MHz, phi_i = (55.14, 309.49) deg, mag = (638.21, 635.15), RMS = (2.00, 1.66) ADU
freq_i = 100.003500 MHz, phi_i = (34.61, 288.94) deg, mag = (637.89, 634.84), RMS = (1.97, 1.69) ADU
freq_i = 100.003750 MHz, phi_i = (13.94, 268.27) deg, mag = (637.83, 635.05), RMS = (1.94, 1.78) ADU
freq_i = 100.004000 MHz, phi_i = (353.26, 247.61) deg, mag = (637.74, 634.97), RMS = (2.11, 1.67) ADU
freq_i = 100.004251 MHz, phi_i = (332.61, 226.94) deg, mag = (637.83, 635.41), RMS = (2.02, 1.76) ADU
freq_i = 100.004499 MHz, phi_i = (312.07, 206.42) deg, mag = (637.69, 634.95), RMS = (2.00, 1.70) ADU
freq_i = 100.004750 MHz, phi_i = (291.39, 185.74) deg, mag = (637.72, 635.27), RMS = (1.80, 1.76) ADU
freq_i = 100.005000 MHz, phi_i = (270.73, 165.08) deg, mag = (638.04, 635.13), RMS = (1.85, 1.67) ADU
freq_i = 100.005250 MHz, phi_i = (250.08, 144.42) deg, mag = (637.90, 635.25), RMS = (2.01, 1.78) ADU
freq_i = 100.005499 MHz, phi_i = (229.53, 123.87) deg, mag = (637.97, 634.75), RMS = (1.89, 1.72) ADU
freq_i = 100.005750 MHz, phi_i = (208.87, 103.21) deg, mag = (638.02, 635.05), RMS = (2.13, 1.83) ADU
freq_i = 100.006000 MHz, phi_i = (188.20, 82.54) deg, mag = (637.81, 634.85), RMS = (2.00, 1.76) ADU
freq_i = 100.006250 MHz, phi_i = (167.54, 61.88) deg, mag = (637.85, 634.97), RMS = (1.89, 1.86) ADU
freq_i = 100.006499 MHz, phi_i = (147.00, 41.33) deg, mag = (638.26, 635.28), RMS = (2.20, 1.70) ADU
freq_i = 100.006750 MHz, phi_i = (126.34, 20.66) deg, mag = (638.03, 635.21), RMS = (2.34, 1.75) ADU
freq_i = 100.007000 MHz, phi_i = (105.66, 0.00) deg, mag = (638.02, 635.01), RMS = (1.87, 1.81) ADU
freq_i = 100.007250 MHz, phi_i = (84.99, 339.34) deg, mag = (637.83, 635.05), RMS = (2.06, 1.68) ADU
freq_i = 100.007501 MHz, phi_i = (64.34, 318.68) deg, mag = (637.92, 634.90), RMS = (2.02, 1.70) ADU
freq_i = 100.007750 MHz, phi_i = (43.80, 298.13) deg, mag = (637.85, 635.15), RMS = (2.12, 1.68) ADU
freq_i = 100.008000 MHz, phi_i = (23.13, 277.47) deg, mag = (637.84, 634.71), RMS = (2.25, 1.69) ADU
freq_i = 100.008250 MHz, phi_i = (2.47, 256.81) deg, mag = (637.90, 634.80), RMS = (2.12, 1.67) ADU
freq_i = 100.008501 MHz, phi_i = (341.80, 236.14) deg, mag = (638.13, 635.10), RMS = (1.83, 1.71) ADU
freq_i = 100.008749 MHz, phi_i = (321.25, 215.59) deg, mag = (638.03, 635.16), RMS = (2.15, 1.68) ADU
freq_i = 100.009000 MHz, phi_i = (300.59, 194.94) deg, mag = (637.84, 635.28), RMS = (2.11, 1.72) ADU
freq_i = 100.009250 MHz, phi_i = (279.93, 174.27) deg, mag = (638.02, 635.22), RMS = (1.96, 1.70) ADU
freq_i = 100.009501 MHz, phi_i = (259.26, 153.61) deg, mag = (637.81, 634.84), RMS = (1.99, 1.72) ADU
freq_i = 100.009749 MHz, phi_i = (238.71, 133.05) deg, mag = (638.12, 635.17), RMS = (2.03, 1.67) ADU

AveragerProgram

Instead of writing your acquisition code from scratch every time, it is usually easier to use AveragerProgram, which combines the QickProgram functionality with a set of built-in acquisition methods. AveragerProgram can also use the tProc to run the acquisition in a loop, which is much faster.

[8]:
class SingleFreqProgram(AveragerProgram):
    def __init__(self,soccfg, cfg):
        super().__init__(soccfg, cfg)

    def initialize(self):
        cfg=self.cfg

        # configure the readout lengths and downconversion frequencies
        for ch in range(2):
            self.declare_readout(ch=ch, length=self.cfg["readout_length"],
                                 freq=self.cfg["pulse_freq"])

        idata = 30000*np.ones(16*cfg["length"])

        for ch in self.cfg['out_ch']:
            self.declare_gen(ch=ch, nqz=1)
            self.add_pulse(ch=ch, name="measure", idata=idata)

        freq=soccfg.freq2reg(cfg["pulse_freq"])  # convert frequency to dac frequency
        self.trigger(pins=[0], t=0) # send a pulse on pmod0_0, for scope trigger
        for ch in self.cfg['out_ch']:
            self.set_pulse_registers(ch=ch, style="arb", freq=freq, phase=cfg["res_phase"], gain=cfg["pulse_gain"],
                                     waveform="measure", mode="periodic")

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

    def body(self):
        self.trigger(adcs=[0,1],adc_trig_offset=self.cfg["adc_trig_offset"])  # trigger the adc acquisition
        for ch in self.cfg['out_ch']:
            self.pulse(ch=ch, t=0) # play readout pulse
        self.wait_all() # control should wait until the readout is over
        self.sync_all(200)  # wait for measurement to complete

config={"out_ch":out_chs,
        "reps":1, # --Fixed
        "res_phase":soccfg.deg2reg(0), # --Fixed

        "length":10, # [Clock ticks]
        # Try varying length from 10-100 clock ticks

        "readout_length":1000, # [Clock ticks]
        # Try varying readout_length from 50-1000 clock ticks

        "pulse_gain":32000, # [DAC units]
        # Try varying pulse_gain from 500 to 30000 DAC units

        "pulse_freq": 100, # [MHz]
        # In this program the signal is up and downconverted digitally so you won't see any frequency
        # components in the I/Q traces below. But since the signal gain depends on frequency,
        # if you lower pulse_freq you will see an increased gain.

        "adc_trig_offset": 1000, # [Clock ticks]
        # Try varying adc_trig_offset from 100 to 220 clock ticks

        "soft_avgs":1
        # Try varying soft_avgs from 1 to 200 averages

       }

Decimated readout

First, let’s run the AveragerProgram in the same mode that we were using before: decimated readout, where we save the full waveform. If you increase the value of soft_avgs, the AveragerProgram will average multiple waveforms before giving you the results, so the measurement will take longer (past 10, the readout time is noticeable) but the RMS of the waveform will decrease.

[9]:
res=[]
config['reps'] = 1

# change this from 1 to 50
config['soft_avgs'] = 1

# for f0 in [101]:
for f0 in f0_v:
    config['pulse_freq'] = f0
    prog =SingleFreqProgram(soccfg, config)
#     print(prog)
    data = prog.acquire_decimated(soc, progress=False)
#     print(data)
    res.append([calculate_phase(d) for d in data])
    print_results(res[-1])
fi_v=np.array([[a[0] for a in r] for r in res]).T

soc.reset_gens()

freq_i = 100.000000 MHz, phi_i = (323.42, 217.77) deg, mag = (637.98, 635.24), RMS = (1.90, 1.69) ADU
freq_i = 100.000249 MHz, phi_i = (302.88, 197.23) deg, mag = (637.91, 634.95), RMS = (1.93, 1.79) ADU
freq_i = 100.000500 MHz, phi_i = (282.20, 176.56) deg, mag = (637.82, 635.05), RMS = (2.07, 1.81) ADU
freq_i = 100.000750 MHz, phi_i = (261.55, 155.90) deg, mag = (637.98, 635.02), RMS = (2.05, 1.74) ADU
freq_i = 100.001000 MHz, phi_i = (240.89, 135.24) deg, mag = (638.04, 634.95), RMS = (2.09, 1.67) ADU
freq_i = 100.001249 MHz, phi_i = (220.35, 114.69) deg, mag = (637.88, 635.09), RMS = (2.21, 1.66) ADU
freq_i = 100.001500 MHz, phi_i = (199.67, 94.02) deg, mag = (638.07, 635.03), RMS = (2.06, 1.74) ADU
freq_i = 100.001750 MHz, phi_i = (179.01, 73.36) deg, mag = (638.10, 635.05), RMS = (1.97, 1.78) ADU
freq_i = 100.002000 MHz, phi_i = (158.35, 52.71) deg, mag = (637.91, 635.22), RMS = (1.97, 1.73) ADU
freq_i = 100.002251 MHz, phi_i = (137.70, 32.04) deg, mag = (637.92, 635.03), RMS = (1.98, 1.74) ADU
freq_i = 100.002500 MHz, phi_i = (117.15, 11.49) deg, mag = (637.92, 635.09), RMS = (2.11, 1.76) ADU
freq_i = 100.002750 MHz, phi_i = (96.48, 350.83) deg, mag = (638.13, 635.19), RMS = (2.14, 1.72) ADU
freq_i = 100.003000 MHz, phi_i = (75.82, 330.15) deg, mag = (637.93, 634.98), RMS = (2.52, 1.73) ADU
freq_i = 100.003251 MHz, phi_i = (55.15, 309.49) deg, mag = (638.07, 635.13), RMS = (2.21, 1.72) ADU
freq_i = 100.003500 MHz, phi_i = (34.60, 288.94) deg, mag = (637.97, 635.15), RMS = (2.21, 1.69) ADU
freq_i = 100.003750 MHz, phi_i = (13.94, 268.29) deg, mag = (638.27, 635.06), RMS = (2.11, 1.75) ADU
freq_i = 100.004000 MHz, phi_i = (353.28, 247.62) deg, mag = (637.86, 634.91), RMS = (2.09, 1.67) ADU
freq_i = 100.004251 MHz, phi_i = (332.62, 226.96) deg, mag = (637.91, 635.13), RMS = (2.18, 1.72) ADU
freq_i = 100.004499 MHz, phi_i = (312.08, 206.41) deg, mag = (637.73, 635.03), RMS = (2.04, 1.68) ADU
freq_i = 100.004750 MHz, phi_i = (291.41, 185.75) deg, mag = (637.82, 635.15), RMS = (2.09, 1.79) ADU
freq_i = 100.005000 MHz, phi_i = (270.74, 165.08) deg, mag = (637.77, 635.10), RMS = (2.31, 1.68) ADU
freq_i = 100.005250 MHz, phi_i = (250.07, 144.41) deg, mag = (637.66, 634.93), RMS = (1.95, 1.72) ADU
freq_i = 100.005499 MHz, phi_i = (229.53, 123.88) deg, mag = (637.85, 635.08), RMS = (2.24, 1.64) ADU
freq_i = 100.005750 MHz, phi_i = (208.87, 103.21) deg, mag = (637.72, 635.20), RMS = (2.02, 1.78) ADU
freq_i = 100.006000 MHz, phi_i = (188.20, 82.54) deg, mag = (637.79, 634.77), RMS = (2.07, 1.76) ADU
freq_i = 100.006250 MHz, phi_i = (167.55, 61.89) deg, mag = (637.85, 634.97), RMS = (1.96, 1.68) ADU
freq_i = 100.006499 MHz, phi_i = (147.00, 41.34) deg, mag = (637.96, 635.10), RMS = (2.00, 1.68) ADU
freq_i = 100.006750 MHz, phi_i = (126.34, 20.68) deg, mag = (637.99, 634.99), RMS = (1.87, 1.78) ADU
freq_i = 100.007000 MHz, phi_i = (105.67, 0.01) deg, mag = (638.05, 635.02), RMS = (2.04, 1.73) ADU
freq_i = 100.007250 MHz, phi_i = (85.01, 339.35) deg, mag = (637.87, 634.86), RMS = (2.08, 1.68) ADU
freq_i = 100.007501 MHz, phi_i = (64.34, 318.69) deg, mag = (638.10, 635.23), RMS = (1.94, 1.76) ADU
freq_i = 100.007750 MHz, phi_i = (43.79, 298.13) deg, mag = (638.25, 635.09), RMS = (2.01, 1.71) ADU
freq_i = 100.008000 MHz, phi_i = (23.13, 277.48) deg, mag = (638.20, 635.12), RMS = (2.10, 1.66) ADU
freq_i = 100.008250 MHz, phi_i = (2.46, 256.80) deg, mag = (638.09, 635.09), RMS = (2.19, 1.72) ADU
freq_i = 100.008501 MHz, phi_i = (341.80, 236.14) deg, mag = (637.98, 634.96), RMS = (2.22, 1.77) ADU
freq_i = 100.008749 MHz, phi_i = (321.25, 215.60) deg, mag = (638.29, 635.14), RMS = (2.04, 1.72) ADU
freq_i = 100.009000 MHz, phi_i = (300.59, 194.93) deg, mag = (638.05, 635.16), RMS = (2.12, 1.79) ADU
freq_i = 100.009250 MHz, phi_i = (279.93, 174.26) deg, mag = (638.03, 635.07), RMS = (1.98, 1.69) ADU
freq_i = 100.009501 MHz, phi_i = (259.26, 153.60) deg, mag = (638.16, 635.17), RMS = (2.25, 1.69) ADU
freq_i = 100.009749 MHz, phi_i = (238.72, 133.06) deg, mag = (638.35, 635.34), RMS = (1.97, 1.72) ADU

Accumulated readout

Since the demodulated waveform is constant over the acquisition window, we don’t really need to save the full waveform. In other words, we should use the QICK’s accumulating buffer (which sums the values in the window) instead of the decimated buffer.

Since we now get one number per acquisition, the RMS is now calculated over acquisitions, not over waveform samples.

You should see that a “repetition” takes much less time than a “soft average.” Normally, decimated readout is only used for finding the correct window to accumulate over, or for debugging.

[10]:
# change this from 10 to 1000
config['reps'] = 1000

config['soft_avgs'] = 1
res=[]
for f0 in f0_v:
    config['pulse_freq'] = f0
    prog =SingleFreqProgram(soccfg, config)
    avg_data = prog.acquire(soc, progress=False)
    data = [[prog.di_buf[i]/config['readout_length'], prog.dq_buf[i]/config['readout_length']] for i in range(2)]
    res.append([calculate_phase(d) for d in data])
    print_results(res[-1])
fi_v=np.array([[a[0] for a in r] for r in res]).T
soc.reset_gens()

freq_i = 100.000000 MHz, phi_i = (323.43, 217.78) deg, mag = (637.98, 635.13), RMS = (0.13, 0.14) ADU
freq_i = 100.000249 MHz, phi_i = (302.88, 197.23) deg, mag = (638.04, 635.22), RMS = (0.13, 0.14) ADU
freq_i = 100.000500 MHz, phi_i = (282.22, 176.57) deg, mag = (638.14, 635.35), RMS = (0.14, 0.13) ADU
freq_i = 100.000750 MHz, phi_i = (261.56, 155.90) deg, mag = (638.07, 635.25), RMS = (0.13, 0.15) ADU
freq_i = 100.001000 MHz, phi_i = (240.89, 135.24) deg, mag = (638.00, 635.29), RMS = (0.12, 0.15) ADU
freq_i = 100.001249 MHz, phi_i = (220.35, 114.69) deg, mag = (637.94, 635.31), RMS = (0.14, 0.14) ADU
freq_i = 100.001500 MHz, phi_i = (199.68, 94.03) deg, mag = (638.01, 635.25), RMS = (0.13, 0.14) ADU
freq_i = 100.001750 MHz, phi_i = (179.02, 73.37) deg, mag = (637.97, 635.29), RMS = (0.14, 0.13) ADU
freq_i = 100.002000 MHz, phi_i = (158.36, 52.70) deg, mag = (638.08, 635.32), RMS = (0.15, 0.15) ADU
freq_i = 100.002251 MHz, phi_i = (137.69, 32.04) deg, mag = (637.90, 635.21), RMS = (0.14, 0.13) ADU
freq_i = 100.002500 MHz, phi_i = (117.15, 11.49) deg, mag = (638.06, 635.25), RMS = (0.15, 0.14) ADU
freq_i = 100.002750 MHz, phi_i = (96.48, 350.83) deg, mag = (638.04, 635.23), RMS = (0.14, 0.14) ADU
freq_i = 100.003000 MHz, phi_i = (75.82, 330.16) deg, mag = (638.05, 635.26), RMS = (0.13, 0.13) ADU
freq_i = 100.003251 MHz, phi_i = (55.16, 309.50) deg, mag = (637.93, 635.21), RMS = (0.13, 0.15) ADU
freq_i = 100.003500 MHz, phi_i = (34.61, 288.95) deg, mag = (637.97, 635.20), RMS = (0.14, 0.13) ADU
freq_i = 100.003750 MHz, phi_i = (13.95, 268.29) deg, mag = (638.15, 635.41), RMS = (0.14, 0.15) ADU
freq_i = 100.004000 MHz, phi_i = (353.29, 247.63) deg, mag = (637.92, 635.14), RMS = (0.15, 0.13) ADU
freq_i = 100.004251 MHz, phi_i = (332.62, 226.96) deg, mag = (638.05, 635.30), RMS = (0.13, 0.13) ADU
freq_i = 100.004499 MHz, phi_i = (312.08, 206.42) deg, mag = (638.06, 635.21), RMS = (0.13, 0.14) ADU
freq_i = 100.004750 MHz, phi_i = (291.42, 185.76) deg, mag = (637.92, 635.23), RMS = (0.13, 0.15) ADU
freq_i = 100.005000 MHz, phi_i = (270.75, 165.09) deg, mag = (637.94, 635.21), RMS = (0.15, 0.14) ADU
freq_i = 100.005250 MHz, phi_i = (250.09, 144.43) deg, mag = (637.91, 635.29), RMS = (0.14, 0.13) ADU
freq_i = 100.005499 MHz, phi_i = (229.54, 123.88) deg, mag = (637.94, 635.23), RMS = (0.13, 0.14) ADU
freq_i = 100.005750 MHz, phi_i = (208.88, 103.22) deg, mag = (638.19, 635.34), RMS = (0.15, 0.14) ADU
freq_i = 100.006000 MHz, phi_i = (188.21, 82.55) deg, mag = (638.13, 635.35), RMS = (0.13, 0.12) ADU
freq_i = 100.006250 MHz, phi_i = (167.55, 61.89) deg, mag = (638.07, 635.28), RMS = (0.14, 0.16) ADU
freq_i = 100.006499 MHz, phi_i = (147.01, 41.35) deg, mag = (638.14, 635.44), RMS = (0.13, 0.14) ADU
freq_i = 100.006750 MHz, phi_i = (126.34, 20.68) deg, mag = (638.12, 635.37), RMS = (0.13, 0.14) ADU
freq_i = 100.007000 MHz, phi_i = (105.68, 0.02) deg, mag = (638.14, 635.39), RMS = (0.14, 0.13) ADU
freq_i = 100.007250 MHz, phi_i = (85.01, 339.35) deg, mag = (638.07, 635.31), RMS = (0.13, 0.14) ADU
freq_i = 100.007501 MHz, phi_i = (64.35, 318.69) deg, mag = (638.22, 635.30), RMS = (0.13, 0.14) ADU
freq_i = 100.007750 MHz, phi_i = (43.81, 298.14) deg, mag = (638.15, 635.28), RMS = (0.14, 0.15) ADU
freq_i = 100.008000 MHz, phi_i = (23.14, 277.48) deg, mag = (638.24, 635.35), RMS = (0.14, 0.14) ADU
freq_i = 100.008250 MHz, phi_i = (2.48, 256.81) deg, mag = (638.34, 635.30), RMS = (0.14, 0.13) ADU
freq_i = 100.008501 MHz, phi_i = (341.81, 236.15) deg, mag = (638.24, 635.47), RMS = (0.14, 0.14) ADU
freq_i = 100.008749 MHz, phi_i = (321.27, 215.60) deg, mag = (638.25, 635.39), RMS = (0.13, 0.14) ADU
freq_i = 100.009000 MHz, phi_i = (300.60, 194.94) deg, mag = (638.22, 635.37), RMS = (0.13, 0.15) ADU
freq_i = 100.009250 MHz, phi_i = (279.94, 174.28) deg, mag = (638.16, 635.47), RMS = (0.12, 0.14) ADU
freq_i = 100.009501 MHz, phi_i = (259.27, 153.61) deg, mag = (638.18, 635.43), RMS = (0.13, 0.14) ADU
freq_i = 100.009749 MHz, phi_i = (238.73, 133.06) deg, mag = (638.10, 635.42), RMS = (0.14, 0.14) ADU

Summary, and RAveragerProgram

This mode (AveragerProgram with accumulated readout) is one of the typical ways that we run the QICK.

The other way uses RAveragerProgram, which has a tProc program with two nested loops: an inner loop for repetitions (just like AveragerProgram) and an outer loop for sweeping a parameter. Because Python can no longer be the bottleneck limiting the measurement speed, this is often much faster.

However, RAveragerProgram cannot be used for this particular measurement because we need to sweep the frequency of both the DAC and the ADC simultaneously - the tProc can control the DAC frequency, but not the ADC frequency.

Analysis

Now we can plot the phase as a function of frequency.

As explained at the beginning, this data can be represented as the sum of a phase offset and a time delay. Over a small frequency range, this looks like a linear dependence on frequency.

[11]:
# Plot results.
# plt.figure(2)
fig, axs = plt.subplots(2,1)

for ch in range(2):
    plot = axs[ch]
    plot.plot(f0_v,fi_v[ch])
    plot.plot(f0_v,fi_v[ch], marker='.', linestyle="None",color="Red")
    plot.set_title(r"$\phi$ vs $f$")
    plot.set_ylabel(r"$\phi$ (degrees)")
    plot.set_xlabel(r"$f$ (MHz)")

fig.savefig("images/Phase_calibration.pdf", dpi=350)
../../_images/tutorials_tprocv1_01_Phase_coherent_readout_21_0.png

For example, if your cavity tone was generated with an IF of 420.9 MHz, you would look at the above table and see that freq_i = 420.9 MHz corresponds to phi_i = 308.7 degrees (that’s specific to this board session, for your QICK it will be different). That value of phi_i will be used as the value of the config file parameter associated with the offset phase of the cavity pulse (the parameter is called res_phase in the demo 00_Send_recieve_pulse).

We can try a fit:

[12]:
def phase_residuals(data,prediction):
    r = np.remainder(data-prediction+180,360)-180
    return r

def phase_model(x, f0):
    return np.remainder(x[0] - 360*x[1]*(f0), 360)

def phase_func(x, arg):
    resid = phase_residuals(arg, phase_model(x, f0_v))
    return resid

from scipy.optimize import least_squares
fig, axs = plt.subplots(2,2)
for ch in range(2):

    slopes = -1*(fi_v[ch,1:]-fi_v[ch,:-1])/(360*(f0_v[1:]-f0_v[:-1]))
#     print(slopes)
    x0 = np.zeros(2)
    x0[1] = np.median(slopes)
    x0[0] = np.remainder(np.median(phase_residuals(fi_v[ch],phase_model(x0, f0_v))),360)
    print("initial estimate: %.2f deg shift, %.2f us delay"% tuple(x0))

    fit = least_squares(phase_func, x0, args=(fi_v[ch],))
#     fit = least_squares(phase_func, x0, args=(fi_v[ch]), method='lm', x_scale='jac')
#     fit = least_squares(phase_func, x0, args=(fi_v[ch]), method='lm', x_scale=(1,1e-5))
    fit.x[0] = np.remainder(fit.x[0],360)
#     print(fit.status)
    print("after minimization: %.2f deg shift, %.2f us delay"% tuple(fit.x))

    plot = axs[0,ch]
    plot.set_title(r"$\phi$ vs $f$")
    plot.set_ylabel(r"$\phi$ (degrees)")
    plot.set_xlabel(r"$f$ (MHz)")
    plot.plot(f0_v,fi_v[ch], marker='.', linestyle="None",color="Red")
    plot.plot(f0_v, phase_model(fit.x, f0_v))

    plot = axs[1,ch]
    plot.set_ylabel(r"residual (degrees)")
    plot.set_xlabel(r"$f$ (MHz)")

    plot.plot(f0_v, phase_func(fit.x, fi_v[ch]), marker='.', linestyle="None",color="Red")
#     plot.plot(f0_v, phase_func(x0, (fi_v[ch])), marker='.', linestyle="None",color="Red")

initial estimate: 113.27 deg shift, 229.28 us delay
after minimization: 32.03 deg shift, 251.41 us delay
initial estimate: 42.41 deg shift, 229.29 us delay
after minimization: 290.80 deg shift, 251.41 us delay
../../_images/tutorials_tprocv1_01_Phase_coherent_readout_24_1.png
[13]:
# "product" (0) and "dds" (1) work as advertised; "input" (2) seems identical to "product" and 3 to "dds"
#     soc.readouts[0].outsel_reg = 0

#nsamp=1 or 2 fails in a strange way:
#the signal generator plays the entire waveform memory repeatedly for 10-20 seconds
#during this time, new "set" commands don't work


# is there ever a reason not to enable both avg and buf?
#         soc.avg_bufs[ch].config_buf(address=0, length=readout_length)
#         soc.avg_bufs[ch].enable(avg=False, buf=True)



#         print(soc.get_accumulated(ch=ch, length=1).T/readout_length, xavg)