0

So recently I decided to write a program to draw the mandelbrot set using turtle, and it works very well, except for one thing; it's quite slow, and it slows down as it draws. The way it draws is, if I remember correctly, as follows:

def drawpoint(x,y,colour):
    t.color(colour)
    t.setpos(x,y)
    t.down()
    t.forward(1)
    t.up()

and the program in general calculates the point (using math (specifically its trigonometry) and its colour and then draws it.

I can't work out why it's so slow, because it's not exactly an astonishing set of calculations. I'm fairly sure it's to do with turtle, and I was wondering if:

a) Python is slowed down by moving between math and turtle
b) Turtle slows down as you draw more points
c) Something else entirely

Is it any of these, and if so, how can I speed it up?

cdlane
  • 40,441
  • 5
  • 32
  • 81
  • 1
    https://docs.python.org/2/library/profile.html – BlackBear Sep 05 '16 at 09:18
  • 1
    There are multiple ways to make turtle faster: http://stackoverflow.com/a/16120087/380038 – Framester Sep 05 '16 at 09:24
  • 1
    Turtle is designed to draw lines (and arcs), it's not very efficient at drawing pixels. If you want speed take a look at PIL for generating images, and you can display the generated image using Tkinter. FWIW, Turtle uses Tkinter to perform the actual drawing operations. – PM 2Ring Sep 05 '16 at 09:32
  • No, 'switching' between modules is not a thing. Everything is just objects on the heap, and modules let you load objects in groups as well as managed your namespaces, nothing more. – Martijn Pieters Sep 05 '16 at 17:47

1 Answers1

0

Since you didn't give a clock value to quite slow, I have to make assumptions. (And I don't disagree with PM 2Ring that turtle.py may not be your best choice for this purpose.) I wrote a mandelbrot program, using generic turtle operations, which took nearly an hour to draw the 320 x 240 image below. However, with a few turtle optimizations, I was able to get that down to just over a minute. And there may be room to do better (see further below.) Specific optimizations:

  • Maximize the turtle drawing speed via turtle.speed("fastest")

  • Use screen.tracer(0, 0) and screen.update() to do the bulk of the drawing offscreen. In my code below, I update on each vertical slice of the image so you can see still its progress but don't delay for each individual pixel to be drawn.

  • I found stamping my pixels to be more efficient than your forward(1) approach. This isn't intuitive as stamping keeps track of the stamps and does a bunch of polygon operations but it still times out better. This many be because it avoids the pen up and down operations.

  • Make the turtle invisible either from creation using turtle = Turtle(visible=False) or later via turtle.hideturtle(). No need to waste time drawing the turtle itself for every pixel.

  • Turn off undo. (In the case of stamping, you can't turn it off due to a bug in turtle.py, but) in general, if you're drawing lots and don't plan to undo you, can turtle.setundobuffer(None) to turn off this feature and not store every operation in a buffer.

  • As with any speed optimized program, avoid doing anything at runtime that you can do ahead -- waste lots of space doing so if needed. Avoid doing things at runtime that are redundant. In my example below, I tried to minimize time spent on setting colors by precomputing the color values and avoiding turtle.color(...) if the pen color is already set to the correct color.

enter image description here

from turtle import Turtle, Screen

COORDINATES = (-2.0, -1.0, 1.0, 1.0)

WIDTH, HEIGHT = 320, 240

colors = {i: ((i & 0b1111) << 4, (i & 0b111) << 5, (i & 0b11111) << 3) for i in range(2 ** 5)}

def mandelbrot(turtle, minimum_x, minimum_y, maximum_x, maximum_y, iterations, width, height):

    step_x, step_y = (maximum_x - minimum_x) / width, (maximum_y - minimum_y) / height

    real = minimum_x

    current_color = 0

    while real < maximum_x:

        imaginary = minimum_y

        while imaginary < maximum_y:

            color = 0

            c, z = complex(real, imaginary), 0j

            for i in range(iterations):
                if abs(z) >= 4.0:
                    color = i & 31
                    break

                z = z * z + c

            if color != current_color:
                turtle.color(colors[color])
                current_color = color

            turtle.setpos(real, imaginary)
            turtle.stamp()
            imaginary += step_y

        screen.update()
        real += step_x

screen = Screen()
screen.setup(WIDTH, HEIGHT)
screen.setworldcoordinates(*COORDINATES)
screen.colormode(255)
screen.tracer(0, 0)

turtle = Turtle(visible=False)
turtle.speed("fastest")
turtle.setundobuffer(1)  # unfortunately setundobuffer(None|0) broken for turtle.stamp()

turtle.begin_poly()
# Yes, this is an empty polygon but produces a dot -- I don't know if this works on all platforms
turtle.end_poly()

screen.register_shape("pixel", turtle.get_poly())
turtle.shape("pixel")
turtle.up()

mandelbrot(turtle, *COORDINATES, 31, WIDTH, HEIGHT)

screen.exitonclick()

An unresolved issue is, as you noted, it slows down as draws. It only takes 15 seconds to draw the first 2/3rds of the above image, the last third takes a minute. I tried to find a reason but nothing so far. An interesting twist is that the last 5% of the drawing speeds back up.

cdlane
  • 40,441
  • 5
  • 32
  • 81