Pulsar QRM

In this demo we showcase the upload, play and digitization of a waveform using the Qblox Pulsar QRM.

NB: This hardware demo interacts with firmware low-level interfaces. When building applications, we highly recommend using Quantify framework.

Physical setup

In this demo we use a single QRM and we connect its outputs to its own inputs as follows using two SMA cables

  • \(\text{O}^1 \rightarrow \text{I}^1\)

  • \(\text{O}^2 \rightarrow \text{I}^2\)

# Import python utilities
import os
import scipy.signal
import math
import json
import matplotlib.pyplot as plt

Connect to the QRM

# Add Pulsar QRM interface
from pulsar_qrm.pulsar_qrm import pulsar_qrm
# Connect to device over Ethernet
pulsar = pulsar_qrm("qrm", "192.168.0.3")

Tip

If you need to re-instantiate the instrument without re-starting the Jupyter Notebook (or the ipython shell) run the following command first pulsar.close(). This closes the QCoDeS instrument instance.

# Print device information
print(pulsar.get_idn())
# Get system status
print(pulsar.get_system_status())

Generate waveforms for QRM

# Generate waveforms
waveform_len = 1000 # ns
waveforms    = {
                 "gaussian": {"data": [], "index": 0},
                 "sine":     {"data": [], "index": 1},
                 "sawtooth": {"data": [], "index": 2},
                 "block":    {"data": [], "index": 3}
               }

#Create gaussian waveform
if "gaussian" in waveforms:
    waveforms["gaussian"]["data"] = scipy.signal.gaussian(waveform_len, std=0.133*waveform_len)

#Create gaussian waveform
if "sine" in waveforms:
    waveforms["sine"]["data"] = [math.sin((2*math.pi/waveform_len)*i) for i in range(0, waveform_len)]

#Create sawtooth waveform
if "sawtooth" in waveforms:
    waveforms["sawtooth"]["data"] = [(1.0 / (waveform_len)) * i for i in range(0, waveform_len)]

#Create block waveform
if "block" in waveforms:
    waveforms["block"]["data"] = [1.0 for i in range(0, waveform_len)]
import matplotlib.pyplot as plt
import numpy as np

time = np.arange(0, max(map(lambda d: len(d["data"]), waveforms.values())), 1)

fig, ax = plt.subplots(1,1)

ax.set_ylabel("Waveform primitive amplitude")
ax.set_xlabel("Time (ns)")

for wf, d in waveforms.items():
    ax.plot(time[:len(d["data"])], d["data"])

Upload program and waveforms into QRM

In order to play the waveforms and digitize the input the firmware needs to receive the waveforms and an assembly-like program.

The program for this demo is somewhat elaborated and we will not examine it in detail. It is intended to be generated by a higher-level compiler.

# Sequencer programs

