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:
- computing a grid of Mandelbrot colour index values
- 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.