I updated my Inflector Gadget, adding a keyframe animation feature among other goodies. I also made a new page for it, where all the downloads and documentation are to be found. Go check it out!

PS: Inflector Gadget can make images like these in very little time:

]]>Previously I wrote about an automated Julia morphing method extrapolating patterns in the binary representation of external angles, and then tracing external rays. However this was impractical as it was \(O(p^2)\) for final period \(p\) and the period typically more than doubles at each next level of morphing. This week I devised an \(O(p)\) algorithm, which requires a little bit of setting up and doesn't always work but when it works it works very well.

The first key insight was that in embedded Julia sets, the primary spirals and tips are distinguishable by the preperiods of the Misiurewicz points at their centers. Moreover when using the "full" Newton's method algorithm for Misiurewicz points that rejects lower preperiods by division, the basins of attraction comfortably enclose the center of the embedded Julia set itself.

So, we can choose the appropriate (pre)period to get to the center of the spiral either inwards towards the main body of the Mandelbrot set or outwards towards its tips. Now, from a Misiurewicz center of a spiral, Newton's method for periodic nucleus finding will work for any of the periods that form the structural spine of the spiral - these go up by a multiple of the period of the influencing island. From these nuclei we can jump to the Misiurewicz spiral on the other side, using Newton's method again. In this way we can algorithmically find any nucleus or Misiurewicz point in the structure of the embedded Julia set.

Some images should make this clearer at this point: blue means Newton's method for nucleus, red means Newton's method for Misiurewicz point, nuclei are labeled with their period, Misiurewicz points with preperiod and period in that order, separated by 'p'.

The second key insight was that the atom domain coordinate of the tip of the treeward branch at each successive level was scaled by a power of 1.5 from the one at the previous level. Because atom domain coordinates correspond to the unit disc, this means they are closer to the nucleus. This allowed an initial guess for finding the Misiurewicz point at the tip more precisely (the first insight does only apply to "top-level" embedded Julia sets, not their morphings - there is a "symmetry trap" that breaks Newton's method because the boundary of the basins of attraction passes through the point we want to start from). I implemented a Newton's method iteration to find a point with a given atom domain coordinate. This relationship is only true in the limit, so the input to the automatic morphing algorithm starts at the first morphing, rather than the top level embedded Julia set.

My first test was quite challenging: to morph a tree with length 7 arms, from an embeddded Julia set at angled internal address:

1_{1/2}→2_{1/2}→3_{2/5}→15_{4/7}→88

The C code (full link at the bottom) that sets up the parameters for this morphing looks like this:

#ifdef EXAMPLE_1 const char *embedded_julia_ray = ".011100011100011011100011011100011100011100011011100011011100011100011100011011100011100001110001101110010010010010010010010001110001110001101110001101110001110001110001101110001101110001110001110001101110001110000111000110111(001)"; int ray_preperiod = 225; int ray_period = 3; double _Complex ray_endpoint = -1.76525599938987623396492597243303e+00 + 1.04485517375987067290733632798876e-02 * I; int influencing_island_period = 3; int embedded_julia_set_period = 88; int denominator_of_rotation = 5; int arm_length = 7; double view_size_multiplier = 3600; #endif

The ray lands on the treeward-tip Misiurewicz point of the first morphed Julia set, this end point is cached to avoid long ray tracing computations. The next 4 numbers are involved in the iterative morphing calculations of the relevant periods and preperiods, with the arm length being the primary variable to adjust once the Julia set is found. The view size multiplier sets how to zoom out from the central morphed figure to frame the result nicely, maybe I can find a good heuristic to determine this based on arm length.

The morphing looks like this:

The second example is similar, starting with the island with this angled internal address, with tree morphing arm length 9.

1_{1/2}→2_{1/2}→3_{1/2}→4_{1/2}→8_{1/15}→116_{1/2}→119

The third and final example (for now) is simpler still, starting at the island with this internal address, with tree morphing arm length 1.

1_{1/3}→3_{1/2}→4_{11/23}→89

The code for example 3 contains an ugly hack, because the method for guessing the location of the next Misiurewicz point (for starting Newton's method iterations) isn't good enough - the radius is accurate, but the angle is not - my atom domain coordinate method is clearly not the correct one in general...

Here are the timings in seconds for calculating the coordinates (not parallelized) and rendering the images (I used m-perturbator-offline at 1280x720, the parallel efficiency is somewhat low because it doesn't know the center point is already a good reference and it tries to find one in the view - it would be much faster if I let it take the primary reference as external input - more things TODO):

morph | coordinates | image rendering | |||||||
---|---|---|---|---|---|---|---|---|---|

eg1 | eg2 | eg3 | eg1 | eg2 | eg3 | ||||

real | user | real | user | real | user | ||||

1 | 0 | 0 | 0 | 0.633 | 2.04 | 0.625 | 2.00 | 0.551 | 1.74 |

2 | 0 | 0 | 0 | 0.817 | 2.41 | 0.943 | 2.31 | 0.932 | 3.19 |

3 | 0 | 0 | 0 | 1.16 | 3.56 | 1.43 | 4.53 | 1.26 | 4.19 |

4 | 1 | 0 | 0 | 1.37 | 4.38 | 1.86 | 6.06 | 1.73 | 5.86 |

5 | 1 | 2 | 1 | 2.29 | 7.45 | 3.43 | 11.4 | 2.67 | 8.78 |

6 | 2 | 4 | 2 | 3.95 | 12.3 | 5.73 | 18.4 | 4.26 | 14.9 |

7 | 10 | 14 | 2 | 7.42 | 23.6 | 8.66 | 26.7 | 6.90 | 21.1 |

8 | 24 | 37 | 7 | 28.2 | 95.1 | 42.4 | 142 | 12.6 | 36.4 |

9 | 92 | 155 | 27 | 63.7 | 257 | 92.6 | 292 | 21.5 | 63.5 |

10 | 288 | 442 | 51 | 141 | 419 | 207 | 609 | 77.5 | 263 |

total | 418 | 654 | 90 | 259 | 774 | 372 | 1120 | 137 | 430 |

The code is part of my mandelbrot-numerics project. You also need my mandelbrot-symbolics project to compile the example program, and you may also want mandelbrot-perturbator to render the output (note: the GTK version is currently hardcoded to 65536 maximum iteration count, which isn't enough for deeper morphed Julia sets - adding runtime configuration for this is my next priority). Other deep zoomers are available, for example my Kalles Fraktaler 2 + GMP fork with Windows binaries available (that also work in WINE on Linux).

]]>On the fractal chats Discord server, it was discussed that the "elliptic" variation in fractal flame renderers suffered from precision problems. So I set about trying to fix them. The test parameters are here: elliptic-precision-problems.flame. It looks like this:

The black holes are the problem. Actually it turns out that the main cause of the hole was the addition of an epsilon to prevent division by zero in the "spherical variation", removing that gives this image, still with small black holes in the spirals:

The original code for the flam3 implementation of the elliptic variation is:

void var62_elliptic (flam3_iter_helper *f, double weight) { /* Elliptic in the Apophysis Plugin Pack */ double tmp = f->precalc_sumsq + 1.0; double x2 = 2.0 * f->tx; double xmax = 0.5 * (sqrt(tmp+x2) + sqrt(tmp-x2)); double a = f->tx / xmax; double b = 1.0 - a*a; double ssx = xmax - 1.0; double w = weight / M_PI_2; if (b<0) b = 0; else b = sqrt(b); if (ssx<0) ssx = 0; else ssx = sqrt(ssx); f->p0 += w * atan2(a,b); if (f->ty > 0) f->p1 += w * log(xmax + ssx); else f->p1 -= w * log(xmax + ssx); }

When x is near +/-1 and y is near 0, xmax is near 1, so a is near +/- 1, so there is a catastrophic cancellation (loss of significant digits) in the calculation of b = 1 - a*a. But it turns out that b doesn't need to be computed at all, because atan(a / sqrt(1 - a*a)) is the same as asin(a).

There is a second problem with ssx = xmax - 1, as xmax is near 1 there is a catastrophic cancellation here too. So the next step is to see how to calculate ssx without subtracting two values of roughly equal size and thus losing precision. Some algebra:

ssx = xmax - 1 = 0.5 (sqrt(tmp+x2)+sqrt(tmp-x2)) - 1 = 0.5 (sqrt(tmp+x2)+sqrt(tmp-x2) - 2) = 0.5 (sqrt(tmp+x2)-1 + sqrt(tmp-x2)-1 = 0.5 (sqrt(x*x+y*y+2*x+1)-1 + sqrt(x*x+y*y-2*x+1)-1) = 0.5 (sqrt(u+1)-1 + sqrt(v+1)-1)

Now we have subexpressions of the form sqrt(u+1)-1, which will lose precision when u is near 0. One way of doing this is to use a Taylor series for the function expanded about u=0, then converting this to a Padé approximant. I used a Wolfram Alpha Open Code Notebook to do this, here is the highlight:

> PadeApproximant[Normal[Series[Sqrt[x+1]-1, {x, 0, 8}]], {x, 0, 4}] (x/2+(3 x^2)/4+(5 x^3)/16+x^4/32) / (1+(7 x)/4+(15 x^2)/16+(5 x^3)/32+x^4/256)

Inspecting a plot of the difference between the approximant and the original function shows that it's accurate to about 1e-16 in the range -0.0625..+0.0625, which gives the following code implementation:

double sqrt1pm1(double x) { if (-0.0625 < x && x < 0.0625) { double num = 0; double den = 0; num += 1.0 / 32.0; den += 1.0 / 256.0; num *= x; den *= x; num += 5.0 / 16.0; den += 5.0 / 32.0; num *= x; den *= x; num += 3.0 / 4.0; den += 15.0 / 16.0; num *= x; den *= x; num += 1.0 / 2.0; den += 7.0 / 4.0; num *= x; den *= x; den += 1.0; return num / den; } return sqrt(1 + x) - 1; }

Now we can compute xmax - 1 without subtracting, and finally we can use log1p() to avoid inaccuracy from log of values near 1. The final code looks like this:

void var62_elliptic (flam3_iter_helper *f, double weight) { double x = f->tx; double y = f->ty; double x2 = 2.0 * x; double sq = f->precalc_sumsq; double u = sq + x2; double v = sq - x2; double xmaxm1 = 0.5 * (sqrt1pm1(u) + sqrt1pm1(v)); double a = x / (1 + xmaxm1); double ssx = xmaxm1; double w = weight / M_PI_2; if (ssx<0) ssx = 0; else ssx = sqrt(ssx); f->p0 += w * asin(clamp(a, -1, 1)); if (y > 0) f->p1 += w * log1p(xmaxm1 + ssx); else f->p1 -= w * log1p(xmaxm1 + ssx); }

The pudding, it works: the small black holes in the spirals are gone!

Finally, it seems elliptic is similar but not quite equal to the complex function 1 - acos(z) * 2 / PI. The standard library implementations probably has accuracy-preserving techniques that might be worth a look, I haven't checked yet. But the difference may be significant for images, notably the acos thing is conformal while the elliptic variation doesn't seem to be. Here's a comparison (elliptic on the left, acos on the right):

]]>It is known that in the Mandelbrot set, the smallest hyperbolic component of a given period is in the utter west of the antenna. Each atom is 16 times smaller and 4 times nearer the tip, which also means each successive atom domains is 4 times smaller (they all meet at the tip). This gives atom size \(O(16^{-p})\) and domain size \(O(4^{-p})\) where \(p\) is the period. I verified this relationship with some code:

#include <stdio.h> #include <mandelbrot-numerics.h> int main() { for (int period = 1; period < 64; period += 1) { printf("# period %d\n", period); fflush(stdout); mpfr_prec_t prec = 16 * period + 8; mpc_t guess, nucleus, size; mpc_init2(guess, prec); mpc_init2(nucleus, prec); mpc_init2(size, prec); mpc_set_d(guess, -2, MPC_RNDNN); while (prec > 2) { mpfr_prec_round(mpc_realref(guess), prec, MPFR_RNDN); mpfr_prec_round(mpc_imagref(guess), prec, MPFR_RNDN); m_r_nucleus(nucleus, guess, period, 64); printf("%ld ", prec); fflush(stdout); m_r_size(size, nucleus, period); mpc_norm(mpc_realref(size), size, MPFR_RNDN); mpfr_log2(mpc_realref(size), mpc_realref(size), MPFR_RNDN); mpfr_div_2ui(mpc_realref(size), mpc_realref(size), 1, MPFR_RNDN); mpfr_printf("%Re ", mpc_realref(size)); fflush(stdout); m_r_domain_size(mpc_realref(size), nucleus, period); mpfr_prec_round(mpc_realref(size), 53, MPFR_RNDN); mpfr_log2(mpc_realref(size), mpc_realref(size), MPFR_RNDN); mpfr_printf("%Re\n", mpc_realref(size)); fflush(stdout); prec--; mpfr_prec_round(mpc_realref(nucleus), prec, MPFR_RNDN); mpfr_prec_round(mpc_imagref(nucleus), prec, MPFR_RNDN); } printf("\n\n"); fflush(stdout); } return 0; }

Plotting the output shows it is so:

This makes a rough worst case estimate of the precision required to
accurately compute a size estimate (whether for atom or domain) from the
nucleus of the atom be around \(4 p\) bits. *(EDIT 2017-11-08 an earlier version of
this post incorrectly stated \(16 p\) bits, but I forgot to take the log2
properly when converting from size to bits required.)* But it turns out that
(at aleast for this sequence of atoms heading to the tip of the antenna) a
lot less precision is actually required. Here's a graph showing a seam
where the size estimate breaks down when the precision gets too low, and
doing some maths shows that the seam is at precision \(2 p\), a factor of
\(2\) better than the first guess:

It remains to investigate other sequences of atoms, to see how they behave. Chances are the \(2 p\) bits of precision required estimate is only necessary in rare cases like heading toward filament tips, and that aesthetically-chosen iterated Julia morphing atoms (for example) will be very much larger than would be expected from their period.

]]>Above is what atom domain rendering typicaly looks like. But there is a wealth of information in the periods of islands in the filaments, which isn't visible unless you scan for periods and annotate the image with labels (as in my previous post). After some experimentation, I figured out a way to make their domains visible and moreover get a domain size estimate (useful for labelling).

The hack I came up with is in two parts: the first part is in the iteration calculations. Atom domain calculation typically works like this:

for (iteration = 1; iterating; ++iteration) { z = z * z + c; if (abs(z) < minimum) { minimum = abs(z); p = iteration; zp = z; } ... }

The filtering hack adds another pair of \((q, z_q)\) to the existing \((p, z_p)\), only this time filtered by a function of iteration number:

... if (abs(z) < minimum2 && accept(iteration)) { minimum2 = abs(z); q = iteration; zq = z; } ...

For the image above the `accept()`

filter function was:

bool accept(int p) { return p >= 129 && (p % 4) != 1; }

The second part of the hack is filtering when colouring, to allow the original regular atom domains to be visible too (without this part they get squashed by the new domains). Here's the colouring hack for the image above:

... int p = (computed p); double _Complex zp = (computed zp); if (reject(p)) { p = (computed q); zp = (computed zq); } // colour using p and zp ... bool reject(int p) { return p < 129; }

The image below uses slightly different filters:

bool accept(int p) { return p > 129 && (p % 4) == 2; } bool reject(int p) { return p <= 129 || (p % 4) == 1; }

I did manage to find some filters that showed domains in the filaments at this deeper zoom level, but I lost them while making other changes and I've been unable to recreate them - very frustrating.

