Declarative DSL Style¶
LabOne Q's domain-specific language (DSL) is the immediate user interface to define experiments, calibrations and everything else LabOne Q specific.
Particularly when you define experiments, you can choose between the imperative and the declarative DSL style. Each one might have some distinct advantages useful for specific situations.
In this notebook, you will learn how to:
- Use the declarative DSL style to build a Ramsey experiment
- How to manipulate sections to change the experiment's behavior without having to redefine the entire experiment.
In the following, it might help you to think of your experiment as a tree: Often, its trunk is a real-time acquisition loop, which governs the averaging. It's branches are sections, which contain either more sections or pulses.
The Imperative Style¶
Most of our example notebooks use the imperative DSL style to define experiments. The imperative style makes extensive use of Python's with
statements, and the structure of the code represents the structure of the experiment.
The Declarative Style¶
In the declarative DSL style, you define the constituent elements (sweeps, sections) of your experiments, and then "declare" how they interact with each other.
Imports¶
# Import required packages
from laboneq.simple import *
from laboneq.contrib.example_helpers.descriptors.shfsg_shfqa_pqsc import (
descriptor_shfsg_shfqa_pqsc,
)
import numpy as np
Device Setup and Calibration¶
We'll load a descriptor file to define our device setup and logical signal lines.
In contrast to actual experiments, we do not apply a setup calibration to keep the notebook short. Please refer to the Calibration Reference for details.
# Define and Load our Device Setup
device_setup = DeviceSetup.from_descriptor(
descriptor_shfsg_shfqa_pqsc,
server_host="my_ip_address", # ip address of the LabOne dataserver used to communicate with the instruments
server_port="8004", # port number of the dataserver - default is 8004
setup_name="my_QCCS_setup", # setup name
)
Ramsey Experiment¶
We start by defining the pulses and the parameter sweep
# pulse definitions
drive_pulse = pulse_library.gaussian(uid="gaussian_drive", length=700e-9, amplitude=1)
readout_pulse = pulse_library.const(uid="Readout", length=300e-9, amplitude=0.8)
# averages
n_average = 2
# sweep parameters
n_steps = 7
start_delay = 0
stop_delay = 13e-6
time_sweep = LinearSweepParameter(
uid="time_sweep_param", start=start_delay, stop=stop_delay, count=n_steps
)
Then, we start to define the experiment.
The experiment comprises a real-time acquisition loop, which governs the averaging; and a parameter sweep, which governs the delay between the pulses.
We'll use three sections to define our experimental sequence: In the first section, we will excite the qubit, in the second section, we'll measure it, and the third section introduces some delay for the qubit thermalization.
exp_ramsey = Experiment(uid="Ramsey_Experiment")
exp_ramsey.add_signal(
"drive",
connect_to=device_setup.logical_signal_groups["q0"].logical_signals["drive_line"],
)
exp_ramsey.add_signal(
"measure",
connect_to=device_setup.logical_signal_groups["q0"].logical_signals["measure_line"],
)
exp_ramsey.add_signal(
"acquire",
connect_to=device_setup.logical_signal_groups["q0"].logical_signals["acquire_line"],
)
acquire_loop = AcquireLoopRt(
uid="RT_Shots",
count=n_average,
averaging_mode=AveragingMode.CYCLIC,
repetition_mode=RepetitionMode.AUTO,
)
sweep = Sweep(
uid="Ramsey_Sweep", parameters=[time_sweep], alignment=SectionAlignment.RIGHT
)
# Qubit Excitation
excitation_section = Section(uid="qubit_excitation")
excitation_section.play(signal="drive", pulse=drive_pulse)
excitation_section.delay(signal="drive", time=time_sweep)
excitation_section.play(signal="drive", pulse=drive_pulse)
# Qubit Readout
readout_section = Section(uid="readout")
readout_section.play_after = excitation_section
readout_section.play(signal="measure", pulse=readout_pulse)
readout_section.acquire(
signal="acquire",
handle="ramsey",
kernel=readout_pulse,
)
# Qubit Thermalization
delay_section = Section(uid="delay", length=2e-6)
delay_section.play_after = readout_section
After having defined the constituent elements, we can put them together:
We add the acquisition loop to the experiment, the sweep to the acquisition loop, and the sections to the sweep.
exp_ramsey.add(acquire_loop)
acquire_loop.add(sweep)
sweep.add(excitation_section)
sweep.add(readout_section)
sweep.add(delay_section)
Inspect Experiment Tree¶
You can use the print
command to print the experiment tree as you have just defined it:
print(exp_ramsey)
Experiment Compilation¶
Before you can compile (or execute) the experiment, you need to open a session that connects to the instruments (or, here, emulates that connection).
session = Session(device_setup=device_setup)
session.connect(do_emulation=True)
compiled_exp = session.compile(exp_ramsey)
Inspect with Pulse Sheet Viewer¶
Once the experiment is compiled, we can view the pulses in a Pulse Sheet Viewer, a HTML file which shows the sections, pulses, and their relative timings.
show_pulse_sheet("Ramsey_Declarative", compiled_exp)
Changing Elements After the Experiment Definition¶
When you use the declarative style of the DSL, the elements of your experiment can be changed individually. Assume that you require a longer thermalization time:
delay_section.length = 10e-6
compiled_exp = session.compile(exp_ramsey)
show_pulse_sheet("Slower_Ramsey_Declarative", compiled_exp)
Reusing Sections¶
The declarative DSL style is particularly handy when you want to re-use sections. You could, for example, define a personal gate and apply it twice.
We create an experiment from scratch, define a gate section, and use it several times across the experiment.
gate_section = Section()
gate_section.play(signal="drive", pulse=drive_pulse, amplitude=0.5)
gate_section.play(signal="drive", pulse=drive_pulse, amplitude=0.5, phase=np.pi / 2)
readout_section = Section(uid="readout")
readout_section.play(signal="measure", pulse=readout_pulse)
readout_section.acquire(
signal="acquire",
handle="ramsey",
kernel=readout_pulse,
)
readout_section.reserve(signal="drive")
exp_gate = Experiment(uid="personal_experiment")
exp_gate.add_signal(
"drive",
connect_to=device_setup.logical_signal_groups["q0"].logical_signals["drive_line"],
)
exp_gate.add_signal(
"measure",
connect_to=device_setup.logical_signal_groups["q0"].logical_signals["measure_line"],
)
exp_gate.add_signal(
"acquire",
connect_to=device_setup.logical_signal_groups["q0"].logical_signals["acquire_line"],
)
We still want the readout section to be played after the gate section. However, using the play_after
command is not possible anymore, because we cannot specify whether it should be played after the first or the second gate section. Instead, we reserve the drive line in the readout section, to make sure that the sections do not overlap in time:
rt_loop = AcquireLoopRt(count=2**4)
parent_section = Section(uid="parent_section")
parent_section.add(gate_section)
parent_section.add(gate_section)
parent_section.add(readout_section)
rt_loop.add(parent_section)
exp_gate.add(rt_loop)
compiled_gate_exp = session.compile(exp_gate)
show_pulse_sheet("Reusing_sections", compiled_gate_exp)