Execution
QAT also provides utility for executing quantum programs and interpreting the results.
In the pipelines, we saw that programs could be executed using QAT().execute(pkg)
.
Here we break this down into the lower-level details. In particular, execution in QAT is
composed of two elements: the Engine and the Runtime.
Below is a diagram that explains how execution is done in QAT, in particular, for the
SimpleRuntime
. The native code is fed to the
runtime, which passes it to the engine. The engine communicates with the target to
execute the program and fetch the results. These results then enter the post-processing
pipeline - a series of passes that each mutate the results according the program. The
mutated readout results are returned to the user.

Engines
The NativeEngine
is the base class used to
implement an engine. The engine is expected to uphold a contract with the Runtime:
Packages can be executed through the method
NativeEngine.execute
, which expects to receive anExecutable
(the native code) as an argument.In return, the engine returns the results to the runtime in an expected format, which is a dictionary of acquisition results (one result per acquisition). The result is an array of readout acquisitions, whose shape will depend on the acquisition mode. The key for the acquisition in the dictionary is the
output_variable
stored in theAcquireData
.The number of shots to execute is stored in the attribute
compiled_shots
. Note that while the total number of shots in a program might be larger than thecompiled_shots
, sometimes the target cannot support the required amount of shots. When this is the case, shots will be batched.
Engines available in QAT
The following engines are available to use within QAT. Engines written for proprietary OQC hardware is not available here.
EchoEngine
: an engine compatible only with theWaveformV1Backend
that simply “echos” back the readout pulses, primarily used for testing purposes.ZeroEngine
: returns all readout responses as zeroes, again used for testing purposes.QiskitEngine
: a legacy engine that simulates quantum circuits using Qiskit’s AerSimulator. To be refactored to make full use of the pipelines API.RealtimeChipSimEngine
: OQC’s home-made simulator for accurate and realistic simulation of superconducting qubits. Also a legacy engine and needs to be refactored to make full use of the pipelines API.
Echo engine example
As an example, let us use the EchoEngine
to execute a QASM2 program. For simplicity, we will make use of a pipeline to compile the
program, but then use to engine independently to execute the program.
1from qat import QAT
2from qat.pipelines.echo import echo8
3from qat.engines.waveform_v1 import EchoEngine
4from compiler_config.config import CompilerConfig, Tket
5
6qasm_str = """
7OPENQASM 2.0;
8include "qelib1.inc";
9qreg q[2];
10creg c[2];
11h q[0];
12cx q[0], q[1];
13measure q -> c;
14"""
15config = CompilerConfig(repeats=10, optimizations=Tket().disable())
16
17core = QAT()
18core.pipelines.add(echo8, default=True)
19pkg, _ = core.compile(qasm_str, config)
20results = EchoEngine().execute(pkg)
The results returned as a dictionary: the keys correspond to output variables assigned
to the readouts at compilation, in this case, it has the format c[{clbit}]_{qubit}
,
where clbit
corresponds to the bit specified in the QASM program, and the
qubit
denotes the qubit that is read out (note this may differ to what is
specified in the QASM program if optimizations are used). Since the
AcquireMode.INTEGRATOR
is used by
default for readout acquisitions, the values in the dictionary are arrays with one readout
per shot. For this example, the results are:
results = {
'c[0]_0': array([1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j]),
'c[1]_1': array([1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j])
}
Connection handling with engines
Sometimes an engine requires a connection to be made with the target. Connection
capabilities can be specified by mixing in a
ConnectionMixin
.
To demonstrate how connection handling can be specified, see the following example, which
adds a mock connection to the ZeroEngine
.
1from qat.engines import ConnectionMixin
2from qat.engines.zero import ZeroEngine
3
4class NewEngine(ZeroEngine, ConnectionMixin):
5 is_connected: bool = False
6
7 def connect(self):
8 self.is_connected = True
9 print("Engine has connected.")
10 return self.is_connected
11
12 def disconnect(self):
13 self.is_connected = False
14 print("Engine has disconnected.")
15 return self.is_connected
Runtimes
The Runtime is the object that is used to fully execute a program. When provided with a
package, it makes calls to the engine to execute the “quantum parts” of the program, and
then runs the results it receives through a post-processing pipeline to execute the
“classical parts”. See qat.runtime.passes
for a full list of post-processing passes
available. The standard runtime to use is the
SimpleRuntime
, which simply calls the
engine (possibly multiple times if the shots are batched) and then processes the results.
In the future, there may be more complex runtimes such as hybrid runtimes that allow for a
more comprehensive interplay of classical and quantum computation.
For engines where a connection is required, the Runtime can be provided a
ConnectionMode
flag that instructs the
runtime on how the connection should be handled. For example, if a connection should always
be maintained for the entire lifetime of a runtime, we can use the flag
ConnectionMode.ALWAYS
. Alternatively, if
we want to delegate the responsibility of connection to the user, we can use the
ConnectionMode.MANUAL
flag.
Simple runtime
The following example shows how to use the
SimpleRuntime
with a
ZeroEngine
and a custom pipeline. For completeness,
it also shows how to add a connection flag, although it will be of no use here as the
ZeroEngine
does not require a connection!
1from qat import QAT
2from qat.pipelines.echo import echo8
3from qat.engines.zero import ZeroEngine
4from qat.runtime import SimpleRuntime
5from qat.runtime.connection import ConnectionMode
6from qat.passes.pass_base import PassManager
7from compiler_config.config import CompilerConfig, QuantumResultsFormat
8from qat.runtime.transform_passes import (
9 AssignResultsTransform,
10 InlineResultsProcessingTransform,
11 PostProcessingTransform,
12 ResultTransform
13)
14
15qasm_str = """
16OPENQASM 2.0;
17include "qelib1.inc";
18qreg q[2];
19creg c[2];
20h q[0];
21cx q[0], q[1];
22measure q -> c;
23"""
24config = CompilerConfig(repeats=10, results_format=QuantumResultsFormat().binary_count())
25
26core = QAT()
27core.pipelines.add(echo8, default=True)
28pkg, _ = core.compile(qasm_str, config)
29
30pipeline = (
31 PassManager()
32 | PostProcessingTransform()
33 | InlineResultsProcessingTransform()
34 | AssignResultsTransform()
35 | ResultTransform()
36)
37
38runtime = SimpleRuntime(ZeroEngine(), pipeline, ConnectionMode.ALWAYS)
39results = runtime.execute(pkg, compiler_config=config)
Since the Runtime takes care of post-processing responsibilities, the results returned look quite a bit different to what was returned from the engine:
results = {'c': {'11': 10}}
Legacy runtime
QAT pipelines also have support for legacy engines through the
LegacyRuntime
. For example, we can
define a runtime for the RTCS:
1from qat.runtime import LegacyRuntime
2from qat.model.loaders.legacy import RTCSModelLoader
3from qat.purr.backends.realtime_chip_simulator import RealtimeChipSimEngine
4
5model = RTCSModelLoader().load()
6runtime = LegacyRuntime(RealtimeChipSimEngine(model))
Warning
Legacy engines can vary in the post-processing responsibilities that they carry out. An appropriate post-processing pipeline must be picked to match the legacy engine.