Search code examples
python-turtle

How do I optimize this code so that it executes faster (max ~5 mins)?


I am trying to make a program in turtle that creates a Lyapunov fractal. However, as using timeit shows, this should take around 3 hours to complete, 1.5 if I compromise resolution (N).

import turtle as t; from math import log; from timeit import default_timer as dt
t.setup(2000,1000,0); swid=t.window_width(); shei=t.window_height(); t.up(); t.ht(); t.tracer(False); t.colormode(255); t.bgcolor('pink')

def lyapunov(seq,xmin,xmax,ymin,ymax,N,tico):
    truseq=str(bin(seq))[2:]
    for x in range(-swid//2+2,swid//2-2):
        tx=(x*(xmax-xmin))/swid+(xmax+xmin)/2
        if x==-swid//2+2:
            startt=dt()
        for y in range(-shei//2+11,shei//2-1):
            t.goto(x,y); ty=(y*(ymax-ymin))/shei+(ymax+ymin)/2; lex=0; xn=prevxn=0.5
            for n in range(1,N+1):
                if truseq[n%len(truseq)]=='0': rn=tx
                else: rn=ty
                xn=rn*prevxn*(1-prevxn)
                prevxn=xn
                if xn!=1: lex+=(1/N)*log(abs(rn*(1-2*xn)))
            if lex>0: t.pencolor(0,0,min(int(lex*tico),255))
            else: t.pencolor(max(255+int(lex*tico),0),max(255+int(lex*tico),0),0)
            t.dot(size=1); t.update()
        if x==-swid//2+2:
            endt=dt()
            print(f'Estimated total time: {(endt-startt)*(swid-5)} secs')

#Example: lyapunov(2,2.0,4.0,2.0,4.0,10000,100)

I attempted to use yield but I couldn't figure out where it should go.


Solution

  • On my slower machine, I was only able to test with a tiny N (e.g. 10) but I was able to speed up the code about 350 times. (Though this will be clearly lower as N increases.) There are two problems with your use of update(). The first is you call it too often -- you should outdent it from the y loop to the x loop so it's only called once on each vertical pass. Second, the dot() operator forces an automatic update() so you get no advantage from using tracer(). Replace dot() with some other method of drawing a pixel and you'll get back the advantage of using tracer() and update(). (As long as you move update() out of innermost loop as I noted.)

    My rework of your code where I tried out these, and other, changes:

    from turtle import Screen, Turtle
    from math import log
    from timeit import default_timer
    
    def lyapunov(seq, xmin, xmax, ymin, ymax, N, tico):
        xdif = xmax - xmin
        ydif = ymax - ymin
    
        truseq = str(bin(seq))[2:]
    
        for x in range(2 - swid_2, swid_2 - 2):
            if x == 2 - swid_2:
                startt = default_timer()
    
            tx = x * xdif / swid + xdif/2
    
            for y in range(11 - shei_2, shei_2 - 1):
                ty = y * ydif / shei + ydif/2
                lex = 0
                xn = prevxn = 0.5
    
                for n in range(1, N+1):
                    rn = tx if truseq[n % len(truseq)] == '0' else ty
    
                    xn = rn * prevxn * (1 - prevxn)
                    prevxn = xn
    
                    if xn != 1:
                        lex += 1/N * log(abs(rn * (1 - xn*2)))
    
                if lex > 0:
                    turtle.pencolor(0, 0, min(int(lex * tico), 255))
                else:
                    lex_tico = max(int(lex * tico) + 255, 0)
                    turtle.pencolor(lex_tico, lex_tico, 0)
    
                turtle.goto(x, y)
                turtle.pendown()
    
            turtle.penup()
            screen.update()
    
            if x == 2 - swid_2:
                endt = default_timer()
                print(f'Estimated total time: {(endt - startt) * (swid - 5)} secs')
    
    screen = Screen()
    screen.setup(2000, 1000, startx=0)
    screen.bgcolor('pink')
    screen.colormode(255)
    screen.tracer(False)
    
    swid = screen.window_width()
    shei = screen.window_height()
    
    swid_2 = swid//2
    shei_2 = shei//2
    
    turtle = Turtle()
    turtle.hideturtle()
    turtle.penup()
    turtle.setheading(90)
    
    lyapunov(2, 2.0, 4.0, 2.0, 4.0, 10, 100)
    
    screen.exitonclick()