Using the Python API

This tutorial is applicable to all SHFSG Instruments.

Goals and Requirements

The previous tutorials showed how to use the SHFSG with the LabOne user interface. However, APIs provide an important alternative method to controlling the SHFSG. In this tutorial, we focus on the Python API, showing how to use it to connect to the SHFSG, as well as how to upload and play a sequence that uses user-defined waveforms. 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

First we connect to the SHFSG using Python. For this we first open a daq-session and then connect to the instrument using the following code and by replacing devXXXXX with the id of our SHFSG instrument, e.g. 12050:

# Load the LabOne API and other necessary packages
import zhinst.core
import zhinst.utils as utils
import time

device_id='devXXXXX'
apilevel_example=6

server_host = 'localhost'
server_port = 8004

## connect to data server
daq = zhinst.core.ziDAQServer(host=server_host, port=server_port, api_level=6)

## connect to device
device_interface = '1gbe'
daq.connectDevice(device_id, device_interface)

Defining the data server and port allows users to connect to the instrument in the local network when using local_host 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 version is compatible with the LabOne Python API version:

utils.api_server_version_check(daq)

If it returns True, we are now in the position to access the device. If it returns False, please update your API version.

Often the first parameters that need to be set for every experiment are the Center Frequency and Range of the Output Channel. To see the parameter updates that will be performed by the Python script, open the Output 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 Channel 1 to 6 GHz and the Output Range to 10 dBm.

import zhinst.deviceutils.shfsg as shfsg_utils

chan_index=0
synth=0

rf_frequency = 6.0 # GHz
daq.setDouble(f'/{device_name}/SYNTHESIZERS/{synth}/CENTERFREQ', rf_frequency*1e9)
output_range = 10.0
daq.setDouble(f'/{device_name}/SGCHANNELS/{chan_index}/OUTPUT/RANGE', output_range)

Observe how the corresponding GUI values in the first panel of the Output tab change their values correspondingly. Note that in both 4- and 8-channel variants of the SHFSG, there are 4 synthesizers. In the 8-channel variant, neighboring channels (1&2, 3&4, etc.) therefore share one synthesizer, which is why the synthesizer frequencies are set using the /{device_name}/SYNTHESIZERS/{synth}/CENTERFREQ node and not within the /{device_name}/SGCHANNELS/ branch. To check which synthesizer is being used by a particular SGCHANNEL, you can use the get command:

daq.getInt(f'/{device_name}/SGCHANNELS/{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 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

daq.setDouble(f'/{device_name}/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 through reading the node value in Python using the get command:

daq.getDouble(f'/{device_name}/SYNTHESIZERS/{synth}/CENTERFREQ')

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

# Base for node path
path = f"/{device_name}/sgchannels/{chan_index}/"

#determine which synthesizer is used by the desired channel
synth = daq.getInt(path + "synthesizer")

settings = [
    # RF output settings
    [path + "output/range", 10], #output range in dBm
    [path + "output/rflfpath", 1], #use RF path, not LF path
    #set the corresponding synthesizer frequency in Hz
    ["/%s/synthesizers/%d/centerfreq" % (device_id,synth), 6.0e9],
    [path + "output/on", 1], #enable output

    # Digital modulation settings
    [path + "awg/outputamplitude", 0.5], #set the amplitude for the AWG outputs
    [path + "oscs/0/freq", 10.0d6], #frequency of oscillator 1 in Hz
    [path + "oscs/1/freq", -500.0e6], #frequency of oscillator 2 in Hz
    [path + "awg/modulation/enable", 1], #enable digital modulation

    #use first marker bit of waveform as marker source
    [path + "marker/source", 4]
]

# Set the values
daq.set(settings) #use a transactional set to update all settings

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, 1, 2, w, 10);     // Create a wave table entry with placeholder waveform, index 10
playWave(1, 2, w, 1, 2, w);

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

# Import the device-utils for the SHFSG
import zhinst.deviceutils.shfsg as shfsg_utils

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

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

# Upload the sequence
shfsg_utils.load_sequencer_program(daq, device_id, chan_index, awg_seqc)

We have made use of the device-utils for the SHFSG, which contains 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

#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

#Convert them and send to the instrument
wave_raw = utils.convert_awg_waveform(wave, markers=marker)
daq.set(f'/{device_name}/sgchannels/{chan_index}/awg/waveform/waves/10', wave_raw)

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

# Enable sequencer with single mode
single = 1
shfsg_utils.enable_sequencer(daq, device_id, chan_index, 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
wave_raw_a = zhinst.utils.convert_awg_waveform(wave_a, markers=marker_a)
wave_raw_bc = zhinst.utils.convert_awg_waveform(wave_b, wave_c, markers=marker_bc)
settings = [
    [path + "awg/waveform/waves/10", wave_raw_a],
    [path + "awg/waveform/waves/11", wave_raw_bc]
    ]
daq.set(settings)