Some of my images are in an online exhibition this month:

eRR0R(iii)

an exploration of the fertility of errors

in a world that covers its flaws in the blinding light of universal truths and institutionally reinforced regimes of visibility, we are interested in the fertile shades opened up by errors. the antiseptic intellectual environment our societies try to achieve, while arguably “healthy” and “safe” for the established values, has the huge disadvantage of obscuring any fundamentally different modes of existence. we [looked for] submissions that explore the fertility of errors and question our inherited worldview.

Here's my mini statement:

I work with mathematics and algorithms to make art. Sometimes it doesn't go to plan. I present recent failures experienced on the road to successful implementation of desired results.

I'm not sure if it'll be archived after the month is over, so experience it while you can.

]]>The essence of perturbation is to find the difference between the high precision values of a function at two nearby points, while using only the low precision value of the difference between the points. In this post I'll write the high precision points in CAPITALS and the low precision deltas in lowercase. There are two auxiliary operations needed to define the perturbation \(P\), \(B\) replaces all variables by their high precision version, and \(W\) replaces all variables by the sum of the high precision version and the low precision delta. Then \(P = W - B\):

\[\begin{aligned} B(f) &= f(X) &\text{ (emBiggen)}\\ W(f) &= f(X + x) &\text{ (Widen)}\\ P(f) &= W(f) - B(f) \\ &= f(X + x) - f(X) &\text{ (Perturb)} \end{aligned}\]

For example, perturbation of \(f(z, c) = z^2 + c\), ie, \(P(f)\), works out like this:

\[\begin{aligned} & P(f) \\ \to & f(Z + z, C + c) - f(z, c) \\ \to & (Z + z)^2 + (C + c) - (Z^2 + C) \\ \to & Z^2 + 2 Z z + z^2 + C + c - Z^2 - C \\ \to & 2 Z z + z^2 + c \end{aligned}\]

where in the final result the additions of \(Z\) and \(z\) have mostly cancelled out and all the terms are "small".

For polynomials, regular algebraic manipulation can lead to successful outcomes, but for other functions it seems some "tricks" are needed. For example, \(|x|\) (over \(\mathbb{R}\)) can be perturbed with a "diffabs" function proceeding via case analysis:

// evaluate |X + x| - |X| without catastrophic cancellation function diffabs(X, x) { if (X >= 0) { if (X + x >= 0) { return x; } else { return -(2 * X + x); } } else { if (X + x > 0) { return 2 * X + x; } else { return -x; } } }

This formulation was developed by laser blaster at fractalforums.com.

For transcendental functions, other tricks are needed. Here for example is a derivation of \(P(\sin)\):

\[\begin{aligned} & P(\sin) \\ \to & \sin(X + x) - \sin(X) \\ \to & \sin(X) \cos(x) + \cos(X) \sin(x) - \sin(X) \\ \to & \sin(X) (\cos(x) - 1) + \cos(X) \sin(x) \\ \to & \sin(X) \left(-2\sin^2\left(\frac{x}{2}\right)\right) + \cos(X) \sin(x) \\ \to & \sin(X) \left(-2\sin^2\left(\frac{x}{2}\right)\right) + \cos(X) \left(2 \cos\left(\frac{x}{2}\right) \sin\left(\frac{x}{2}\right)\right) \\ \to & 2 \sin\left(\frac{x}{2}\right) \left(-\sin(X) \sin\left(\frac{x}{2}\right) + \cos(X) \cos\left(\frac{x}{2}\right)\right) \\ \to & 2 \sin\left(\frac{x}{2}\right) \cos\left(X + \frac{x}{2}\right) \end{aligned}\]

Knowing when to apply the sum- and double-angle-formulae, is a bit of a mystery, especially if the end goal is not known beforehand. This makes implementing a symbolic algebra program that can perform these derivations quite a challenge.

In lieu of a complete symbolic algebra program that does it all on demand, here are a few formulae that I calculated, some by hand, some using Wolfram Alpha:

\[\begin{aligned} P(a) &= 0 \\ P(a f) &= a P(f) \\ P(f + g) &= P(f) + P(g) \\ P(f g) &= P(f) W(g) + B(f) P(g) \\ P\left(\frac{1}{f}\right) &= -\frac{P(f)}{B(f)W(f)} \\ P(|f|) &= \operatorname{diffabs}(B(f), P(f)) \\ P(\exp) &= \exp(X) \operatorname{expm1}(x) \\ P(\log) &= \operatorname{log1p}\left(\frac{x}{X}\right) \\ P(\sin \circ f) &= \phantom{-}2 \sin\left(\frac{P(f)}{2}\right)\cos\left(\frac{W(f)+B(f)}{2}\right) \\ P(\cos \circ f) &= -2 \sin\left(\frac{P(f)}{2}\right)\sin\left(\frac{W(f)+B(f)}{2}\right) \\ P(\tan \circ f) &= \frac{\sin(P(f))}{\cos(B(f))\cos(W(f))} \\ P(\sinh \circ f) &= 2 \sinh\left(\frac{P(f)}{2}\right)\cosh\left(\frac{W(f)+B(f)}{2}\right) \\ P(\cosh \circ f) &= 2 \sinh\left(\frac{P(f)}{2}\right)\sinh\left(\frac{W(f)+B(f)}{2}\right) \\ P(\tanh \circ f) &= \frac{\sinh(P(f))}{\cosh(B(f))\cosh(W(f))} \\ \end{aligned}\]

I hope to find time to add these to et soon.

**EDIT** there is a simpler and more general way to derive \(P(\sin)\)
and so on, using \(\sin(a) \pm \sin(b)\) formulae...

Atom domains in the Mandelbrot set surround mini-Mandelbrot islands. So too in the Burning Ship fractal. These pictures are coloured using the period for hue, and distance estimation for value. Saturation is a simple switch on escaped vs unescaped pixels. Rendered with some Fragmentarium code.

The algorithm is simple: store the iteration count when |Z| reaches a new minimum. The last iteration count so stored is the atom domain. Better start checking after the first iteration if you initialize with 0. IEEE floating point has infinities so you can initialize the stored |Z| value to 1.0/0.0.

I was hoping to use atom domains for interior checking, by using Newton's method to find limit cycles and seeing if their maximal Lyapunov exponent is less than 1, but it didn't work. My guesses are that Newton's method doesn't converge to the limit cycle, but instead to some phantom attractor, or that the maximal Lyapunov exponent isn't an indicator of interiority as I had hoped (I tried with plain determinant too, no joy there either). The method marked some exterior points as interior.

One thing that is interesting to me is the grey region of unescaped pixels with chaotic atom domains (the region is that colour because the anti-aliasing blends subpixels scattered across the whole spectrum into a uniform grey). I'm not sure whether it is an artifact of rendering at a limited iteration count and should be exterior, or if it really is interior and chaotic.

]]>The Burning Ship fractal is defined by iterations of:

\[ \begin{aligned} X &\leftarrow X^2 - Y^2 + A \\ Y &\leftarrow 2|XY| + B \end{aligned} \]

The Burning Ship set is those points \(A + i B \in \mathbb{C}\) whose iteration starting from \(X + i Y = 0\) remains bounded. In practice one iterates a maximum number of times, or until the point diverges (exercise suggested on Reddit: prove a lower bound on an escape radius that is sufficient for the Burning Ship, the Mandelbrot set has the bound \(R = 2\)). Note that traditionally the Burning Ship is rendered with the imaginary \(B\) axis increasing downwards, which makes the "ship" the right way up.

Traditional (continuous) iteration count (escape time) rendering tends to lead to a grainy appearance for this fractal, so I prefer distance estimation. To compute a distance estimate one can use partial derivatives (aka Jacobian matrix):

\[ \begin{aligned} \frac{\partial X}{\partial A} &\leftarrow 2 \left(X \frac{\partial X}{\partial A} - Y \frac{\partial Y}{\partial A}\right) + 1 \\ \frac{\partial X}{\partial B} &\leftarrow 2 \left(X \frac{\partial X}{\partial B} - Y \frac{\partial Y}{\partial B}\right) \\ \frac{\partial Y}{\partial A} &\leftarrow 2 \operatorname{sgn}(X) \operatorname{sgn}(Y) \left( X \frac{\partial Y}{\partial A} + \frac{\partial X}{\partial A} Y \right) \\ \frac{\partial Y}{\partial B} &\leftarrow 2 \operatorname{sgn}(X) \operatorname{sgn}(Y) \left( X \frac{\partial Y}{\partial B} + \frac{\partial X}{\partial B} Y \right) + 1 \end{aligned} \]

Then the distance estimate for an escaped point is (thanks to gerrit on fractalforums.org):

