# 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 Zurich Instruments Toolkit, 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. 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.

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 SHFSG 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 SHFSG 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 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.

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 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.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.

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 SHFSG 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);
"""

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

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.

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)