I wrote up a script a while back to let me play around with fractals. The idea was to have direct access to the script that creates the fractal. None of that close, edit, then run hassle; just edit then run.

renderscript.py contains the GUI:

import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfile

class View(tk.Frame):
    count = 0
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        tk.Button(self, text="open", command=self.open).pack(fill=tk.X)
        tk.Button(self, text="save", command=self.save).pack(fill=tk.X)
        tk.Button(self, text="run program", command=self.draw).pack(fill=tk.X)
        self.txt = tk.Text(self, height=30)
        scr = tk.Scrollbar(self)
        scr.pack(side="right", fill="y", expand=False)
        self.txt.pack(side="left", fill="both", expand=True)
    def draw(self, size=500):
        exec(str(self.txt.get(1.0, tk.END)))
        self.pixels = [[(0, 0, 0) for y in range(size)] for x in range(size)]
        self.pixels = render(self.pixels)
        window = tk.Toplevel(self)
        canvas = tk.Canvas(window, width=size, height=size, bg='white')
        img = tk.PhotoImage(width=size, height=size)
        canvas.create_image((size/2, size/2), image=img, state="normal")
        for y in range(size):
            for x in range(size):
                img.put(self.rgbtohex(self.pixels[x][y]), (x,y))
    def rgbtohex(self, rgb):
        return ("#" + "{:02X}" * 3).format(*rgb)
    def open(self):
        self.txt.delete(1.0, tk.END)
        self.txt.insert(tk.END, open(askopenfilename()).read())
    def save(self):
        f = asksaveasfile(mode='w', defaultextension=".py")
        if f is None:
        text2save = str(self.txt.get(1.0, tk.END))

if __name__ == "__main__":
    root = tk.Tk()
    main = View(root)

renderscript.py screenshot

fractal.py contains example fractal routines:

class Fractal:
    def mandelbrot(self, x, y, scale, center=(2.2, 1.5)):
        n = lambda c: self.iterate_mandelbrot(c)
        return self.calcolor(x, y, scale, center, n)

    def julia(self, x, y, scale, center=(1.5, 1.5)):
        n = lambda c: self.iterate_mandelbrot(complex(0.3, 0.6), c)
        return self.calcolor(x, y, scale, center, n)

    def calcolor(self, x, y, scale, center, nf):
        c = complex(x * scale - center[0], y * scale - center[1])
        n = nf(c)
        if n is None:
            v = 1
            v = n/100.0
        return v

    def iterate_mandelbrot(self, c, z = 0):
        for n in range(256):
            z = z*z +c
            if abs(z) > 2.0:
                return n
        return None

    def griderator(self, w, h):
        for x in range(w):
            for y in range(h):
                yield x, y

    def render(self, pixels):
        scale = 1.0/(len(pixels[0])/3)
        for x, y in self.griderator(len(pixels), len(pixels[0])):
            i = self.mandelbrot(x, y, scale) * 256
            r, g, b = int(i % 16 * 16), int(i % 8 * 32), int(i % 4 * 63)
            pixels[x][y] = (r, g, b)
        return pixels

global render
render = Fractal().render

fractal.py screenshot alternate fractal.py screenshot

The script does block while rendering the script. Try replacing mandelbrot with julia. I am looking for feedback on style and usability.

  • \$\begingroup\$ I'm just curious-- how long does it take you to render the Mandelbrot set? \$\endgroup\$
    – Myridium
    Jun 28 '15 at 6:04
  • \$\begingroup\$ @Myridium 8 seconds on my box \$\endgroup\$
    – motoku
    Jun 28 '15 at 6:46

Some suggestions:

  1. In calcator, I would use a ternary expression: return 1. if n is None else n/100.
  2. calcator can be made even more efficient by having iterate_mandelbrot return 100. if the loop finished. Then you just divide the result of that by 100. This will result in 1. if the loop exits, avoiding the if test entirely.
  3. I would only do run self.mandelbrot in your loop, and store the result of that function to a 2D numpy array. Then you can vectorize the rest of the calculation, since it is all just basic math. This should substantially increase performance. You can even move the n/100 outside the for loop to further improve performance.
  4. If the previous suggestion does not increase performance enough, you might be able use multiprocessing.Pool.imap to further increase the performance of the loop.
  5. For an even more extreme vectorization, you can do all your calculations on all pixels at once.
  6. Rather than having a griditor function, just use itertools.product.
  7. Follow pep8
  8. I would put the current contents of the if __name__ == "__main__": block in a main function and just call that function inside the if __name__ == "__main__": block.
  9. In render I would allow the code to pass a string (which defaults to mandelbrot, and use getattr to dynamically call method with that name.
  10. I would rename iterate_mandelbrot to iterate_pixel.
  11. I would put in mandelbrot an argument for z that lets the user change z. Similarly, I would put an argument in julia that lets the user change (0.3, 0.6) to something else.
  12. render should accept *args, **kwargs that are then passed directly to the mandelbrot or julia method.
  13. I would move the julia and mandelbrot lambdas into their own methods. Or better yet, I would refactor so you just pass the c and z argument.
  14. In render, I would let the user set the scale with an argument. The scale argument would default to None. If it is None, it would be computed automatically as is done now.
  15. In render, you only ever work with integers. So I would use n//100 in calcator to make sure it returns an integer. This allows you to avoid the later integer conversions.

So here is my version of Fractal

import numpy as np

class Fractal:        
    def mandelbrot(self, pixels, scale, center=(2.2, 1.5), z=0.):
        return self.calcolor(pixels, scale, center, zs=z)

    def julia(self, pixels, scale, center=(1.5, 1.5), c=(0.3, 0.6)):
        if not hasattr(c, 'imag'):
            c = complex(*c)
        return self.calcolor(pixels, scale, center, cs=c)

    def calcolor(self, pixels, scale, center, cs=None, zs=None):
        pixels = pixels.asarray(pixels)

        xpixels = np.arange(pixels.shape[0])[None, :]
        ypixels = np.arange(pixels.shape[1])[:, None]

        val = (xpixels+ypixels*1j)*scale-complex(*center)
        if cs is None and zs is None:
           raise ValueError('Either cs or zs must be specified')
        if cs is None:
           cs = val
        if zs is None:
           zs = val
        ns = np.full_like(val, 100, dtype='int16')

        for n in range(256):
            zs = zs**2 +cs
            ns[ns>0 & np.abs(zs)>2.0] = n
        return ns

    def calc_pixels(self, cs, zs):

    def render(self, pixels, scale=None, method='mandelbrot', *args, **kwargs):
        if scale is None:
            scale = 1.0/(len(pixels[0])/3)

            func = getattr(self, method)
        except AttributeError:
            raise ValueError('Unknown method %s' % method)
        i = func(pixels, scale, *args, **kwargs)*256

        r = i%16 * 16
        g = i*8 * 32
        b = i%4 * 63

        return np.dstack([r, g, b])

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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