Fractal Pillars and Zebras

Dylan R. Cope · December 2, 2019

In this post, Reddit user u/FlyingCow313 shows an image that resulted from thier attempt to implement a Mandlebrot renderer.

In this notebook we are going to recreate the pattern and investigate how it is generated.

First, lets create a working Mandlebrot renderer, then we will try to mess with it to recreate the effect.

Note: I have adapted basic generation and rendering code from this and this.

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
def create_mandlebrot(
    n = 700, # screen size pixels

    s = 1,  # Scale.
    p = 0.6 + 0j, # screen offset
    
    horizon = 2.0 ** 20,
    iterations=500
):
    
    s = int(s * n * 4 / 7) # maps scale factor to pixel space
    x = np.linspace(-n / s, n / s, num=n).reshape((1, n))
    y = np.linspace(-n / s, n / s, num=n).reshape((n, 1))
    
    Z = np.tile(x, (n, 1)) + 1j * np.tile(y, (1, n)) - p
    C = Z.copy()
    
    M = np.full((n, n), True, dtype=bool)
    N = np.zeros((n, n))
    
    log_horizon = np.log(np.log(horizon))/np.log(2)
    
    for i in range(iterations):
        Z[M] = Z[M] * Z[M] + C[M]
        M[np.abs(Z) > horizon] = False
        N[M] = i - np.log(np.log(np.abs(Z[M]) + 1))/np.log(2) + log_horizon
        
    return N
N = create_mandlebrot(n=2000, iterations=2000)
fig = plt.figure(figsize=(20, 20))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1)
ax.set_xticks([])
ax.set_yticks([])
plt.imshow(np.flipud(np.log(1 + N)), cmap='magma');

png

Recreating the Effect

The key error is in the following code snippet (from this file):

    while comparative(z, '2500+2500i', '<') > 1 and comparative(z, '-2500-2500i', '>') > 1 and loops < 500:
        z = comp(comp(str(z), str(z), 'times'), str(r/100)+'+'+str(f/100)+'i', 'add')
        loops += 1
    if comparative(z, '1000+1000i', '<') > 1:
        return 'black'
    else:
        return 'white'

The comparative function takes two strings representing complex numbers and a third representing the comparative operator. Internally, for complex arguments a + bi and x + iy, and comparative operator op, it returns true if and only if (a op b) and (b op y). Therefore the condition comparative(z, '2500+2500i', '<') is only true if the imaginary and real components of z are less than 2500.

In normal Mandelbrot rendering our terminal condition is based upon the magnitude of z, whereas in this situation the iteration stops when z enters into one of the two boxes [(2500, 2500), (\infty, \infty)] or [(-2500, -2500), (-\infty, -\infty)].

def create_mandlebrot_pillars(
    n = 700, # screen size pixels

    s = 1,  # Scale.
    p = 0.6 + 0j, # screen offset
    
    iterations=500
):

    s *= n * 4 / 7 # maps scale factor to pixel space
    x = np.linspace(-n / s, n / s, num=n).reshape((1, n))
    y = np.linspace(-n / s, n / s, num=n).reshape((n, 1))
    Z = np.tile(x, (n, 1)) + 1j * np.tile(y, (1, n)) - p

    C = Z.copy()
    M = np.full((n, n), False, dtype=bool)
    N = np.zeros((n, n))
    
    for i in range(iterations):
        Z[M] = Z[M] * Z[M] + C[M]
        M = (
            ((Z.imag < 2500) & (Z.real < 2500)) & 
            ((Z.imag > -2500) & (Z.real > -2500))
        )
    
    M = (Z.imag < 1000) & (Z.real < 1000)
    return ~M
N = create_mandlebrot_pillars(iterations=500, n=2000)
fig = plt.figure(figsize=(20, 20))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1)
ax.set_xticks([]); ax.set_yticks([])
plt.imshow(np.flipud(N), cmap='gray');

png

def create_mandlebrot_zebra(
    n = 700, # screen size pixels

    s = 1,  # Scale.
    p = 0.6 + 0j, # screen offset
    
    iterations=500
):
    
    s *= n * 4 / 7 # maps scale factor to pixel space
    x = np.linspace(-n / s, n / s, num=n).reshape((1, n))
    y = np.linspace(-n / s, n / s, num=n).reshape((n, 1))
    Z = np.tile(x, (n, 1)) + 1j * np.tile(y, (1, n)) - p

    C = Z.copy()
    M = np.full((n, n), False, dtype=bool)
    N = np.zeros((n, n))
    
    for i in range(iterations):
        Z[~M] = Z[~M] * Z[~M] + C[~M]
        M = (
            ((Z.imag > 2500) & (Z.real > 2500)) | 
            ((Z.imag < -2500) & (Z.real < -2500))
        )
    
    M = (Z.imag < 1000) & (Z.real < 1000)
    return M
N = create_mandlebrot_zebra(iterations=500, n=2000)
fig = plt.figure(figsize=(20, 20))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1)
ax.set_xticks([]); ax.set_yticks([])
plt.imshow(np.flipud(N), cmap='gray');

png

def create_mandlebrot_with_angles(
    n = 700, # screen size pixels

    s = 1,  # Scale.
    p = 0.6 + 0j, # screen offset
    
    horizon = 2.0 ** 10,
    iterations=500
):
    
    s = int(s * n * 4 / 7) # maps scale factor to pixel space
    x = np.linspace(-n / s, n / s, num=n).reshape((1, n))
    y = np.linspace(-n / s, n / s, num=n).reshape((n, 1))
    
    Z = np.tile(x, (n, 1)) + 1j * np.tile(y, (1, n)) - p
    C = Z.copy()
    
    M = np.full((n, n), True, dtype=bool)
    N = np.zeros((n, n))
    
    for i in range(iterations):
        Z[M] = Z[M] * Z[M] + C[M]
        M[np.abs(Z) > horizon] = False
        N[M] = np.angle(Z[M]) + np.pi
        
    return N
N = create_mandlebrot_with_angles(n=2000)
fig = plt.figure(figsize=(20, 20))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1)
ax.set_xticks([]); ax.set_yticks([])
plt.imshow(np.flipud(N), cmap='magma');

png

N = create_mandlebrot_with_angles(n=2000, horizon=2, iterations=50)
fig = plt.figure(figsize=(20, 20))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1)
ax.set_xticks([]); ax.set_yticks([])
plt.imshow(np.flipud(N), cmap='magma');

png

N = create_mandlebrot_with_angles(n=2000, horizon=2, iterations=2000, s=2.5, p=1+0j)
fig = plt.figure(figsize=(20, 20))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1)
ax.set_xticks([]); ax.set_yticks([])
plt.imshow(np.flipud(N), cmap='magma');

png

Twitter, Facebook