Using the Python API¶
Note
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
of 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.
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).
Note
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¶
Note
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)
Note
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
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.
Note
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.enable(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
resetOscPhase(); // Reset oscillator phase
playWave(1,2, w); // Play wave
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); // Play wave
"""
## 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 = Waveforms()
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.
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, false); // Allocate a waveform with one marker
wave w_b = placeholder(LENGTH, true, false); // Allocate a waveform with one marker
wave w_c = placeholder(LENGTH, false, false); // Allocate a waveform without markers
assignWaveIndex(1, 2, w_a, 10); // Declare a single-channel waveform w_a, slot 10
assignWaveIndex(1, 2, w_b, 1, 2, w_c, 11); // Declare a dual-channel waveform with w_b and w_c respectively as real and imaginary part, slot 11
playWave(1, 2, w_a); // Play a single channel waveform (only amplitude modulation)
playWave(1, 2, w_b, 1, 2, w_c); // Play a dual channel waveform (full IQ modulation)
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 the instrument, one per physical channel. The marker integer array encodes the available markers in its least significant bit.
##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([np.ones(32), np.zeros(LENGTH-32)]).astype(int)
marker_bc = np.concatenate([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)