In an appendix to the paper from which I implemented the slow mating algorithm in my previous post, there is a brief description of another algorithm:

The Thurston Algorithm for quadratic matings

Initialization A.2 (Spider algorithm with a path)Suppose \(\theta = \theta_1 \in \mathbb{Q} \backslash \mathbb{Z}\) has prepreperiod \(k\) and period \(p\). Define \((x_1(t), \ldots, x_{k+p}(t))\) for \(0 \le t \le 1\) as

\[ x_1(t) = t e^{i 2 \pi \theta_1} \\ x_p(t) = (1 - t) e^{i 2 \pi \theta_p}, \text{ if } k = 0 \\ x_j(t) = e^{i 2 \pi \theta_j}, \text{ otherwise.} \]Pull this path back continuously with \(x_i(t + 1) = \pm \sqrt{x_{i+1}(t)-x_1(t)}\). Then it converges to the marked points of \(f_c\) with appropriate collisions.

In short, given a rational \(\theta\) measured in turns, this provides a way to calculate \(c\) in the Mandelbrot set that has corresponding dynamics. Here \(\theta_j = 2^{j - 1} \theta \mod 1\), and the desired \(c = x_1(\infty)\).

This week I implemented it in my mandelbrot-numerics library, in the hope that it might be faster than my previous method of tracing external rays. Alas, it wasn't to be: both algorithms are \(O(n^2)\) when ignoring the way cost varies with numerical precision, and the spider path algorithm has higher constant factors and requires \(O(n)\) space vs ray tracing \(O(1)\) space. This meant spider path was about 6x slower than ray tracing when using a single-threaded implementation, in one test at period 469, and I imagine it would be slower still at higher periods and precisions.

This isn't entirely surprising, spider path does \(s n\) complex square roots to extend the paths by \(t \to t + 1\), while ray trace does \(s t\) arithmetical operations to extend the ray from depth \(t \to t + 1\). The \(O(n^2)\) comes from \(t\) empirically needing to be about \(2 n\) to be close enough to switch to the faster Newton's root finding method.

Moreover spider path needs very high precision all the way through, the initial points on the unit circle need at least \(n\) bits (I used about \(2 n\) to be sure) to resolve the small differences in external angles, even though the final root can usually be distinguished from other roots of the same period using much less precision. In fact I measured spider path time to be around \(O(n^{2.9})\), presumably because of the precision. Ray tracing was very close to \(O(n^2)\).

Ray tracing has a natural stopping condition: when the ray enters the atom domain with period \(p\), Newton's method is very likely to converge to the nucleus at its center. I imagine something similar will apply to preperiodic Misiurewicz domains, but I have not checked yet. I tried it with spider path but in one instance I got a false positive and ended up at a different minibrot to the one I wanted.

The only possible advantages that remain for the spider path algorithm is that it can be parallelized more effectively than ray tracing, and that the numbers are all in the range \([-2,2]\) which means fixed point could be used. Perhaps a GPU implementation of spider path would be competitive with ray tracing on an elapsed wall-clock time metric, though it would probably still lose on power consumption.

I plotted a couple of graphs of the spider paths, the path points end up log-spiraling around their final resting places. I think this means it converges linearly. Ray tracing is also linear when you are far from the landing point (before the period-doubling cascade starts in earnest). Newton's method converges quadratically, which means the number of accurate digits doubles each time, but you need to start from somewhere accurate enough.

]]>I recently came across Arnaud Chéritat's polynomial mating movies and just had to try to recreate them.

If \(p\) and \(q\) are in the Mandelbrot set, they have connected Julia sets for the quadratic polynomial functions \(z^2+p\) and \(z^2+q\). If they are not in conjugate limbs (a limb is everything beyond the main period 1 cardioid attached at the root of a given immediate child bulb, conjugation here is reflection in the real axis, the 1/2 limb is self-conjugate) then the Julia sets can be mated: glue the Julia sets together respecting external angles so that the result fills the complex plane (which is conveniently represented as the Riemann sphere). It turns out that this mating is related to the Julia set of a rational function of the form \(\frac{z^2+a}{z^2+b}\).

One algorithm to compute \(a\) and \(b\) is called "slow mating". Wolf Jung has a pre-print which explains how to do it in chapter 5: The Thurston Algorithm for quadratic matings.

My first attempts just used Wolf Jung's code and later my own code, to compute the rational function and visualize it in Fragmentarium (FragM fork). This only worked for displaying the final limit set, while Chéritat's videos had intermediate forms. I found a paper which had this to say about it:

On The Notions of Mating

Carsten Lunde Petersen & Daniel Meyer

5.4. Cheritat moviesIt is easy to see that \(R_\lambda\) converges uniformly to the monomial \(z^d\) as \(\lambda \to \infty\). Cheritat has used this to visualize the path of Milnor intermediate matings \(R_\lambda\), \(\lambda \in ]1,\infty[\) of quadratic polynomials through films. Cheritat starts from \(\lambda\) very large so that \(K_w^\lambda\) and \(K_b^\lambda\) are essentially just two down scaled copies of \(K_w\) and \(K_b\), the first near \(0\), the second near \(\infty\). From the chosen normalization and the position of the critical values in \(K_w^\lambda \cup K_b^\lambda\) he computes \(R_\sqrt{\lambda}\). From this \(K_w^\sqrt{\lambda} \cup K_b^\sqrt{\lambda}\) can be computed by pull back of \(K_w^\lambda \cup K_b^\lambda\) under \(R_\sqrt{\lambda}\). Essentially applying this procedure iteratively one obtains a sequence of rational maps \(R_{\lambda_n}\) and sets \(K_w^{\lambda_n} \cup K_b^{\lambda_n}\), where \(\lambda_n \to 1+\) and \(\lambda_n^2 = \lambda_{n-1}\). For more details see the paper by Cheritat in this volume.

