# NumPy for representing vectors and matrices
import numpy as np
# Matplotlib is used for visualization
import matplotlib.pyplot as plt
This notebook is part of the q4p (Quantum Computing for Programmers) series. The original notebook can be found on Github.
Introduction to complex numbers for quantum computing
This notebook gives you a primer on complex numbers, which are essential to understand in 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 \(a\) and an imaginary part \(b\).
The structure of a complex number (\(\mathbb{C}\)) is \(z = a + bi\), where \(a\) and \(b\) are real numbers (\(a,b \in \mathbb{R}\)), and \(i^2 = -1\).
In the 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\). For mathematical notation we will use \(i\), while in code we will use \(j\).
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.
# Complex number as a standard type in Python
= complex(2 + 1j) z
If we add a j
to the number, Python will automatically create a complex number.
= 2 + 1j
py_complex_number py_complex_number
(2+1j)
With this type we can pick of 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.
= np.complex128(2 + 1j) numpy_complex_number
NumPy allows us more operations to transform complex numbers, which will be used extensively in the q4p course.
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."""
=(6, 4))
plt.figure(figsize= plt.gca()
ax 0, 0, z.real, z.imag, angles="xy", scale_units="xy", scale=1, color="blue", label=f"{z}")
plt.quiver("equal")
ax.set_aspect(-3, 3)
plt.xlim(-3, 3)
plt.ylim(=0, color="k", linestyle="-", linewidth=0.5)
plt.axhline(y=0, color="k", linestyle="-", linewidth=0.5)
plt.axvline(x"Real")
plt.xlabel("Imaginary")
plt.ylabel(True)
plt.grid(
plt.legend()"Complex Number as a 2D Vector")
plt.title(
2 + 1j) plot_complex_number(
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
1 + 0j, 0 + 0j]) np.array([
array([1.+0.j, 0.+0.j])
Complex matrices will be used in quantum computing to represent operators.
# Complex matrix
0, -1j], [1j, 0]]) np.array([[
array([[ 0.+0.j, -0.-1.j],
[ 0.+1.j, 0.+0.j]])
By definition of the complex numbers, \(\sqrt{-1} = i\).
-1 + 0j) np.sqrt(
1j
Make sure that the number is a complex number or you will get a nan
(not a number) from NumPy.
-1) np.sqrt(
/var/folders/d5/1qd2ftjn7ts_5_k6w2k2g4c00000gn/T/ipykernel_73954/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.
= 2 + 1j
z z
(2+1j)
\(r = |z|\) (absolute value of z)
= np.abs(z)
r r
2.23606797749979
\(\theta = arctan(\frac{\text{Im}(z)}{\text{Re}(z)})\). \(Im =\) Imaginary part, \(Re =\) Real part
= np.arctan2(z.imag, z.real)
theta 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.
= r * np.cos(theta) + r * np.sin(theta) * 1j
polar_form 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}\).
= r * np.exp(1j * theta)
exp_form 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)\]
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\]
= (1 / np.sqrt(2)) + (1 / np.sqrt(2)) * 1j
z z
(0.7071067811865475+0.7071067811865475j)
= np.angle(z)
theta theta
0.7853981633974483
First we check that \(z = cos(\theta) + i sin(\theta)\) for a normalized state:
= np.cos(theta) + np.sin(theta) * 1j
norm_polar_form 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:
= np.exp(1j * theta)
norm_exp_form 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\]
= 3 + 2j
z 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]
1 + 2j, 3 + 4j]).conj() np.array([
array([1.-2.j, 3.-4.j])
Conjugate Transpose (\(\dagger\))
Another common quantum computing operation you will encounteris the conjugate transpose (\(\dagger\)). This is simply an extension of the complex conjugate where we take 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}\]
= np.array([[1 + 2j, 3 + 4j]])
vector 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]])
= np.array([[1 + 2j, 3 + 4j], [5 + 6j, 7 + 8j]])
matrix 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]])
And that’s 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
This notebook is part of the q4p (Quantum Computing for Programmers) series. The original notebook can be found on Github.