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.
# 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
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 )
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)
session = Session(device_setup=device_setup) session.connect(do_emulation=True)
compiled_exp = session.compile(exp_ramsey)
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.
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)
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)