What seems to be the paper referred to contains this comment:

Tan Lei and Shishikura’s example of non-mateable degree 3 polynomials without a Levy cycle

Arnaud Chéritat

Figure 4The Riemann surface \(S_R\) conformally mapped to the Euclidean sphere, painted with the drawings of Figure 2. The method for producing such a picture is interesting and will be explained in a forthcoming article; it does not work by computing the conformal map, but instead by pulling-back Julia sets by a series of rational maps. It has connections with Thurston's algorithm.

I could not find that "forthcoming" article despite the volume having been published in 2012 following the 2011 workshop, so I emailed Arnaud Chéritat and got a reply to the effect that it had been cancelled by the author.

My first attempts at coding the slow mating algorithm worked by pulling back the critical orbits as described in Wolf Jung's preprint. The curves look something like this:

A little magic formula for finding the parameters \((a, b)\) for the function \(\frac{z^2+a}{z^2+b}\):

\[a = \frac{C(D-1)}{D^3(1-C)}\] \[b = \frac{D-1}{D^2(1-C)}\]

where \((C,D)\) are the pulled back \(1\)th iterates. This was reverse-engineered from Wolf Jung's code, which worked with separate real and imaginary components, with no comments and heavy reuse of the same variable names. I'm not sure if it is correct but it seems to give useable results when plugged into FragM for visualization:

I struggled to implement the intermediate images at first: I tried pulling back from the coordinates of a filled in Julia set but that needed huge amounts of memory and the resolution was very poor:

Eventually I figured out that I could invert each pullback function into something of the form \(\frac{az^2+b}{cz^2+d}\) and push forward from pixel coordinates to colour according to which hemisphere it reached:

I struggled further, until I found the two bugs that were almost cancelling each other out. The coordinates in each respective hemisphere can be rescaled, and thereafter regular iterations of \(z^2+c\) until escape or maximum iterations could be used to colour the filled in Julia sets expanding within each hemisphere:

After that it was quite simple to bolt on dual-complex-numbers for automatic differentiation, to compute the derivatives for distance estimation to make the filaments of some Julia sets visible:

I also added an adaptive super-sampling scheme: if the standard deviation of the current per-pixel sample population divided by the number of samples is less than a threshold, I assume that the next sample will make negligible changes to the appearance, and so I stop. This speeds up interior regions (which need to be computed to the maximum iteration count) because the standard deviation will be 0 and it will stop after only the minimum sample count. I also have a maximum sample count to avoid taking excessive amounts of time. I do blending of samples in linear colour space, with sRGB conversion only for the final output.

Get the code:

git clone https://code.mathr.co.uk/mating.git

Currently about 600 lines of C99 with GNU getopt for argument parsing, but I may port the image generation part to OpenCL because my GPU is about 8x faster than my CPU for some double-precision numerical algorithms, which will help when rendering animations.

]]>Computerized representations of numbers come in a few different flavours.
The most common is an `int`

, which is typically 4 bytes long, or
32 bits. A bit is either 0 or 1, and a byte is the smallest unit addressable
by the CPU and consists of 8 bits, able to store 2^{8} or 256 distinct
values. An `unsigned int`

stores numbers from `0`

to
`2`

, and a ^{32}-1`signed int`

uses
twos-complement to represent negative numbers: the most significant bit is
`1`

for negative numbers, and *sign extension* means it
really represents an infinite stream of `1`

s extending to the left.
You can find the twos-complement of a number by inverting all the bits and then
add 1 to the whole number.

Computers also have `float`

numbers, which have a floating point,
like scientific notation for numbers. The number of significant digits is fixed,
but you can represent very large numbers and very small numbers by combining the
mantissa digits with an exponent, which moves the point separating the fractional
part. I'll talk more about this in a future post. Floating point typically uses
a magnitude-and-sign representation instead of twos-complement.

Fixed-point numbers are somewhere in between, they can represent numbers with fractional parts, but the number of digits after the point is fixed, so very small numbers cannot be represented as accurately as numbers with magnitude near 1. For some purposes this is just fine, as the numbers you are concerned with always have a magnitude near 1. For example, when calculating the Mandelbrot set, each pixel's c value is between -2-2i and +2+2i, and the interesting parts have |c|>0.25 too. The escape time algorithm iterates z→z²+c starting from 0 until |z|>2 (or you give up after an arbitrary iteration count limit) which means most the z values have magnitude near 1. This makes high precision fixed-point a good fit: you need more digits when zooming in, otherwise you can't represent neighbouring pixels' c values, but you don't need the more-complex algorithms of floating-point numerics because you don't need to represent very small or very large numbers.

So how do you implement fixed-point numerics on a computer? I used
`unsigned int`

limbs, which are essentially digits in base 2^{32},
and stored numbers as an array of limbs and two numbers counting the number of
limbs before the point and the number of limbs after the point. My code is quite
general, with mixed precision operations, but this aspect is not so well tested as
the Mandelbrot set calculations only need 1 limb before the point. The most primitive
operations are reading a limb from the vector, which does sign extension in
twos-complement for limbs larger than the stored array, and pads with 0 for smaller
limbs; and writing a limb to a vector, which has some assertions to crash fast
instead of corrupting memory with an out of bounds write.

**-a** is implemented by complementing all the limbs and adding 1
in the least significant place, propagating carries along towards the more significant
limbs. Carries can be calculated by comparing the result of addition with the
addends, if the result is smaller then it overflowed (`unsigned int`

arithmetic is modulo 2^{32}) and you need to add 1 to the next limb.

**a<<n** and **a>>n** can be calculated
by reading two adjacent limbs into a double-wide `unsigned long long int`

(there is disagreement over whether `long`

is 32bit or 64bit on
different systems, `long long`

is 64bit almost everywhere I think) and
shift the correct 32bit part out of it while looping over the arrays, being careful
not to clobber the array you are reading from in case it is aliased to the array
you are writing to (it is nice to be able to shift in-place to save memory).

**a+b** is implemented by zipping from least significant limb to
most significant, propagating carries along and adding them to the next pair of
limbs. **a-b** is implemented similarly, only propagating borrows
to subtract from the next pair. Borrow is calculated by the subtracted limb being
larger than the limb from which it is subtracted. OpenGL 4 / GLSL 400 and above
have `uaddCarry()`

and `usubBorrow()`

functions which make
this easier to implement, but I wrote my implementation in OpenCL / C / C++ which
afaik doesn't have these.

**a ^{2}** is implemented as an optimization of multiplication
taking advantage of symmetry to optimize duplicated work like

`x*y+y*x`

as `(x*y)<<1`

which may make a small difference. Multiplication
`a`

by every limb in `b`

and collect up the results, so that is what I did as
a first pass for simplicity (more efficient algorithms like Karatsuba multiplication
may be interesting to investigate later). It differs from integer multiplication as
found in libgmp (for example) because the least significant limbs of the result can
often be discarded or not computed, at least if we don't care about correct rounding
(all the operations I implemented simply truncate towards negative infinity).
I made a diagram (click for bigger) showing how I implemented it (actually a version
of this diagram came before I started writing any code at all):The input numbers are fed in from the right hand side and the bottom, into limb-by-limb multiplication (32×32→64bit) which outputs the most significant and least significant limbs of the result to two wide accumulator busses that flow diagonally down and left. At the end the accumulators are combined into limbs by propagating carries upwards. The accumulators don't need to be super wide, I think they need O(log(number of limbs)) extra bits beyond the width of the limb, but 64bit is certainly plenty and is usually available.

One final thing to note, I tried benchmarking both big-endian and little-endian limb orderings, to see how that affected things. To my surprise big-endian came out a little faster, surprising because most operations operate over limbs from the little end first, but I think that was because I was only testing with 1 limb before the point, which potentially made an addition (statically known to be +0) be completely omitted by the optimizer. At zoom 1e90, which corresponds to about 10 limbs per number, simple escape time fixed-point computation of a Mandelbrot set location on my AMD RX 580 GPU was only about 12-13 times slower (total elapsed wall-clock time) than KF using advanced perturbation and series approximation algorithms on my AMD Ryzen 7 2700X CPU. I want to find a way to compare total power consumption, maybe I need to get a metering hardware plug or some such.

You can find my implementation here:
`git clone https://code.mathr.co.uk/fixed-point.git`

On Thursday 11th July 2019 in the late afternoon, I'll be presenting my
paper *At the Helm of the Burning Ship* at the
EVA London
conference. The proceedings should be free to access, I'll update this
post with links once they are published. I don't know if the talks will
be recorded. I'll put my slides online afterwards too.

At the Helm of the Burning ShipAbstract: The Burning Ship fractal is a non-analytic variation of the Mandelbrot set, formed by taking absolute values in the recurrence. Iterating its Jacobian can identify the period of attracting orbits; Newton’s root-finding method locates their mini-ships. Size estimates tell how deep to zoom to find the mini-ship or its embedded quasi-Julia set. Pre-periodic Misiurewicz points with repelling dynamics are located by Newton’s method. Stretched regions are automatically unskewed by the Jacobian, which is also good for colouring images using distance estimation. Perturbation techniques cheapen deep zooming. The mathematics can be generalised to other fractal formulas. Some artistic zooming techniques and domain colouring methods are also described.

Keywords: Burning Ship. Dynamical systems. Fractal art. Numerical algorithms. Perturbation theory.

If you're in town with cash to splash on registration fees, I think you have to book before Sunday. I'll also be attending on the Tuesday, there looks like a good session on robot drawing.

**EDIT** I made a micro-site for the paper and slides.

Wikipedia on Autostereograms doesn't exactly say how to construct them, so I drew some diagrams and scribbled some equations, and came up with this.

Given background distance and eye separation in inches, resolution in dots per inch, width in pixels, and count the number of vertical strips in the image background, compute the accomodation distance as follows:

accomodation = background * (1 - (width / count) / (separation * resolution))

This will be less than the background distance for positive eye separation (wall-eyed viewing) and greater for negative eye separation (cross-eyed viewing).

Then compute a depth value for each pixel, with the far plane at background inches from the camera. Ray marching a distance field is one way to do this, see Syntopia's blog for details. The scene should be between the camera and the far plane. Sharp depth discontinuities are disturbing, so position it as close to the far plane as possible.

The next step is converting the depth to a horizontal offset at the accomodation plane, using similar triangles:

