Pulse-level Sequencing with the Command Table

This tutorial is applicable to all SHFSG 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 SHFSG.

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.

fig tutorial basic setup marker
Figure 1. Connections for the arbitrary waveform generator command table 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. 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

To begin with, we configure the output and digital modulation settings of the SHFSG, to be able to observe our signal on a scope. We use the Python API 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).

import zhinst.utils
import json

device_id='dev12050'

server_host = 'localhost'
server_port = 8004

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

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

chan_index = 0 # which channel to be used, here: first channel

# Base for node path
path = f"/{device_id}/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
    [f"/{device_id}/synthesizers/{synth}/centerfreq", 1.0e9], #set the corresponding synthesizer frequency in Hz
    [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.0e6], #frequency of oscillator 1 in Hz
    [path + "oscs/1/freq", -150.0e6], #frequency of oscillator 2 in Hz
    [path + "awg/modulation/enable", 1], #enable digital modulation
    # Triggering settings
    [path + "marker/source", 0] #AWG trigger 1
    ]

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

In this case, we will use 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 16384 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 <dev>/SGCHANNELS/<n>/AWG/WAVEFORM/WAVES/<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 <dev>/SGCHANNELS/<n>/AWG/COMMANDTABLE/DATA

Basic command table use

We start by defining a sequencer program that uses the command table.

// Define waveform
wave w_a = gauss(2048, 1, 1024, 256);

// Assign a dual channel waveform to wave table entry 0
assignWaveIndex(1, 2, w_a, 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);

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 entries using Python. It often comes in handy to define the command table as a Python dictionary, which is then converted into a JSON format at upload.

# index of wave table and command table entries
ct_index = 0
wave_index = 0
gain = 1.0

# command table as python dictionary
ct = [{"index": ct_index,
        "waveform": {"index": wave_index},
        "amplitude00": {
            "value": gain
        },
        "amplitude01": {
            "value": -gain
        },
        "amplitude10": {
            "value": gain
        },
        "amplitude11": {
            "value": gain
        },
        "phase": {
            "value": 0.0
        }
    },
    {"index": ct_index + 1,
        "waveform": {"index": wave_index},
        "amplitude00": {
            "value": gain/2
        },
        "amplitude01": {
            "value": -gain/2
        },
        "amplitude10": {
            "value": gain/2
        },
        "amplitude11": {
            "value": gain/2
        },
        "phase": {
            "value": 180.0
        }
    }
    ]

command_table = {"$schema": "https://docs.zhinst.com/shfsg/commandtable/v1_0/schema",
                "header": {"version": "1.0"},
                "table": ct}

In this example, we generate a first command table entry with index "ct_index", which plays the dual-channel 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 Digital Modulation Tutorial, 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.

To upload the command table to the SHFSG, 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 - generate string from dictionary
daq.setVector(f"/{device_id}/SGCHANNELS/{chan_index}/AWG/COMMANDTABLE/DATA", json.dumps(command_table))

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:

settings = [
    [path + "awg/single", 1], #don't repeat sequence
    [path + "awg/enable",1] #enable sequencer
    ]
daq.set(settings)

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.

fig tutorial awg ct basic 1
Figure 2. Output of the first channel from the basic command table example

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 Gain01 will have no effect if amplitude01 is specified in the command table entry.

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.

// Define a single waveform
wave w_a = ones(1024);

// Assign a dual channel waveform to wave table entry
assignWaveIndex(1,2,w_a, 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);
}

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.

# Define command table as dict
ct = [{"index": 0,
        "waveform": {"index": 0},
        "amplitude00": {
            "value": 0.0
        },
        "amplitude01": {
            "value": -0.0
        },
        "amplitude10": {
            "value": 0.0
        },
        "amplitude11": {
            "value": 0.0
        }
    },
    {"index": 1,
        "waveform": {"index": 0},
        "amplitude00": {
            "value": 0.05,
            "increment": True
        },
        "amplitude01": {
            "value": -0.05,
            "increment": True
        },
        "amplitude10": {
            "value": 0.05,
            "increment": True
        },
        "amplitude11": {
            "value": 0.05,
            "increment": True
        }
    }
    ]

command_table = {"$schema": "https://docs.zhinst.com/shfsg/commandtable/v1_0/schema",
                "header": {"version": "1.0"},
                "table": ct}

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.

fig tutorial awg ct increment
Figure 3. Incrementing waveform amplitudes using the command table increment functionality

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

Phase sweeps can be achieved in a similar way by using the command table below.

# Define command table as dict
ct = [{"index": 0,
        "waveform": {"index": 0},
        "phase": {
            "value": 90.0
        }
    },
    {"index": 1,
        "waveform": {"index": 0},
        "phase": {
            "value": 0.1,
            "increment": True
        }
    }
    ]

command_table = {"$schema": "https://docs.zhinst.com/shfsg/commandtable/v1_0/schema",
                "header": {"version": "1.0"},
                "table": ct}

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.

// Define two wave table entries through paceholders
assignWaveIndex(1,2,placeholder(32), 1,2,placeholder(32), 0);
assignWaveIndex(1,2,placeholder(64), 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);

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

# 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_1*np.exp(-x_1**2/width_1**2)],
    [amp_2*np.exp(-x_2**2/width_2**2), amp_2*np.exp(-x_2**2/width_2**2)]]

# upload waveforms to instrument
settings = []
for i, wave in enumerate(waves):
    wave_raw = zhinst.utils.convert_awg_waveform(wave[0],wave[1])
    settings.append((f'/{device_id}/sgchannels/0/awg/waveform/waves/{i}', wave_raw))
daq.set(settings)

Finally, we also generate and upload a command table to the instrument.

# Define command table as dict
ct = [{"index": 0,
      "waveform": {
          "index": 0
          },
      "amplitude00": {
          "value": 1.0
          },
      "amplitude01": {
          "value": -1.0
          },
      "amplitude10": {
          "value": 1.0
          },
      "amplitude11": {
          "value": 1.0
          },
      "phase": {
          "value": 0
          },
      "oscillatorSelect": {
          "value": 0
          }
      },
      {"index": 1,
      "waveform": {
          "index": 1
          },
      "amplitude00": {
          "value": 1.0
          },
      "amplitude01": {
          "value": -1.0
          },
      "amplitude10": {
          "value": 1.0
          },
      "amplitude11": {
          "value": 1.0
          },
      "phase": {
          "value": 0
          },
      "oscillatorSelect": {
          "value": 1
          }
      },
      {"index": 2,
      "waveform": {
          "index": 0
          },
      "amplitude00": {
          "value": 0.5
          },
      "amplitude01": {
          "value": -0.5
          },
      "amplitude10": {
          "value": 0.5
          },
      "amplitude11": {
          "value": 0.5
          },
      "phase": {
          "value": 90
          },
      "oscillatorSelect": {
          "value": 0
          }
      }
    ]

command_table = {"$schema": "https://docs.zhinst.com/shfsg/commandtable/v1_0/schema",
                "header": {"version": "1.0"},
                "table": ct}

The sequencer program can now be run and will produce output as shown in Figure 4.

fig tutorial awg ct advanced
Figure 4. Advanced command table example output, including oscillator selection

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.

The documentation of all possible parameters in the command table JSON file is contained in the publicly hosted JSON Schema file. The URL of this schema file should be referenced from the JSON string when working on it in a JSON-enabled editor. In this way, you can easily access the documentation via auto complete and in-line help, as shown in the screenshot below for the example of Microsoft’s Visual Studio Code.

fig tutorial awg visualstudio
Figure 5. JSON editing with inline documentation in Visual Studio Code