Compilers#

In order to execute a Schedule on physical hardware or a simulator one needs to compile the schedule. This is done using a QuantifyCompiler. The compile() method requires both the Schedule to compile and a CompilationConfig that describes the information required to perform the compilation.

Upon the start of the compilation, the QuantifyCompiler defines a directed acyclic graph in which individual nodes represent compilation steps. A Schedule can be compiled by traversing the graph. The Schedule class serves as the intermediate representation which is modified by the compiler passes. For most practical purposes, a user does not need to be aware of the internal structure of the compilation backends.

Compiling a schedule#

To compile a schedule, one needs to instantiate a QuantifyCompiler and call the compile() method. This method requires both the Schedule to compile as well as a generate_compilation_config(). This config can conveniently be generated from a QuantumDevice object which describes the knowledge required for compilation.

Note

Here we focus on using a QuantifyCompiler to compile a Schedule in isolation. When executing schedules, one needs to interact with and manage the parameters of an experimental setup. For this we refer to the section on execution in the user guide.

First, we set up a mock setup and create a simple schedule that we want to compile.

import numpy as np
from quantify_scheduler.device_under_test.mock_setup import set_up_mock_transmon_setup, set_standard_params_transmon
from quantify_scheduler.schedules.timedomain_schedules import echo_sched

# instantiate the instruments of the mock setup
mock_setup = set_up_mock_transmon_setup()

# provide some sensible values to allow compilation without errors
set_standard_params_transmon(mock_setup)

echo_schedule = echo_sched(times=np.arange(0, 60e-6, 1.5e-6), qubit="q0", repetitions=1024)

Next, we retrieve the CompilationConfig from the quantum device and see for which compilation backend this is suitable. In the current example we have a simple SerialCompiler that is used to do different compilation passes as a linear chain.

quantum_device = mock_setup["quantum_device"]
config = quantum_device.generate_compilation_config()

print(config.backend)
<class 'quantify_scheduler.backends.graph_compilation.SerialCompiler'>

We can then instantiate the compiler and compile the program.

from quantify_scheduler.backends.graph_compilation import SerialCompiler

compiler = SerialCompiler(name="Device compile")
comp_sched = compiler.compile(schedule=echo_schedule, config=config)

comp_sched

CompiledSchedule "Echo" containing (43) 200  (unique) operations.

Understanding the structure of compilation#

A compilation backend defines a graph of compilation steps. This makes it really easy to visualize the different steps in the compilation process by drawing the graph.

Here we show the compilation structure for several commonly used compilers. To do this, we will use the example configuration files of the different compilers and then use the quantum device to generate the relevant CompilationConfig s. Note that in the future we want to improve how the hardware compilation config is managed so one does not need to set a custom dictionary to the hardware config parameter of the quantum_device object.

from quantify_scheduler.schemas.examples import utils

QBLOX_HARDWARE_COMP_CFG = utils.load_json_example_scheme("qblox_hardware_compilation_config.json")
ZHINST_HARDWARE_COMP_CFG = utils.load_json_example_scheme("zhinst_hardware_compilation_config.json")

dev_cfg = quantum_device.generate_compilation_config()

quantum_device.hardware_config(QBLOX_HARDWARE_COMP_CFG)
qblox_cfg = quantum_device.generate_compilation_config()

quantum_device.hardware_config(ZHINST_HARDWARE_COMP_CFG)
zhinst_cfg = quantum_device.generate_compilation_config()
from quantify_scheduler.backends import SerialCompiler

# Constructing graph is normally done when at compile time as it
# requires information from the compilation config

dev_compiler = SerialCompiler(name="Device compiler")
dev_compiler.construct_graph(dev_cfg)

qblox_compiler = SerialCompiler(name="Qblox compiler")
qblox_compiler.construct_graph(qblox_cfg)

zhinst_compiler = SerialCompiler(name="Zhinst compiler")
zhinst_compiler.construct_graph(zhinst_cfg)

import matplotlib.pyplot as plt
fig, axs = plt.subplots(1,3, figsize=(16,7))

# Show the graph of the currently included compilers
dev_compiler.draw(axs[0])
axs[0].set_title("Device Backend")
qblox_compiler.draw(axs[1])
axs[1].set_title("Qblox Backend")
zhinst_compiler.draw(axs[2])
axs[2].set_title("Zhinst Backend");

../_images/bc1b2770106061dc8aa8bdb80e098b63a12f486f40a5822b934295c4036a541e.png

Performance optimization by not preserving the original schedule#

There is a way to potentially decrease the compilation time by about 20-40 % depending on the schedule if you do not need to keep the original, uncompiled schedule after the compilation. Using keep_original_schedule = False in the CompilationConfig modifies parts of the original schedule, making the original schedule potentially unusable after the function call.

Note

The returned schedule references objects from the original schedule if CompilationConfig.keep_original_schedule = False is used. Refrain from modifying the original schedule after compilation in this case!

config.keep_original_schedule = False

compiler = SerialCompiler(name="Device compile")
comp_sched = compiler.compile(schedule=echo_schedule, config=config)

comp_sched

CompiledSchedule "Echo" containing (43) 200  (unique) operations.