delta = (depth - accomodation) * separation / depth;

Then compute the normalized texture coordinate increment that matches that offset:

increment[i] = 1 / (delta * resolution)

The i here is the horizontal index of the pixel, you need the whole scanline at a time
if you want to center the texture instead of aligning it to an image edge. Now we have the
speed of texture coordinate change, we can **integrate** this
to get the actual texture coordinate for each pixel:

double sum = 0; for (int i = 0; i < width; ++i) { sum += increment[i]; coordinate[i] = sum; }

and then do the texture lookup, rebasing it to the center of the image (twice % because negatives behave weird in C):

int u = floor((coordinate[i] - coordinate[width / 2]) * texture_width); u %= texture_width; u += texture_width; u %= texture_width; int v = j; v %= texture_height; v += texture_height; v %= texture_height; pixel[j][i] = texture[v][u];

Image above uses eye separation = -3 (cross-eyed), background distance = 12, 1920x1080 at 100dpi, count 32, the scene is a power 8 Mandelbulb copy-pasted from Fragmentarium, the texture is a slice of a NASA starfield image made seamless in GIMP.

]]>In the #supercollider channel on talk.lurk.org, there was recently
discussion about a "DJ filter" that transitions smoothly between low-pass
and high-pass filters. This made me curious to see if I could make one.
I found Miller Puckette's book section on
Butterworth filters,
but figure 8.18 is not quite there yet for my purposes: the normalization
is off for "shelf 2" (it would be better if the Nyquist gain was 1, instead
of having the DC gain as 1). The figure has 3 poles and 3 zeroes, but for
simplicity of implementing with 2 cascaded biquad sections I went with a
**4-pole** filter design.

After fixing the order, the next variable is the **center frequency**
\(\beta = 2 \pi f_c / SR\), which determines \(r = \tan(\beta/2)\).
Using the formula from the above link gives the pole locations:

\[ \frac{(1 - r^2) \pm \mathbf{i} (2 r \sin(\alpha)) }{ 1 + r^2 + 2 r \cos(\alpha))} \]

For a 4-pole filter, \(\alpha \in \{ \frac{\pi}{8}, \frac{3 \pi}{8} \} \).

The **hi/lo** control \(o\) is conveniently expressed in octaves relative
to the center frequency. It controls the stop band gain, which levels
off after \(o\)-many octaves (so these are really shelving filters).
The \(o\) control fixes the location of the zeroes of the filter, the
formula is the same as above but with \(r\) modified using
\(r_Z = \frac{r_P}{2^o}\).

The filter is normalized so that the pass-band gain (at DC for low-pass and Nyquist for high-pass) is unity. Then the gain in the stop band is \(-24 o\) dB, the transition slope is fixed by the order, and the center frequency gain is about \(-3\)dB when \(o\) is away from \(0\). This can be done by computing the gain of the unnormalized filter at \(\pm 1\) (sign chosen as appropriate). Computing the gain of a filter specified by poles and zeroes is simple: multiply by the distance to each zero and divide by the distance to each pole (phase response is left as an exercise).

The poles and zeroes come in conjugate pairs, which are easy to transform to biquad coefficients (see my previous post about biquad conversions). I put the gain normalization in the first biquad of the chain, not sure if this is optimal. The filters should be stable as long as the center frequency is not DC or Nyquist, as the poles are inside the unit circle. But modulating the filter may cause blowups - to be investigated.

You can browse my implementation.

]]>Previously on mathr: Newton's method for Misiurewicz points (2015). This week I applied the same "divide by undesired roots" technique to the periodic nucleus Newton iterations. I implemented it GLSL in Fragmentarium, which has a Complex.frag with dual numbers for automatic differentiation (this part of the frag is mostly my work, but I largely copy/pasted from C99 standard library manual pages for the transcendental functions, Wikipedia for basic properties of differentiation like product rule, quotient rule, chain rule...).

Here's the improved Newton's method, with the newly added lines in bold:

vec2 nucleus(vec2 c0, int period, int steps) { vec2 c = c0; for (int j = 0; j < steps; ++j) { vec4 G = cConst(1.0); vec4 z = cConst(0.0); for (int l = 1; l <= period; ++l) { z = cSqr(z) + cVar(c);if (l < period && period % l == 0) G = cMul(z, G);} G = cDiv(z, G); c -= cDiv(G.xy, G.zw); } return c; }

And some results, top half of the image is without the added lines, bottom half of the image is with the added line, from left to right the target periods are 2, 3, 4, 9, 12:

You can download the FragM source code for the images in this article: 2018-11-17_newtons_method_for_periodic_points.frag.

]]>Start with the iteration formula for the Burning Ship escape time fractal:

\[ X := X^2 - Y^2 + A \quad\quad Y := \left|2 X Y\right| + B \]

Perturb this iteration, replacing \(X\) by \(X+x\) (etc) where \(x\) is a small deviation:

\[ x := (2 X + x) x - (2 Y + y) y + a \quad\quad y := 2 \operatorname{diffabs}(X Y, X y + x Y + x y) + b \]

Here \(\operatorname{diffabs}(c,d)\) is laser blaster's formula for evaluating \(|c + d| - |c|\) without catastrophic cancellation; see my post about perturbation algebra for more details.

Now replace \(x\) and \(y\) by bivariate power series in \(a\) and \(b\):

\[ x = \sum_{i,j} s_{i,j} a^i b^j \quad\quad y = \sum_{i,j} t_{i,j} a^i b^j \]

To implement this practically (without lazy evaluation) I pick an order \(o\) and limit the sum to \(i + j \le o\). Substituting these series into the perturbation iterations, and collecting terms, gives iteration formulae for the series coefficients \(s_{i,j}\) and \(t_{i,j}\):

\[ s_{i,j} := 2 X s_{i,j} - 2 Y t_{i,j} + \sum_{k=0,l=0}^{k=i,l=j} \left( s_{k,l} s_{i-k,j-l} - t_{k,l} t_{i-k,j-l} \right) + \mathbb{1}_{i=1,j=0} \]

The formula for \(t\) requires knowing which branch of the \(\operatorname{diffabs}(X Y, 0)\) was taken, which turns out has a nice reduction to \(\operatorname{sgn}(XY)\):

\[ t_{i,j} := 2 \operatorname{sgn}(X Y) \left( X t_{i,j} + s_{i,j} Y + \sum_{k=0,l=0}^{k=i,l=j} \left( s_{k,l} t_{i-k,j-l} \right) \right) + \mathbb{1}_{i=0,j=1} \]

\(\mathbb{1}_F\) is the indicator function, \(1\) when \(F\) is true, \(0\) otherwise. For distance estimation, the series of the derivatives are just the derivatives of the series, which can be computed very easily.

The series is only valid in a region that doesn't intersect an axis, at which point the next iteration will fold the region in a way that a series can't represent. Moreover, the series loses accuracy the further from the reference point, so there needs to be a way to check that the series is ok to use. One approach is to iterate points on the boundary of the region using perturbation, and compare the relative error against the same points calculated with the series. Only if all these probe points are accurate, is it safe to try the next iteration.

This usually means that the series will skip at most \(P\) iterations near a central miniship reference of period \(P\). But one can do better, by subdividing the region into parts that are folded in different ways. The parts that are folded the same way as the reference can continue with series approximation, with probe points at the boundary of each part, with the remainder switching to regular perturbation initialized by the series.

It may even be possible to use knighty's techniques like Taylor shift, which is a way to rebase a series to a new reference point (for example, one in the "other side" of the fold) to split the region into two or more separate parts each with their own series approximation. The Horner shift algorithm is not too complicated, and I think it can be extended to bivariate series by shifting along each variable in succession:

// Horner shift code from FLINT2 for (i = n - 2; i >= 0; i--) for (j = i; j < n - 1; j++) poly[j] += poly[j + 1] * c

Untested Haskell idea for bivariate shift:

shift :: Num v => v -> Map Int v -> Map Int v shift = undefined -- left as an exercise shift2 :: Num v => v -> v -> Map (Int, Int) v -> Map (Int, Int) v shift2 x y = twiddle . pull . fmap (shift x) . push . twiddle . pull . fmap (shift y) . push push :: (Ord a, Ord b) => Map (a, b) v -> Map a (Map b v) push m = M.fromListWith M.union [ (i, M.singleton j e) | ((i,j), e) <- M.assocs m ] pull :: (Ord a, Ord b) => Map a (Map b v) -> Map (a, b) v pull m = M.fromList [((i,j),e) | (i, mj) <- M.assocs m, (j, e) <- M.assocs mj ] twiddle :: (Ord a, Ord b) => Map (a, b) v -> Map (b, a) v twiddle = M.mapKeys (\(i,j) -> (j,i))

Exciting developments! I hope to release a new version of Kalles Fraktaler 2 + containing at least some of these algorithms soon...

]]>The Burning Ship fractal is interesting because it has mini-ships among filaments. However, some of these filaments and mini-ships are highly stretched or skewed non-conformally, which makes them look ugly. Some fractal software (Kalles Fraktaler, Ultrafractal, ...) has the ability to apply a non-uniform scaling to the view, which unstretches the filaments and makes (for example) the period-doubling features on the way to the mini-ship appear more circular. However, to my knowledge, all these software require manual adjustment of the unskewing transformation, for example dragging control points in a graphical user interface.

The size estimate for the Burning Ship mini-sets is based on the size estimate for the Mandelbrot set. The Burning Ship iterations are not a well behaved complex function, so Jacobian matrices over real variables are necessary. Generic pseudo-code for the size estimate of an arbitrary function:

d := degree of smallest non-linear power in f a, b := coordinates of a mini-set with period p x, y := critical point of f (usually 0, 0) L := I B := I for j := 1 to p - 1 x, y := f(x, y) L := Jf(x, y) B := B + L^{-1} size := 1 / sqrt(abs(det(L^(d/(d-1)) B)))

In particular for the power 2 Burning Ship this resolves to:

d := 2 x := 0 y := 0 L(lxx, lxy; lyx, lyy) := (1, 0; 0, 1) B(bxx, bxy; byx, byy) := (1, 0; 0, 1) for j := 1 to p - 1 x, y := (x^2 - y^2 + a, abs(2 x y) + b) L := ( 2 x lxx - 2 y lyx , 2 x lxy - 2 y lyy , 2 sgn(x) sgn(y) (x lyx + lxx y) , 2 sgn(x) sgn(y) (x lyy + lxy y) ) detL := lxx * lyy - lxy * lyx B := ( bxx + lyy / detL , bxy - lyx / detL , byx - lxy / detL , byy + lxx / detL ) detL := lxx * lyy - lxy * lyx detB := bxx * byy - bxy * byx size := 1 / sqrt(abs(detL^2 * detB))

