The aim of this short tutorial is to give a first introduction to the ProjectQ compiler and show the use different preconfigured setups. In particular, we will show how to specify the gate set to which the compiler should translate a quantum program. A more extended tutorial will follow soon. Please check out our ProjectQ paper for an introduction to the basic concepts behind our compiler. If you are interested how to compile to a restricted hardware with, e.g., only nearest neighbour connectivity, please have a look at the mapper_tutorial.ipynb
afterwards.
To compile a quantum program, we begin by creating a compiler called MainEngine
and specify the backend for which the compiler should translate the program. For the purpose of this tutorial, we will use a CommandPrinter
as a backend to display the compiled algorithm. It works the same for all other backends such as, e.g., the simulator or an interface to real hardware.
Let's write a small program:
In [1]:
import projectq
from projectq.backends import CommandPrinter
from projectq.meta import Control
from projectq.ops import All, CNOT, Measure, QFT, QubitOperator, Rx, TimeEvolution, X
# create the compiler and specify the backend:
eng = projectq.MainEngine(backend=CommandPrinter(accept_input=False))
def my_quantum_program(eng):
qubit = eng.allocate_qubit()
qureg = eng.allocate_qureg(3)
with Control(eng, qubit):
hamiltonian = 0.5 * QubitOperator("X0 Y1 Z2")
TimeEvolution(0.1, hamiltonian) | qureg
QFT | qureg
Rx(0.1) | qubit
CNOT | (qubit, qureg[0])
All(Measure) | qureg
Measure | qubit
eng.flush()
my_quantum_program(eng)
In the above example, the compiler did nothing because the default compiler (when MainEngine
is called without a specific engine_list
parameter) translates the individual gates to the gate set supported by the backend. In our case, the backend is a CommandPrinter
which supports any type of gate.
We can check what happens when the backend is a Simulator
by inserting a CommandPrinter
as a last compiler engine before the backend so that every command is printed before it gets sent to the Simulator:
In [2]:
from projectq.backends import Simulator
from projectq.setups.default import get_engine_list
# Use the default compiler engines with a CommandPrinter in the end:
engines2 = get_engine_list() + [CommandPrinter()]
eng2 = projectq.MainEngine(backend=Simulator(), engine_list=engines2)
my_quantum_program(eng2)
As one can see, in this case the compiler had to do a little work because the Simulator does not support a QFT gate. Therefore, it automatically replaces the QFT gate by a sequence of lower-level gates.
ProjectQ's compiler is fully modular, so one can easily build a special purpose compiler. All one has to do is compose a list of compiler engines through which the individual operations will pass in a serial order and give this compiler list to the MainEngine
as the engine_list
parameter.
For common compiler needs we try to provide predefined "setups" which contain a function get_engine_list
which returns a suitable list of compiler engines for the MainEngine
. All of our current setups can be found in projectq.setups. For example there is a setup called restrictedgateset
which allows to compile to common restricted gate sets. This is useful, for example, to obtain resource estimates for running a given program on actual quantum hardware which does not support every quantum gate. Let's look at an example:
In [3]:
import projectq
from projectq.setups import restrictedgateset
from projectq.ops import All, H, Measure, Rx, Ry, Rz, Toffoli
engine_list3 = restrictedgateset.get_engine_list(one_qubit_gates="any",
two_qubit_gates=(CNOT,),
other_gates=(Toffoli,))
eng3 = projectq.MainEngine(backend=CommandPrinter(accept_input=False),
engine_list=engine_list3)
def my_second_program(eng):
qubit = eng3.allocate_qubit()
qureg = eng3.allocate_qureg(3)
H | qubit
Rx(0.3) | qubit
Toffoli | (qureg[:-1], qureg[2])
QFT | qureg
All(Measure) | qureg
Measure | qubit
eng.flush()
my_second_program(eng3)
Please have a look at the documention of the restrictedgateset for details. The above compiler compiles the circuit to gates consisting of any single qubit gate, the CNOT
and Toffoli
gate. The gate specifications can either be a gate class, e.g., Rz
or a specific instance Rz(math.pi)
. A smaller but still universal gate set would be for example CNOT
and Rz, Ry
:
In [4]:
engine_list4 = restrictedgateset.get_engine_list(one_qubit_gates=(Rz, Ry),
two_qubit_gates=(CNOT,),
other_gates=())
eng4 = projectq.MainEngine(backend=CommandPrinter(accept_input=False),
engine_list=engine_list4)
my_second_program(eng4)
As mentioned in the documention of this setup, one cannot (yet) choose an arbitrary gate set but there is a limited choice. If it doesn't work for a specified gate set, the compiler will either raises a NoGateDecompositionError
or a RuntimeError: maximum recursion depth exceeded...
which means that for this particular choice of gate set, one would be required to write more decomposition rules to make it work. Also for some choice of gate set there might be compiler engines producing more optimal code.
In this short example, we want to look at how to build an own compiler engine_list
for compiling to a restricted gate set. Please have a look at the predefined setups for guidance.
One of the important compiler engines to change the gate set is the AutoReplacer
. It queries the following engines to check if a particular gate is supported and if not, it will use decomposition rules to change this gate to supported ones. Most engines just forward this query to the next engine until the backend is reached. The engine after an AutoReplacer
is usually a TagRemover
which removes previous tags in commands such as, e.g., ComputeTag
which allows a following LocalOptimizer
to perform more optimizations (otherwise it would only optimize within a "compute" section and not over the boundaries).
To specify different intermediate gate sets, one can insert an InstructionFilter
into the engine_list
after the AutoReplacer
in order to return True
or False
for the queries of the AutoReplacer
asking if a specific gate is supported.
Here is a minimal example of a compiler which compiles to CNOT and single qubit gates but doesn't perform optimizations (which could be achieved using the LocalOptimizer
). For the more optimal versions, have a look at the restrictricedgateset
setup:
In [5]:
import projectq
from projectq.backends import CommandPrinter
from projectq.cengines import AutoReplacer, DecompositionRuleSet, InstructionFilter
from projectq.ops import All, ClassicalInstructionGate, Measure, Toffoli, X
import projectq.setups.decompositions
# Write a function which, given a Command object, returns whether the command is supported:
def is_supported(eng, cmd):
if isinstance(cmd.gate, ClassicalInstructionGate):
# This is required to allow Measure, Allocate, Deallocate, Flush
return True
elif isinstance(cmd.gate, X.__class__) and len(cmd.control_qubits) == 1:
# Allows a CNOT gate which is an X gate with one control qubit
return True
elif (len(cmd.control_qubits) == 0 and
len(cmd.qubits) == 1 and
len(cmd.qubits[0]) == 1):
# Gate which has no control qubits, applied to 1 qureg consisting of 1 qubit
return True
else:
return False
#is_supported("test", "eng")
rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions])
engine_list5 = [AutoReplacer(rule_set), InstructionFilter(is_supported)]
eng5 = projectq.MainEngine(backend=CommandPrinter(accept_input=False),
engine_list=engine_list5)
def my_third_program(eng):
qubit = eng5.allocate_qubit()
qureg = eng5.allocate_qureg(3)
Toffoli | (qureg[:2], qureg[2])
QFT | qureg
All(Measure) | qureg
Measure | qubit
eng5.flush()
my_third_program(eng5)
In [ ]: