Thicket data repository for the EEG
1{
2 "id": "https://www.tunbury.org/2020/08/23/mandlebrot-set-3d",
3 "title": "Mandelbrot Set 3D",
4 "link": "https://www.tunbury.org/2020/08/23/mandlebrot-set-3d/",
5 "updated": "2020-08-23T12:41:29",
6 "published": "2020-08-23T12:41:29",
7 "summary": "Back in 2015 in one of the earliest posts on this site I wrote about my fascination with the Mandelbrot set.",
8 "content": "<p>Back in 2015 in one of the earliest posts on this site I wrote about my fascination with the Mandelbrot set.</p>\n\n\\[Z_{n+1}=Z_n^2+c\\]\n\n<p>In that <a href=\"https://www.tunbury.org/mandlebrot-set/\">post</a>, I presented a table of giving two example iterations with different values of C showing both a <em>bound</em> and <em>unbound</em> condition. I’d never really thought about the actual value the bound series tended towards, after all the final plot was the number of iterations it took to become unbound. i.e. where \\(\\lvert Z \\rvert > 2\\)</p>\n\n<p>Watching an episode of <a href=\"https://youtu.be/ETrYE4MdoLQ\">Numberphile on YouTube</a>, it became clear that I’d really missed out on some interesting behaviour… about rabbits, which then led me to a <a href=\"https://youtu.be/ovJcsL7vyrk\">second video</a> and a view of the Mandelbrot set as I’d never seen it before.</p>\n\n<p>The table below mirrors that I presented my by original post but additionally shows the outcome at \\(C=-1.3\\).</p>\n\n\n\n \n \n \n C = 0.2\n C = 0.3\n C = -1.3\n \n \n \n \n 0\n 0.000000\n 0.000000\n 0.000000\n \n \n 1\n 0.200000\n 0.300000\n -1.300000\n \n \n 2\n 0.240000\n 0.390000\n 0.390000\n \n \n 3\n 0.257600\n 0.452100\n -1.147900\n \n \n 4\n 0.266358\n 0.504394\n 0.017674\n \n \n 5\n 0.270946\n 0.554414\n -1.299688\n \n \n 6\n 0.273412\n 0.607375\n 0.389188\n \n \n 7\n 0.274754\n 0.668904\n -1.148533\n \n \n 8\n 0.275490\n 0.747432\n 0.019128\n \n \n 9\n 0.275895\n 0.858655\n -1.299634\n \n \n 10\n 0.276118\n 1.037289\n 0.389049\n \n \n 11\n 0.276241\n 1.375968\n -1.148641\n \n \n 12\n 0.276309\n 2.193288\n 0.019376\n \n \n 13\n 0.276347\n 5.110511\n -1.299625\n \n \n 14\n 0.276368\n 26.417318\n 0.389024\n \n \n 15\n 0.276379\n 698.174702\n -1.148660\n \n \n 16\n 0.276385\n #NUM!\n 0.019421\n \n \n 17\n 0.276389\n #NUM!\n -1.299623\n \n \n 18\n 0.276391\n #NUM!\n 0.389020\n \n \n 19\n 0.276392\n #NUM!\n -1.148664\n \n \n 20\n 0.276392\n #NUM!\n 0.019429\n \n \n 21\n 0.276393\n #NUM!\n -1.299623\n \n \n 22\n 0.276393\n #NUM!\n 0.389019\n \n \n 23\n 0.276393\n #NUM!\n -1.148664\n \n \n 24\n 0.276393\n #NUM!\n 0.019430\n \n \n 25\n 0.276393\n #NUM!\n -1.299622\n \n \n 26\n 0.276393\n #NUM!\n 0.389019\n \n \n 27\n 0.276393\n #NUM!\n -1.148665\n \n \n 28\n 0.276393\n #NUM!\n 0.019430\n \n \n 29\n 0.276393\n #NUM!\n -1.299622\n \n \n 30\n 0.276393\n #NUM!\n 0.389019\n \n \n 31\n 0.276393\n #NUM!\n -1.148665\n \n \n\n\n<p>At \\(C=-1.3\\) there is a clear repeating pattern of four values.</p>\n\n<p>In Excel set row 1 as the value of C starting at -2 and incrementing by say 0.02 up to 0.0. Then run the iterations in columns below each value starting at 0. Extend the columns for perhaps 40 iterations.</p>\n\n<p><img alt=\"\" src=\"https://www.tunbury.org/images/Excel-Formulas-Shown.png\"></p>\n\n<p>Now plot iterations 20-40 (when the values are typically stable) against the value of C.</p>\n\n<p><img alt=\"\" src=\"https://www.tunbury.org/images/Excel-Plot.png\"></p>\n\n<p>I want to plot the real component of C on the x-axis, then imaginary component on the y-axis and the real part of the iterated sequence on the z-axis. Where the sequence repeats I’ll plot all points within the sequence which looks to be what was done in the YouTube clip.</p>\n\n<p><img alt=\"\" src=\"https://www.tunbury.org/images/3d-axis.svg\"></p>\n\n<p>I’m sitting here with my new, albeit secondhand, Mac Pro so let’s write this in Swift and do all the calculation and graphics on the GPU using Metal.</p>\n\n<p>The problem is well suited to GPU based calculations with a small kernel running once for each possible set of input coordinates, however the output of a massive sparsely populated three dimensional array seemed unfortunate. Suggesting a resolution of 2048 x 2048 and allowing iterative sequences of up to 1024 gives potentially 4 billion points… Therefore, I have opted for an output vector/array indexed with a shared atomically-incremental counter.</p>\n\n<p>To use the GPU to perform the calculations the program needs to be written in Metal Shading Language which is a variation on C++, but first the GPU need to be initialised from Swift which for this project is pretty straightforward. We’ll need a buffer for the output vector and another one for the counter:</p>\n\n<div><div><pre><code>vertexBuffer = device.makeBuffer(length: MemoryLayout<Vertex>.stride * 2048 * 2048, options: [])\ncounterBuffer = device.makeBuffer(length: MemoryLayout<UInt>.size, options: [])\n</code></pre></div></div>\n\n<p>Then we create a library within the GPU device where the name parameter exactly matches the MTL function name we want to call</p>\n\n<div><div><pre><code>let library = device.makeDefaultLibrary()\nlet calculate_func = library?.makeFunction(name: \"calculate_func\")\npipeLineState = try device.makeComputePipelineState(function: calculate_func!)\n</code></pre></div></div>\n\n<p>The <code>calculate_func</code> is defined as follows</p>\n\n<div><div><pre><code>kernel void calculate_func(device VertexIn* result,\n uint2 index [[ thread_position_in_grid ]],\n device atomic_uint &counter [[ buffer(1) ]]) {\n\n float bufRe[1024];\n float bufIm[1024];\n\n float Cre = (float(index.x) * 3 / 2048) - 2;\n float Cim = (float(index.y) * 3 / 2048) - 1.5;\n\n float Zre = 0;\n float Zim = 0;\n \n bufRe[0] = 0;\n bufIm[0] = 0;\n\n for (int iteration = 1; (iteration < 1024) && ((Zre * Zre + Zim * Zim) <= 4); iteration++) {\n float ZNre = Zre * Zre - Zim * Zim + Cre;\n Zim = 2 * Zre * Zim + Cim;\n Zre = ZNre;\n \n bufRe[iteration] = Zre;\n bufIm[iteration] = Zim;\n \n for (int i = iteration - 1; i; i--) {\n if ((bufRe[iteration] == bufRe[i]) && (bufIm[iteration] == bufIm[i])) {\n for (; i < iteration; i++) {\n float red = abs(bufIm[i]) * 5;\n float green = abs(bufRe[i]) / 2;\n float blue = 0.75;\n \n uint value = atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);\n result[value].position = float3(Cre, Cim, bufRe[i]);\n result[value].color = float4(red, green, blue, 1);\n }\n return;\n }\n }\n }\n}\n</code></pre></div></div>\n\n<p>The first section is the standard calculation for \\(Z_{n+1}\\). The nested loop searches back through the previous values to see if we have had this value before. While this should be an exhaustive check of every value, I haven’t done that for performance reasons, but I did leave the check to be the exact floating point value rather than just 2 or 3 decimal places. If there is a match then all the points are copied to the output vector in a pretty colour.</p>\n\n<p>You can see the full code on <a href=\"https://github.com/mtelvers/threeDbrot\">Github</a>.</p>\n\n ",
9 "content_type": "html",
10 "author": {
11 "name": "Mark Elvers",
12 "email": "mark.elvers@tunbury.org",
13 "uri": null
14 },
15 "categories": [
16 "swift"
17 ],
18 "source": "https://www.tunbury.org/atom.xml"
19}