ProjectQ Compiler Tutorial

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.

The default compiler

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)


Allocate | Qureg[1]
Allocate | Qureg[0]
Allocate | Qureg[2]
Allocate | Qureg[3]
Cexp(-0.1j * (0.5 X0 Y1 Z2)) | ( Qureg[0], Qureg[1-3] )
QFT | Qureg[1-3]
Rx(0.1) | Qureg[0]
CX | ( Qureg[0], Qureg[1] )
Measure | Qureg[1]
Measure | Qureg[2]
Measure | Qureg[3]
Measure | Qureg[0]
Deallocate | Qureg[0]
Deallocate | Qureg[3]
Deallocate | Qureg[2]
Deallocate | Qureg[1]

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)


Allocate | Qureg[1]
Allocate | Qureg[0]
Allocate | Qureg[2]
Allocate | Qureg[3]
Cexp(-0.1j * (0.5 X0 Y1 Z2)) | ( Qureg[0], Qureg[1-3] )
H | Qureg[3]
CR(1.5707963268) | ( Qureg[2], Qureg[3] )
CR(0.785398163397) | ( Qureg[1], Qureg[3] )
H | Qureg[2]
CR(1.5707963268) | ( Qureg[1], Qureg[2] )
H | Qureg[1]
Rx(0.1) | Qureg[0]
CX | ( Qureg[0], Qureg[1] )
Measure | Qureg[1]
Measure | Qureg[2]
Measure | Qureg[3]
Measure | Qureg[0]
Deallocate | Qureg[0]
Deallocate | Qureg[3]
Deallocate | Qureg[2]
Deallocate | Qureg[1]

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.

Using a provided setup and specifying a particular gate set

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)


Allocate | Qureg[3]
Allocate | Qureg[1]
Allocate | Qureg[2]
CCX | ( Qureg[1-2], Qureg[3] )
H | Qureg[3]
Rz(0.785398163398) | Qureg[3]
R(0.785398163398) | Qureg[2]
CX | ( Qureg[2], Qureg[3] )
Rz(11.780972451) | Qureg[3]
CX | ( Qureg[2], Qureg[3] )
R(0.392699081698) | Qureg[1]
Rz(0.392699081698) | Qureg[3]
CX | ( Qureg[1], Qureg[3] )
H | Qureg[2]
Rz(12.1736715327) | Qureg[3]
CX | ( Qureg[1], Qureg[3] )
R(0.785398163398) | Qureg[1]
Rz(0.785398163398) | Qureg[2]
CX | ( Qureg[1], Qureg[2] )
Rz(11.780972451) | Qureg[2]
CX | ( Qureg[1], Qureg[2] )
H | Qureg[1]
Measure | Qureg[1]
Measure | Qureg[2]
Measure | Qureg[3]
Allocate | Qureg[0]
H | Qureg[0]
Rx(0.3) | Qureg[0]
Measure | Qureg[0]
Deallocate | Qureg[0]
Deallocate | Qureg[3]
Deallocate | Qureg[2]
Deallocate | Qureg[1]

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)


Allocate | Qureg[7]
Allocate | Qureg[5]
Allocate | Qureg[6]
CCX | ( Qureg[5-6], Qureg[7] )
H | Qureg[7]
Rz(0.785398163398) | Qureg[7]
R(0.785398163398) | Qureg[6]
CX | ( Qureg[6], Qureg[7] )
Rz(11.780972451) | Qureg[7]
CX | ( Qureg[6], Qureg[7] )
R(0.392699081698) | Qureg[5]
Rz(0.392699081698) | Qureg[7]
CX | ( Qureg[5], Qureg[7] )
H | Qureg[6]
Rz(12.1736715327) | Qureg[7]
CX | ( Qureg[5], Qureg[7] )
R(0.785398163398) | Qureg[5]
Rz(0.785398163398) | Qureg[6]
CX | ( Qureg[5], Qureg[6] )
Rz(11.780972451) | Qureg[6]
CX | ( Qureg[5], Qureg[6] )
H | Qureg[5]
Measure | Qureg[5]
Measure | Qureg[6]
Measure | Qureg[7]
Allocate | Qureg[4]
H | Qureg[4]
Rx(0.3) | Qureg[4]
Measure | Qureg[4]
Deallocate | Qureg[4]
Deallocate | Qureg[7]
Deallocate | Qureg[6]
Deallocate | Qureg[5]

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.

Error messages

By default the MainEngine shortens error messages as most often this is enough information to find the error. To see the full error message one can to set verbose=True, i.e.: MainEngine(verbose=True)

DIY: Build a compiler engine list for a specific gate set

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)


Allocate | Qureg[0]
Allocate | Qureg[1]
Allocate | Qureg[2]
Allocate | Qureg[3]
H | Qureg[3]
CX | ( Qureg[1], Qureg[3] )
T | Qureg[1]
T^\dagger | Qureg[3]
CX | ( Qureg[2], Qureg[3] )
CX | ( Qureg[2], Qureg[1] )
T^\dagger | Qureg[1]
T | Qureg[3]
CX | ( Qureg[2], Qureg[1] )
CX | ( Qureg[1], Qureg[3] )
T^\dagger | Qureg[3]
CX | ( Qureg[2], Qureg[3] )
T | Qureg[3]
T | Qureg[2]
H | Qureg[3]
H | Qureg[3]
R(0.785398163398) | Qureg[2]
Rz(0.785398163398) | Qureg[3]
CX | ( Qureg[2], Qureg[3] )
Rz(11.780972451) | Qureg[3]
CX | ( Qureg[2], Qureg[3] )
R(0.392699081698) | Qureg[1]
Rz(0.392699081698) | Qureg[3]
CX | ( Qureg[1], Qureg[3] )
Rz(12.1736715327) | Qureg[3]
CX | ( Qureg[1], Qureg[3] )
H | Qureg[2]
R(0.785398163398) | Qureg[1]
Rz(0.785398163398) | Qureg[2]
CX | ( Qureg[1], Qureg[2] )
Rz(11.780972451) | Qureg[2]
CX | ( Qureg[1], Qureg[2] )
H | Qureg[1]
Measure | Qureg[1]
Measure | Qureg[2]
Measure | Qureg[3]
Measure | Qureg[0]
Deallocate | Qureg[0]
Deallocate | Qureg[3]
Deallocate | Qureg[2]
Deallocate | Qureg[1]

In [ ]: