Hosted with nbsanity. See source notebook on GitHub.


This notebook is part of the q4p (Quantum Computing for Programmers) series. The original notebook can be found on Github.


Introduction to complex numbers

This notebook gives you a primer on complex numbers, which are an essential part of quantum computing. Even if you are already familiar with them, this can still serve as a good refresher before diving into the q4p course.

Complex number representations

Complex numbers are numbers with two components: a real part \(x\) and an imaginary part \(y\). We can visually represent this as a point on a 2D plane.

The structure of a complex number can be defined as (\(\mathbb{C}\)) is \(z = x + yi\), where \(x\) and \(y\) are real numbers (\(x,y \in \mathbb{R}\)), and \(i^2 = -1\).

In Python we can use the built-in complex type to create a complex number. For example, below we create the complex number \(2 + 2i\). In Python and most of engineering \(j\) is used instead of \(i\). Be aware that \(i\) and \(j\) refer to the same imaginary component.

A definition of \(i\) you will often see is \(\sqrt{-1} = i\). This property allows us to take the square root of negative numbers, which is convenient as it extends our number system to handle calculations that would otherwise be undefined. For example, equations like \(x^2 = -1\) can now be solved. Quantum states and transformations are defined using complex numbers, so it is important to understand how they work. Below we will analyze the properties of complex numbers in-depth.

# NumPy for representing vectors and matrices
import numpy as np
# Matplotlib for visualization
import matplotlib.pyplot as plt
# Complex number as a standard type in Python
z = complex(2 + 1j)

If we add a j to the number, Python will automatically create a complex number.

py_complex_number = 2 + 1j
py_complex_number
(2+1j)

From this Python object we can select the real and imaginary parts.

py_complex_number.real, py_complex_number.imag
(2.0, 1.0)

In the q4p course we will mainly use NumPy to create complex numbers, vectors and matrices. The standard NumPy type for complex numbers is numpy.complex128.

numpy_complex_number = np.complex128(2 + 1j)

NumPy has more operations to transform complex numbers. It also allows us to create complex vectors and matrices.

numpy_complex_number.real, numpy_complex_number.imag
(2.0, 1.0)

A complex number can be viewed as a vector in 2 dimensions. This intuition will be very helpful when we start working with quantum states. Below you can see that \(2+1i\) points to \(2\) on the X-axis and \(1\) on the Y-axis.

def plot_complex_number(z: complex):
    """Plot complex numbers on 2 dimensions."""
    plt.figure(figsize=(6, 4))
    ax = plt.gca()
    plt.quiver(0, 0, z.real, z.imag, angles="xy", scale_units="xy", scale=1, color="blue", label=f"{z}")
    ax.set_aspect("equal")
    plt.xlim(-3, 3)
    plt.ylim(-3, 3)
    plt.axhline(y=0, color="k", linestyle="-", linewidth=0.5)
    plt.axvline(x=0, color="k", linestyle="-", linewidth=0.5)
    plt.xlabel("Real")
    plt.ylabel("Imaginary")
    plt.grid(True)
    plt.legend()
    plt.title("Complex Number as a 2D Vector")


plot_complex_number(2 + 1j)

Creating a vector out of multiple complex numbers adds to the dimensions. In quantum computing we will mainly use vectors to represent statevectors.

# Complex vector
np.array([1 + 0j, 0 + 0j])
array([1.+0.j, 0.+0.j])

Complex matrices are used in quantum computing to represent operators.

# Complex matrix
np.array([[0, -1j], [1j, 0]])
array([[ 0.+0.j, -0.-1.j],
       [ 0.+1.j,  0.+0.j]])

By definition of the complex numbers, \(\sqrt{-1} = i\).

np.sqrt(-1 + 0j)
1j

Make sure that the number is a complex number or you will get a nan (not a number) from NumPy.

np.sqrt(-1)
/var/folders/d5/1qd2ftjn7ts_5_k6w2k2g4c00000gn/T/ipykernel_71088/3438155168.py:1: RuntimeWarning: invalid value encountered in sqrt
  np.sqrt(-1)
nan

Polar form

A complex number can be decomposed as follows using a Polar form:

\[z = a+bi = r cos(\theta) + r sin(\theta) i\]

where \(r = |z|\) is the magnitude of the complex number and \(\theta = \arctan(\frac{\text{Im}(z)}{\text{Re}(z)})\) is the angle (theta) in radians.

Let’s decompose the complex number \(2+1j\) into its polar form and verify that the formula is correct.

z = 2 + 1j
z
(2+1j)

\(r = |z|\) (absolute value of z)

r = np.abs(z)
r
2.23606797749979

\(\theta = arctan(\frac{\text{Im}(z)}{\text{Re}(z)})\). \(Im =\) Imaginary part, \(Re =\) Real part

theta = np.arctan2(z.imag, z.real)
theta
0.4636476090008061

A simplified way to calculate \(arctan(\frac{\text{Im}(z)}{\text{Re}(z)})\) is to use NumPy’s angle function.

np.angle(z)
0.4636476090008061

We can see that \(z = r cos(\theta) + r sin(\theta) i\). np.allclose is used to check two values. np.allclose is True if the values are the same and False otherwise.

polar_form = r * np.cos(theta) + r * np.sin(theta) * 1j
polar_form
(2+1j)
np.allclose(z, polar_form)
True

Exponential form

Another more compact way to represent a complex number is using its exponential form:

\[z = re^{i\theta}\]

Let’s also verify that this identity is correct.

z
(2+1j)

