Tutorial: Timetagging#
Introduction#
Note
The Timetag and TimetagTrace protocols are currently only implemented for the Qblox backend, and they are available on the QTM. Please also see Acquisition details for more information on Qblox module-specific behavior of these operations.
The timetag acquisitions return the time at which the input voltage crossed a set threshold. This tutorial explores various ways to perform timetag acquisitions, using a Qblox cluster with a QCM and a QTM module. Note that only a subset of the features of these acquisitions are shown here, see the reference guide for more info.
Initial setup#
First, we import the required classes and set up the data directory.
from qblox_instruments import Cluster, ClusterType
from quantify_core.data import handling as dh
from quantify_scheduler.backends.graph_compilation import SerialCompiler
from quantify_scheduler.device_under_test.quantum_device import QuantumDevice
from quantify_scheduler.enums import BinMode, TimeRef, TimeSource
from quantify_scheduler.instrument_coordinator.components.qblox import ClusterComponent
from quantify_scheduler.instrument_coordinator.instrument_coordinator import (
InstrumentCoordinator,
)
from quantify_scheduler.operations.acquisition_library import (
Timetag,
TimetagTrace,
Trace,
)
from quantify_scheduler.operations.pulse_library import SquarePulse
from quantify_scheduler.schedules.schedule import Schedule
dh.set_datadir(dh.default_datadir())
Next, we write the hardware configuration:
In the
"hardware_description"
we define a QCM and a QTM module (when using physical hardware, we also connect port 1 of the QCM with port 1 of the QTM).In the
"connectivity"
we assign the QCM port 1 ("cluster0.module{QCM_SLOT}.real_output_0"
) to a mock device port"qcm:out"
, and assign the QTM port 1 ("cluster0.module{QTM_SLOT}.digital_output_0"
) to the mock device port"qtm:in"
.In the Digitization thresholds of
"hardware_options"
we set the value ofin_threshold_primary
field, which is the value of the voltage threshold that needs to be crossed to register a timetag in QTM modules. Note the"qtm:in-digital"
key that is used here;digital
is the default clock assigned to digital channels.
QCM_SLOT = 7
QTM_SLOT = 10
hw_cfg = {
"config_type": "quantify_scheduler.backends.qblox_backend.QbloxHardwareCompilationConfig",
"hardware_description": {
"cluster0": {
"instrument_type": "Cluster",
"ref": "internal",
"modules": {
f"{QCM_SLOT}": {"instrument_type": "QCM"},
f"{QTM_SLOT}": {"instrument_type": "QTM"},
},
},
},
"hardware_options": {
"digitization_thresholds": {"qtm:in-digital": {"in_threshold_primary": 0.5}},
},
"connectivity": {
"graph": [
[f"cluster0.module{QCM_SLOT}.real_output_0", "qcm:out"],
[f"cluster0.module{QTM_SLOT}.digital_input_0", "qtm:in"],
]
},
}
We create an InstrumentCoordinator
instance with a cluster, where we also provide a dummy_cfg
dummy configuration so that this notebook can run without physical hardware (when using a real cluster, provide identifier
instead of dummy_cfg
).
instrument_coordinator = InstrumentCoordinator(name="ic")
cluster = Cluster(
name="cluster0",
# identifier="10.10.10.10",
dummy_cfg={
QCM_SLOT: ClusterType.CLUSTER_QCM,
QTM_SLOT: ClusterType.CLUSTER_QTM,
},
)
cluster_component = ClusterComponent(cluster)
instrument_coordinator.add_component(cluster_component)
Finally, we define a QuantumDevice
(since we do not have a “real” quantum device, this object serves simply to generate the compilation configuration later on).
quantum_device = QuantumDevice(name="quantum_device")
quantum_device.hardware_config(hw_cfg)
Timetag acquisition#
In all examples in this tutorial, except the gate-level example, the QCM will send four pulses with increasing time between the pulses.
In the first example, we’ll record a single timetag using the Timetag
acquisition protocol. The event that triggers this timetag is defined by the time_source
parameter, which we set to the first detected rising edge (TimeSource.FIRST
). The timetag represents the time elapsed since the moment specified by the time_ref
parameter, which we set to the start of the acquisition (TimeRef.START
). We will simply do one repetition of the schedule.
Note
With the bin_mode
parameter it is possible to change the behaviour of the acquisition for multiple repetitions. Please see the reference guide for more information.
sched = Schedule("Timetag")
sched.add(
Timetag(
duration=10e-6,
port="qtm:in",
clock="digital",
time_source=TimeSource.FIRST,
time_ref=TimeRef.START,
)
)
square_pulse = SquarePulse(amp=1.0, duration=200e-9, port="qcm:out")
sched.add(square_pulse, rel_time=100e-9, ref_pt="start")
for rel_time in (1e-6, 2e-6, 3e-6):
sched.add(square_pulse, rel_time=rel_time, ref_pt="start")
Let’s compile the schedule.
compiler = SerialCompiler(name="compiler")
compiled_schedule = compiler.compile(
schedule=sched,
config=quantum_device.generate_compilation_config(),
)
And send it to the hardware, and execute it.
instrument_coordinator.prepare(compiled_schedule)
instrument_coordinator.start()
instrument_coordinator.wait_done(timeout_sec=10)
acquisition = instrument_coordinator.retrieve_acquisition()
The acquisition data shows one timetag, in this case around 73 ns. This value can differ depending on cable length.
acquisition.isel(repetition=0)
<xarray.Dataset> Size: 16B Dimensions: (acq_index_0: 1) Coordinates: * acq_index_0 (acq_index_0) int64 8B 0 Data variables: 0 (acq_index_0) float64 8B 72.97
Next up, we will show you how to record multiple timetags in an acquisition window.
TimetagTrace acquisition#
The TimetagTrace
acquisition can record a stream of timetags. Every time that a rising edge is detected, a timetag is recorded. Therefore, this time we expect to see 4 timetags from the 4 pulses sent by the QCM.
The timetag values are equal to the time difference between the recorded rising edges and the time_ref
(TimeRef
), which we set to the start of the acquisition (TimeRef.START
).
sched = Schedule("TimetagTrace")
sched.add(
TimetagTrace(duration=10e-6, port="qtm:in", clock="digital", time_ref=TimeRef.START)
)
square_pulse = SquarePulse(amp=1.0, duration=200e-9, port="qcm:out")
sched.add(square_pulse, rel_time=100e-9, ref_pt="start")
for rel_time in (1e-6, 2e-6, 3e-6):
sched.add(square_pulse, rel_time=rel_time, ref_pt="start")
We compile the schedule.
compiler = SerialCompiler(name="compiler")
compiled_schedule = compiler.compile(
schedule=sched,
config=quantum_device.generate_compilation_config(),
)
And we execute it on the hardware.
instrument_coordinator.prepare(compiled_schedule)
instrument_coordinator.start()
instrument_coordinator.wait_done(timeout_sec=10)
acquisition = instrument_coordinator.retrieve_acquisition()
As expected, we record 4 timetags. The first one is roughly around the same value as the single timetag recorded above, and the other ones are 1000, 3000, and 6000 ns later, respectively.
acquisition.isel(acq_index_0=0, repetition=0)
<xarray.Dataset> Size: 72B Dimensions: (trace_index_0: 4) Coordinates: acq_index_0 int64 8B 0 * trace_index_0 (trace_index_0) int64 32B 0 1 2 3 Data variables: 0 (trace_index_0) float64 32B 72.99 1.073e+03 ... 6.073e+03
Trace acquisition#
Finally, we can measure a trace of the input signal. The QTM digitizes this signal and will return 0
whenever the voltage is below the digitization threshold, and 1
when the voltage is above. The trace has a 1 ns time resolution.
We use the same schedule again, but with the Trace
protocol. Note the bin_mode
, which must be set to BinMode.FIRST
when using Trace
acquisition on a QTM module.
sched = Schedule("Trace")
sched.add(Trace(duration=10e-6, port="qtm:in", clock="digital", bin_mode=BinMode.FIRST))
square_pulse = SquarePulse(amp=1.0, duration=200e-9, port="qcm:out")
sched.add(square_pulse, rel_time=100e-9, ref_pt="start")
for rel_time in (1e-6, 2e-6, 3e-6):
sched.add(square_pulse, rel_time=rel_time, ref_pt="start")
We compile the schedule.
compiler = SerialCompiler(name="compiler")
compiled_schedule = compiler.compile(
schedule=sched,
config=quantum_device.generate_compilation_config(),
)
And execute it on the hardware again.
instrument_coordinator.prepare(compiled_schedule)
instrument_coordinator.start()
instrument_coordinator.wait_done(timeout_sec=10)
acquisition = instrument_coordinator.retrieve_acquisition()
The result is a trace of the pulses sent by the QCM, digitized by the QTM.
acquisition.to_dataarray().plot()
[<matplotlib.lines.Line2D at 0x78b9ca2aa100>]
Gate-level: NV center example#
To finish the tutorial, we show how to use the timetag acquisition protocol with an NV center defined in the quantum device.
To keep the tutorial focused, we only include the readout laser in the hardware configuration (and leave re-ionization and spin-pump lasers out). Again, we use port 1 of the QCM, this time connected to an optical modulator, and port 1 of the QTM, connected to a single-photon detector:
QCM_SLOT = 7
QTM_SLOT = 10
hw_cfg = {
"config_type": "quantify_scheduler.backends.qblox_backend.QbloxHardwareCompilationConfig",
"hardware_description": {
"cluster0": {
"instrument_type": "Cluster",
"ref": "internal",
"modules": {
f"{QCM_SLOT}": {"instrument_type": "QCM"},
f"{QTM_SLOT}": {"instrument_type": "QTM"},
},
},
"red_laser": {"instrument_type": "LocalOscillator", "power": 1},
"optical_mod_red_laser": {"instrument_type": "OpticalModulator"},
},
"hardware_options": {
"modulation_frequencies": {
"qe0:optical_control-qe0.ge0": {"interm_freq": 0.0, "lo_freq": None},
"qe0:optical_readout-qe0.ge0": {"interm_freq": 0.0, "lo_freq": None},
},
"digitization_thresholds": {
"qe0:optical_readout-qe0.ge0": {"in_threshold_primary": 0.5}
},
},
"connectivity": {
"graph": [
[f"cluster0.module{QCM_SLOT}.real_output_0", "optical_mod_red_laser.if"],
["red_laser.output", "optical_mod_red_laser.lo"],
["optical_mod_red_laser.out", "qe0:optical_control"],
[f"cluster0.module{QTM_SLOT}.digital_input_0", "qe0:optical_readout"],
]
},
}
We now add a mock laser to our instrument coordinator so that the example compiles.
from quantify_scheduler.helpers.mock_instruments import MockLocalOscillator
from quantify_scheduler.instrument_coordinator.components.generic import (
GenericInstrumentCoordinatorComponent,
)
red_laser = MockLocalOscillator(name="red_laser")
instrument_coordinator.add_component(GenericInstrumentCoordinatorComponent(red_laser))
Then we add a BasicElectronicNVElement
(our NV center) to the quantum device. Note that measurement parameters such as time_source
and time_ref
are now defined on the quantum device element.
from quantify_scheduler.device_under_test.nv_element import BasicElectronicNVElement
quantum_device.hardware_config(hw_cfg)
qe0 = BasicElectronicNVElement("qe0")
qe0.measure.pulse_amplitude(1.0)
qe0.measure.time_source(TimeSource.FIRST)
qe0.measure.time_ref(TimeRef.START)
qe0.clock_freqs.ge0.set(470.4e12)
quantum_device.add_element(qe0)
The schedule consists simply of a Measure
operation, which includes a readout pulse (sent from the QCM to the optical modulator) and a timetag acquisition.
from quantify_scheduler.operations.gate_library import Measure
sched = Schedule("NV Timetag")
sched.add(Measure("qe0", acq_protocol="Timetag", bin_mode=BinMode.APPEND))
Finally, we compile the schedule and show the pulse and the acquisition operation in a pulse diagram.
compiler = SerialCompiler(name="compiler")
compiled_schedule = compiler.compile(
schedule=sched,
config=quantum_device.generate_compilation_config(),
)
compiled_schedule.plot_pulse_diagram(plot_backend="plotly")