1

So i created a simple mandelbrot zoom code that zooms in(lmb) or out(rmb) where you click. The portion that is render is halved every click as it zooms in the curve.

The problem is no matter how large the maxiter and additer count is, the fractal always seems to get blurry at around 2^47 zoom value. Here is what it looks like at 2^49 zoom.

Anyways below is my code. Can someone please tell me why it gets blurry and how i can solve this issue?

import numpy as np
from numba import jit
from matplotlib import pyplot as plt
from matplotlib import colors
from datetime import datetime


width, height, resolution = 8, 8, 100
maxiter = 50    #starting maxiter
additer = 50    #value to add to maxiter upon zoom
xmin, xmax = -2, 1
ymin, ymax = -1.5, 1.5
zoom = 1
color = 'terrain'   #https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html


#--------------------------------------------------------------------------------------------------#

@jit
def mandel(c, maxiter):
    z = c
    for n in range(maxiter):
        if (z.real*z.real + z.imag*z.imag)>4:   #if z diverges
            return n
        z = (z * z) + c
    return 0



@jit
def mandelbrot(xmin, xmax, ymin, ymax, maxiter):
    re = np.linspace(xmin, xmax, width*resolution)
    im = np.linspace(ymin, ymax, height*resolution)
    answer = np.empty((width*resolution, height*resolution))
    for y in range(height*resolution):
        for x in range(width*resolution):
            answer[x,y] = mandel(re[x] + 1j*im[y], maxiter) #iteration count at (re, im)
    return answer



def onclick(event):
    global zoom, xmax, xmin, ymax, ymin, maxiter
    if event.button == 1:
        zoom *= 2
        maxiter += additer
    elif event.button == 3:
        zoom/=2
        if maxiter > additer:
            maxiter -= additer
    #get the re,im coordinate position of mouse
    posx = xmin + (xmax-xmin)*event.xdata/(width*resolution)
    posy = ymin + (ymax-ymin)*event.ydata/(height*resolution)
    print(posx, posy, "iter:", maxiter, "zoom:", zoom)
    #change max and min so the coordinate is at the center
    xmin = posx - (1.5/zoom)
    xmax = posx + (1.5/zoom)
    ymin = posy - (1.5/zoom)
    ymax = posy + (1.5/zoom)
    #recalculate and display
    answer = mandelbrot(xmin, xmax, ymin, ymax, maxiter)
    plt.imshow(answer.T, cmap=color)
    plt.draw()

#for benchmarking
'''a = datetime.now()
mandelbrot(xmin, xmax, ymin, ymax, zoom)
print(datetime.now() - a)'''

answer = mandelbrot(xmin, xmax, ymin, ymax, maxiter)
plt.connect('button_press_event', onclick)
plt.imshow(answer.T, cmap=color)
plt.axis('off')
plt.tight_layout()
plt.show()
Orbital
  • 565
  • 3
  • 13
  • 2
    It will until you use a floating point system with more precision. A `double` precision variable uses 53 bits of significand, and performs poorly when you add a very small value to a much larger one. – Weather Vane Feb 04 '20 at 09:35
  • i looked into the numpy documentation but normal complex numbers, clongdouble, and complex128 all seem to store the same number of digits. Is there any other library that can store at a higher precision? – Orbital Feb 04 '20 at 09:54
  • 1
    https://stackoverflow.com/a/11523128/14637 – Thomas Feb 04 '20 at 10:01
  • thanks i'll look into it – Orbital Feb 04 '20 at 10:19
  • so i tried it out and one thing i noticed is how it is slightly inaccurate. Decimal(0.2) outputs 0.200000000000000011102230246251565404236316680908203125. I can fix it with getcontext().prec = 16 but is there any other way to resolve this issue without lowering precision. Also if this cannot be resolved, will the slight error be an issue with very high zoom? – Orbital Feb 05 '20 at 03:56
  • Note that the fraction `1/5` can be **exactly** represented in 10-base as `0.2` but it *cannot* be exactly held in the typical 2-based floating point variable no matter how much precision. In the same way that the fraction `1/7` cannot be exactly represented in either 10-base or in 2-base. – Weather Vane Feb 05 '20 at 14:01
  • One way you *might* be able to improve the arithmetic is to ensure that every pixel coordinate can be exactly represented. For example, if you start with a pixel grid as 1024 * 1024 (powers of 2) spanning the number range -2.0 to + 2.0 then the coordinate of each pixel can be *exact*. Then when you double the scale, you halve the distance between each pixel, which can remain an exact representation until the zoom is quite large. – Weather Vane Feb 05 '20 at 14:13
  • Thanks. The resolution should be a power of 2 since I am zooming in by a factor of 2 right – Orbital Feb 05 '20 at 22:47
  • 1
    Only if you start that way. Consider just 5 pixels (previous comment should be 1025) with values -2, -1, 0, +1, +2 they will need only 1 bit of the signifcand. Double the scale (say at the centre) and they will be -1, -0.5, 0, +0.5, +1 still needing only 1 bit of significand and that will continue to the limit of the exponent. With more pixels that won't be the case forever: the previous case would start with (positive) grid values of 1/512, 2/512, 3/512 etc which can be subdivided to some depth without loss – until you iterate, when it degrades, but still better than unplanned scaling. – Weather Vane Feb 07 '20 at 18:59

1 Answers1

0

Below a 2^-47 image width and for most of the areas where you would like to zoom, the difference between 2 adjacent pixels - considering 1000 pixels width - will yield 0 at the standard double precision.

To go deeper you will need to compute at least one point (reference orbit) with a multi-precision arithmetic package (Decimal, gmpy2). For the remaining pixels, in order to keep the calculation time acceptable you can usually iterate only the delta - with a few caveats that are beyond the scope of this question.

For an example of such implementation using python / numpy you can have a look at package a like fractalshades. The gallery section of the documentation features examples of deep-zooms down to 10^-2000 for Mandelbrot and Burning Ship: https://gbillotey.github.io/Fractalshades/examples/index.html

Disclaimer: I am the author of fractalshades.

GBy
  • 1,719
  • 13
  • 19