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_CONFIG_TRANSMON = utils.load_json_example_scheme("qblox_hardware_config_transmon.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_CONFIG_TRANSMON)
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");
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.