Tutorial: Operations and Qubits

See also

The complete source code of this tutorial can be found in

Operations and Qubits.ipynb

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, quantify-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. Finally, we showcase the hybrid nature of quantify-scheduler, allowing the scheduling of circuit-level and device-level operations side by side in the same schedule.

Many of the gates used in the circuit layer description are defined in gate_library such as Reset, X90 and Measure. Operations are instantiated by providing them with the name of the qubit(s) on which they operate:

from quantify_scheduler.operations.gate_library import CZ, Measure, Reset, X90

q0, q1 = ("q0", "q1")
X90(q0)
Measure(q1)
CZ(q0, q1)
Reset(q0);

Let’s investigate the different components present in the circuit-level description of the operation. As an example, we create a 45 degree rotation operation over the x-axis.

from pprint import pprint
from quantify_scheduler.operations.gate_library import Rxy

rxy45 = Rxy(theta=45.0, phi=0.0, qubit=q0)
pprint(rxy45.data)
{'acquisition_info': [],
 'gate_info': {'operation_type': 'Rxy',
               'phi': 0.0,
               'plot_func': 'quantify_scheduler.schedules._visualization.circuit_diagram.gate_box',
               'qubits': ['q0'],
               'tex': '$R_{xy}^{45, 0}$',
               'theta': 45.0,
               'unitary': array([[0.92387953+0.j        , 0.        -0.38268343j],
       [0.        -0.38268343j, 0.92387953+0.j        ]])},
 'logic_info': {},
 'name': "Rxy(45, 0, 'q0')",
 'pulse_info': []}

As we can see, the structure of a circuit-level operation is similar to a pulse-level operation. However, the information is contained inside the gate_info entry rather than the pulse_info entry of the data dictionary. Importantly, there is no device-specific information coupled to the operation such that it represents the abstract notion of this qubit rotation, rather than how to perform it on any physical qubit implementation.

The entries present above are documented in the operation schema. Generally, these schemas are only important when defining custom operations, which is not part of this tutorial. This schema can be inspected via:

import importlib.resources
import json
from quantify_scheduler import schemas

operation_schema = json.loads(importlib.resources.read_text(schemas, "operation.json"))
pprint(operation_schema["properties"]["gate_info"]["properties"])
{'operation_type': {'description': 'Defines what class of operations this gate '
                                   'refers to (e.g. Rxy, CZ etc.).',
                    'type': 'string'},
 'plot_func': {'description': 'reference to a function for plotting this '
                              'operation. If not specified, defaults to using '
                              ':func:`~quantify_scheduler.schedules._visualization.circuit_diagram.gate_box`.',
               'type': ['string', 'null']},
 'qubits': {'description': 'A list of strings indicating the qubits the gate '
                           'acts on. Valid qubits are strings that appear in '
                           'the device_config.json file.',
            'type': 'array'},
 'symmetric': {'description': 'A boolean to indicate whether a two qubit gate '
                              'is symmetric. This is used in the device config '
                              'compilation stage. By default, it is set as '
                              'False',
               'type': 'boolean'},
 'tex': {'description': 'latex snippet for plotting', 'type': 'string'},
 'unitary': {'description': 'A unitary matrix describing the operation.'}}

Schedule creation from the circuit layer (Bell)

The circuit-level operations can be used to create a Schedule within quantify-scheduler using the same method as for the pulse-level operations. This enables creating schedules on a more abstract level. Here, we demonstrate this extra layer of abstraction by creating a Schedule for measuring Bell violations.

Note

Within a single 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 (Chevron).

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:

import numpy as np
from quantify_scheduler import Schedule
from quantify_scheduler.operations.gate_library import CZ, Measure, Reset, Rxy, X90

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="M q0 {:.2f} deg".format(theta))
    sched.add(  
        Measure(q1, acq_index=acq_idx),
        label="M q1 {:.2f} deg".format(theta),
        ref_pt="start",  # Start at the same time as the other measure
    )

sched
Schedule "Bell experiment" containing (66) 147  (unique) operations.

By scheduling 7 operations for 21 different values for theta we indeed get a schedule containing 7*21=147 operations. To minimize the size of the schedule, identical operations are stored only once. For example, the CZ operation is stored only once but used 21 times, which leaves only 66 unique operations in the schedule.

Note

The acquisitions are different for every iteration due to their different acq_index. The Rxy-gate rotates over a different angle every iteration and must therefore also be different for every iteration (except for the last since \(R^{360}=R^0\)). Hence the number of unique operations is 3*21-1+4=66.

Visualizing the quantum circuit

We can directly visualize the created schedule on the quantum-circuit level. This visualization shows every operation on a line representing the different qubits.

%matplotlib inline
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()
../_images/99ba59a667efb13c8904dc1c27690e2a044346b5ca325ca6902585840ad80bc2.png

In previous tutorials, we visualized the schedules on the pulse level using 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 plot_pulse_diagram() will raise an error which signifies no pulse_info is present in the schedule:

sched.plot_pulse_diagram()
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[6], line 1
----> 1 sched.plot_pulse_diagram()

File ~/.anaconda3/envs/tmp/lib/python3.9/site-packages/quantify_scheduler/schedules/schedule.py:357, in ScheduleBase.plot_pulse_diagram(self, port_list, sampling_rate, modulation, modulation_if, plot_backend, plot_kwargs)
    350 if plot_backend == "mpl":
    351     # NB imported here to avoid circular import
    352     # pylint: disable=import-outside-toplevel
    353     from quantify_scheduler.schedules._visualization.pulse_diagram import (
    354         pulse_diagram_matplotlib,
    355     )
--> 357     return pulse_diagram_matplotlib(
    358         schedule=self,
    359         sampling_rate=sampling_rate,
    360         port_list=port_list,
    361         modulation=modulation,
    362         modulation_if=modulation_if,
    363         **plot_kwargs,
    364     )
    365 if plot_backend == "plotly":
    366     # NB imported here to avoid circular import
    367     # pylint: disable=import-outside-toplevel
    368     from quantify_scheduler.schedules._visualization.pulse_diagram import (
    369         pulse_diagram_plotly,
    370     )

File ~/.anaconda3/envs/tmp/lib/python3.9/site-packages/quantify_scheduler/schedules/_visualization/pulse_diagram.py:517, in pulse_diagram_matplotlib(schedule, port_list, sampling_rate, modulation, modulation_if, x_range, ax)
    478 def pulse_diagram_matplotlib(
    479     schedule: Union[Schedule, CompiledSchedule],
    480     port_list: Optional[List[str]] = None,
   (...)
    485     ax: Optional[matplotlib.axes.Axes] = None,
    486 ) -> Tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]:
    487     """
    488     Plots a schedule using matplotlib.
    489 
   (...)
    515         The matplotlib ax.
    516     """
--> 517     times, pulses = sample_schedule(
    518         schedule,
    519         sampling_rate=sampling_rate,
    520         port_list=port_list,
    521         modulation=modulation,
    522         modulation_if=modulation_if,
    523         x_range=x_range,
    524     )
    525     if ax is None:
    526         _, ax = plt.subplots()

File ~/.anaconda3/envs/tmp/lib/python3.9/site-packages/quantify_scheduler/schedules/_visualization/pulse_diagram.py:402, in sample_schedule(schedule, port_list, modulation, modulation_if, sampling_rate, x_range)
    399             max_x = min(max(t0 + pulse_info["duration"], max_x), x_range[1])
    401 if np.isinf(min_x) or np.isinf(max_x):
