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.config(command=self.txt.yview)
self.txt.config(yscrollcommand=scr.set)
scr.pack(side="right", fill="y", expand=False)
self.txt.pack(side="left", fill="both", expand=True)
self.pack()
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)
window.resizable(0,0)
canvas = tk.Canvas(window, width=size, height=size, bg='white')
canvas.pack()
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))
window.mainloop()
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:
return
text2save = str(self.txt.get(1.0, tk.END))
f.write(text2save)
f.close()
if __name__ == "__main__":
root = tk.Tk()
root.resizable(0,0)
main = View(root)
root.mainloop()
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
else:
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– Myridium2015年06月28日 06:04:57 +00:00Commented Jun 28, 2015 at 6:04
-
\$\begingroup\$ @Myridium 8 seconds on my box \$\endgroup\$motoku– motoku2015年06月28日 06:46:23 +00:00Commented Jun 28, 2015 at 6:46
1 Answer 1
Some suggestions:
- In
calcator
, I would use a ternary expression:return 1. if n is None else n/100.
calcator
can be made even more efficient by havingiterate_mandelbrot
return100.
if the loop finished. Then you just divide the result of that by 100. This will result in 1. if the loop exits, avoiding theif
test entirely.- 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 then/100
outside the for loop to further improve performance. - 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. - For an even more extreme vectorization, you can do all your calculations on all pixels at once.
- Rather than having a
griditor
function, just useitertools.product
. - Follow pep8
- I would put the current contents of the
if __name__ == "__main__":
block in amain
function and just call that function inside theif __name__ == "__main__":
block. - In
render
I would allow the code to pass a string (which defaults tomandelbrot
, and usegetattr
to dynamically call method with that name. - I would rename
iterate_mandelbrot
toiterate_pixel
. - I would put in
mandelbrot
an argument forz
that lets the user changez
. Similarly, I would put an argument injulia
that lets the user change(0.3, 0.6)
to something else. render
should accept*args, **kwargs
that are then passed directly to themandelbrot
orjulia
method.- I would move the
julia
andmandelbrot
lambdas into their own methods. Or better yet, I would refactor so you just pass thec
andz
argument. - In
render
, I would let the user set the scale with an argument. Thescale
argument would default toNone
. If it isNone
, it would be computed automatically as is done now. - In
render
, you only ever work with integers. So I would usen//100
incalcator
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)
try:
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])
Explore related questions
See similar questions with these tags.