See also
A Jupyter notebook version of this tutorial can be downloaded here
.
Photon count based feedback using a QTM#
The QTM enables the experiment to carry out complex operations that rely on photon count based feedback. The Truth table operation inherent in the QTM enables the user to carry out logical operation using the acquisition results from up to 4 input channels of the QTM. Based on these logical operations, one can forward a trigger to the trigger network thus enabling count based feedback.
For this tutorial, we need a QCM and a QTM module. We will output digital pulses from channels 1 and 3 of the QTM. These pulses will be acquired by the channels 5 and 7 of the same QTM.
Note
Each channel of the QTM can both work in output or input mode. Their indexing in the qblox_instruments
API goes from :meth:Module.io_channel0
to :meth:Module.io_channel7
, whereas the indexing labeled on the front panel of the module (from which we were referring to in the text above) goes from 1 to 8.
The QCM will play a series of pulses depending on the acquisition measurements carried out by the QTM. We will require an oscilloscope to display the outcomes from the acquisition measurement carried out by the QTM.
To display the behavior of the QCM, its marker channel M1 should be used to trigger an oscilloscope. We will then observe the outcome of the acquisition experiment by monitoring \(\text{O}^{1}\) of the QCM on the oscilloscope.
Setup#
First, we are going to import the required packages and connect to the instrument.
[1]:
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
import matplotlib.pyplot as plt
import numpy as np
import scipy
from qcodes.instrument import find_or_create_instrument
from qblox_instruments import Cluster, ClusterType, Module
from quantify_scheduler.waveforms import skewed_hermite
if TYPE_CHECKING:
from qblox_instruments.qcodes_drivers.io_channel import IOChannel
from qblox_instruments.qcodes_drivers.sequencer import Sequencer
Scan For Clusters#
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).
!qblox-pnp list
[2]:
cluster_ip = "10.10.200.42"
cluster_name: str = "cluster0"
Connect to th Qblox cluster#
We now make a connection with the Cluster.
[3]:
cluster: Cluster = find_or_create_instrument(
Cluster,
recreate=True,
name=cluster_name,
identifier=cluster_ip,
dummy_cfg=(
{
2: ClusterType.CLUSTER_QCM,
4: ClusterType.CLUSTER_QRM,
6: ClusterType.CLUSTER_QCM_RF,
8: ClusterType.CLUSTER_QRM_RF,
10: ClusterType.CLUSTER_QTM,
}
if cluster_ip is None
else None
),
)
Get connected modules#
Connect to the QTM and QCM modules
[4]:
# QRM baseband modules
timetag_modules: dict[int, Module] = cluster.get_connected_modules(lambda mod: mod.is_qtm_type)
qtm: Module = timetag_modules[next(iter(timetag_modules))]
# QCM baseband modules
control_modules: dict[int, Module] = cluster.get_connected_modules(
lambda mod: mod.is_qcm_type and not mod.is_rf_type
)
qcm: Module = control_modules[next(iter(control_modules))]
[5]:
# Define helper function to prepare sequencer by passing required arguments for the sequence dictionary.
def prepare_sequence_object(
sequencer: Sequencer,
program: str | None = None,
waveforms: dict | None = None,
acquisitions: dict | None = None,
weights: dict | None = None,
) -> None:
if acquisitions is None:
acquisitions = {}
if weights is None:
weights = {}
if waveforms is None:
waveforms = {}
if program is None:
program = "stop"
sequencer.sequence(
{
"waveforms": waveforms,
"weights": weights,
"acquisitions": acquisitions,
"program": program,
}
)
sequencer.arm_sequencer()
Coincidence detection based feedback on two channels#
In this example, we will demonstrate how to carry out low latency feedback based upon a coincident detection of two photons. Coincident detection of detection events is a cornerstone of several experiments involving quantum computation and networking. It is indeed at the very core of correlation measurements, where various quantum effects are expected to occur (non-classical correlations is a common entanglement witness). It is also desirable in the context of heralding driven entanglement generation, which nowadays is a standard technique in photonics.
Here we will use the truth-table feature of the QTM to generate a feedback signal based on the condition that two photons are detected within a certain time window.
Protocol for the experiment#
Photon incidence in this demonstration is simulated by digital pulses generated by the QTM itself. We will configure two channels to output digital pulses, with the possibility to creating a delay on one channel. The digital pulses will be acquired on two other channels within an acquisition window. These detections in the acquisition window are threshold-based on predefined count threshold, to determine the outcome of the acquisition. We will use this count threshold to define photon detection success. A detection event that passes this success condition on both channels defines a coincidence detection.
We will configure the LUT to send triggers to different trigger addresses depending on:
Whether a coincidence detection occurred.
Or a single detection on either channel occurred.
No detections occurred.
Based on which trigger address is updated, we will use the QCM to play different sets of pulses which will be displayed on an oscilloscope, thereby demonstrating feedback.
Configure QTM and QCM channels#
First we configure the QTM channels which will detect the photons and provide the data for the look up table.
The QTM currently only supports one to one channel to sequencer mapping: it is a good idea to define a helper functions meant to simplify the configuration of multiple sequencers which need to be initialized in a similar way. We will define such function in both cases of input and output channels.
[6]:
# Define a helper function that will configure a QTM channel and sequencer to be able to carry out an acquisition.
def config_qtm_input_channel(io: IOChannel, seq: Sequencer, analog_thresh: float) -> None:
seq.sync_en(True)
io.out_mode("disabled")
io.in_threshold_primary(analog_thresh)
io.binned_acq_threshold_source("thresh0")
io.binned_acq_time_ref("start") # for timetag
io.binned_acq_time_source("first")
io.binned_acq_on_invalid_time_delta("record_0")
[7]:
# Define helper function to allow QTM to output TTL signals.
def config_qtm_output_channel(io: IOChannel, seq: Sequencer) -> None:
seq.sync_en(True)
io.out_mode("sequencer")
Configure the QCM channel and sequencer, and update trigger count thresholds.
[8]:
# Configure the QCM channels to output waveforms, and set the trigger address thresholds.
qcm.disconnect_outputs()
qcm.sequencer0.connect_sequencer("out0")
qcm.sequencer0.mod_en_awg(True)
qcm.sequencer0.sync_en(True)
qcm.sequencer0.nco_freq(100e6)
# Update trigger count thresholds.
qcm.sequencer0.trigger1_count_threshold(1)
qcm.sequencer0.trigger1_threshold_invert(False)
qcm.sequencer0.trigger2_count_threshold(1)
qcm.sequencer0.trigger2_threshold_invert(False)
qcm.sequencer0.trigger3_count_threshold(1)
qcm.sequencer0.trigger3_threshold_invert(False)
QCM Waveforms#
Here we create and plot the different waveforms that will be played when the QCM receives the trigger from the LUT.
[9]:
# Waveform parameters
WAVEFORM_LENGTH: int = 200 # time in nanoseconds
TIMES: np.ndarray = np.arange(WAVEFORM_LENGTH) * 1e-9 # goes from 0 to (WAVEFORM_LENGTH-1)
GAUSSIAN: np.ndarray = scipy.signal.windows.gaussian(WAVEFORM_LENGTH, std=0.12 * WAVEFORM_LENGTH)
HERMITE: np.ndarray = np.real(
skewed_hermite(t=TIMES, duration=WAVEFORM_LENGTH * 1e-9, amplitude=0.6, skewness=0, phase=0)
)
# Waveform dictionary (data will hold the samples and index will be used to select the waveforms in the instrument).
WAVEFORMS = {
"single_detection": {"data": GAUSSIAN.tolist(), "index": 0},
"coincident_detection": {"data": HERMITE.tolist(), "index": 1},
"no_detection": {"data": [0.0] * WAVEFORM_LENGTH, "index": 2},
}
Let’s plot the waveforms to see what we have created.
[10]:
fig, ax = plt.subplots(1, 1)
for wf, d in WAVEFORMS.items():
ax.plot(TIMES, d["data"], label=wf)
ax.legend()
ax.grid(alpha=1 / 10)
ax.set_ylabel("Waveform primitive amplitude")
ax.set_xlabel("Time (ns)")
plt.draw()
plt.show()

