Using the Python API

This tutorial is applicable to all SHFQC Instruments.

Goals and Requirements

The previous tutorials showed how to use the SHFQC with the LabOne user interface. However, APIs provide an important alternative method to controlling the SHFQC. In this tutorial, we focus on the Zurich Instruments Toolkit, showing how to use it to connect to the SHFQC, as well as how to upload and play a sequence on the Signal Generator channel that uses user-defined waveforms. The Toolkit is based on the core Python API, zhinst-core. In this tutorial you will learn how to:

  • connect to the instrument using Python

  • control the Output, Modulation, and DIO settings of the instrument using nodes

  • compile and upload a sequence using Python

  • include user-defined waveforms in a sequence with Python

Preparation

Connect the cables as illustrated below. Make sure that the instrument is powered on and connected by Ethernet to your local area network (LAN) where the host computer resides. After starting LabOne, the default web browser opens with the LabOne graphical user interface.

fig tutorial basic setup marker
Figure 1. Connections for the arbitrary waveform generator Python tutorial

The tutorial can be started with the default instrument configuration (e.g. after a power cycle) and the default user interface settings (e.g. as is after pressing F5 in the browser).

The instrument can also be connected via the USB interface, which can be simpler for a first test. As a final configuration for measurements, it is recommended to use the 1GbE interface, as it offers a larger data transfer bandwidth.

Connecting to to the instrument

This tutorial makes use of the Zurich Instruments Toolkit. Setting a node in the Toolkit uses the format "device.path.to.node(value)." For the base Python API core, the equivalent node setting would be daq.set(f'/{device_id}/path/to/node', value).

First we connect to the SHFQC using Python. For this we first create a session with the Zurich Instruments Toolkit and then connect to the instrument using the following code and by replacing DEVXXXXX with the id of our SHFQC instrument, e.g. DEV12001:

# Load the LabOne API and other necessary packages
from zhinst.toolkit import Session

DEVICE_ID = 'DEVXXXXX'
SERVER_HOST = 'localhost'

## connect to data server
session = Session(SERVER_HOST)

## connect to device
device = session.connect_device(DEVICE_ID)

Defining the data server allows users to connect to the instrument in the local network when using localhost or to specify a specific address, for example when a remote connection needs to be established to the instrument. Remember that for a remote connection, Connectivity needs to be set From Everywhere.

After successfully running the above code snippet, we check whether the Data Server, instrument firmware, and zhinst versions are compatible with each other:

device.check_compatibility()

If it does not throw an error, we are now in the position to access the device. If it returns an error, resolve the mismatched components identified in the error message.

Often the first parameters that need to be set for every experiment are the Center Frequency and Range of the Input or Output Channel. To see the parameter updates that will be performed by the Python script, open the In/Out Tab of the GUI and select the All-subtab. In our Python script, we use the following code snippet to set the nodes for the Center Frequency of the Signal Generators Channel 1 to 6 GHz and the Output Range to 10 dBm.

SG_CHAN_INDEX=0
synth = device.sgchannels[SG_CHAN_INDEX].synthesizer()

rf_frequency = 6.0 # GHz
device.synthesizers[synth].centerfreq(rf_frequency*1e9)
output_range = 10.0
device.sgchannels[SG_CHAN_INDEX].output.range(output_range)

When using the LF path, the corresponding node for setting the center frequency is device.sgchannels[SG_CHAN_INDEX].digitalmixer.centerfreq(value). This value can be set independently for each Signal Generator Channel.

Observe how the corresponding GUI values in the first panel of the Output tab change their values correspondingly. Note that in the SHFQC, there are 4 synthesizers. Synthesizer 0 drives the Quantum Analyzer channel, whereas synthesizers 1 to 3 drive each drive two subsequent Signal Generator channels, i.e. 1&2, 3&4, or 5&6. Therefore it often makes sense to set the synthesizers using the device.synthesizers[synth].centerfreq node and not within the device.sgchannels[SG_CHAN_INDEX] branch. To check which synthesizer is being used by a particular Signal Generator channel, you can query the node:

device.sgchannels[SG_CHAN_INDEX].synthesizer()

Note that in the GUI and on the front panel of the instrument, lists (e.g. Channel numbers) always start at 1, but all representations in the APIs start counting at 0. Hence, Channel 1 on the front panel corresponds to SG_CHAN_INDEX=0 in the API.

To find out which node is linked to a specific setting in the GUI, either check out the command log at the bottom of the user interface or the node tree documentation.

If we set an invalid value, e.g. a value of 6.05 GHz for the Center Frequency (note that this value can only be set in multiples of 100 MHz) through

device.synthesizers[synth].centerfreq(6.05*1e9)

then the instrument rounds the value automatically to the nearest possible value (here: 6.1 GHz). This is immediately indicated in the GUI or by querying the node:

device.synthesizers[synth].centerfreq()

In preparation for running a sequence in the next section, we will set several node values together using the API:

#determine which synthesizer is used by the desired channel
synth = device.sgchannels[SG_CHAN_INDEX].synthesizer()

