See also

A Jupyter notebook version of this tutorial can be downloaded here.

Continuous waveform mode

In this tutorial we will demonstrate continuous waveform mode (see Continuous waveform mode). In addition, we will observe the output on an oscilloscope to demonstrate the results.

In addition, we will observe the output on an oscilloscope to demonstrate the results.

This tutorial is meant for a Cluster QCM, but can be applied also to a QRM module with the opportune modifications.

To run this tutorial please make sure you have installed and enabled ipywidgets:

pip install ipywidgets
jupyter nbextension enable --py widgetsnbextension

Setup

First, we are going to import the required packages.

[1]:
# Import ipython widgets
import json
import math
import os

import ipywidgets as widgets
import matplotlib.pyplot
import numpy

# Set up the environment.
import scipy.signal
from IPython.display import display
from ipywidgets import fixed, interact, interact_manual, interactive
from qcodes import Instrument
from qblox_instruments import Cluster, PlugAndPlay, Pulsar

Scan For Clusters

We scan for the available clusters on our network using the Plug & Play functionality of the Qblox Instruments package (see Plug & Play for more info).

[2]:
# Scan for available devices and display
with PlugAndPlay() as p:
    # get info of all devices
    device_list = p.list_devices()

names = {
    dev_id: dev_info["description"]["name"] for dev_id, dev_info in device_list.items()
}
ip_addresses = {
    dev_id: dev_info["identity"]["ip"] for dev_id, dev_info in device_list.items()
}

# create widget for names and ip addresses
connect = widgets.Dropdown(
    options=[(names[dev_id] + " @" + ip_addresses[dev_id], dev_id)
             for dev_id in device_list.keys()],
    description="Select Device",
)
display(connect)

Connect to Cluster

We now make a connection with the Cluster selected in the dropdown widget. We also define a function to find the modules we’re interested in. We select the readout and control module we want to use.

[3]:
# Connect to device
dev_id = connect.value
# Close the chosen QCodes instrument as to prevent name clash.
try:
    Instrument.find_instrument(names[dev_id]).close()
except KeyError:
    pass

cluster = Cluster(name=names[dev_id], identifier=ip_addresses[dev_id])

print(f"{connect.label} connected")
print(cluster.get_system_state())
Marketing-Cluster @10.10.200.99 connected
Status: OKAY, Flags: NONE, Slot flags: NONE

We then find all available cluster modules to connect to them individually.

[4]:
def select_module_widget(device, select_all=False, select_rf_type: bool = False):
    """Create a widget to select modules of a certain type

    default is to show only QRM baseband

    Args:
        devices : Cluster we are currently using
        select_all (bool): ignore filters and show all modules
        select_rf_type (bool): filter RF/baseband
    """
    options = [[None, None]]

    for module in device.modules:
        if module.present():
            if select_all or (module.is_rf_type == select_rf_type):
                options.append(
                    [
                        f"{device.name} "
                        f"{module.short_name} "
                        f"({module.module_type}{'_RF' if module.is_rf_type else ''})",
                        module,
                    ]
                )
    widget = widgets.Dropdown(options=options)
    display(widget)

    return widget
[5]:
print("Select the control module (QCM) from the available modules:")
select_module = select_module_widget(cluster, select_rf_type=False)
Select the control module (QCM) from the available modules:

Finally, we connect to the selected Cluster module.

[6]:
module = select_module.value
print(f"{module} connected")
<QcmQrm: Marketing-Cluster_module3 of Cluster: Marketing-Cluster> connected

Reset the Cluster

We reset the Cluster to enter a well-defined state. Note that resetting will clear all stored parameters, so resetting between experiments is usually not desirable.

[ ]:
cluster.reset()
print(cluster.get_system_state())

Generate waveforms

Next, we are going to generate a couple of waveforms that we are going to upload to the instrument in the next step.

[7]:
# Waveform parameters
waveform_length = 120  # nanoseconds (needs to be a minimum of 4 ns)

# Waveform dictionary (data will hold the samples and index will be used to select the waveforms in the instrument).
waveforms = {
    "gaussian": {
        "data": scipy.signal.gaussian(
            waveform_length, std=0.12 * waveform_length
        ).tolist(),
        "index": 0,
    },
    "sine": {
        "data": [
            math.sin((2 * math.pi / waveform_length) * i)
            for i in range(0, waveform_length)
        ],
        "index": 1,
    },
    "sawtooth": {
        "data": [(1.0 / (waveform_length)) * i for i in range(0, waveform_length)],
        "index": 2,
    },
    "block": {"data": [1.0 for i in range(0, waveform_length)], "index": 3},
}

Let’s plot the waveforms to see what we have created.

