Overview and Learning Objectives
As often in mathematics, transferring a problem from the real into the complex world can lead to significant simplifications. At first sight, this may seem a bit surprising since complex numbers are more difficult to understand than real numbers. As an application of complex numbers, let us consider the problem of finding solutions to polynomial equations. The equation $z^2-1=0$ has the two solutions $z=+1$ and $z=-1$ while the equation $z^2+1=0$ does not have any solution when only considering real numbers. Extending $\mathbb{R}$ (the space of real numbers) to $\mathbb{C}$ (the space of complex numbers), however, one also finds for the second equation two solutions given by $z=+i$ and $z=-i$, where $i$ denotes the complex unit. In other words, considering polynomial equations over $\mathbb{C}$ (rather than $\mathbb{R}$) makes the problem much easier to understand. In general, an extension of the real numbers to the complex numbers not only gives a broader view but also provides additional tools and structures. We will encounter another application for complex numbers in Unit 7, where we study a complex extension of the exponential function and its relation to trigonometric identities.
In this unit, we review the basic properties of complex numbers. In particular, we provide Python code examples for visualizing complex numbers using either Cartesian coordinates or polar coordinates. Such visualizations, while being a nice application of the library matplotlib
introduced in Unit 5, should help you gain a geometric understanding of complex numbers and the effect of their algebraic operations. In Exercise 1, you will apply previously introduced Python code to rotate complex numbers and visualize the effect. Then, in Exercise 2, we address the problem of finding the roots of a given polynomial using the NumPy function np.roots
. The roots' visualizations will give you a feeling of how the roots distribute in the complex plane depending on the polynomials' coefficients. As another application of complex numbers, we discuss in Exercise 3 how to generate the Mandelbrot set, which is a famous and one of the most beautiful examples for a fractal set. When going through this unit, we recommend that you do the first two exercises while the third exercise is left as a playground for exploring the beauty of fractals and the power of visualizations (e.g., tweaking around with color maps).
Basic Definitions¶
We can write a complex number $c = a + ib$ with real part $\mathrm{Re}(c) = a$, imaginary part $\mathrm{Im}(c) = b$, and imaginary unit $i = \sqrt{-1}$. In Python, the symbol j
is used to denote the imaginary unit. Furthermore, a coefficient before j
is needed. To specify a complex number, one can also use the constructor complex
.
a = 1.5
b = 0.8
c = a + b*1j
print('c = ', c, ', type(c) = ', type(c))
c2 = complex(a,b)
print('c2 = ', c2, ', type(c2) = ', type(c2))
Python offers the built-in math
package for basic processing of complex numbers. As an alternative, we use here the external package numpy
, which was introduced in the PCP notebook on NumPy Basics.
import numpy as np
print(np.real(c))
print(np.imag(c))
A complex number $c = a+ib$ can be plotted as a point $(a,b)$ in the Cartesian coordinate system. This point is often visualized by an arrow starting at $(0,0)$ and ending at $(a,b)$. The next code cell serves the following purposes:
- We provide a function
plot_vector
for plotting such an arrow for a given complex number $c$. - We provide a function
generate_figure
used to open a figure with adjusted x- and y-axes. - We show how to apply the functions and how to place text elements in the figure.
from matplotlib import pyplot as plt
%matplotlib inline
def generate_figure(figsize=(2, 2), xlim=[0, 1], ylim=[0, 1]):
"""Generate figure for plotting complex numbers
Notebook: PCP_06_complex.ipynb
Args:
figsize: Width, height in inches (Default value = (2, 2))
xlim: Limits for x-axis (Default value = [0, 1])
ylim: Limits for y-axis (Default value = [0, 1])
"""
plt.figure(figsize=figsize)
plt.grid()
plt.xlim(xlim)
plt.ylim(ylim)
plt.xlabel('$\mathrm{Re}$')
plt.ylabel('$\mathrm{Im}$')
def plot_vector(c, color='k', start=0, linestyle='-'):
"""Plot arrow corresponding to difference of two complex numbers
Notebook: PCP_06_complex.ipynb
Args:
c: Complex number
color: Color of arrow (Default value = 'k')
start: Complex number encoding the start position (Default value = 0)
linestyle: Linestyle of arrow (Default value = '-')
Returns:
plt.arrow: matplotlib.patches.FancyArrow
"""
return plt.arrow(np.real(start), np.imag(start), np.real(c), np.imag(c),
linestyle=linestyle, head_width=0.05,
fc=color, ec=color, overhang=0.3, length_includes_head=True)
c = 1.5 + 0.8j
generate_figure(figsize=(7.5, 3), xlim=[0, 2.5], ylim=[0, 1])
v = plot_vector(c, color='k')
plt.text(1.5, 0.8, '$c$', size='16')
plt.text(0.8, 0.55, '$|c|$', size='16')
plt.text(0.25, 0.05, '$\gamma$', size='16');
Polar Representation¶
The absolute value (or modulus) of a complex number $a+ib$ is defined by
$$|c| := \sqrt{a^2 + b^2}.$$
The angle (given in radians) is given by
$$\gamma := \mathrm{atan2}(b, a).$$
This yields a number in the interval $(-\pi,\pi]$, which can be mapped to $[0,2\pi)$ by adding $2\pi$ to negative values. The angle (given in degrees) is obtained by
$$360 \cdot \frac{\gamma}{2\pi}.$$
The complex number $c=a+ib$ is uniquely defined by the pair $(|c|, \gamma)$, which is also called the polar representation of $c$. One obtains the Cartesian representation $(a,b)$ from the polar representation $(|c|,\gamma)$ as follows:
\begin{eqnarray} a &=& |c| \cdot \cos(\gamma) \\ b &=& |c| \cdot \sin(\gamma) \end{eqnarray}
In the following code cell, we introduce some NumPy-functions for computing the absolute values and angle of a complex number.
c = 1.5 + 0.8j
print('c = :', c)
print('Absolute value:', np.abs(c))
print('Angle (in radians):', np.angle(c))
print('Angle (in degree):', np.rad2deg(np.angle(c)))
print('Angle (in degree):', 180 * np.angle(c) / np.pi )
print(f'Cartesian representation: ({np.real(c)}, {np.imag(c)})')
print(f'Polar representation: ({np.abs(c)}, {np.angle(c)})')
Complex Operations¶
For two complex numbers $c_1=a_1+ib_1$ and $c_2=a_2+ib_2$, the sum
$$ c_1 + c_2 = (a_1 + ib_1) + (a_2 + ib_2) := (a_1 + a_2) + i(b_1 + b_2) $$
is defined by summing their real and imaginary parts individually. The geometric intuition of addition can be visualized by a parallelogram:
c1 = 1.3 - 0.3j
c2 = 0.3 + 0.5j
c = c1 + c2
generate_figure(figsize=(7.5, 3), xlim=[-0.3, 2.2], ylim=[-0.4, 0.6])
v1 = plot_vector(c1, color='k')
v2 = plot_vector(c2, color='b')
plot_vector(c1, start=c2, linestyle=':', color='lightgray')
plot_vector(c2, start=c1, linestyle=':', color='lightgray')
v3 = plot_vector(c, color='r')
plt.legend([v1, v2, v3], ['$c_1$', '$c_2$', '$c_1+c_2$']);
Complex multiplication of two numbers $c_1=a_1+ib_1$ and $c_2=a_2+ib_2$ is defined by:
$$c = c_1 \cdot c_2 = (a_1 + ib_1) \cdot (a_2 + ib_2) := (a_1a_2 - b_1b_2) + i(a_1b_2 + b_1a_2).$$
Geometrically, the product is obtained by adding angles and by multiplying the absolute values. In other words, if $(|c_1|, \gamma_1)$ and $(|c_2|, \gamma_2)$ are the polar representations of $c_1$ and $c_1$, respectively, then the polar representation $(|c|, \gamma)$ of $c$ is given by:
\begin{eqnarray} \gamma &=& \gamma_1 + \gamma_2 \\ |c| &=& |c_1| \cdot |c_2| \end{eqnarray}
c1 = 1.0 - 0.5j
c2 = 2.3 + 0.7j
c = c1 * c2
generate_figure(figsize=(7.5, 3), xlim=[-0.5, 4.0], ylim=[-0.75, 0.75])
v1 = plot_vector(c1, color='k')
v2 = plot_vector(c2, color='b')
v3 = plot_vector(c, color='r')
plt.legend([v1, v2, v3], ['$c_1$', '$c_2$', '$c_1 \cdot c_2$']);
Given a complex number $c = a + bi$, the complex conjugation is defined by $\overline{c} := a - bi$. Many computations can be expressed in a more compact form using the complex conjugate. The following identities hold: As for the real and imaginary part as well as the absolute value, one has:
\begin{eqnarray} a &=& \frac{1}{2} (c+\overline{c}) \\ b &=& \frac{1}{2i} (c-\overline{c}) \\ |c|^2 &=& c\cdot \overline{c}\\ \overline{c_1+c_2} &=& \overline{c_1} + \overline{c_2}\\ \overline{c_1\cdot c_2} &=& \overline{c_1} \cdot \overline{c_2} \end{eqnarray}
Geometrically, conjugation is reflection on the real axis.
c = 1.5 + 0.4j
c_conj = np.conj(c)
generate_figure(figsize=(7.5, 3), xlim=[0, 2.5], ylim=[-0.5, 0.5])
v1 = plot_vector(c, color='k')
v2 = plot_vector(c_conj, color='r')
plt.legend([v1, v2], ['$c$', r'$\overline{c}$']);
matplotlib
allows for using certain LaTeX code to render mathematical text in the figures. To this end, one needs to activate certain settings and uses specific encodings in order to avoid conflicts between special characters used both in Python and LateX for different purposes. In particular, the backslash \
needs to be handled with care, which can be done by using so-called raw strings marked by r'...'
. For further details, we refer to the Python documentation and other tutorials available on the web.
For a non-zero complex number $c = a + bi$, there is an inverse complex number $c^{-1}$ with the property that $c\cdot c^{-1} = 1$. The inverse is given by:
$$c^{-1} := \frac{a}{a^2 + b^2} + i \frac{-b}{a^2 + b^2} = \frac{a}{|c|^2} + i \frac{-b}{|c|^2} = \frac{\overline{c}}{|c|^2}.$$
c = 1.5 + 0.4j
c_inv = 1 / c
c_prod = c * c_inv
generate_figure(figsize=(7.5, 3), xlim=[-0.3, 2.2], ylim=[-0.5, 0.5])
v1 = plot_vector(c, color='k')
v2 = plot_vector(c_inv, color='r')
v3 = plot_vector(c_prod, color='gray')
plt.legend([v1, v2, v3], ['$c$', '$c^{-1}$', '$c*c^{-1}$']);
With the inverse, division can be defined:
$$\frac{c_1}{c_2} = c_1 c_2^{-1} = \frac{a_1 + ib_1}{a_2 + ib_2} := \frac{a_1a_2 + b_1b_2}{a_2^2 + b_2^2} + i\frac{b_1a_2 - a_1b_2}{a_2^2 + b_2^2} = \frac{c_1\cdot \overline{c_2}}{|c_2|^2}.$$
c1 = 1.3 + 0.3j
c2 = 0.8 + 0.4j
c = c1 / c2
generate_figure(figsize=(7.5, 3), xlim=[-0.25, 2.25], ylim=[-0.5, 0.5])
v1 = plot_vector(c1, color='k')
v2 = plot_vector(c2, color='b')
v3 = plot_vector(c, color='r')
plt.legend([v1, v2, v3], ['$c_1$', '$c_2$', '$c_1/c_2$']);
Polar Coordinate Plot¶
Finally, we show how complex vectors can be visualized in a polar coordinate plot. Also, the following code cell illustrates some functionalities of the Python libraries numpy
and matplotlib
.
def plot_polar_vector(c, label=None, color=None, start=0, linestyle='-'):
"""Plot arrow in polar plot
Notebook: PCP_06_complex.ipynb
Args:
c: Complex number
label: Label of arrow (Default value = None)
color: Color of arrow (Default value = None)
start: Complex number encoding the start position (Default value = 0)
linestyle: Linestyle of arrow (Default value = '-')
"""
# plot line in polar plane
line = plt.polar([np.angle(start), np.angle(c)], [np.abs(start), np.abs(c)], label=label,
color=color, linestyle=linestyle)
# plot arrow in same color
this_color = line[0].get_color() if color is None else color
plt.annotate('', xytext=(np.angle(start), np.abs(start)), xy=(np.angle(c), np.abs(c)),
arrowprops=dict(facecolor=this_color, edgecolor='none',
headlength=12, headwidth=10, shrink=1, width=0))
c_abs = 1.5
c_angle = 45 # in degree
c_angle_rad = np.deg2rad(c_angle)
a = c_abs * np.cos(c_angle_rad)
b = c_abs * np.sin(c_angle_rad)
c1 = a + b*1j
c2 = -0.5 + 0.75*1j
plt.figure(figsize=(6, 6))
plot_polar_vector(c1, label='$c_1$', color='k')
plot_polar_vector(np.conj(c1), label='$\overline{c}_1$', color='gray')
plot_polar_vector(c2, label='$c_2$', color='b')
plot_polar_vector(c1*c2, label='$c_1\cdot c_2$', color='r')
plot_polar_vector(c1/c2, label='$c_1/c_2$', color='g')
plt.ylim([0, 1.8]);
plt.legend(framealpha=1);
Exercises and Results¶
import libpcp.complex
show_result = True
Create and plot the following complex numbers using the functions described above.
- Create a complex number $c$ with an angle of $20$ degrees and an absolute value of $1.2$. Also plot its conjugate and inverse.
- Write a function
rotate_complex
that rotates a complex number $c$ by $r$ degrees in clockwise direction. Apply this function for $c= 1 + 0.5i$ and $r\in\{10,20, 30\}$. Plot all resulting complex numbers.
#<solution>
# Your Solution
#</solution>
libpcp.complex.exercise_complex(show_result=show_result)
Let $p(z)= p_0 z^N + p_1 z^{N-1} + \ldots + p_{N-1}z + p_N$ be a complex-valued polynomial of degree $N\in\mathbb{N}$ with coefficients $p_n\in\mathbb{C}$ for $n\in[0:N]$. Define a function
vis_root
that inputs a polynomial and visualizes all roots of the polynomial (i.e., all zeros of the polynomial). To compute the roots, use the NumPy function np.roots
. To encode the polynomial follow the conventions as used for np.roots
, where the above polynomial is represented by the array (p[0],p[1], ..., p[N])
. For the visualization, use the function plt.scatter
for representing each root as a dot in the Cartesian plane. Apply the function for the following polynomials and discuss the results.
- $p(z)=z^2-2$ (
p = np.array([1, 0, -2])
) - $p(z)=z^2+2$ (
p = np.array([1, 0, 2])
) - $p(z)=z^8-1$ (
p = np.array([1, 0, 0, 0, 0, 0, 0, 0, -1])
) - $p(z)=z^8 + z^7 + z^6$ (
p = np.array([1, 1, 1, 0, 0, 0, 0, 0, 0])
) - $p(z)=z^8 + z^7 + z^6 + 0.000001$ (
p = np.array([1, 1, 1, 0, 0, 0, 0, 0, 0.000001])
) - $p(z)=z^3 -2iz^2 + (2+4i)z + 3 $ (
p = np.array([1, -2j, 2 + 4j, 3])
)
#<solution>
# Your Solution
#</solution>
libpcp.complex.exercise_polynomial(show_result=show_result)
import IPython.display as ipd
ipd.display(ipd.YouTubeVideo('b005iHf8Z3g', width=600, height=450))
Let $c\in\mathbb{C}$ be a complex number and $f_c:\mathbb{C}\to\mathbb{C}$ the function defined by $f_c(z)=z^2+c$ for $z\in\mathbb{C}$. Starting with $z=0$, we consider the iteration $v_c(0):=f_c(0)$ and $v_c(k) := f_c(v_c(k-1))$ for $k\in\mathbb{N}$. The Mandelbrot set is the set of complex numbers $c$ for which the series $(v_c(k))_{k\in\mathbb{N}}$ stays bounded (i.e., if there is a constant $\gamma_c$ such that $v_c(k)\leq \gamma_c$ for all $k\in\mathbb{N}$. Write a function that plots the Mandelbrot set in the Cartesian plane, where a number $c$ is colored black if it belongs to the Mandelbrot set and otherwise is colored white.
- Model the Mandelbrot set as a binary indicator function $\chi:\mathbb{C}\in\{0,1\}$, where $\chi(c)=1$ if $c$ belongs to the Mandelbrot set and $\chi(c)=0$ otherwise.
- Only consider complex numbers $c=a+ib$ on a discrete grid on a bounded range. It suffices to consider the range $a\in[-2,1]$ and $b\in[-1.2,1.2]$. Furthermore, for efficiency reasons, use a grid spacing that is not too fine. First, try out $\Delta a = \Delta b = 0.01$. To create the grid, you may use the function
np.meshgrid
. - Test for each $c=a+ib$ on that grid, if $(v_c(k))_{k\in\mathbb{N}}$ remains bounded or not. Computationally, this cannot be tested easily. However, usually, the sequence $(v_c(k))$ increases in an exponential fashion in the case that it is not bounded. Therefore, a pragmatic (yet not always correct) test is to fix a maximum number of iterations (e.g., $K = 50$) and a threshold (e.g., $L = 100$).
- Plot $\chi$ using the function
plt.imshow
, use the colormap'gray_r'
. Furthermore, use the parameterextent
to adjust ranges of the horizontal axis $[-2,1]$ (real part) and vertical axis $[-1.2,1.2]$ (imaginary part).
#<solution>
# Your Solution
#</solution>
libpcp.complex.exercise_mandelbrot(show_result=show_result)
libpcp.complex.exercise_mandelbrot_fancy(show_result=show_result)