Color Centers: Using Time Tagger with neartime callback functions¶
The following notebook is intended to show how to use a time tagger from a third-party to integrate the readout of color center inside a LabOne Q experiment. We will use a Time Tagger of Swabian Instruments for the purpose of this demonstration.
0 - General Imports¶
from __future__ import annotations
# convenience Import for all LabOne Q Functionality
import matplotlib.pyplot as plt
# utilities
import numpy as np
from laboneq.dsl.result.acquired_result import AcquiredResult
from laboneq.simple import *
# Mock TimeTagger
class MockCountBetweenMarkers:
def __init__(self, tagger, click_channel: int, begin_channel: int, end_channel: int, n_values: int): #noqa: ARG002
self.n_values = n_values
def getData(self): #noqa: N802
"""Return mock data."""
return np.random.randint(0,100, self.n_values)
def createMockTimeTaggerNetwork(address): #noqa: N802
"""Mock createMockTimeTaggerNetwork."""
return MockTimeTagger()
class MockTimeTagger:
def setTriggerLevel(self,counter_input, threshold): #noqa: N802
"""Mock setTriggerLevel."""
def setInputDelay(self,counter_input, delay): #noqa: N802
"""Mock setInputDelay."""
def setTriggerLevel(self, gate_start_input, threshold): #noqa: N802
"""Mock setTriggerLevel."""
def setInputDelay(self, gate_start_input, delay):# noqa: N802
"""Mock setInputDelay."""
# swabian libraries
try:
from TimeTagger import CountBetweenMarkers, createTimeTaggerNetwork
except ImportError:
print("TimeTagger not found, using MockTimeTagger instead. Users are advised to install TimeTagger for more accurate result.")
CountBetweenMarkers = MockCountBetweenMarkers
createTimeTaggerNetwork = createMockTimeTaggerNetwork #noqa: N816
# ADDRESS FOR THE SERVER
dataserver = (
"localhost" # address of LabOne dataserver and TimeTagger server for Swabian
)
# SETTINGS FOR THE SHFSG
shfsg_address = "devXXXXX" # address of SHFSG
drive_channel = 1 # channel used for the drive of the qubit
apd_channel = 2 # channel used for simulating the pulses for the tagger
# SETTINGS FOR THE TIME TAGGER
timetagger_server = "localhost" # server connected to the timetagger
timetagger_port = 00000 # port of the timetagger server
counter_input = 2 # input to act as Gated counter
gate_start_input = 1 # input for the start gate signal
gate_end_input = -1 # input for the end gate signal
1 - Setting up all devices¶
We first need to define a calibration and our device setup. In this notebook, we will define our connections programmatically
setup = DeviceSetup("TimeTagger_setup")
# add a dataserver
setup.add_dataserver(host=dataserver, port=8004)
# add an SHFSG
setup.add_instruments(
SHFSG(uid="device_shfsg", address=shfsg_address, device_options="SHFSG8")
)
# add connections
setup.add_connections(
"device_shfsg",
create_connection(
to_signal="q0/drive_line", ports=f"SGCHANNELS/{drive_channel}/OUTPUT"
),
create_connection(
to_signal="q0/apd_line", ports=f"SGCHANNELS/{apd_channel}/OUTPUT"
),
create_connection(
to_signal="q0/aom_line", ports=f"SGCHANNELS/{apd_channel}/OUTPUT"
),
)
# shortcut to connections
drive_lsg = setup.logical_signal_groups["q0"].logical_signals["drive_line"]
apd_lsg = setup.logical_signal_groups["q0"].logical_signals["apd_line"]
aom_lsg = setup.logical_signal_groups["q0"].logical_signals["aom_line"]
1.1 - Calibration¶
Read about applying instrument settings through calibration objects. Here, we set a very simple calibration acting directly on the logical signal lines. For the drive_line
, we use a typical RF pulse to excite an NV center. For the apd_line
, we set the frequency to zero and set it to the LF path, since we plan on using this channel only for DC pulses. Finally, the aom_line
will be used to distributed TTL signal to an AOM when we will use the setup in our NV center configuration. For this reason, we will leave it without calibration
# NV center drive
drive_lsg.calibration = SignalCalibration()
drive_lsg.local_oscillator = Oscillator("NV_lo_osc", frequency=2.8e9)
drive_lsg.oscillator = Oscillator("NV_osc", frequency=-13e6)
drive_lsg.port_mode = PortMode.RF
drive_lsg.range = 10 # dBm
# channel to distribute trigger and simulate APD output
apd_lsg.port_mode = PortMode.LF
apd_lsg.range = 10 # dBm
apd_lsg.local_oscillator = Oscillator(uid="apd_lo", frequency=0)
apd_lsg.oscillator = Oscillator(uid="apd_osc", frequency=0)
# channel used to trigger AOM with TTL, left uncalibrated
aom_lsg.calibration = SignalCalibration()
1.2 - Connect LabOne Q Session¶
The Session class provides the connection to the instruments, and it can also be used to emulate the connection so that no hardware is necessary for testing.
Note: In this notebook, emulate = False
is compulsory, as we will use Near-time Callback Functions and 3rd-Party Devices that cannot be emulated.
session = Session(device_setup=setup)
session.connect(do_emulation=True)
1.3 Connect Swabian TimeTagger¶
We now connect to the Time Tagger of Swabian instrument. In this case, our counter is connected to the same ip of the dataserver, so we just need to set the correct port to find an instrument. To see in more details how to set a server, see the Swabian Time Tagger Manual
# address of the Time Tagger
address = f"{timetagger_server}:{timetagger_port}"
# connect to the server
timetagger = createTimeTaggerNetwork(address)
2 - Test the TimeTagger¶
In this section, we will perform some basic test on the Time Tagger, and show how to control it and sweep its parameter inside the LabOne Q DSL. For this purpose, we will use the setup illustrated in the picture below. The marker of channel 2 is used to gate the counter card, and channel output will simulate pulses like they are coming from an APD.
2.1 - Calibrate trigger level of the counter¶
Let's set the important parameters in the timetagger following the example of the tutorial Confocal Florescence Microscope
# for the counter input
timetagger.setTriggerLevel(counter_input, 0.25)
timetagger.setInputDelay(counter_input, 0)
# for the gate input
timetagger.setTriggerLevel(gate_start_input, 0.5)
timetagger.setInputDelay(gate_start_input, 0)
These parameters, however, are just initial guesses. To check the proper level of the trigger, one can repeat a simple calibration experiment where a fixed amount of pulses is sent to the counter, and sweeping the trigger level of the timetagger to see when the correct number of pulses is shown. To implement this sweep, one can use neartime callback functions.
# function to set timetagger at a specific level
def settrigger(
session: Session,
value,
):
timetagger.setTriggerLevel(counter_input, value)
# register it to session
session.register_neartime_callback(settrigger, "timetagger_sweep")
Next, we write a function for a simple experiment sending a specific number of clicks to our counter input while sweeping the trigger level simultaneously
def simulate_pulses(
pulse_count: int,
timetagger_trigger_sweep,
click_up_length=13e-9,
click_down_length=10e-9,
click_amplitude=1.0,
):
exp = Experiment(
uid="testtrigger",
signals=[ExperimentSignal("apd", map_to=apd_lsg)],
)
# define what a click is
click = pulse_library.const(
"click", length=click_up_length, amplitude=click_amplitude
)
# call trigger level settings inside the sweep
with exp.sweep(parameter=timetagger_trigger_sweep):
exp.call("timetagger_sweep", value=timetagger_trigger_sweep)
with exp.acquire_loop_rt(
uid="counts",
count=1,
):
# send fixed number of pulses
with exp.section(
uid="readout",
trigger={"apd": {"state": True}},
):
for _ in range(pulse_count):
exp.delay(signal="apd", time=click_down_length)
exp.play(signal="apd", pulse=click)
return exp
Let's create and compile an experiment using a sweep parameter to check the trigger levels
# define a sweep with trigger levels
trigger_levels = LinearSweepParameter(
uid="tlevel", start=0.1, stop=1, count=30, axis_name="trigger level [V]"
)
# create an experiment to perform the calibration
timetagger_calibration_experiment = simulate_pulses(100, trigger_levels)
# compile experiment
cexp = session.compile(timetagger_calibration_experiment)
We can now call the timetagger class to prepare for the readout. We want to set the timetagger to count the number of received pulses inside a window defined by the gate. We use the measurement class CountBetweenMarkers to achieve this. Since our number of averages is set to 1, the number of expected gate is exactly equal to the dimension of our sweep, so we set the class accordingly:
# prepare counter based on the dimension of the sweep
counter = CountBetweenMarkers(
tagger=timetagger,
click_channel=counter_input,
begin_channel=gate_start_input,
end_channel=gate_end_input,
n_values=trigger_levels.count,
)
result = session.run(cexp)
Let's plot the result: we expect that for low trigger level nothing will be visible, and once the level surpass the pulse height, we should see exactly the number of pulses sent to the tagger, in this case 100.
# take axis from experiment, and results from the timetagger
x = trigger_levels.values
# take Y axis from timetagger
y = counter.getData()
plt.plot(x, y, "o-")
plt.title("calibration results of time tagger")
plt.xlabel(trigger_levels.axis_name)
plt.ylabel("detected counts")
From the results above, we decide to set the trigger level to 0.2.
# set correct trigger level
timetagger.setTriggerLevel(counter_input, 0.2)
2.2 - Calibrate delay of the counter¶
Similarly as before, we can use the same trick to calibrate the delay to the input of the TimeTagger. Again here we will simulate the input with an SG channel, but we stress that for a proper calibration the users should always use the APD of the setup by performing a simple measurement of the NV center. Again, let's define a callback function to sweep over the delay input
# function to set timetagger at a specific level
def setinputdelay(
session: Session,
value,
):
timetagger.setInputDelay(counter_input, value)
We can now use the same experiment as before, we just need to switch the previous callback-function with the new one we just wrote
# register it to session
session.register_neartime_callback(setinputdelay, "timetagger_sweep")
With the two functions switched we can run the experiment again as done previously.
# define a sweep with trigger levels
delay_levels = LinearSweepParameter(
uid="dlevel", start=0, stop=1000, count=100, axis_name="Delay level [ps]"
)
# create an experiment to perform the calibration
timetagger_calibration_experiment = simulate_pulses(100, delay_levels)
# compile experiment
cexp = session.compile(timetagger_calibration_experiment)
# prepare counter based on the dimension of the sweep
counter = CountBetweenMarkers(
tagger=timetagger,
click_channel=counter_input,
begin_channel=gate_start_input,
end_channel=gate_end_input,
n_values=delay_levels.count,
)
result = session.run(cexp)
Finally, we plot again the result as before
# take axis from experiment, and results from the timetagger
x = delay_levels.values
# take Y axis from timetagger
y = counter.getData()
plt.plot(x, y, "o-")
plt.title("calibration results of time tagger")
plt.xlabel(delay_levels.axis_name)
plt.ylabel("detected counts")
2.3 - Sweep number of pulses¶
Now that the level has been calibrated, let's perform another basic check. We will create an experiment using the counter and sending a variable number of pulses to the timetagger. We will then verify after running it that the tagger indeed received the same number of pulses we provided. We follow the Tutorial in order to efficiently sweep pulses in our experiments.
def sweepclick(
pulse_count: int | SweepParameter | LinearSweepParameter,
click_up_length=13e-9,
click_down_length=10e-9,
click_amplitude=1.0,
repetition_time=100e-6,
):
exp = Experiment(
uid="sweepclicks",
signals=[ExperimentSignal("apd", map_to=apd_lsg)],
)
# define what a click is
click = pulse_library.const(
"click", length=click_up_length, amplitude=click_amplitude
)
# compute maximum length of a pulse
if isinstance(pulse_count, (LinearSweepParameter, SweepParameter)):
max_pulses = pulse_count.values.max()
else:
max_pulses = pulse_count
# compute maximum length, we will fix the readout section to this to make it constant and match the timing between sweeps
max_length = max_pulses * (click_up_length + click_down_length)
# describe experiment
with exp.acquire_loop_rt(
uid="counts",
count=1,
repetition_mode=RepetitionMode.CONSTANT,
repetition_time=repetition_time,
):
with exp.sweep(parameter=pulse_count):
with exp.section(
uid="readout",
length=max_length,
trigger={"apd": {"state": True}},
):
# match to different cases to control number of pulses per section
with exp.match(sweep_parameter=pulse_count):
for number_of_pulses in pulse_count.values:
with exp.case(number_of_pulses):
for _ in range(int(number_of_pulses)):
exp.delay(signal="apd", time=click_down_length)
exp.play(signal="apd", pulse=click)
return exp
We now define an array containing the number of clicks sent to the counter card in each readout section. We use this vector to define the LabOne Q experiment
pulse_count = LinearSweepParameter(
start=1, stop=100, count=100, axis_name="number of pulses"
)
exp = sweepclick(pulse_count)
We now compile the experiment in a format ready to be sent to the device
cexp = session.compile(exp)
We now run the experiment again by preparing the data class appropriately
counter = CountBetweenMarkers(
tagger=timetagger,
click_channel=counter_input,
begin_channel=gate_start_input,
end_channel=gate_end_input,
n_values=pulse_count.values.size,
)
result = session.run(cexp)
Let's visualize the result, both what the timetagger has measured, and the difference from the pulses sent (which if the settings are right, should be all at zeros)
x = pulse_count.values
y = counter.getData()
y1 = y - x
plt.plot(x, y, "o-", label="clicks detected")
plt.plot(x, y1, "o-", label="difference from pulse sent")
plt.legend()
plt.title("result of test")
plt.xlabel("Number of pulses sent")
2.4 Check minimum distance between clicks resolved by the timetagger¶
One more experiment we can attempt is checking if small distance between clicks influence in any way our final results for the timetagger. To do this, we create an experiment that sends a large amount pulses to the timetagger, and we then sweep the distance between these pulses
def sweepclickdistance(
pulse_count: int,
delay_sweep,
click_up_length=13e-9,
click_amplitude=1.0,
):
exp = Experiment(
uid="click_delay",
signals=[ExperimentSignal("apd", map_to=apd_lsg)],
)
# define what a click is
click = pulse_library.const(
"click", length=click_up_length, amplitude=click_amplitude
)
# define maximum section between exp
repetition_time = 1e-6 + (click.length + delay_sweep.values.max()) * pulse_count
with exp.acquire_loop_rt(
uid="counts",
count=1,
repetition_mode=RepetitionMode.CONSTANT,
repetition_time=repetition_time,
):
with exp.sweep(parameter=delay_sweep) as delay:
# send fixed number of pulses
with exp.section(
uid="readout",
trigger={"apd": {"state": True}},
):
for _ in range(pulse_count):
exp.delay(signal="apd", time=delay)
exp.play(signal="apd", pulse=click)
return exp
We now test distances from 5 ns in steps of 500 ps, which corresponds to the sampling rate of our SHFSG. We also test the special case of zero distance between pulses. In this corner case, our AWG will interpolate samples of consecutive pulses, effectively generating a single larger pulse. In this special case, we expect only one pulse to be recorded, in all other cases, we should see the timetagger receive all pulses generated.
delay_sweep = LinearSweepParameter(
start=5e-9, stop=0e-9, count=11, axis_name="distance between pulses [s]"
)
npulses_for_test = 3000
exp = sweepclickdistance(npulses_for_test, delay_sweep)
cexp = session.compile(exp)
As always, we call an instance of the counter and run the experiment.
counter = CountBetweenMarkers(
tagger=timetagger,
click_channel=counter_input,
begin_channel=gate_start_input,
end_channel=gate_end_input,
n_values=delay_sweep.count,
)
result = session.run(cexp)
We now plot the result by checking the number of pulses detected per sweep step and subtracting the number of pulse generated for the AWG. It can be seen that outside of the special step of zero distance, all other steps correctly detected all pulses.
x = delay_sweep.values / 1e-9 # in ns
y = counter.getData()
y1 = y - npulses_for_test
print(y1)
plt.plot(x, y1, "o-", label="difference from pulse sent")
plt.legend()
plt.title("Delay sweep between clicks")
plt.xlabel("Distance between clicks [ns]")
2.5 Check how much time is needed between shots¶
One last relevant value to check, is how long we can wait between shots to give enough time for the counter card to recover. This will give us an idea of how long we should wait to avoid overloading the device
def sweepresetdelay(
pulse_count: int,
delay_sweep,
click_up_length=13e-9,
click_down_length=10e-9,
click_amplitude=1.0,
):
exp = Experiment(
uid="click_delay",
signals=[ExperimentSignal("apd", map_to=apd_lsg)],
)
# define what a click is
click = pulse_library.const(
"click", length=click_up_length, amplitude=click_amplitude
)
with exp.acquire_loop_rt(
uid="counts",
count=1,
):
with exp.sweep(parameter=delay_sweep) as delay:
# send fixed number of pulses
with exp.section(
uid="readout",
trigger={"apd": {"state": True}},
):
for _ in range(pulse_count):
exp.delay(signal="apd", time=click_down_length)
exp.play(signal="apd", pulse=click)
# reset delay section with swept length
with exp.section(uid="reset_delay"):
exp.delay(signal="apd", time=delay)
return exp
let's run this experiment again
reset_delay_sweep = LinearSweepParameter(
start=90e-6, stop=52e-6, count=50, axis_name="Reset delay [s]"
)
npulses_for_test = 10
exp = sweepresetdelay(npulses_for_test, reset_delay_sweep)
cexp = session.compile(exp)
# prepare counter based on the dimension of the sweep
counter = CountBetweenMarkers(
tagger=timetagger,
click_channel=counter_input,
begin_channel=gate_start_input,
end_channel=gate_end_input,
n_values=reset_delay_sweep.count,
)
result = session.run(cexp)
x = reset_delay_sweep.values / 1e-9 # in ns
y = counter.getData()
y1 = y - npulses_for_test
plt.plot(x, y1, "o-", label="difference from pulse sent")
plt.legend()
plt.title("Reset sweep between clicks")
plt.xlabel("Distance between shots [ns]")
3 Write a function to run a general experiment using the counter card¶
Now that we checked that the tagger is running as intended and is correctly set, what we miss is integrating his functionality in our LabOne Q object. Calling neartime callback functions is a possibility, but it would slow down the experiment byu exiting the neartime loop. Instead, we set the timetagger in advance to the number of gate that it should expect, and then we perform offline the operation needed to transport the result of the timetagger to the Result object.
def run_with_counter(
session: Session,
experiment: Experiment,
handle="timetagger",
):
# gete the acquire_loop_rt of an experiment
rt_loop = experiment.get_rt_acquire_loop()
# averages times sweep dimensions
n_average = rt_loop.count
n_nt_sweep = tuple(
s.parameters[0].values.size
for s in experiment.all_sections()
if isinstance(s, (Sweep)) and s.execution_type == ExecutionType.NEAR_TIME
)
n_rt_sweep = tuple(
s.parameters[0].values.size
for s in experiment.all_sections()
if isinstance(s, (Sweep)) and s.execution_type == ExecutionType.REAL_TIME
)
total_sweep_steps = max(1, np.prod(n_nt_sweep)) * max(1, np.prod(n_rt_sweep))
# set total number of acquire instruction expected
size_of_result_object = n_average * total_sweep_steps
# before the experiment, activate the counter in the way desired
counter_between_markers = CountBetweenMarkers(
timetagger,
click_channel=counter_input,
begin_channel=gate_start_input,
end_channel=gate_end_input,
n_values=size_of_result_object,
)
# run the experiment, assuming gate are properly set, this will create the correct number of results
my_result = session.run(experiment)
# now result is inside the counter, reshape to sweep size
data = counter_between_markers.getData()
# reshape and averaging according to AveragingMode of rt loop
if rt_loop.averaging_mode == AveragingMode.SINGLE_SHOT:
processed_data = data
elif rt_loop.averaging_mode == AveragingMode.CYCLIC:
processed_data = np.mean(
data.reshape(*n_nt_sweep, n_average, *n_rt_sweep), axis=len(n_nt_sweep)
)
elif rt_loop.averaging_mode == AveragingMode.SEQUENTIAL:
processed_data = np.mean(
data.reshape(*n_nt_sweep, *n_rt_sweep, n_average),
axis=len(n_nt_sweep) + len(n_rt_sweep),
)
my_result.acquired_results = {
handle: AcquiredResult(
data=processed_data,
axis=[
s.parameters[0].values
if isinstance(s.parameters, list)
else s.parameters.uid
for s in experiment.all_sections()
if isinstance(s, (Sweep))
],
axis_name=[
s.parameters[0].uid
if isinstance(s.parameters, list)
else s.parameters.uid
for s in experiment.all_sections()
if isinstance(s, (Sweep))
],
handle=handle,
)
}
return my_result, counter_between_markers
4 Run general experiment for Color Centers using a Timetagger¶
Now that we have a general function to use the TimeTagger, we can run any experiment that we wish using this method. As long as a gate is present in the sequence, also previously defined experiment can be used for the purpose, for example the one illustrated in the example Color Centers - Basic Experiments. In this chapter, we will use a setup like the one illustrated for the picture below. We will use just a green laser for simplicity, i.e. just channel 2 of the SHFSG. Adding more AOM is trivial as illustrated in the next section.
4.0 Define parameters for the notebook¶
Let's define sections following the same convention of our previous example. Like this, the experiments can be written more compactly, leaving to the user just the task of writing the drive section. We will use the line drive for both sending RF signal and opening the gate for the counter card, while a second line, AOM, will be used to send a trigger to the laser. For this implementation, we switch to markers to achieve a sample precise trigger. Check Triggers and Markers to see the difference between the two in more details.
# Parameters
Trigger_Pulse_length = 250e-9
AOM_pulse_length = 3e-6 + Trigger_Pulse_length
repetition_time = 100e-6
# Signals
## function to automatically generate ExperimentSignal needed for single NV centers
def generate_nv_signals():
signals = [
ExperimentSignal("drive", map_to=drive_lsg),
ExperimentSignal("AOM", map_to=aom_lsg),
]
return signals
# Pulses
pi_pulse = pulse_library.const(
uid="pi_pulse", length=500e-9, amplitude=0.9, can_compress=True
)
pi_half_pulse = pulse_library.const(
uid="pi_2_pulse", length=250e-9, amplitude=0.9, can_compress=True
)
# Sections
## AOM, activate laser
AOM = Section(uid="AOM")
AOM.play(
signal="AOM",
pulse=None,
marker={"marker1": {"start": 0, "length": AOM_pulse_length}},
)
AOM.reserve(signal="drive")
## Readout, activate laser and the timetagger
Readout = Section(
uid="readout",
)
Readout.play(
signal="AOM",
pulse=None,
marker={"marker1": {"start": 0, "length": Trigger_Pulse_length}},
)
Readout.play(
signal="drive",
pulse=None,
marker={"marker1": {"start": 0, "length": Trigger_Pulse_length}},
)
4.1 ODMR¶
odmr_exp = Experiment(uid="odmr", signals=generate_nv_signals())
freq_sweep = LinearSweepParameter(
start=-300e6, stop=300e6, count=50, axis_name="Frequency [Hz]"
)
with odmr_exp.acquire_loop_rt(
count=1000, repetition_mode=RepetitionMode.CONSTANT, repetition_time=repetition_time
):
with odmr_exp.sweep(parameter=freq_sweep):
# shine laser
odmr_exp.add(AOM)
# qubit manipulation
with odmr_exp.section(uid="manipulation"):
odmr_exp.play(signal="drive", pulse=pi_pulse)
# readout
odmr_exp.add(Readout)
cal = Calibration()
cal["drive"] = SignalCalibration(oscillator=Oscillator(frequency=freq_sweep))
odmr_exp.set_calibration(cal)
Now let's run it with the TimeTagger! It is as easy as calling the function of the defined experiment
result_odmr, counter_odmr = run_with_counter(session, odmr_exp)
4.2 Ramsey¶
ramsey_exp = Experiment(uid="ramsey", signals=generate_nv_signals())
ramsey_sweep = LinearSweepParameter(start=0, stop=5e-6, count=11, axis_name="Delay [s]")
with ramsey_exp.acquire_loop_rt(
count=1000, repetition_mode=RepetitionMode.CONSTANT, repetition_time=repetition_time
):
with ramsey_exp.sweep(parameter=ramsey_sweep) as delay:
# shine laser
ramsey_exp.add(AOM)
# qubit manipulation
with ramsey_exp.section(uid="manipulation"):
ramsey_exp.play(signal="drive", pulse=pi_half_pulse)
ramsey_exp.delay(signal="drive", time=delay)
ramsey_exp.play(signal="drive", pulse=pi_half_pulse)
# readout
ramsey_exp.add(Readout)
ramsey_result, ramsey_counter = run_with_counter(session, ramsey_exp)
4.3 Hahn Echo¶
hecho_exp = Experiment(uid="hahn echo", signals=generate_nv_signals())
hecho_sweep = LinearSweepParameter(start=0, stop=5e-6, count=11, axis_name="Delay [s]")
with hecho_exp.acquire_loop_rt(
count=1000, repetition_mode=RepetitionMode.CONSTANT, repetition_time=repetition_time
):
with hecho_exp.sweep(parameter=hecho_sweep) as delay:
# shine laser
hecho_exp.add(AOM)
# qubit manipulation
with hecho_exp.section(uid="manipulation"):
hecho_exp.play(signal="drive", pulse=pi_half_pulse)
hecho_exp.delay(signal="drive", time=delay)
hecho_exp.play(signal="drive", pulse=pi_pulse)
hecho_exp.delay(signal="drive", time=delay)
hecho_exp.play(signal="drive", pulse=pi_half_pulse)
# readout
hecho_exp.add(Readout)
hecho_result, hecho_counter = run_with_counter(session, hecho_exp)
4.4 Dynamical decoupling¶
Using our decorator from before, we can also compactly implement a Dynamical decoupling sequence
dd_exp = Experiment(uid="dynamical_decoupling", signals=generate_nv_signals())
dd_sweep = LinearSweepParameter(
start=1, stop=20, count=20, axis_name="number of pulses"
)
with dd_exp.acquire_loop_rt(
count=1000, repetition_mode=RepetitionMode.CONSTANT, repetition_time=repetition_time
):
with dd_exp.sweep(parameter=dd_sweep) as npulses:
# Add sequence
# shine laser
dd_exp.add(AOM)
# qubit manipulation
with dd_exp.section(uid="manipulation"):
# sweep pulses
with dd_exp.match(sweep_parameter=npulses):
for number_of_pulses in npulses.values:
with dd_exp.case(number_of_pulses):
# first pi_half pulse
dd_exp.play(signal="drive", pulse=pi_half_pulse)
# N times pi pulses
for _ in range(int(number_of_pulses)):
dd_exp.delay(signal="drive", time=100e-9)
dd_exp.play(signal="drive", pulse=pi_pulse)
# last pi_half pulse
dd_exp.play(signal="drive", pulse=pi_half_pulse)
# readout
dd_exp.add(Readout)
dd_result, dd_counter = run_with_counter(session, dd_exp)
4.5 Rabi¶
rabi_exp = Experiment(uid="rabi", signals=generate_nv_signals())
rabi_sweep = LinearSweepParameter(start=0, stop=3e-6, count=21, axis_name="Length [s]")
with rabi_exp.acquire_loop_rt(
count=1000, repetition_mode=RepetitionMode.CONSTANT, repetition_time=repetition_time
):
with rabi_exp.sweep(parameter=rabi_sweep) as length:
# shine laser
rabi_exp.add(AOM)
# qubit manipulation
with rabi_exp.section(uid="manipulation"):
rabi_exp.play(signal="drive", pulse=pi_pulse, length=length)
# readout
rabi_exp.add(Readout)
rabi_result, rabi_counter = run_with_counter(session, rabi_exp)
4.6 T1 Relaxometry¶
t1_exp = Experiment(uid="T1 relaxometry", signals=generate_nv_signals())
t1_sweep = LinearSweepParameter(start=0, stop=10e-3, count=101, axis_name="Delay [s]")
with t1_exp.acquire_loop_rt(
count=1000, repetition_mode=RepetitionMode.CONSTANT, repetition_time=11e-3
):
with t1_exp.sweep(parameter=t1_sweep) as delay:
# shine laser
t1_exp.add(AOM)
# qubit manipulation
with t1_exp.section(uid="manipulation"):
t1_exp.play(signal="drive", pulse=pi_pulse)
t1_exp.delay(signal="drive", time=delay)
# readout
rabi_exp.add(Readout)
t1_result, t1_counter = run_with_counter(session, t1_exp)