A TK Mandelbrot Set

My first attempt to make a faster Mandelbrot set in python used GTK3 instead of matplotlib. The result can still be seen, but more recently I decided to try rewriting it using the TK (tkinter) toolkit instead. The result is better, so the GTK3 version should be quietly forgotten.

Download mandel_tk.py.

The main reason it is better is that tkinter is more likely to be available on a python installation than GTK3. This is particularly true for installations on MacOS or Windows. A second reason is that it is faster. This is partly because the tkinter functions used scale bitmaps by integer ratios, whereas GTK3 does more sophisiticated arbitrary scaling. And I have now worked for longer on the TK version, and increased its functionality beyond the GTK3 version. However, it is now over 900 lines, which leaves much more room for mistakes than in the matplotlib version of around 120 lines. This code is useable: the gallery is proof of that. Perfectly polished it is not.

Starting by repeating the image from the GTK3 page,

$ curl -O https://www.sci-pi.org.uk/maths/mandel_tk.py
$ chmod +x mandel_tk.py
$ ./mandel_tk.py &

Click on Set, and paste
(-0.353978+0.629697i) to (-0.333842+0.649833i)
into the "coord string" box. Click "Check", the values of xmin etc. should change, then click "OK". Change "Max iter" to 384, click "Recompute", and then change the colour palette. One can select a region with the mouse to zoom in. If the application does not run, but complains about missing python modules, see the "getting started" links at the top right.

Mandelbrot Set, detail

the main difference with the TK version is that the ability to choose different colour maps has been added. Also the runtime is better than halved for this example.

The code uses numba for speed, although if numba is unavailable one simply needs to comment out the two lines

  from numba import jit

and

  @jit(nogil=True)

and it uses numpy and matplotlib (for its colormaps). Everything else it uses is in the standard python3 distribution.

How impressive is the speed of this example? One can guess the number of floating point operations performed. One iteration of the inner Mandelbrot loop contains a complex multiplication, a complex add, and a complex modulus: eleven real floating point operations (or is it nine?). In the above picture there are 804x804 pixels to calculate, and none took more than 384 iterations, so there were fewer than 2.73 billion floating point operations. It took 0.63 seconds, so it did not achieve more than 4.3 GFLOPS. One could try modifying the code to include a global counter which is updated by the number of iterations spent on each pixel, and one could then print a more accurate measure of the performance in GFLOPS at the end of each calculation. Or one could calculate an area which is entirely within the Mandelbrot set, so each pixel needs the full number of iterations.

A slightly unfortunate consequence of the decision to use as little as possible that is not in a standard python3 distribution is that there is no function available to write PNG images, so that function is written from scratch.

There follow a few notes on some of the subtleties of the code.

Firstly tkinter can be called from the main thread only, and strange effects occur if one breaks this rule (such as it mostly working on Linux, and mostly not working on MacOS). In order to keep the GUI responsive whilst a potentially long calculation is progressing, the calculation is done in a separate thread. But this thread may not directly update any window, nor even call tkinter's PhotoImage function which merely manipulates data without plotting anything.

The solution is that the calculation thread and the main thread share a queue. The calculation thread puts on this queue functions it wishes the main thread to call, and the main thread checks the queue whenever it is idle. To save unnecessary process activity, this queue checking is performed only whilst a calculation is running.

Being able to copy and paste the co-ordinates reported above the image can be useful, but neither GTK3 nor TK allow the contents of labels to be selected by the mouse. So for the TK version the top line of the title is an editable text entry widget in read-only mode, which solves this issue.

The dialog box for manually entering co-ordinates is not perfect, but the TK version does not grab focus, so one can still move the mouse across the image and have the co-ordinates displayed in the bottom right update. It does then have to disable the buttons on the main window, or else one could launch multiple dialog boxes, and start calculations whilst editing coordinates. It then needs to make sure that it re-enables them however it is closed.

The dialog box now also has a field for entering a string of co-ordinates such as those copied from the top status line, or from the examples in the gallery on this site. The string parsing is fairly basic, it just looks for four numbers, and will fail to parse "i" on its own, so instead one must write "1i" or "1.0i". The "check" part is necessary as the idea of the image being square is still firmly written into the code. Perhaps this should be relaxed, but this code is not meant to be perfect and complete in every way.

The recompute button changes to an abort calculation box whilst a calculation is progressing.

The canvas on which the image is drawn has been made scrollable, so one can produce images greater than the size of one's screen.

And if this is not fast enough, one can parallelise it across the multiple cores in (most) Pis.