--> 402     raise RuntimeError(
    403         f"Attempting to sample schedule {schedule.name}, "
    404         "but the schedule does not contain any `pulse_info`. "
    405         "Please verify that the schedule has been populated and "
    406         "device compilation has been performed."
    407     )
    409 time_window = [min_x, max_x]
    411 logger.debug(f"time_window {time_window}, port_map {port_map}")

RuntimeError: Attempting to sample schedule Bell experiment, but the schedule does not contain any `pulse_info`. Please verify that the schedule has been populated and device compilation has been performed.

And similarly for the timing_table:

sched.timing_table
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[7], line 1
----> 1 sched.timing_table

File ~/.anaconda3/envs/tmp/lib/python3.9/site-packages/quantify_scheduler/schedules/schedule.py:432, in ScheduleBase.timing_table(self)
    429 for schedulable in self.schedulables.values():
    430     if "abs_time" not in schedulable:
    431         # when this exception is encountered
--> 432         raise ValueError("Absolute time has not been determined yet.")
    433     operation = self.operations[schedulable["operation_repr"]]
    435     for i, op_info in chain(
    436         enumerate(operation["pulse_info"]),
    437         enumerate(operation["acquisition_info"]),
    438     ):

ValueError: Absolute time has not been determined yet.

Device configuration

Up until now, the schedule is not specific to any qubit implementation. The aim of this section is to add device-specific information to the schedule. This knowledge is contained in the device configuration, which we introduce in this section. By compiling the schedule to the quantum-device layer, we incorporate the device configuration into the schedule (for example by adding pulse information to every gate) and thereby enable it to run on a specific qubit implementation.

To start this section, we will unpack the structure of the device configuration. Here we will use an example device configuration for a transmon-based system that is used in the quantify-scheduler test suite.

from quantify_scheduler.backends.circuit_to_device import DeviceCompilationConfig
from quantify_scheduler.schemas.examples.device_example_cfgs import (
    example_transmon_cfg,
)

device_cfg = DeviceCompilationConfig.parse_obj(example_transmon_cfg)

list(device_cfg.dict())
['backend', 'clocks', 'elements', 'edges']

Before explaining how this can be used to compile schedules, let us first investigate the contents of the device configuration.

device_cfg.backend
<function quantify_scheduler.backends.circuit_to_device.compile_circuit_to_device(schedule: 'Schedule', config: 'CompilationConfig | DeviceCompilationConfig | Dict | None' = None, *, device_cfg: 'DeviceCompilationConfig | Dict | None' = None) -> 'Schedule'>

The backend of the device configuration specifies what function will be used to add pulse information to the gates. In other words, it specifies how to interpret the qubit parameters present in the device configuration and achieve the required gates. Let us briefly investigate the backend function:

help(device_cfg.backend)
Help on function compile_circuit_to_device in module quantify_scheduler.backends.circuit_to_device:

compile_circuit_to_device(schedule: 'Schedule', config: 'CompilationConfig | DeviceCompilationConfig | Dict | None' = None, *, device_cfg: 'DeviceCompilationConfig | Dict | None' = None) -> 'Schedule'
    Add pulse information to all gates in the schedule.
    
    Before calling this function, the schedule can contain abstract operations (gates or
    measurements). This function adds pulse and acquisition information with respect to
    `config` as they are expected to arrive to device (latency or distortion corrections
    are not taken into account).
    
    From a point of view of :ref:`sec-compilation`, this function converts a schedule
    defined on a quantum-circuit layer to a schedule defined on a quantum-device layer.
    
    Parameters
    ----------
    schedule
        The schedule to be compiled.
    config
        Compilation config for
        :class:`~quantify_scheduler.backends.graph_compilation.QuantifyCompiler`, of
        which only the :attr:`.CompilationConfig.device_compilation_config`
        is used in this compilation step.
    device_cfg
        (deprecated) Device compilation config. Pass a full compilation config instead
        using `config` argument. Note, if a dictionary is passed, it will be parsed to a
        :class:`~.DeviceCompilationConfig`.
    
    Returns
    -------
    :
        A copy of `schedule` with pulse information added to all gates.
    
    Raises
    ------
    ValueError
        When both `config` and `device_cfg` are supplied.

The device configuration also contains the parameters required by the backend for all qubits and edges.

print(list(device_cfg.elements))
print(list(device_cfg.edges))
print(list(device_cfg.clocks))
['q0', 'q1']
['q0_q1']
['q0.01', 'q0.ro', 'q1.01', 'q1.ro']

For every qubit and edge, we can investigate the contained parameters.

print(device_cfg.elements["q0"])
print(device_cfg.elements["q0"]["Rxy"].factory_kwargs)
{'reset': OperationCompilationConfig(factory_func=<class 'quantify_scheduler.operations.pulse_library.IdlePulse'>, factory_kwargs={'duration': 0.0002}, gate_info_factory_kwargs=None), 'Rxy': OperationCompilationConfig(factory_func=<function rxy_drag_pulse at 0x7f849f3cda60>, factory_kwargs={'amp180': 0.32, 'motzoi': 0.45, 'port': 'q0:mw', 'clock': 'q0.01', 'duration': 2e-08}, gate_info_factory_kwargs=['theta', 'phi']), 'Z': OperationCompilationConfig(factory_func=<class 'quantify_scheduler.operations.pulse_library.SoftSquarePulse'>, factory_kwargs={'amp': 0.23, 'duration': 4e-09, 'port': 'q0:fl', 'clock': 'cl0.baseband'}, gate_info_factory_kwargs=None), 'measure': OperationCompilationConfig(factory_func=<function dispersive_measurement at 0x7f849f3df0d0>, factory_kwargs={'port': 'q0:res', 'clock': 'q0.ro', 'pulse_type': 'SquarePulse', 'pulse_amp': 0.25, 'pulse_duration': 1.6e-07, 'acq_delay': 1.2e-07, 'acq_duration': 3e-07, 'acq_channel': 0}, gate_info_factory_kwargs=['acq_index', 'bin_mode', 'acq_protocol'])}
{'amp180': 0.32, 'motzoi': 0.45, 'port': 'q0:mw', 'clock': 'q0.01', 'duration': 2e-08}
print(device_cfg.edges)
{'q0_q1': {'CZ': OperationCompilationConfig(factory_func=<function composite_square_pulse at 0x7f849f3cdc10>, factory_kwargs={'square_port': 'q0:fl', 'square_clock': 'cl0.baseband', 'square_amp': 0.5, 'square_duration': 2e-08, 'virt_z_parent_qubit_phase': 44, 'virt_z_parent_qubit_clock': 'q0.01', 'virt_z_child_qubit_phase': 63, 'virt_z_child_qubit_clock': 'q1.01'}, gate_info_factory_kwargs=None)}}
print(device_cfg.clocks)
{'q0.01': 6020000000.0, 'q0.ro': 7040000000.0, 'q1.01': 5020000000.0, 'q1.ro': 6900000000.0}

Lastly, the complete example device configuration (also see DeviceCompilationConfig):