with device.set_transaction():
    # RF output settings
    device.sgchannels[SG_CHAN_INDEX].output.range(10) #output range in dBm
    device.sgchannels[SG_CHAN_INDEX].output.rflfpath(1) #use RF path, not LF path
    device.synthesizers[synth].centerfreq(6.0e9) #synthesizer frequency in Hz
    device.sgchannels[SG_CHAN_INDEX].output.on(1) #enable output

    # Digital modulation settings
    device.sgchannels[SG_CHAN_INDEX].awg.outputamplitude(0.5) #amplitude for the AWG outputs
    device.sgchannels[SG_CHAN_INDEX].oscs[0].freq(10.0e6) #frequency of oscillator 1 in Hz
    device.sgchannels[SG_CHAN_INDEX].oscs[1].freq(-500e6) #frequency of oscillator 2 in Hz
    device.sgchannels[SG_CHAN_INDEX].awg.modulation.mode(1) #enable digital modulation

    # Trigger and marker settings
    device.sgchannels[SG_CHAN_INDEX].marker.source(4) #use first marker bit of waveform as marker source
]

Using these settings, we set the RF center frequency and output power, turn on the output, set up digital modulation settings for generating complex signals, and select the marker source for triggering the scope. We also use a transactional set, which is useful for setting many nodes at the same time. This method is faster than using a daq.setInt or daq.setDouble command for each node setting, because with a transactional set the communication latency has to be paid only once.

Uploading and running sequences

We now show how to upload a sequence via API. Very often, user-defined waveforms will be needed. We therefore also cover how to use custom waveforms in a sequence, as it is possible to load a waveform directly from the API. In the sequence the waveform should be declared using the placeholder function to define size and type of the waveform.

const LENGTH = 1024;
wave w = placeholder(LENGTH, true, false); // Create a waveform of size LENGTH, with one marker
assignWaveIndex(1,2, w, 10);     // Create a wave table entry with placeholder waveform, index 10
playWave(1,2, w);

We upload this sequence to the SHFQC using the following Python code:

# Define string that contains sequence from above
seqc_program = """
const LENGTH = 1024;
wave w = placeholder(LENGTH, true, false); // Create a waveform of size LENGTH, with one marker
assignWaveIndex(1,2, w, 10);     // Create a wave table entry with placeholder waveform, index 10

resetOscPhase(); //reset oscillator phase
playWave(1,2, w);
"""

# Upload the sequence
device.sgchannels[SG_CHAN_INDEX].awg.load_sequencer_program(seqc_program)

In addition to being able to set nodes, the Toolkit offers built-in functions for commonly performed actions, such as configuring the output and digital modulation settings as well as compiling and uploading sequences. The uploaded sequence will not run until a valid waveform has been loaded. This can be done for example in Python.

import numpy as np
from zhinst.toolkit import Waveforms

#Generate a waveform and marker
LENGTH = 1024
wave = np.exp(np.linspace(0, -5, LENGTH)) #exponentially decaying waveform
marker = np.concatenate([np.ones(32), np.zeros(LENGTH-32)]).astype(int) #marker waveform with 32 samples high

# Upload waveforms
waveforms = Waveform()
waveforms[10] = (wave,None,marker) # I-component wave, Q-component None, marker
device.sgchannels[SG_CHAN_INDEX].awg.write_to_waveform_memory(waveforms)

Now that we’ve uploaded both the sequence and the waveforms, we can run the sequence:

# Enable sequencer with single mode true
single = 1
device.sgchannels[SG_CHAN_INDEX].awg.enable_sequencer(single = single)

After running the sequence, we observe the signal shown in Figure 2 on the scope.

fig tutorial awg vector wfm
Figure 2. Waveform loaded by the API

The custom waveform data can be arbitrary, but consider that the final signal will pass through the analog output stage of the instrument where the signals get interpolated from 2 GSa/s to 6 GSa/s. This means that the signal may not correspond exactly to the programmed waveform. In particular, this concerns sharp transitions from one sample to the next.

Depending on the output channel assignment (the optional first arguments of assignWaveIndex and playWave instructions), the AWG compiler may create implicit waveform table entries. Therefore, we recommend a usage of the instructions placeholder, assignWaveIndex, and playWave that is as explicit as possible. The following code, for example, is valid but not recommended because it is not easy to read:

const LENGTH = 1024;
wave w = placeholder(LENGTH);
assignWaveIndex(1, w, 10);
assignWaveIndex(w, w, 11);

playWave(1, w);
playWave(w, w);

Instead, it’s recommended to use a unique waveform variable name for each intended single-channel memory entry, and to use this variable name with consistent output channel assignment in placeholder, assignWaveIndex, and playWave as is done in the following example:

const LENGTH = 1024;
wave w_a = placeholder(LENGTH, true, true);
wave w_b = placeholder(LENGTH, true, true);
wave w_c = placeholder(LENGTH, true, true);
assignWaveIndex(1, 2, w_a, 10);
assignWaveIndex(1, 2, w_b, 1, 2, w_c, 11);

playWave(1, 2, w_a);
playWave(1, 2, w_b, 1, 2, w_c);

In the latter case, a possible Python code to update the wave table is shown below. Note that we use the full amount of markers available in this example. The marker integer array encodes the available markers in its least significant bits.

#Generate a waveform and marker
LENGTH = 1024
wave_a = np.exp(np.linspace(0, -5, LENGTH))
wave_b = np.exp(np.linspace(0, -15, LENGTH))
wave_c = np.exp(np.linspace(0, -2.5, LENGTH))

marker_a  = np.concatenate([  0b11*np.ones(32), np.zeros(LENGTH-32)]).astype(int)
marker_bc = np.concatenate([0b1111*np.ones(32), np.zeros(LENGTH-32)]).astype(int)

#Convert and send them to the instrument
waveforms = Waveforms()
waveforms[10] = (wave_a,None,marker_a)
waveforms[11] = (wave_b,wave_c,marker_bc)
device.sgchannels[SG_CHAN_INDEX].awg.write_to_waveform_memory(waveforms)