[8]:
time = numpy.arange(
    0, max(map(lambda d: len(d["data"]), waveforms.values())), 1)
fig, ax = matplotlib.pyplot.subplots(1, 1, figsize=(10, 10 / 1.61))

for wf, d in waveforms.items():
    ax.plot(time[: len(d["data"])], d["data"], ".-", linewidth=0.5, label=wf)

ax.legend(loc=4)
ax.yaxis.grid()
ax.xaxis.grid()
ax.set_ylabel("Waveform primitive amplitude")
ax.set_xlabel("Time (ns)")

matplotlib.pyplot.draw()
matplotlib.pyplot.show()
../../_images/tutorials_q1asm_tutorials_cont_wave_mode_17_0.png

Upload waveforms

Now that we know that the waveforms are what we expect them to be, let’s upload them to the instrument. To do this we need to store the waveforms in a JSON file together with a Q1ASM program for the sequence processor. Since we are going to use continuous waveform mode, the sequence processor will be bypassed and the Q1ASM program can be trivial (i.e. stop).

[9]:
# Sequence program.
seq_prog = "stop"

# Check waveform length.
for name in waveforms:
    assert (
        len(waveforms[name]["data"]) % 4
    ) == 0, (
        "In continuous waveform mode the lenght of a waveform must be a multiple of 4!"
    )

# Add sequence program and waveforms to single dictionary and write to JSON file.
sequence = {
    "waveforms": waveforms,
    "weights": {},
    "acquisitions": {},
    "program": seq_prog,
}
with open("cont_wave_mode.json", "w", encoding="utf-8") as file:
    json.dump(sequence, file, indent=4)
    file.close()

Let’s write the JSON file to the instruments. We will use sequencer 0 and 1, which will drive outputs \(\text{O}^{[1-2]}\) and \(\text{O}^{[3-4]}\) respectively.

[10]:
# Upload sequence.
module.sequencer0.sequence("cont_wave_mode.json")
module.sequencer1.sequence("cont_wave_mode.json")

Play waveforms

The waveforms have been uploaded to the instrument. Now we need to configure the instrument to run in continuous waveform mode. We do this by setting the following parameters of the sequencers.

[11]:
# Configure the sequencers to run in continuous waveform mode.
for sequencer in [module.sequencer0, module.sequencer1]:
    sequencer.cont_mode_en_awg_path0(True)
    sequencer.cont_mode_en_awg_path1(True)

# Map sequencers to specific outputs (but first disable all sequencer connections).
for sequencer in module.sequencers:
    for out in range(0, 4):
        if hasattr(sequencer, "channel_map_path{}_out{}_en".format(out % 2, out)):
            sequencer.set("channel_map_path{}_out{}_en".format(
                out % 2, out), False)

# If it is a QRM, we only map sequencer 0 to the outputs.
module.sequencer0.channel_map_path0_out0_en(True)
module.sequencer0.channel_map_path1_out1_en(True)
if module.is_qcm_type:
    module.sequencer1.channel_map_path0_out2_en(True)
    module.sequencer1.channel_map_path1_out3_en(True)

# Set specific waveform to specific output.
module.sequencer0.cont_mode_waveform_idx_awg_path0(0)  # Gaussian on O1
module.sequencer0.cont_mode_waveform_idx_awg_path1(1)  # Sine on O2
module.sequencer1.cont_mode_waveform_idx_awg_path0(2)  # Sawtooth on 03
module.sequencer1.cont_mode_waveform_idx_awg_path1(3)  # DC on O4

Now let’s start playback.

[12]:
# Arm and start both sequencers.
module.arm_sequencer(0)
module.arm_sequencer(1)
module.start_sequencer()

# Print status of both sequencers (should now say Q1 stopped, because of the stop instruction).
print(module.get_sequencer_state(0))
print(module.get_sequencer_state(1))
Status: STOPPED, Flags: NONE
Status: STOPPED, Flags: NONE

Check waveforms

The instrument is now running in continuous waveform mode. Now let’s connect an oscilloscope and check the outputs. We connect all output channels of the QXM to two/four channels of an oscilloscope. On the scope we are able to see that all waveforms are being generated correctly:

continuous waveform mode scope image

Outputs: Yellow=\(\text{O}^{1}\), Blue=\(\text{O}^{2}\), Purple=\(\text{O}^{3}\) and Green=\(\text{O}^{4}\)

Reset the Cluster

We reset the Cluster to enter a well-defined state. Note that resetting will clear all stored parameters, so resetting between experiments is usually not desirable.

[13]:
cluster.reset()
print(cluster.get_system_state())
Status: OKAY, Flags: NONE, Slot flags: NONE