Tutorial 1. Scheduler concepts

In this tutorial we explore how to program a basic experiment using the quantify.scheduler. We will give an overview of the sequncer module and show different visualization backends as well as compilation onto a hardware backend.


The quantify.scheduler can be used to schedule operations on the control hardware. The quantify.scheduler is designed to provide access to hardware functionality at a high-level interface.

The quantify.scheduler is built around the Schedule, a JSON-based data structure containing operations , timing_constraints , and resources . Take a look at the quantify.scheduler.Schedule documentation for more details.

An Operation contains information on how to represent the operation at different levels of abstraction, such as the quantum-circuit (gate) level or the pulse level. The quantify.scheduler comes with a gate_library and a pulse_library , both containing common operations. When adding an Operation to a Schedule, the user is not expected to provide all information at once. Only when specific information is required by a backend such as a simulator or a hardware backend does the information need to be provided.

A compilation step is a transformation of the Schedule and results in a new Schedule. A compilation step can be used to e.g., add pulse information to operations containing only a gate-level representation or to determine the absolute timing based on timing constraints. A final compilation step translates the Schedule into a format compatible with the desired backend.

The following diagram provides an overview of how to interact with the Schedule class. The user can create a new schedule using the quantify API, or load a schedule based on one of the supported frontends for QASM-like formats such as qiskit QASM or OpenQL cQASM (todo). One or multiple compilation steps modify the Schedule until it contains the information required for the visualization used for visualization, simulation or compilation onto the hardware or back into a common QASM-like format.

blockdiag quantify API Q A S M-like formats Visualization backends Hardware backends Simulator backends Q A S M-like formats Schedule Compile Input formats Backends Compilation

The benefit of allowing the user to mix the high-level gate description of a circuit with the lower-level pulse description can be understood through an example. Below we first give an example of basic usage using Bell violations. We next show the Chevron experiment in which the user is required to mix gate-type and pulse-type information when defining the Schedule.

Ex: A basic quantum circuit: the Bell experiment

As the first example, we want to perform the Bell experiment . In this example, we will go quite deep into the internals of the schedule to show how the data structures work.

The goal of the Bell experiment is to create a Bell state \(|\Phi ^+\rangle=\frac{1}{2}(|00\rangle+|11\rangle)\) followed by a measurement and observe violations of the CSHS inequality.

By changing the basis in which one of the detectors measures, we can observe an oscillation which should result in a violation of Bell’s inequality. If everything is done properly, one should observe this oscillation:


Bell circuit

Below is the QASM code used to perform this experiment in Quantum Inspire as well as a circuit diagram representation. We will be creating this same experiment using the quantify.scheduler.

version 1.0

# Bell experiment

qubits 2

prep_z q[0:1]

X90 q[0]
cz q[0],q[1]

# change the value to change the basis of the detector
Rx q[0], 0.15


Creating a schedule

We start by initializing an empty Schedule

from quantify.scheduler import Schedule
sched = Schedule('Bell experiment')
Schedule "Bell experiment" containing (0) 0  (unique) operations.

Under the hood, the Schedule is based on a dictionary that can be serialized

{'operation_dict': {},
 'timing_constraints': [],
 'resource_dict': {},
 'name': 'Bell experiment'}

We also need to define the resources. For now these are just strings because I have not implemented them properly yet.

# define the resources
# q0, q1 = Qubits(n=2) # assumes all to all connectivity
q0, q1 = ('q0', 'q1') # we use strings because Resources have not been implemented yet

We will now add some operations to the schedule. Because this experiment is most conveniently described on the gate level, we use operations defined in the quantify.scheduler.gate_library .

from quantify.scheduler.gate_library import Reset, Measure, CZ, Rxy, X90

# Define the operations, these will be added to the circuit
init_all = Reset(q0, q1)
x90_q0 = Rxy(theta=90, phi=0, qubit=q0)
cz = CZ(qC=q0, qT=q1)
Rxy_theta = Rxy(theta=23, phi=0, qubit=q0) # will be not be used in the experiment loop.
meass_all = Measure(q0, q1)

Similar to the schedule, Operation objects are also based on dicts.

