Quantum gates

You may be familiar with the idea of classical logic gates. Each individual bit in a computer’s memory has either state 0 or state 1. By applying certain operations called logic gates, we can change the values of these bits and hence perform some computation. Examples of these operations include

  • NOT: changes 1 to 0 and 0 to 1,
  • AND: outputs 1 if both inputs are 1, and outputs 0 otherwise,
  • OR: outputs 1 if one or both inputs are 1, and 0 if both inputs are 0, etc

Quantum computation makes use of a similar set of fundamental operations. However, because qubits can exist in superposition, we have a much richer set of states available to us and a much richer selection of gates to choose from. Here we will discover a few of the major ones and write functions to compute them (hint: we’ve already done all the hard work!).

The Pauli gates, X, Y and Z

If you have studied quantum mechanics or complex matrices before you may be familiar with the Pauli matrices, normally labelled \(\sigma_x,\sigma_y,\sigma_z\) or something similar. These are of mathematical interest because they form a basis of the space of \(2\times 2\) complex matrices, and they are useful in quantum computation for similar reasons: they allow us to form any \(2\times 2\) unitary matrix we would like. However they also have a more direct use in their own right. They are represented by the matrices $$\sigma_x = \begin{bmatrix}0&1\\1&0\end{bmatrix},\quad \sigma_y=\begin{bmatrix}0&-i\\i&0\end{bmatrix}, \quad \sigma_z=\begin{bmatrix}1&0\\0&-1\end{bmatrix}.$$ These are usually called simply X, Y and Z in this field. These are all unitary, which I invite you to confirm. The X gate is a direct analogue of the classical NOT operation, which we can see by applying it to our basis vectors: $$\begin{bmatrix}0&1\\1&0\end{bmatrix}\begin{bmatrix}1\\0\end{bmatrix}=\begin{bmatrix}0\\1\end{bmatrix},\qquad \begin{bmatrix}0&1\\1&0\end{bmatrix}\begin{bmatrix}0\\1\end{bmatrix}=\begin{bmatrix}1\\0\end{bmatrix}.$$ The Y and Z gates have similarly straight-forward operation, though no direct classical analogues exist. I’ve given you the matrices so I won’t show all the calculations. I’ll just write the functions. Luckily we don’t really need to do much, since we’re basically just multiplying matrices for which we’ve already written a function. While I’m here I’m going to create a class called matrices, simply to store the more common objects we will need.

class Matrices():
    i = complex(0,1)
    PauliX = [[0,1], [1,0]]
    PauliX = [[0, -i], [i, 0]]
    PauliX = [[1, 0], [0, -1]]

def x(state_vector):
    return matrix_matrix(Matrices.PauliX, state_vector)

def y(state_vector):
    return matrix_matrix(Matrices.PauliY, state_vector)

def z(state_vector):
    return matrix_matrix(Matrices.PauliZ, state_vector)

Hopefully you now see why all that preparation was worthwhile. You’ll see as we advance that it will save us an awful lot of effort in the long run.

The X (NOT) gate is the one that will most frequently be of use to us, though the others certainly have their uses.

The Hadamard gate

The Hadamard gate is an absolutely indispensible weapon in our quantum computing arsenal. You will rarely see a quantum circuit where it doesn’t feature. It’s affect (in the computational basis) is to produce a balanced superposition. The reason that’s so important is because we wouldn’t have access to any quantum behaviour if we couldn’t produce states in superposition. Its affect on the computational basis states is $$H|0\rangle = \frac{1}{\sqrt 2}\begin{bmatrix}1&1\\1&-1\end{bmatrix}\begin{bmatrix}1\\0\end{bmatrix}=\frac{1}{\sqrt 2}\begin{bmatrix}1\\1\end{bmatrix},\\ H|1\rangle = \frac{1}{\sqrt 2}\begin{bmatrix}1&1\\1&-1\end{bmatrix}\begin{bmatrix}0\\1\end{bmatrix}=\frac{1}{\sqrt 2}\begin{bmatrix}1\\-1\end{bmatrix}.$$ The factor \(1/\sqrt 2\) is a normalization constant required to make this matrix unitary. The resulting states occur so frequently and are so useful that they are given the names \(|+\rangle\) and \(|-\rangle\), for hopefully-obvious reasons. Again, all the legwork for building the Hadamard function has been done, and the code for it is simply

Matrices.Hadamard = [
    [1/math.sqrt(2), 1/math.sqrt(2)],
    [1/math.sqrt(2), -1/math.sqrt(2)]

def hadamard(state_vector):
    return matrix_matrix(Matrices.Hadamard, state_vector)

The first four lines simply adds the matrix Hadamard to the Matrices class. If you’re writing your own code to go along with these articles you should declare it within the class itself.

The CNOT gate

CNOT is short for controlled-NOT and it is the last operation we’ll look at for now. It is the first elementary operator which acts on two qubits: the first qubit is the control qubit, and the second qubit is the target qubit. If the first qubit is in state \(|0\rangle\), then nothing happens no matter the state of the second. If the first qubit is in state \(|1\rangle\), then the NOT operation is applied to the second qubit. CNOT is the map $$
|00\rangle \mapsto |00\rangle,\quad
|01\rangle \mapsto |01\rangle,\\
|10\rangle \mapsto |11\rangle,\quad
|11\rangle \mapsto |10\rangle,
and in matrix form it is$$\textrm{CNOT} =
The CNOT is most commonly used to produce entanglement between two qubits; that is, it creates a state wherein the state of the second qubit depends upon the state of the first. In the simplest case, we would apply it to the (non-entangled) superposition state \(|00\rangle+|10\rangle \) in order to produce the entangled state \(|00\rangle+|11\rangle\). I recommend not moving on until you have convinced yourself that this is correct.

The CNOT function is defined just like the others.

Matrices.CNOT = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 1],
    [0, 0, 1, 0]

def CNOT(state_vector):
    return matrix_matrix(Matrices.CNOT, state_vector)


We have previously defined the functions rows(matrix), columns(matrix), add(matrix1, matrix2), subtract(matrix1, matrix2), zeroes(num_rows, num_cols=-1), ones(num_rows, num_cols=-1), eye(size), scalar_matrix(scalar, matrix), get_row(matrix, row_num), get_column(matrix, col_num), normalize(vector), transpose(matrix), conjugate_matrix(matrix), adjoint(matrix), is_unitary(matrix), kronecker(matrix), and have just added

  • x(state_vector),
  • y(state_vector),
  • z(state_vector),
  • hadamard(state_vector),
  • CNOT(state_vector).

Previous: Qubits and quantum circuits