These images show another minor development, colouring the domains according to the quadrants of \(z_p\), which meet at the nucleus. I was inspired to do this by Algorithm 9 of Wolf Jung's Mandel which shows the zeros of a particular period (I wanted to show all periods at once). This forms the basis of an improved periodicity scan algorithm, which iterates an image one step at a time, scanning for meeting quadrants at a local minimum of \(z_p\) and recording their locations and periods to an output buffer. This new algorithm is much more scalable to deep zooms (it will work fine with perturbation techniques, though I haven't implemented that yet - the previous algorithm definitely needed lots of arbitrary precision calculations). It might even be possible to accelerate on GPU, its parallelism is amenable.

The size estimate for the filtered domains is fairly similar to the atom domain size estimate I derived previously:

double filtered_domain_size(double _Complex nucleus, int period) { double _Complex z = 0; double _Complex dc = 0; double zq = 1.0/0.0; for (int q = 1; q <= period; ++q) { dc = 2 * z * dc + 1; z = z * z + nucleus; double zp = cabs(z); if (q < period && zp < zq && accept(q)) { zq = zp; } } return zq / cabs(dc); }

This can return infinity if the filter is too restrictive, but domain size of the period 1 cardioid is infinite too, so it's not a big deal for the caller to check and deal with it as appropriate.

I'll clean up the code a bit and push to my mandelbrot-* repositories soon.

]]>The Mandelbrot set contains many hyperbolic components (cardioid-like and disc-like regions), with hairy filaments connecting them in a tree-like way. Each component has a nucleus at its center, which has a periodic orbit containing 0. Each component is surrounded by an atom domain, which for discs has about 4 times the radius (the relationship for cardioids is less regular, but often has about the square root of the size). Labelling a picture of the Mandelbrot set with the periods can provide insights into its deeper structure, and most of the time using the atom domain size as the label size works pretty well.

Inspired by a feature of Power MANDELZOOM (scroll down to the 3rd image titled "Embedded Julia set") that locates periodic points that are too deep to see, I implemented a grid scan algorithm to find periodic points. I vaguelly recall Robert P. Munafo explaining this algorithm to me in private email, so most of the credit belongs with him. The font size variation is all mine though.

Using my mandelbrot-numerics and mandelbrot-graphics libraries, the period scan works like this:

// scan successively finer grids for periodsfor (int grid = mingridsize << 8; grid >= mingridsize; grid >>= 1) for (int y = grid/2; y < h; y += grid) for (int x = grid/2; x < w; x += grid) { double _Complex c0 = x + I * y; double _Complex dc0 = grid;// transform pixel coodinates to the 'c' planem_d_transform_forward(transform, &c0, &dc0);// find the period of a nucleus within a large box// uses Robert P. Munafo's Jordan curve methodint p = m_d_box_period_do(c0, 4.0 * cabs(dc0), maxiters); if (p > 0)// refine the nucleus location (uses Newton's method)if (m_converged == m_d_nucleus(&c0, c0, p, 16)) {// verify the period with a small box// if the period is wrong, the size estimates will be way offas[atoms].period = m_d_box_period_do(c0, 0.001 * cabs(dc0), 2 * p); if (as[atoms].period > 0) { as[atoms].nucleus = c0;// size of component using algorithm from ibiblio.org M-set e-notesas[atoms].size = cabs(m_d_size(c0, as[atoms].period));// size of atom domain using algorithm from an earlier blog post of mineas[atoms].domain_size = m_d_domain_size(c0, as[atoms].period);// shape of component (either cardioid or disc) after Dolotin and Morozov (2008 eq. 5.8)as[atoms].shape = m_d_shape_discriminant(m_d_shape_estimate(c0, as[atoms].period)); atoms++; } } }

This does give duplicates in the output array, but these can be removed later (I found it better to use a mask image (2D array) in which I marked circles around each label, after checking whether the location has already been marked, than to use a quadratic-time loop comparing locations with a threshold distance). Depending on the size of the circles, this also helps prevents messy label overlaps.