Note the renormalization of the \(c=a+bi\) plane by:

\[L^\frac{d}{d - 1} B\]

The size estimate takes out the uniform scaling factor from this 2x2 matrix, but it's also possible to use it to extract rotation and non-uniform scaling (stretching, skew) parameters. A problem comes when trying to raise the matrix \(L\) to a non-integer power when \(d > 2\), so I fudged it and hoped that the non-conformal contribution from \(L\) is non-existent, and just used \(B\). To cancel out the uniform scaling from \(B\), I divide by the square root of its determinant, and I take its inverse to use as a coordinate transformation for the \(c=a+bi\) plane:

\[T = \left(\frac{B}{\sqrt{\det B}}\right)^{-1}\]

I tested it briefly and it seems to work for Burning Ship power 2 and 3, and also Mandelbar/Tricorn power 2. There is a problem however: to get this automatic skew matrix, one needs to zoom deep enough so that Newton's method can find a mini-set, but doing that is tricky in hard-skewed areas. So I still need to write GUI code for manual skew, to make that pre-navigation easier.

Here's just one image pair, power 2 Burning Ship, the first is with an identity transformation, the second with automatically calculated unskewing transformation:

One last thing: it should be possible to separate skew from rotation by applying polar decomposition.

]]>Define the iterated quadratic polynomial:

\[ f_c^0(z) = 0 \\ f_c^{n+1}(z) = f_c(f_c^n(z))^2 + c \]

The Mandelbrot set is those \(c\) for which \(f_c^n(0)\) remains bounded for all \(n\). Misiurewicz points are dense in the boundary of the Mandelbrot set. They are strictly preperiodic, which means they satisfy this polynomial equation:

\[ f_c^{q+p}(0) = f_c^{q}(0) \\ p > 0 \\ q > 0\]

and moreover the period \(p\) and the preperiod \(q\) of a Misiurewicz point \( c \in M_{q,p} \) are the lowest values that make the equation true. For example, \(-2 \in M_{2,1}\) and \(i \in M_{2,2}\), which can be verified by iterating the polynomial (exercise: do that).

Misiurewicz points are algebraic integers (a subset of the algebraic numbers), which means they are the roots of a monic polynomial with integer coefficients. A monic polynomial is one with leading coefficient \(1\), for example \(c^2+c\). Factoring a monic polynomial gives monic polynomials as factors. Factoring over the complex numbers \(\mathbb{C}\) gives the \(M_{q,p}\) in linear factors, factoring over the integers \(\mathbb{Z}\) can give irreducible polynomials of degree greater than \(1\). For example, here's the equation for \(M_{2,2}\):

\[c^3\,\left(c+1\right)^2\,\left(c+2\right)\,\left(c^2+1\right)\]

Note that the repeated root \(0\) corresponds to a hyperbolic component of period \(1\) (the nucleus of the top level cardioid of the Mandelbrot set), and the repeated root \(-1\) corresponds to the period \(2\) circle to the left. And \(-2 \in M_{2,1}\), so the "real" equation we are interested in is the last term, \(c^2+1\), which is irreducible over the integers, but has complex roots \(\pm i\). There are two roots, so \(\left|M_{2,2}\right| = 2\).

So, a first **attempt** at enumerating Misiurewicz points works like this:

-- using numeric-prelude and MemoTrie from Hackage type P = MathObj.Polynomial.T Integer -- h with all factors g removed divideAll :: P -> P -> P divideAll h g | isZero h = h | isOne g = h | isZero g = error "/0" | otherwise = case h `divMod` g of (di, mo) | isZero mo -> di `divideAll` g | otherwise -> h -- h with all factors in the list removed divideAlls :: P -> [P] -> P divideAlls h [] = h divideAlls h (g:gs) = divideAlls (h `divideAll` g) gs -- the variable for the polynomials c :: P c = fromCoeffs [ 0, 1 ] -- the base quadratic polynomial f :: P -> P f z = z^2 + c -- the iterated quadratic polynomial fn :: Int -> P fn = memo fn_ where fn_ 0 = 0 ; fn_ n = f (fn (n - 1)) -- the raw M_{q,p} polynomial m_raw :: Int -> Int -> P m_raw = memo2 m_raw_ where m_raw_ q p = fn (q + p) - fn q -- the M_{q,p} polynomial with lower (pre)periods removed m :: Int -> Int -> P m = memo2 m_ where m_ q p = m_raw q p `divideAlls` [ mqp | q' <- [ 0 .. q ] , p' <- [ 1 .. p ] , q' + p' < q + p , p `mod` p' == 0 , let mqp = m q' p' , not (isZero mqp) ] -- |M_{q,p}| d :: Int -> Int -> Int d q p = case degree (m q p) of Just k -> k ; Nothing -> -1

This is using numeric-prelude and MemoTrie from Hackage, but with a reimplemented divMod for monic polynomials that doesn't try to divide by an Integer (which will always be \(1\) for monic polynomials). The core polynomial divMod from numeric-prelude needs a Field for division, and the integers don't form a field.

Tabulating this **attempt** at \(\left|M_{q,p}\right|\) (`d q p`

)
for various small \(q,p\) gives:

q | p | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|

1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | |

0 | 1 | 1 | 3 | 6 | 15 | 27 | 63 | 120 | 252 | 495 | 1023 | 2010 | 4095 | 8127 | 16365 | 32640 |

1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |

2 | 1 | 2 | 6 | 12 | 30 | 54 | 126 | 240 | 504 | 990 | 2046 | 4020 | 8190 | 16254 | ||

3 | 3 | 3 | 12 | 24 | 60 | 108 | 252 | 480 | 1008 | 1980 | 4092 | 8040 | 16380 | |||

4 | 7 | 8 | 21 | 48 | 120 | 216 | 504 | 960 | 2016 | 3960 | 8184 | 16080 | ||||

5 | 15 | 15 | 48 | 90 | 240 | 432 | 1008 | 1920 | 4032 | 7920 | 16368 | |||||

6 | 31 | 32 | 96 | 192 | 465 | 864 | 2016 | 3840 | 8064 | 15840 | ||||||

7 | 63 | 63 | 189 | 384 | 960 | 1701 | 4032 | 7680 | 16128 | |||||||

8 | 127 | 128 | 384 | 768 | 1920 | 3456 | 8001 | 15360 | ||||||||

9 | 255 | 255 | 768 | 1530 | 3840 | 6912 | 16128 | |||||||||

10 | 511 | 512 | 1533 | 3072 | 7680 | 13824 | ||||||||||

11 | 1023 | 1023 | 3072 | 6144 | 15345 | |||||||||||

12 | 2047 | 2048 | 6144 | 12288 | ||||||||||||

13 | 4095 | 4095 | 12285 | |||||||||||||

14 | 8191 | 8192 | ||||||||||||||

15 | 16383 |

\(|M_{0,p}|\) is known to be A000740. \(|M_{2,p}|\) appears to be A038199. \(|M_{q,1}|\) appears to be A000225. \(|M_{q,2}|\) appears to be A166920.

**HOWEVER there is a fatal flaw**. The polynomials might
not be irreducible, which means that `divideAlls`

might not be
removing all of the lower (pre)period roots! A proper solution would be
to port the code to a computer algebra system that can factor polynomials
into irreducible polynomials. Or alternatively, mathematically prove that
the polynomials in question will always be irreducible (as far as I know
this is an open question, verified only for \(M_{0,p}\) up to \(p = 10\),
according to
Corollary 5.6 (Centers of Components as Algebraic Numbers)).

You can download my full Haskell code.

**UPDATE** I wrote some Sage code (Python-based) with an
improved algorithm (I think it's perfect now). The values all matched the
original table, and I extended it with further values and links to OEIS.
All the polynomials in question are irreducible, up to the \(p + q < 16\)
limit. No multiplicities greater than one were reported. Code:

@parallel(16) def core(q, p, allroots): mpq = 0 roots = set() R.<x> = ZZ[] w = 0*x for i in range(q): w = w^2 + x wq = w for i in range(p): w = w^2 + x wqp = w f = wqp - wq r = f.factor() for i in r: m = i[0] k = i[1] if not (m in allroots) and not (m in roots): roots.add(m) mpq += m.degree() if k > 1: print(("multiplicity > 1", k, "q", q, "p", p, "degree", m.degree())) return (q, p, mpq, roots) allroots = set() for n in range(16): print(n) res = sorted(list(core([(q, n - q, allroots) for q in range(n)]))) for r in res: t = r[1] q = t[0] p = t[1] mpq = t[2] roots = t[3] print((q, p, mpq, len(roots), [root.degree() for root in roots])) allroots |= roots

**UPDATE2** I bumped the table to \(q + p < 17\). I
ran into some OOM-kills, so I had to run it with less parallelism to get
it to finish.

**UPDATE3** I found a simple function that fits all the
data in the table, but I don't know if it is correct or will break for
larger values. Code (the function is called `f`

):

import Math.NumberTheory.ArithmeticFunctions (divisors, moebius, runMoebius) -- arithmoi import Data.Set (toList) -- containers mu :: Integer -> Integer mu = runMoebius . moebius mqps :: [[Integer]] mqps = [[1,1,3,6,15,27,63,120,252,495,1023,2010,4095,8127,16365,32640] ,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ,[1,2,6,12,30,54,126,240,504,990,2046,4020,8190,16254] ,[3,3,12,24,60,108,252,480,1008,1980,4092,8040,16380] ,[7,8,21,48,120,216,504,960,2016,3960,8184,16080] ,[15,15,48,90,240,432,1008,1920,4032,7920,16368] ,[31,32,96,192,465,864,2016,3840,8064,15840] ,[63,63,189,384,960,1701,4032,7680,16128] ,[127,128,384,768,1920,3456,8001,15360] ,[255,255,768,1530,3840,6912,16128] ,[511,512,1533,3072,7680,13824] ,[1023,1023,3072,6144,15345] ,[2047,2048,6144,12288] ,[4095,4095,12285] ,[8191,8192] ,[16383] ] m :: Integer -> Integer -> Integer m q p = mqps !! fromInteger q !! fromInteger (p - 1) f :: Integer -> Integer -> Integer f 0 p = sum [ mu (p `div` d) * 2 ^ (d - 1) | d <- toList (divisors p) ] f 1 _ = 0 f q 1 = 2 ^ (q - 1) - 1 f q p = (2 ^ (q - 1) - if q `mod` p == 1 then 1 else 0) * f 0 p check :: Bool check = and [ f q p == m q p | n <- [1 .. 16], p <- [1 .. n], let q = n - p ] main :: IO () main = print check

**UPDATE4** Progress! I found a paper with the answer:

Misiurewicz Points for Polynomial Maps and Transversality

Benjamin Hutz, Adam Towsley

Corollary 3.3.The number of \((m,n)\) Misiurewicz points for \(f_{d,c}\) is \[ M_{m,n} = \begin{cases} \sum_{k \mid n} \mu\left(n \over k \right) d^{k-1} & m = 0 \\ (d^m - d^{m-1} - d + 1) \sum_{k \mid n} \mu\left(n \over k \right) d^{k-1} & m \ne 0 \text{ and } n \mid (m - 1) \\ (d^m - d^{m-1}) \sum_{k \mid n} \mu\left(n \over k \right) d^{k-1} & \text{otherwise} \end{cases} \]

They have \(f_{d,c}(z) = z^d + c\), so this result is more general than the case \(d = 2\) I was researching in this post. The formula I came up with is the same, with minor notational differences.

]]>Recently I've been experimenting with different ways to try to improve image quality of fractal renders. The first key step was using jitter, which is adding independent uniform random offsets to the pixel coordinates before calculating the fractal iterations. This converts strong aliased frequencies (like Moiré patterns) into much perceptually acceptable noise with a broad spectrum. The noise can be reduced by averaging many images with different pseudo-random number generator seed values. I used a Wang/Burtle uint32 hash of the pixel coordinates and a subframe index, the quality of which isn't well known, but visually indistinguishable from the cryptographic hash MD5 in my tests.