Configure the truth-table.#
Here we will configure and enable the truth-table on the QTM. The truth-table data (specified in the manner of a dictionary) sets the conditions under which a trigger will be sent on the network and on which address.
[11]:
# Enable the look up table.
qtm.io_channel4.thresholded_acq_truth_table_en(True)
# Configure the data in the truth-table.
qtm.io_channel4.thresholded_acq_trigger_en(True)
[12]:
# Here we define the truth-table (tt) config , that encodes the conditions under which a trigger will be sent.
tt_config: list[dict[Literal["conditions", "trigger"], list[dict[str, int | str]] | int]] = [
{
"conditions": [
{"channel": 4, "measurement": "count", "level": "high"},
{"channel": 6, "measurement": "count", "level": "high"},
],
"trigger": 1,
},
{
"conditions": [
{"channel": 4, "measurement": "count", "level": "mid"},
{"channel": 6, "measurement": "count", "level": "high"},
],
"trigger": 2,
},
{
"conditions": [
{"channel": 4, "measurement": "count", "level": "high"},
{"channel": 6, "measurement": "count", "level": "mid"},
],
"trigger": 3,
},
]
# Upload truth table config to the QTM channel.
qtm.io_channel4.thresholded_acq_truth_table_data(tt_config)
Below we define parameters for photon acquisition and thresholding. We define the two-photon count thresholds that will determine the success of a photon detection. The QTM supports dual-count based thresholds: these thresholds control how the number of detection events within an acquisition window map to the experiment outcome.
Counts less than first threshold (
THRESH_0
) returns the outcomelow
.Counts equal or greater than first threshold (
THRESH_0
) returns the outcomemid
.Counts greater than second threshold (
THRESH_1
) returns the outcomehigh
.
These counts will then be used by the truth-table to perform conditional logic and send trigger pulses.
[13]:
THRESH_0: int = 0 # Acquisition will pass `mid` to LUT for counts that are equal to 0.
THRESH_1: int = 1 # Acquisition will pass `high` to LUT for counts that are equal to 1.
# Define acquisition parameters.
ACQUISITION_WINDOW: int = 2000 # nanoseconds.
[14]:
# Define acquisition dictionary.
ACQUISITION_MODE: dict[str, dict[str, int]] = {
"timetag_single": {"num_bins": 1, "index": 0},
}
Here we define the Q1ASM program responsible for opening and closing the acquisition window on the QTM channels that will detect the photon. In the program, we also update the two thresholds for count based thresholding.
[15]:
# Define program to detect incoming photons within a certain time window.
DETECTION_SCRIPT: str = f"""
upd_thres 0, {THRESH_0}, 4
upd_thres 1, {THRESH_1}, 4
wait_sync 4
acquire_timetags 0, 0, 1, 0, 4 # acq_idx, bin_idx, enable, fine_delay, duration
wait {ACQUISITION_WINDOW - 4}
acquire_timetags 0, 0, 0, 0, 4
stop
"""
To simulate the arrival of photons, we send a digital pulse from two channels of QTM using two sequencers. In order to deterministically fulfill or fail the truth-table conditions, we create helper function that allows us to add or remove a delay between the two sequencers.
[16]:
# Define function that returns a program for QTM channel to output digital pulse.
def digital_out(delay: int = 200) -> str:
prog = f"""
wait_sync 4
wait {delay}
set_digital 1,1,0
upd_param 4
wait 100
set_digital 0,1,0
upd_param 4
stop
"""
return prog
[17]:
# Define program to use conditional logic to play different waveforms depending on the result of an acquisition.
QCM_FEEDBACK_PROG: str = f"""
set_latch_en 1, 4 # Enable all trigger network address counters.
latch_rst 4 # Reset all trigger network address counters back to 0.
wait_sync 4
wait {ACQUISITION_WINDOW + 1200} # Add extra wait time to give trigger network time to react.
set_mrk 1
set_cond 1, 1, 0, 8 # 1 = Enable, 1 = trig add, 0 = OR, 8 = wait_time.
play 1, 1, 1000
play 1, 1, 1000
set_cond 1, 2, 0, 8 # 1 = Enable, 2 = trig add, 0 = OR, 8 = wait_time.
play 2, 2, 1000
play 0, 0, 1000
set_cond 1, 4, 0, 8 # 1 = Enable, 3 = trig add, 0 = OR, 8 = wait_time.
play 0, 0, 1000
play 2, 2, 1000
set_cond 0, 0, 0, 8 # Disable conditional.
set_mrk 0
upd_param 4
stop # Stop the sequence after the last iteration.
"""
[18]:
# Configure channels 5 and 7 to carry out analog thresholded TTL acquisition and timetagging.
config_qtm_input_channel(qtm.io_channel4, qtm.sequencer4, 0.5)
config_qtm_input_channel(qtm.io_channel6, qtm.sequencer6, 0.5)
[19]:
# Configure output of two QTM channels.
config_qtm_output_channel(qtm.io_channel0, qtm.sequencer0)
config_qtm_output_channel(qtm.io_channel2, qtm.sequencer2)
[20]:
# Upload and arm sequencers used for the detection.
prepare_sequence_object(qtm.sequencer4, acquisitions=ACQUISITION_MODE, program=DETECTION_SCRIPT)
prepare_sequence_object(qtm.sequencer6, acquisitions=ACQUISITION_MODE, program=DETECTION_SCRIPT)
# Upload and arm sequencers used for the pulse generation, with the choice to include a delay on both channels.
prepare_sequence_object(qtm.sequencer0, program=digital_out())
prepare_sequence_object(qtm.sequencer2, program=digital_out())
[21]:
# Prepare sequencer of QCM to play series of pulses depending on outcome of detection.
prepare_sequence_object(qcm.sequencer0, waveforms=WAVEFORMS, program=QCM_FEEDBACK_PROG)
[22]:
qtm.start_sequencer()
qcm.start_sequencer()
Outcome of the experiment.#
The outcome of the experiments we run should display the following results depending on the initial pulse conditions that we set.
Since we sent both digital pulses at the same time, the oscilloscope should display two pulses as shown in the figure below.
We can add a delay to channel 1, such that the pulse arrives after the acquisition window has closed. In such a case, the oscilloscope display will look like this.
Adding a delay to channel 3 results in an output that looks like this.
[23]:
qtm.stop_sequencer()
qcm.stop_sequencer()