Many use cases require the freedom to define waveforms on a sample-basis. The "Simple" sequence type provided by the zhinst-toolkit allows for exactly that.

If the Simple sequence is configured on the AWG Core, the user can add waveforms to a queue of waveforms that are uploaded to the AWG Core. All the waveforms in the queue will then be played in that order. The waveforms are defiend as simple numpy arrays, with every value in the array corresponding to one sample. Since the waveforms are defined purely by samples, the duration of the waveform depends on the sampling rate of the AWG Core.

For our examples we initialize one HDAWG and one UHFQA, both configured to use the same ‘Multi Device Connection’.

[1]:

import zhinst.toolkit as tk
import numpy as np

# create a 'Multi Device Connection'
mdc = tk.MultiDeviceConnection(host="10.42.0.226")
mdc.setup()

# connect the devices
mdc.connect_device(tk.HDAWG("hdawg 1", "dev8030"))
mdc.connect_device(tk.UHFQA("uhfqa 1", "dev2266"))

# references to the instruments are held in attributes 'hdawgs' and 'uhfqas'
hdawg = mdc.hdawgs["hdawg 1"]
uhfqa = mdc.uhfqas["uhfqa 1"]

Successfully connected to data server at 10.42.0.2268004 api version: 6
Successfully connected to device DEV8030 on interface 1GBE
Successfully connected to device DEV2266 on interface 1GBE


The sequence parameter sequence_type has to be set to "Simple" for the AWG Cores we want to use. Other sequence parameter are for now left at their default values.

[2]:

# on the HDAWG
hdawg.awgs[0].set_sequence_params(sequence_type="Simple")

# ont he UHFQA
uhfqa.awg.set_sequence_params(sequence_type="Simple")


## Sample-based waveforms¶

We can define two waveforms as numpy arrays. For each channel pair of an AWG Core a waveform is defined by specifying the wave data on both channels. For wave1 on channel 1 we create an array of 1.0 s with 1000 samples. The wave2 array for channel 2 is the same but with the opposite amplitude.

[3]:

wave1 =  1.0 * np.ones(1000)
wave2 = -1.0 * np.ones(1000)


We can now reset the AWG’s waveform queue (to be sure it’s empty) and add our waveform to the queue with queue_waveform(...). We use wave1 for output channel 1 (the first argument) and wave2 for output channel 2 (the second argument).

[4]:

hdawg.awgs[0].reset_queue()
hdawg.awgs[0].queue_waveform(wave1, wave2)

Current length of queue: 1


Now we have added our waveform to the queue. To be sure, let’s check what’s in the queue:

[5]:

hdawg.awgs[0].waveforms

[5]:

[<zhinst.toolkit.helpers.waveform.Waveform at 0x1dc51e82808>]


The Waveform object in the queue holds the data for the waveforms on the two channels of the AWG Core. It aligns the numpy arrays to the minimum waveform length and sample granularity and brings the data in a format that is easily uplaoded to the device.

The next step is to compile the corresponding .seqC sequence program to the AWG Core. We do this by invoking the compile() method of the AWG Core. It is important to note that the compiled sequence program only initializes placeholders for the waveforms in the correct length that match the waveforms in the queue. However, the waveforms are not uploaded yet. This is done separately by the method upload_waveforms(). If after the compilation the waveform queue is modified, the placeholders in the compiled program do not match the uplaoded waveform data, there will be an error! This is why for the ‘Simple’ sequence, there is a special method compile_and_upload_waveforms() that combines the two commands and makes sure that the correct sequence program is compiled on the AWG Core before uploading the waveform data.

⚠️ for a ‘Simple’ sequence use compile_and_upload_waveforms()

[6]:

hdawg.awgs[0].compile_and_upload_waveforms()

Compilation successful
hdawg 1-0: Sequencer status: ELF file uploaded
Upload of 1 waveforms took 0.037978 s


The .seqC sequence program on the AWG Core looks as follows

// Zurich Instruments sequencer program
// sequence type:              Simple
// automatically generated:    28/04/2020 @14:38

wave w1_1 = randomUniform(1008);
wave w1_2 = randomUniform(1008);
setTrigger(0);

repeat(1){
// waveform 1 / 1
wait(28374);
playWave(w1_1, w1_2);
waitWave();
}


with one playWave(...) command within a repeat(..) loop. The loop is only reapeated once, because by default the value for the sequence parameter repetitions is 1. The waveform placeholders are defined before the main loop (randomUniform(...)). The waveform data however is replaced upon uploading the waveforms in the queue. You can check this in the LabOne UI in the AWG Sequencer Tab by going to Waveform Viewer and selecting the uploaded waveform on the right under Waveform.

## Timing considerations¶

Because the waveforms played by a ‘Simple’ sequence are defined by sample, the time-axis of the waveform is given by the sampling rate of the AWG Core. It is the user’s responsibility to calculate the correct timing of their waveforms with the given sampling rate. The HDAWG ’s sampling rate defaults to 2.4 Gs and the UHFQA ’s to 1.8 Gs.

⚠️ the time-axis of the waveforms is given by the AWG sampling rate

If, for example, we want to play a 1 us long waveform on the HDAWG, we need to calculate the required number of samples in the waveform. This results in a waveform with 1e-6 / 2.4e9 = 2400 samples.

Until now we have only uploaded a single custom waveform. As the queue suggests, it is also possible with a ‘Simple’ sequence to upload and play multiple different waveforms. They simply need to be added to the queue and will then be played in that order inside the main loop of the sequence program.

For example, we might want to define custom waveforms with different amplitudes and add them to the queue:

[7]:

hdawg.awgs[0].reset_queue()

amplitudes = np.linspace(-1, 1, 21)
wave = np.ones(1600)

for amp in amplitudes:
hdawg.awgs[0].queue_waveform(amp * wave, amp * wave)

Current length of queue: 1
Current length of queue: 2
Current length of queue: 3
Current length of queue: 4
Current length of queue: 5
Current length of queue: 6
Current length of queue: 7
Current length of queue: 8
Current length of queue: 9
Current length of queue: 10
Current length of queue: 11
Current length of queue: 12
Current length of queue: 13
Current length of queue: 14
Current length of queue: 15
Current length of queue: 16
Current length of queue: 17
Current length of queue: 18
Current length of queue: 19
Current length of queue: 20
Current length of queue: 21


Now there are 21 different waveforms in the queue.

[9]:

len(hdawg.awgs[0].waveforms)

[9]:

21


Let’s compile the program and upload the waveforms …

[10]:

hdawg.awgs[0].compile_and_upload_waveforms()

Compilation successful
hdawg 1-0: Sequencer status: ELF file uploaded
Upload of 21 waveforms took 0.11493 s


… and we see that the .seqC program on the AWG looks like this

// Zurich Instruments sequencer program
// sequence type:              Simple
// automatically generated:    28/04/2020 @15:03

wave w1_1 = randomUniform(1600);
wave w1_2 = randomUniform(1600);
wave w2_1 = randomUniform(1600);
wave w2_2 = randomUniform(1600);
...
wave w21_1 = randomUniform(1600);
wave w21_2 = randomUniform(1600);
setTrigger(0);

repeat(1){

// waveform 1 / 21
wait(28300);
playWave(w1_1, w1_2);
waitWave();

// waveform 2 / 21
wait(28300);
playWave(w2_1, w2_2);
waitWave();

...

// waveform 21 / 21
wait(28300);
playWave(w21_1, w21_2);
waitWave();

}


with 21 different waveforms initialized and their sample data replaced by the uplaoded waeforms. All 21 waveforms are played one after the other within the main program loop. The distance between consecutive waveforms is given by the period sequence parameter. It is of course also possible to upload waveforms of different lengths.

## Waveform delays¶

Another powerful feature of the ‘Simple’ sequence is the possibility to define separate delays for each waveform in the queue. If not specified, this delay value defaults to 0 and all waveforms in the queue are played with the same alignment to the time origin t=0. However, it can be very useful to define an offset from t=0 that is different for every single waveform in the queue (as opposed to a common shift with trigger_delay). This can be done by specifying the keyword argument delay in the method queue_waveform which assigns a delay time in seconds to the queued waveform. With a positive value for delay, the waveform is shifted forward in time with respect to the time origin.

A simple example:

[17]:

hdawg.awgs[0].reset_queue()

# define a rectangular wave of length 333 ns
wave = np.ones(800)

# define the delay times
delays = np.linspace(0, 1e-6, 11)

# queue waves with different delays!
for d in delays:
hdawg.awgs[0].queue_waveform(wave, wave, delay=d)


Current length of queue: 1
Current length of queue: 2
Current length of queue: 3
Current length of queue: 4
Current length of queue: 5
Current length of queue: 6
Current length of queue: 7
Current length of queue: 8
Current length of queue: 9
Current length of queue: 10
Current length of queue: 11
Compilation successful
hdawg 1-0: Sequencer status: ELF file uploaded
Upload of 11 waveforms took 0.057966 s

[18]:

hdawg.awgs[0].sequence_params["sequence_parameters"]["delay_times"]

[18]:

[0.0, 1e-07, 2e-07, 3e-07, 4e-07, 5e-07, 6e-07, 7e-07, 8e-07, 9e-07, 1e-06]


Having a look at the .seqC sequence program on the device, we see that the wait time between the playWave commands is different between each entry.

...
repeat(1){

// waveform 1 / 11
wait(28400);
playWave(w1_1, w1_2);
waitWave();

// waveform 2 / 11
wait(28370);
playWave(w2_1, w2_2);
waitWave();
wait(30);

...

// waveform 11 / 11
wait(28100);
playWave(w11_1, w11_2);
waitWave();
wait(300);

}


This means that the waveform at each point in the queue is played with a different offset to the time origin t=0.

+          t=0 +              +              +              +              +
|           |  |           |  |           |  |           |  |           |  |
|        XXX|  |       XXX |  |      XXX  |  |     XXX   |  |    XXX    |  |
+--------XXX+--+-------XXX-+--+------XXX--+--+-----XXX---+--+----XXX----+--+--  --  --
|   delay 1    |   delay 2    |   delay 3    |     ...      |    ...       |
+              +              +              +              +              +


## Aligned waveform playback¶

In a typical use case, it is required to play (custom) waveforms on different AWG Cores and different instruments in a way such that the waveforms are perfectly aligned. As a straightforward example we want to use one AWG Core of the HDAWG to play a rectangular waveform while at the same time triggering the waveform playback on the UHFQA. For the triggering, the ‘Mark’ output of the AWG on the HDAWG needs to be connected with a coax cable to the Trig/Ref 1 on the UHFQA.

         HDAWG 1                                                         t=0
+-----------+                        :______________________________:         :
+----+   AWG 1   |  Trigger              _|                         XXXXX|_________:_
|    +-----------+  ("Send Trigger")      :                              :         :
|                                         :                              :         :
|                                         :                              :         :
|     UHFQA                               :                              :         :
|    +-----------+                        :                              :         :
+-----------+ ("External Trigger")


We configure both AWGs with the same number or repetitions and the same period. The trigger mode of the AWG Core on the HDAWG is set to "Send Trigger", i.e. to send out a trigger signal at the start of every period. The trigger mode of the UHFQA is set to wait for a trigger input at the start of every period with "External Trigger".

[4]:

period = 20e-6
repetitions = 1e3

# configure sequence parameters of master trigger
hdawg.awgs[0].set_sequence_params(
sequence_type="Simple",
period=period,
repetitions=repetitions,
trigger_mode="Send Trigger",     # send out the trigger signal
alignment="End with Trigger",    # end waveform on t=0
)

# configure sequence parameters of UHFQA
uhfqa.awg.set_sequence_params(
sequence_type="Simple",
period=period,
repetitions=repetitions,
trigger_mode="External Trigger", # wait for the trigger signal
)


By setting the parameter alignment to "End with Trigger" on the HDAWG and "Start with Trigger" on the UHFQA, we make sure that the waveforms from different devices are played right after each other.

For both AWG Cores we add a waveform to the queue. The waveform is defined by the numpy array np.ones(...) of a certain length. With the method compile_and_upload_waveforms() we tell the AWG to compile the corresponding sequence program and upload the waveforms in the queue.

[19]:

# queue rectangular waveform on HDAWG
hdawg.awgs[0].reset_queue()
hdawg.awgs[0].queue_waveform(np.ones(1000), -np.ones(1000))

# queue rectangular waveform on UHFQA
uhfqa.awg.reset_queue()
uhfqa.awg.queue_waveform(-np.ones(1000), np.ones(1000))

Current length of queue: 1
Compilation successful
hdawg 1-0: Sequencer status: ELF file uploaded
Upload of 1 waveforms took 0.021994 s
Current length of queue: 1
Compilation successful
uhfqa 1-0: Sequencer status: ELF file uploaded
Upload of 1 waveforms took 0.045054 s


Now run the experiment. First, make sure the outputs are on. Start the AWG of the UHFQA first, then the Master AWG on the HDAWG.

[20]:

# turn outputs on
hdawg.awgs[0].outputs(("on", "on"))
uhfqa.awg.outputs(("on", "on"))

# arm the UHFQA
uhfqa.arm(length=repetitions, averages=1)

# start uhfqa awg, waiting for trigger
uhfqa.awg.run()

# start master awg
hdawg.awgs[0].run()
hdawg.awgs[0].wait_done()


Verify the outputs with a scope, e.g. the built-in scope of the UHFQA. The rectangular waveform output of the HDAWG should be followed directly by the waveform from the UHFQA.

## Master trigger waveform playback¶

The alignment of waveforms played on two AWG Cores can be generalized to a Master Trigger setup. The idea is to use one AWG Core as a Master Trigger for all other AWGs. The trigger AWG thus defines the start of every period and all other AWGs wait for its trigger signal. A setup with one HDAWG and one UHFQA could be configured like this:

              HDAWG 1
+-----------+                     ______________________________
+----<----+   AWG 1   |  Trigger          _|                              |_________:_
|         +-----------+                    :                              :         :
+----+----->  AWG 2   |  AWGs[0]          _:________________________|XXXXX|_________:_
|    |-----------+                    :                              :         :
+----->  AWG 3   |  AWGs[1]          _:_____________________|XXXXXXXX|_________:_
|    |-----------+                    :                              :         :
+----->  AWG 4   |  AWGs[2]          _:__________________|XXXXXXXXXXX|_________:_
|    +-----------+                    :                              :         :
|                                     :                              :         :
|     UHFQA                           :                              :         :
|    +-----------+                    :                              :         :
+-----------+


Note that the Trigger AWG could just as well play a ‘Simple’ sequence and have its trigger mode set to ‘Send Trigger’.

If for example the AWG Cores of the HDAWG are used to drive qubits and the UHFQA is configured for multilexed dispersive readout, we would want the waveforms to be aligned such that the readout pulse starts right after the control pulses have ended. This would be done by having all drive AWGs aligned to ‘End with Trigger’ and the UHFQA’s AWG to ‘Start with Trigger’.

[4]:

# group and rename AWG Cores
trigger = hdawg.awgs[0]
awgs = hdawg.awgs[1:]

# common sequence parameters
period = 20e-6
repetitions = 1000

# configure trigger AWG
trigger.set_sequence_params(
sequence_type="Trigger",
period=period,
repetitions=repetitions,
)
trigger.compile()

# configure triggered AWG Cores
for awg in awgs:
awg.set_sequence_params(
sequence_type="Simple",
period=period,
repetitions=repetitions,
alignment="End with Trigger",
trigger_mode="External Trigger",
)
sequence_type="Simple",
period=period,
repetitions=repetitions,
trigger_mode="External Trigger",
)

Compilation successful
hdawg 1-0: Sequencer status: ELF file uploaded


For demonstration purposes we can upload waveforms of different lengths to the drive AWGs and to the UHFQA.

[5]:

# reset queues
[awg.reset_queue() for awg in awgs]

# queue waveforms
awgs[0].queue_waveform(np.ones(800), -np.ones(800))
awgs[1].queue_waveform(np.ones(1000), -np.ones(1000))
awgs[2].queue_waveform(np.ones(1200), -np.ones(1200))

# compile and upload triggered AWGs

Current length of queue: 1
Current length of queue: 1
Current length of queue: 1
Current length of queue: 1
Compilation successful
hdawg 1-1: Sequencer status: ELF file uploaded
Upload of 1 waveforms took 0.06787 s
Compilation successful
hdawg 1-2: Sequencer status: ELF file uploaded
Upload of 1 waveforms took 0.017991 s
Compilation successful
hdawg 1-3: Sequencer status: ELF file uploaded
Upload of 1 waveforms took 0.022224 s
Compilation successful
uhfqa 1-0: Sequencer status: ELF file uploaded
Upload of 1 waveforms took 0.063616 s