Pulse-level Sequencing with the Command Table
This tutorial is applicable to all SHFQC Instruments. |
Goals and Requirements
Pulse-level sequencing is an efficient way to encode pulses in a sequence by uploading a minimal amount of information to the device, allowing measurements to be performed more quickly and programmed more intuitively. The goal of this tutorial is to demonstrate pulse-level sequencing using the command table feature of the Signal Generator channels of the SHFQC.
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) in which the control 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. after pressing F5 in the browser). Additionally, this tutorial requires the use of one of our APIs, in order to be able to define and upload the command table itself. The examples shown here use the Python API - for an introduction see also the Python tutorial. Similar functionality is also available for other APIs.
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. |
Configure the Output
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). |
The minimum waveform length when using the command table is 16 samples. |
To begin with, we configure the output and digital modulation settings of the SHFQC, to be able to observe our signal on a scope. We use the Zurich Instruments Toolkit, available in Python, to set the corresponding nodes after connecting to the instrument. The code below establishes a connection to the device before setting the node values (see also the Using the Python API Tutorial).
# Load the LabOne API and other necessary packages
from zhinst.toolkit import Session, CommandTable
DEVICE_ID = 'DEVXXXXX'
SERVER_HOST = 'localhost'
## connect to data server
session = Session(SERVER_HOST)
## connect to device
device = session.connect_device(DEVICE_ID)
SG_CHAN_INDEX = 0 # which channel to be used, here: first channel
#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(1.0e9) #set the corresponding 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) #set the 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(-150.0e6) #frequency of oscillator 2 in Hz
device.sgchannels[SG_CHAN_INDEX].awg.modulation.enable(1) #enable digital modulation
# Triggering settings
device.sgchannels[SG_CHAN_INDEX].marker.source(0) #AWG trigger 1
In this case, we will use Signal Generator output channel 1 with a maximum output power of 10 dBm and an RF center frequency of 1.0 GHz. We will also enable digital modulation using an oscillator frequency of 10 MHz. This will yield a final output frequency of 1.01 GHz after configuring upper sideband modulation with the command table later. The amplitude of the AWG outputs is set to 0.5 to avoid saturating the outputs.
Introduction to the Command Table
The command table allows the sequencer to group waveform playback instructions with other timing-critical phase and amplitude setting commands into a single instruction that executes within one sequencer clock cycle of 4 ns. The command table is a unit separate from the sequencer and waveform memory and can thus be exchanged separately. Both the phase and the amplitude can be set in absolute and in incremental modes. Additionally, the active oscillator can be set with the command table, enabling fast, phase-coherent frequency switching on a given output channel. Even when not using digital modulation or amplitude settings, working with the command table has the advantage of being more efficient in sequencer instruction memory compared to standard sequencing. Starting a waveform playback with the command table always requires just a single sequencer clock cycle, as opposed to 2 or 3 when using a playWave
instruction.
When using the command table, three components play together during runtime to generate the waveform output and apply the phase and amplitude setting instructions:
-
Sequencer: the unit executing the runtime instructions, namely in this context the
executeTableEntry
instruction. This instruction executes one entry of the command table, and its input argument is a command table index. In its compiled form, which can be seen in the AWG Advanced sub-tab, the sequence program can contain up to 32768 instructions. -
Wave table: a list of up to 16000 indexed waveforms. This list is defined by the sequence program using the index assignment instruction
assignWaveIndex
combined with a waveform or waveform placeholder. The wave table index referring to a waveform can be used in two ways: it is referred to from the command table, and it is used to directly write waveform data to the instrument memory using the node<DEVICE_ID>/SGCHANNELS/<SG_CHAN_INDEX>/AWG/WAVEFORM/WAVES/<WAVE_INDEX>
Node Documentation -
Command table: a list of up to 4096 indexed entries (command table index), each containing the index of a waveform to be played (wave table index), a sine generator phase setting, a set of four AWG amplitude settings for complex modulation, and an oscillator index selection. The command table is specified by a JSON formatted string written to the node
<DEVICE_ID>/SGCHANNELS/<SG_CHAN_INDEX>/AWG/COMMANDTABLE/DATA
Basic command table use
We start by defining a sequencer program that uses the command table.
seqc_program = """\
// Define waveform
wave w_a = gauss(2048, 1, 1024, 256);
// Assign a single channel waveform to wave table entry 0
assignWaveIndex(1,2, w_a, 0);
// Reset the oscillator phase
resetOscPhase();
// Trigger the scope
setTrigger(1);
setTrigger(0);
// execute the first command table entry
executeTableEntry(0);
// execute the second command table entry
executeTableEntry(1);
"""
# Upload sequence
device.sgchannels[SG_CHAN_INDEX].awg.load_sequencer_program(seqc_program)
The sequence can be compiled and uploaded via API using the methods shown in the Python API Tutorial. The sequence defines a Gaussian pulse of unit amplitude and length of 2048 samples. This waveform is then assigned as a dual-channel waveform with explicit output assignment to the wave table entry with index 0, and the final lines execute the two first command table entries. This program cannot be run yet, as the command table is not yet defined.
If a sequence program contains a reference to a command table entry that has not been defined, or if a command table entry refers to a waveform that has not been defined, the sequence program can’t be run. |
In general the command table is defined as a JSON formatted string. Below, we show an example of how to define a command table with two table entries using Python.
For ease of programming, here we define the command table as a CommandTable
object, which is converted into a JSON string automatically at upload. Such object also validate the fields of the command table.
# Load CommandTable class
from zhinst.toolkit import CommandTable
# Initialize command table
ct_schema = device.sgchannels[SG_CHAN_INDEX].awg.commandtable.load_validation_schema()
ct = CommandTable(ct_schema)
# Index of wave table and command table entries
TABLE_INDEX = 0
WAVE_INDEX = 0
gain = 1.0
# Waveform with amplitude and phase settings
ct.table[TABLE_INDEX].waveform.index = WAVE_INDEX
ct.table[TABLE_INDEX].amplitude00.value = gain
ct.table[TABLE_INDEX].amplitude01.value = -gain
ct.table[TABLE_INDEX].amplitude10.value = gain
ct.table[TABLE_INDEX].amplitude11.value = gain
ct.table[TABLE_INDEX].phase.value = 0
# Same waveform with different amplitude and phase settings
ct.table[TABLE_INDEX+1].waveform.index = WAVE_INDEX
ct.table[TABLE_INDEX+1].amplitude00.value = gain/2
ct.table[TABLE_INDEX+1].amplitude01.value = -gain/2
ct.table[TABLE_INDEX+1].amplitude10.value = gain/2
ct.table[TABLE_INDEX+1].amplitude11.value = gain/2
ct.table[TABLE_INDEX+1].phase.value = 180
In this example, we generate a first command table entry with index "TABLE_INDEX", which plays the waveform referenced in the wave table at index "WAVE_INDEX", with amplitude and phase settings specified. The four amplitude settings of the command table have the same effect as the four gain settings of the
with analogous naming convention, i.e. amplitude01
maps to Gain01
. The signs of the amplitudes are chosen to yield upper sideband modulation when using a positive oscillator frequency.
Here we use a single-channel waveform, since we modulate only the amplitude of our pulses. Therefore, coefficients |
To upload the command table to the Signal Generator channel of the SHFQC, we need to connect to the device and then write the command table to the correct node. In Python, this is achieved as follows:
# Upload command table
device.sgchannels[SG_CHAN_INDEX].awg.commandtable.upload_to_device(ct)
During compilation of a sequencer program, any previously uploaded command table is reset, and will need to be uploaded again before it can be used. |
Now that we’ve uploaded both the sequence and the command table, we can run the sequence:
device.sgchannels[SG_CHAN_INDEX].awg.enable_sequencer(single = True)
The expected output is shown in Figure 2. Note how the amplitude of the second waveform is half the magnitude of the first waveform, and that there is a phase shift of 180 degrees between them. This is due to the amplitude and phase settings in the command table. Also note that these amplitude settings are persistent. If a value is not explicitly specified in the command table, it uses either the default value or the value set by a previous usage of the 'executeTableEntry' instruction.