# Rxy_theta  # produces the same output
{'gate_info': {'unitary': array([[0.9799247+0.j        , 0.       -0.19936793j],
         [0.       -0.19936793j, 0.9799247+0.j        ]]),
  'tex': '$R_{xy}^{23, 0}$',
  'plot_func': 'quantify.scheduler.visualization.circuit_diagram.gate_box',
  'qubits': ['q0'],
  'operation_type': 'Rxy',
  'theta': 23,
  'phi': 0},
 'pulse_info': [],
 'logic_info': {},
 'name': 'Rxy(23.00, 0.00) q0'}

Now we create the Bell experiment, including observing the oscillation in a simple for loop.

import numpy as np

# we use a regular for loop as we have to unroll the changing theta variable here
for theta in np.linspace(0, 360, 21):
    sched.add(Rxy(theta=theta, phi=0, qubit=q0))
    sched.add(Measure(q0, q1), label='M {:.2f} deg'.format(theta))

Let’s take a look at the internals of the Schedule.

Schedule "Bell experiment" containing (24) 105  (unique) operations.

We can see that the number of unique operations is 24 corresponding to 4 operations that occur in every loop and 21 unique rotations for the different theta angles. (21+4 = 25 so we are missing something.

dict_keys(['operation_dict', 'timing_constraints', 'resource_dict', 'name'])

The schedule consists of a hash table containing all the operations. This allows efficient loading of pulses or gates to memory and also enables efficient adding of pulse type information as a compilation step.

from itertools import islice
# showing the first 5 elements of the operation dict
dict(islice(sched.data['operation_dict'].items(), 5))
{-2608349557500302401: {'gate_info': {'unitary': None, 'tex': '$|0\\rangle$', 'plot_func': 'quantify.scheduler.visualization.circuit_diagram.reset', 'qubits': ['q0', 'q1'], 'operation_type': 'reset'}, 'pulse_info': [], 'logic_info': {}, 'name': "Reset ('q0', 'q1')"},
 -4958875037583492356: {'gate_info': {'unitary': array([[0.70710678+0.j        , 0.        -0.70710678j],
        [0.        -0.70710678j, 0.70710678+0.j        ]]), 'tex': '$R_{xy}^{90, 0}$', 'plot_func': 'quantify.scheduler.visualization.circuit_diagram.gate_box', 'qubits': ['q0'], 'operation_type': 'Rxy', 'theta': 90, 'phi': 0}, 'pulse_info': [], 'logic_info': {}, 'name': 'Rxy(90.00, 0.00) q0'},
 4957578441480748832: {'gate_info': {'unitary': array([[ 1,  0,  0,  0],
        [ 0,  1,  0,  0],
        [ 0,  0,  1,  0],
        [ 0,  0,  0, -1]]), 'tex': 'CZ', 'plot_func': 'quantify.scheduler.visualization.circuit_diagram.cz', 'qubits': ['q0', 'q1'], 'operation_type': 'CZ'}, 'pulse_info': [], 'logic_info': {}, 'name': 'CZ (q0, q1)'},
 477338727513809640: {'gate_info': {'unitary': array([[1.+0.j, 0.-0.j],
        [0.-0.j, 1.+0.j]]), 'tex': '$R_{xy}^{0, 0}$', 'plot_func': 'quantify.scheduler.visualization.circuit_diagram.gate_box', 'qubits': ['q0'], 'operation_type': 'Rxy', 'theta': 0.0, 'phi': 0}, 'pulse_info': [], 'logic_info': {}, 'name': 'Rxy(0.00, 0.00) q0'},
 9071396920288674277: {'gate_info': {'unitary': None, 'plot_func': 'quantify.scheduler.visualization.circuit_diagram.meter', 'tex': '$\\langle0|$', 'qubits': ['q0', 'q1'], 'operation_type': 'measure'}, 'pulse_info': [], 'logic_info': {}, 'name': "Measure ('q0', 'q1')"}}

The timing constraints are stored as a list of pulses.

[{'label': '9546e460-77e8-4ada-a2ad-57432d613a2f',
  'rel_time': 0,
  'ref_op': None,
  'ref_pt_new': 'start',
  'ref_pt': 'end',
  'operation_hash': -2608349557500302401},
 {'label': '84ab543e-c25b-4895-8178-7eb7da400bfb',
  'rel_time': 0,
  'ref_op': None,
  'ref_pt_new': 'start',
  'ref_pt': 'end',
  'operation_hash': -4958875037583492356},
 {'label': '8c39f36b-d4b7-4e77-94a0-b5278876a752',
  'rel_time': 0,
  'ref_op': None,
  'ref_pt_new': 'start',
  'ref_pt': 'end',
  'operation_hash': 4957578441480748832},
 {'label': '32369388-99f5-4147-9e4c-7116067ec867',
  'rel_time': 0,
  'ref_op': None,
  'ref_pt_new': 'start',
  'ref_pt': 'end',
  'operation_hash': 477338727513809640},
 {'label': 'M 0.00 deg',
  'rel_time': 0,
  'ref_op': None,
  'ref_pt_new': 'start',
  'ref_pt': 'end',
  'operation_hash': 9071396920288674277},
 {'label': 'e7841380-4564-42c3-ad96-635c1cbaebe6',
  'rel_time': 0,
  'ref_op': None,
  'ref_pt_new': 'start',
  'ref_pt': 'end',
  'operation_hash': -2608349557500302401}]

Visualization using a circuit diagram

So far we have only defined timing constraints but the duration of pulses is not known.

For this purpose we do our first compilation step:

from quantify.scheduler.compilation import _determine_absolute_timing
# We modify the schedule in place adding timing information
# setting clock_unit='ideal' ignores the duration of operations and sets it to 1.
_determine_absolute_timing(sched, clock_unit='ideal')
Schedule "Bell experiment" containing (24) 105  (unique) operations.

And we can use this to create a default visualizaton:

%matplotlib inline

from quantify.scheduler.backends import visualization as viz
f, ax = viz.circuit_diagram_matplotlib(sched)
# all gates are plotted, but it doesn't all fit in a matplotlib figure
ax.set_xlim(-.5, 9.5)
(-0.5, 9.5)
../_images/Tutorial 1. Scheduler concepts_11_1.png

Compilation onto a Transmon backend

Of course different Qubits are driven with different techniques which must be defined. Here we have a pair of Transmon qubits, which respond to microwave pulses:

#  q0 ro_pulse_modulation_freq should be 80e6, requires issue38 resolution
device_test_cfg = {
          "q0": {"mw_amp180": 0.5, "mw_motzoi": -0.25, "mw_duration": 20e-9,
                 "mw_modulation_freq": 50e6, "mw_ef_amp180": 0.87, "mw_ch": "qcm0.s0",
                 "ro_ch": "qrm0.s0", "ro_pulse_amp": 0.5, "ro_pulse_modulation_freq": 80e6,
                 "ro_pulse_type": "square", "ro_pulse_duration": 150e-9,
                 "ro_acq_delay": 120e-9, "ro_acq_integration_time": 700e-9,
                 "ro_acq_weigth_type": "SSB",
                 "init_duration": 250e-6
          "q1": {"mw_amp180": 0.45, "mw_motzoi": -0.15, "mw_duration": 20e-9,
                 "mw_modulation_freq": 80e6, "mw_ef_amp180": 0.27, "mw_ch": "qcm1.s0",
                 "ro_ch": "qrm0.s1", "ro_pulse_amp": 0.5, "ro_pulse_modulation_freq": -23e6,
                 "ro_pulse_type": "square", "ro_pulse_duration": 100e-9,
                 "ro_acq_delay": 120e-9, "ro_acq_integration_time": 700e-9,
                 "ro_acq_weigth_type": "SSB",
                 "init_duration": 250e-6 }
          "q0-q1": {
              "flux_duration": 40e-9,
              "flux_ch_control": "qcm0.s1", "flux_ch_target": "qcm1.s1",
              "flux_amp_control": 0.5,  "flux_amp_target": 0,
              "phase_correction_control": 0,
              "phase_correction_target": 0}

With this information, the compiler can now generate the waveforms required.


Our gates and timings are now defined but we still need to describe how the various devices in our experiments are connected; Quantify uses the quantify.scheduler.types.Resource to represent this. Of particular interest to us are the quantify.scheduler.resources.CompositeResource and the quantify.scheduler.resources.Pulsar_QCM_sequencer, which represent a collection of Resources and a single Core on the Pulsar QCM:

from quantify.scheduler.resources import CompositeResource, Pulsar_QCM_sequencer, Pulsar_QRM_sequencer
qcm0 = CompositeResource('qcm0', ['qcm0.s0', 'qcm0.s1'])
qcm0_s0 = Pulsar_QCM_sequencer('qcm0.s0', instrument_name='qcm0', seq_idx=0)
qcm0_s1 = Pulsar_QCM_sequencer('qcm0.s1', instrument_name='qcm0', seq_idx=1)

qcm1 = CompositeResource('qcm1', ['qcm1.s0', 'qcm1.s1'])
qcm1_s0 = Pulsar_QCM_sequencer('qcm1.s0', instrument_name='qcm1', seq_idx=0)
qcm1_s1 = Pulsar_QCM_sequencer('qcm1.s1', instrument_name='qcm1', seq_idx=1)

qrm0 = CompositeResource('qrm0', ['qrm0.s0', 'qrm0.s1'])
# Currently mocking a readout module using an acquisition module
qrm0_s0 = Pulsar_QRM_sequencer('qrm0.s0', instrument_name='qrm0', seq_idx=0)
qrm0_s1 = Pulsar_QRM_sequencer('qrm0.s1', instrument_name='qrm0', seq_idx=1)

sched.add_resources([qcm0, qcm0_s0, qcm0_s1, qcm1, qcm1_s0, qcm1_s1, qrm0, qrm0_s0, qrm0_s1])

With this information added, we can now compile the full program with an appropriate backend:

from quantify.scheduler.compilation import qcompile
import quantify.scheduler.backends.pulsar_backend as pb
sched, config_dict = qcompile(sched, device_test_cfg, backend=pb.pulsar_assembler_backend)

Let’s take a look at what our finished configuration looks like:

{'qcm0.s0': '/home/docs/checkouts/readthedocs.org/user_builds/quantify-quantify-scheduler/envs/0.1.0/lib/python3.8/site-packages/data/20201021/20201021-165420-735-6ea442-Bell experiment_schedule/schedule/qcm0.s0_sequencer_cfg.json',
 'qcm0.s1': '/home/docs/checkouts/readthedocs.org/user_builds/quantify-quantify-scheduler/envs/0.1.0/lib/python3.8/site-packages/data/20201021/20201021-165420-735-6ea442-Bell experiment_schedule/schedule/qcm0.s1_sequencer_cfg.json',
 'qcm1.s0': '/home/docs/checkouts/readthedocs.org/user_builds/quantify-quantify-scheduler/envs/0.1.0/lib/python3.8/site-packages/data/20201021/20201021-165420-735-6ea442-Bell experiment_schedule/schedule/qcm1.s0_sequencer_cfg.json',
 'qcm1.s1': '/home/docs/checkouts/readthedocs.org/user_builds/quantify-quantify-scheduler/envs/0.1.0/lib/python3.8/site-packages/data/20201021/20201021-165420-735-6ea442-Bell experiment_schedule/schedule/qcm1.s1_sequencer_cfg.json',
 'qrm0.s0': '/home/docs/checkouts/readthedocs.org/user_builds/quantify-quantify-scheduler/envs/0.1.0/lib/python3.8/site-packages/data/20201021/20201021-165420-735-6ea442-Bell experiment_schedule/schedule/qrm0.s0_sequencer_cfg.json',
 'qrm0.s1': '/home/docs/checkouts/readthedocs.org/user_builds/quantify-quantify-scheduler/envs/0.1.0/lib/python3.8/site-packages/data/20201021/20201021-165420-735-6ea442-Bell experiment_schedule/schedule/qrm0.s1_sequencer_cfg.json'}

It contains a list of JSON files representing the configuration for each device. Now we are ready to deploy to hardware.

The compiler also provides pulse schedule visualization, which can be useful for a quick verification that your schedule is as expected:

from quantify.scheduler.backends.visualization import pulse_diagram_plotly
fig = pulse_diagram_plotly(sched, ch_list=['qcm0.s0', 'qcm1.s0', 'qrm0.s0', 'qrm0.r0'])
{'qcm0.s0': 0, 'qcm1.s0': 1, 'qrm0.s0': 2, 'qrm0.r0': 3}