seq_prog = (""
"                  wait_sync    4                    #Wait for synchronization\n"
"start:            move         4,R0                 #Init number of waveforms\n"
"                  move         0,R1                 #Init waveform index\n"
""
"mult_wave_loop:   move         166,R2               #Init number of singe wave loops (increasing wait)\n"
"                  move         166,R3               #Init number of singe wave loops (decreasing wait)\n"
"                  move         24,R4                #Init number of dynamic wait time (total 4us)\n"
"                  move         3976,R5              #Init number of dynamic wait time\n"
"                  move         32768,R6             #Init gain\n"
""
"sngl_wave_loop_0: move         800,R7               #Init number of long delay cycles\n"
"                  set_mrk      15                   #Set marker to 0xF\n"
"                  upd_param    4                    #Update all parameters and wait 4ns\n"
"                  set_mrk      0                    #Set marker to 0\n"
"                  upd_param    96                   #Update all parameters and wait 96ns\n"
""
"                  wait         R4                   #Dynamic wait\n"
"                  add          R4,24,R4             #Increase wait\n"
""
"                  set_mrk      1                    #Set marker to 1\n"
"                  play         R1,R1,4              #Play waveform and wait 992ns\n"
"                  acquire      R1,R1,992            #Acquire while playing\n"
"                  set_mrk      0                    #Set marker to 0\n"
"                  upd_param    4                    #Update all parameters and wait for 4ns\n"
""
"                  wait         R5                   #Compensate previous dynamic wait\n"
"                  sub          R5,24,R5             #Decrease wait\n"
""
"                  sub          R6,98,R6             #Decrease gain\n"
"                  nop\n"
"                  set_awg_gain R6,R6                #Set gain\n"
""
"long_wait_loop_0: wait         50000                #Wait 50 us\n"
"                  loop         R7,@long_wait_loop_0 #Wait number of long delay cycles\n"
"                  loop         R2,@sngl_wave_loop_0 #Repeat single wave\n"
""
"sngl_wave_loop_1: move         800,R7               #Init number of long delay cycles\n"
"                  set_mrk      15                   #Set marker to 0xF\n"
"                  upd_param    8                    #Update all parameters and wait 8ns\n"
"                  set_mrk      0                    #Set marker to 0\n"
"                  upd_param    92                   #Update all parameters and wait 92ns\n"
""
"                  wait         R4                   #Dynamic wait\n"
"                  sub          R4,24,R4             #Decrease wait\n"
""
"                  set_mrk      1                    #Set marker to 1\n"
"                  play         R1,R1,4              #Play waveform and wait 992ns\n"
"                  acquire      R1,R1,992            #Acquire while playing\n"
"                  set_mrk      0                    #Set marker to 0\n"
"                  upd_param    4                    #Update all parameters and wait 4ns\n"
""
"                  wait         R5                   #Compensate previous dynamic wait\n"
"                  add          R5,24,R5             #Increase wait\n"
""
"                  sub          R6,98,R6             #Decrease gain\n"
"                  nop\n"
"                  set_awg_gain R6,R6                #Set gain\n"
""
"long_wait_loop_1: wait         50000                #Wait for 50 us\n"
"                  loop         R7,@long_wait_loop_1 #Wait number of long delay cycles\n"
"                  loop         R3,@sngl_wave_loop_1 #Repeat single wave\n"
""
"                  add          R1,1,R1              #Adjust waveform index\n"
"                  loop         R0,@mult_wave_loop   #Move to next waveform\n"
"                  jmp          @start               #Jump back to start\n")
# Write waveforms and programs to JSON files
for name in waveforms:
    if str(type(waveforms[name]["data"]).__name__) == "ndarray":
        waveforms[name]["data"] = waveforms[name]["data"].tolist()

wave_and_prog_dict = {"waveforms": {"awg": waveforms,
                                    "acq": waveforms},
                      "program": seq_prog}

seq = 0
with open("demo_seq{}.json".format(seq), 'w', encoding='utf-8') as file:
    json.dump(wave_and_prog_dict, file, indent=4)
    file.close()
# Upload waveforms and programs
pulsar.set("sequencer{}_waveforms_and_program".format(seq),
        os.path.join(os.getcwd(), "demo_seq{}.json".format(seq)))
print(pulsar.get_assembler_log())

Configure the QRM

# Configure the sequencers

pulsar.set("sequencer{}_sync_en".format(seq),                          True)  # Enable the sequecer
pulsar.set("sequencer{}_cont_mode_en_awg_path0".format(seq),           False) # Disable continuous waveform repetition on O1
pulsar.set("sequencer{}_cont_mode_en_awg_path1".format(seq),           False) # Disable continuous waveform repetition on O2
pulsar.set("sequencer{}_gain_awg_path0".format(seq),                   1.0)   # Set unitary gain on O1
pulsar.set("sequencer{}_gain_awg_path1".format(seq),                   1.0)   # Set unitary gain on O2
pulsar.set("sequencer{}_offset_awg_path0".format(seq),                 0)     # No offset on O1
pulsar.set("sequencer{}_offset_awg_path1".format(seq),                 0)     # No offset on O1
pulsar.set("sequencer{}_mod_en_awg".format(seq),                       True)  # Enable modulation
pulsar.set("sequencer{}_nco_freq".format(seq),                         10e6)  # Set modulation frequency
pulsar.set("sequencer{}_nco_phase".format(seq),                        0)     # Set modulation phase
pulsar.set("sequencer{}_trigger_mode_acq_path0".format(seq),           False) # Acquisition will be triggered "manually" in this notebook
pulsar.set("sequencer{}_trigger_mode_acq_path1".format(seq),           False) # Acquisition will be triggered "manually" in this notebook

Play the modulated waveforms and digitize the inputs

