Visualising the Mandelbrot Set

Definition

The Mandelbrot set is defined as the set of all values $c$ in the complex plane where the recursion relation $$ z_{n+1} = z_n^2 + c $$ is bounded as $n \to \infty $, and $ z_0 = 0 $. That is, a point $c$ is in the Mandelbrot set if $ |z_n | \leq 2 $ for all $n >0$.

Code

I started by writing a function that checks if a complex point c = $ ( x_c, iy_c ) $ is in the Mandelbrot set. Since we are dealing with complex points $ z_n = ( x_z, iy_z ) $, we have $$ | z_n | \leq 2 \iff \sqrt{x_z^2 + y_z^2} \leq 2 \iff x_z^2 + y_z^2 \leq 4 $$

as the condition to remain in the Mandelbrot set. In the code I check if the condition is broken. Similarly, when we expand, we get $$ z_n^2 = ( x_z^2 - y_z^2, i 2 x_z y_z) $$

Then, when we check the next iterative step $z_{n+1}$ and update our values of $x_z$ and $y_z$, we say $$ x_{z, n+1} = x_z^2 - y_z^2 + x_c \qquad y_{z, n+1} = 2 x_z y_z + y_c $$

Using these relations, the function looks like this:

Mandelbrot Code

@njit(parallel = True)
def mandelbrot(cx, cy, max_iters): 
    '''
    Checks if a complex number (represented by pixel (x, iy)) is in the mandelbrot set.
    Calculates zn+1 = zn^2 + c
    Returns the number of iterations before breaking. If max_iters is returned, c is in the set
    --------
    cx: float - Re(c)
    cy: float - Im(c)
    max_iters: int - Max number of iterations to be in the set
    '''
    
    ### Starting at the point (0,0i),
    zx = 0.0
    zy = 0.0
    
    for i in prange(max_iters): 
        # Mandelbrot Condition to break out 
        if zx**2 + zy**2 > 4: 
            return i
            
        # Update
        zx, zy = zx**2 - zy**2 + cx, 2*zx*zy + cy
    
    return max_iters


It's pretty straightforward, we keep evaluating the Mandelbrot condition for each iteration until we get to max_iters. If the condition is broken, then we know that the recursion relation at that point is unbounded, and we return the number of iterations it took for the condition to break. Otherwise, if we go all the way to the end, we return max_iters and that point is in the Mandelbrot set.

Then, we need to use the mandelbrot function on every point in our complex plane. This is defined in a function called eval_mandelbrot and looks like this:

Code to Evaluate the Mandelbrot Function

def eval_mandelbrot(
    height, width, x_start, y_start, x_end, y_end, max_iters:int): 
    '''
    Evaluates mandelbrot function for each point in the complex plane
    Returns a NumPy array with the number of iterations as elements
    --------
    height: int - height of the array / image
    width: int - width of the array / image
    x_start: float - Start point on the x (real) axis 
    x_end: float - End point on the x (real) axis 
    y_start: float - Start point on the y (imaginary) axis 
    y_end: float - End point on the y (imaginary) axis 
    max_iters: int - Max number of iterations to for criterion into the set
    '''

    # Define our axes
    x = np.linspace(x_start, x_end, width)
    y = np.linspace(y_start, y_end, height)

    result = np.zeros((width, height, 3))
    for i, cx in enumerate(x): # Rows
        for j, cy in enumerate(y): 
            res = mandelbrot(cx,cy, max_iters)
            colour = colouring(res, max_iters)
            result[i,j] = colour
    
    # results array needs to be uint8 when using PIL to generate image 
    return np.uint8(result)


def colouring(n, max_iters): 
    '''
    Returns a list of length 3 defining a colouring in HSV format
    '''
    hue = int(255 * n / max_iters)
    saturation = 255

    if n < max_iters:
        colour = [hue, saturation, 255]
    else: 
        colour = [hue, saturation, 0]
    
    return colour


This is also pretty straightforward. I define two axes, iterate over them to check each point, and keep the results in a 3D NumPy results array. The reason why I used a 3D array instead of a 2D array has to do with the colouring of the final image, since we want points with a similar number of iterations to have similar colours. The results array holds the HSV colour scheme of each pixel / point in the picture. Also, I was too lazy to figure out the colouring so I found some a code snippet online and wrapped it in a function which I called ```colouring```.

Then, using the eval_mandelbrot function, I made a PIL image object and generated a static image over over the region (x_start, y_start) = (-2.75, -1.00), (x_end, y_end) = (1.00, 1.00) with height = 1000, width = 2048 and max_iters = 100.

I also checked how the Mandelbrot set forms as the number of iterations increases.

Lastly, I thought it would be cool to zoom in a little and see how complex the Mandelbrot set can be.