3
\$\begingroup\$

I've written up some code that represents the Mandelbrot set in an, admittedly, quite inefficient way. It probably looks awful due to the fact that I did this completely solo, without any external sources aside from the wiki page. What I was wondering is, how can I, without rewriting it completely, fix my code so that it either runs faster, or is higher definition?

Final image:

final image

import turtle
x = -200
y = 99
RN = -2
i = float(1.0)
c = complex(RN+i)
win = turtle.Screen()
win.setup(400,400)
t = turtle.Turtle()
t.speed(0)
t.penup()
t.ht()
pattern = []

#this calculates what the color hex value each pixel will be. if the 10th
#iteration results in a number greater than 2, then that pixel is outside
#of the set.
def set_color(RN,pattern,z,c):
    for v in range(10):
        z = z**2 + c
        pattern.append(z)
    if abs(z) > 3.7918623e+90:
        t.color("#191970")
    elif abs(z) > 1.9472705e+45:
        t.color("#16167F")
    elif abs(z) > 4.4127888e+22:
        t.color("#13138F")
    elif abs(z) > 210066388901:
        t.color("#10109F")
    elif abs(z) > 458330:
        t.color("#0D0DAF")
    elif abs(z) > 677:
        t.color("#0B0BBF")
    elif abs(z) > 26:
        t.color("#0808CF")
    elif abs(z) > 5:
        t.color("#0505DF")
    elif abs(z) > 2:
        t.color("#0202EF")
    elif abs(z) > 1.75:
        t.color("yellow")
    elif abs(z) > 1.5:
        t.color("gold")
    elif abs(z) > 1.25:
        t.color("goldenrod")
    elif abs(z) > 1:
        t.color("darkgoldenrod")
    else:
        t.color("black")

#draws the set
def Mandelbrot(RN,pattern,precision,check_rate,move_rate,x,y,i,c):
#draws line by line, 100 times
    for e in range(100):
        t.pendown()
#draws one line
        for e in range(precision):
            z = 0
            set_color(RN,pattern,z,c)
            pattern = []
            RN = float(RN + move_rate)
            c = complex(RN,i)
            x = x + check_rate
            t.setposition(x,y)
#sets up values you'll need next row
        RN = -2.0
        i = i-.02
        t.penup()
        x = -200
        y = y - 2
        t.setposition(x,y)
    t.penup()

#purely decorative black background for the set
def borders():
    t.color("black")
    t.setposition(-200,200)
    t.pendown()
    t.begin_fill()
    for i in range(4):
        t.forward(400)
        t.right(90)
    t.end_fill()

precision = 400
check_rate = float(400 / precision)
move_rate = float(check_rate/100)

t.penup()
borders()
t.setposition(x,y)
Mandelbrot(RN,pattern,precision,check_rate,move_rate,x,y,i,c)
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Wow! That is really slow. I changed range(100) to range(5), just to get some timing numbers without dying of boredom. So with that as my baseline, let's work on speeding it up.

set_color

def set_color(RN,pattern,z,c):
    for v in range(10):
        z = z**2 + c
        pattern.append(z)
    if abs(z) > 3.7918623e+90:
        t.color("#191970")
    elif abs(z) > 1.9472705e+45:
        t.color("#16167F")
    elif abs(z) > 4.4127888e+22:
        t.color("#13138F")
    elif abs(z) > 210066388901:
        t.color("#10109F")
    elif abs(z) > 458330:
        t.color("#0D0DAF")
    elif abs(z) > 677:
        t.color("#0B0BBF")
    elif abs(z) > 26:
        t.color("#0808CF")
    elif abs(z) > 5:
        t.color("#0505DF")
    elif abs(z) > 2:
        t.color("#0202EF")
    elif abs(z) > 1.75:
        t.color("yellow")
    elif abs(z) > 1.5:
        t.color("gold")
    elif abs(z) > 1.25:
        t.color("goldenrod")
    elif abs(z) > 1:
        t.color("darkgoldenrod")
    else:
        t.color("black")

The first argument, RN, is not used in this function. So we can omit that, and save a little time not passing a useless argument.

        z = 0
        set_color(RN,pattern,z,c)
        pattern = []

The second argument, pattern, is being accumulated, and then discarded. So busy work involving memory allocation for extending a list who's value is never used. We can remove that argument and pattern.append(z).

The third argument, z, is always coming in as zero. No point passing it as an argument; just initialize it inside the set_color function, which will save a little bit of time because we don't pass the extra argument.

Which just leaves passing in the argument c. Getting better.

Then, you have a long string of if abs(z) > # checks. How many times are you computing abs of a complex number? How many times will the result be different? Maybe we can compute it just once?

    abs_z = abs(z)
    if z > 3.7918623e+90:
        t.color("#191970")
    elif abs_z > 1.9472705e+45:
        ...

