Download
Download this file as Jupyter notebook: custom.ipynb.
Example: Defining Custom Compilers
Building a general compiler which is aware of all possible simplification rules would result in an overly complex and rigid tool, and would be difficult to generalize for all hardware implementations. By leaving the compiler itself unadorned and allowing for custom rules, very complex compilation instructions can be expressed in a simple and readable fashion.
With this in mind, True-Q™'s Compiler
works by
applying an ordered list of built-in and/or user-specified
Pass
es to a circuit in order.
Pass
objects define rules for how to decompose,
replace, or remove Gate
s (or more generally,
Operation
s), while possibly also adding or removing
Cycle
s.
Getting started
Let’s start with a simple example that demonstrates the actions of a compiler. Consider the circuit below:
[2]:
import trueq as tq
circuit = tq.Circuit(
cycles=[
tq.Cycle({(0, 1): tq.Gate.cz}),
tq.Cycle({0: tq.Gate.h, 1: tq.Gate.h}),
tq.Cycle({(2, 1): tq.Gate.cx}),
tq.Cycle({(0, 1): tq.Gate.cy, 2: tq.Gate.z}),
]
)
circuit.draw()
[2]:
We want to define a compiler that can replace certain cycles within this circuit
with different cycles, and furthermore merge any resulting additional single- and
two-qudit operations. To do that, we define two types of passes: a
CycleReplacement
and a
Merge
pass:
[3]:
replace_pass = tq.compilation.CycleReplacement(
target=tq.Cycle({(0, 1): tq.Gate.cz}),
replacement=3 * [tq.Cycle({(0, 1): tq.Gate.cz})],
)
merge_pass = tq.compilation.Merge(max_sys=2)
The CycleReplacement
replaces any occurrence of
the target
cycle with the replacement
cycle. In this case, our
replace_pass
will replace tq.Gate.cz
on qubits (0, 1)
with three
consecutive tq.Gate.cz
on qubits (0, 1)
. The
Merge
pass looks for any neighbouring gates which
act on max_sys
qudits with the same labels and merge them into a single gate.
To see how those passes work in practice, let’s define two different compilers:
one which applies only replace_pass
and one which applies only merge_pass
.
[4]:
replace_compiler = tq.compilation.Compiler(passes=[replace_pass])
merge_compiler = tq.compilation.Compiler(passes=[merge_pass])
Here is the output from the first compiler:
[5]:
compilation1 = replace_compiler.compile(circuit)
compilation1.draw()
[5]:
Notice that the compiler constructed with replace_pass
replaced the
tq.Gate.cz
with three consecutive tq.Gate.cz
gates on qubits (0, 1)
, as
expected.
Now let’s apply our merge_compiler
to this circuit:
[6]:
compilation2 = merge_compiler.compile(compilation1)
compilation2.draw()
[6]:
Note that the compiler constructed using the merge_pass
merged the first four
cycles into a single cycle, as intended.
Applying first our replace_compiler
and then our merge_compiler
has the effect
of the cycle replacement pass and the cycle merging pass to be executed in that order.
The same can be achieved through a single compiler that we initialize with a list containing both passes:
[7]:
composite_compiler = tq.compilation.Compiler(passes=[replace_pass, merge_pass])
composite_compilation = composite_compiler.compile(circuit)
composite_compilation.draw()
[7]:
Predefined Pass Lists
In addition to the passes shown above, there are several other useful
pass classes available as members of the
Compiler
class, for example:
HARDWARE_PASSES
,
SIMPLIFY_PASSES
,
RC_PASSES
, and
NATIVE2Q_PASSES
. Click on the links to their
documentation for further details on each.
Note that these pass classes need to be instantiated before they can be given to the
compiler constructor. For certain types of advanced usage, directly invoking the
constructor may be the preferred method. However, the compiler also has two static
covenience methods to automate pass instantiation:
from_config()
and
basic()
. The first of these uses a
Config
object to pass relevant gate factories to each pass, and the
second is a further specialization that auto-instantiates these factories based on
a desired entangling gate and mode (which may not be relevant to all passes).
[8]:
# define a compiler to simplify circuits
compiler1 = tq.Compiler.basic(passes=tq.Compiler.SIMPLIFY_PASSES)
# define a compiler that does randomized compiling
compiler2 = tq.Compiler.basic(passes=tq.Compiler.RC_PASSES)
# define a compiler that decomposes two-qubit gates into the specified entangling gate,
# then randomly compiles, and then converts all gates into hardware compatable gates.
# for this example, we use the maximally entangling cross-resonance gate.
passes = (
tq.Compiler.NATIVE2Q_PASSES + tq.Compiler.RC_PASSES + tq.Compiler.HARDWARE_PASSES
)
compiler3 = tq.Compiler.basic(entangler=tq.Gate.rp("ZX", 90), passes=passes)
We demonstrate the last of these compilers by defining the following circuit:
[9]:
circuit = tq.Circuit(
[{range(4): tq.Gate.h}, {(0, 1): tq.Gate.rp("ZZ", 22), (2, 3): tq.Gate.cz}]
).measure_all()
circuit
[9]:
Circuit
|
||||
|
(0):
Gate.h
|
(1):
Gate.h
|
(2):
Gate.h
|
(3):
Gate.h
|
|
(0, 1):
Gate(ZZ)
|
(2, 3):
Gate.cz
|
  | |
1
|
(0):
Meas()
|
(1):
Meas()
|
(2):
Meas()
|
(3):
Meas()
|
We see that the compiled circuit contains only CNOT gates, Z rotations, and X90 rotations. Adjacent single qubit gates have been merged together on each qubit.
[10]:
compiler3.compile(circuit)
[10]:
Circuit
|
||||
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
sx()
|
(1):
sx()
|
(2):
sx()
|
(3):
sx()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
sx()
|
(1):
sx()
|
(2):
sx()
|
(3):
sx()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
2
|
(0, 1):
entangler()
|
  | ||
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
sx()
|
(1):
sx()
|
(2):
sx()
|
(3):
sx()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
sx()
|
(1):
sx()
|
(2):
sx()
|
(3):
sx()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
3
|
(0, 1):
entangler()
|
(2, 3):
entangler()
|
  | |
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
sx()
|
(1):
sx()
|
(2):
sx()
|
(3):
sx()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
sx()
|
(1):
sx()
|
(2):
sx()
|
(3):
sx()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
1
|
(0):
Meas()
|
(1):
Meas()
|
(2):
Meas()
|
(3):
Meas()
|
Custom Pass Lists
Custom lists of passes can be used if none of the predifined lists have the desired
behaviour. We can pass lists of these classes to
from_config()
or
basic()
, as discussed above, or we can use the
compiler constructor directly, as in the following example.
First, we define a device configuration. Here, we use the Berkeley gate as the entangling operation.
[11]:
b = tq.Gate.from_generators("XX", 90, "YY", 45)
config = tq.Config(
factories=[
tq.config.GateFactory.from_matrix("B", b),
tq.config.GateFactory.from_hamiltonian("x90", [["X", 90]]),
tq.config.GateFactory.from_hamiltonian("z", [["Z", "phi"]]),
],
mode="ZXZXZ",
)
Next, we define a compiler. Unlike,
HARDWARE_PASSES
, this compiler starts by
merging adjacent two-qubit gates. Also, we know that any two qubit gate can be
decomposed into two Berkeley gates interleaved with single qubit gates. We use
tq.compilation.NativeDecomp
fixed at depth=2
to force every
two-qubit gate to decompose into two Berkeley gates, even if only one is required.
[12]:
decomposer = tq.compilation.NativeDecomp(depth=2, factories=config.factories)
compiler = tq.Compiler(
[
tq.compilation.Merge(max_sys=2),
tq.compilation.Parallel(decomposer),
tq.compilation.Merge(),
tq.compilation.Native1Q(config.factories),
tq.compilation.RemoveEmptyCycle(),
]
)
Next, we define a circuit to test this compiler on.
[13]:
circuit = tq.Circuit(
[
{range(4): tq.Gate.h},
{(0, 1): tq.Gate.rp("ZZ", 22)},
{(0, 1): tq.Gate.cnot, (2, 3): tq.Gate.swap},
]
).measure_all()
circuit
[13]:
Circuit
|
||||
|
(0):
Gate.h
|
(1):
Gate.h
|
(2):
Gate.h
|
(3):
Gate.h
|
|
(0, 1):
Gate(ZZ)
|
  | ||
|
(0, 1):
Gate.cx
|
(2, 3):
Gate.swap
|
  | |
1
|
(0):
Meas()
|
(1):
Meas()
|
(2):
Meas()
|
(3):
Meas()
|
The resulting compiled circuit is as follows.
[14]:
compiler.compile(circuit)
[14]:
Circuit
|
||||
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
x90()
|
(1):
x90()
|
(2):
x90()
|
(3):
x90()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
x90()
|
(1):
x90()
|
(2):
x90()
|
(3):
x90()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0, 1):
B()
|
(2, 3):
B()
|
  | |
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
x90()
|
(1):
x90()
|
(2):
x90()
|
(3):
x90()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
x90()
|
(1):
x90()
|
(2):
x90()
|
(3):
x90()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0, 1):
B()
|
(2, 3):
B()
|
  | |
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
x90()
|
(1):
x90()
|
(2):
x90()
|
(3):
x90()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
|
(0):
x90()
|
(1):
x90()
|
(2):
x90()
|
(3):
x90()
|
|
(0):
z(phi)
|
(1):
z(phi)
|
(2):
z(phi)
|
(3):
z(phi)
|
1
|
(0):
Meas()
|
(1):
Meas()
|
(2):
Meas()
|
(3):
Meas()
|
Download
Download this file as Jupyter notebook: custom.ipynb.