Complex numbers

Like the real numbers, complex numbers form what is known as a field. This means all the familiar properties of real numbers such as addition, multiplication and division adapt straightforwardly to complex numbers. In the complex number system there is a very special number called the complex unit. It is denoted \(i\) and it satisfies the curious formula,

\[i^2 = -1. \]

Evidently, \(i\) cannot be any real number. A complex number \(z\) is always of the form

\[z = x + yi \]

where \(x\) and \(y\) are real numbers. Here \(x\) is the real part of \(z\) while \(y\) is the imaginary part. Complex numbers are a combination of their real and imaginary parts.

Libary import

from class_scripts import cplxnums as cx

Attributes

In cplxnums.py we have the class cplx. Instances of cplx are complex numbers. Below are attributes of this class.

help(cx.cplx)
Help on class cplx in module class_scripts.cplxnums:

class cplx(builtins.object)
 |  cplx(coeffs)
 |  
 |  Methods defined here:
 |  
 |  __add__(self, other)
 |  
 |  __init__(self, coeffs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __mul__(self, other)
 |  
 |  __pow__(self, n)
 |  
 |  __repr__(self)
 |      Return repr(self).
 |  
 |  __str__(self)
 |      Return str(self).
 |  
 |  __sub__(self, other)
 |  
 |  __truediv__(self, other)
 |  
 |  mtrx(self)
 |  
 |  toPolar(self)
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(cls, coeffs)
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  cplx_unit = [0, 1]
 |  
 |  significant_figures = 4

The class cplx passes arrays of length two, [x, y]. The first entry x is the real part and second entry y is the imaginary part. When initialised, any complex number comes with these defining properties and its norm-squared, which is \(x^2 + y^2\).

For the complex unit \(i\), its real part is 0 and imaginary part is 1. Its attrubutes are:

cplx_unit = cx.cplx([0, 1])

print(f"The real part of {cplx_unit} is {cplx_unit.re}")
print(f"The imaginary part of {cplx_unit} is {cplx_unit.im}")
print(f"The norm squared of {cplx_unit} is {cplx_unit.normsq}")
The real part of 0 + 1i is 0
The imaginary part of 0 + 1i is 1
The norm squared of 0 + 1i is 1

Note that we have printed the complex unit above as 0 + 1i. This is due to our modification of __str__. In raw form, the complex unit is represented as:

print(repr(cplx_unit))
cplx([0, 1])

Complex number generator

The following code generates instances of complex numbers with integral real and imaginary parts. It passes in amt, the amount of complex numbers to generate and rng, the integer range to sample for real and imaginary parts.

from random import randint

def generateCplx(amt, rng):
    cplx_nums = []
    for _ in range(amt):
        re = randint(rng[0], rng[-1])
        im = randint(rng[0], rng[-1])
        cplx_nums += [cx.cplx([re, im])]
        
    return cplx_nums

Below is a list of 10 complex numbers with integral real and imaginary parts between -10 and 10. Their norm-squared is printed alonside.

amt = 10
rng = [-10, 10]

cplx_num_lst = generateCplx(amt, rng)
for num in cplx_num_lst:
    print(f"For {num} its norm-squared is {num.normsq}")
For 2 + 0i its norm-squared is 4
For 9 + 4i its norm-squared is 97
For -2 - 4.0i its norm-squared is 20
For -7 - 3.0i its norm-squared is 58
For 2 + 1i its norm-squared is 5
For -8 - 2.0i its norm-squared is 68
For -6 - 6.0i its norm-squared is 72
For -2 - 8.0i its norm-squared is 68
For -1 - 2.0i its norm-squared is 5
For 3 + 5i its norm-squared is 34

Arithmetic

Complex numbers can be added, multiplied, subtracted and divided. Modifying the relevant the dunders allows for adapting +, *, -, / to instances of cplx. To illustrate, for the following two complex numbers

\[ \begin{align} z_1 = -2 + 4i && \mbox{and} && z_2 = 4 - i \end{align} \]

we will return:

  • \(z_1 + z_2\);

  • \(z_1 - z_2\);

  • \(z_1 \cdot z_2\);

  • \(z_1/z_2\).

cplx_1 = cx.cplx([-2, 4])
cplx_2 = cx.cplx([4, -1])

print(cplx_1 + cplx_2)
print(cplx_1 - cplx_2)
print(cplx_1 * cplx_2)
print(cplx_1 / cplx_2)
2 + 3i
-6.0 + 5.0i
-4.0 + 18.0i
-0.7059 + 0.8235i

Note

Decimal places are reported to 4 significant figures.

Integer powers

With multiplication comes power. The dunder __pow__ allows for adapting ** to instances of cplx. As an illustration, below are the first 5 powers of the complex unit i.

for i in range(5 + 1):
    print(f"The {i}-th power of {cplx_unit} is: {cplx_unit**i}")
The 0-th power of 0 + 1i is: 1 + 0i
The 1-th power of 0 + 1i is: 0 + 1i
The 2-th power of 0 + 1i is: -1.0 + 0.0i
The 3-th power of 0 + 1i is: 0.0 - 1.0i
The 4-th power of 0 + 1i is: 1.0 + 0.0i
The 5-th power of 0 + 1i is: 0.0 + 1.0i

Matrix representation

Preliminaries

It is possible to study complex numbers without ever mentioning the complex unit \(i = \sqrt{-1}\) and instead only considering real numbers. But what we gain in doing so is conceded by having to deal with matrices.

To see why matrices are relevant, recall that any complex number \(z\) is specified by two real numbers \(x, y\). The correspondence \(z \mapsto (x, y)\) gives an isomorphism of sets \(\mathbb C\cong \mathbb R^2\). As the complex numbers form a field we have the operation of multiplication \(\mathbb C\times \mathbb C \rightarrow \mathbb C\). Equivalently (or rather, dually), any complex number \(z\) can be thought of via the multiplication \(z : \lambda \mapsto \lambda \cdot z\). Hence any complex number is a transformation \(\mathbb C \rightarrow \mathbb C\). Note that it is linear since \(a\lambda \mapsto (a\lambda) z = a(\lambda z)\). Hence moreover, any complex number will be a linear transformation. Using that \(\mathbb C\cong \mathbb R^2\), we deduce that any complex number can be thought of as a linear transformation \(\mathbb R^2 \rightarrow \mathbb R^2\), i.e., as a \(2\times 2\) matrix.

Explicitly, with \(z = x + yi\), this \(2\times 2\) matrix representation is

\[\begin{split}z = x + yi \longleftrightarrow \left(\begin{array}{rr} x & -y\\ y & x\end{array}\right). \end{split}\]

Let \(M(z)\) denote the matrix representation of \(z\). The unit \(1\) and complex unit \(i\) are represented respectively by,

\[\begin{split} \begin{align} M(1) = \left(\begin{array}{rr} 1 & 0\\ 0 & 1\end{array}\right) &&\mbox{and}&& M(i) = \left(\begin{array}{rr} 0 & -1\\ 1 & 0\end{array}\right). \end{align} \end{split}\]

A straightforward calculation will show for two any complex numbers \(z_1, z_2\) that \(M(z_1z_2) = M(z_1)M(z_2)\). Hence that \(M : \mathbb C \rightarrow \mathrm{GL}_2(\mathbb R)\) defines a representation of the complex number field \(\mathbb C\) on the group of \(2\times 2\), real matrices.

Note

The representation \(\mathbb C \stackrel{M}{\rightarrow} \mathrm{GL}_2(\mathbb R)\) is “faithful”, i.e., injective, since \(M(z) = 0\) if and only if \(z = 0 + 0i\).

Implementation

The class cplx in the module cplxnum.py contains the method mtrx(). Calling this on any instance of cplx, i.e., on any complex number \(z\), returns its matrix representation \(M(z)\). To illustrate, for the following complex numbers:

  • \(1 + 0i\);

  • \(0 + 1i\);

  • \(-3 + 5i\)

their matrix representations are:

unit = cx.cplx([1, 0])
cplx_unit = cplx_unit
cplx_3 = cx.cplx([-3, 5])


cplx_nums_to_rep = [unit, cplx_unit, cplx_3]

for cplx in cplx_nums_to_rep:
    print(cplx.mtrx())
[[1 0]
 [0 1]]
[[ 0 -1]
 [ 1  0]]
[[-3 -5]
 [ 5 -3]]

Note

For any instance of cplx, the matrix representation method mtrx() returns a numpy array.

Recovering attributes

From the matrix representation of a complex number \(z\), its real and imaginary parts can be ontained as follows. Recall that the trace of a matrix is the sum of entries on its diagonal. Hence with \(z = x + yi\) see that \(\mathrm{tr}~M(z) = 2x\). The real part of \(z\) is therefore \(\frac{1}{2}\mathrm{tr}~M(z)\).

As for the imaginary part it is necessary to first multiply by \(-i\). This has the effect of rotating real and imaginary to imaginary and real. The imaginary part of \(z\) is therefore \(\frac{1}{2}\mathrm{tr}~M(-zi)\). Since we know \(M(z_1z_2) = M(z_1)M(z_2)\), we can obtain the imaginary part equivalently by \(-\frac{1}{2} \mathrm{tr}(M(z)M(i))\).

Concerning the norm squared attribute, it can be recovered as the determinant of the matrix representation. That is, if \(\|z\|^2\) denotes the norm-squared of the complex number \(z\) then

\[\|z\|^2 = \det M(z). \]

This makes it clear that the length of a complex number can be related to an area spanned by real vectors in two dimensions. We leave it to the intrepid reader of these documents to test these claims using the class cplx.

Polar coordinates

With an isomorphism \(\mathbb C \cong \mathbb R^2\), two important reference objects are defined: a pole and axis. The pole here is the origin \((0, 0)\); the axis is the \(x\)-axis. Through these objects, any complex number when mapped to a vector in \(\mathbb R^2\) can be faithfully represented by:

  • its distance from the pole, \(r\);

  • the angle it makes with the \(x\)-axis, \(\varphi\).

The image of a complex number in the \((r, \varphi)\)-plane is referred to as its polar representation.

Note

The distance \(r\) is necessarily positive.

Polar transformation

The mapping of \(z\) onto the polar coordinate plane is referred to as a polar transformation. For \(z = x + yi\), it maps onto \(\mathbb R^2\) by \(z\mapsto (x, y)\). The polar transformation is obtained by specifying where the real and imaginary parts map to in the \((r, \varphi)\)-plane. Since \(r\) is the distance to the pole, which in this case is the origin, see that

\[r = \sqrt{x^2 + y^2}. \]

With \(r\) known, the angle \(\varphi\) made by \(z\) and the \(x\)-axis is given by

\[\begin{split}\varphi = \left\{ \begin{array}{ll} \arccos(x/r) & \mbox{if $y \geq 0, r\neq0$} \\ -\arccos(x/r) & \mbox{if $y< 0, r\neq0$} \\ \mbox{undefined} & \mbox{if $r = 0$.} \end{array} \right. \end{split}\]

Implementation

The method toPolar(), callable on any instance of cplx sends a complex number to its polar representation. Recall the complex numbers from earlier:

  • \(1 + 0i\);

  • \(0 + 1i\);

  • \(-3 + 5i\).

Their polar representations are:

for cplx_num in cplx_nums_to_rep:
    print(cplx_num.toPolar())
(1.0, 0.0)
(1.0, 1.5707963267948966)
(5.830951894845301, 2.1112158270654806)

Polar coordinates are a convenient coordinate system to express, model and graph complex systems and functions. In a forthcoming section on complex functions we will return to polar coordinate representations as a means to visualize and graph functions.

Fractional powers

Preliminaries

The polar transformation sends any complex number \(z\) to a radial and angular coordinate \((r, \varphi)\). This reflects the alternate representation of any complex number,

\[z = re^{\varphi i}. \]

With \(z = x + yi\), Euler’s identity states \(x = r\cos\varphi\) and \(y = r\sin\varphi\). That is,

\[e^{\varphi i} = \cos\varphi + (\sin\varphi) i. \]

The utility of this formula lies in calculating fractional powers of complex numbers. For any fraction \(a/b\) we have

\[\begin{split} \begin{align} z^{a/b} &= (re^{\varphi i})^{a/b} \\ &= r^{a/b}e^{\frac{a\varphi}{b}i} \\ &= r^{a/b}\left(\cos\left(\frac{a\varphi}{b}\right) + \sin\left(\frac{a\varphi}{b}\right)i\right) \end{align} \end{split}\]

where \(r^{a/b}\) is the fractional power of \(r\) which, recall, is a real number.

Implementation

In cplxnums.py the function powFrac() passes instances of cplx and a rational number. It returns their fractional power. To illustrate, consider the following complex number,

\[z = -28 + 3i. \]

A fifth root, \(z^{1/5}\), is:

take_fifth_of_num = cx.cplx([-28, 3])
print(cx.powFrac(take_fifth_of_num, 1/5))
1.6013 + 1.112i

As a check we have:

fifth_root_of_num = cx.powFrac(take_fifth_of_num, 1/5)

print(fifth_root_of_num**5)
-28.0 + 3.0i

Warning

Due to multi-valuedness of trigonometric functions, there are more fifth roots of \(z\) than what we calculated above. Four more in fact. These can be found through multiplying by (primitive) roots of nity.

Roots of unity

A famous equation among complex numbers is the equation for the unit,

\[z^n = 1 \]

for a given \(n\). From the fundamental theorem of algebra there will be exactly \(n\) solutions to the above equation, counted with multiplicity.

Note

In the case where \(n\) is a prime number, there will be \(n\) distinct solutions.

For each \(n\), any solution to \(z^n = 1\) is referred to as an \(n\)-th root of unity. The set of all \(n\)-th roots of unity forms what is known as a group. This is due to the following features:

  • if \(z_1, z_2\) are solutions, then \(z_1^n = 1\) and \(z_2^n = 1\). Note that their product \(z_1z_2\) will also be a root of unity,

\[(z_1z_2)^n = z_1^nz_2^n = (1)(1) = 1; \]
  • if \(z^n = 1\) then \((1/z)^n = 1/z^n = 1/1 = 1\), so \(1/z\) is a root of unity;

  • the unit \(z = 1\) is itself a root of unity.

Roots of polynomials \(f\) (solutions to \(f(x) =0)\) often involve forming fractional powers of complex numbers. The operation of forming such powers is also known as root extraction - an essential part of the notion of a radical.

As we have seen above, there is an ambiguity here since there is no “single” root of a complex number. If any solution is known, multiplying it by powers of an appropriate root of unity recovers other solutions. In this way, roots of unity can efficiently allow for generating new solutions from old.

The function rootsUnity() in cplxnum.py passses integers \(n\) and returns a list of all \(n\)-rots of unity. For example, a list of all the \(7\)-th roots of unity is:

seventh_roots = cx.rootsUnity(7)
for rt in seventh_roots:
    print(f"a root is {rt}. Its 7th power is {rt**7}")
a root is 1.0 + 0.0i. Its 7th power is 1.0 + 0.0i
a root is 0.6235 + 0.7818i. Its 7th power is 0.9999 - 0.0002i
a root is -0.2225 + 0.9749i. Its 7th power is 0.9998 - 0.0001i
a root is -0.901 + 0.4339i. Its 7th power is 1.0002 + -0.0i
a root is -0.901 - 0.4339i. Its 7th power is 1.0002 + 0.0i
a root is -0.2225 - 0.9749i. Its 7th power is 0.9998 + 0.0001i
a root is 0.6235 - 0.7818i. Its 7th power is 0.9999 + 0.0002i

Further application

Recall earlier that we calculated a fifth root of -28 + 3i. To find the other four, we can multiply the solution we found by any fifth root of unity.

Note

In fact, we would need to take a primitive root of unity. Any root (except the unit) to a prime will be primitive however. E.g., since \(5\) is prime, any \(5\)-th root of unity (other than 1) will be a primitive root.

Recall that we labelled the number -28 + 3i as take_fifth_root_of. We obtained a fifth root which we labelled fifth_root_of_num. The other fifth roots of -28 + 3i are:

fifth_roots = cx.rootsUnity(5)
fifth_root = fifth_roots[-1]

all_fifth_roots = []
for i in range(1, 5+1):
    all_fifth_roots += [fifth_root_of_num * (fifth_root**i)]
    
print(*all_fifth_roots, sep = '\n')
1.5524 - 1.1793i
-0.6419 - 1.8408i
-1.9491 + 0.0416i
-0.5627 + 1.8665i
1.6013 + 1.112i

As a consistency check we can return the fifth power of each number in all_fifth_roots to ensure we indeed recover the original number -28 + 3i. And so, up to floating point error, we have:

for root in all_fifth_roots:
    print(root**5)
-28.0015 + 2.9989i
-27.999 + 3.0022i
-28.0019 + 2.9992i
-27.9968 + 3.0003i
-28.0017 + 2.9987i

as expected.

Note

In this example we chose the fifth root of unity fifth_root = fifth_roots[-1]. We could of course choose any other (non-unit) fifth root of unity, e.g., fifth_root = fifth_roots[-2] or fifth_root = fifth_roots[-3]. This would return the same fifth roots of -28 + 3i, albeit in a different order.

Evaluation

With the arithmetic of complex numbers established, it will be possible to evaluate general polynomials on complex numbers. The appropriate method to call is cplxEval() on any instance of Poly. This method passes an instance of cplx and returns an instance of cplx.

As an illustration, consider the polynomial \(f(x) = 1 + x^2\). On the complex number \(i\) we have:

from class_scripts import polynomial as pnml

c_num = pnml.cplx([0, 1])
poly_to_evaluate = pnml.Poly([1, 0, 1])

print(poly_to_evaluate.cplxEval(c_num))
0.0 + 0.0i

Note

In the above code block we import polynomial.py from the directory class_scripts. And in class_scripts/polynomial.py we have the code from cplxnum.py. In order to run code from one script we write our complex number here as c_num = pnml.cplx([0, 1]) instead of cx.cplx([0, 1]) as in earlier parts of this document. Calling cplxEval() and passing cx.cplx([0, 1]) would result in a type error here.