We have already calculated \(r\) and \(\theta\) above. We show that indeed \(z = re^{i\theta}\).

exp_form = r * np.exp(1j * theta)
exp_form
(2+1j)
np.allclose(z, exp_form)
True

Normalized States

In quantum computing we generally only encounter vectors where \(r=1\), since quantum states must be normalized. This means that we can simplify our equations to:

\[z = e^{i\theta} = cos(\theta) + i sin(\theta)\]

A benefit of normalized complex numbers is that they can be visualized on a unit circle, like so:

#

An example of a normalized state you will encounter in quantum computing is the superposition of two states:

\[z = \frac{1}{\sqrt{2}} + \frac{1}{\sqrt{2}}i\]

This is the case because \(\sqrt{a^2 + b^2} = 1\) with \(z = a + bi\):

\[|z| = \sqrt{\left(\frac{1}{\sqrt{2}}\right)^2 + \left(\frac{1}{\sqrt{2}}\right)^2} = \sqrt{\frac{1}{2} + \frac{1}{2}} = \sqrt{1} = 1\]

z = (1 / np.sqrt(2)) + (1 / np.sqrt(2)) * 1j
z
(0.7071067811865475+0.7071067811865475j)
theta = np.angle(z)
theta
0.7853981633974483

First we check that \(z = cos(\theta) + i sin(\theta)\) for a normalized state:

norm_polar_form = np.cos(theta) + np.sin(theta) * 1j
norm_polar_form
(0.7071067811865476+0.7071067811865475j)
np.allclose(z, norm_polar_form)
True

Now we check that \(z = e^{i\theta}\) for a normalized state:

norm_exp_form = np.exp(1j * theta)
norm_exp_form
(0.7071067811865476+0.7071067811865475j)
np.allclose(z, norm_exp_form)
True

Complex Conjugate (\(*\))

An operation we often encounter in quantum computing is the complex conjugate (\(*\)). This simply means that the complex part of the number is flipped. With NumPy we can use the np.conj function to get the complex conjugate.

\[z = a + bi\]

\[z^* = a - bi\]

z = 3 + 2j
z
(3+2j)
# 3+2j -> 3-2j
np.conj(z)
(3-2j)

For NumPy numbers and arrays we can call .conj() directly on the object.

# 3+2j -> 3-2j
np.complex128(z).conj()
(3-2j)
# [1+2j, 3+4j] -> [1-2j, 3-4j]
np.array([1 + 2j, 3 + 4j]).conj()
array([1.-2.j, 3.-4.j])

Conjugate Transpose (\(\dagger\))

Another common quantum computing operation you will encounter is the conjugate transpose (\(\dagger\)). This is simply an extension of the complex conjugate where we take both the complex conjugate (\(*\)) and the transpose \(^T\). The conjugate transpose is formally denoted by a dagger (\(\dagger\)) and is sometimes called the “Dagger operation”. In quantum mechanics you may also encounter the terms “Hermitian conjugate” or “Hermitian adjoint” for this operation. In NumPy we simply add .T to our complex conjugate (.conj().T) to get the conjugate transpose.

For vectors the conjugate transpose simplify flips a row vector into a column vector and vice versa. It also flips the sign of the imaginary part (\(*\)): \[v = \begin{bmatrix} a_1 & a_2 & \cdots & a_n \end{bmatrix}\]

\[v^\dagger = \begin{bmatrix} a_1^* \\ a_2^* \\ \vdots \\ a_n^* \end{bmatrix}\]

For matrices the conjugate transpose flips the rows and columns and again flips the sign of the imaginary part (\(*\)):

\[A = \begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \end{bmatrix}\]

\[A^\dagger = \begin{bmatrix} a_{11}^* & a_{21}^* & \cdots & a_{n1}^* \\ a_{12}^* & a_{22}^* & \cdots & a_{n2}^* \\ \vdots & \vdots & \ddots & \vdots \\ a_{1n}^* & a_{2n}^* & \cdots & a_{nn}^* \end{bmatrix}\]

vector = np.array([[1 + 2j, 3 + 4j]])
vector
array([[1.+2.j, 3.+4.j]])
# [[1+2j, 3+4j]] -> [[1-2j,
#                    [3-4j]]
vector.conj().T
array([[1.-2.j],
       [3.-4.j]])
matrix = np.array([[1 + 2j, 3 + 4j], [5 + 6j, 7 + 8j]])
matrix
array([[1.+2.j, 3.+4.j],
       [5.+6.j, 7.+8.j]])
# [[1+2j, 3+4j] -> [[1-2j, 5-6j]
#  [5+6j, 7+8j]]    [3-4j, 7-8j]]
matrix.conj().T
array([[1.-2.j, 5.-6.j],
       [3.-4.j, 7.-8.j]])

That is the core of what you should know about complex numbers to get started with quantum computing! For additional video instruction I would recommend the following videos: - Quantum Computing Course: 0.1 Introduction to Imaginary and Complex Numbers - Quantum Computing Course: 0.2 Complex Numbers on the Number Plane - Imaginary numbers aren’t imaginary - Complex numbers aren’t complex - The Square Root of Negative One - Complex Numbers in Quantum Mechanics

Continue with the q4p course: - On Github - On Kaggle

If you are viewing this notebook on Kaggle, consider giving an upvote and leaving a comment. Your feedback is very welcome and I will try to implement your suggestions in this notebook.

If you are viewing this notebook on Github, please consider giving a star to the repository.


This notebook is part of the q4p (Quantum Computing for Programmers) series. The original notebook can be found on Github.