Unfortunately, that still leaves an ugly long string of if/elif/elif/else statements. With 14 possibilities, you take an average of 7 tests to determine the correct value; with a binary search, this would reduce to 4. We just need to put the values in a list, and use bisect.bisect to determine where the abs(z) would be inserted in the list.

import bisect

MAG = (1, 1.25, 1.5, 1.75, 2, 5,
       26, 677, 458330, 210066388901, 4.4127888e+22, 1.9472705e+45,
       3.7918623e+90)

COLOR = ("black", "darkgoldenrod", "goldenrod", "gold", "yellow", "#0202EF",
         "#0505DF", "#0808CF", "#0B0BBF", "#0D0DAF", "#10109F", "#13138F",
         "#16167F", "#191970")

def set_color(c):
    z = 0
    for _ in range(10):
        z = z**2 + c
    t.color(COLOR[bisect.bisect(MAG, abs(z))])

My timing hasn't shown a significant impact in speed yet, but I certainly like the set_color function much better now. If you want to add more colours, it is simply a matter of filling in additional values in the arrays.

Mandelbrot

def Mandelbrot(RN,pattern,precision,check_rate,move_rate,x,y,i,c):
#draws line by line, 100 times
    for e in range(100):
        t.pendown()
#draws one line
        for e in range(precision):
            set_color(c)                 # Note: modified for new set_color
            RN = float(RN + move_rate)
            c = complex(RN,i)
            x = x + check_rate
            t.setposition(x,y)
#sets up values you'll need next row
        RN = -2.0
        i = i-.02
        t.penup()
        x = -200
        y = y - 2
        t.setposition(x,y)
    t.penup()

This looks backwards. You pass in RN, but RN is computed in the loop. You pass in x, but x is initialize to -200 each time through the loop. You pass in c, but c is computed in the loop. And most things seem to be computed just after they've been used for the next loop iteration!

Let's re-work this, moving calculation to before they are used, and see what it looks like. Along the way, variable = variable + adjustment will be replaced with variable += adjustment, because it results in one less variable lookup in the Python interpreter. Also, RN + move_rate is already a float, so we can omit the redundant float() call. When looping over a range of values (y starting at 99, and going down by two 100 times, a for y in range(...) construct is used:

def mandelbrot(precision, check_rate, move_rate):
    i = 1.0
    t.penup()

    for y in range(99, 99 - 2 * 100, -2):
        rn = -2.0
        x = -200
        t.setposition(x, y)

        t.pendown()
        for _ in range(precision):
            c = complex(rn, i)
            set_color(c)

            rn += move_rate
            x += check_rate
            t.setposition(x, y)

        i -= 0.02
        t.penup()

Several PEP-8 changes: commas are followed by a space, variable names and method names are lowercase (technically, snake_case). _ is used as the unused variable. Operators have a space on each side.

Main

Again, float( ) calls are unnecessary.

if __name__ == '__main__':
    precision = 400
    check_rate = 400 / precision
    move_rate = check_rate / 100

    t.penup()
    borders()
    mandelbrot(precision, check_rate, move_rate)

Speed Up?

Despite my initial efforts, my very rough timing measurements don't show I've sped things up any. But I do think the code has been cleaned up significantly.

Next step in my efforts will be to split this up into two parts:

  1. computing a grid of Mandelbrot colour index values
  2. drawing the image (using the computed grid of colour index values) with turtle graphics

Then, I can get more accurate timing information for each section.

Step 1 is open to parallel processing (Python threads won't help, due to the Global Interpreter Lock (GIL), but using processes should help), but that will only help if it is the large consumer of time. I fear it is the turtle graphics.

\$\endgroup\$
4
  • \$\begingroup\$ Wow! This helps a ton. I'll fix my code to match yours, and see what I can do from there. This is way more intuitive than mine, as my code was done almost completely brute-force. Can't thank you enough! However, in set_color(c), couldn't I just use indices to choose which color value to use? \$\endgroup\$ Commented Mar 1, 2020 at 18:39
  • \$\begingroup\$ Just realized that when i plug in the set_color(c) part, it throws me this error: NotImplementedError: bisect is not yet implemented in Skulpt on line -181 Why does this happen? \$\endgroup\$ Commented Mar 1, 2020 at 19:33
  • \$\begingroup\$ What is Skulpt? Seems like the bisect module isn't present, which is odd 'cause that is a standard library. \$\endgroup\$
    – AJNeufeld
    Commented Mar 1, 2020 at 22:12
  • \$\begingroup\$ Interesting. I am using a coding website called repl.it, though I don't see why that would change anything, as all other modules I've checked work. I think that Skulpt is an in-browser version of Python, though I have no idea why it would needed in any way for a Python module. I wonder if it says the same thing in IDLE and PyCharm. \$\endgroup\$ Commented Mar 2, 2020 at 4:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.