## Quantum Control Gates in Python

###### Periklis Sakkaris, Jan 03 2016

Suppose you are building a quantum computer in your garage; eventually you will have to build control gates. Let's say you build a control NOT gate (CNOT), how do you know it works? The most efficient way to check your work is to test a small amount of qubits against a classical emulation.

A classical emulation of a quantum computer is when you run quantum algorithms on a classical computer with a normal programming language such as Python (kinda similar to a nintendo emulator, only way cooler). The emulation won't scale, you can only do 10-12 qubits on your computer and maybe push it up to 15-20 qubits on high powered cloud computers. However, it is very useful to test against 2, 3, ... 10 qubits because you can test your outputs to theory. Also, it is a great way to learn quantum computing.

Emulating a quantum gate in Python is basically doing a matrix operation since quantum gates are represented as matrices. In all quantum computing books you read that the CNOT gate is:

$$CNOT = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ \end{bmatrix}$$

But what does this matrix mean? The above matrix means: on a two qubit system (such as |00>, |10>, |11>, etc.) if the first qubit is a one, apply the not gate (X) on the second qubit. That's cool, but what if you have a 4 qubit system (|0101>) or an 8 qubit system (|00010011>) ? Also, let's say on the 8 qubit system you want to apply the X gate on the 7th qubit if the 2nd is one, how would you go about constructing this matrix?

##### Cookbook Method¶

Most books and articles will discuss a cookbook method where you build a matrix based on what the operation does to the base states. Let's work through this for the 2 qubit CNOT gate above. The above gate says if the first qubit is 1 then apply an X to the 2nd qubit. An X just changes the qubit from a 0 to a 1 or a 1 to a 0. The base states for a two qubit system are: |00>, |01>, |10>, |11>

How do I know those are the 4 states? Quantum qubit systems grow as $2^q$ where q = number of qubits. For a two qubit system $q=2$ therefore there are 4 qubit states. Then you count from 0 to 3 in binary. That is all.

First, this is how CNOT operates on the base states:

$$CNOT|00> \rightarrow |00>$$$$CNOT|01> \rightarrow |01>$$$$CNOT|10> \rightarrow |11>$$$$CNOT|11> \rightarrow |10>$$

To build a matrix you use the following trick:

$$CNOT = \begin{bmatrix} <00|CNOT|00> & <00|CNOT|01> & <00|CNOT|10> & <00|CNOT|11> \\ <01|CNOT|00> & <01|CNOT|01> & <01|CNOT|10> & <01|CNOT|11> \\ <10|CNOT|00> & <10|CNOT|01> & <10|CNOT|10> & <10|CNOT|11> \\ <11|CNOT|00> & <11|CNOT|01> & <11|CNOT|10> & <11|CNOT|11> \\ \end{bmatrix} \\$$$$= \begin{bmatrix} <00|00> & <00|01> & <00|11> & <00|10> \\ <01|00> & <01|01> & <01|11> & <01|10> \\ <10|00> & <10|01> & <10|11> & <10|10> \\ <11|00> & <11|01> & <11|11> & <11|10> \\ \end{bmatrix} \\$$$$= \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ \end{bmatrix}$$

This is nice, but now suppose you want a CNOT gate between the 3rd and 9th qubit of a 10 qubit system? You will have $2^{10}$ base states and a $2^{10} \times 2^{10}$ matrix! Good luck with the cookbook method! What we need is an algorithmic method that we can code up in Python.

##### The Algorithmic Method¶

A better algorithmic way to think about quantum control gates is by using operators and tensor products. Suppose we have a two qubit system.

To say "If the first qubit is |0> leave the second qubit alone:": $$|0><0| \otimes I$$ to leave a qubit alone you apply the Identity operator / matrix $I$

To say "If the first qubit is |1> apply X to the second qubit":

$$|1><1| \otimes X$$

Now put them together by adding them, "If the first qubit is |0> leave the second qubit alone and If the first qubit is |1> apply X to the second qubit":

$$|0><0| \otimes I + |1><1| \otimes X$$
In [3]:
from qudotpy import qudot
import numpy as np

zero_matrix = qudot.ZERO.ket * qudot.ZERO.bra
one_matrix = qudot.ONE.ket * qudot.ONE.bra

CNOT = np.kron(zero_matrix, np.eye(2)) + np.kron(one_matrix, qudot.X.matrix)
print(CNOT)

[[ 1.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  1.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  1.+0.j]
[ 0.+0.j  0.+0.j  1.+0.j  0.+0.j]]


Nice! This is a good algorithmic way to make CNOT gates (or control gates in general). For example, suppose now you want a CNOT gate of a 10 qubit system where the 3rd qubit is the control and the 9th qubit is the target:

"If the third qubit is |0> leave the ninth qubit alone:" $$I \otimes I \otimes |0><0| \otimes I \otimes I \otimes I \otimes I \otimes I \otimes I \otimes I$$

"If the third qubit is |1> apply X to the ninth qubit": $$I \otimes I \otimes |1><1| \otimes I \otimes I \otimes I \otimes I \otimes I \otimes X \otimes I$$

Add those two expression together and you have your 10 qubit CNOT gate.

The above algorithm is straightforward to implement in Python. Below is an example from the implementation of QuDotPy


@classmethod
def init_control_gate(cls, qu_gate, control_qubit=1, target_qubit=2, num_qubits=2):
index = 1
# start with 1x1 matrix, a.k.a a number
control_mat = 1
target_mat = 1
# build control and target matrices
while index <= num_qubits:
if index == control_qubit:
control_mat = np.kron(control_mat, ZERO.ket * ZERO.bra)
target_mat = np.kron(target_mat, ONE.ket * ONE.bra)
elif index == target_qubit:
control_mat = np.kron(control_mat, np.eye(2))
target_mat = np.kron(target_mat, qu_gate.matrix)
else:
control_mat = np.kron(control_mat, np.eye(2))
target_mat = np.kron(target_mat, np.eye(2))

index += 1

control_gate = control_mat + target_mat
return QuGate(control_gate)


##### QuDotPy Implementation¶

Using QuDotPy you can leverage this aglorithm to build generalized control gates. For example,

In [9]:
from qudotpy import qudot

CNOT12 = qudot.QuGate.init_control_gate(qudot.X, control_qubit=1, target_qubit=2, num_qubits=2)
print(CNOT12.matrix)

[[ 1.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  1.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  1.+0.j]
[ 0.+0.j  0.+0.j  1.+0.j  0.+0.j]]

In [16]:
CNOT34 = qudot.QuGate.init_control_gate(qudot.X, control_qubit=3, target_qubit=4, num_qubits=4)
print(CNOT34.matrix)

[[ 1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j  0.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j]
[ 0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j
0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  1.+0.j  0.+0.j]]