The aim of this short tutorial is to give an introduction to the ProjectQ mappers.
ProjectQ allows a user to write a quantum program in a high-level language. For example, one can apply quantum operations on n-qubits, e.g., QFT
, and the compiler will decompose this operations into two-qubit and single-qubit gates. See the compiler_tutorial for an introduction.
After decomposing a quantum program into two-qubit and single-qubit gates which a quantum computer supports, we have to take the physical layout of these qubits into account. Two-qubit gates are only possible if the qubits are next to each other. For example the qubits could be arranged in a linear chain or a two-dimensional grid and only nearest neighbour qubits can perform a two-qubit gate. ProjectQ uses mappers which move the positions of the qubits close to each other using Swap
operations in order that we can execute a two-qubit gate.
The implementation and some results of ProjectQ's mappers are discussed in our paper (section 3C)
In [1]:
import projectq
from projectq.backends import ResourceCounter
from projectq.ops import CNOT, QFT
from projectq.setups import restrictedgateset
engine_list = restrictedgateset.get_engine_list(one_qubit_gates="any",
two_qubit_gates=(CNOT,))
resource_counter = ResourceCounter()
eng = projectq.MainEngine(backend=resource_counter, engine_list=engine_list)
qureg = eng.allocate_qureg(15)
QFT | qureg
eng.flush()
print(resource_counter)
Now let's assume our qubits are arrange on a linear chain, we can use an already predefined compiler setup to compile to this architecture:
In [2]:
from projectq.setups import linear
engine_list2 = linear.get_engine_list(num_qubits=15, cyclic=False,
one_qubit_gates="any",
two_qubit_gates=(CNOT,))
resource_counter2 = ResourceCounter()
eng2 = projectq.MainEngine(backend=resource_counter2, engine_list=engine_list2)
qureg2 = eng2.allocate_qureg(15)
QFT | qureg2
eng2.flush()
print(resource_counter2)
One can see that once we restricted the hardware to a linear chain, the same program requires a lot more CNOT
(also called CX
) gates. This is due to additionals Swap
operations to move the qubits around (a Swap
gate can be constructed out of three CX
gates).
ProjectQ also has a predefined setup to map to a two-dimensional grid.
In [3]:
from projectq.setups import grid
engine_list3 = grid.get_engine_list(num_rows=3, num_columns=5,
one_qubit_gates="any",
two_qubit_gates=(CNOT,))
resource_counter3 = ResourceCounter()
eng3 = projectq.MainEngine(backend=resource_counter3, engine_list=engine_list3)
qureg3 = eng3.allocate_qureg(15)
QFT | qureg3
eng3.flush()
print(resource_counter3)
We can see that mapping a QFT
to a two-dimensional grid layout requires fewer CX
gates than mapping to a linear chain as expected.
In [4]:
from projectq.backends import CommandPrinter
from projectq.ops import X, Swap
engine_list4 = linear.get_engine_list(num_qubits=3, cyclic=False,
one_qubit_gates="any",
two_qubit_gates=(CNOT, Swap))
eng4 = projectq.MainEngine(backend=CommandPrinter(), engine_list=engine_list4)
# For instructional purposes we change that the eng4 gives logical ids starting
# from 10. This could e.g. be the case if a previous part of the program
# already allocated 10 qubits
eng4._qubit_idx = 10
qubit0 = eng4.allocate_qubit()
qubit1 = eng4.allocate_qubit()
qubit2 = eng4.allocate_qubit()
X | qubit0
# Remember that allocate_qubit returns a quantum register (Qureg) of size 1,
# so accessing the qubit requires qubit[0]
print("This logical qubit0 has the unique ID: {}".format(qubit0[0].id))
print("This logical qubit1 has the unique ID: {}".format(qubit1[0].id))
print("This logical qubit2 has the unique ID: {}".format(qubit2[0].id))
eng4.flush()
As we can see qubit0
has a logical ID equal to 10. The LinearMapper in this compiler setup then places these qubits on a linear chain with the following physical qubit ID ordering:
0 -- 1 -- 2
where -- indicates that these two qubits can perform a CNOT
gate. If you are interested in knowing where a specific logical qubit is currently placed, you can access this information via the current_mapping
property of the mapper:
In [5]:
# eng.mapper gives back the mapper in the engine_list
current_mapping = eng4.mapper.current_mapping
# current_mapping is a dictionary with keys being the
# logical qubit ids and the values being the physical ids on
# on the linear chain
print("Physical location of qubit0: {}".format(current_mapping[qubit0[0].id]))
print("Physical location of qubit1: {}".format(current_mapping[qubit1[0].id]))
print("Physical location of qubit2: {}".format(current_mapping[qubit2[0].id]))
Suppose we now perform a CNOT
between qubit0
and qubit2
, then the mapper needs to swap these two qubits close to each other to perform the operation:
In [6]:
CNOT | (qubit0, qubit2)
eng4.flush()
# Get current mapping:
current_mapping = eng4.mapper.current_mapping
print("\nPhysical location of qubit0: {}".format(current_mapping[qubit0[0].id]))
print("Physical location of qubit1: {}".format(current_mapping[qubit1[0].id]))
print("Physical location of qubit2: {}".format(current_mapping[qubit2[0].id]))
We see that the compiler added a Swap
gate to change the location of the logical qubits in this chain so that the CNOT can be performed.
While the compiler automatically remaps logical qubits to different physical locations, how does this affect the high-level programmer?
The short answer is not at all.
If you want to measure a logical qubit, just apply a measurement gate as before and the compiler will automatically find the correct physical qubit to measure:
In [7]:
from projectq.backends import Simulator
from projectq.ops import Measure
engine_list5 = linear.get_engine_list(num_qubits=3, cyclic=False,
one_qubit_gates="any",
two_qubit_gates=(CNOT, Swap))
eng5 = projectq.MainEngine(backend=Simulator(), engine_list=engine_list5)
qubit0 = eng5.allocate_qubit()
qubit1 = eng5.allocate_qubit()
qubit2 = eng5.allocate_qubit()
X | qubit0
Measure | qubit0
eng5.flush()
print("qubit0 was measured in state: {}".format(int(qubit0)))
All the simulator functionalities, e.g., get_probability
or get_amplitude
work as usual because they take logical qubits as arguments so the programmer does not need to worry about at which physical location qubit0
is at the moment:
In [8]:
eng5.backend.get_probability('1', qubit0)
Out[8]:
In [ ]: