See also

An IPython notebook version of this tutorial can be downloaded here:

ttl_acquisition.ipynb

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.

[2]:
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 qblox_instruments import Cluster, PlugAndPlay, Pulsar

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).

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

# create widget for names and ip addresses
connect = widgets.Dropdown(
    options=[(device_list[key]["description"]["name"]) for key in device_list.keys()],
    description="Select Device",
)
print(
    "The following widget displays all the existing modules that are connected to your PC which includes the Pulsar modules as well as a Cluster. Select the device you want to run the notebook on."
)
display(connect)
The following widget displays all the existing modules that are connected to your PC which includes the Pulsar modules as well as a Cluster. Select the device you want to run the notebook on.

Pulsar QRM

Choose the Pulsar QRM, run the following cell. Skip to the Cluster QRM section if you selected a Cluster module.

[ ]:
# Close existing connections to the Pulsar modules
Pulsar.close_all()

# Retrieve device name and IP address
device_name = connect.value
device_number = connect.options.index(device_name)
ip_address = device_list[device_keys[device_number]]["identity"]["ip"]

# Connect to device and reset
qrm = Pulsar(f"{device_name}", ip_address, debug=True)
qrm.reset()
print(f"{device_name} connected at {ip_address}")
print(qrm.get_system_state())

Skip to Generate Waveforms if you have not selected a Cluster module.

Cluster QRM

First we connect to the Cluster using its IP address. Go to the Pulsar QRM section if you are using a Pulsar.

[4]:
# close all previous connections to the cluster
Cluster.close_all()

# Retrieve device name and IP address
device_name = connect.value
device_number = connect.options.index(device_name)
ip_address = device_list[device_keys[device_number]]["identity"]["ip"]

# connect to the cluster and reset
cluster = Cluster(device_name, ip_address)
cluster.reset()
print(f"{device_name} connected at {ip_address}")
cluster-mm connected at 192.168.2.150

We then find all available cluster modules to connect to them individually.

[5]:
# Find all QRM/QCM modules
available_slots = {}
for module in cluster.modules:
    # if module is currently present in stack
    if cluster._get_modules_present(module.slot_idx):
        # check if QxM is RF or baseband
        if module.is_rf_type:
            available_slots[f"module{module.slot_idx}"] = ["QCM-RF", "QRM-RF"][
                module.is_qrm_type
            ]
        else:
            available_slots[f"module{module.slot_idx}"] = ["QCM", "QRM"][
                module.is_qrm_type
            ]

# List of all QxM modules present
connect_qxm = widgets.Dropdown(options=[key for key in available_slots.keys()])

print(available_slots)
# display widget with cluster modules
print()
print("Select the QRM module from the available modules in your Cluster:")
display(connect_qxm)
{'module8': 'QRM'}

Select the QRM module from the available modules in your Cluster:

Finally, we connect to the selected Cluster module.

[6]:
# Connect to the cluster QRM
qrm = getattr(
    cluster, connect_qxm.value
)  # Connect to the module that you have chosen above
print(f"{available_slots[connect_qxm.value]} connected")
print(cluster.get_system_state())
QRM connected
Status: OKAY, Flags: NONE, Slot flags: NONE

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.

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

Create Q1ASM programs

Now that we have the waveform and acquisition speficied, 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 aqcuiring
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 acquistitions 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.
qrm.sequencer0.sequence(sequence_awg)
qrm.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)
for sequencer in qrm.sequencers:
    for out in range(0, 2):
        sequencer.set("channel_map_path{}_out{}_en".format(out % 2, out), False)
qrm.sequencer0.channel_map_path0_out0_en(True)
qrm.sequencer0.channel_map_path1_out1_en(True)

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

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

# Configure scope mode
qrm.scope_acq_sequencer_select(1)

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

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

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

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

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

# Print status of sequencer.
print(qrm.get_sequencer_state(0, 1))
print(qrm.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.
qrm.get_acquisition_state(1, 1)

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

# Get acquisition list from instrument.
data = qrm.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_ttl_acquisition_28_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 and wait_time. 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 1000ns
               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,
    }

    qrm.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.
qrm.delete_acquisition_data(1, "ttl")

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

We then arm the sequencers and play the sequence.

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

# Print status of sequencer.
print(qrm.get_sequencer_state(0, 1))
print(qrm.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.
qrm.get_acquisition_state(1, 1)

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

# Get acquisition list from instrument.
data = qrm.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: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_ttl_acquisition_38_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 aqcuiring
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.
qrm.delete_acquisition_data(1, "ttl")

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

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

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

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

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

# Map sequencer to specific outputs (but first disable all sequencer connections)
for sequencer in qrm.sequencers:
    for out in range(0, 2):
        sequencer.set("channel_map_path{}_out{}_en".format(out % 2, out), False)
qrm.sequencer0.channel_map_path0_out0_en(True)
qrm.sequencer0.channel_map_path1_out1_en(True)
[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.
    qrm.arm_sequencer(0)
    qrm.arm_sequencer(1)
    qrm.start_sequencer()
    qrm.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.
qrm.get_acquisition_state(1, 1)

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

# Get acquisition list from instrument.
data = qrm.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 distrubition 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):
    res = {}
    runs = data[0]
    old_temp = 0
    for count, value in enumerate(data):
        # Reached end of data
        if str(value) == 'nan': break
        new_temp = runs - value
        # Only add if something changed
        if new_temp == old_temp: continue
        num_pulses = new_temp - old_temp
        old_temp = new_temp
        res[count] = num_pulses
    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("Occurence")
plt.show()
../_images/tutorials_ttl_acquisition_49_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.
qrm.stop_sequencer()

# Print status of sequencer.
print(qrm.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 the instrument connection.
Pulsar.close_all()
Cluster.close_all()
Status: STOPPED, Flags: FORCED_STOP