One problem is that the range of atom domain sizes can be huge, with domains in filaments being orders of magnitude smaller than the sizes present in embedded Julia sets. This can be fixed with some hacks:

The image above calculates the font size like this:

// convert to pixel coordinatesint p = as[a].period; double _Complex c0 = as[a].nucleus; double _Complex dc0 = p == 1 ? 1 : as[a].domain_size;// period 1 domain is infinitem_d_transform_reverse(transform, &c0, &dc0);// shrink disc labels a bit to avoid overlapsdouble fs = (as[a].shape == m_cardioid ? 1 : 0.5) * cabs(dc0);// rescale filament labels using properties of periods in this particular embedded Julia setif ((p % 4) != (129 % 4)) fs = 8 * log2(fs) + maxfontsize;// ensure a minimum label sizefs = fmax(fs, minfontsize);

The image below replaces the specific period property
`(p % 4) != (129 % 4)`

with `(p % 4) != 0`

. I'll
figure out how best to generalize this and allow command-line arguments,
at the moment I've just been editing the code and recompiling to adapt
to different views, hardly ideal.

You can click the pictures for bigger versions (a few MB each). The
last 3 images are centered on
`-1.9409856638151786271684397e+00 + 6.4820395780451436662598436e-04 i`

.
After a few more cleanups I'll push the code to my mandelbrot-graphics
git repository linked above.

**BEGIN UPDATE** I made a page about this
Kalles Fraktaler 2 + GMP
project. Fresh binary with bugfixes as of 2017-04-06! **END UPDATE**

Back at the end of 2014 I wrote some little programs to manipulate the output from Kalles Fraktaler: kf-extras. At that time I had tried to compile the source code on Linux, but failed miserably. More recently I tried again, and was finally successful. I then set about making some improvements, chief among which is using GMP (or optionally MPFR) for the high precision computations, instead of the original homebrew implementation. I used the Boost wrappers for C++. The advantage is a very significant speed boost from those libraries' optimized implementations.

My forked source is available, in the claude-gmp branch:

git clone https://code.mathr.co.uk/kalles-fraktaler-2.git cd kalles-fraktaler-2 git checkout claude-gmp

There are 64bit Windows binaries available too (which work fine in WINE), cross-compiled from Debian using MINGW (the first one, updated to fix a crasher bug in today's first version, is probably the one you want, unless you're testing for regressions):

2.11.1+gmp.20170330.1 (sig) |

2.11.1+gmp.20170330 (sig) |

2.11.1+gmp.20170313 (sig) |

2.11.1+gmp.20170307 (sig) |

2.9.3+gmp.20170307 (sig) |

Significant differences from upstream (apart from the GMP support) are:

- make-based build system for MINGW g++;
- various C++ fixes to make g++ happy;
- long double support built-in instead of separate DLL;
- zoom depth limited only by the system memory needed to store very high precision numbers (GMP has a limit too but it is unlikely to be reached).

Support is available via FractalForums, or send me an email.

]]>I hacked on Inflector Gadget to make it use double precision floating point (53 bits of mantissa compared to 24 for single precision). This allows more playing time before ugly pixelation artifacts appear. It requires OpenGL 4, if that doesn't work on your machine then you'll have to go back to v0.1 (sorry). Downloads:

inflector-gadget-0.2.tar.bz2 (sig)

|inflector-gadget-0.2-win.zip (sig)

or get the freshest source from git (browse inflector-gadget):

git clone https://code.mathr.co.uk/inflector-gadget.git

There's also a new command, 'D', which prints out the inflection coordinates. I added it so I could play with adaptive precision algorithms, more on that soon.

]]>Inspired by recent threads on FractalForums, I wrote a little gadget to do inflection mapping. This means translation and squaring of complex coordinates before regular Mandelbrot or Julia set iterations. Check the source for full details, the fragment shader GLSL is the place to start. You can download it here:

inflector-gadget-0.1.1.tar.bz2 (sig)

|inflector-gadget-0.1-win.zip (sig)