When a command table entry is called, the amplitude and phase are set persistently. Subsequent waveform playbacks on the same channel will need to take this into account, unless the amplitude and phase settings are explicitly included for them in their corresponding command table entries. Additionally, the values of the command table amplitude and phase settings take precedence over the corresponding gain and phase node settings set via API or in the LabOne UI, e.g. the value of |
Efficient pulse incrementation
One illustrative use case of the command table feature is the efficient incrementation of the amplitude or phase of a waveform.
We again start by writing a sequencer program that plays two entries of the command table.
seqc_program = """\
// Define a single waveform
wave w_a = ones(1024);
// Assign a single channel waveform to wave table entry
assignWaveIndex(1,2, w_a, 0);
// Reset the oscillator phase
resetOscPhase();
// Trigger the scope
setTrigger(1);
setTrigger(0);
// execute the first command table entry
executeTableEntry(0);
repeat(20) {
executeTableEntry(1);
}
"""
# Upload sequence
device.sgchannels[SG_CHAN_INDEX].awg.load_sequencer_program(seqc_program)
Here we have defined a single wave table entry, where both channels contain the same constant waveform.
In Python we then define a command table with just two entries, in this case both referencing the same waveform index. In the second command table entry, we set the increment
field to true
, such that the amplitude is incremented each time that the second command table entry is called in the sequence.
# Initialize command table
ct_schema = device.sgchannels[SG_CHAN_INDEX].awg.commandtable.load_validation_schema()
ct = CommandTable(ct_schema)
# Waveform with initial amplitude
ct.table[0].waveform.index = 0
ct.table[0].amplitude00.value = 0
ct.table[0].amplitude01.value = 0
ct.table[0].amplitude10.value = 0
ct.table[0].amplitude11.value = 0
# Waveform with incremented amplitude
ct.table[1].waveform.index = 0
ct.table[1].amplitude00.value = 0.05
ct.table[1].amplitude01.value = -0.05
ct.table[1].amplitude10.value = 0.05
ct.table[1].amplitude11.value = 0.05
ct.table[1].amplitude00.increment = True
ct.table[1].amplitude01.increment = True
ct.table[1].amplitude10.increment = True
ct.table[1].amplitude11.increment = True
# Upload command table
device.sgchannels[SG_CHAN_INDEX].awg.commandtable.upload_to_device(ct)
# Enable sequencer
device.sgchannels[SG_CHAN_INDEX].awg.enable_sequencer(single = 1)
After uploading the command table to the instrument and executing the sequencer program, the channel then produces the output shown in Figure 3.
Here, the first call to the first command table entry plays the waveform with all amplitude settings set to 0.
The subsequent calls to the second command table entry increment these amplitudes each time by 0.05, with a negative increment on amplitude01
, and a positive increment on the others. Although in this example we increment all amplitudes together, it is possible to increment only a subselection of the amplitude settings as well, by changing the appropriate increment settings to False. Incrementing amplitudes this way enables waveform memory-efficient amplitude sweeps.

