Having hopefully now convinced you that floating point is the right choice for general purpose numerical computing, provided you put some thought into how you use it, we're ready to start implementing the library.
We shall start with some basic mathematical types and functions upon which we can build more advanced algorithms.
First up are the complex numbers. A complex number \(z = x + iy\) has both a real part \(x\) and an imaginary part \(y\) which is multiplied by \(i = \sqrt{1}\). We can define arithmetic operations for complex numbers...
Whoa! Hold up there! I'm taking the square root of what now?!
Since multiplying a real number by itself always results in a nonnegative number it might not be entirely obvious what taking the square root of a negative number actually means. I could be glib and simply assure you that you don't need to worry about it but I think I can argue from analogy that it's a perfectly reasonable concept.
Imagine that we've defined a symbol \(k\) to mean a multiple of a thousand. Clearly we don't want to mix up the \(k\) part and the unit part of a number so must keep track of them during arithmetic. For example
Now division is a slightly trickier operation that is easiest to define in terms of the product of the numerator and the reciprocal of the denominator. The latter is given by
The realisation that functions could be extended to complex numbers in this way lead to a flurry of activity analysing the properties of functions of complex numbers from which grew the unimaginatively titled subject of complex analysis. Some of its results provide suprising insights into the properties of functions of real numbers but, given that this is a blog about numerical computing and not an undergraduate course in mathematics, we'll not be delving into them.
The form of the exponential of a complex number is suggestive of an alternative representation for complex numbers; if we treat the real and imaginary parts as Cartesian coordinates \((x, y)\) then we can uniquely identify them with their distances to the origin, \((0, 0)\), known as the magnitude, \(r\), of the number, and the anticlockwise angle between the line from the origin and the positive \(x\)axis, known as the argument, \(\theta\), as illustrated by figure 1.
Now expressing complex numbers in this form makes some calculations a great deal easier. For example, to raise a complex number to a real power we have
This can lead to some surprising consequences. For example, consider
We're quite used to this when dealing with square roots of real numbers; when taking the square root of the square of a negative number our calculators and computers will give a positive number. If the behaviour of roots of complex numbers seems surprising it is simply because we are less familiar with them.
The ordered pair that we use to represent the complex number is here provided by the
So if the object is constructed with a single number then that is assigned to its real part and the imaginary part is left at zero, whilst a pair of numbers set the real and imaginary parts in that order. Finally we have construction from an object which initialises the real and imaginary parts with the value of the
These functions do pretty much the same thing as their equivalents for numbers,
For complex numbers we also have the argument, the angle they make with the positive \(x\)axis when plotted on a graph, provided by a new overload
Addition proceeds exactly as we have discussed, with mixed arithmetic treating real numbers as complex numbers with an imaginary part of zero, as illustrated by listing 5.
Multiplication, division and exponentiation also follow the rules described above as shown, omitting mixed arithmetic overloads, in listing 6.
Now if we're using complex numbers it's convenient to redefine
The full complement of overloads of existing operations, new overloads and replacements for those that are well defined for complex arithmetic are given in listing 8.
Note that we must check that the dispatch functions for
The implementations of these overloads can be found in
Plotting the results of a function of complex numbers is a bit of a tricky job since we need to convey two pieces of information; either the real and imaginary parts or the magnitude and argument. The latter are generally easier to deal with, as illustrated by program 2.
Here we have two new chart objects, both attached to the same canvas. The first is a contour plot which represents the result of a function of two arguments with the colour of the pixel plotted at the point they represent. The second is a vector plot which represents starting points with dots and connects them to end points with lines.
The colours used to represent the values in the contour plot are set by the
The vector plot requires an array of
In program 2 the contour plot represents the magnitude of the result of the function, with darker hues indicating smaller numbers, and the vector plot just their arguments, by giving the pointers constant length.
As we should expect, those values with large negative real parts and small imaginary parts point in the direction of \(\pm i\) and those with large negative real parts and small imaginary parts point in the direction of +1.
Now this is all well and good, but let's face it, there's only one kind of graph of complex numbers that we're really interested in...
Beloved of undergraduate mathematicians the world over, fractals hit mainstream popular culture in the late 1980s. In fact, I still have a Tshirt with a black and white print of a fractal.
Made from hemp.
As is tragically shown in figure 2.
Perhaps the most famous fractal is the Mandelbrot set, although I suspect that the majority of my fellow unwashed hippies whose torsos it adorned had no idea how it was defined.
The truly remarkable thing about it is how simple it actually is. At its heart is the quadratic equation
In general it's quite tricky to actually prove that a complex number is in the Mandelbrot set, so the usual tactic is to approximate it with the set of numbers that don't lead to \(z\) growing above some given magnitude within some given number of assignments of \(z = f_c(z)\). Rather than simply plot the numbers that are in the Mandelbrot set program 3 plots the number of iterations it takes for \(z\) to grow above that magnitude for that familiar psychedelic image.
Cosmic.
We shall start with some basic mathematical types and functions upon which we can build more advanced algorithms.
First up are the complex numbers. A complex number \(z = x + iy\) has both a real part \(x\) and an imaginary part \(y\) which is multiplied by \(i = \sqrt{1}\). We can define arithmetic operations for complex numbers...
Whoa! Hold up there! I'm taking the square root of what now?!
Since multiplying a real number by itself always results in a nonnegative number it might not be entirely obvious what taking the square root of a negative number actually means. I could be glib and simply assure you that you don't need to worry about it but I think I can argue from analogy that it's a perfectly reasonable concept.
Imagine that we've defined a symbol \(k\) to mean a multiple of a thousand. Clearly we don't want to mix up the \(k\) part and the unit part of a number so must keep track of them during arithmetic. For example
\[
(2k + 623) + (1k + 491) = (3k + 1104)
\]
Now if whenever we we see a number greater than a thousand we replace it with \(k\) plus that number minus one thousand we have a rule for transforming it into a unique, unambiguous representation. In this case
\[
(3k + 1104) = (3k + k + 104) = (4k + 104)
\]
If we similarly think of \(i\) as being nothing more than a symbol with the transformation rule that \(i^2 = 1\) we can unambiguously define rules of arithmetic for complex numbers. For example, addition is defined as
\[
z_0 + z_1 = (x_0 + i y_0) + (x_1 + i y_1) = (x_0 + x_1) + i (y_0 + y_1)
\]
and multiplication as
\[
\begin{align*}
z_0 \times z_1 &= (x_0 + i y_0) \times (x_1 + i y_1)\\
&= (x_0 \times x_1) + (x_0 \times i y_1) + (x_1 \times i y_0) + (i y_0 \times i y_1)\\
&= (x_0 x_1  y_0 y_1) + i (x_0 y_1 + x_1 y_0)
\end{align*}
\]
The notion that \(i\) is just a symbol with a rule for transforming it is very much in keeping with the modern view of mathematics as the study of systems of symbol manipulation. Rather than worry about what it all actually means mathematicians are generally more concerned that the rules have interesting consequences and do not lead to any contradictions.Now division is a slightly trickier operation that is easiest to define in terms of the product of the numerator and the reciprocal of the denominator. The latter is given by
\[
\frac{1}{x + i y} = \frac{x  i y}{x^2 + y^2}
\]
as can be seen by multiplying it by \(x + i y\).
\[
\begin{align*}
(x + i y) \times \frac{x  i y}{x^2 + y^2} &= \frac{(x + i y) \times (x  i y)}{x^2 + y^2}\\
&= \frac{x^2  x \times i y + x \times i y  i^2 y^2}{x^2 + y^2}\\
&= \frac{x^2 + y^2}{x^2 + y^2}\\
&= 1
\end{align*}
\]
Hence division is defined as
\[
\begin{align*}
(x_0 + i y_0) \div (x_1 + i y_1) &= (x_0 + i y_0) \times \frac{x_1  i y_1}{x_1^2 + y_1^2}\\
&= \frac{(x_0 + i y_0) \times (x_1  i y_1)}{x_1^2 + y_1^2}\\
&= \frac{(x_0 x_1  y_0 y_1) + i (x_0 y_1 + x_1 y_0)}{x_1^2 + y_1^2}
\end{align*}
\]
To define general mathematical operations we shall rely again upon our favourite mathematical tool, Taylor's theorem. To illustrate how, consider the exponential of a complex number
\[
e^{x + i y} = e^x \times e^{i y}
\]
Now the \(e^x\) term can be calculated with Math.exp
so we just need to work out the value of \(e^{i y}\). Plugging \(i y\) into the Taylor series expansion of the exponential function at zero, also known as the Maclaurin series, we have
\[
\begin{align*}
e^{i y} &= 1 + i y + \tfrac{1}{2} (i y)^2 + \tfrac{1}{6} (i y)^3 + \tfrac{1}{24} (i y)^4 + \tfrac{1}{120} (i y)^5 + \dots + \tfrac{1}{n!} (i y)^n + \dots\\
&= 1 + i y  \tfrac{1}{2} y^2  \tfrac{1}{6} i y^3 + \tfrac{1}{24} y^4 + \tfrac{1}{120} i y^5 + \dots + \tfrac{1}{n!} (i y)^n + \dots\\
&= \left(1  \tfrac{1}{2} y^2 + \tfrac{1}{24} y^4 + \dots + \tfrac{(1)^n}{(2n)!} y^{2n} + \dots\right) + \left(i y  \tfrac{1}{6} i y^3 + \tfrac{1}{120} i y^5 + \dots + \tfrac{(1)^n}{(2n+1)!} i y^{2n+1} + \dots\right)\\
&= \left(1  \tfrac{1}{2} y^2 + \tfrac{1}{24} y^4 + \dots + \tfrac{(1)^n}{(2n)!} y^{2n} + \dots\right) + i \left(y  \tfrac{1}{6} y^3 + \tfrac{1}{120} y^5 + \dots + \tfrac{(1)^n}{(2n+1)!} y^{2n+1} + \dots\right)
\end{align*}
\]
By gathering the real and imaginary terms separately we can see by inspection that they are the Maclaurin series' for the cosine and sine respectively.
\[
\begin{align*}
\cos y &= 1  \tfrac{1}{2}y^2 + \tfrac{1}{24}y^4 + \dots + \frac{(1)^n}{(2n)!}y^{2n} + \dots\\
\sin y &= y  \tfrac{1}{6}y^3 + \tfrac{1}{120}y^5 + \dots + \frac{(1)^n}{(2n+1)!}y^{2n+1} + \dots
\end{align*}
\]
All three of these series converge for any value of \(y\) so the exponential of a complex number is therefore given by
\[
e^{x + i y} = e^x \cos y + i e^x \sin y
\]
This is known as Euler’s formula and, if we set \(x=0\) and \(y=\pi\), yields the surprising identity
\[
e^{i \pi} = 1
\]
linking negative numbers, transcendental irrationals and complex numbers; three extremely important developments in the concept of number.The realisation that functions could be extended to complex numbers in this way lead to a flurry of activity analysing the properties of functions of complex numbers from which grew the unimaginatively titled subject of complex analysis. Some of its results provide suprising insights into the properties of functions of real numbers but, given that this is a blog about numerical computing and not an undergraduate course in mathematics, we'll not be delving into them.
Figure 1: Complex Numbers As Coordinates
\[
\begin{align*}
x + i y &= r (\cos \theta + i \sin \theta) = r e^{i \theta}\\
r &= \sqrt{x^2 + y^2}\\
\theta &= \arctan \tfrac{y}{x}
\end{align*}
\]
In some sense this provides an interpretation of \(i\) beyond an arbitrary symbol; if we consider positive real numbers to represent forwards and negative real numbers to represent backwards then \(i\) represents left.Now expressing complex numbers in this form makes some calculations a great deal easier. For example, to raise a complex number to a real power we have
\[
(x + i y)^n = \left(r e^{i \theta}\right)^n = r^n e^{i n \theta} = r^n \cos n \theta + i r^n \sin n \theta
\]
Unfortunately, since the sine and cosine functions are periodic (adding \(2\pi\) to their arguments yields the same results) there are an infinite number of representations of this form for each complex number. The usual convention is to choose a specific \(2\pi\) wide range of angles from which we choose the value of the argument; we shall use \(\pi<\theta\leqslant\pi\).This can lead to some surprising consequences. For example, consider
\[
\left((1)^3\right)^\frac{1}{3}
\]
Expressing this in the magnitudeargument form we have
\[
\begin{align*}
\left((e^{i\pi})^3\right)^\frac{1}{3} &= \left(e^{3i\pi}\right)^\frac{1}{3}\\
&= \left(e^{i\pi}\right)^\frac{1}{3}\\
&= e^{\frac{i\pi}{3}}
\end{align*}
\]
Putting this back into real and imaginary parts yields
\[
\cos \tfrac{\pi}{3} + i \sin \tfrac{\pi}{3} = 0.5 + \tfrac{\sqrt 3}{2} i
\]
and not minus one as we might have expected. The reason for this is that when using complex numbers there are three cubic roots of minus one rather than the one we'd expect when using real numbers. Our calculation just happens to result in a different one than we started with.We're quite used to this when dealing with square roots of real numbers; when taking the square root of the square of a negative number our calculators and computers will give a positive number. If the behaviour of roots of complex numbers seems surprising it is simply because we are less familiar with them.
A Complex Class
Rather than try to work out how to represent \(i\) as an object, we shall simply define complex numbers as ordered pairs of reals with rules of arithmetic that map onto those of complex numbers. For example
\[
(x_0, y_0) + (x_1, y_1) = (x_0 + x_1, y_0 + y_1)\\
(x_0, y_0) \times (x_1, y_1) = (x_0 x_1  y_0 y_1, x_0 y_1 + x_1 y_0)
\]
The definition of the ak.complex
class is illustrated by listing 1.Listing 1: ak.complex
ak.COMPLEX_T = 'ak.complex'; function Complex(){} Complex.prototype = {TYPE: ak.COMPLEX_T, valueOf: function(){return ak.NaN;}}; ak.complex = function() { var c = new Complex(); var arg0 = arguments[0]; var state = {re: 0, im: 0}; constructors[ak.nativeType(arg0)](state, arg0, arguments); c.re = function() {return state.re;}; c.im = function() {return state.im;}; c.toString = function() { return '('+state.re.toString()+','+state.im.toString()+'i)'; }; c.toExponential = function(d) { return '('+state.re.toExponential(d)+','+state.im.toExponential(d)+'i)'; }; c.toFixed = function(d) { return '('+state.re.toFixed(d)+','+state.im.toFixed(d)+'i)'; }; c.toPrecision = function(d) { return '('+state.re.toPrecision(d)+','+state.im.toPrecision(d)+'i)'; }; return Object.freeze(c); }; var constructors = {};
The ordered pair that we use to represent the complex number is here provided by the
state
object which is populated by a call to a member of the constructors
object. Aside from this we have trivial accessors methods re
and im
for the real and imaginary parts of the number and methods for conversion to string. Being an arithmetic type implemented with floating point I felt it appropriate to implement equivalents of the string conversions supported by JavaScript's Number
object.
ak.complex Constructors
Having a state of just two numbers, the constructors forak.complex
are relatively straightforward, as shown in listing 2.Listing 2: ak.complex Constructors
constructors[ak.NUMBER_T] = function(state, re, args) { var arg1 = args[1]; state.re = Number(re); constructors[ak.NUMBER_T][ak.nativeType(arg1)](state, arg1); }; constructors[ak.NUMBER_T][ak.NUMBER_T] = function(state, im) { state.im = Number(im); }; constructors[ak.NUMBER_T][ak.UNDEFINED_T] = function() { }; constructors[ak.OBJECT_T] = function(state, obj) { state.re = ak.nativeType(obj.re)===ak.FUNCTION_T ? Number(obj.re()) : Number(obj.re); state.im = ak.nativeType(obj.im)===ak.FUNCTION_T ? Number(obj.im()) : Number(obj.im); };
So if the object is constructed with a single number then that is assigned to its real part and the imaginary part is left at zero, whilst a pair of numbers set the real and imaginary parts in that order. Finally we have construction from an object which initialises the real and imaginary parts with the value of the
re
and im
members, or to the results of them if they're methods.
ak.complex Overloads
As usual we shall implemented the bulk of the functionality with overloads, the simplest of which are given in listing 3.Listing 3: ak.complex abs And neg
function abs(c) { return Math.sqrt(Math.pow(c.re(), 2) + Math.pow(c.im(), 2)); } ak.overload(ak.abs, ak.COMPLEX_T, abs); function neg(c) { return ak.complex(c.re(), c.im()); } ak.overload(ak.neg, ak.COMPLEX_T, neg);
These functions do pretty much the same thing as their equivalents for numbers,
neg
negating both the real and imaginary parts and abs
returning the distance of the complex number from zero when interpreted as Cartesian coordinates, known as the magnitude. For normal numbers the latter simply removes the sign, making the result positive, but for those that is the distance from zero.For complex numbers we also have the argument, the angle they make with the positive \(x\)axis when plotted on a graph, provided by a new overload
arg
. Another new overload is conj
which returns the conjugate of a complex number in which the imaginary part is negated. Both are given in listing 4, together with overloads for real numbers.Listing 4: ak.complex arg And conj
function arg(c) { return Math.atan2(c.im(), c.re()); } function argR(r) { return r>=0 ? 0 : ak.PI; } ak.overload(ak.arg, ak.COMPLEX_T, arg); ak.overload(ak.arg, ak.NUMBER_T, argR); function conj(c) { return ak.complex(c.re(), c.im()); } function conjR(r) { return r; } ak.overload(ak.conj, ak.COMPLEX_T, conj); ak.overload(ak.conj, ak.NUMBER_T, conjR);
Addition proceeds exactly as we have discussed, with mixed arithmetic treating real numbers as complex numbers with an imaginary part of zero, as illustrated by listing 5.
Listing 5: ak.complex add
function add(c0, c1) { return ak.complex(c0.re()+c1.re(), c0.im()+c1.im()); } function addCR(c, r) { return ak.complex(r+c.re(), c.im()); } function addRC(r, c) { return ak.complex(r+c.re(), c.im()); } ak.overload(ak.add, [ak.COMPLEX_T, ak.COMPLEX_T], add); ak.overload(ak.add, [ak.COMPLEX_T, ak.NUMBER_T], addCR); ak.overload(ak.add, [ak.NUMBER_T, ak.COMPLEX_T], addRC);
Multiplication, division and exponentiation also follow the rules described above as shown, omitting mixed arithmetic overloads, in listing 6.
Listing 6: ak.complex mul, div And exp
function mul(c0, c1) { return ak.complex(c0.re()*c1.re()c0.im()*c1.im(), c0.re()*c1.im()+c0.im()*c1.re()); } ak.overload(ak.mul, [ak.COMPLEX_T, ak.COMPLEX_T], mul); function div(c0, c1) { var abs2 = Math.pow(c1.re(), 2) + Math.pow(c1.im(), 2); var inv_r1 = c1.re()/abs2; var inv_i1 = c1.im()/abs2; return ak.complex(c0.re()*inv_r1c0.im()*inv_i1, c0.re()*inv_i1+c0.im()*inv_r1); } ak.overload(ak.div, [ak.COMPLEX_T, ak.COMPLEX_T], div); function exp(c) { var e = Math.exp(c.re()); return ak.complex(e*Math.cos(c.im()), e*Math.sin(c.im())); } ak.overload(ak.exp, ak.COMPLEX_T, exp);
Now if we're using complex numbers it's convenient to redefine
ak
overloads that might have invalid results for negative reals but that are perfectly well defined for complex numbers. For example, the square root of a negative number should return a complex number rather than NaN
if we have decided to use complex numbers. The square roots of complex and real numbers are given in listing 7.Listing 7: ak.complex sqrt
function sqrt(c) { var r = Math.sqrt(ak.abs(c)); var a = arg(c) / 2; return ak.complex(r*Math.cos(a), r*Math.sin(a)); } function sqrtR(r) { return (r>=0) ? Math.sqrt(r) : ak.complex(0, Math.sqrt(r)); } ak.overload(ak.sqrt, ak.COMPLEX_T, sqrt); ak.overload(ak.sqrt, ak.NUMBER_T, sqrtR);
The full complement of overloads of existing operations, new overloads and replacements for those that are well defined for complex arithmetic are given in listing 8.
Listing 8: ak.complex Overloads
if(!ak.arg) ak.arg = function(x) {return ak.arg[ak.type(x)](x)}; if(!ak.conj) ak.conj = function(x) {return ak.conj[ak.type(x)](x)}; ak.overload(ak.abs, ak.COMPLEX_T, abs); ak.overload(ak.arg, ak.COMPLEX_T, arg); ak.overload(ak.arg, ak.NUMBER_T, argR); ak.overload(ak.conj, ak.COMPLEX_T, conj); ak.overload(ak.conj, ak.NUMBER_T, conjR); ak.overload(ak.cos, ak.COMPLEX_T, cos); ak.overload(ak.cosh, ak.COMPLEX_T, cosh); ak.overload(ak.exp, ak.COMPLEX_T, exp); ak.overload(ak.inv, ak.COMPLEX_T, inv); ak.overload(ak.log, ak.COMPLEX_T, log); ak.overload(ak.log, ak.NUMBER_T, logR); ak.overload(ak.neg, ak.COMPLEX_T, neg); ak.overload(ak.sin, ak.COMPLEX_T, sin); ak.overload(ak.sinh, ak.COMPLEX_T, sinh); ak.overload(ak.sqrt, ak.COMPLEX_T, sqrt); ak.overload(ak.sqrt, ak.NUMBER_T, sqrtR); ak.overload(ak.tan, ak.COMPLEX_T, tan); ak.overload(ak.tanh, ak.COMPLEX_T, tanh); ak.overload(ak.add, [ak.COMPLEX_T, ak.COMPLEX_T], add); ak.overload(ak.add, [ak.COMPLEX_T, ak.NUMBER_T], addCR); ak.overload(ak.add, [ak.NUMBER_T, ak.COMPLEX_T], addRC); ak.overload(ak.dist, [ak.COMPLEX_T, ak.COMPLEX_T], dist); ak.overload(ak.dist, [ak.COMPLEX_T, ak.NUMBER_T], distCR); ak.overload(ak.dist, [ak.NUMBER_T, ak.COMPLEX_T], distRC); ak.overload(ak.div, [ak.COMPLEX_T, ak.COMPLEX_T], div); ak.overload(ak.div, [ak.COMPLEX_T, ak.NUMBER_T], divCR); ak.overload(ak.div, [ak.NUMBER_T, ak.COMPLEX_T], divRC); ak.overload(ak.eq, [ak.COMPLEX_T, ak.COMPLEX_T], eq); ak.overload(ak.eq, [ak.COMPLEX_T, ak.NUMBER_T], eqCR); ak.overload(ak.eq, [ak.NUMBER_T, ak.COMPLEX_T], eqRC); ak.overload(ak.mul, [ak.COMPLEX_T, ak.COMPLEX_T], mul); ak.overload(ak.mul, [ak.COMPLEX_T, ak.NUMBER_T], mulCR); ak.overload(ak.mul, [ak.NUMBER_T, ak.COMPLEX_T], mulRC); ak.overload(ak.ne, [ak.COMPLEX_T, ak.COMPLEX_T], ne); ak.overload(ak.ne, [ak.COMPLEX_T, ak.NUMBER_T], neCR); ak.overload(ak.ne, [ak.NUMBER_T, ak.COMPLEX_T], neRC); ak.overload(ak.pow, [ak.COMPLEX_T, ak.COMPLEX_T], pow); ak.overload(ak.pow, [ak.COMPLEX_T, ak.NUMBER_T], powCR); ak.overload(ak.pow, [ak.NUMBER_T, ak.COMPLEX_T], powRC); ak.overload(ak.pow, [ak.NUMBER_T, ak.NUMBER_T], powRR); ak.overload(ak.sub, [ak.COMPLEX_T, ak.COMPLEX_T], sub); ak.overload(ak.sub, [ak.COMPLEX_T, ak.NUMBER_T], subCR); ak.overload(ak.sub, [ak.NUMBER_T, ak.COMPLEX_T], subRC);
Note that we must check that the dispatch functions for
arg
and conj
haven't been defined by another type before we add them, unlike those that we know for certain have been defined in the core ak
library.The implementations of these overloads can be found in
Complex.js
Using ak.complex
Program 1 illustrates some of the basic arithmetic operations described above.
Program 1: Complex Arithmetic