or get the freshest source from git (browse inflector-gadget):

git clone https://code.mathr.co.uk/inflector-gadget.git

It's a bit rough around the edges, press H to print the help to the terminal you started it from (or read the documentation), but in short, each click wraps the pattern twice around the clicked point, allowing you to sculpt patterns out of Julia sets (or the Mandelbrot set if you press M).

Similar sculpting can be achieved by zooming into specfic locations in the Mandelbrot set. The effect is much the same (the outer decorations are missing in the inflector gadget) but it takes a zillion times longer to zoom so deep as required.

]]>My video piece Monotone has been accepted to MADATAC 08 (2017). The exhibition runs January 12th to February 5th, at Centro Conde Duque, Madrid, Spain, and there is also a screening on January 17th.

There were some issues with video codecs, this is the one that worked out:

]]>ffmpeg -i video.mkv -i audio.wav \ -pix_fmt yuv420p -codec:v libx264 -profile:v high -level:v 4.1 -b:v 20M -b:a 192k \ monotone.mov

The Mandelbrot set is full of hyperbolic components (the circle-like and cardioid-like regions), each of which has a nucleus at its center, which has a superstable periodic orbit. For example the biggest cardioid has center 0 and period 1, while the circle to the left has center -1 and period 2 (verify by: \((0^2 + (-1))^2) + (-1) = 0\)).

Suppose we know the location of the nucleus (the \(c\) parameter) and we want to render a picture of the corresponding hyperbolic component. To do this we need to know its size. I tried to derive a size estimate myself, using Taylor series for \(\frac{\partial}{\partial z}\) using the fact that this derivative tends to \(1\) at the boundary of the component and is \(0\) at the nucleus, but the truncation error smashed everything to pieces. So I fell back on plan B: trying to understand the existing size estimate I found on ibiblio.org.

The size estimate using the notation on that page (go read it first) is \(\frac{1}{\beta \Lambda_n^2}\). I found the page a bit confusing at the first many readings, but reading the referenced paper and thinking hard while writing notes on paper helped me crack it. The size estimate forms a small section of the paper near the start, for reference:

Structure in the parameter dependence of order and chaos for the quadratic map

Brian R Hunt and Edward Ott

J. Phys. A: Math. Gen. 30 (1997) 7067–7076

Many dynamical systems are thought to exhibit windows of attracting periodic behaviour for arbitrarily small perturbations from parameter values yielding chaotic attractors. This structural instability of chaos is particularly well documented and understood for the case of the one-dimensional quadratic map. In this paper we attempt to numerically characterize the global parameter-space structure of the dense set of periodic "windows" occurring in the chaotic regime of the quadratic map. In particular, we use scaling techniques to extract information on the probability distribution of window parameter widths as a function of period and location of the window in parameter space. We also use this information to obtain the uncertainty exponent which is a quantity that globally characterizes the ability to identify chaos in the presence of small parameter uncertainties.

The basic idea is that under iteration of \(z\to z^2+c\), the small neighbourhood of the nucleus \(c\) bounces around the complex plane, being slightly distorted and stretched each time, except for one "central interval" at which the neighbourhood of \(z_{k p}\) contains the origin \(0\) and the next iteration folds the interval in half with quadratic scaling. Now the bouncing around the plane can be approximated as linear, with scaling given by the first derivative (with respect to \(z\)), and there is only one interval \(n = kp\) in which the full quadratic map needs to be preserved. We end up with something like this:

\[\begin{aligned} z_{n + p} = & c + \frac{\partial}{\partial z} z_{n+p-1} ( \\ & \vdots \\ & c + \frac{\partial}{\partial z} z_{n+3} ( \\ & c + \frac{\partial}{\partial z} z_{n+2} ( \\ & c + \frac{\partial}{\partial z} z_{n+1} ( \\ & c + z_n^2 ) ) ) \ldots ) \end{aligned}\]

Expanding out the brackets gives:

\[ z_{n + p} = \left(\prod_{k = 1}^{p - 1} \frac{\partial}{\partial z} z_{n + k}\right) z_n + \left(\sum_{m = 1}^p \prod_{k = m}^{p - 1} \frac{\partial}{\partial z} z_{n+k}\right) c \]

Writing:

\[\begin{aligned} \lambda_m &= \prod_{k = 1}^{m} \frac{\partial}{\partial z} z_{n + k} \\ \Lambda &= \lambda_{p - 1} \end{aligned}\]

the sum can have a factor of \(\Lambda\) drawn out to give:

\[ z_{n + p} = \Lambda \left( z_n^2 + \left( 1 + \lambda_1^{-1} + \lambda_2^{-1} + \ldots + \lambda_{p - 1}^{-1} \right) c \right) = \Lambda \left( z_n^2 + \beta c \right) \]

The final step is a change of variables where \(c_0\) is the nucleus:

\[\begin{aligned} Z &= \Lambda z \\ C &= \beta \Lambda^2 \left(c - c_0\right) \end{aligned}\]

Now there is self-similarity (aka renormalization):

\[Z_{n+1} = Z_n^2 + C\]

ie, one iteration of the new variable corresponds to \(p\) iterations of the original variable. (Exercise: verify the renormalization.) Moreover the definition of \(C\) gives the scale factor in the parameter plane, which gives the size estimate when we multiply by the size of the top level window (the paper referenced above uses \(\frac{9}{4}\) as the size, corresponding to the interval \(\left[-2,\frac{1}{4}\right]\) from cusp to antenna tip - using \(\frac{1}{2}\) makes circles' sizes approximately their radii).

Finally some C99 code to show how easy this size estimate is to compute in practice (see also my mandelbrot-numerics library):

double _Complex m_size(double _Complex nucleus, int period) { double _Complex l = 1; double _Complex b = 1; double _Complex z = 0; for (int i = 1; i < period; ++i) { z = z * z + nucleus; l = 2 * z * l; b = b + 1 / l; } return 1 / (b * l * l); }

As a bonus, using complex values gives an orientation estimate in addition to the size estimate - just use \(\arg\) and \(\left|.\right|\) on the result.

]]>My video piece Monotone has been accepted to the Mozilla Festival art exhibition. Mozilla Festival 2016 takes place October 28-30, at Ravensbourne College, London.

Since submitting the pre-rendered video loop I've been working on improving the real-time rendering mode of the Monotone software. The main bottle-neck at this time is the histogram equalisation to take the high dynamic range calculations down to a low dynamic range image for display. I did manage to get a large speed boost by calculating the histogram on a \(\frac{1}{4} \times \frac{1}{4}\) downscaled image, but on my hardware it only achieves \(\frac{1}{2} \times \frac{1}{2}\) of the desired resolution (HD 1920x1080). On my NVIDIA GTX 550 Ti with 192 CUDA cores I get 960x540 at 30 frames per second. Recent hardware has 1000s of cores, so perhaps it's just a matter of throwing more grunt at the problem.

If you want to try it (and have OpenGL 4 capable hardware, and development headers installed for GLFW and JACK, among other things; only tested on Debian):

git clone https://code.mathr.co.uk/monotone.git git clone https://code.mathr.co.uk/clive.git cd monotone/src make ./monotone

You can also browse the monotone source code repository. If you do have a significantly more powerful GPU than me, you can try to edit the source code monotone.cpp to change "#define SCALE 2" to "#define SCALE 1", which will make it target 1920x1080 instead of half that in each dimension. I'd love to hear back if you get it working (or if you have trouble getting it running, maybe I can help).

**UPDATE** here are some photos, the projection was really
impressive, so I'm satisfied even though the sound aspect was absent:

The festival was pretty interesting, many many things all going on at once. Highlight was the Sonic Pi workshop (though I spent most of the time dist-upgrade-ing to Debian Stretch so I could install it), and the Nature In Code workshop was also interesting (though it was packed full and uncomfortable so I didn't attend the full session).

]]>