See also

A Jupyter notebook version of this tutorial can be downloaded here.

TTL acquisition

In this tutorial we will demonstrate the sequencer based TTL (Transistor-Transistor-Logic) acquisition procedure. The TTL acquisition enables us to count trigger pulses, based on a settable threshold. The acquisition protocol allows us to save the number of triggers in separate bins, or average the triggers on the fly (see section TTL Acquisitions). We will showcase this functionality by using a QRM of which output \(\text{O}^{[1]}\) is directly connected to input \(\text{I}^{[1]}\), to both send pulses and acquire the resulting data.

To run this tutorial please make sure you have installed and enabled ipywidgets:

pip install ipywidgets
jupyter nbextension enable --py widgetsnbextension

Setup

First, we are going to import the required packages.

[6]:
import math
import os

import matplotlib.pyplot as plt
import numpy as np
from numpy import random

# Set up the environment.
import scipy.signal
from IPython.display import display
import ipywidgets as widgets
from typing import List
from qcodes import Instrument
from qblox_instruments import Cluster, PlugAndPlay

Scan For Devices

We scan for the available devices connected via ethernet using the Plug & Play functionality of the Qblox Instruments package (see Plug & Play for more info).

[7]:
# Scan for available devices and display
with PlugAndPlay() as p:
    # get info of all devices
    device_list = p.list_devices()

names = {
    dev_id: dev_info["description"]["name"] for dev_id, dev_info in device_list.items()
}
ip_addresses = {
    dev_id: dev_info["identity"]["ip"] for dev_id, dev_info in device_list.items()
}

# create widget for names and ip addresses
connect = widgets.Dropdown(
    options=[(names[dev_id] + " @" + ip_addresses[dev_id], dev_id)
             for dev_id in device_list.keys()],
    description="Select Device",
)
display(connect)

Connect to Cluster

We now make a connection with the Cluster selected in the dropdown widget. We also define a function to find the modules we’re interested in. We select the readout and control module we want to use.

[8]:
# Connect to device
dev_id = connect.value
# Close the chosen QCodes instrument as to prevent name clash.
try:
    Instrument.find_instrument(names[dev_id]).close()
except KeyError:
    pass

cluster = Cluster(name=names[dev_id], identifier=ip_addresses[dev_id], debug=True) # TODO remove debug

print(f"{connect.label} connected")
print(cluster.get_system_state())
QAE-Cluster @10.10.200.42 connected
Status: OKAY, Flags: NONE, Slot flags: NONE
[9]:
def select_module_widget(device, select_all=False, select_qrm_type: bool=True, select_rf_type: bool=False):
    """Create a widget to select modules of a certain type

    default is to show only QRM baseband

    Args:
        devices : Cluster we are currently using
        select_all (bool): ignore filters and show all modules
        select_qrm_type (bool): filter QRM/QCM
        select_rf_type (bool): filter RF/baseband
    """
    options = [[None, None]]


    for module in device.modules:
        if module.present():
            if select_all or (module.is_qrm_type == select_qrm_type and module.is_rf_type == select_rf_type):
                options.append(
                    [
                        f"{device.name} "
                        f"{module.short_name} "
                        f"({module.module_type}{'_RF' if module.is_rf_type else ''})",
                        module,
                    ]
                )
    widget = widgets.Dropdown(options=options)
    display(widget)

    return widget
[10]:
print("Select the readout module from the available modules:")
select_readout_module = select_module_widget(cluster, select_qrm_type=True, select_rf_type=False)
Select the readout module from the available modules:
Select the control module from the available modules:
[11]:
readout_module = select_readout_module.value
print(f"{readout_module} connected")
<QcmQrm: QAE-Cluster_module2 of Cluster: QAE-Cluster> connected
None connected

Reset the Cluster

We reset the Cluster to enter a well-defined state. Note that resetting will clear all stored parameters, so resetting between experiments is usually not desirable.

[ ]:
cluster.reset()
print(cluster.get_system_state())

Generate waveforms

Next, we need to create the waveforms used by the sequence for playback on the outputs. Here, we define a single waveform consisting of a 16 ns block pulse with an amplitude of 0.5.

[7]:
# Waveform length parameter
waveform_length = 16  # nanoseconds

waveforms= {
     "block": {"data": [0.5 for i in range(0, waveform_length)], "index": 0},
}

Specify acquisitions

We need to specify the acquisitions so that the instrument can allocate the required memory for its acquisition list. Here, we create one acquisition that consists of 100 bins, at acquisition index 0.

[8]:
# Acquisitions
acquisitions = {
    "ttl":   {"num_bins": 100, "index": 0},
}

Create Q1ASM programs

Now that we have the waveform and acquisition specified, we define a simple Q1ASM program that sequences the waveforms and one that triggers the acquisitions. We will send 5 block pulses of 16 ns at 1 MHz (with 984 ns in between them). At the same time we will perform a 6000 ns TTL acquisition. Note that 1 MHz is the maximum continuous rate for a TTL acquisition.

The TTL acquisition is carried out with the acquire_ttl command that takes four arguments. The first argument is the index of what acquisition should be done, the second specifies in what bin index it is stored, the third toggles the acquisition on or off and finally the fourth argument is the amount of ns to wait. See the documentation for a more detailed overview of the sequencer instructions.

[9]:
# Sequence program for AWG.
seq_prog_awg = """
        wait_sync 4           #Wait for sequencers to synchronize and then wait another 4ns.
        move      5,R0        #Loop iterator.
loop:
        play      0,0,16      #Play a block on output path 0 and wait 16ns.
        wait      984         #Wait 984ns
        loop      R0, @loop   #Repeat loop until R0 is 0

        stop                  #Stop the sequence after the last iteration.
"""

# Sequence program for acquiring
seq_prog_acq = """
        wait_sync 4           #Wait for sequencers to synchronize and then wait another 4ns.
        wait 140              #Approximate time of flight
        acquire_ttl 0,0,1,4   #Turn on TTL acquire on input path 0 and wait 4ns.
        wait 6000             #Wait 6000ns.
        acquire_ttl 0,0,0,4   #Turn off TTL acquire on input path 0 and wait 4ns.

        stop                   #Stop sequencer.
"""

Upload sequence

The sequences are uploaded to the sequencers. We will use sequencer 0 to send the pulses and sequencer 1 to acquire them.

[10]:
# Add sequence program, waveform and acquisitions to single dictionary.
sequence_awg = {
    "waveforms": waveforms,
    "weights": {},
    "acquisitions": {},
    "program": seq_prog_awg,
    }
sequence_acq= {
       "waveforms": {},
       "weights": {},
       "acquisitions": acquisitions,
       "program": seq_prog_acq,
   }
[11]:
# Upload sequence.
readout_module.sequencer0.sequence(sequence_awg)
readout_module.sequencer1.sequence(sequence_acq)

Play sequence

Now we configure the sequencers, and play the sequence.

We will use sequencer 0 which will drive output \(\text{O}^{1}\), and sequencer 1 which will acquire on input \(\text{I}^{1}\), enabling syncing, and prepare the (scope) acquisition.

Then the TTL acquisition is configured by using ttl_acq_input_select to select \(\text{I}^{1}\) as input. We set ttl_acq_auto_bin_incr_en to False such that our TTL count will be put in one bin. We set our TTL threshold to a value of 0.5 of our input range (corresponding to 0.5 V) using ttl_acq_threshold, and our input gain to 0 dB using in0_gain.

[13]:
# Map sequencer to specific outputs (but first disable all sequencer connections)
readout_module.disconnect_outputs()
readout_module.disconnect_inputs()

# Drive through first output
readout_module.sequencer0.connect_out0("I")

# Acquire through first input
readout_module.sequencer1.connect_acq_I('in0')

# Enable sync
readout_module.sequencer0.sync_en(True)
readout_module.sequencer1.sync_en(True)

# Delete previous acquisition.
readout_module.delete_acquisition_data(1, "ttl")

# Configure scope mode
readout_module.scope_acq_sequencer_select(1)

# Choose threshold and input gain
threshold = 0.5
input_gain = 0

# Configure the TTL acquisition
readout_module.sequencer1.ttl_acq_input_select(0)
readout_module.sequencer1.ttl_acq_auto_bin_incr_en(False)

#Set input gain and threshold
readout_module.in0_gain(input_gain)
readout_module.sequencer1.ttl_acq_threshold(threshold)

We start the sequence, and print the status flags of our sequencers.

[14]:
# Arm and start sequencer.
readout_module.arm_sequencer(0)
readout_module.arm_sequencer(1)
readout_module.start_sequencer()

# Print status of sequencer.
print(readout_module.get_sequencer_state(0, 1))
print(readout_module.get_sequencer_state(1, 1))
Status: STOPPED, Flags: NONE
Status: STOPPED, Flags: ACQ_SCOPE_DONE_PATH_0, ACQ_SCOPE_DONE_PATH_1, ACQ_BINNING_DONE

Retrieve acquisition

We retrieve the acquisition data from sequencer 1. Then, both plot the scope data for the first 6000 ns and print the number of counted pulses, that is stored in data['acquisition']["bins"]["avg_cnt"][0].

[15]:
# Wait for the sequencer to stop with a timeout period of one minute.
readout_module.get_acquisition_state(1, 1)

# Move acquisition data from temporary memory to acquisition list.
readout_module.store_scope_acquisition(1, "ttl")

# Get acquisition list from instrument.
data = readout_module.get_acquisitions(1)["ttl"]

# Plot acquired signal on both inputs.
print("pulses detected: " + str(data["acquisition"]["bins"]["avg_cnt"][0]))

fig, ax = plt.subplots(1, 1, figsize=(15, 15 / 2 / 1.61))
ax.plot(data["acquisition"]["scope"]["path0"]["data"][0:6000], label='Trace')
ax.axhline(y=threshold, color='r', label='Threshold')
ax.set_xlabel("Time (ns)")
ax.set_ylabel("Amplitude (Volt)")
plt.legend(loc="upper right")
plt.show()
pulses detected: 5
../../_images/tutorials_q1asm_tutorials_ttl_acquisition_26_1.png

We observe that we indeed do see five 16 ns pulses in the scope acquisition. Furthermore we observe that the amount of pulses counted matches the amount we of the scope trace. You are encouraged to change the threshold value and input gain yourself, and learn about th importance of calibrating these values.

Note

It is important to correctly calibrate your input gain and threshold. For example in a setup with noise and/or interferences, setting the input gain too high might result in these kind of pulses:

download.png

Such a single pulse results in multiple counts, as the threshold is passed multiple times. We therefore strongly recommend calibrating the TTL acquisition using the scope acquisition as is shown above.

Short pulse bursts

As the acquisition module in the sequencer has an internal buffer of 8 data entries, any pulse bursts of <8 will be handled correctly when exceeding the maximum continuous rate of 1 MHz. When exceeding the buffer limit, a ACQ_BINNING_FIFO_ERROR will be thrown. To illustrate this we will define a function generate_pulse_program that takes as input num_pulses (the number of pulses to generate) and wait_time (the time to wait between pulses in ns). We also define a function to upload the sequence to the AWG.

[16]:
# Sequence program for AWG.
def generate_pulse_program(num_pulses, wait_time):
    seq_prog_awg = f"""
        wait_sync 4           #Wait for sequencers to synchronize and then wait another 4ns.
        move      {num_pulses},R0  #Loop iterator.
loop:
        play      0,0,16      #Play a block on output path 0 and wait 16ns.
        wait      {wait_time} #Wait wait_time ns
        loop      R0, @loop   #Repeat loop until R0 is 0

        stop                  #Stop the sequence after the last iteration.
    """
    return seq_prog_awg

# Upload sequence to AWG
def upload_sequence(seq_prog_awg):
    sequence_awg = {
    "waveforms": waveforms,
    "weights": {},
    "acquisitions": {},
    "program": seq_prog_awg,
    }

    readout_module.sequencer0.sequence(sequence_awg)

We now generate the program, upload it and set the threshold and input gain.

[17]:
seq_prog_awg = generate_pulse_program(num_pulses=5, wait_time=20)
upload_sequence(seq_prog_awg)

# Choose threshold and input gain
threshold = 0.5
input_gain = 0

# Delete previous acquisition.
readout_module.delete_acquisition_data(1, "ttl")

#Set input gain and threshold
readout_module.in0_gain(input_gain)
readout_module.sequencer1.ttl_acq_threshold(threshold)

We then arm the sequencers and play the sequence.

[18]:
# Arm and start sequencer.
readout_module.arm_sequencer(0)
readout_module.arm_sequencer(1)
readout_module.start_sequencer()

# Print status of sequencer.
print(readout_module.get_sequencer_state(0, 1))
print(readout_module.get_sequencer_state(1, 1))
Status: STOPPED, Flags: NONE
Status: STOPPED, Flags: ACQ_SCOPE_DONE_PATH_0, ACQ_SCOPE_DONE_PATH_1, ACQ_BINNING_DONE

We retrieve the acquisition and plot it as in the previous section.

[19]:
# Wait for the sequencer to stop with a timeout period of one minute.
readout_module.get_acquisition_state(1, 1)

# Move acquisition data from temporary memory to acquisition list.
readout_module.store_scope_acquisition(1, "ttl")

# Get acquisition list from instrument.
data = readout_module.get_acquisitions(1)["ttl"]

# Plot acquired signal on both inputs (first 200 ns).
print("pulses detected: " + str(data["acquisition"]["bins"]["avg_cnt"][0]))

fig, ax = plt.subplots(1, 1, figsize=(15, 15 / 2 / 1.61))
ax.plot(data["acquisition"]["scope"]["path0"]["data"][0:200], label='Trace')
ax.axhline(y=threshold, color='r', label='Threshold')
ax.set_xlabel("Time (ns)")
ax.set_ylabel("Amplitude (Volt)")
plt.legend(loc="upper right")
plt.show()
pulses detected: 5
../../_images/tutorials_q1asm_tutorials_ttl_acquisition_36_1.png

We encourage you to play around with the num_pulses, wait_time, input_gain and threshold yourself.

Auto bin increment

In the play sequence section we set the ttl_acq_auto_bin_incr_en to False, meaning all of our pulses get counted within one bin. When it is set to True our data will be stored in separate bins, where the bin index is incremented by one for every detected pulse. The pulse count is therefore equal to the number of valid bins. When doing multiple measurements, this allows us to acquire a cumulative probability distribution of counted triggers.

To illustrate the usage we will define a function which returns a Q1ASM program that plays a N amount of pulses (at a 1 MHz rate), where N is a random number between 1 and 100 taken from a Poissonian distribution. This is meant to mock a stochastic physical process, e.g the amount of photons emitted by a laser. We will call the function a 1000 times and run it, without deleting the acquisition data between the runs. After this we will plot a histogram from our acquired data to inspect the result.

We will now define new Q1ASM programs. We define a function which returns a program that generates num_pulses number of pulses. These pulses are 16 ns long, and are send at a rate of 1 MHz again.

For the acquiring sequencer we will execute the same program as in the previous section, albeit with an acquisition window of 100.000 ns.

[20]:
# Sequence program for acquiring
seq_prog_acq = """
        move      10,R0       #Loop iterator.
        wait_sync 4           #Wait for sequencers to synchronize and then wait another 4ns.
        wait 140              #Approximate time of flight
        acquire_ttl 0,0,1,4   #Turn on TTL acquire on input path 0 and wait 4ns.
loop:
        wait 10000            #Wait 10000 ns
        loop      R0, @loop   #Repeat loop until R0 is 0
        acquire_ttl 0,0,0,4   #Turn off TTL acquire on input path 0 and wait 4ns.

        stop                   #Stop sequencer.
"""

We will first configure our sequencers. Then use random to generate an amount of pulses, and repeat this 10000 times.

[21]:
# Choose threshold and input gain
threshold = 0.5
input_gain = 0

# Delete previous acquisition.
readout_module.delete_acquisition_data(1, "ttl")

# Configure the TTL acquisition
readout_module.sequencer1.ttl_acq_input_select(0)
readout_module.sequencer1.ttl_acq_auto_bin_incr_en(True)

# Enable sync
readout_module.sequencer0.sync_en(True)
readout_module.sequencer1.sync_en(True)

#Set input gain and threshold
readout_module.in0_gain(input_gain)
readout_module.sequencer1.ttl_acq_threshold(threshold)

sequence_acq= {
    "waveforms": {},
    "weights": {},
    "acquisitions": acquisitions,
    "program": seq_prog_acq,
}

#Upload acquire sequence.
readout_module.sequencer1.sequence(sequence_acq)

# Map sequencer to specific outputs (but first disable all sequencer connections)
readout_module.disconnect_outputs()
readout_module.disconnect_inputs()

# Drive through first output
readout_module.sequencer0.connect_out0("I")

# Acquire through first input
readout_module.sequencer1.connect_acq_I('in0')
[22]:
num_pulses_list = random.poisson(lam=50, size=1000)
wait_time = 1000

for num_pulses in num_pulses_list:
    seq_prog_awg = generate_pulse_program(num_pulses, wait_time)
    upload_sequence(seq_prog_awg)
    # Arm and start sequencer.
    readout_module.arm_sequencer(0)
    readout_module.arm_sequencer(1)
    readout_module.start_sequencer()
    readout_module.get_acquisition_state(1, 1)

Create histogram

We retrieve the acquired data from the sequence to take a look at it.

[23]:
# Wait for the sequencer to stop with a timeout period of one minute.
readout_module.get_acquisition_state(1, 1)

# Move acquisition data from temporary memory to acquisition list.
readout_module.store_scope_acquisition(1, "ttl")

# Get acquisition list from instrument.
data = readout_module.get_acquisitions(1)["ttl"]["acquisition"]["bins"]["avg_cnt"]

# Plot acquired signal on both inputs.
print(f"counts per bin: {data}")
counts per bin: [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 999, 999, 998, 995, 987, 981, 973, 959, 940, 917, 893, 859, 814, 766, 711, 657, 614, 566, 512, 462, 401, 344, 294, 255, 210, 181, 150, 112, 91, 70, 51, 36, 27, 19, 11, 10, 7, 6, 3, 2, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan]

Every time the acquisition_ttl is ran the sequencer starts over with incrementing the bins (from bin 0). In every bin, the counts are summed up for all 1000 runs. Therefore, the data is a cumulative probability distribution of triggers counted. We now reorganize the acquired data into a probability distribution function (a histogram), and plot the result.

[24]:
def create_histogram(data):
    data_diff = np.diff(data)
    res = {
        count + 1: -v
        for (count, v) in enumerate(data_diff)
        if not (np.isnan(v) or np.isclose(v, 0))
    }
    return res

res = create_histogram(data)
plt.bar(res.keys(), res.values(), 1, color='g')
plt.title("1000 runs with a Poissonian distribution (µ = 50) of pulses")
plt.xlabel("Trigger Count")
plt.ylabel("Occurrence")
plt.show()
../../_images/tutorials_q1asm_tutorials_ttl_acquisition_47_0.png

Stop

Finally, let’s stop the sequencers if they haven’t already and close the instrument connection. One can also display a detailed snapshot containing the instrument parameters before closing the connection by uncommenting the corresponding lines.

[25]:
# Stop sequencer.
readout_module.stop_sequencer()

# Print status of sequencer.
print(readout_module.get_sequencer_state(0))
print()

# Uncomment the following to print an overview of the instrument parameters.
# Print an overview of the instrument parameters.
# print("Snapshot:")
# qrm.print_readable_snapshot(update=True)

# Close all QCoDeS Instrument instances.
Instrument.close_all()
Status: STOPPED, Flags: FORCED_STOP