Code:

/* gcc -std=c99 -Wall -pedantic -Wextra graph-paper.c -o graph-paper -lGLEW -lGL -lGLU -lglut ./graph-paper for i in *.pgm do pnmtopng -force -interlace -compression 9 -phys 11811 11811 1 <$i >${i%pgm}png done */ #include <stdio.h> #include <string.h> #include <GL/glew.h> #include <GL/glut.h> #define GLSL(s) #s static const int width = 2480; static const int height = 3508; static const float size = 4.0; static int gcd(int x, int y) { if (y == 0) { return x; } else { return gcd(y, x % y); } } static const char *src = GLSL( uniform vec3 twist; vec2 clog(vec2 x) { return vec2(log(length(x)), atan2(x.y, x.x)) * 0.15915494309189535; } void main() { vec2 p = gl_TexCoord[0].xy; p *= mat2(0.0, -1.0, 1.0, 0.0); vec2 q = clog(p); float a = atan2(twist.y, twist.x); float h = length(vec2(twist.x, twist.y)); q *= mat2(cos(a), sin(a), -sin(a), cos(a)) * h; float d = length(vec4(dFdx(q), dFdy(q))); float l = ceil(-log2(d)); float f = pow(2.0, l + log2(d)) - 1.0; l -= 6.0; float o[2]; for (int i = 0; i < 2; ++i) { l += 1.0; vec2 u = q * pow(2.0, l); u *= twist.z; u -= floor(u); float r = min ( min(length(u), length(u - vec2(1.0, 0.0))) , min(length(u - vec2(0.0, 1.0)), length(u - vec2(1.0, 1.0))) ); float c = clamp(1.5 * r / (pow(2.0, l) * d), 0.0, 1.0); vec2 v = q * pow(2.0, l - 2.0); v *= twist.z; v -= floor(v); float s = min(min(v.x, v.y), min(1.0 - v.x, 1.0 - v.y)); float k = clamp(0.75 + 0.25 * s / (pow(2.0, l - 2.0) * d), 0.0, 1.0); o[i] = c * k; } gl_FragColor = vec4(vec3(mix(o[1], o[0], f)), 1.0); } ); int main(int argc, char **argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); glutCreateWindow("graphpaper"); glewInit(); GLint success; int prog = glCreateProgram(); int frag = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(frag, 1, (const GLchar **) &src, 0); glCompileShader(frag); glAttachShader(prog, frag); glLinkProgram(prog); glGetProgramiv(prog, GL_LINK_STATUS, &success); if (!success) exit(1); glUseProgram(prog); GLuint utwist = glGetUniformLocation(prog, "twist"); GLuint tex; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, 4096, 4096, 0, GL_RED, GL_UNSIGNED_BYTE, 0); glBindTexture(GL_TEXTURE_2D, 0); GLuint fbo; glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); glViewport(0, 0, width, height); glLoadIdentity(); gluOrtho2D(0, 1, 1, 0); unsigned char *buffer = malloc(width * height); for (int x = 1; x <= 5; ++x) { for (int y = 0; y <= x; ++y) { if ((x > 1 && y == 0) || (y > 0 && gcd(x, y) != 1)) { continue; } for (int n = 5; n <= 8; ++n) { glUniform3f(utwist, x, y, n / 8.0); glBegin(GL_QUADS); { float u = size / 2.0; float v = size * (height - width / 2.0) / width; glTexCoord2f( u, -v); glVertex2f(1, 0); glTexCoord2f( u, u); glVertex2f(1, 1); glTexCoord2f(-u, u); glVertex2f(0, 1); glTexCoord2f(-u, -v); glVertex2f(0, 0); } glEnd(); glReadPixels(0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, buffer); char fname[200]; snprintf(fname, 100, "graphpaper-%d-%d-%d.pgm", x, y, n); FILE *f = fopen(fname, "wb"); fputs("P5\n2480 3508\n255\n", f); fflush(f); fwrite(buffer, width * height, 1, f); fflush(f); fclose(f); } } } free(buffer); glDeleteFramebuffers(1, &fbo); glDeleteTextures(1, &tex); glDeleteShader(frag); glDeleteProgram(prog); glutReportErrors(); return 0; }

Output:

This is an excavation from my archives, around 2013 or so. Print out your favourites and enjoy doodling!

]]>This blog post is about two separate things, but both involve Möebius transformations so I combined them into one.

Rotations of the Riemann sphere correspond to those elliptic Möebius transformations whose fixed points are antipodal. Suppose we have two vectors \(u, v \in \mathbb{R}^3\) with \(|u| = |v| = 1\) and we want to find the Möebius transformation for the corresponding rotation of the Riemann sphere that takes \(u\) to \(v\) in the shortest way. This rotation has fixed points \(w_\pm = \pm \frac{u \times v}{| u \times v |}\). By stereographic projection these become the fixed points of the Möebius transformation: \(g = \frac{w_x + i w_y}{1 - w_z}\) for each \(\pm\). Further, the elliptic transformation has characteristic constant \(k = e^{i \theta} = \cos \theta + i \sin \theta = u \cdot v + i |u \times v|\) from all of which the transformation is:

\[ M_{u \to v} = \begin{pmatrix} g_+ - k g_- & (k - 1) g_+ g_- \\ 1 - k & k g_+ - g_- \end{pmatrix} \]

If instead of vectors we wanted to rotate between complex numbers, just use stereographic unprojection to get the 3D coordinates and proceed as before: \( \frac{(2x, 2y, x^2+y^2-1)}{x^2+y^2+1} \). (Note: there may be a sign issue with the \(\sin \theta\) calculation, in my use case I didn't need to worry about it as all the issues cancelled each other out.)

Bézier curves are useful for generating smooth curves when only linear interpolation is available (the De Casteljau's algorithm construction works by repeated linear interpolation). Linear interpolation for Möebius transformations involves 2x2 complex matrix diagonalisation for raising to fractional powers (between 0 and 1). I wrote about this in more detail in my 2015 blog post interpolating Möebius transformations.

A C1-continuous (but not C2-continuous at the join points) piecewise cubic Bézier spline can be defined by specifying the points (Möebius transformations) \(P_i\) through which the curve passes, and the tangent \(T_i\) at each point. Then the pieces of the spline are defined by control points \((P_i, P_i T_i, T_{i+1}^{-1} P_{i+1}, P_{i+1})\). The tangents \(T_i\) might be "small" for smoother results, for example these can be constructed by linearly interpolating between the identity (with large weight) and an arbitrary transform (with small weight). This interpolation scheme gives visually much more sensible results than naive Catmull-Rom spline interpolation between each individual coefficient components separately, while still maintaining smoothness.

]]>Earlier today I wrote about
atom domain coordinates,
and thought about extending it to
Misiurewicz domains.
By simple analogy, define the **Misiurewicz domain coordinate** \(G\) with
\(0 \le r \lt q\) and \(1 \le p\):

\[G(c, p, q, r) = \frac{F^{q + p}(0, c) - F^{q}(0, c)}{F^{r + p}(0, c) - F^{r}(0, c)}\]

Calculated similarly to the
atom domain size estimate,
the **Misiurewicz domain size estimate** is:

\[|h| = \left| \frac{F^{r + p}(0, c) - F^{r}(0, c)}{\frac{\partial}{\partial c}F^{q + p}(0, c) - \frac{\partial}{\partial c}F^{q}(0, c)} \right| \]

Like the atom domain coordinate, Newton's method can be used to find a point with a given Misiurewicz domain coordinate. Implementing this is left as an exercise (expect an implementation in my mandelbrot-numerics repository at some point soon).

]]>Previously I wrote about
atom domain size estimates
in the Mandelbrot set. A logical step given \(G(c) = 0\) at the center and
\(|G(c)| = 1\) on the boundary is to take \(G(c)\) as the
**atom domain coordinate**
for \(c\). It turns out to make sense to make \(1 \le q \lt p\) arguments to
the function, and evaluate them at the central nucleus, because otherwise the
assumption that \(p, q\) are constant throughout the domain can be violated
(particularly with neighbouring domains in embedded Julia sets, where the higher
period one is not "influenced" by the medium period one it overlaps, but instead
by the lower period "parent" of both):

\[G(c, q, p) = \frac{F^p(0, c)}{F^q(0, c)}\]

In my efficient automated Julia morphing experiments recently I used the atom domain coordinates for guessing an initial point for Newton's method to find Misiurewicz points. This worked because each next level of morph had an atom domain coordinate approximately the previous raised to the power \(\frac{3}{2}\). To do this I needed to implement Newton's method iterations to find \(c\) given \(G(c), p, q\). Pseudo-code for that looks like this:

double _Complex m_domain_coord ( double _Complex c0 , double _Complex G , int q , int p , int n ) { double _Complex c = c0; for (int j = 0; j < n; ++j) { double _Complex zp = 0; double _Complex dcp = 0; double _Complex z = 0; double _Complex dc = 0; for (int i = 1; i <= p; ++i) { dc = 2 * z * dc + 1; z = z * z + c; if (i == q) { zp = z; dcp = dc; } } double _Complex f = z / zp - G; double _Complex df = (dc * zp - z * dcp) / (zp * zp); c = c - f / df; } return c; }

You can find fuller implementations (including arbitrary precision) in my mandelbrot-numerics repository.

]]>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).

Today I presented GULCII, my Graphical Untyped Lambda Calculus Interactive Interpreter, at the University of Edinburgh Informatics department. It went well I think. The first go around in the morning I missed some slides about de Bruijn indexes but the afternoon audience seemed to be familiar with it. First I performed with GULCII, the same set as the FARM conference music evening, testing an equivalence between Church encoding and Scott encoding of natural numbers. Then I presented some slides about those encoding schemes of data in untyped lambda calculus, and a bit about how GULCII works, some shortcomings, and ideas for future developments.

A week before the talk I discovered a bad bug in the evaluator ("exp two two" with Church numerals was not equal to "four"), but I managed to rewrite it in time, using the "scope extrusion" rules from the Lambdascope paper. However, sharing is still broken, so the next version will probably switch to using Lambdascope as a library, so that full lazy graph reduction will work properly. The bug had been there since 2011, and is visible in the video of my FARM performance.

You can download the slides from my presentation,
or view the Pandoc Markdown for LaTex Beamer
source code.
I might upload some video of the second talk soon. GULCII itself is
on Hackage so you can install
it with `cabal install gulcii`

.

Incidentally the brickwork opposite the Informatics Forum reminded me of Donky Kong, with its ladders:

]]>**tanh()** is a nice function for saturation in audio, amongst
other applications. Near 0 it is similar to the identity function \(x\), leaving
sound unchanged. Far from 0 it tends to constant \(\pm 1\), with a smooth roll-off
curve that gives a soft-clipping effect, where louder signals are more distorted,
and the output never exceeds 0dBFS.

Unfortunately tanh() is computationally expensive, so approximations are desirable. One common approximation is a rational function:

\[ \tanh(x) \approx x \frac{27 + x^2}{27 + 9 x^2} \]

which the apparent source describes as

based on the pade-approximation of the tanh function with tweaked coefficients.

I also found mention Padé-approximants on a page primarily focussed on approximating tanh() with a continued fraction. So I set out to discover what they were. Some dead ends (a book appendix with 6 pages of badly-OCR'd FORTRAN code, to name but one) and I eventually struck gold with this PDF preview:

Outlines of Padé Approximation by Claude Brezinski in

Computational Aspects of Complex Analysis pp 1-50part of theNATO Advanced Study Institutes Series book series (ASIC, volume 102)Abstract: In the first section Padé approximants are defined and some properties are given. The second section deals with their algebraic properties and, in particular, with their connection to orthogonal polynomials and quadrature methods. Recursive schemes for computing sequences of Padé approximants are derived. The third section is devoted to some results concerning the convergence problem. Some applications and extensions are given in the last section.

The preview contains a system of equations involving a rational function:

\[ [p/q]_f := \frac{\sum_{i=0}^p a_i x^i}{\sum_{i=0}^q b_i x^i} \]

where without loss of generality \(b_0 = 1\). The coefficients are derived from the coefficients \(c_i\) of the Maclaurin series of \(f\) (which is its Taylor series expanded about \(0\)):

\[ f := \sum_{i=0}^\infty c_i x^i \]

The system of equations is very regular, here's some Haskell code to solve for the \(a\)s and \(b\)s given the \(c\)s:

pade :: [Rational] -> Int -> Int -> ([Rational], [Rational]) pade c p q = (a, b) where m = take q . map (take q) . tails . drop (p - q + 1) $ c v = take q . drop (p + 1) . map negate $ c Just m1 = inverse m -- FIXME this will crash if it isn't invertible b = 1 : reverse (m1 `mulmv` v) a = take (p + 1) $ [ reverse cs `dot` bs | (bs, cs) <- map unzip . drop 1 . inits $ zip (b ++ repeat 0) c ]

This needs an `inverse :: [[Rational]] -> Maybe [[Rational]]`

, the
pure Gaussian elimination code from
an old mailing list post
works fine with minor modifications for current GHC versions (it needs an extra
Eq constraint now).

With some extra output beautification code (and of course the coefficients of the power series for tanh(), which involve the Bernoulli numbers) the first few Padé approximants for tanh() are:

[1/0] = x [1/2] = x*(3)/(3+x**2) [3/2] = x*(15+x**2)/(15+6*x**2) [3/4] = x*(105+10*x**2)/(105+45*x**2+x**4) [5/4] = x*(945+105*x**2+x**4)/(945+420*x**2+15*x**4) [5/6] = x*(10395+1260*x**2+21*x**4)/(10395+4725*x**2+210*x**4+x**6) [7/6] = x*(135135+17325*x**2+378*x**4+x**6)/(135135+62370*x**2+3150*x**4+28*x**6) [7/8] = x*(2027025+270270*x**2+6930*x**4+36*x**6)/(2027025+945945*x**2+51975*x**4+630*x**6+x**8)

Some of these are shown in the image at the top of this post, along with the music-dsp approximation with "tweaked coefficients". The [7/6] approximation agrees with the truncated Lambert's continued fraction from the linked page, I'll probably end up using the [5/6] in various experiments. You can download the source code here: Pade.hs.

]]>The Mandelbrot set is approximately self-similar, containing miniature baby Mandelbrot set copies. However, all of these copies are distorted, because there is only one perfect circle in the Mandelbrot set. The complex-valued size estimate can be used as a multiplier for looping zoom animations, though the difference in decorations and visible distortion make the seam a little jarring. Here are some examples:

period \(3\) near \(-2\)

period \(4\) near \(i\)

period \(5\) near \(-1.5 + 0.5 i\)

The trick to the looping zoom is to find an appropriate center: if the nucleus of the baby is \(c\) and the complex size is \(r\), there is another miniature copy near the baby around \(c + r c\) with size approximately \(r^2\). Taking the limit gives a geometric progression:

\[c + r c + r^2 c + \cdots = \frac{c}{1 - r}\]

Here's the code used to render the images (also found in the mandelbrot-graphics repository):

#include <stdio.h> #include <mandelbrot-graphics.h> int main(int argc, char **argv) { (void) argc; (void) argv; const double _Complex r0 = 1; const double _Complex c0 = 0; int periods[3] = { 3, 4, 5 }; double _Complex c1s[3] = { -2, I, -1.5 + I * 0.5 }; int w = 512; int h = 512; m_pixel_t red = m_pixel_rgba(1, 0, 0, 1); m_pixel_t black = m_pixel_rgba(0, 0, 0, 1); m_pixel_t white = m_pixel_rgba(1, 1, 1, 1); double er = 600; int maxiters = 1000; m_image *image = m_image_new(w, h); if (image) { m_d_colour_t *colour = m_d_colour_minimal(red, black, white); if (colour) { for (int k = 0; k < 3; ++k) { int period = periods[k]; double _Complex c1 = c1s[k]; m_d_nucleus(&c1, c1, period, 64); double _Complex r1 = m_d_size(c1, period); for (int frame = 0; frame < 50; ++frame) { double f = (frame + 0.5) / 50; double _Complex r = cpow((r1), f) * cpow((r0), 1 - f); double _Complex c = c1 / (1 - r1); m_d_transform *rect = m_d_transform_rectangular(w, h, 0, 1); m_d_transform *move1 = m_d_transform_linear(- c / 2.25, 1); m_d_transform *zoom = m_d_transform_linear(0, r * 2.25); m_d_transform *move2 = m_d_transform_linear(c, 1); m_d_transform *rm1 = m_d_transform_compose(rect, move1); m_d_transform *zm2 = m_d_transform_compose(zoom, move2); m_d_transform *transform = m_d_transform_compose(rm1, zm2); m_d_render_scanline(image, transform, er, maxiters, colour); char filename[100]; snprintf(filename, 100, "%d-%02d.png", k, frame); m_image_save_png(image, filename); m_d_transform_delete(transform); m_d_transform_delete(zm2); m_d_transform_delete(rm1); m_d_transform_delete(move2); m_d_transform_delete(zoom); m_d_transform_delete(move1); m_d_transform_delete(rect); } } m_d_colour_delete(colour); } m_image_delete(image); } return 0; }

I used ImageMagick to convert each PNG to GIF, then gifsicle to combine into animations.

]]>The Mandelbrot is asymptotically self-similar about pre-periodic Misiurewicz points. The derivative of the cycle (with respect to \(z\)) can be used as a multiplier for seamlessly looping zoom animations. Here are some examples:

const dvec2 c0 = dvec2(-0.22815549365396179LF, 1.1151425080399373LF); const int pre = 3; const int per = 1;

const dvec2 c0 = dvec2(-0.10109636384562216LF, 0.9562865108091415LF); const int pre = 4; const int per = 3;

(The above example's Misiurewicz point has period 1, but using 3 here avoids rapid spinning.)

const dvec2 c0 = dvec2(-0.77568377LF, 0.13646737LF); const int pre = 24; const int per = 2;

Here is the rest of the code that made the images, it's for Fragmentarium with
my (as yet unreleased, but coming soon) `Complex.frag`

enhancements
for dual-numbers and double-precision:

]]>#version 400 core #include "Complex.frag" #include "Progressive2D.frag" uniform float time; // insert snippets from above in here to choose image const double r0 = 0.00001LF; vec3 color(vec2 p) { // calculate multiplier for zoom dvec4 z = cVar(0.0LF); dvec4 c = cConst(c0); for (int i = 0; i < pre; ++i) z = cSqr(z) + c; z = cVar(cVar(z)); for (int i = 0; i < per; ++i) z = cSqr(z) + c; dvec2 m = r0 * dvec2(cPow(vec2(cInverse(cDeriv(z))), mod(time, float(per)) / float(per))); const int maxiters = 1000; const double er2 = 1000.0LF; c = cVar(c0 + cMul(m, p)); z = cConst(0.0LF); double pixelsize = cAbs(m) * double(length(vec4(dFdx(p), dFdy(p)))); int i; for (i = 0; i < maxiters; ++i) { z = cSqr(z) + c; if (cNorm(z) > er2) { break; } } if (i == maxiters) { return vec3(1.0, 0.7, 0.0); } else { double de = 2.0 * cAbs(z) * double(log(float(cAbs(z)))) / cAbs(cDeriv(z)); float grey = tanh(clamp( float(de/pixelsize), 0.0, 8.0 )); return vec3(grey); } }

Wolf Jung's Mandel's "algorithm 9" allows locating zeroes of the iterated polynomial at a certain period where 4 colours meet. But I wanted to find the zeroes for lots of periods all at once. Previously I did this in a way that didn't scale efficiently to deep zooms, so I adapted the "algorithm 9" technique. Not implemented yet is the extension of this code to use perturbation techniques for deep zooms, but it should be perfectly possible.

The first thing to do is initialize the array of \(c\) values, here I use my mandelbrot-graphics library as the support code (not shown here) uses it for imaging:

void initialize_cs(int m, int n, m_d_transform *t, double _Complex *cs) { #pragma omp parallel for for (int j = 0; j < n; ++j) { for (int i = 0; i < m; ++i) { double _Complex c = i + I * j; double _Complex dc = 1; m_d_transform_forward(t, &c, &dc); int k = i + j * m; cs[k] = c; } } }

Then in the iteration step, calculate a flag for which quadrant the \(z\) iterate is in. This is set as a bit mask, so ORing many masks together corresponds to set union:

void step_zs(int mn, char *qs, double _Complex *zs, const double _Complex *cs) { #pragma omp parallel for for (int i = 0; i < mn; ++i) { // load double _Complex c = cs[i]; double _Complex z = zs[i]; // step z = z * z + c; // compute quadrant char q = 1 << ((creal(z) > 0) | ((cimag(z) > 0) << 1)); // store zs[i] = z; qs[i] = q; } }

Now the meat of the algorithm: it scans across the data with a 3x3 window, to find where all 4 colours meet in one small square. Then if that happens, check that the 3x3 square has a local minimum at its center, which means that the point found is really near a zero (a proof for that assertion follows immediately from the minimum modulus principle).

int scan_for_zeroes(int m, int n, int ip, int *ops, double _Complex *ocs, const char *qs, const double _Complex *zs, const double _Complex *ics) { int o = 0; // loop over image interior, to avoid tests in inner 3x3 loop #pragma omp parallel for for (int j = 1; j < n - 1; ++j) { for (int i = 1; i < m - 1; ++i) { // find where 4 quadrants meet in 3x3 region char q = 0; for (int dj = -1; dj <= 1; ++dj) { int jdj = j + dj; for (int di = -1; di <= 1; ++di) { int idi = i + di; int kdk = idi + jdj * m; q |= qs[kdk]; } } if (q == 0xF) { // 4 quadrants meet, check for local minimum at center double minmz = 1.0/0.0; for (int dj = -1; dj <= 1; ++dj) { int jdj = j + dj; for (int di = -1; di <= 1; ++di) { int idi = i + di; int kdk = idi + jdj * m; double mz = cabs(zs[kdk]); minmz = mz < minmz ? mz : minmz; } } int k = i + j * m; double mz = cabs(zs[k]); if (mz <= minmz && minmz < 1.0/0.0) { // we found a probable zero, output it double _Complex ic = ics[k]; int out; #pragma omp atomic capture out = o++; ops[out] = ip; ocs[out] = ic; } } } } return o; }

To be safe, the output arrays should be sized at least the desired number of elements plus the number of pixels in the image (which is the maximum number that can be output in one pass). Most of the extra space will be unused by the time the stopping condition (enough space left) is reached.

An earlier version was several times slower, partly due to caching
`cabs()`

calls for every pixel, though only very few pixels
are near a zero at any given iteration.

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.

]]>