Unfortunately it's not perfect, artifacts can return to visibility after averaging (which means they were never completely eliminated in the first place, possibly the colouring algorithm computing image-space derivatives of the fractal iteration data might have defeated the jitter in some way). It seems correctly bandlimiting and sampling fractal images is a hard problem, and simpler techniques like supersampling by a large factor with properly filtered downscaling can be better than jitter alone.

The 1D problem seems more tractable. Here you can see spectrum of the audio version, being a sine sweep from 8kHz going up 4 octaves - with uniform sampling it folds over into a descending sweep:

With jittered sampling the aliased energy becomes white noise:

Unfortunately the noise reduction from averaging isn't great: doubling the number of jittered copies (each section in the image below) reduces the noise level by only 3dB. Uniformly oversampling by only 6x would eliminate aliasing completely for this toy example, but some signals might not have a known upper bandwidth limit.

Performance is terrible too, many minutes of CPU time for the few seconds of output. Anyway, here's source code for the audio experiment, you can listen to the output:

// gcc non-uniform_sampling.c -lfftw3 -lm -O3 // time ./a.out > out.raw // audacity # import out.raw f64 mono 48000Hz #include <complex.h> #include <math.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fftw3.h> #define PI 3.141592653589793 // sweep parameters #define HZ 8000 #define OCTAVES 4 #define SPEED 1 #define SAMPLERATE 48000 #define N (OCTAVES * SAMPLERATE / SPEED) // fft parameters (size and overlap) #define F 256 #define O 4 // jitter amount #define SCALE 1 // buffers double t[N]; // sample time double f[N]; // sample value double g[N]; // accumulated data double h[N]; // normalized data for output // fft buffers fftw_complex *fft; fftw_complex *sig; // raised cosine window for overlap-add resynthesis double window(double t) { return 0.5 - 0.5 * cos(2 * PI * t); } // entrypoint int main() { // initialize fft = fftw_malloc(sizeof(fftw_complex) * F); sig = fftw_malloc(sizeof(fftw_complex) * F); fftw_plan plan = fftw_plan_dft_1d(F, fft, sig, FFTW_BACKWARD, FFTW_MEASURE); memset(g, 0, sizeof(g)); // average many passes with distinct random seeds int pass = 0; for (int PASSES = 0; PASSES < 10; ++PASSES) { for (; pass < 1 << PASSES; ++pass) { fprintf(stderr, "%d\n", pass); // sample non-bandlimited signal at non-uniform intervals for (int i = 0; i < N; ++i) { t[i] = i + (rand() / (double) RAND_MAX - 0.5) * SCALE; f[i] = sin(2 * PI * exp(log(HZ << OCTAVES) * t[i] / N + (1 - t[i] / N) * log(HZ))); } // overlap-add resynthesis for (int start = -F; start < N; start += F / O) { // window and DFT memset(fft, 0, sizeof(fftw_complex) * F); for (int i = start; i < start + F; ++i) { if (0 <= i && i < N) { double t0 = (t[i] - start) / F; double f0 = window(t0) * f[i]; for (int k = 0; k < F; ++k) { double w0 = 2 * PI * (k > F/2 ? k - F : k); double w = - t0 * w0; fft[k] += f0 * (cos(w) + I * sin(w)); } } } // do IFFT fftw_execute(plan); // window and accumulate for (int i = start; i < start + F; ++i) { if (0 <= i && i < N) { double t1 = (i - start) / (double) F; double w = window(t1); double s = sig[i - start]; g[i] += w * creal(s); } } } } // normalize double grms2 = 0; for (int i = 0; i < N; ++i) grms2 += g[i] * g[i]; grms2 /= N; grms2 = sqrt(grms2); grms2 *= 4; for (int i = 0; i < N; ++i) h[i] = g[i] / grms2; // output fwrite(h, sizeof(h), 1, stdout); fflush(stdout); } // cleanup fftw_destroy_plan(plan); fftw_free(sig); fftw_free(fft); return 0; }

Reporting this "failed" experiment in the interest of science!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

**EDIT 2019-12-23** added \(P(\exp \circ f)\) via \(\exp = \sinh + \cosh\)