# Arm the sequencer
pulsar.arm_sequencer()
# Start the sequencers
pulsar.start_sequencer()
print(pulsar.get_sequencer_state(seq))
# Stop the sequencers
pulsar.stop_sequencer()
print(pulsar.get_sequencer_state(seq))

Retrieve acquired data

The data acquired above was stored in a temporary memory (FPGA memory). This memory is fast but at the cost of being relatively small in size. Its contents are overwritten (automatically) on each new acquisition!

Because of this there is an intermediate memory (CPU RAM of the Pulsar) to which the acquisitions are stored and later retrieved into the host PC for offline analysis. Note that it is possible to copy many acquisitions into the intermediate memory before retrieving them all.

# Get acquisition

# Deletes any previous data stored in the intermediate memory (CPU RAM of the Pulsar)
pulsar.delete_acquisitions(seq)

# Copies last aquired data from the FPGA memory into the intermediate memory
# NB: The FPGA memory is only accessible when the sequencer is stopped
pulsar.store_acquisition(seq, "meas_0", 1200) # Copy only the first 1200 samples, if not specified will dump full FPGA memory

# Retriev aquired data from the intermediate memory of the Pulsar into the host PC
acq = pulsar.get_acquisitions(seq)
# Plot aquired signal on both inputs

fig, ax = plt.subplots(1, 1, figsize=(15, 15/2/1.61))
ax.plot(acq["meas_0"]["path_0"]["data"])
ax.set_xlabel('Time (ns)')
ax.set_ylabel('Relative amplitude')
ax.plot(acq["meas_0"]["path_1"]["data"])
plt.show()
Plot of digitized waveforms at the QRM inputs

The digitized signal values are relative to the ADC reference, see datasheet for details.

Above we showcase the capabilities of outputting and digitizing I and Q signals with a Gaussian envelope.

Retrieving multiple acquisitions at once

Below we showcase the retrieving of multiple measurements results at once. We also exemplify how distinct parts of the FPGA memory can be stored.

pulsar.delete_acquisitions(seq) # Clear intermediate memory

# Run and store first measurement
pulsar.set("sequencer{}_gain_awg_path0".format(seq), 0.25)
pulsar.set("sequencer{}_gain_awg_path1".format(seq), 0.25)
pulsar.arm_sequencer()
pulsar.start_sequencer()
print(pulsar.get_sequencer_state(seq))
pulsar.stop_sequencer()
pulsar.store_acquisition(seq, "meas_0", 800)

# Run and store second measurement
pulsar.set("sequencer{}_gain_awg_path0".format(seq), 0.5)
pulsar.set("sequencer{}_gain_awg_path1".format(seq), 0.3)
pulsar.arm_sequencer()
pulsar.start_sequencer()
print(pulsar.get_sequencer_state(seq))
pulsar.stop_sequencer()
pulsar.store_acquisition(seq, "meas_1", 900)

# Run and store third measurement
pulsar.set("sequencer{}_gain_awg_path0".format(seq), 1.0)
pulsar.set("sequencer{}_gain_awg_path1".format(seq), 1.0)
pulsar.arm_sequencer()
pulsar.start_sequencer()
print(pulsar.get_sequencer_state(seq))
pulsar.stop_sequencer()
pulsar.store_acquisition(seq, "meas_2") # Store full FPGA memory

# Retriev aquired data from the intermediate memory of the Pulsar into the host PC
acq = pulsar.get_acquisitions(seq)

fig, ax = plt.subplots(1, 1, figsize=(15, 15 / 1.61))

for msmt, plt_offset in zip(["meas_0", "meas_1", "meas_2"], [0, 1, 2]):
    ax.plot(np.array(acq[msmt]["path_0"]["data"]) + plt_offset,
            label="{} I".format(msmt))  # Plot I quadrature
    ax.plot(np.array(acq[msmt]["path_1"]["data"]) + plt_offset,
            label="{} q".format(msmt))  # Plot Q quadrature

    ax.set_xlabel('Time (ns)')
    ax.set_ylabel('Relative amplitude')

# Plot only relevant region
ax.set_xlim(0, 1300)
Plot of digitized waveforms at the QRM inputs

Since we are using a QCoDeS interface for our instrument we can retrieve an overview of its parameters:

# Print the instrument snapshot
# See QCoDeS documentation for details
pulsar.print_readable_snapshot(update=True)