# NumPy for representing vectors and matrices
import numpy as np
# Matplotlib 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
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.
# 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)
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
.
= np.complex128(2 + 1j) numpy_complex_number
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."""
=(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 are 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_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.
= 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)\]
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\]
= (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 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}\]
= 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]])
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.