\[ d = \frac{||\begin{pmatrix}X & Y\end{pmatrix})||^2 \log ||\begin{pmatrix}X & Y\end{pmatrix}||}{\left|\left|\begin{pmatrix}X & Y\end{pmatrix} \cdot \begin{pmatrix} \frac{\partial X}{\partial A} & \frac{\partial X}{\partial B} \\ \frac{\partial Y}{\partial A} & \frac{\partial Y}{\partial B} \end{pmatrix} \right|\right|} \]

Then scale \(d\) by the pixel spacing, colouring points with small distance dark, and large distance light. I colour interior points dark too.

Perturbation techniques can be used for efficient deep zooms. Compute a high precision orbit of \(A,B,X,Y\), and have low precision deltas \(a,b,x,y\) for each pixel. It works out as:

\[ \begin{aligned} x &\leftarrow (2 X + x) x - (2 Y + y) y + a \\ y &\leftarrow 2 \operatorname{diffabs}(XY, Xy + xY + xy) + b \end{aligned} \]

where \(\operatorname{diffabs}(c, d) = |c + d| - |c|\) but expanded into case analysis to avoid catastrophic cancellation with limited precision floating point (this is I believe due to laser blaster on fractalforums.com):

\[ \operatorname{diffabs}(c, d) = \begin{cases} d & c \ge 0, c + d \ge 0 \\ -2c - d & c \ge 0, c + d < 0 \\ 2c + d & c < 0, c + d > 0 \\ -d & c < 0, c + d \le 0 \end{cases} \]

Due to the non-analytic functions, series approximation cannot be used. As with perturbation rendering of the Mandelbrot set, glitches can occur. It seems that Pauldelbrot's glitch criterion (originally posted on fractalforums.com) is also applicable, with a glitch when:

\[ |(X + x) + (Y + y) i|^2 < 10^{-3} |X + i Y|^2 \]

Glitched pixels can be recalculated with a new reference. It may be beneficial to pick as new references those pixels with the smallest LHS of the glitch criterion. The derivatives for distance estimation don't need to be perturbed as they are not "small", one can use \(X + x\) etc in the derivative recurrences.

When navigating the Burning Ship, it is noticeable that "mini-ships" occur, being distorted self-similar copies of the whole set. When passing by, embedded Julia sets appear, similarly to the Mandelbrot set, with period doubling when approaching mini-ships. To zoom directly to mini-ships, one can use Newton's method in 2 real variables. First one needs the period, which can be found by iterating the corners of a polygon until it surrounds the origin, that iteration number is the period (this method is due to Robert Munafo's mu-ency, originally for the Mandelbrot set, but seems to work for the Burning Ship too: perhaps the non-conformal folding is sufficiently rare to be unproblematic in practice). Newton's method iterations are like this:

\[ \begin{pmatrix} A \\ B \end{pmatrix} \leftarrow \begin{pmatrix} A \\ B \end{pmatrix} - \begin{pmatrix} \frac{\partial X}{\partial A} & \frac{\partial X}{\partial B} \\ \frac{\partial Y}{\partial A} & \frac{\partial Y}{\partial B} \end{pmatrix}^{-1} \begin{pmatrix} X \\ Y \end{pmatrix} \]

The final part is the mini-ship size estimate, to know how deep to zoom. The Mandelbrot size estimate seems to work with minor modifications to use Jacobian matrices instead of complex numbers.

These concrete equations are specific to the quadratic Burning Ship, but the methods in principle apply to many escape time fractals.

]]>Recently I've been revisiting the code from my Monotone, extending it to use OpenGL cube maps to store the feedback texture instead of nonlinear warping in a regular texture. This means I can use Möebius transformations instead of simple similarities and still avoid excessively bad blurriness and edge artifacts. I've been toying with colour too: but unlike the chaos game algorithm for fractal flames (which can colour according to a "hidden" parameter, leading to interesting and dynamic colour structures), the texture feedback mechanism I'm using can only cope with "structural" RGB colours (with an alpha channel for overall brightness). A 4x4 colour matrix seems to be more interesting than the off-white multipliers I was using to start with.

Some videos:

- Moebius Bubble Chamber (stereographic projection, black and white)
- Moebius Blueprints (360, slight colour, low resolution)
- Moebius Blueprints 2 (360, more colour, high resolution)
- Moenotone Demo (360, colour, high resolution)
- Moenotone Demo 2 (stereographic projection, colour)

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):

**EDIT 2017-11-27** I also changed the badval threshold from 1e10
to 1e100, and I've been informed that this change is also critical for getting
the good appearance (i.e., you need both the numerical voodoo and the threshold
increase).

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.

]]>