Finally, the last accessory I ordered for my Raspberry Pi Zero (that’s the little red thing behind my keyboard) has arrived – an Acer 43″ ET430K monitor. The Zero won’t quite drive this monitor at its maximum resolution of 3840×2160 pixels, but as you can see, you get enough real estate to do real graphics with “ASCII Art” (well, Unicode Art, anyway)!
Some readers will recognise the image as the famous Mandelbrot Set, named after the mathematician Benoit Mandelbrot, who studied fractals. According to Wikipedia a fractal is a mathematical set that exhibits a repeating pattern displayed at every scale – they are interesting because they produce patterns that are very similar to things we can see around us – both in living organisms and landscapes – they seem to be part of the fundamental fabric of the universe.
Fractals are are also interesting because they produce images of staggering beauty, as you can see on the pages linked to above and one of our rotating banner pages, where a one-line form of the APL expression which produced the image is embedded:
The colourful images of the Mandelbrot set are produced by looking at a selection of points in the complex plane. For each point c, start with a value of 0 for z, repeat the computation z = c + z², and colour the point according to the number of iterations required before the function becomes “unbounded in value”.
In APL, the function for each iteration can be expressed as {c+⍵*2}
, where c is the point and ⍵ (the right argument of the function) is z (initially 0, and subsequently the result of the previous iteration):
f←{c+⍵*2} ⍝ define the function
c←1J1 ⍝ c is 1+i (above and to the right of the set)
(f 0)(f f 0)(f f f 0)(f f f f 0) (f f f f f 0)
1J1 1J3 ¯7J7 1J¯97 ¯9407J¯193
If you are not familiar with complex numbers, those results may look a bit odd. While complex addition just requires adding the real and the imaginary parts independently, the result of multiplication of two complex numbers (a + bi) and (c + di) is defined as (ac-db) + (bc+ad)i. Geometrically, complex multiplication is a combined stretching and rotation operation.
Typing all those f’s gets a bit tedious, fortunately APL has a power operator ⍣
, a second-order function that can be used to repeatedly apply a function. We can also compute the magnitude of the result number using |
:
(|(f⍣6)0)(|(f⍣7)0)
8.853E7 7.837E15
Let’s take a look at what happens if we choose a point inside the Mandelbrot set:
c←0.1J0.1
{|(f⍣⍵)0}¨ 1 2 3 4 5 6 7 8 9 10
0.1414 0.1562 0.1566 0.1552 0.1547 0.1546 0.1546 0.1546 0.1546 0.1546
Above, I used an anonymous function so I could pass the number of iterations in as a parameter, and use the each operator ¨
to generate all the results up to ten iterations. In this case, we can see that the magnitude of the result stabilises, which is why the point 0.1 + 0.1i is considered to be inside the Mandelbrot set.
Points which are just outside the set will require varying numbers of applications of f before the magnitude “explodes”, and if you colour points according to how many iterations are needed and pick interesting areas along the edge of the set, great beauty is revealed.
The above image is in the public domain and was created by Jonathan J. Dickau using ChaosPro 3.3 software, which was created by Martin Pfingstl.
Our next task is to create array of points in the complex plane. A helper function unitstep
generates values between zero and 1 with a desired number of steps, so I can vary the resolution and size of the image. Using it, I can create two ranges, multiply one of them by i (0J1
in APL) and use an addition table (∘.+
) to generate the array:
⎕io←0 ⍝ use index origin zero
unitstep←{(⍳⍵+1)÷⍵}
unitstep 6
0 0.1667 0.3333 0.5 0.6667 0.8333 1
c←¯3 × 0.7J0.5 - (0J1×unitstep 6)∘.+unitstep 6
c
¯2.1J¯1.5 ¯1.6J¯1.5 ¯1.1J¯1.5 ¯0.6J¯1.5 ¯0.1J¯1.5 0.4J¯1.5 0.9J¯1.5
¯2.1J¯1 ¯1.6J¯1 ¯1.1J¯1 ¯0.6J¯1 ¯0.1J¯1 0.4J¯1 0.9J¯1
¯2.1J¯0.5 ¯1.6J¯0.5 ¯1.1J¯0.5 ¯0.6J¯0.5 ¯0.1J¯0.5 0.4J¯0.5 0.9J¯0.5
¯2.1 ¯1.6 ¯1.1 ¯0.6 ¯0.1 0.4 0.9
¯2.1J00.5 ¯1.6J00.5 ¯1.1J00.5 ¯0.6J00.5 ¯0.1J00.5 0.4J00.5 0.9J00.5
¯2.1J01 ¯1.6J01 ¯1.1J01 ¯0.6J01 ¯0.1J01 0.4J01 0.9J01
¯2.1J01.5 ¯1.6J01.5 ¯1.1J01.5 ¯0.6J01.5 ¯0.1J01.5 0.4J01.5 0.9J01.5
The result is subtracted from 0.7J0.5 (0.7 + 0.5i) to get the origin of the complex plane slightly off centre in the middle, and multiplied the whole thing by 3 to get a set of values that brackets the Mandelbrot set.
Note that APL expressions are typically rank and shape invariant, so our function f can be applied to the entire array without changes. Since our goal is only to produce ASCII art, we don’t need to count iterations, we can just compare the magnitude of the result with 9 to decide whether the point has escaped:
9<|f 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
9<|(f⍣3) 0 ⍝ Apply f 3 times
1 1 1 0 0 0 1
1 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
1 0 0 0 0 0 0
1 1 1 0 0 0 1
We can see that points around the edge start escaping after 3 iterations. Using an anonymous function again, we can observe more and more points escape, until we recognise the (very low resolution) outline of the Mandelbrot set:
]box on
{((⍕⍵)@(⊂0 0))' #'[9>|(f⍣⍵)0]}¨2↓⍳9
┌───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│2######│3 ### │4 │5 │6 │7 │8 │
│#######│ ######│ ### │ # │ # │ # │ # │
│#######│#######│ ##### │ #### │ #### │ ### │ ### │
│#######│#######│###### │ ##### │ ##### │ ##### │ #### │
│#######│#######│ ##### │ #### │ #### │ ### │ ### │
│#######│ ######│ ### │ # │ # │ # │ # │
│#######│ ### │ │ │ │ │ │
└───────┴───────┴───────┴───────┴───────┴───────┴───────┘
The last example allowed me to sneak in a preview of the new “at” operator coming in Dyalog v16.0. It is a functional merge operator that I am using to insert the formatted right argument (the number of iterations) into position (0 0) of each matrix.
If I use our remote IDE (RIDE) on my Windows machine and connect to the Pi, I can have an APL session on the Pi with 3840×2160 resolution. In this example, I experimented with grey scale “colouring” the result by rounding down and capping the result at 10, and using the character ⍟ (darkest) for 0, ○ for 1-5, × for 6-9, and blank for points that “escaped”, by indexing into an array of ten characters:
'⍟○○○○○×××× '[10⌊⌊|(f⍣9)c]
Who needs OpenGL?! (click on the image to enlarge)