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
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:
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
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()
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