pprint(example_transmon_cfg)
{'backend': 'quantify_scheduler.backends.circuit_to_device.compile_circuit_to_device',
 'clocks': {'q0.01': 6020000000.0,
            'q0.ro': 7040000000.0,
            'q1.01': 5020000000.0,
            'q1.ro': 6900000000.0},
 'edges': {'q0_q1': {'CZ': {'factory_func': 'quantify_scheduler.operations.pulse_factories.composite_square_pulse',
                            'factory_kwargs': {'square_amp': 0.5,
                                               'square_clock': 'cl0.baseband',
                                               'square_duration': 2e-08,
                                               'square_port': 'q0:fl',
                                               'virt_z_child_qubit_clock': 'q1.01',
                                               'virt_z_child_qubit_phase': 63,
                                               'virt_z_parent_qubit_clock': 'q0.01',
                                               'virt_z_parent_qubit_phase': 44}}}},
 'elements': {'q0': {'Rxy': {'factory_func': 'quantify_scheduler.operations.pulse_factories.rxy_drag_pulse',
                             'factory_kwargs': {'amp180': 0.32,
                                                'clock': 'q0.01',
                                                'duration': 2e-08,
                                                'motzoi': 0.45,
                                                'port': 'q0:mw'},
                             'gate_info_factory_kwargs': ['theta', 'phi']},
                     'Z': {'factory_func': 'quantify_scheduler.operations.pulse_library.SoftSquarePulse',
                           'factory_kwargs': {'amp': 0.23,
                                              'clock': 'cl0.baseband',
                                              'duration': 4e-09,
                                              'port': 'q0:fl'}},
                     'measure': {'factory_func': 'quantify_scheduler.operations.measurement_factories.dispersive_measurement',
                                 'factory_kwargs': {'acq_channel': 0,
                                                    'acq_delay': 1.2e-07,
                                                    'acq_duration': 3e-07,
                                                    'clock': 'q0.ro',
                                                    'port': 'q0:res',
                                                    'pulse_amp': 0.25,
                                                    'pulse_duration': 1.6e-07,
                                                    'pulse_type': 'SquarePulse'},
                                 'gate_info_factory_kwargs': ['acq_index',
                                                              'bin_mode',
                                                              'acq_protocol']},
                     'reset': {'factory_func': 'quantify_scheduler.operations.pulse_library.IdlePulse',
                               'factory_kwargs': {'duration': 0.0002}}},
              'q1': {'Rxy': {'factory_func': 'quantify_scheduler.operations.pulse_factories.rxy_drag_pulse',
                             'factory_kwargs': {'amp180': 0.4,
                                                'clock': 'q1.01',
                                                'duration': 2e-08,
                                                'motzoi': 0.25,
                                                'port': 'q1:mw'},
                             'gate_info_factory_kwargs': ['theta', 'phi']},
                     'measure': {'factory_func': 'quantify_scheduler.operations.measurement_factories.dispersive_measurement',
                                 'factory_kwargs': {'acq_channel': 1,
                                                    'acq_delay': 1.2e-07,
                                                    'acq_duration': 3e-07,
                                                    'clock': 'q1.ro',
                                                    'port': 'q1:res',
                                                    'pulse_amp': 0.21,
                                                    'pulse_duration': 1.6e-07,
                                                    'pulse_type': 'SquarePulse'},
                                 'gate_info_factory_kwargs': ['acq_index',
                                                              'bin_mode',
                                                              'acq_protocol']},
                     'reset': {'factory_func': 'quantify_scheduler.operations.pulse_library.IdlePulse',
                               'factory_kwargs': {'duration': 0.0002}}}}}

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, quantify-scheduler provides the QuantumDevice and DeviceElement classes.

These classes contain the information necessary to generate the device configs and allow changing their parameters on-the-fly. The QuantumDevice class represents the DUT containing different DeviceElement s. Currently, quantify-scheduler contains the BasicTransmonElement class to represent a fixed-frequency transmon qubit connected to a feedline. We show their interaction below:

from quantify_scheduler.device_under_test.quantum_device import QuantumDevice
from quantify_scheduler.device_under_test.transmon_element import BasicTransmonElement

# 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()
(<QuantumDevice: DUT>, ['qubit'])

The different transmon properties can be set through attributes of the BasicTransmonElement class instance, e.g.:

qubit.clock_freqs.f01(6e9)

print(list(qubit.submodules.keys()))
print()
for submodule_name, submodule in qubit.submodules.items():
    print(f"{qubit.name}.{submodule_name}: {list(submodule.parameters.keys())}")
['reset', 'rxy', 'measure', 'ports', 'clock_freqs']

