The Mandelbrot set has two prominent solid regions. There is a cardioid, which is associated with fixed point (period 1) attractors, and a circle to the left, which is associated with period 2 attractors. The rest of the cardioid- and circle-like components in the Mandelbrot set are distorted.

These shapes can be described as implicit functions. For example, the circle is centered on \(-1+0i\) and has radius \(\frac{1}{4}\), and the function \[C_2(x, y) = (x - (-1))^2 + (y - 0)^2 - \left(\frac{1}{4}\right)^2\] is negative inside, zero on the boundary, and positive outside, and the same applies to the more complicated function for the cardioid: \[C_1(x, y) = \left( \left(x - \frac{1}{4}\right)^2 + y^2 \right)^2 + \left(x - \frac{1}{4}\right) \left(\left(x - \frac{1}{4}\right)^2 + y^2\right) - \frac{1}{4} y^2 \]

These implicit functions can be used to accelerate Mandelbrot set rendering. You can test if each \(c=x+iy\) is in the cardioid or circle quickly and easily, saving iterating the pixel all the way to the maximum iteration count (being interior to the Mandelbrot set means iterations of \(z \to z^2 + c\) will never escape to infinity).

But we can accelerate further. If the whole viewing rectangle of a zoomed in view is far away from the circle and cardioid, then these per-pixel cardioid and circle tests are a waste of time, as they will never say they are inside. By analysing the coordinates of an axis-aligned bounding box (AABB) it's possible to decide when it's worth doing per pixel tests (that is, when the boundary of the shapes passes through the box - otherwise it's 100% interior or (more likely) 100% exterior to the shapes). This can save \(O(W H)\) work.

For example for the circle, if the lower edge of the box is above \(\frac{1}{4}\), or the upper edge of the box is below \(-\frac{1}{4}\), or the right edge of the box is left of \(-\frac{5}{4}\), or the left edge of the box is right of \(-\frac{3}{4}\), clearly it cannot overlap the circle. And if all corners are inside the circle, the whole box must be inside the circle. If some corners are inside and some are outside, then the boundary passes through.

But if all corners are outside it gets complicated: the box could be surrounding the whole circle, or a bulge of the circle could pop into an edge of the box. So the next step is to consider the vertices of the circle (the points most left/right/top/bottom): axis alignment means that if a bulge pops into the box, the vertex must lead the way. All in all there are many cases to consider, but it's not insurmountable.

Similarly for the cardioid, with the added complication that the vertices are not rational. However, squaring the coordinates does give dyadic rationals, so comparing \(y^2\) with \(\frac{3}{64}\) and \(\frac{27}{64}\) can do the trick.

For deep zooms, coordinates need high precision (lots of digits, most of which are the same for nearby pixels). Perturbation techniques mean using a high precision reference, with low precision differences to nearby points. This can also be applied to the implicit functions for the cardioid and circle: symbolically expand and cancel the large terms \(X, Y\) leaving only small terms of the scale of \(x, y\) in: \[c(X, Y, x, y) = C(X + x, Y + y) - C(X, Y)\] then evaluate \(C(X, Y)\) in high precision, round it to low precision, and add \(c(X, Y, x, y)\) evaluated in low precision. For accuracy, some coefficients in \(c\) will need to be calculated at high precision before rounding to low precision.

For example, the cardioid: \[c_1(X, Y, x, y) = a_x x + a_y y + a_{x^2} x^2 + a_{xy} xy + a_{y^2} y^2 + a_{x^3} x^3 + a_{x^2y} x^2y + a_{xy^2} xy^2 + a_{y^3} y^3 + x^4 + 2x^2y^2 + y^4 \] where \[a_x = (32XY^2+32X^3-6X+1)/8; a_y = (32Y^3+(32X^2-6)Y)/8; a_{x^2} = (16Y^2+48X^2-3)/8; \ldots \] I used wxMaxima to find all of the \(a\) coefficients, and calculate them one time per view using the reference, along with \(C_1(X, Y)\). For accuracy, with fixed point calculations you need about 4 times the number of fractional bits for intermediate calculations, and the values at low precision need to be relatively high accuracy (in my tests 24 bits was not enough to achieve good images, 53 seemed ok, and I use 64 bits just to be safe).

The previous discussion about rejecting interior checks for the whole view can be applied with perturbation too, but some magic numbers need to be calculated at high precision before rounding to low precision, namely the special points (vertices and cusps): \[ X + \frac{5}{4}, X + 1, X + \frac{3}{4}, X + \frac{1}{8}, X - \frac{1}{4}, X - \frac{3}{8} \] and \[ Y + \frac{1}{4}, Y - \frac{1}{4}, Y^2 - \frac{3}{64}, Y^2 - \frac{27}{64} \] the addends are all dyadic rationals so can be represented exactly in binary fixed point or floating point.

The circle and cardioid also have parametric forms. Here's the cardioid: \[C_1(t) = \left(\frac{\sin(t)^2-(\cos(t)-1)^2+1}{4}, \frac{(\cos(t)-1)\sin(t)}{2}\right) \] If you could work out the distance to the nearest point of the curve, then all views with a smaller circumradius and same center would be 100% exterior (or 100% interior). For the cardioid, considering that the dot product of the tangent of the curve at the nearest point and the vector from the point to the nearest point of the curve must be zero (perpendicular), it means solving \[(4\cos(t)-4\cos(2t))y+(4\sin(2t)-4\sin(t))x-\sin(t)=0\] which can be rearrange to a 9 or 10 degree polynomial in \(\tan(t)\) using trigonometric identities. This is altogether a hard problem to solve, most practical is bisection of the trigonometric form on segments between by \(t = k\frac{\pi}{3}\). Linear distance estimate using Taylor expansion gives a closed form \(d(x,y)\), but it's not accurate, especially near the cusp. Quadratic Taylor expansion gives a high degree polynomial to solve. On the other hand, exact distance to a circle is easy.

(Actually 0.8.0.1 because Hackage complained about 0.8 in a way that
`cabal check`

didn't warn about before uploading.)

A new release of **mandulia** (a zooming visualization
of the Mandelbrot Set as many Julia Sets, with Lua scripting support)
is installable from
Hackage:

cabal update cabal install mandulia

After you have installed it, you can try it out by running:

mandulia main

and hit the 'a' key to enter attract mode or use the key controls listed in the README for interactive navigation. 'F11' toggles full screen mode, and 'ESC' quits.

mandulia-0.8.0.1 has no new features, the only changes are to make it work with the current state of Hackage 12 years later:

- base-4.6 has modifyIORef'
- containers-0.5 has Data.Map.Strict
- hslua-0.4 and above changed API, so restrict to older versions

Tested and working with latest stable versions of GHC from ghc-8.0.2 up, only ghc-9.4.2 needs --allow-newer=text because OpenGLRaw has an outdated dependency. Older versions of GHC may work, but I haven't tried. I have freeglut3-dev installed on Debian Bookworm (current testing distribution), but I don't know what other system dependencies are needed.

Source code is GPLv3+ licensed, Git repository at mandulia on code.mathr.co.uk, or you can download mandulia-0.8.0.1.tar.gz.

]]>Since last year's article on deep zoom theory and practice, two new developments have been proposed by Zhuoran on fractalforums.org: Another solution to perturbation glitches.

The first technique ("**rebasing**"), explained in the
first post of the forum thread, means resetting the reference iteration to the
start when the pixel orbit (i.e. \(Z+z\), the reference plus delta) gets
near a critical point (like \(0+0i\) for the Mandelbrot set). If there is
more than one critical point, you need reference orbits starting at each
of them, and this test can switch to a different reference orbit. For
this case, pick the orbit \(o\) that minimizes \(|(Z-Z_o)+z|\), among the
current reference orbit at iteration whatever, and the critical point
orbits at iteration number \(0\). Rebasing means you only need as many
reference orbits as critical points (which for simple formulas like the
Mandelbrot set and Burning Ship means only one), and glitches are avoided
rather than detected, needing to be corrected later. This is a big
boost to efficiency (which is nice) and correctness (which is much more
important).

