1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
|
# This file is part of h5py, a Python interface to the HDF5 library.
#
# http://www.h5py.org
#
# Copyright 2008-2013 Andrew Collette and contributors
#
# License: Standard 3-clause BSD; see "license.txt" for full license terms
# and contributor agreement.
"""
Demonstrates use of h5py in a multi-threaded GUI program.
In a perfect world, multi-threaded programs would practice strict
separation of tasks, with separate threads for HDF5, user interface,
processing, etc, communicating via queues. In the real world, shared
state is frequently encountered, especially in the world of GUIs. It's
quite common to initialize a shared resource (in this case an HDF5 file),
and pass it around between threads. One must then be careful to regulate
access using locks, to ensure that each thread sees the file in a
consistent fashion.
This program demonstrates how to use h5py in a medium-sized
"shared-state" threading application. Two threads exist: a GUI thread
(Tkinter) which takes user input and displays results, and a calculation
thread which is used to perform computation in the background, leaving
the GUI responsive to user input.
The computation thread calculates portions of the Mandelbrot set and
stores them in an HDF5 file. The visualization/control thread reads
datasets from the same file and displays them using matplotlib.
"""
import tkinter as tk
import threading
import numpy as np
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import h5py
file_lock = threading.RLock() # Protects the file from concurrent access
t = None # We'll use this to store the active computation thread
class ComputeThread(threading.Thread):
"""
Computes a slice of the Mandelbrot set, and saves it to the HDF5 file.
"""
def __init__(self, f, shape, escape, startcoords, extent, eventcall):
""" Set up a computation thread.
f: HDF5 File object
shape: 2-tuple (NX, NY)
escape: Integer giving max iterations to escape
start: Complex number giving initial location on the plane
extent: Complex number giving calculation extent on the plane
"""
self.f = f
self.shape = shape
self.escape = escape
self.startcoords = startcoords
self.extent = extent
self.eventcall = eventcall
threading.Thread.__init__(self)
def run(self):
""" Perform computations and record the result to file """
nx, ny = self.shape
arr = np.ndarray((nx,ny), dtype='i')
xincr = self.extent.real/nx
yincr = self.extent.imag/ny
def compute_escape(pos, escape):
""" Compute the number of steps required to escape """
z = 0+0j;
for i in range(escape):
z = z**2 + pos
if abs(z) > 2:
break
return i
for x in range(nx):
if x%25 == 0: print("Computing row %d" % x)
for y in range(ny):
pos = self.startcoords + complex(x*xincr, y*yincr)
arr[x,y] = compute_escape(pos, self.escape)
with file_lock:
dsname = "slice%03d" % len(self.f)
dset = self.f.create_dataset(dsname, (nx, ny), 'i')
dset.attrs['shape'] = self.shape
dset.attrs['start'] = self.startcoords
dset.attrs['extent'] = self.extent
dset.attrs['escape'] = self.escape
dset[...] = arr
print("Calculation for %s done" % dsname)
self.eventcall()
class ComputeWidget:
"""
Responsible for input widgets, and starting new computation threads.
"""
def __init__(self, f, master, eventcall):
self.f = f
self.eventcall = eventcall
self.mainframe = tk.Frame(master=master)
entryframe = tk.Frame(master=self.mainframe)
nxlabel = tk.Label(entryframe, text="NX")
nylabel = tk.Label(entryframe, text="NY")
escapelabel = tk.Label(entryframe, text="Escape")
startxlabel = tk.Label(entryframe, text="Start X")
startylabel = tk.Label(entryframe, text="Start Y")
extentxlabel = tk.Label(entryframe, text="Extent X")
extentylabel = tk.Label(entryframe, text="Extent Y")
self.nxfield = tk.Entry(entryframe)
self.nyfield = tk.Entry(entryframe)
self.escapefield = tk.Entry(entryframe)
self.startxfield = tk.Entry(entryframe)
self.startyfield = tk.Entry(entryframe)
self.extentxfield = tk.Entry(entryframe)
self.extentyfield = tk.Entry(entryframe)
nxlabel.grid(row=0, column=0, sticky=tk.E)
nylabel.grid(row=1, column=0, sticky=tk.E)
escapelabel.grid(row=2, column=0, sticky=tk.E)
startxlabel.grid(row=3, column=0, sticky=tk.E)
startylabel.grid(row=4, column=0, sticky=tk.E)
extentxlabel.grid(row=5, column=0, sticky=tk.E)
extentylabel.grid(row=6, column=0, sticky=tk.E)
self.nxfield.grid(row=0, column=1)
self.nyfield.grid(row=1, column=1)
self.escapefield.grid(row=2, column=1)
self.startxfield.grid(row=3, column=1)
self.startyfield.grid(row=4, column=1)
self.extentxfield.grid(row=5, column=1)
self.extentyfield.grid(row=6, column=1)
entryframe.grid(row=0, rowspan=2, column=0)
self.suggestbutton = tk.Button(master=self.mainframe, text="Suggest", command=self.suggest)
self.computebutton = tk.Button(master=self.mainframe, text="Compute", command=self.compute)
self.suggestbutton.grid(row=0, column=1)
self.computebutton.grid(row=1, column=1)
self.suggest = 0
def compute(self, *args):
""" Validate input and start calculation thread.
We use a global variable "t" to store the current thread, to make
sure old threads are properly joined before they are discarded.
"""
global t
try:
nx = int(self.nxfield.get())
ny = int(self.nyfield.get())
escape = int(self.escapefield.get())
start = complex(float(self.startxfield.get()), float(self.startyfield.get()))
extent = complex(float(self.extentxfield.get()), float(self.extentyfield.get()))
if (nx<=0) or (ny<=0) or (escape<=0):
raise ValueError("NX, NY and ESCAPE must be positive")
if abs(extent)==0:
raise ValueError("Extent must be finite")
except (ValueError, TypeError) as e:
print(e)
return
if t is not None:
t.join()
t = ComputeThread(self.f, (nx,ny), escape, start, extent, self.eventcall)
t.start()
def suggest(self, *args):
""" Populate the input fields with interesting locations """
suggestions = [(200,200,50, -2, -1, 3, 2),
(500, 500, 200, 0.110, -0.680, 0.05, 0.05),
(200, 200, 1000, -0.16070135-5e-8, 1.0375665-5e-8, 1e-7, 1e-7),
(500, 500, 100, -1, 0, 0.5, 0.5)]
for entry, val in zip((self.nxfield, self.nyfield, self.escapefield,
self.startxfield, self.startyfield, self.extentxfield,
self.extentyfield), suggestions[self.suggest], strict=True):
entry.delete(0, 999)
entry.insert(0, repr(val))
self.suggest = (self.suggest+1)%len(suggestions)
class ViewWidget:
"""
Draws images using the datasets recorded in the HDF5 file. Also
provides widgets to pick which dataset is displayed.
"""
def __init__(self, f, master):
self.f = f
self.mainframe = tk.Frame(master=master)
self.lbutton = tk.Button(self.mainframe, text="<= Back", command=self.back)
self.rbutton = tk.Button(self.mainframe, text="Next =>", command=self.forward)
self.loclabel = tk.Label(self.mainframe, text='To start, enter values and click "compute"')
self.infolabel = tk.Label(self.mainframe, text='Or, click the "suggest" button for interesting locations')
self.fig = Figure(figsize=(5, 5), dpi=100)
self.plot = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.mainframe)
self.canvas.draw_idle()
self.loclabel.grid(row=0, column=1)
self.infolabel.grid(row=1, column=1)
self.lbutton.grid(row=2, column=0)
self.canvas.get_tk_widget().grid(row=2, column=1)
self.rbutton.grid(row=2, column=2)
self.index = 0
self.jumptolast()
def draw_fractal(self):
""" Read a dataset from the HDF5 file and display it """
with file_lock:
name = list(self.f.keys())[self.index]
dset = self.f[name]
arr = dset[...]
start = dset.attrs['start']
extent = dset.attrs['extent']
self.loclabel["text"] = 'Displaying dataset "%s" (%d of %d)' % (dset.name, self.index+1, len(self.f))
self.infolabel["text"] = "%(shape)s pixels, starts at %(start)s, extent %(extent)s" % dset.attrs
self.plot.clear()
self.plot.imshow(arr.transpose(), cmap='jet', aspect='auto', origin='lower',
extent=(start.real, (start.real+extent.real),
start.imag, (start.imag+extent.imag)))
self.canvas.draw_idle()
def back(self):
""" Go to the previous dataset (in ASCII order) """
if self.index == 0:
print("Can't go back")
return
self.index -= 1
self.draw_fractal()
def forward(self):
""" Go to the next dataset (in ASCII order) """
if self.index == (len(self.f)-1):
print("Can't go forward")
return
self.index += 1
self.draw_fractal()
def jumptolast(self,*args):
""" Jump to the last (ASCII order) dataset and display it """
with file_lock:
if len(self.f) == 0:
print("can't jump to last (no datasets)")
return
index = len(self.f)-1
self.index = index
self.draw_fractal()
if __name__ == '__main__':
f = h5py.File('mandelbrot_gui.hdf5', 'a')
root = tk.Tk()
display = ViewWidget(f, root)
root.bind("<<FractalEvent>>", display.jumptolast)
def callback():
root.event_generate("<<FractalEvent>>")
compute = ComputeWidget(f, root, callback)
display.mainframe.grid(row=0, column=0)
compute.mainframe.grid(row=1, column=0)
try:
root.mainloop()
finally:
if t is not None:
t.join()
f.close()
|