qubit.reset: ['duration']
qubit.rxy: ['amp180', 'motzoi', 'duration']
qubit.measure: ['pulse_type', 'pulse_amp', 'pulse_duration', 'acq_channel', 'acq_delay', 'integration_time', 'reset_clock_phase', 'acq_weights_a', 'acq_weights_b', 'acq_weights_sampling_rate', 'acq_weight_type']
qubit.ports: ['microwave', 'flux', 'readout']
qubit.clock_freqs: ['f01', 'f12', 'readout']

The device configuration is now simply obtained using dut.generate_device_config(). In order for this command to provide a correct device configuration, the different properties need to be set to applicable values in the BasicTransmonElement and QuantumDevice objects.

pprint(dut.generate_device_config())
DeviceCompilationConfig(backend=<function compile_circuit_to_device at 0x7f849f42ee50>, clocks={'qubit.01': 6000000000.0, 'qubit.12': nan, 'qubit.ro': nan}, elements={'qubit': {'reset': OperationCompilationConfig(factory_func=<class 'quantify_scheduler.operations.pulse_library.IdlePulse'>, factory_kwargs={'duration': 0.0002}, gate_info_factory_kwargs=None), 'Rxy': OperationCompilationConfig(factory_func=<function rxy_drag_pulse at 0x7f849f3cda60>, factory_kwargs={'amp180': nan, 'motzoi': 0, 'port': 'qubit:mw', 'clock': 'qubit.01', 'duration': 2e-08, 'reference_magnitude': None}, gate_info_factory_kwargs=['theta', 'phi']), 'measure': OperationCompilationConfig(factory_func=<function dispersive_measurement at 0x7f849f3df0d0>, factory_kwargs={'port': 'qubit:res', 'clock': 'qubit.ro', 'pulse_type': 'SquarePulse', 'pulse_amp': 0.25, 'pulse_duration': 3e-07, 'acq_delay': 0, 'acq_duration': 1e-06, 'acq_channel': 0, 'acq_protocol_default': 'SSBIntegrationComplex', 'reset_clock_phase': True, 'reference_magnitude': None, 'acq_weights_a': None, 'acq_weights_b': None, 'acq_weights_sampling_rate': None}, gate_info_factory_kwargs=['acq_index', 'bin_mode', 'acq_protocol'])}}, edges={})

Mixing pulse and circuit layer operations (Chevron)

As well as defining our schedules in terms of gates, we can also mix the circuit layer representation with pulse-level operations. This can be useful for experiments involving pulses not easily represented by Gates, such as the Chevron experiment. In this experiment, we want to vary the length and amplitude of a square pulse between X gates on a pair of qubits.

from quantify_scheduler import Schedule
from quantify_scheduler.operations.gate_library import Measure, Reset, X, X90
from quantify_scheduler.operations.pulse_library import SquarePulse
from quantify_scheduler.resources import ClockResource

sched = Schedule("Chevron Experiment")
acq_idx = 0

for duration in np.linspace(start=20e-9, stop=60e-9, num=6):
    for amp in np.linspace(start=0.1, stop=1.0, num=10):
        reset = sched.add(Reset("q0", "q1"))
        sched.add(X("q0"), ref_op=reset, ref_pt="end")  # Start at the end of the reset
        # We specify a clock for tutorial purposes, Chevron experiments do not necessarily use modulated square pulses
        square = sched.add(SquarePulse(amp=amp, duration=duration, port="q0:mw", clock="q0.01"))
        sched.add(X90("q0"), ref_op=square)  # Start at the end of the square pulse
        sched.add(X90("q1"), ref_op=square)
        sched.add(Measure(q0, acq_index=acq_idx), label=f"M q0 {acq_idx}")
        sched.add(
            Measure(q1, acq_index=acq_idx),
            label=f"M q1 {acq_idx}",
            ref_pt="start",  # Start at the same time as the other measure
        )

        acq_idx += 1


# 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("q1.01", 6.02e9), ClockResource("q0.ro", 5.02e9), ClockResource("q1.ro", 5.02e9)]) 
fig, ax = sched.plot_circuit_diagram()
ax.set_xlim(-0.5, 9.5)
for t in ax.texts:
    if t.get_position()[0] > 9.5:
        t.set_visible(False)
../_images/5ee6e48b8c20ac54275361344aeff7dfe3638642a5a4158b330d5cc63a0a00e5.png

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.

Device and Hardware compilation combined: Serial Compiler

SerialCompiler can be used to execute the device and hardware compilation separately, or execute both in one call. Here we will not set the hardware configuration thus only executing device compilation. The Compiling to Hardware tutorial demonstrates how to set the hardware configuration.

SerialCompiler requires a CompilationConfig and this holds both the device and hardware configurations (when set). In the example below, we generate a CompilationConfig via generate_compilation_config() of QuantumDevice.

from quantify_scheduler.backends import SerialCompiler
from quantify_scheduler.device_under_test.quantum_device import QuantumDevice
from quantify_scheduler.device_under_test.transmon_element import BasicTransmonElement

dut.close()
dut = QuantumDevice("DUT")
q0 = BasicTransmonElement("q0")
q1 = BasicTransmonElement("q1")
dut.add_element(q0)
dut.add_element(q1)
dut.get_element("q0").rxy.amp180(0.6)
dut.get_element("q1").rxy.amp180(0.6)

compiler = SerialCompiler(name='compiler')
compiled_sched = compiler.compile(schedule=sched, config=dut.generate_compilation_config())

So, finally, we can show the timing table associated with the Chevron schedule and plot its pulse diagram:

compiled_sched.timing_table.hide(slice(11, None), axis="index").hide(
    "waveform_op_id", axis="columns"
)
/home/rsoko/.anaconda3/envs/tmp/lib/python3.9/site-packages/quantify_scheduler/schedules/schedule.py:451: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.
  timing_table = pd.concat(timing_table_list, ignore_index=True)
  port clock is_acquisition abs_time duration operation wf_idx
0 None cl0.baseband False 0.0 ns 200,000.0 ns Reset('q0','q1') 0
1 None cl0.baseband False 0.0 ns 200,000.0 ns Reset('q0','q1') 1
2 q0:mw q0.01 False 200,000.0 ns 20.0 ns X(qubit='q0') 0
3 q0:mw q0.01 False 200,020.0 ns 20.0 ns SquarePulse(amp=0.1,duration=2e-08,port='q0:mw',clock='q0.01',reference_magnitude=None,phase=0,t0=0) 0
4 q0:mw q0.01 False 200,040.0 ns 20.0 ns X90(qubit='q0') 0
5 q1:mw q1.01 False 200,040.0 ns 20.0 ns X90(qubit='q1') 0
6 None q0.ro False 200,060.0 ns 0.0 ns Measure('q0', acq_index=0, acq_protocol="None", bin_mode=None) 0
7 q0:res q0.ro False 200,060.0 ns 300.0 ns Measure('q0', acq_index=0, acq_protocol="None", bin_mode=None) 1
8 q0:res q0.ro True 200,060.0 ns 1,000.0 ns Measure('q0', acq_index=0, acq_protocol="None", bin_mode=None) 0
9 None q1.ro False 200,060.0 ns 0.0 ns Measure('q1', acq_index=0, acq_protocol="None", bin_mode=None) 0
10 q1:res q1.ro False 200,060.0 ns 300.0 ns Measure('q1', acq_index=0, acq_protocol="None", bin_mode=None) 1
f, ax = compiled_sched.plot_pulse_diagram(plot_kwargs={"x_range": (200e-6, 200.4e-6)})
../_images/85c75cd628b4af30d5c7f02528641b9d2a447511fb11ddfdfae14b867057ea89.png