Rebasing also works for hybrids, though you need more reference orbits, because the reference iteration can be reset at any phase in the hybrid loop. For example, if you have a hybrid loop of "(M,BS,M,M)", you need reference orbits for each of "(M,BS,M,M)", "(BS,M,M,M)", "(M,M,M,BS)" and "(M,M,BS,M)". Similarly if there is a pre-periodic part, you need references for each iteration (though for a zoomed in view, the minimum escaping iteration in the image determines whether they will be used in practice): "M,M,(BS,BS,M,BS)" needs reference orbits "M,M,(BS,BS,M,BS)", "M,(BS,BS,M,BS)" and the four rotations of "(BS,BS,M,BS)". Each of these phases needs as many reference orbits as the starting formula has critical points. As each reference orbit calculation is intrinsically serial work, and modern computers typically have many cores, the extra wall-clock time taken by the additional references is minimal because they can be computed in parallel.

The second technique ("**bilinear approximation**") is
only hinted at in the thread. If you have a deep zoom, the region of
\(z\) values starts very small, and bounces around the plane typically
staying small and close together, in a mostly linear way, except for
when the region gets close to a critical point (e.g. \(x=0\) and \(y=0\) for the
Mandelbrot set) or line (e.g. either \(x=0\) or \(y=0\) for the Burning Ship),
when non-linear stuff happens (like complex squaring, or absolute
folding). For example for the Mandelbrot set, the perturbed iteration

\[ z \to 2 Z z + z^2 + c \]

when \(Z\) is not small and \(z\) is small, can be approximated by

\[ z \to 2 Z z + c \]

which is linear in \(z\) and \(c\) (two variables call this "bilinear"). In particular, this approximation is valid when \( z^2 << 2 Z z + c \), which can be rearranged with some handwaving (for critical point at \(0\)) to

\[ z < r = \max\left(0, \epsilon \frac{\left|Z\right| - \max_{\text{image}} \left\{|c|\right\}}{\left|J_f(Z)\right| + 1}\right) \]

where \(\epsilon\) is the hardware precision (e.g. \(2^{-24}\)), and \(J_f(Z) = 2Z\) for the Mandelbrot set. For Burning Ship replace \(|Z|\) with \(\min(|X|,|Y|)\) where \(Z=X+iY\). In practice I divide \(|Z|\) by \(2\) just to be extra safe. For non-complex-analytic functions I use the operator norm for the Jacobian matrix, implemented in C++ by:

template <typename real> inline constexpr real norm(const mat2<real> &a) { using std::max; using std::sqrt, ::sqrt; const mat2<real> aTa = transpose(a) * a; const real T = trace(aTa); const real D = determinant(aTa); return (T + sqrt(max(real(0), sqr(T) - 4 * D))) / 2; } template <typename real> inline constexpr real abs(const mat2<real> &a) { using std::sqrt, ::sqrt; return sqrt(norm(a)); }

This gives a bilinear approximation for one iteration, which is not so useful. The acceleration comes from combining neighbouring BLAs into a BLA that skips many iterations at once. For neighbouring BLAs \(x\) and \(y\), where \(x\) happens first in iteration order, skipping \(l\) iterations via \(z \to A z + B c\), one gets:

\[\begin{aligned} l_{y \circ x} &= l_y + l_x \\ A_{y \circ x} &= A_y A_x \\ B_{y \circ x} &= A_y B_x + B_y \\ r_{y \circ x} &= \min\left(r_x, \max\left(0, \frac{r_y - |B_x| \max_{\text{image}}\left\{|c|\right\}}{|A_x|}\right) \right) \end{aligned}\]

This is a bit handwavy again, higher order terms of Taylor expansion are probably necessary to get a bulletproof radius calculation, but it seems to work ok in practice.

For a reference orbit iterated to \(M\) iterations, one can construct a BLA table with \(2M\) entries. The first level has \(M\) 1-step BLAs for each iteration, the next level has \(M/2\) combining neighbours (without overlap), the next \(M/4\), etc. It's best for each level to start from iteration \(1\), because iteration \(0\) is always starting from a critical point (which makes the radius of BLA validity \(0\)). Now when iterating, pick the BLA that skips the most iterations, among those starting at the current reference iteration that satisfy \(|z| < |r|\). In between, if no BLA is valid, do regular perturbation iterations, rebasing as required. You need one BLA table for each reference orbit, which can be computed in parallel (and each level of reduction can be done in parallel too, perhaps using OpenCL on GPU).

BLA is an alternative to series approximation for the Mandelbrot set, but it's conceptually simpler, easier to implement, easier to parallelize, has better understood stopping conditions, is more general (applies to other formulas like Burning Ship, hybrids, ...) - need to do benchmarks to see how it compares speed-wise before declaring an overall winner.

It remains to research the BLA initialisation for critical points not at \(0\), and check rebasing with multiple critical points: so far I've only actually implemented it for formulas with a single critical point at \(0\), so there may be bugs or subtleties lurking in the corners.

]]>A new release: kf-2.15.5. Kalles Fraktaler 2 + is fast deep zooming Free Software for fractal graphics (Mandelbrot, Burning Ship, etc). Full change log:

kf-2.15.5 (2021-12-05)

- new: single-reference implementation for avoiding glitches (thanks Zhuoran https://fractalforums.org/f/28/t/4360/msg29835#msg29835); enabled by default; also supported in nanomb1

- known issue: does not work with every hybrid formula (only very simple cases work)
- known issue: may fail if there is more than one critical point
- new: start of support for convergent formulas

- known issue: convergent formulas are not supported in OpenCL
- known issue: convergent formulas are not supported with derivatives (this means neither analytic DE nor analytic slopes)
- new: many new formulas (thanks to Alexandre Vachon aka FractalAlex)
- new: Nova formula; variant implemented with critical point at 0 instead of 1, to avoid precision loss when deep zooming

- known issue: no OpenCL support yet
- known issue: no derivatives support yet
- known issue: Newton zooming does not work properly
- new: Separated Perpendicular formula (thanks Mr Rebooted); variant implemented with critical point at 0, and custom function to avoid precision loss when deep zooming

- known issue: no OpenCL support yet
- known issue: single reference method does not cure all glitches
- new: hybrid formulas support division operator (thanks FractalAlex)

- known issue: implementation is incomplete
- new: Triangle Inequality Average colouring algorithm can be enabled in Formula dialog; requires OpenCL; replaces final angle in phase T channel data

- known issue: likely to change in future versions, use at own risk
- known issue: disable Series Approximation for predictable results
- fix: Newton dialog uses a compact layout (by popular request)
- fix: Newton zooming functions are correctly linked into the EXE (only kf-2.15.4 was broken)
- fix: control-click to zoom correctly views framed rectangle (thanks CFJH)
- fix: NR zoom log should no longer go out of the window (reported by Uma410)
- fix: typo bug in general power Mandelbrot series approximation (thanks superheal)
- fix: some typo bugs in CFixedFloat operators (maybe did not affect anything in the old code, if only by chance)
- fix: some typo bugs in the build system
- fix: name Polarbrot correctly everywhere
- fix: there is no long long in OpenCL (thunks shapeweaver)
- fix: command line detailed status reporting works for all frames of zoom out sequence
- fix: be more robust about stopping rendering before changing internal state; should fix some crashes like changing approximation terms (reported by CFJH)
- internal: support for custom reference orbit values for caching repeated computations (time/space trade-off)

- known issue: no OpenCL support yet
- internal: output stream operators for more types
- internal: refactor smooth iterations handling
- internal: delete obsolete GlitchIter handling
- internal: more functions for CFixedFloat(): log() cosh() sinh()
- internal: more functions for floatexp: cosh() (thanks FractalAlex)
- internal: more functions for complex: sin() cos() cosh() (thanks FractalAlex)
- internal: more functions for preprocessor: cosh() sqrt() (thanks FractalAlex)
- internal: hack for fractions in preprocessor
- internal: complex constructor taking int to allow complex<T> x = 0
- internal: custom glitch tests in formula XML
- internal: brute force (high precision) renderers for tests

Get it from mathr.co.uk/kf/kf.html.

This is likely to be the last KF release from me for the foreseeable future as I'm increasingly busy with other things.

]]>The Mandelbrot set \(M\)is formed by iterations of the function \(z \to z^2 + c\) starting from \(z = 0\). If this remains bounded for a given \(c\), then \(c\) is in \(M\), otherwise (if it escapes to infinity) then \(c\) is not in \(M\). The interior of \(M\) is characterized by collections of cardioid-like and disk-like shapes, these are hyperbolic components each associated with an integer, its period. For the \(c\) at the center of each component, if the period is \(p\) then after \(p\) iterations, \(z\) returns to \(0\), and the iterations repeat (hence the name period). For points in the complex plane (either in \(M\) or not) sufficiently near to a hyperbolic compoenent of period \(p\), \(|z|\) reaches a new minimum (discounting the initial \(z=0\)) at iteration \(p\). The region for which this is true is called the atom domain associated with the hyperbolic component.

To find the center (sometimes called nucleus) of a hyperbolic component, one can use Newton's root-finding method in one complex variable. Iterate the derivative with respect to \(c\) along with \(z\) (using \(\frac{\partial z}{\partial c} \to 2 \frac{\partial z}{\partial c} z + 1\)) for \(p\) iterations, then update \(c \to c - \frac{z}{\frac{\partial z}{\partial c}}\) until it converges. However, Newton's method requires a good initial guess for \(c\). As there are multiple roots, and if you are near to a root Newton's method brings you nearer to it, there must be a boundary where which root is reached depends sensitively on the initial guess. It turns out (if there are more than 2 roots) that the boundary is fractal, and for any point on the boundary, an arbitrarily small neighbourhood will be spread to all the roots. See 3blue1brown's videos on YouTube about the Newton fractal for further information. Which comes to my conjecture:

Conjecture: points in the complement of the Mandelbrot in an atom domain of period \(p\) are good initial guesses for Newton's method to find the root of period \(p\) at the center of that atom domain.

It turns out that **this conjecture is false**. The proof
is by counter-example. The counter-example is the period \(18\) island
with angled internal address \(1 \to_{1/2} 2 \to_{1/8} 16 \to_{1/2} 18\),
whose upper external angle is \(.(010101010101100101)\) when
expressed in binary. I found this counter-example by brute force search:
for every period increasing from \(1\), trace every ray pair of that
period until the endpoints reach the atom domain. Then from each use
Newton's method to find the center of the hyperbolic component. Compare
the two centers reached, if they aren't the same then we have found a
counter-example. Here is a picture:

The Mandelbrot set is shown in black, using distance estimation to make its filaments visible. The fractal boundary of the Newton basins of period \(18\) is shown in white. The atom domain is shown in red. The complement of the Mandelbrot set is shown with a binary decomposition grid that follows the external rays and equipotentials. You can see that the path of the ray that goes from the cusp of the mini Mandelbrot island will intersect the Newton basin boundary at the corner of the atom domain, so that the eventual point of convergence of Newton's method is unpredictable. In my experiment it converged to the child bulb with angled internal address \(1 \to_{1/2} 2 \to_{1/9} 18\).

The above image was using regular Newton's method, without factoring out the roots of lower period that divide the target period. With the reduced polynomials, the basins are typically a little bigger, but in this case it made no difference and the problem persists with this counter-example:

I uploaded a short video showing the counter-example with both variants of Newton's method: watch on diode.zone. You can download the FragM source code for the visualisation.

This counter-example shows that the strategy of tracing rays until the atom domain is reached, before switching to Newton's method to find the root, is unsafe. A guaranteed safe strategy remains to be investigated.

]]>The Mandelbrot set fractal is formed by iterations of \(z \to z^2 + c\) where \(c\) is determined by the coordinates of the pixel and \(z\) starts at the critical point \(0\). The critical point is where the derivative w.r.t. \(z\) is \(0\). The image is usually coloured by how quickly \(z\) escapes to infinity, regions that remain bounded forever are typically coloured black. It has a distinctive shape, with a cardioid adjoined by many disks decreasing in size, each with further disks attached. Looking closely there are smaller "island" copies of the whole, but they are actually attached to the main "continent" by thin filaments.

The Newton fractal is formed by applying Newton's method to the cube roots of unity, iterating \(z \to z - \frac{z^3 - 1}{3z^2}\) where the initial \(z\) is determined by the coordinates of the pixel. The image is usually coloured by which of the 3 complex roots of unity is reached, with brightness and saturation showing how quickly it converged. It has its own distinctive shape, with three rays extending from the origin towards infinity separating the immediate basins of attraction, each with an intricate shape: at each point on the Julia set, different points in an arbitrarily small neighbourhood will converge to all three of the roots.

The Nova fractal mashes these two fractals together: the Newton fractal is perturbed with a \(+ c\) term that is determined by the pixel, and the iteration starts from a critical point (any of the cube roots of unity). The image is coloured by how quickly the iteration converges to a fixed point (a different point for each pixel) and points that don't converge (or converge to a cycle of length greater than 1) are usually coloured black. The fractal appearance combines features of the Newton fractal and the Mandelbrot set, with mini-Mandelbrot set islands appearing in the filaments.

Deep zooms of the Mandelbrot set can be rendered efficiently using perturbation techniques: consider \[z = (Z+z) - Z \to ((Z + z)^2 + (C + c)) - (Z^2 + C) = (2 Z + z) z + c \] where \(Z, C\) is a "large" high precision reference and \(z, c\) is a "small" low precision delta for nearby pixels. Going deeper one can notice "glitches" around mini-Mandelbrot sets when the reference is not suitable, but these can be detected with Pauldelbrot's criterion \(|Z+z| << |Z|\), at which point you can use a different reference that might be more appropriate for the glitched pixels.

Trying to do the same thing for the Nova fractal works at first, but going deeper (to about \(10^{30}\) zoom factor) it breaks down and glitches occur that are not fixed by using a nearer reference. These glitches are due to the non-zero critical point recurring in the periodic mini-Mandelbrot sets: precision loss occurs when mixing tiny values with large values. They also occur when affine-conjugating the quadratic Mandelbrot set to have a critical point away from zero (e.g. \(z \to z^2 - 2 z + c\) has a critical point at \(z = 1\)). Affine-conjugation means using an affine function \(m(z) = a z + b\) to conjugate two functions \(f, F\) like \(m(f(z)) = F(m(z))\).

The solution is to affine-conjugate the Nova fractal formula, to move the starting critical point from \(1\) to \(0\). One way of doing it gives the modified Nova formula \[ z \to \frac{ \frac{2}{3} z^3 - 2 z - 1 }{ (z + 1)^2 } + c + 1 \] which seems to work fine when going beyond \(10^{30}\) at the same locations where the variant with critical point \(z=1\) fails. For example, see the ends of the following two short videos:

]]>In a previous post I presented a neat form for the series approximation coefficient updates for the quadratic Mandelbrot set. Note that the series for the derivative is redundant, as you can just take the derivative of the main series term by term. Recently superheal on fractalforums.org asked about series approximation for other powers. I explored a bit with SageMath (which is based on Python) and came up with this code (empirically derived formula highlighted):

def a(k): return var("a_" + str(k)) m = 10 for p in range(2,5): f(c, z) = z^p + c print(f) g(C, Z, c, z) = (f(C + c, Z + z) - f(C, Z)).expand().simplify() print(g) h0(C, Z, c) = sum( a(k) * c^k for k in range(1, m) ).series(c, m) print(h0) h1(C, Z, c) = g(C, Z, c, h0(C, Z, c)).expand().series(c, m) print(h1) def x(C, Z, c, k): return h1(C, Z, c).coefficient(c^k)def y(C, Z, c, k): return ((1 if k == 1 else 0) + sum([binomial(p, t) * Z^(p-t) * sum([Permutations(qs).cardinality() * product([a(q) for q in qs]) for qs in Partitions(k, length=t).list()]) for t in range(1, k + 1)]))for k in range(1, m): print((x(C, Z, c, k) - y(C, Z, c, k)).full_simplify(), " : ", a(k)," := ", x(C, Z, c, k)) print()

Example output:

(c, z) |--> z^2 + c (C, Z, c, z) |--> 2*Z*z + z^2 + c (C, Z, c) |--> (a_1)*c + (a_2)*c^2 + (a_3)*c^3 + (a_4)*c^4 + (a_5)*c^5 + (a_6)*c^6 + (a_7)*c^7 + (a_8)*c^8 + (a_9)*c^9 + Order(c^10) (C, Z, c) |--> (2*Z*a_1 + 1)*c + (a_1^2 + 2*Z*a_2)*c^2 + (2*a_1*a_2 + 2*Z*a_3)*c^3 + (a_2^2 + 2*a_1*a_3 + 2*Z*a_4)*c^4 + (2*a_2*a_3 + 2*a_1*a_4 + 2*Z*a_5)*c^5 + (a_3^2 + 2*a_2*a_4 + 2*a_1*a_5 + 2*Z*a_6)*c^6 + (2*a_3*a_4 + 2*a_2*a_5 + 2*a_1*a_6 + 2*Z*a_7)*c^7 + (a_4^2 + 2*a_3*a_5 + 2*a_2*a_6 + 2*a_1*a_7 + 2*Z*a_8)*c^8 + (2*a_4*a_5 + 2*a_3*a_6 + 2*a_2*a_7 + 2*a_1*a_8 + 2*Z*a_9)*c^9 + Order(c^10) 0 : a_1 := 2*Z*a_1 + 1 0 : a_2 := a_1^2 + 2*Z*a_2 0 : a_3 := 2*a_1*a_2 + 2*Z*a_3 0 : a_4 := a_2^2 + 2*a_1*a_3 + 2*Z*a_4 0 : a_5 := 2*a_2*a_3 + 2*a_1*a_4 + 2*Z*a_5 0 : a_6 := a_3^2 + 2*a_2*a_4 + 2*a_1*a_5 + 2*Z*a_6 0 : a_7 := 2*a_3*a_4 + 2*a_2*a_5 + 2*a_1*a_6 + 2*Z*a_7 0 : a_8 := a_4^2 + 2*a_3*a_5 + 2*a_2*a_6 + 2*a_1*a_7 + 2*Z*a_8 0 : a_9 := 2*a_4*a_5 + 2*a_3*a_6 + 2*a_2*a_7 + 2*a_1*a_8 + 2*Z*a_9 (c, z) |--> z^3 + c (C, Z, c, z) |--> 3*Z^2*z + 3*Z*z^2 + z^3 + c (C, Z, c) |--> (a_1)*c + (a_2)*c^2 + (a_3)*c^3 + (a_4)*c^4 + (a_5)*c^5 + (a_6)*c^6 + (a_7)*c^7 + (a_8)*c^8 + (a_9)*c^9 + Order(c^10) (C, Z, c) |--> (3*Z^2*a_1 + 1)*c + (3*Z*a_1^2 + 3*Z^2*a_2)*c^2 + (a_1^3 + 6*Z*a_1*a_2 + 3*Z^2*a_3)*c^3 + (3*a_1^2*a_2 + 3*Z*a_2^2 + 6*Z*a_1*a_3 + 3*Z^2*a_4)*c^4 + (3*a_1*a_2^2 + 3*a_1^2*a_3 + 6*Z*a_2*a_3 + 6*Z*a_1*a_4 + 3*Z^2*a_5)*c^5 + (a_2^3 + 6*a_1*a_2*a_3 + 3*Z*a_3^2 + 3*a_1^2*a_4 + 6*Z*a_2*a_4 + 6*Z*a_1*a_5 + 3*Z^2*a_6)*c^6 + (3*a_2^2*a_3 + 3*a_1*a_3^2 + 6*a_1*a_2*a_4 + 6*Z*a_3*a_4 + 3*a_1^2*a_5 + 6*Z*a_2*a_5 + 6*Z*a_1*a_6 + 3*Z^2*a_7)*c^7 + (3*a_2*a_3^2 + 3*a_2^2*a_4 + 6*a_1*a_3*a_4 + 3*Z*a_4^2 + 6*a_1*a_2*a_5 + 6*Z*a_3*a_5 + 3*a_1^2*a_6 + 6*Z*a_2*a_6 + 6*Z*a_1*a_7 + 3*Z^2*a_8)*c^8 + (a_3^3 + 6*a_2*a_3*a_4 + 3*a_1*a_4^2 + 3*a_2^2*a_5 + 6*a_1*a_3*a_5 + 6*Z*a_4*a_5 + 6*a_1*a_2*a_6 + 6*Z*a_3*a_6 + 3*a_1^2*a_7 + 6*Z*a_2*a_7 + 6*Z*a_1*a_8 + 3*Z^2*a_9)*c^9 + Order(c^10) 0 : a_1 := 3*Z^2*a_1 + 1 0 : a_2 := 3*Z*a_1^2 + 3*Z^2*a_2 0 : a_3 := a_1^3 + 6*Z*a_1*a_2 + 3*Z^2*a_3 0 : a_4 := 3*a_1^2*a_2 + 3*Z*a_2^2 + 6*Z*a_1*a_3 + 3*Z^2*a_4 0 : a_5 := 3*a_1*a_2^2 + 3*a_1^2*a_3 + 6*Z*a_2*a_3 + 6*Z*a_1*a_4 + 3*Z^2*a_5 0 : a_6 := a_2^3 + 6*a_1*a_2*a_3 + 3*Z*a_3^2 + 3*a_1^2*a_4 + 6*Z*a_2*a_4 + 6*Z*a_1*a_5 + 3*Z^2*a_6 0 : a_7 := 3*a_2^2*a_3 + 3*a_1*a_3^2 + 6*a_1*a_2*a_4 + 6*Z*a_3*a_4 + 3*a_1^2*a_5 + 6*Z*a_2*a_5 + 6*Z*a_1*a_6 + 3*Z^2*a_7 0 : a_8 := 3*a_2*a_3^2 + 3*a_2^2*a_4 + 6*a_1*a_3*a_4 + 3*Z*a_4^2 + 6*a_1*a_2*a_5 + 6*Z*a_3*a_5 + 3*a_1^2*a_6 + 6*Z*a_2*a_6 + 6*Z*a_1*a_7 + 3*Z^2*a_8 0 : a_9 := a_3^3 + 6*a_2*a_3*a_4 + 3*a_1*a_4^2 + 3*a_2^2*a_5 + 6*a_1*a_3*a_5 + 6*Z*a_4*a_5 + 6*a_1*a_2*a_6 + 6*Z*a_3*a_6 + 3*a_1^2*a_7 + 6*Z*a_2*a_7 + 6*Z*a_1*a_8 + 3*Z^2*a_9 (c, z) |--> z^4 + c (C, Z, c, z) |--> 4*Z^3*z + 6*Z^2*z^2 + 4*Z*z^3 + z^4 + c (C, Z, c) |--> (a_1)*c + (a_2)*c^2 + (a_3)*c^3 + (a_4)*c^4 + (a_5)*c^5 + (a_6)*c^6 + (a_7)*c^7 + (a_8)*c^8 + (a_9)*c^9 + Order(c^10) (C, Z, c) |--> (4*Z^3*a_1 + 1)*c + (6*Z^2*a_1^2 + 4*Z^3*a_2)*c^2 + (4*Z*a_1^3 + 12*Z^2*a_1*a_2 + 4*Z^3*a_3)*c^3 + (a_1^4 + 12*Z*a_1^2*a_2 + 6*Z^2*a_2^2 + 12*Z^2*a_1*a_3 + 4*Z^3*a_4)*c^4 + (4*a_1^3*a_2 + 12*Z*a_1*a_2^2 + 12*Z*a_1^2*a_3 + 12*Z^2*a_2*a_3 + 12*Z^2*a_1*a_4 + 4*Z^3*a_5)*c^5 + (6*a_1^2*a_2^2 + 4*Z*a_2^3 + 4*a_1^3*a_3 + 24*Z*a_1*a_2*a_3 + 6*Z^2*a_3^2 + 12*Z*a_1^2*a_4 + 12*Z^2*a_2*a_4 + 12*Z^2*a_1*a_5 + 4*Z^3*a_6)*c^6 + (4*a_1*a_2^3 + 12*a_1^2*a_2*a_3 + 12*Z*a_2^2*a_3 + 12*Z*a_1*a_3^2 + 4*a_1^3*a_4 + 24*Z*a_1*a_2*a_4 + 12*Z^2*a_3*a_4 + 12*Z*a_1^2*a_5 + 12*Z^2*a_2*a_5 + 12*Z^2*a_1*a_6 + 4*Z^3*a_7)*c^7 + (a_2^4 + 12*a_1*a_2^2*a_3 + 6*a_1^2*a_3^2 + 12*Z*a_2*a_3^2 + 12*a_1^2*a_2*a_4 + 12*Z*a_2^2*a_4 + 24*Z*a_1*a_3*a_4 + 6*Z^2*a_4^2 + 4*a_1^3*a_5 + 24*Z*a_1*a_2*a_5 + 12*Z^2*a_3*a_5 + 12*Z*a_1^2*a_6 + 12*Z^2*a_2*a_6 + 12*Z^2*a_1*a_7 + 4*Z^3*a_8)*c^8 + (4*a_2^3*a_3 + 12*a_1*a_2*a_3^2 + 4*Z*a_3^3 + 12*a_1*a_2^2*a_4 + 12*a_1^2*a_3*a_4 + 24*Z*a_2*a_3*a_4 + 12*Z*a_1*a_4^2 + 12*a_1^2*a_2*a_5 + 12*Z*a_2^2*a_5 + 24*Z*a_1*a_3*a_5 + 12*Z^2*a_4*a_5 + 4*a_1^3*a_6 + 24*Z*a_1*a_2*a_6 + 12*Z^2*a_3*a_6 + 12*Z*a_1^2*a_7 + 12*Z^2*a_2*a_7 + 12*Z^2*a_1*a_8 + 4*Z^3*a_9)*c^9 + Order(c^10) 0 : a_1 := 4*Z^3*a_1 + 1 0 : a_2 := 6*Z^2*a_1^2 + 4*Z^3*a_2 0 : a_3 := 4*Z*a_1^3 + 12*Z^2*a_1*a_2 + 4*Z^3*a_3 0 : a_4 := a_1^4 + 12*Z*a_1^2*a_2 + 6*Z^2*a_2^2 + 12*Z^2*a_1*a_3 + 4*Z^3*a_4 0 : a_5 := 4*a_1^3*a_2 + 12*Z*a_1*a_2^2 + 12*Z*a_1^2*a_3 + 12*Z^2*a_2*a_3 + 12*Z^2*a_1*a_4 + 4*Z^3*a_5 0 : a_6 := 6*a_1^2*a_2^2 + 4*Z*a_2^3 + 4*a_1^3*a_3 + 24*Z*a_1*a_2*a_3 + 6*Z^2*a_3^2 + 12*Z*a_1^2*a_4 + 12*Z^2*a_2*a_4 + 12*Z^2*a_1*a_5 + 4*Z^3*a_6 0 : a_7 := 4*a_1*a_2^3 + 12*a_1^2*a_2*a_3 + 12*Z*a_2^2*a_3 + 12*Z*a_1*a_3^2 + 4*a_1^3*a_4 + 24*Z*a_1*a_2*a_4 + 12*Z^2*a_3*a_4 + 12*Z*a_1^2*a_5 + 12*Z^2*a_2*a_5 + 12*Z^2*a_1*a_6 + 4*Z^3*a_7 0 : a_8 := a_2^4 + 12*a_1*a_2^2*a_3 + 6*a_1^2*a_3^2 + 12*Z*a_2*a_3^2 + 12*a_1^2*a_2*a_4 + 12*Z*a_2^2*a_4 + 24*Z*a_1*a_3*a_4 + 6*Z^2*a_4^2 + 4*a_1^3*a_5 + 24*Z*a_1*a_2*a_5 + 12*Z^2*a_3*a_5 + 12*Z*a_1^2*a_6 + 12*Z^2*a_2*a_6 + 12*Z^2*a_1*a_7 + 4*Z^3*a_8 0 : a_9 := 4*a_2^3*a_3 + 12*a_1*a_2*a_3^2 + 4*Z*a_3^3 + 12*a_1*a_2^2*a_4 + 12*a_1^2*a_3*a_4 + 24*Z*a_2*a_3*a_4 + 12*Z*a_1*a_4^2 + 12*a_1^2*a_2*a_5 + 12*Z*a_2^2*a_5 + 24*Z*a_1*a_3*a_5 + 12*Z^2*a_4*a_5 + 4*a_1^3*a_6 + 24*Z*a_1*a_2*a_6 + 12*Z^2*a_3*a_6 + 12*Z*a_1^2*a_7 + 12*Z^2*a_2*a_7 + 12*Z^2*a_1*a_8 + 4*Z^3*a_9

You can try it online. The first column should be all 0 if the formula is correct, which it seems to be for all the cases I've tried. But it remains to be proven rigourously that it is correct for all terms of all powers.

Efficiently implementing the general series coefficient update formula would probably need a multi-stage process: first (one-time cost given power and number of terms) calculate tables of constants (binomials, partitions as (index, multiplicity) pairs, partition permutation cardinalities). Second stage (once per iteration) calculate tables of powers of series coefficient variables (only going as far as the highest needed multiplicity for each index), and powers of Z. Third stage (once per series coefficient variable) combine all the powers and constants. Final stage, add 1 to the first variable.

It should be possible to generate OpenCL code for this at runtime. The expressions for each variable are of very different sizes but bundling a_k with a_(m-k) might give a more uniform load per execution unit.

]]>Newton's method can be used to trace external rays in the Mandelbrot set. See:

An algorithm to draw external rays of the Mandelbrot set

Tomoki Kawahira

April 23, 2009

AbstractIn this note I explain an algorithm to draw the external rays of the Mandelbrot set with an error estimate. Newton’s method is the main tool. (I learned its principle by M. Shishikura, but this idea of using Newton’s method is probably well-known for many other people working on complex dynamics.)

The algorithm uses \(S\) points in each dwell band, this number is called the "sharpness". Increasing the sharpness presumably makes the algorithm more robust when using the previous ray point \(c_n\) as the initial guess for Newton's method to find the next ray point \(c_{n+1}\) as the points are closer together.

I hypothesized that it might be better (faster) to use a different method for choosing the initial guess for the next ray point. I devised 3 new methods in addition to the existing one:

- nearest
- \( c_{n+1} := c_n \)
- linear
- \( c_{n+1} := c_n + (c_n - c_{n-1}) \)
- hybrid
- \( c_{n+1} := c_n + (c_n - c_{n-1}) \left| \frac{c_n - c_{n-1}}{c_{n-1} - c_{n-2}} \right| \)
- geometric
- \( c_{n+1} := c_n + \frac{(c_n - c_{n-1})^2}{c_{n-1} - c_{n-2}} \)

I implemented the methods in a branch of my mandelbrot-numerics repository:

git clone https://code.mathr.co.uk/mandelbrot-numerics.git cd mandelbrot-numerics git checkout exray-methods git diff HEAD~1

I wrote a test program for real-world use of ray tracing, namely tracing rays of preperiod + period ~= 500 to dwell ~1000, with all 4 methods and varying sharpness. I tested for correctness by comparing with the previous method, which was known to work well with sharpness around 4 through 8.

Results were disappointing. The hybrid and geometric methods failed in all cases, no matter the sharpness. The linear method failed for sharpness below 7, but when it worked (sharpness 7 or 8) it took about 68% of the time of the nearest method. However, the nearest method at sharpness 4 took 62% of the time of nearest at sharpness 8, so this is not so impressive.

The nearest method seemed to work all the way down to sharpness 2, which was surprising, and warrants further investigation: nearest at sharpness 2 took only 41% of the time of nearest at sharpness 8, if it turns out to be reliable this would be a good speed boost.

You can download my raw data.

Reporting this failed experiment in the interests of science.

]]>Another 2 months later and kf-2.15.4 is ready. Kalles Fraktaler 2 + is fast deep zooming Free Software for fractal graphics (Mandelbrot, Burning Ship, etc). Full change log:

kf-2.15.4 (2021-07-22)

- new: rewritten GUI for window size / image size (by popular request)
- new: “imaginary axis points up” option in the transformation dialog (requested by saka and others, makes complex plane comply with maths conventions)
- new: Hidden Mandelbrot formula (thanks to FractalAlex, Bruce Dawson) https://fractalforums.org/f/22/t/3576/msg22122#msg22122
- new: Hidden Mandelbrot a la Cos formula (thanks to 3Dickulus) https://fractalforums.org/f/74/t/3591/msg22215#msg22215

- set Factor A real and imaginary parts to control shape (e.g. 1+1i)
- new: Polarbrot formula, for p = 2, 3, 4 (thanks to gerrit) https://fractalforums.org/f/15/t/1916/msg23377#msg23377

- set Factor A real part to control power a
- fractional and/or negative power a is possible
- known issue: need to set Bailout escape radius very high (but not so high that derivatives overflow: try 1e10 or so)
- known issue: for positive a, reference at 0 fails (blank image) (workaround: offset the center very slightly in the Location dialog)
- known issue: for negative a, blank image (workaround: set Formula seeds non-zero (1e-300); this will reduce accuracy for deeper zooms)
- known issue: seams with numeric differences DE (analytic DE is ok)
- known issue: Newton zooming is not functional yet
- known issue: auto-skew is not functional yet
- new: convert between built-in formulas and hybrid formulas (when possible) with new buttons in the Formula dialog
- new: option Ignore Hybrids in the Formula dialog to list only the built-in formulas that don’t have hybrid equivalents
- new: optimized some built-in formulas using common subexpression elimination (6%-58% faster perturbation calculations)

- Burning Ship power 2, 3, 4, 5
- Buffalo power 2, 3, 4
- new: optimized some hybrid OpenCL perturbation calculations
- fix: Hybrid operator multiplication works with OpenCL
- fix: Omnibrot works with OpenCL
- fix: Mandelbrot power 4 and above with derivatives works with OpenCL
- fix: formulas 52, 53, 69, 70, 71 now work with OpenCL
- fix: formulas 4 (power 3), 20, 23-26, 42-50 now have correct derivatives for analytic DE
- fix: renamed some formulas (Abs General Quadratic Plus/Minus, Omnibrot) (suggested by gerrit)
- fix: Zoom Amount spinner in Transformation dialog works live
- fix: Transformation dialog Zoom Amount sign inversion
- fix: right mouse button drag in Transformation dialog stretches in a more intuitive way
- fix: Transformation dialog displays/edits total transformation instead of difference from last set transformation
- fix: Newton zoom dialog (atom domain) size of period <=1 is set to 1
- fix: OpenCL error dialog no longer appears and disappears again instantly
- internal: formula build system refactored for parallel building and much faster incremental builds
- internal: include structure rationalized for faster builds
- internal: use intermediate ar archives for linking many object files
- internal: formula preprocessor supports temporary variables (can be used for common subexpression elimination)
- upgrade to gsl-2.7
- upgrade to openexr-2.5.7

Get it from mathr.co.uk/kf/kf.html.

]]>Julia morphing, an artistic Mandelbrot set zooming technique, gives angled internal addresses that end with a regular structure like:

\[ \cdots p \to_{1/3} 2p+k \to_{1/3} 2(2p+k)+k \cdots \]

Back in 2013, I made an animation of an embedded Julia set orbit in the hairs around several period 7 islands. Embedded Julia sets are (relatively) surface features with simple angled internal addresses, while Julia morphs are much deeper and the addresses are more complicated.

My first attempt simply replaced a prefix of a template Julia morph
with 2^{N} prefixes based on the period patterns in the hairs
and while I got images (the angled internal addresses were realizable by
actual locations in the Mandelbrot set) they didn't form a coherent
animation in any way. I asked
a question on mathoverflow
about this.

I went back to the graphical Mandelbrot set explorers, and figured out that the suffix needed to change when changing the prefix, because k (as in 2p+k) changes according to which child bulb the hair is attached to. Moreover, there is another case to consider, whether the child bulb's period appears in the address (=> address has more numbers) or not (=> address is the simple 2p+k pattern).

I did the initial graphical analysis in m-perturbator-gtk, heavily using its annotation features such as periodic point marking and external ray tracing to find external angles. Then I used mandelbrot-symbolics in ghci, to convert between angles and addresses. Here's some of the Haskell code:

-- ... main = do [depth] <- map read <$> getArgs let prefixes = (".(" ++) . (++ "0111)") . concat <$> replicateM depth [lo, hi] julia = 3 * depth + 4 morph = 4 lo = "011" hi = "100" period (Sym.Angled _ _ a) = period a period (Sym.Unangled p) = p writeFile "a.txt" . unlines . map (\prefix -> let Just addressPrefix@(Sym.Angled 1 _ (Sym.Angled 2 _ (Sym.Angled 3 pq _))) = (Sym.angledAddress . Sym.rational) =<< Txt.parse prefix addressSuffix (Sym.Angled p t a) = Sym.Angled p t (addressSuffix a) addressSuffix (Sym.Unangled p) = Sym.Angled p (1 Sym.% 2) (go morph p0) where p0 | m < julia = julia + m | m > julia = 2 * p go 0 p | m < julia = Sym.Unangled p go n p | m < julia = Sym.Angled p (1 Sym.% 3) (go (n - 1) (2 * p + m)) go 0 p | m > julia = Sym.Angled p (1 Sym.% 2) (Sym.Angled (p + 1) (1 Sym.% 2) (Sym.Unangled (p + 2))) go n p | m > julia = Sym.Angled p (1 Sym.% 2) (Sym.Angled (p + 1) (1 Sym.% 2) (Sym.Angled (p + 2) (1 Sym.% 3) (go (n - 1) (2 * (p + 2) + m - 2)))) m = 3 * fromIntegral (Sym.denominator pq) address = addressSuffix addressPrefix node (Sym.Angled p1 _ (Sym.Unangled p2)) | m < julia = p1 + p2 node (Sym.Angled p1 _ (Sym.Angled _ _ (Sym.Angled _ _ (Sym.Unangled p2)))) | m > julia = p1 + p2 node (Sym.Angled _ _ a) = node a Just ray = Txt.plain . Sym.binary . <$> Sym.addressAngles address in ray ++ "\t" ++ show (period address) ++ "\t" ++ show (node address)) $ prefixes

That outputs one of the external angles of the angled internal address,
of the Julia morph corresponding to each prefix, as well as its period, and
the period of a neighbouring node in the Julia morph which I used in some
C code to align all the morphs in screen-space. I use the command line tool
`parallel -k`

to parallelize tracing these external rays using
`m-exray-in`

from `mandelbrot-numerics`

, then found
the periodic nucleus of the minibrot islands with `m-nucleus`

, and
the approximate zoom level using `m-domain-size`

and some
`ghc -e`

scripting (starting `ghc`

is slow, better
to run it once to handle multiple inputs). I fed this to some custom C code
that aligns the view with the node period:

#include <stdio.h> #include <mandelbrot-numerics.h> int main(int argc, char **argv) { if (! (argc > 1)) return 1; int Period = atoi(argv[1]); // hardcoded precision, should be based on zoom level mpfr_t Re, Im, Zoom; mpfr_init2(Re, 1000); mpfr_init2(Im, 1000); mpfr_init2(Zoom, 53); // parse KFR input (no error checking) getc(stdin); getc(stdin); getc(stdin); getc(stdin); mpfr_inp_str(Re, stdin, 10, MPFR_RNDN); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); mpfr_inp_str(Im, stdin, 10, MPFR_RNDN); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); mpfr_inp_str(Zoom, stdin, 10, MPFR_RNDN); getc(stdin); // find nearby node nucleus and use it to align view mpfr_d_div(Zoom, 2, Zoom, MPFR_RNDN); mpfr_mul_d(Zoom, Zoom, 0.3, MPFR_RNDN); mpc_t c1; mpc_init2(c1, 1000); mpfr_add(mpc_realref(c1), Re, Zoom, MPFR_RNDN); mpfr_set(mpc_imagref(c1), Im, MPFR_RNDN); m_r_nucleus(c1, c1, Period, 64, 1); mpfr_sub(mpc_realref(c1), mpc_realref(c1), Re, MPFR_RNDN); mpfr_sub(mpc_imagref(c1), mpc_imagref(c1), Im, MPFR_RNDN); mpc_abs(Zoom, c1, MPFR_RNDN); mpfr_div_d(Zoom, Zoom, 0.3, MPFR_RNDN); mpfr_d_div(Zoom, 2, Zoom, MPFR_RNDN); mpfr_printf("Re: %Re\r\nIm: %Re\r\nZoom: %.18e\r\n", Re, Im, mpfr_get_d(Zoom, MPFR_RNDN)); mpc_arg(Zoom, c1, MPFR_RNDN); printf("RotateAngle: %.18f\r\nIterations: 15000\r\n", mpfr_get_d(Zoom, MPFR_RNDN) / (2 * 3.141592653589793) * 360); return 0; }

Then I rendered the final images with `kf.x86_64.exe`

.
All of this was orchestrated by a Bash shell script, `a.txt`

generated by the Haskell with depth 12 (4096 lines) (this took the most
time, almost 2 days):

#!/bin/bash prec=1000 cat a.txt | while read ray period node do echo "m-exray-in 100 '${ray}' 8 $((2 * 8 * ${period}))" done | parallel -k | paste a.txt - | tee b.txt cat b.txt | while read ray period node re im do echo m-nucleus $prec $re $im $period 64 1 done | parallel -k | paste b.txt - | tee c.txt cat c.txt | while read ray period node ere eim nre nim do echo m-domain-size $prec $nre $nim $period done | parallel -k | paste c.txt - | tee d.txt cat d.txt | while read ray period node ere eim nre nim ds do echo $ds done | ghc -e "interact $ unlines . map (show . (\s -> 2 / (10 * s**1.125)) . read) . lines" | paste d.txt - | tee e.txt cat -n e.txt | while read n ray period node ere eim nre nim ds zoom do echo -e "Re: $nre\r\nIm: $nim\r\nZoom: $zoom\r" | m-align-nodes ${node} > $(printf %04d.kfr $n) done for i in ????.kfr do kf.x86_64.exe -s settings.kfs -l "$i" -c palette.kfp -t "$i.tif" 2>&1 | grep -v fixme # silence Wine error flood convert -colorspace RGB $i.tif -geometry 1920x1080 -colorspace sRGB "${i%kfr}tif" && rm "$i.tif" done ffmpeg -r 60 -i "%04d.tif" -pix_fmt yuv420p -profile:v high -level:v 5.1 -crf:v 20 spin.mkv

I asked another question about the magic number 1.125 in this script.

While the rays were tracing (using the CPU) I ran some of the commands
on the first ray to get the first KFR file, and I rendered a zoom video
(using the GPU) to the minibrot at its center, controlling the zoom speed
and depth in `zoomasm`

so that the zoom video slowed down and
paused at the exact zoom depth of the spin video (I tried to use maths
to work out the numbers to enter into zoomasm, but couldn't figure it out
so I eventually used binary search by hand and got it close enough).

I also made a soundtrack, using the pairs of rays of each spin video
frame, angles expressed in binary, converted to wavetables and stretched
to the length of the frame using FFT/zeropad/IFFT. I processed the result
of this in `audacity`

, to extend it to the length of the whole
video (making sure to keep the spin section correctly aligned in time):

#include <complex.h> #include <math.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sndfile.h> #include <fftw3.h> int main(int argc, char **argv) { const int SR = 192000; const int FPS = 60; const int length = SR / FPS; float audio[length][2]; double _Complex *ifft_in = fftw_malloc(length * sizeof(*ifft_in)); double _Complex *ifft_out = fftw_malloc(length * sizeof(*ifft_out)); fftw_plan backward = fftw_plan_dft_1d(length, ifft_in, ifft_out, FFTW_BACKWARD, FFTW_PATIENT | FFTW_DESTROY_INPUT); int old_period = 0; double _Complex *fft_in = 0; double _Complex *fft_out = 0; fftw_plan forward = 0; SF_INFO onfo = { 0, SR, 2, SF_FORMAT_WAV | SF_FORMAT_FLOAT, 0, 0 }; SNDFILE *ofile = sf_open("audio.wav", SFM_WRITE, &onfo); while (true) { int period = 0; char *ray[2] = { 0, 0 }; if (3 != scanf("%d\t%ms\t%ms\n", &period, &ray[0], &ray[1])) { break; } if (period != old_period) { if (fft_in) { fftw_free(fft_in); } if (fft_out) { fftw_free(fft_out); } if (forward) { fftw_destroy_plan(forward); } fft_in = fftw_malloc(period * sizeof(*fft_in)); fft_out = fftw_malloc(period * sizeof(*fft_out)); forward = fftw_plan_dft_1d(period, fft_in, fft_out, FFTW_FORWARD, FFTW_MEASURE | FFTW_DESTROY_INPUT); } for (int c = 0; c < 2; ++c) { int v = 0; for (int p = 0; p < period; ++p) { fft_in[p] = v; v += ray[c][p] == '0' ? -1 : 1; } double s = v / (double) period; double _Complex dc = 0; for (int p = 0; p < period; ++p) { dc += (fft_in[p] -= p * s); } dc /= period; for (int p = 0; p < period; ++p) { fft_in[p] -= dc; } fftw_execute(forward); memset(ifft_in, 0, length * sizeof(*ifft_in)); for (int p = 0; p < period/2; ++p) { ifft_in[p] = fft_out[p]; ifft_in[length - 1 - p] = fft_out[period - 1 - p]; } fftw_execute(backward); double rms = 0; for (int p = 0; p < length; ++p) { audio[p][c] = creal(ifft_out[p]); rms += audio[p][c] * audio[p][c]; } rms = 0.25 / sqrt(rms / length); for (int p = 0; p < length; ++p) { audio[p][c] *= rms; } free(ray[c]); } sf_writef_float(ofile, &audio[0][0], length); old_period = period; } sf_close(ofile); return 0; }

Then I split the zoom video at this point and splicing in the spin video:

ffmpeg -i zoom.mkv -t 136 -codec:v copy zoom-1.mkv ffmpeg -i zoom.mkv -ss 136 -codec:v copy zoom-2.mkv cat > zoom.txt <<EOF file 'zoom-1.mkv' file 'spin.mkv' file 'zoom-2.mkv' EOF ffmpeg -f concat -i zoom.txt -i soundtrack.m4a \ -codec:v copy -codec:a copy "Julia morph orbit in the hairs.mkv"

Then two-pass encoding to a lower bitrate version for web streaming:

ffmpeg -i "Julia morph orbit in the hairs.mkv" \ -codec:a copy -profile:v high -level:v 5.1 -g 120 \ -x264opts keyint=120:no-scenecut -b:v 6M -movflags +faststart \ -pass 1 -y "Julia morph orbit in the hairs.mp4" ffmpeg -i "Julia morph orbit in the hairs.mkv" \ -codec:a copy -profile:v high -level:v 5.1 -g 120 \ -x264opts keyint=120:no-scenecut -b:v 6M -movflags +faststart \ -pass 2 -y "Julia morph orbit in the hairs.mp4"

And that's the video linked from the image at the top of the page.

]]>*Old Wood Dish* (2010) by James W. Morris is a fractal artwork,
a zoomed-in view of part of the Mandelbrot set. The magnification factor
of 10^{152} is quite shallow by today's standards, but in 2010 the
perturbation and series approximation techniques for speeding up image
generation had not yet been developed: this is a deep zoom for that era.
Thankfully JWM's (now defunct) gallery included the parameter files, the
image linked above is a high resolution re-creation in Kalle's Fraktaler,
thanks to a parameter file conversion script I wrote. You can find out
more about JWM's software MDZ and see more of his images on my
mirror of part of his old website.

*Old Wood Dish* is an example of what would now be called
"Julia morphing", using the property that zooming in towards baby Mandelbrot
set islands doubles-up (and then quadruples, octuples, ...) the features
you pass. This allows you to sculpt patterns, here the pattern has a tree
structure.

Each baby Mandelbrot set islands has a positive integer associated to
it: its period. Iteration of the center of its cardioid repeats with that
period, returning to 0. Atom periods are "near miss" periods, where the
iteration gets nearer to 0 than it ever did before. They indicate a nearby
baby Mandelbrot set island (or child bulb) of that period.
The atom periods of the center of *Old Wood Dish* are:

1, 2, 34, 70, 142, 286,574,862, 1438, 2878, 5758

One can see a pattern: 2 * 34 + 2 = 70; 2 * 70 + 2 = 142; 2 * 142 + 2 = 286. But this pattern is broken at the numbers highlighted: 2 * 574 + 2 = 1150 != 862.

Using Newton's root-finding method in one complex variable,
one can find the nearby baby Mandelbrot sets with those periods. When zooming
out, these eventually each become the lowest period island in the view in turn
(higher periods are closer to the starting point), and the zoom level at which
this happens is usually significant in terms of the decisions made when performing
Julia morphing. These zoom levels (log base 10) for *Old Wood Dish* are:

0.114, 0.591, 4.69, 8.44, 14.0, 22.4, 30.8, 43.4, 66.6, 101, 152

and the successive ratios of these numbers are

5.15, 7.94, 1.79, 1.66, 1.59,1.37,1.40, 1.53, 1.52, 1.50

Repeated Julia morphing leads to these ratios tending to a constant (often 1.5), but the two numbers highlighted are clearly outside the curve: one can see that these correspond to the two mismatching periods. I'll have to ask him to see if this was intentional or an accident.

A list of atom domain periods is related to a concept called an
internal address, which is an ascending list of the lowest periods of
the hyperbolic components (cardioid-like or disk-like shapes) that you
pass through along the filaments on the way to the target from the origin.
An extension, angled internal addresses, removes the ambiguity of which
way to turn (for example, there are two period 3 bulbs attached to the
period 1 cardioid, they have internal angles 1/3 and 2/3). One can find
angled internal addresses by converting from external angles, and one
can find external angles by tracing rays outwards from a point towards
infinity. The angled internal address of *Old Wood Dish* starts:

1 1/2 2 16/17 33 1/2 34 1/3 69 1/2 70 1/3 141 1/2 142 1/3 285 1/2 286 ...

and the pattern can be extended indefinitely by

... 1/3 (p-1) 1/2 p 1/3 (2p+1) 1/2 (2p+2) ...

The numerators of the angles in an angled internal address can be varied
freely, so one can create a whole family of variations. Varying the 1/3 to
2/3 only changes the alignments of the decorations outside the tree structure,
but varying 16/17 changes the shapes that tree is built from. Here are
*Old Wood Dish* variations 1-16, with the irregular zoom pattern
adjusted to a fully-regular zoom ending up with period 9214:

I found the center coordinates for these images by tracing external rays towards each period 9214 inner island. This took almost 5 hours wall-clock time with 16 threads in parallel (one for each ray). I then found the approximate view radius by atom domain size raised to the power of 1.125, multiplied by 10. These magic numbers were found by exploring shallower versions graphically. Using this radius I used Newton's method again, to find the pair of period 13820 minibrots at the first junctions near the center. I found these periods using KF-2.15.3's newly improved Newton zooming dialog. I used their locations to rotate and scale all the images for precise alignment. Animated it looks quite hypnotic I think:

Software used:

- mandelbrot-numerics m-describe program and script to get rough idea of period structure;
- mandelbrot-perturbator GTK program to explore the shallow layers and trace external rays to find external angles;
- mandelbrot-symbolics Haskell library in GHCI REPL to convert (both directions) between external angles and angled internal addresses;
- mandelbrot-numerics m-exray-in program to trace rays inwards given external angles;
- mandelbrot-numerics m-nucleus program to find periodic root from ray end point;
- mandelbrot-numerics m-domain-size program to find approximate view size;
- kf-2.15.3 interactive Newton zooming dialog to find period of the first junction nodes;
- custom code in C to align views, using mandelbrot-numerics library;
- custom code in bash shell to combine everything into KFS+KFR files;
- kf-2.15.3 command line mode to render each KFS+KFR to very large TIFF files;
- ImageMagick convert program to downscale for anti-aliasing (PNG for web, and smaller GIFs);
- gifsicle program to combine the 16 frames into 1 animated GIF.

After almost 2 months of work I'm happy to announce a new release of Kalles Fraktaler 2 +, fast deep zooming Free Software for fractal graphics (Mandelbrot, Burning Ship, etc). Most of the focus has been on speed improvements, with rescaled iterations ala Pauldelbrot providing a big speedup for most deep zoom locations. Full change log:

kf-2.15.3 (2021-05-26)

- new: updated progress reporting in status bar to include more information
- new: texture resize control in colouring dialog (disable to get actual image pixels in OpenGL GLSL shaders)
- new: texture browse dialog allows selecting BMP and PNG images as well as JPEG
- new: Newton-Raphson zooming dialog changes

- user interface redesigned from scratch
- new absolute zooming modes (previous mode is called relative)
- new atom domain mode (for Mandelbrot set power 2 and hybrids only)
- new size factor control
- can auto-capture zoom depth after Newton zoom
- auto skew (escape) moved to transformation dialog
- new: transformation dialog changes

- new zoom adjustment control (with rotation on left mouse button)
- now shows the difference between the original transformation and the new transformation, instead of the total transformation
- stretch amount now displayed in cents for a more friendly range
- spin buttons added so scroll wheel and arrows can adjust values, which live-update the transformed image
- auto skew (escape) moved from Newton-Raphson zooming dialog
- new: single precision floating point support for shallow zooms (until zoom e20) (disabled by default due to some locations having undetected glitches)
- new: single precision floatexp extended floating point for arbitrarily deep zooms (disabled by default due to some locations having undetected glitches)
- new: OpenCL can work in single precision mode, for example on devices that don’t support double precision
- new: rescaled perturbation calculations for arbitrarily deep zooms (usually faster than old long double and floatexp implementations; with or without derivatives; with or without OpenCL; single or double precision, single precision disabled by default due to some locations having undetected glitches); supported formulas:

- Mandelbrot power 2
- Mandelbrot power 3
- Burning Ship power 2
- hybrid formula editor
- new: rescaled series approximation calculations for Mandelbrot power 2 (about 30% faster than the all-floatexp implementation, can be disabled if necessary in the perturbation and series approximation tuning dialog)
- new: number type selection dialog (advanced menu) allows fine-tuning allowed implementations
- new: “reuse reference” (advanced menu) can be used together with “auto solve glitches” (this uses additional memory for the primary reference)
- fix: “reuse reference” re-calculates reference when the used number type changes (fixes some issues with bad images and/or crashes)
- new: “reference strict zero” control in perturbation and series approximation tuning dialog (advanced menu, experimental); affects rescaled iterations only
- new: lower level implementation of reference calculations for hybrids is over 7x faster (now only 10% slower than built in versions)
- new: OpenCL can run threaded to improve user interface responsiveness (enabled by default; can be disabled in OpenCL device selection dialog)
- new: “‘Open’ resets default parameters” setting can be disabled to load minimal KFR/KFP without resetting missing parameters to defaults (this setting is enabled by default for backwards compatibility)
- new: “glitch low tolerance” can be a fraction between 0 and 1
- new: “approx low tolerance” can be a fraction between 0 and 1
- new: crash recovery offers to restore settings as well as parameters
- fix: correct power calculation for multiplied hybrid operators (symptom: seams between iteration bands with numeric DE)
- fix: documentation uses subsections instead of lists for improved navigation and table of contents
- known issue: some locations (especially Burning Ship “deep needle”) are much slower and need much more memory; workaround:

- disable “rescaled single” in number type selection dialog; and if still slow:
- disable “rescaled double” in number type selection dialog
- known issue: some locations have undetected glitches in single precision; workaround:

- disable “single”, “rescaled single” and “floatexp single” in number type selection dialog; or
- enable “glitch low tolerance” in perturbation and series approximation tuning dialog

Get it from mathr.co.uk/kf/kf.html.

Now I'll probably take a break from coding KF until after the summer, apart from bugfixes, as I don't have a big exciting idea to inspire and motivate me. Some ideas for when I come back include:

- stripe average colouring via a ring buffer of last few iterations
- auto skew method based on directional DE distribution in an image
- OpenCL/OpenGL sharing
- use multiple OpenCL platforms and devices at the same time
- GLSL file watcher so you can use your favourite text editor when writing colouring algorithms
- plain iterations (without perturbation) for very shallow zooms
- port/embed mandelbrot-perturbator engine for glitch correction of power 2 Mandelbrot by "rebasing and carrying on"
- store starting zoom in Newton-Raphson progress updates and add resume functionality
- rip out 75% of the built-in formulas and replace with hybrid formula designer versions
- refactor the build system for faster incremental development

If anyone out there wants to work on any of these or other ideas, I'll be more than happy to help you get started navigating the code to know where the changes should be made.

]]>