The amplitude of the waveform generated at the output can be influenced in several different ways: through the amplitude of the waveform itself, through the amplitude settings in the command table, through the output amplitude setting in the Modulation Tab, and finally through the Range setting of the SHFQC Signal Generator output channel. |
Phase sweeps can be achieved in a similar way by using the command table below.
# Define command table
# Initialize command table
ct_schema = device.sgchannels[SG_CHAN_INDEX].awg.commandtable.load_validation_schema()
ct = CommandTable(ct_schema)
# Waveform with initial phase
ct.table[0].waveform.index = 0
ct.table[0].phase.value = 90
# Waveform with incremented phase
ct.table[1].waveform.index = 0
ct.table[1].phase.value = 0.1
ct.table[1].phase.increment = True
# Upload command table
device.sgchannels[SG_CHAN_INDEX].awg.commandtable.upload_to_device(ct)
# Enable sequencer
device.sgchannels[SG_CHAN_INDEX].awg.enable_sequencer(single = 1)
In this case, executing the first table entry will set the phase to 90 degrees, and the second table entry will increment this value each time it is called in steps of 0.1 degrees.
Pulse-level sequencing with the command table
All previous examples have used the pulse library in the AWG sequencer to define waveforms. In more advanced scenarios, waveforms are uploaded through the API, as we will demonstrate next. We start with the following sequence program, where we assign wave table entries using the placeholder command with a waveform length as argument.
seqc_program = """\
// Define two wave table entries through placeholders
assignWaveIndex(1,2, placeholder(32), 0);
assignWaveIndex(1,2, placeholder(64), 1);
// Reset the oscillator phase
resetOscPhase();
// Trigger the scope
setTrigger(1);
setTrigger(0);
// execute command table
executeTableEntry(0);
executeTableEntry(1);
executeTableEntry(2);
"""
# Upload sequence
device.sgchannels[SG_CHAN_INDEX].awg.load_sequencer_program(seqc_program)
In this form, the sequence program cannot be run, first because the command table is not yet uploaded, and second because the waveform memory in the wave table has not been defined. We can use the numpy package to define complex-valued Gaussian waveforms directly in Python, and upload them to the instrument using the appropriate node.
import numpy as np
from zhinst.toolkit import Waveforms
# parameters for waveform generation
amp_1 = 1
length_1 = 32
width_1 = 1/4
amp_2 = 1
length_2 = 64
width_2 = 1/4
x_1 = np.linspace(-1, 1, length_1)
x_2 = np.linspace(-1, 1, length_2)
# define waveforms as list of real-values arrays - here: Gaussian functions
waves = [
[amp_1*np.exp(-x_1**2/width_1**2)],
[amp_2*np.exp(-x_2**2/width_2**2)]]
# upload waveforms to instrument
waveforms = Waveforms()
for i, wave in enumerate(waves):
waveforms[i] = (wave[0])
device.sgchannels[SG_CHAN_INDEX].awg.waveform.wave[i](waveforms)
Finally, we also generate and upload a command table to the instrument.
# Define command table
# Initialize command table
ct_schema = device.sgchannels[SG_CHAN_INDEX].awg.commandtable.load_validation_schema()
ct = CommandTable(ct_schema)
# Waveform 0 with oscillator 1
ct.table[0].waveform.index = 0
ct.table[0].amplitude00.value = 1.0
ct.table[0].amplitude01.value = -1.0
ct.table[0].amplitude10.value = 1.0
ct.table[0].amplitude11.value = 1.0
ct.table[0].phase.value = 0.0
ct.table[0].oscillatorSelect.value = 0
# Waveform 1 with oscillator 2
ct.table[1].waveform.index = 0
ct.table[1].amplitude00.value = 1.0
ct.table[1].amplitude01.value = -1.0
ct.table[1].amplitude10.value = 1.0
ct.table[1].amplitude11.value = 1.0
ct.table[1].phase.value = 0.0
ct.table[1].oscillatorSelect.value = 1
# Waveform 1 with oscillator 1 and different phase
ct.table[2].waveform.index = 0
ct.table[2].amplitude00.value = 1.0
ct.table[2].amplitude01.value = -1.0
ct.table[2].amplitude10.value = 1.0
ct.table[2].amplitude11.value = 1.0
ct.table[2].phase.value = 90.0
ct.table[2].oscillatorSelect.value = 0
# Upload command table
device.sgchannels[SG_CHAN_INDEX].awg.commandtable.upload_to_device(ct)
# Enable sequencer
device.sgchannels[SG_CHAN_INDEX].awg.enable_sequencer(single = 1)
Running the sequencer program will produce output as shown in Figure 4.

