See also
A Jupyter notebook version of this tutorial can be downloaded here.
Operations and qubits#
Gates, measurements and qubits#
In the previous tutorials, experiments were created on the quantum-device level. On this level, operations are defined in terms of explicit signals and locations on the chip, rather than the qubit and the intended operation. To work at a greater level of abstraction, qblox-scheduler allows creating operations on the
quantum-circuit level. Instead of signals, clocks, and ports, operations are defined by the effect they have on specific qubits. This representation of the schedules can be compiled to the quantum-device level to create the pulse schemes.
In this tutorial we show how to define operations on the quantum-circuit level, combine them into schedules, and show their circuit-level visualization. We go through the configuration file needed to compile the schedule to the quantum-device level and show how these configuration files can be created automatically and dynamically.
Many of the gates used in the circuit layer description are defined in qblox_scheduler.operations.gate_library such as qblox_scheduler.operations.gate_library.Reset, qblox_scheduler.operations.gate_library.X90 and qblox_scheduler.operations.gate_library.Measure. Operations are instantiated by providing them with the name of the qubit(s) on which they operate:
[1]:
from qblox_scheduler.operations import CZ, X90, Measure, Reset
q0, q1 = ("q0", "q1")
X90(q0)
Measure(q1)
CZ(q0, q1)
Reset(q0)
[1]:
{'name': 'Reset q0', 'gate_info': {'unitary': None, 'tex': '$|0\\rangle$', 'plot_func': 'qblox_scheduler.schedules._visualization.circuit_diagram.reset', 'device_elements': ['q0'], 'operation_type': 'reset', 'device_overrides': {}}, 'pulse_info': {}, 'acquisition_info': {}, 'logic_info': {}, 'statement_info': {}}
Within a single qblox_scheduler.schedules.schedule.Schedule, high-level circuit layer operations can be mixed with quantum-device level operations. This mixed representation is useful for experiments where some pulses cannot easily be represented as qubit gates. An example of this is given by the Chevron experiment given in Mixing pulse and circuit layer operations.
Circuit layer schedule example: Bell test#
We demonstrate the extra layer of abstraction that circuit-level operations offer by creating a qblox_scheduler.schedules.schedule.Schedule for measuring Bell violations.
As the first example, we want to create a schedule for performing the Bell experiment. The goal of the Bell experiment is to create a Bell state \(|\Phi ^+\rangle=\frac{1}{2}(|00\rangle+|11\rangle)\) which is a perfectly entangled state, followed by a measurement. By rotating the measurement basis, or equivalently one of the qubits, it is possible to observe violations of the CSHS inequality.
We create this experiment using the quantum-circuit level description. This allows defining the Bell schedule as:
[2]:
import numpy as np
from qblox_scheduler import Schedule
from qblox_scheduler.operations import CZ, X90, Measure, Reset, Rxy
sched = Schedule("Bell experiment")
for acq_idx, theta in enumerate(np.linspace(0, 360, 21)):
sched.add(Reset(q0, q1))
sched.add(X90(q0))
sched.add(X90(q1), ref_pt="start") # Start at the same time as the other X90
sched.add(CZ(q0, q1))
sched.add(Rxy(theta=theta, phi=0, qubit=q0))
sched.add(Measure(q0, acq_index=acq_idx), label=f"M q0 {theta:.2f} deg")
sched.add(
Measure(q1, acq_index=acq_idx),
label=f"M q1 {theta:.2f} deg",
ref_pt="start", # Start at the same time as the other measure
)
sched
[2]:
<qblox_scheduler.schedule.Schedule at 0x7f78143f4ad0>
Visualizing the quantum circuit#
We can directly visualize the created schedule on the quantum-circuit level with the qblox_scheduler.schedules.schedule.ScheduleBase.plot_circuit_diagram method. This visualization shows every operation on a line representing the different qubits.
[3]:
import matplotlib.pyplot as plt
_, ax = sched.plot_circuit_diagram()
# all gates are plotted, but it doesn't all fit in a matplotlib figure.
# Therefore we use :code:`set_xlim` to limit the number of gates shown.
ax.set_xlim(-0.5, 9.5)
plt.show()
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
findfont: Failed to find font weight 300, now using 400.
In previous tutorials, we visualized the schedules on the pulse level using qblox_scheduler.schedules.schedule.ScheduleBase.plot_pulse_diagram . Up until now, however, all gates have been defined on the quantum-circuit level without defining the corresponding pulse shapes. Therefore, trying to run qblox_scheduler.schedules.schedule.ScheduleBase.plot_pulse_diagram will raise an error which signifies no pulse_info is present in the schedule:
[4]:
sched.plot_pulse_diagram()
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[4], line 1
----> 1 sched.plot_pulse_diagram()
File /.venv/lib/python3.14/site-packages/qblox_scheduler/schedule.py:489, in Schedule.plot_pulse_diagram(self, port_list, sampling_rate, modulation, modulation_if, plot_backend, x_range, combine_waveforms_on_same_port, timeable_schedule_index, num_repetitions, **backend_kwargs)
487 raise IndexError(f"No timeable schedule at index {timeable_schedule_index}")
488 timeable_schedule = timeable_schedules[timeable_schedule_index]
--> 489 return timeable_schedule.plot_pulse_diagram(
490 port_list,
491 sampling_rate,
492 modulation,
493 modulation_if,
494 plot_backend,
495 x_range,
496 combine_waveforms_on_same_port,
497 num_repetitions,
498 **backend_kwargs,
499 )
File /.venv/lib/python3.14/site-packages/qblox_scheduler/schedules/schedule.py:450, in TimeableScheduleBase.plot_pulse_diagram(self, port_list, sampling_rate, modulation, modulation_if, plot_backend, x_range, combine_waveforms_on_same_port, num_repetitions, **backend_kwargs)
446 # NB imported here to avoid circular import
448 from qblox_scheduler.schedules._visualization.pulse_diagram import sample_schedule
--> 450 sampled_pulses_and_acqs = sample_schedule(
451 self,
452 sampling_rate=sampling_rate,
453 port_list=port_list,
454 modulation=modulation,
455 modulation_if=modulation_if,
456 x_range=x_range,
457 combine_waveforms_on_same_port=combine_waveforms_on_same_port,
458 num_repetitions=num_repetitions,
459 )
461 if plot_backend == "mpl":
462 # NB imported here to avoid circular import
464 from qblox_scheduler.schedules._visualization.pulse_diagram import (
465 pulse_diagram_matplotlib,
466 )
File /.venv/lib/python3.14/site-packages/qblox_scheduler/schedules/_visualization/pulse_diagram.py:649, in sample_schedule(schedule, port_list, modulation, modulation_if, sampling_rate, x_range, combine_waveforms_on_same_port, num_repetitions)
646 acq_infos: dict[str, list[ScheduledInfo]] = defaultdict(list)
647 marker_infos: dict[str, list[ScheduledInfo]] = defaultdict(list)
--> 649 _extract_schedule_infos(
650 schedule,
651 port_list,
652 0,
653 offset_infos,
654 pulse_infos,
655 acq_infos,
656 marker_infos,
657 num_repetitions,
658 )
660 x_min, x_max = x_range
662 sampled_pulses = get_sampled_pulses_from_voltage_offsets(
663 schedule=schedule,
664 offset_infos=offset_infos,
(...) 668 modulation_if=modulation_if,
669 )
File /.venv/lib/python3.14/site-packages/qblox_scheduler/schedules/_visualization/pulse_diagram.py:518, in _extract_schedule_infos(operation, port_list, time_offset, offset_infos, pulse_infos, acq_infos, marker_infos, num_repetitions)
516 for schedulable in operation.schedulables.values():
517 inner_operation = operation.operations[schedulable["operation_id"]]
--> 518 abs_time = schedulable["abs_time"]
519 _extract_schedule_infos(
520 inner_operation,
521 port_list,
(...) 527 num_repetitions,
528 )
529 elif isinstance(operation, ConditionalOperation):
File /usr/local/lib/python3.14/collections/__init__.py:1151, in UserDict.__getitem__(self, key)
1149 if hasattr(self.__class__, "__missing__"):
1150 return self.__class__.__missing__(self, key)
-> 1151 raise KeyError(key)
KeyError: 'abs_time'
And similarly for the timing_table:
[5]:
sched.timing_table
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[5], line 1
----> 1 sched.timing_table
File /.venv/lib/python3.14/site-packages/qblox_scheduler/schedule.py:597, in Schedule.timing_table(self)
595 if timeable_schedule is None:
596 raise ValueError("can not plot timing table for schedules with untimed operations")
--> 597 return timeable_schedule.timing_table
File /.venv/lib/python3.14/site-packages/qblox_scheduler/schedules/schedule.py:650, in TimeableScheduleBase.timing_table(self)
558 """
559 A styled pandas dataframe containing the absolute timing of pulses and acquisitions in a schedule.
560
(...) 647
648 """ # noqa: E501
649 timing_table_list = []
--> 650 self._generate_timing_table_list(self, 0, timing_table_list, None, 1)
651 timing_table = pd.concat(timing_table_list, ignore_index=True)
652 timing_table = timing_table.sort_values(by="abs_time")
File /.venv/lib/python3.14/site-packages/qblox_scheduler/schedules/schedule.py:506, in TimeableScheduleBase._generate_timing_table_list(cls, operation, time_offset, timing_table_list, operation_id, num_repetitions)
503 for schedulable in operation.schedulables.values():
504 if "abs_time" not in schedulable:
505 # when this exception is encountered
--> 506 raise ValueError(
507 "Absolute time has not been determined yet. Please compile your schedule."
508 )
509 cls._generate_timing_table_list(
510 operation.operations[schedulable["operation_id"]],
511 time_offset + schedulable["abs_time"],
(...) 514 num_repetitions,
515 )
516 elif isinstance(operation, LoopOperation):
ValueError: Absolute time has not been determined yet. Please compile your schedule.
Quantum devices and elements#
The device configuration contains all knowledge of the physical device under test (DUT). To generate these device configurations on the fly, qblox-scheduler provides the qblox_scheduler.device_under_test.quantum_device.QuantumDevice and qblox_scheduler.device_under_test.device_element.DeviceElement classes.
These classes contain the information necessary to generate the device configs and allow changing their parameters on-the-fly. The qblox_scheduler.device_under_test.quantum_device.QuantumDevice class represents the DUT containing different qblox_scheduler.device_under_test.device_element.DeviceElement s. Currently, qblox-scheduler contains the qblox_scheduler.device_under_test.transmon_element.BasicTransmonElement class to represent a fixed-frequency transmon qubit connected to a
feedline. We show their interaction below:
[6]:
from qblox_scheduler import BasicTransmonElement, QuantumDevice
# First create a device under test
dut = QuantumDevice("DUT")
# Then create a transmon element
qubit = BasicTransmonElement("qubit")
# Finally, add the transmon element to the QuantumDevice
dut.add_element(qubit)
dut, dut.elements
/tmp/ipykernel_20034/2795591975.py:1: FutureWarning: 'BasicTransmonElement' will be removed in 1.0 and made available in a separate repository, see release notes for more details.
from qblox_scheduler import BasicTransmonElement, QuantumDevice
[6]:
(QuantumDevice(name='DUT', elements={'qubit': BasicTransmonElement(name='qubit', element_type='BasicTransmonElement', reset=IdlingReset(name='reset', duration=0.0002), rxy=RxyDRAG(name='rxy', amp180=nan, beta=0.0, duration=2e-08, reference_magnitude=ReferenceMagnitude(name='reference_magnitude', dBm=nan, V=nan, A=nan)), measure=DispersiveMeasurement(name='measure', pulse_type='SquarePulse', pulse_amp=0.25, pulse_duration=3e-07, acq_label=0, acq_channel=None, acq_delay=0.0, integration_time=1e-06, reset_clock_phase=True, acq_weights_a=array([], dtype=float64), acq_weights_b=array([], dtype=float64), acq_weights_sampling_rate=1000000000.0, acq_weight_type='SSB', acq_rotation=0.0, acq_threshold=0.0, num_points=1, reference_magnitude=ReferenceMagnitude(name='reference_magnitude', dBm=nan, V=nan, A=nan)), pulse_compensation=PulseCompensationModule(name='pulse_compensation', max_compensation_amp=nan, time_grid=nan, sampling_rate=nan, min_duration=nan), ports=Ports(name='ports', microwave='qubit:mw', flux='qubit:fl', readout='qubit:res'), clock_freqs=ClocksFrequencies(name='clock_freqs', f01=nan, f12=nan, readout=nan))}, edges={}, instr_instrument_coordinator=None, cfg_sched_repetitions=1024, keep_original_schedule=True, hardware_config=None, scheduling_strategy=<SchedulingStrategy.ASAP: 'asap'>),
{'qubit': BasicTransmonElement(name='qubit', element_type='BasicTransmonElement', reset=IdlingReset(name='reset', duration=0.0002), rxy=RxyDRAG(name='rxy', amp180=nan, beta=0.0, duration=2e-08, reference_magnitude=ReferenceMagnitude(name='reference_magnitude', dBm=nan, V=nan, A=nan)), measure=DispersiveMeasurement(name='measure', pulse_type='SquarePulse', pulse_amp=0.25, pulse_duration=3e-07, acq_label=0, acq_channel=None, acq_delay=0.0, integration_time=1e-06, reset_clock_phase=True, acq_weights_a=array([], dtype=float64), acq_weights_b=array([], dtype=float64), acq_weights_sampling_rate=1000000000.0, acq_weight_type='SSB', acq_rotation=0.0, acq_threshold=0.0, num_points=1, reference_magnitude=ReferenceMagnitude(name='reference_magnitude', dBm=nan, V=nan, A=nan)), pulse_compensation=PulseCompensationModule(name='pulse_compensation', max_compensation_amp=nan, time_grid=nan, sampling_rate=nan, min_duration=nan), ports=Ports(name='ports', microwave='qubit:mw', flux='qubit:fl', readout='qubit:res'), clock_freqs=ClocksFrequencies(name='clock_freqs', f01=nan, f12=nan, readout=nan))})
The different transmon properties can be set through attributes of the qblox_scheduler.device_under_test.transmon_element.BasicTransmonElement class instance, e.g.:
[7]:
qubit.clock_freqs.f01 = 6e9
Mixing pulse and circuit layer operations#
We can mix the circuit layer representation with pulse-level operations, which can be useful for experiments involving pulses not easily represented by gates.
[8]:
from qblox_scheduler import ClockResource, Schedule
from qblox_scheduler.operations import X90, Measure, Reset, SquarePulse, X
sched = Schedule("Experiment with both pulse and circuit layer operations")
reset = sched.add(Reset("q0"))
sched.add(X("q0"), ref_op=reset, ref_pt="end") # Start at the end of the reset
# We specify a clock for tutorial purposes
square = sched.add(SquarePulse(amplitude=0.1, duration=1e-6, port="q0:mw", clock="q0.01"))
sched.add(Measure(q0, coords={"amplitude": 0.1}, acq_channel="S_21"), label="M q0")
# Specify the frequencies for the clocks; this can also be done via the DeviceElement (BasicTransmonElement) instead
sched.add_resources(
[
ClockResource("q0.01", 6.02e9),
ClockResource("q0.ro", 5.02e9),
]
)
/.venv/lib/python3.14/site-packages/qblox_scheduler/operations/gate_library.py:1181: FutureWarning: Using the `acq_channel` argument is deprecated. Use the `acq_label` argument instead.
warn_acq_channel()
[9]:
sched.plot_circuit_diagram()
[9]:
(<Figure size 1000x100 with 1 Axes>,
<Axes: title={'center': 'Experiment with both pulse and circuit layer operations schedule 1'}>)
This example shows that we add gates using the same interface as pulses. Gates are Operations, and as such support the same timing and reference operators as Pulses.
Schedule compilation with the HardwareAgent#
The HardwareAgent is responsible for instantiating everything that is necessary to run an experiment, as well as actually running the experiment. A hardware configuration is required, but a device configuration is not. The Compiling to Hardware section demonstrates how to set the hardware configuration.
[10]:
from qblox_scheduler import BasicTransmonElement, HardwareAgent, QuantumDevice
dut.close()
dut = QuantumDevice("DUT")
q0_dev = BasicTransmonElement("q0")
q1_dev = BasicTransmonElement("q1")
dut.add_element(q0_dev)
dut.add_element(q1_dev)
dut.get_element("q0").clock_freqs.f01 = 4e9
dut.get_element("q0").rxy.amp180 = 0.65
dut.get_element("q1").rxy.amp180 = 0.55
dut.get_element("q0").measure.pulse_amp = 0.28
dut.get_element("q1").measure.pulse_amp = 0.22
agent = HardwareAgent("./dependencies/configs/hw_cfg.json", dut)
compiled_sched = agent.compile(schedule=sched)
/tmp/ipykernel_20034/408239406.py:1: FutureWarning: 'BasicTransmonElement' will be removed in 1.0 and made available in a separate repository, see release notes for more details.
from qblox_scheduler import BasicTransmonElement, HardwareAgent, QuantumDevice
/tmp/ipykernel_20034/408239406.py:3: UserWarning: QuantumDevice is not an instrument, no need to close it!
dut.close()
/.venv/lib/python3.14/site-packages/qblox_scheduler/qblox/hardware_agent.py:452: UserWarning: Failed to connect the cluster to the hardware before compilation, several attributes such as `compiled_operations` will not be available! (cause: timed out)
warnings.warn(
/.venv/lib/python3.14/site-packages/qblox_scheduler/backends/circuit_to_device.py:451: RuntimeWarning: Clock 'q0.01' has conflicting frequency definitions: 6020000000.0 Hz in the schedule and 4000000000.0 Hz in the device config. The clock is set to '6020000000.0'. Ensure the schedule clock resource matches the device config clock frequency or set the clock frequency in the device config to np.NaN to omit this warning.
warnings.warn(
So, finally, we can show the timing table associated with the Chevron schedule and plot its pulse diagram:
[11]:
compiled_sched.timing_table.hide(slice(11, None), axis="index").hide(
"waveform_op_id", axis="columns"
)
[11]:
| port | clock | abs_time | duration | is_acquisition | operation | operation_hash | |
|---|---|---|---|---|---|---|---|
| 0 | None | q0.ro | 0.0 ns | 0.0 ns | False | ResetClockPhase(clock='q0.ro',t0=0.0) | -4286490525643995838 |
| 1 | None | q0.01 | 0.0 ns | 0.0 ns | False | ResetClockPhase(clock='q0.01',t0=0.0) | 1461023042494702578 |
| 2 | None | cl0.baseband | 0.0 ns | 4.0 ns | False | IdlePulse(duration=4e-09) | 6599992397395000799 |
| 3 | None | cl0.baseband | 4.0 ns | 200,000.0 ns | False | Reset('q0') | 6990468815810843418 |
| 4 | q0:mw | q0.01 | 200,004.0 ns | 20.0 ns | False | X(qubit='q0') | -8670200507130007494 |
| 5 | q0:mw | q0.01 | 200,024.0 ns | 1,000.0 ns | False | SquarePulse(amplitude=0.1,duration=1e-06,port='q0:mw',clock='q0.01',reference_magnitude=None,t0=0.0) | -6160929646317021755 |
| 6 | None | q0.ro | 201,024.0 ns | 0.0 ns | False | ResetClockPhase(clock='q0.ro',t0=0.0) | -4286490525643995838 |
| 7 | q0:res | q0.ro | 201,024.0 ns | 300.0 ns | False | SquarePulse(amplitude=0.28,duration=3e-07,port='q0:res',clock='q0.ro',reference_magnitude=None,t0=0.0) | 8098039196289922997 |
| 8 | q0:res | q0.ro | 201,024.0 ns | 1,000.0 ns | True | IQIntegration(port='q0:res', clock='q0.ro', duration=1e-06, acq_label='S_21', coords={'amplitude': 0.1}, bin_mode='average_append', t0=0.0) | 992558390455123802 |
[12]:
f, ax = compiled_sched.plot_pulse_diagram(x_range=(200e-6, 200.4e-6))
Overriding device parameters on circuit-level operations#
The Qblox-scheduler compiler has an additional feature which adds more low-level control for users how a circuit-level operation is compiled to device-level. It is possible to override the parameters of the DeviceElement using the device_overrides keyword argument in each circuit-level operation.
For example, to override the DRAG parameter beta:
[13]:
pulse_duration = 1e-6
q0_dev.rxy.beta = 0.5 * pulse_duration / 8
Let’s create, compile and show it on an actual schedule.
[14]:
from qblox_scheduler import HardwareAgent
from qblox_scheduler.operations import IdlePulse
from qblox_scheduler.operations.loop_domains import DType, linspace
agent = HardwareAgent("./dependencies/configs/hw_cfg.json", "./dependencies/configs/dev_cfg.json")
sched = Schedule("X train")
beta = 1e-9
with sched.loop(linspace(start=-1, stop=1, num=10, dtype=DType.AMPLITUDE)) as amp:
sched.add(IdlePulse(duration=30e-9))
sched.add(X("q0", amp180=amp, beta=beta))
compiled_sched = agent.compile(schedule=sched)
/.venv/lib/python3.14/site-packages/qblox_scheduler/qblox/hardware_agent.py:452: UserWarning: Failed to connect the cluster to the hardware before compilation, several attributes such as `compiled_operations` will not be available! (cause: timed out)
warnings.warn(
[15]:
f, ax = compiled_sched.plot_pulse_diagram(x_range=(000e-6, 1800.4e-6), port_list=["q0:mw"])
As you can see, the amplitude of the pulse (which was compiled from the X gate) changes.
Attention: A few device element parameter names do not correspond to the device_overrides key names. The integration_time of device elements can be overridden with the "acq_duration" key. These discrepancies are rare; in all cases the device element’s factory_kwargs must be used in the already generated compilation config.