Plotting the results of a function of complex numbers is a bit of a tricky job since we need to convey two pieces of information; either the real and imaginary parts or the magnitude and argument. The latter are generally easier to deal with, as illustrated by program 2.
Program 2: Complex Square Root



Here we have two new chart objects, both attached to the same canvas. The first is a contour plot which represents the result of a function of two arguments with the colour of the pixel plotted at the point they represent. The second is a vector plot which represents starting points with dots and connects them to end points with lines.
The colours used to represent the values in the contour plot are set by the
palette
method which expects the number of levels as the first argument and an array of colour levels to interpolate between from the lowest, 0, to the highest, 1. Note that the bounds must be three, rather than two, dimensional arrays so that the chart can choose the correct colour for the value at each point.The vector plot requires an array of
ak.ui.pointer
objects rather than the points that are expected by simple graphs. These expect the starting point as an array for their first argument and the offsets to the end point as an array for the second.In program 2 the contour plot represents the magnitude of the result of the function, with darker hues indicating smaller numbers, and the vector plot just their arguments, by giving the pointers constant length.
As we should expect, those values with large negative real parts and small imaginary parts point in the direction of \(\pm i\) and those with large negative real parts and small imaginary parts point in the direction of +1.
Now this is all well and good, but let's face it, there's only one kind of graph of complex numbers that we're really interested in...
Fractals Baby!
Figure 2: What On Earth Was I Thinking?
Made from hemp.
As is tragically shown in figure 2.
Perhaps the most famous fractal is the Mandelbrot set, although I suspect that the majority of my fellow unwashed hippies whose torsos it adorned had no idea how it was defined.
The truly remarkable thing about it is how simple it actually is. At its heart is the quadratic equation
\[
f_c(z) = z^2 + c
\]
Now, if for some complex \(c\) and \(z=0\), we repeatedly set \(z = f_c(z)\) and \(z\) never flies off to infinity then \(c\) is a member of the Mandelbrot set. For example, the real value minus one is a member since
\[
\begin{align*}
f_{1}(0) &= \phantom{}0^2 + 1 = 1\\
f_{1}(1) &= 1^2 + 1 = \phantom{}0
\end{align*}
\]
whereas plus one isn't because
\[
\begin{align*}
f_{1}(0) &= 0^2 + 1 = 1\\
f_{1}(1) &= 1^2 + 1 = 2\\
f_{1}(2) &= 2^2 + 1 = 5\\
f_{1}(5) &= 5^2 + 1 = 26
\end{align*}
\]
and so on.In general it's quite tricky to actually prove that a complex number is in the Mandelbrot set, so the usual tactic is to approximate it with the set of numbers that don't lead to \(z\) growing above some given magnitude within some given number of assignments of \(z = f_c(z)\). Rather than simply plot the numbers that are in the Mandelbrot set program 3 plots the number of iterations it takes for \(z\) to grow above that magnitude for that familiar psychedelic image.
Program 3: The Mandelbrot Set



Cosmic.
Leave a comment