The first command table entry plays a Gaussian pulse with amplitude settings for upper sideband modulation, a phase of 0 degrees, and using oscillator 1 (at 10 MHz). The second command table entry plays a different Gaussian pulse envelope with similar amplitude and phase settings, but now using oscillator 2 (at -500 MHz, leading to an output frequency of 500 MHz). The third and final command table entry plays the first Gaussian pulse envelope with different amplitude and phase settings, but again using oscillator 1. Such a set of pulses could correspond to playing an X-gate on qubit 1, then an X-gate on qubit 2, then a Y/2-gate on qubit 1 again. Using the oscillatorSelect
field thereby allows users to interleave pulses for different qubits while maintaining phase coherence between oscillator switches. Because each channel has 8 oscillators, this allows gates for up to 8 different qubits or transitions to be interleaved on the same RF line.
It is also possible to define a command table entry that changes parameters without playing a waveform. This can be particularly useful for efficient nested loops, e.g. Rabi amplitude sweeps with cyclic or sequential averaging. Furthermore, it is possible to define a playZero (and other waveforms) from within the command table as well. To see this functionality, upload the following sequence:
seqc_program = """\
// Define waveform
const len = 1024;
const amp = 1;
wave w = gauss(len,amp,len/2,len/8);
// Assign waveform index
assignWaveIndex(1,2, w, 0);
// Reset the oscillator phase
resetOscPhase();
// Trigger the scope
setTrigger(1);
setTrigger(0);
executeTableEntry(0); //set initial parameters
repeat (5) {
executeTableEntry(1); //play waveform
executeTableEntry(2); //playZero
executeTableEntry(3); //set different parameters
}
"""
# Upload sequence
device.sgchannels[SG_CHAN_INDEX].awg.load_sequencer_program(seqc_program)
After uploading the sequence, we upload the following command table as well:
# Initialize command table
ct_schema = device.sgchannels[SG_CHAN_INDEX].awg.commandtable.load_validation_schema()
ct = CommandTable(ct_schema)
# Initial amplitude and phase settings
ct.table[0].amplitude00.value = 0.1
ct.table[0].amplitude01.value = -0.1
ct.table[0].amplitude10.value = 0.1
ct.table[0].amplitude11.value = 0.1
ct.table[0].phase.value = 0.0
# Play waveform
ct.table[1].waveform.index = 0
# Play playZero
ct.table[2].waveform.playZero = True
ct.table[2].waveform.length = 32
# Set new parameters
ct.table[3].amplitude00.value = 0.05
ct.table[3].amplitude00.increment = True
ct.table[3].amplitude01.value = -0.05
ct.table[3].amplitude01.increment = True
ct.table[3].amplitude10.value = 0.05
ct.table[3].amplitude10.increment = True
ct.table[3].amplitude11.value = 0.05
ct.table[3].amplitude11.increment = True
# Upload command table
device.sgchannels[SG_CHAN_INDEX].awg.commandtable.upload_to_device(ct)
# Enable sequencer
device.sgchannels[SG_CHAN_INDEX].awg.enable_sequencer(single = 1)
The above combination of sequence and command table will use the first executeTableEntry
command (table index 0) to set initial amplitude and phase parameters without playing a waveform. The second executeTableEntry
command (table index 1) plays a waveform using the parameters set by the previous command. The third executeTableEntry
plays a playZero
of length 32 samples. The fourth executeTableEntry
(table index 3) sets new parameters without playing a waveform. Because of the repeat loop, the sequence will play the pulse 5 times, each time with a different set of parameters. In total, we play a waveform with 5 different sets of parameters, but we need only two command table entries (table indices 0 and 3) to set the parameters and one entry to play the waveform (table index 1). We would still need only these three table entries (four including the playZero) even if we want to do a parameter sweep of 100 or 1000 different values (e.g. with repeat (100)
). Running the sequence as written yields the following signal on the oscilloscope.
The benefit of using
It is better to use the command table in the case the criteria above are not fulfilled, or for minimal play length of 16 samples, or if the command table is randomly accessed in real-time with a variable. |
Command table entries fields
The documentation of all possible parameters in the command table JSON file can be found by pulling the schema from the device itself using the node
/<dev>/SGCHANNELS/<n>/AWG/COMMANDTABLE/SCHEMA
. The Python CommandTable
object automatically uses the schema from the device when initialized like this:
# Initialize command table
ct_schema = awg.commandtable.load_validation_schema()
ct = CommandTable(ct_schema)
Table 1 contains all elements that can be programmed as part of a command table entry as well as the default value which is applied if this element is not specified by the user. Table 2 contains all parameters of a waveform
element, as well as each parameter’s default value. Analogously, Table 4 contains the parameters of a phase type element (phase
), Table 5 those of an amplitude type entry (amplitude00
, amplitude01
, amplitude10
or amplitude11
) and Table 3 contains the oscillator selector (oscillatorSelect
).
If a phase element is specified in any entry of the command table, the absolute phase will be set to zero at the start of the execution.
Field | Description | Type | Range/Value | Mandatory | Default |
---|---|---|---|---|---|
index |
Index of the entry |
Integer |
[0—4095] |
yes |
mandatory |
waveform |
Waveform command and its properties |
Waveform |
no |
No waveform played |
|
oscillatorSelect |
Oscillator used for the modulation |
Oscillator Select |
no |
No change of oscillator |
|
phase |
Phase command of the modulation |
Phase |
no |
No change to phase setting |
|
amplitude00 |
Amplitude command for AWG output gain00 |
Amplitude |
no |
No change to amplitude setting |
|
amplitude01 |
Amplitude command for AWG output gain01 |
Amplitude |
no |
No change to amplitude setting |
|
amplitude10 |
Amplitude command for AWG output gain10 |
Amplitude |
no |
No change to amplitude setting |
|
amplitude11 |
Amplitude command for AWG output gain11 |
Amplitude |
no |
No change to amplitude setting |
Field | Description | Type | Range/Value | Mandatory | Default |
---|---|---|---|---|---|
index |
Index of the waveform to play as defined with the |
integer |
[0—15999] |
if playZero or playHold is False |
No waveform played |
length |
The length of the waveform in samples |
integer |
[16—WFM_LEN] |
if playZero or playHold is True |
the waveform length as declared in the sequence |
samplingRateDivider |
Integer exponent n of the sampling rate divider: SampleRate / 2n |
integer |
[0—13] |
no |
0 |
playZero |
Play a zero-valued waveform for specified length of waveform |
bool |
[True,False] |
no |
False |
playHold |
Hold the value of the last waveform and marker sample played for specified length |
bool |
[True,False] |
no |
False |
Field | Description | Type | Range/Value | Mandatory | Default |
---|---|---|---|---|---|
value |
Index of oscillator that is selected for sine/cosine generation |
integer |
[0—7] |
Yes |
mandatory |
Field | Description | Type | Range/Value | Mandatory | Default |
---|---|---|---|---|---|
value |
Phase value of the given sine generator in degree |
float |
[-180.0—180.0) values outside of this range will be clamped |
Yes |
mandatory |
increment |
Set to true for incremental phase value, or to false for absolute |
bool |
[True,False] |
No |
False |
Field | Description | Type | Range/Value | Mandatory | Default |
---|---|---|---|---|---|
value |
Amplitude scaling factor of the given AWG channel |
float |
[-1.0—1.0] |
Yes |
mandatory |
increment |
Set to true for incremental amplitude value, or to false for absolute |
bool |
[True,False] |
No |
False |