Color Centers - Basic Experiments¶
The following notebook is intended to show some of the basic functionality of LabOne Q and provide links to get you started with color centers applications. We'll run through ODMR spectroscopy, length Rabi and Ramsey experiments.
The device ID in the descriptor and the IP address in this notebook should be updated to match your device and connection configuration.
0. General Imports¶
# LabOne Q:
from laboneq.simple import *
# for plotting of the simulation
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
# for saving results and pulse sheets
from pathlib import Path
1. Device Setup¶
We first need to define a calibration and our device setup.
1.1 Calibration¶
Read about applying instrument settings through calibration objects and their properties.
Here, we configure two lines of the SHFSG signal generator to play the pulses we need: The first line is used to drive the color center and is centered around 2.9 GHz. The second line will drive the AOM and uses the low-frequency output mode of the SHFSG channels.
def calibrate_devices(device_setup):
lo_shfsg_1_2 = Oscillator(
"lo_shfsg_1_2",
frequency=2.9e9,
)
lo_shfsg_3 = Oscillator(
"lo_shfsg_3",
frequency=0,
)
device_setup.logical_signal_groups["q0"].logical_signals[
"drive_line"
].calibration = SignalCalibration(
oscillator=Oscillator(
uid="drive_osc",
frequency=-3e8,
modulation_type=ModulationType.HARDWARE,
),
local_oscillator=lo_shfsg_1_2,
range=10,
)
device_setup.logical_signal_groups["q0"].logical_signals[
"drive_AOM_line"
].calibration = SignalCalibration(
oscillator=Oscillator(
uid="drive_AOM_osc",
frequency=2e8,
modulation_type=ModulationType.HARDWARE,
),
port_mode=PortMode.LF,
local_oscillator=lo_shfsg_3,
range=5,
)
1.2 Create device setup¶
Read about device setups and descriptors.
descriptor_shfsg_nv = """
instrument_list:
SHFSG:
- address: DEV12XXX
uid: device_shfsg
connections:
device_shfsg:
- iq_signal: q0/drive_line
ports: SGCHANNELS/0/OUTPUT
- iq_signal: q0/drive_AOM_line
ports: SGCHANNELS/2/OUTPUT
"""
def create_device_setup(
descriptor=descriptor_shfsg_nv,
serverhost="localhost",
):
device_setup = DeviceSetup.from_descriptor(
yaml_text=descriptor,
server_host=serverhost,
server_port="8004",
setup_name="SHFSG_Standalone",
)
calibrate_devices(device_setup)
return device_setup
1.3 Connect¶
device_setup = create_device_setup()
shfsg_address = device_setup.instruments[0].address
q0 = device_setup.logical_signal_groups["q0"].logical_signals
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: Set emulate = False
when running on real hardware!
emulate = True
my_session = Session(device_setup=device_setup)
my_session.connect(do_emulation=emulate)
2. Pulse definitions - To be used throughout¶
x90 = pulse_library.drag(uid="x90", length=100e-9, amplitude=0.4, sigma=0.3, beta=0.4)
x180 = pulse_library.drag(uid="x180", length=100e-9, amplitude=0.8, sigma=0.3, beta=0.4)
# trigger
Trigger_Pulse_length = 250e-9
AOM_Pulse_length = 3e-6 + Trigger_Pulse_length
3. ODMR Spectroscopy¶
3.1 More pulse parameters¶
We define a frequency sweep to be used in the spectroscopy experiment, in which we'll use a rectangular excitation pulse of 500 ns length, which we also define here.
# set up sweep parameters
freq_sweep_q0 = LinearSweepParameter(
uid="freq_qubit", start=-300e6, stop=300e6, count=50
)
# how many averages per point: 2^n_average
n_average = 10
# square pulse to excite the qubit
pulse_length = 500e-9
square_pulse = pulse_library.const(uid="const_iq", length=pulse_length, amplitude=0.89)
In LabOne Q, experiments contain signals, sections, pulses, and pulse commands. Understanding their use will let you get the most out of your experiment. To learn the basics, you can follow our section tutorial.
We can use triggers or marker to output a TTL signal that can be used to activate a third-party instrument. In this examples, a trigger is used, by creating a specific section where the trigger on the ExperimentalSignal "drive" is switched on.
# function that returns a qubit spectroscopy experiment- accepts frequency sweep range as parameter
def qubit_spectroscopy(freq_sweep):
# Create qubit spectroscopy Experiment - uses qubit drive, readout drive and data acquisition lines
exp_nv = Experiment(
uid="Spectroscopy",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("AOM"),
],
)
## define experimental pulse sequence
with exp_nv.acquire_loop_rt(
uid="qfreq_shots",
count=pow(2, n_average),
):
# qubit drive
with exp_nv.sweep(uid="qfreq_sweep", parameter=freq_sweep):
with exp_nv.section(uid="parent_section", alignment=SectionAlignment.LEFT):
# shine the laser. Here, the AOM line is used only to send a trigger to the laser
with exp_nv.section(
uid="excitation",
length=AOM_Pulse_length,
trigger={"AOM": {"state": 1}},
) as AOM:
exp_nv.reserve(signal="AOM")
# excite the state using the drive line
with exp_nv.section(uid="manipulation", play_after="excitation"):
exp_nv.reserve(signal="AOM")
exp_nv.play(signal="drive", pulse=square_pulse)
# shine laser again
exp_nv.add(AOM)
# start DAQ trigger
with exp_nv.section(
uid="trigger",
length=Trigger_Pulse_length,
play_after="manipulation",
trigger={"drive": {"state": 1}},
):
exp_nv.reserve(signal="drive")
# delay next average
with exp_nv.section(uid="delay"):
exp_nv.reserve(signal="AOM")
exp_nv.delay(signal="drive", time=1e-6)
return exp_nv
3.3 Experiment Calibration and Signal Map¶
Here, we apply an experiment calibration and apply a mapping of the experimental signals to our logical lines (and thus to the physical hardware.)
# experiment signal calibration for qubit 0
exp_calibration_q0 = Calibration()
exp_calibration_q0["drive"] = SignalCalibration(
oscillator=Oscillator(
frequency=freq_sweep_q0,
modulation_type=ModulationType.HARDWARE,
),
)
# signal map for qubit 0
q0_map = {
"drive": q0["drive_line"],
"AOM": q0["drive_AOM_line"],
}
3.4 Apply Experiment Settings and Run¶
When running your experiments, you can create a convenient reference to the results object that is created in the following way: my_results = my_session.run()
.
The simulation parses the generated Sequencer C code and generates simulated output signals.
Note that it is often convenient to look at the pulse sheet viewer, simulator, and Sequencer C code before running on hardware.
Additionally, you can read about the what the LabOne Q compiler settings do here.
# define experiment with frequency sweep for qubit 0
exp_nv = qubit_spectroscopy(freq_sweep_q0)
# apply calibration and signal map for qubit 0
exp_nv.set_calibration(exp_calibration_q0)
exp_nv.set_signal_map(q0_map)
# compile and run the experiment on qubit 0
compiled_qspec = my_session.compile(exp_nv)
qspec_results = my_session.run(compiled_qspec)
3.5 Using the Output Simulator¶
The output simulator displays a true time-domain representation of the pulses played within the experiment.
plot_simulation(compiled_qspec, 0, length=50e-6, plot_width=25)
3.6 Pulse Sheet Viewer¶
The pulse sheet viewer displays the higher-level pulse sheet.
Path("Pulse_sheets").mkdir(parents=True, exist_ok=True)
show_pulse_sheet("Pulse_sheets/Spectroscopy", compiled_experiment=compiled_qspec)
4. Length Rabi¶
A sweep object is rather flexible, and can be used for many different purposes. For example, let's use it to sweep the length of a drive pulse to obtain a Rabi sequence.
4.1 Define Pulse Parameters¶
## define length sweep parameter
length_sweep_parameter = LinearSweepParameter(
uid="length_sweep", start=0, stop=3e-6, count=20
)
drive_pulse = pulse_library.const(
uid="rabi_drive_pulse",
length=100e-9,
amplitude=1,
can_compress=True, # <-- pulse can be compressed by the compiler!
)
n_average = 10
4.2 Experiment Definition and Sequence¶
An alternative to trigger are markers. Their functionality inside of an experiment is the same, but they are synchronized to the wave output of a signal line. Hence, they have an increased precision compared to triggers. Notice that because of this they don't live in the section, but they are rather connected to a specific play instruction.
## Create Experiment
exp_nv_rabi = Experiment(
"Rabi_length",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("AOM"),
],
)
# define experiment
with exp_nv_rabi.acquire_loop_rt(
uid="shots", count=pow(2, n_average), averaging_mode=AveragingMode.CYCLIC
):
with exp_nv_rabi.sweep(parameter=length_sweep_parameter):
with exp_nv_rabi.section(
uid="excitation", length=AOM_Pulse_length, trigger={"AOM": {"state": 1}}
) as AOM:
exp_nv_rabi.reserve(signal="AOM")
# sweep length of the pulse used for manipulation
with exp_nv_rabi.section(
uid="manipulation",
alignment=SectionAlignment.LEFT,
play_after="excitation",
):
exp_nv_rabi.reserve(signal="AOM")
exp_nv_rabi.play(
signal="drive",
pulse=drive_pulse,
length=length_sweep_parameter, # <--- sweep parameter overloads the length!
)
exp_nv_rabi.add(AOM)
# other way to manipulate the trigger: markers completely synchronized with a waveform!
with exp_nv_rabi.section(uid="trigger", play_after="manipulation"):
exp_nv_rabi.play(
signal="drive",
pulse=None,
amplitude=0.01,
marker={"marker1": {"start": 0, "length": Trigger_Pulse_length}},
) # <----- Markers are used here instead
with exp_nv_rabi.section(uid="delay"):
exp_nv_rabi.delay(signal="AOM", time=3e-6)
4.3 Set Map and Update Oscillator Freq¶
# define signal maps for different qubits
map_q0 = {
"drive": q0["drive_line"],
"AOM": q0["drive_AOM_line"],
}
4.4 Apply Settings and Run Experiment¶
# set experiment calibration and signal map
exp_nv_rabi.set_signal_map(map_q0)
compiled_rabi = my_session.compile(exp_nv_rabi)
rabi_results = my_session.run(compiled_rabi)
4.5 Plot with Output Simulator¶
plot_simulation(compiled_rabi, length=160e-6, plot_width=25)
4.6 Show in Pulse Sheet Viewer¶
show_pulse_sheet("Pulse_sheets/Rabi_length", compiled_rabi)
5. Ramsey Experiment¶
5.1 All-in-one experiment definitions and signal mapping¶
Let's make our experiment customizable by creating a function that allow us to specify the parameters later on. This time, our sweep parameter is the time that we wait between two pi/2 pulses.
def make_ramsey_experiment(
start=0e-9,
stop=5000e-9,
count=11,
average_exponent=12,
averaging_mode=AveragingMode.CYCLIC,
repetition_mode=RepetitionMode.AUTO,
lsg=q0,
):
# Create Experiment
exp = Experiment(
"Ramsey",
signals=[
ExperimentSignal("drive"),
ExperimentSignal("AOM"),
],
)
# Connect experiment signals to logical signals
exp.map_signal("drive", lsg["drive_line"])
exp.map_signal("AOM", lsg["drive_AOM_line"])
sweep_parameter = LinearSweepParameter(
uid="delay", start=start, stop=stop, count=count
)
with exp.acquire_loop_rt(
uid="shots",
count=pow(2, average_exponent),
averaging_mode=averaging_mode,
repetition_mode=repetition_mode,
repetition_time=0,
reset_oscillator_phase=False,
):
with exp.sweep(
uid="sweep",
parameter=sweep_parameter,
reset_oscillator_phase=False,
):
with exp.section(
uid="excitation", length=AOM_Pulse_length, trigger={"AOM": {"state": 1}}
) as AOM:
exp.reserve(signal="AOM")
with exp.section(
uid="manipulation",
length=stop + 2 * x90.length,
play_after="excitation",
alignment=SectionAlignment.RIGHT,
):
exp.reserve(signal="AOM")
## what to put here?
exp.play(signal="drive", pulse=x90)
exp.delay(signal="drive", time=sweep_parameter)
exp.play(signal="drive", pulse=x90)
exp.add(AOM)
with exp.section(
uid="DAQ trigger",
length=Trigger_Pulse_length,
trigger={"drive": {"state": 1}},
):
exp.reserve(signal="drive")
return exp
5.2 Apply settings and run¶
avg = 10
exp_nv_ramsey = make_ramsey_experiment(count=10, average_exponent=avg)
compiled_ramsey = my_session.compile(exp_nv_ramsey)
# Run without a specified experiment to use compiled experiment with the compiler settings:
ramsey_results = my_session.run(compiled_ramsey)
5.3 Plot in Output Simulator¶
plot_simulation(compiled_ramsey, 0e-7, 100e-6, plot_width=25)
5.4 Show Pulse Sheet¶
show_pulse_sheet("Pulse_sheets/Ramsey", compiled_ramsey)