Source code for scitools.TkGUI

"""
Module with functions and classes used in the GUI chapters of
the book "Python Scripting for Computational Science".
"""
# NOTE: This file merges the previous modules
# CanvasCoords, FunctionSelector, DrawFunction, FuncDependenceViz,
# ParameterInterface
#

import Tkinter, os
from scitools.misc import import_module
Pmw = import_module('Pmw')

[docs]class DrawFunction: """ Interactive drawing of y=f(x) functions. The drawing takes place in a Pmw.Blt.Graph widget. """
[docs] def __init__(self, xcoor, parent, ymin=0.0, ymax=1.0, width=500, height=200, curvename=' ', ylabel='', xlabel='', curvecolor='green', curvewidth=4, yrange_widgets=True): """ Interactive drawing of a function. xcoor grid points (on the x axsis) for interpolation parent parent widget ymin, ymax initial extent of the y axis width, height size of widget curvename name of function to be drawn xlabel, ylabel labels on the axis curvecolor color of the drawn curve curvewidth line thickness of the drawn curve yrange_widgets True: add text entries for range of y axis These parameters, except for parent and yrange_widgets, can also be set as keyword arguments in the configure method. """ self.master = parent self.top = Tkinter.Frame(self.master) # packed in self.pack(); the user can then place this # DrawFunction wherever it is desired in a big GUI frame1 = Tkinter.Frame(self.top); frame1.pack(side='top') if yrange_widgets: column1 = Tkinter.Frame(frame1) column1.pack(side='left') if ylabel: yl = ylabel else: yl = 'y' self.ymin_widget = Pmw.EntryField(column1, labelpos='n', label_text=yl+' min', entry_width=6, command=self.__ymin) self.ymin_widget.pack(side='top',padx=2,pady=2) self.ymax_widget = Pmw.EntryField(column1, labelpos='n', label_text=yl+' max', entry_width=6, command=self.__ymax) self.ymax_widget.pack(side='top',padx=2,pady=2) Pmw.alignlabels([self.ymin_widget,self.ymax_widget]) self.g = Pmw.Blt.Graph(frame1, width=width, height=height) self.g.pack(side='left', expand=1, fill='both') if yrange_widgets: self.set_yaxis(ymin, ymax) self.configure(xcoor=xcoor, width=width, height=height, curvename=curvename, curvecolor=curvecolor, curvewidth=curvewidth, xlabel=xlabel, ylabel=ylabel) self.g.grid_on() self.g.bind('<ButtonPress>', self.mouse_down) self.g.bind('<ButtonRelease>', self.mouse_up) row1 = Tkinter.Frame(self.top) row1.pack(side='top') Tkinter.Button(row1, text='Interpolate to grid', width=20, command=self.interpolate).pack(side='left',padx=2) Tkinter.Button(row1, text='Erase drawing', width=20, command=self.erase).pack(side='left',padx=2) self.erase() # some init
[docs] def pack(self, **kwargs): self.top.pack(kwargs, expand=1, fill='both')
[docs] def configure(self, **kwargs): """ Legal parameters (kwargs): xcoor grid points (on the x axsis) for interpolation width, height size of widget curvename name of function to be drawn xlabel, ylabel labels on the axis curvecolor color of the drawn curve curvewidth line thickness of the drawn curve ymin and ymax are set in set_yaxis method. """ for name in kwargs: if name == 'xcoor': xcoor = kwargs['xcoor'] self.xcoor = xcoor self.xmin = min(xcoor); self.xmax = max(xcoor) self.g.xaxis_configure(min=self.xmin, max=self.xmax) elif name == 'width': self.g.configure(width=kwargs['width']) elif name == 'height': self.g.configure(width=kwargs['height']) elif name == 'curvename': self.curvename = kwargs['curvename'] elif name == 'curvecolor': self.curvecolor = kwargs['curvecolor'] elif name == 'curvewidth': self.curvewidth = kwargs['curvewidth'] elif name == 'xlabel': self.g.xaxis_configure(title=kwargs['xlabel']) elif name == 'ylabel': self.g.xaxis_configure(title=kwargs['ylabel']) if self.curvename == ' ': self.legend = '' else: self.legend = self.curvename
[docs] def set_yaxis(self, ymin, ymax): try: self.ymax_widget.setvalue(ymax) self.ymin_widget.setvalue(ymin) except: # no widgets for ymin, ymax pass self.g.yaxis_configure(min=ymin, max=ymax)
def __ymin(self): self.g.yaxis_configure(min=float(self.ymin_widget.get())) def __ymax(self): self.g.yaxis_configure(max=float(self.ymax_widget.get()))
[docs] def erase(self): """delete all curves and make new empty self.x and self.y""" for curvename in self.g.element_show(): self.g.element_delete(curvename) self.x = Pmw.Blt.Vector() self.y = Pmw.Blt.Vector() self.g.configure(title='0 drawn points')
[docs] def mouse_drag(self, event): # transform screen coordinates of the mouse position, # (event.x,event.y) to physical coordinates (x,y): x = self.g.xaxis_invtransform(event.x) y = self.g.yaxis_invtransform(event.y) self.x.append(x); self.y.append(y) # plot the curve as soon as we have two points, BLT vectors # will automatically update the graph when they get new # elements... if len(self.x) == 2: if self.g.element_exists(self.curvename): self.g.element_delete(self.curvename) self.g.line_create(self.curvename, label=self.legend, xdata=self.x, ydata=self.y, color=self.curvecolor, linewidth=self.curvewidth, outlinewidth=0, fill='') self.g.configure(title='%d points drawn' % len(self.x))
[docs] def mouse_down(self, event): self.g.bind('<Motion>', self.mouse_drag)
[docs] def mouse_up(self, event): self.g.unbind('<Motion>')
[docs] def interpolate(self): # first build a new list with the approved (x,y) pairs: # if the drawn curve is too short, stretch the end points: if self.x[0] > self.xmin: self.x[0] = self.xmin if self.x[-1] < self.xmax: self.x[-1] = self.xmax x = []; y = []; current_xmax = -9.9E+20 for i in range(len(self.x)): # skip points outside [xmin,xmax]: if self.x[i] >= self.xmin and self.x[i] <= self.xmax: if self.x[i] > current_xmax: x.append(self.x[i]); y.append(self.y[i]) current_xmax = self.x[i] # ensure that the end points are included: if x[0] > self.xmin: x.insert(0,self.xmin); y.insert(0,y[0]) if x[-1] < self.xmax: x.append(self.xmax); y.append(y[-1]) self.f = points2grid(x, y, self.xcoor) # the interpolated curve is now (self.xcoor,self.f) # both are NumPy arrays if self.g.element_exists(self.curvename + '_i'): self.g.element_delete(self.curvename + '_i') if len(self.xcoor) > 30: # many points, remove drawn curve, replace by interpolated data: self.g.element_delete(self.curvename) circles = 0 else: # keep drawn curve, add circles at interpolation points: circles = 1 self.g.line_create(self.curvename + '_i', label=self.legend, xdata=tuple(self.xcoor), ydata=tuple(self.f), color='blue', linewidth=1, outlinewidth=circles, fill='')
[docs] def get(self): """ return points (x,y), interpolated to the grid, where x and y are NumPy arrays of coordinates """ try: return self.xcoor, self.f except: raise AttributeError('No drawing! Draw the curve first!')
[docs]def points2grid(x, y, xcoor): "Transform points (x,y) to a uniform grid with coordinates xcoor." L = 0; R = 0 n = len(xcoor) m = len(x) from numpy import zeros f = zeros(n) for i in range(n): xi = xcoor[i] # find j such that xi is between x[j-1] and x[j] j = L # we know that xcoor[i-1]>L so x[i]>L while j < m-1 and x[j] <= xi: j += 1 if j < m: L = j-1; R = j #print "i=%d xi=%g; in between x[%d]=%g and x[%d]=%g" % (i,xi,L,x[L],R,x[R]) # linear interpolation: f[i] = y[L] + (y[R]-y[L])/(x[R]-x[L])*(xi-x[L]) else: raise ValueError("bug") return f
[docs]class DrawFunctionDialog:
[docs] def __init__(self, xcoor, parent=None): """Dialog box with DrawFunction widget""" self.d = Pmw.Dialog(parent, title='Programmer-Defined Dialog', buttons=('Approved', 'Cancel'), command=self.action) self.d_gui = DrawFunction(xcoor, self.d.interior()) self.d_gui.pack(padx=10,pady=10)
[docs] def action(self, result): if result == 'Approved': self.x, self.f = self.d_gui.get() self.d.destroy()
[docs] def get(self): return self.x, self.f
def _test_DrawFunction(): root = Tkinter.Tk() Pmw.initialise(root) import scitools.misc; scitools.misc.fontscheme6(root) root.title('DrawFunction demo') from numpy import linspace x = linspace(0, 1, 21) df = DrawFunction(x, root) df.pack() # Tkinter.Button(root, text='Print coordinates (interpolated to grid)', # command=lambda o=df: sys.stdout.write(str(o.get()[1]))).pack(pady=2) root.mainloop()
[docs]def roundInt(a): return int(a+0.5)
[docs]class CanvasCoords: """ Utilities for transforming between canvas coordinates and physical (real) coordinates. """
[docs] def __init__(self): # 400x400 pixels is default: self.canvas_x = self.canvas_y = 400 # origin: lower left corner: self.x_origin = 0; self.y_origin = self.canvas_y # x and y measured in pixels: self.x_range = self.canvas_x self.xy_scale = self.canvas_x/self.x_range
[docs] def set_coordinate_system(self, canvas_width, canvas_height, x_origin, y_origin, x_range = 1.0): """ Define parameters in the physical coordinate system (origin, width) expressed in canvas coordinates. x_range is the width of canvas window in physical coordinates. """ self.canvas_x = canvas_width # width of canvas window self.canvas_y = canvas_height # height of canvas window # the origin in canvas coordinates: self.x_origin = x_origin self.y_origin = y_origin # x range (canvas_x in physical coords): self.x_range = x_range self.xy_scale = self.canvas_x/self.x_range
[docs] def print_coordinate_system(self): print "canvas = (%d,%d)" % (self.canvas_x, self.canvas_y) print "canvas origin = (%d,%d)" % (self.x_origin, self.y_origin) print "range of physical x coordinate =", self.x_range print "xy_scale (from physical to canvas): ", self.xy_scale # --- transformations between physical and canvas coordinates: ---
[docs] def physical2canvas(self, x, y): """Transform physical (x,y) to canvas 2-tuple.""" return (roundInt(self.x_origin + x*self.xy_scale), roundInt(self.y_origin - y*self.xy_scale))
[docs] def cx(self,x): """Transform physical x to canvas x.""" return roundInt(self.x_origin + x*self.xy_scale)
[docs] def cy(self,y): """Transform physical y to canvas y.""" return roundInt(self.y_origin - y*self.xy_scale)
[docs] def physical2canvas4(self, coords): """ Transform physical 4-tuple (x1,x2,y1,y2) to canvas 4-tuple. """ return (roundInt(self.x_origin + coords[0]*self.xy_scale), roundInt(self.y_origin - coords[1]*self.xy_scale), roundInt(self.x_origin + coords[2]*self.xy_scale), roundInt(self.y_origin - coords[3]*self.xy_scale))
[docs] def canvas2physical(self, x, y): """Inverse of physical2canvas.""" return (float((x - self.x_origin)/self.xy_scale), float((self.y_origin - y)/self.xy_scale))
[docs] def canvas2physical4(self, coords): """Inverse of physical2canvas4.""" return (float((coords[0] - self.x_origin)/self.xy_scale), float((self.y_origin - coords[1])/self.xy_scale), float((coords[2] - self.x_origin)/self.xy_scale), float((self.y_origin - coords[3])/self.xy_scale))
[docs] def scale(self, dx): """ Transform a length in canvas coordinates to a length in physical coordinates. """ return self.xy_scale*dx # short forms:
c2p = canvas2physical c2p4 = canvas2physical4 p2c = physical2canvas p2c4 = physical2canvas4
def _CanvasCoords_test(): root = Tkinter.Tk() c = Tkinter.Canvas(root,width=400, height=400) c.pack() # let physical (x,y) be at (200,200) and let the x range be 2: C.set_coordinate_system(400,400, 200,200, 2.0) cc = C.p2c4((0.2, 0.2, 0.6, 0.6)) c.create_oval(cc[0],cc[1],cc[2],cc[3],fill='red',outline='blue') c1, c2 = C.physical2canvas(0.2,0.2) c.create_text(c1, c2, text='(0.2,0.2)') c1, c2 = C.physical2canvas(0.6,0.6) c.create_text(c1, c2, text='(0.6,0.6)') c.create_line(C.cx(0.2), C.cy(0.2), C.cx(0.6), C.cy(0.2), C.cx(0.6), C.cy(0.6), C.cx(0.2), C.cy(0.6), C.cx(0.2), C.cy(0.2)) """ Utilities for holding and displaying data about input parameters. """ import re import scitools.modulecheck import scitools.misc try: PQ = import_module('Scientific.Physics.PhysicalQuantities') except ImportError: pass from scitools.misc import str2bool, str2obj
[docs]class InputPrm: """Class for holding data about a parameter."""
[docs] def __init__(self, name=None, default=0.0, str2type=None, help=None, unit=None, cmlarg=None, prmclass=None): """ default default value str2type string to type conversion (float, int, str, str2bool) name parameter name help description of parameter unit physical unit (dimension) cmlarg command-line argument for sending this prm to an external program prmclass classification of this parameter, e.g., 'numerics', 'physics', 'material', etc. Note: value with unit only works if str is float or int >>> p=InputPrm('q', 1, float, unit='m') >>> p.set(6) >>> p.get() 6.0 >>> p.set('6 cm') >>> p.get() 0.059999999999999998 >>> p=InputPrm('q', '1 m', float, unit='m') >>> p.set('1 km') >>> p.get() 1000.0 >>> p.get_wunit() '1000.0 m' >>> p.unit 'm' """ self.str2type = str2type self.name = name self.help = help self.unit = unit self.cmlarg = cmlarg self.prmclass = prmclass if str2type is None: self.str2type = scitools.misc.str2obj # check that unit is a valid physical dimension: if self.unit is not None: try: q = PQ.PhysicalQuantity('1.0 ' + str(self.unit)) except: raise ValueError( 'unit=%s is an illegal physical unit' % str(self.unit)) if self.str2type is float or self.str2type is int: pass # must have float or int when units are present else: raise ValueError( 'str2type must be float or int, not %s' % \ str(self.str2type)) self.set(default) # set parameter value scitools.modulecheck.exception('InputPrm constructor', 'Scientific')
[docs] def get(self): """Return the value of the parameter.""" return self._v
[docs] def set(self, value): """Set the value of the parameter.""" self._v = self.str2type(self._scan(value))
v = property(fget=get, fset=set, doc='value of parameter')
[docs] def get_wunit(self): """ Return value with unit (dimension) as string, if it has. Otherwise, return value (with the right type). """ if self.unit is not None: return str(self.get()) + ' ' + self.unit else: return self.get()
[docs] def __repr__(self): """Application of eval to this output creates the instance.""" return "InputPrm(name='%s', default=%s, str2type=%s, "\ "help=%s, unit=%s, cmlarg=%s)" % \ (self.name, self.__str__(), self.str2type.__name__, self.help, self.unit, self.cmlarg)
[docs] def __str__(self): """ Compact output; just the value as a formatted string. Note that __str__ is used by __repr__ so strings must be enclosed in quotes. """ return repr(self._v) # ensure quotes in strings
def _handle_unit(self, v): """ Check if v is of the form 'value unit', extract value, after conversion to correct unit (if necessary). """ if isinstance(v, PQ.PhysicalQuantity): v = str(v) # convert to 'value unit' string if isinstance(v, str) and isinstance(self.unit, str) and \ (self.str2type is float or self.str2type is int): if ' ' in v: # 'value unit' string? try: self.pq = PQ.PhysicalQuantity(v) except: raise ValueError('%s should be %s; illegal syntax' % \ (v, self.str2type.__name__)) if not self.pq.isCompatible(self.unit): raise ValueError( 'illegal unit (%s); %s is registered with unit %s' % \ (v, self.name, self.unit)) self.pq.convertToUnit(self.unit) v = self.str2type(str(self.pq).split()[0]) return v else: # string value without unit given: return self.str2type(v) else: # no unit handling if isinstance(v, str): # check if a unit was given: try: PQ.PhysicalQuantity(v) raise ValueError( 'parameter %s given with dimension: %s, but '\ 'dimension is not registered' % (self.name,v)) except: pass return self.str2type(v)
[docs] def getPhysicalQuantity(self): if self.unit is not None: try: return self.pq # may be computed in _handle_unit except: return PQ.PhysicalQuantity(self.get_wunit()) else: raise AttributeError('parameter %s has no registered unit' % \ self.name)
def _scan(self, s): """Interpret string s. Return number (for self._v).""" # multiple loops? v = self._handle_unit(s) return self.str2type(v)
[docs]def commandline2dict(argv, parameters): """ Load data from the command line into a dictionary of parameter values. The argv argument is typically sys.argv[1:]. Each option --opt in argv is extracted and the proceeding value v is assigned to parameters: parameters[opt].set(v) Hence, parameters must hold objects that have a set function. Normally, parameters is a dictionary of InputPrm objects. """ p = scitools.misc.cmldict(sys.argv[1:], cmlargs=None, validity=0) # p[key] holds all command-line args, we are only interested # in those keys corresponding to parameters.keys() for key in p.keys(): if key in parameters.keys(): parameters[key].set(p[key])
[docs]class InputPrmGUI(InputPrm): """Represent an input parameter by a widget.""" GUI_toolkit = 'Tkinter/Pmw'
[docs] def __init__(self, name=None, default=0.0, str2type=None, widget_type='entry', values=None, parent=None, help=None, unit=None, cmlarg=None): """ @param default: default value @param str2type: function from string to type @param name: name of parameter @param widget_type: entry, slider, option, checkbutton @param values: (min,max) interval or options @param parent: parent widget @param help: description of parameter @param unit: physical unit (dimension) @param cmlarg: command-line argument for sending this prm to an external program """ if str2type is None: str2type = scitools.misc.str2obj # bind self._v to an object with get and set methods # for assigning and extracting the parameter value # in the associated widget: if InputPrmGUI.GUI_toolkit.startswith('Tk'): # use Tkinter variables self.make_GUI_variable_Tk(str2type, unit, name) else: raise ValueError( 'The desired GUI toolkit %s is not supported' % \ InputPrmGUI.GUI_toolkit) # How to implement support for other toolkits: # self._v must point to an object with a get and set method # for extracting and setting the value of the parameter in # the associated widget. In Tkinter self._v is a Tkinter # variable (DoubleVar, StringVar, IntVar). In another toolkit # one can just create a corresponding class: # class GUIVariable: # def __init__(self): pass # def attach(self, widget): # self.widget = widget # can be done in make_widget_* # def get(self): # self.widget.get() # for example # def set(self, value): # self.widget.set(value) # for example InputPrm.__init__(self, name, default, str2type, help, unit, cmlarg) self._widget_type = widget_type self.parent = parent self._values = values # (from, to) interval for parameter self.widget = None # no widget created (yet) self._validate = None # no validation of answers by default if str2type == str2bool and self._widget_type != 'checkbutton': self._widget_type = 'checkbutton' # no warning because minimal input, just name and a # bool value, leads us here - which is okay - all other # widgets become entries
[docs] def get_widget_type(self): return self._widget_type
widget_type = property(fget=get_widget_type) # read-only
[docs] def make_GUI_variable_Tk(self, str2type, unit, name): """ Bind self._v to a variable with set and get methods for setting and getting the value in/from a GUI. """ if unit is not None: self._v = Tkinter.StringVar() # value with unit else: if str2type == float: self._v = Tkinter.DoubleVar() self._validate = {'validator' : 'real'} elif str2type == str: self._v = Tkinter.StringVar() elif str2type == int: self._v = Tkinter.IntVar() self._validate = {'validator' : 'int'} elif str2type == str2bool: self._v = Tkinter.StringVar() elif str2type == complex: self._v = Tkinter.StringVar() elif str2type == str2obj: self._v = Tkinter.StringVar() else: raise ValueError( 'str2type %s for parameter %s is not supported' % \ (str2type, name))
[docs] def make_widget(self): if InputPrmGUI.GUI_toolkit.startswith('Tk'): self.make_widget_Tk() else: raise ValueError( 'The desired GUI toolkit %s is not supported' % \ InputPrmGUI.GUI_toolkit)
[docs] def make_widget_Tk(self): """Make Tk widget according to self._widget_type.""" if self.name is None: raise TypeError("name attribute must be set before "\ "widget can be created") if self.parent is None: raise TypeError("parent attribute must be set before "\ "widget can be created") # consistency/type check of values, if it is supplied: if self._values is not None: if type(self._values) != type([]) and \ type(self._values) != type(()): raise TypeError("values attribute must be list or tuple") if self.unit is None: label = self.name else: label = '%s (%s)' % (self.name, self.unit) if self._widget_type == 'entry': if self._validate is not None and self._values is not None: self._validate['min'] = self._values[0] self._validate['max'] = self._values[1] self.widget = Pmw.EntryField(self.parent, labelpos='w', label_text=label, validate=self._validate, entry_width=15, entry_textvariable=self._v) elif self._widget_type == 'slider': # we require values: if self._values is None: raise TypeError( "values attribute must be set for slider '%s'" % \ self.name) min = float(self._values[0]); max = float(self._values[1]) try: step = float(self._values[2]) # try if present except: step = (max - min)/100.0 # default self.widget = Tkinter.Scale(self.parent, orient='horizontal', from_=min, to=max, tickinterval=(max - min)/5.0, resolution=step, label=label, #font="helvetica 12 italic", length=300, variable=self._v) elif self._widget_type == 'option': # we require values, which now contains the option values if self._values is None: raise TypeError( "values attribute must be set for option menu '%s'" % \ self.name) self.widget = Pmw.OptionMenu(self.parent, labelpos='w', # n, nw, ne, e and so on label_text=label, items=self._values, menubutton_textvariable=self._v ) elif self._widget_type == 'checkbutton': self.widget = Tkinter.Checkbutton(self.parent, text=label, variable=self._v) # no packing of widgets return self.widget # if desired (it's stored in the class too)
[docs] def get(self): """ Get GUI text/number, handle special input like numbers with units, if necessary. """ # self.str2type(self._scan(gui)) is sufficient here # but we check if we have a unit and then if the unit # is registered: try: gui = self._v.get() # fails if value has unit except ValueError, msg: if self.unit is None: print msg, '\nvalue with unit, but no registered unit!' sys.exit(1) # else: ok, go on with self._scan and interpret r = self._scan(gui) return r
[docs] def set(self, value): self._v.set(self.str2type(self._scan(value)))
[docs] def __repr__(self): """Application of eval to this output creates the object.""" return "InputPrmGUI(name='%s', default=%s, str2type=%s, "\ "widget_type='%s', parent=None, values=%s, "\ "help=%s, unit=%s, cmlarg=%s)" % \ (self.name, self.__str__(), self.str2type.__name__, self._widget_type, str(self._values), self.help, self.unit, self.cmlarg)
[docs]class InputPrmCGI(InputPrm): """Represent a parameter by a form variable in HTML."""
[docs] def __init__(self, name=None, default=0.0, str2type=None, widget_type='entry', values=None, form=None, help=None, unit=None, cmlarg=None): """ default default value str2type function from string to type name name of parameter widget_type entry, slider, option, checkbutton values option values form cgi.FieldStorage object help description of parameter unit physical unit (dimension) cmlarg command-line argument for sending this prm to an external program """ InputPrm.__init__(self, name, default, str2type, help, unit, cmlarg) self._widget_type = widget_type self._form = form self._values = values
[docs] def make_form_entry(self): """Write the form's input field, according to widget_type.""" if self.name is None: raise TypeError("name attribute must be set before "\ "widget can be created") value = str(self.get()) s = "" # HTML code to be returned is stored in s if self._widget_type == 'entry' or self._widget_type == 'slider': s += """<input type="text" name="%s" size=15 value="%s">""" % \ (self.name, value) elif self._widget_type == 'option': # we require values, which now contains the option values if self._values is None: raise TypeError( "values attribute must be set for option menu '%s'" % \ self.name) s += """<select name="%s" size=1 value="%s">\n""" % \ (self.name, value) for v in self._values: s += """<option value="%s">%s </option>\n""" % \ (v,v) s += """</select><br>\n""" elif self._widget_type == 'checkbutton': s += """<input type="checkbox" name="%s" value="%s">"""\ """&nbsp; <br>\n""" % \ (self.name, value) return s
[docs] def get(self): if self._form is not None: InputPrm.set(self,\ self._form.getvalue(self.name, str(self._v))) # InputPrm.set handles units return self._v # just inherit def set(self, value):
[docs] def __repr__(self): """Application of eval to this output creates the object.""" return "InputPrmCGI(name='%s', default=%s, str2type=%s, "\ "widget_type='%s', form=None, values=%s, "\ "help=%s, unit=%s, cmlarg=%s)" % \ (self.name, self.__str__(), self.str2type.__name__, self._widget_type, str(self._values), self.help, self.unit, self.cmlarg)
[docs]def createInputPrm(interface, name, default, str2type=None, widget_type='entry', values=None, parent=None, form=None, help=None, unit=None, cmlarg=None): """Unified interface to parameter classes InputPrm/GUI/CGI.""" if interface == '' or interface == 'plain': p = InputPrm(name=name, default=default, str2type=str2type, help=help, unit=unit, cmlarg=cmlarg) elif interface == 'GUI': p = InputPrmGUI(name=name, default=default, str2type=str2type, widget_type=widget_type, values=values, parent=parent, help=help, unit=unit, cmlarg=cmlarg) elif interface == 'CGI': p = InputPrmCGI(name=name, default=default, str2type=str2type, widget_type=widget_type, values=values, form=form, help=help, unit=unit, cmlarg=cmlarg) else: raise ValueError("interface '%s' not supported" % interface) return p
[docs]class Parameters: """ Class for holding a set of InputPrm-type parameters. See src/py/examples/simviz/simviz1cp.py for examples on usage. Some attributes may be useful in application code: self.dict is a dictionary of InputPrm-type objects. self.parameters_sequence (and self._seq) is a list of InputPrm-type objects in the sequence they were registered. self.sliders_sequence is a list of InputPrm-type objects, with slider widget representation in a GUI, in the sequence they were registered. self.entries_sequence, self.checkbt_sequence, self.options_sequence are similar for text entries, checkbuttons, and option menus. The self.*_sequence lists can be used to build GUIs or CGI scripts. Normally, this is automated in classes like AutoSimVizGUI and AutoSimVizCGI. """
[docs] def __init__(self, interface='plain', form=None, prm_dict={}): """ @param interface: 'plain', 'CGI', or 'GUI' @param form: cgi.FieldStorage() object @param prm_dict: dictionary with (name,value) pairs (will be added using the add method) """ self.dict = {} # holds InputPrm/GUI/CGI objects self._seq = [] # holds self.dict items in sequence self._interface = interface self._form = form # used for CGI for prm in prm_dict: self.add(prm, prm_dict[prm])
[docs] def add(self, name, default, str2type=None, widget_type='entry', values=None, help=None, unit=None, cmlarg=None): """Add a new parameter.""" self.dict[name] = createInputPrm(self._interface, name, default, str2type, widget_type=widget_type, values=values, help=help, unit=unit, cmlarg=cmlarg) self._seq.append(self.dict[name])
[docs] def endadd(self): """Process parameters, make internal data structures.""" self.parameters_sequence = self._seq if self._interface == 'GUI': self.sliders_sequence = [] self.entries_sequence = [] self.options_sequence = [] self.checkbt_sequence = [] for p in self._seq: if p.widget_type == 'slider': self.sliders_sequence.append(p) # add instance ref. elif p.widget_type == 'entry': self.entries_sequence.append(p) elif p.widget_type == 'option': self.options_sequence.append(p) elif p.widget_type == 'checkbutton': self.checkbt_sequence.append(p) else: raise ValueError('unknown widget_type "%s"' \ % p.widget_type) elif self._interface == 'CGI': for p in self._seq: p.form = self._form
[docs] def __setitem__(self, name, value): self.dict[name].set(value) if name in self.__dict__: # is item attribute too (name2attr)? # self.__dict__[name] = value # will not handle string w/unit self.__dict__[name] = self.dict[name].get()
[docs] def __getitem__(self, name): return self.dict[name].get()
[docs] def keys(self): """ Return parameter names. With this method Parameter objects p can be used in dictionary update functions: somedict.update(p). """ return self.dict.keys()
[docs] def __iter__(self): """Iterate over keys in self.dict.""" # short cut using generator function for name in self.dict: yield name
[docs] def get(self): """Return dictionary with (name,value) pairs.""" d = {} for name in self: d[name] = self[name] # same as self.dict[name].get() return d
[docs] def name2attr(self): """ Turn all item keys into attributes. Warning: values are copied! __setitem__ and __setattr__ (or properties) must take care of parallel updates. """ for name in self.dict: self.__dict__[name] = self.dict[name].get()
[docs] def __setattr__(self, name, value): """ If name2attr is called, self.m = 2.3 (using this function) is safe, because this also implies update of the corresponding InputPrm-type object in self.dict. """ self.__dict__[name] = value if name in self.dict: self.dict[name].set(value) #if str(a) in self.dict: # self.dict[str(a)].set(value)
[docs] def parse_options(self, argv): """ Examine the command line and for each -opt val pair, set the value of parameter opt to val, if opt is a registered parameter. argv is typically sys.argv[1:] Note that the name of a parameter may contain blanks. A blank is replaced by two underscores in the command-line options. """ p = scitools.misc.cmldict(argv, cmlargs=None, validity=0) # p[key] holds all command-line args, we are only interested # in those keys corresponding to self.dict.keys() for key in p.keys(): if key.find('__') != -1: key_blanks = key.replace('__', ' ') else: key_blanks = key if key_blanks in self.dict: self.dict[key_blanks].set(p[key])
[docs] def usage(self): """Print legal command-line options.""" s = '' # returned message for p in self.dict: if p.find(' ') != -1: opt = p.replace(' ', '__') else: opt = p if self.dict[p].help is not None: s += '--' + '%-30s' % opt + ' ' + self.dict[p].help + '\n' else: s += '--' + opt + ' value ' return s
[docs] def dump(self): s = '' for p in self.dict: s += repr(self.dict[p]) + '\n' return s
[docs] def __str__(self): return str(self.get())
[docs] def __repr__(self): s = 'Parameters: interface="%s"\n' % self._interface for name in self.dict: s += repr(self.dict[name]) + '\n' return s
[docs]def parametersGUI(p, parent, pack_side='top', scrolled={'height': 400, 'width': 350}): """ Load all parameters in a Parameters object p into a GUI. parent parent widget pack_side packing is donw with widget.pack(side=pack_side, expand=1, fill='both') scrolled False: use standard Tk Frame non-empty dict: use Pmw.ScrolledFrame with the prescribed height and width """ p.endadd() # for safety if scrolled: frame = Pmw.ScrolledFrame(parent, usehullsize=1, hull_width=scrolled['width'], hull_height=scrolled['height']) frame.pack(side=pack_side, fill='both', expand=1) frame = frame.interior() else: frame = Tkinter.Frame(parent, borderwidth=2) frame.pack(side=pack_side, fill='both', expand=1) widgets = [] # for alignment for obj in p.parameters_sequence: # must be set on beforehand: obj.widget_type = 'entry' if obj.widget_type is None: raise TypeError("widget_type attribute "\ "must be set for InputPrmGUI '%s'" % obj.name) obj.parent = frame obj.make_widget() #obj.widget.pack(side='top', padx=5, pady=5, fill='x', expand=1) obj.widget.pack(side='top', padx=5, pady=3, anchor='w') if obj.widget_type == 'entry' or \ obj.widget_type == 'option': widgets.append(obj.widget) Pmw.alignlabels(widgets) # nice alignment
[docs]class AutoSimVizGUI: """ Organize a set of widgets for input data together with buttons for running a simulator and performing visualizations. The input data are represented by a Parameters object from the ParameterInterface module. The individual parameters in this object are represented as InputPrmGUI instances. The application code creates Parameters object (recall to call addend() after all parameters are registered). The method make_prmGUI takes the Parameters objects, makes the associated widgets and packs them in an appropriate GUI. All widgets may appear in one column, in the order the parameters were registered in the Parameters object, if sort_widgets is false. Otherwise, two column of widgets are made: one with sliders and one with the rest (checkbuttons, entries, options). The sequence of widgets in the latter case is determined by the sequence of registration in the Parameters, e.g., all sliders are grouped in their original sequence, all option menus are grouped in their original sequence, and so on. The method make_buttonGUI creates buttons for simulation and visualization, plus an optional logo and a help button. If more buttons are wanted, one can add these to the button_frame Tkinter.Frame attribute. There is an optional column of widgets with BLT graphs for curve plotting, enabled by the make_curveplotGUI method. The great advantage of this class is that the application code can concentrate on defining input parameters to a problem, the simulation and visualization functions, and leave it to this class to put everything together. It is then an easy task to change the layout of the whole GUI in one common place. """
[docs] def __init__(self): import modulecheck modulecheck.exception("Class AutoSimVizGUI", 'Pmw', 'Tkinter') return
[docs] def make_prmGUI(self, parent, parameters, sort_widgets=0, height=None, pane=0 ): """ The height parameter controls the height (in pixels) of the GUI. The columns are realized by Pmw.ScrolledFrame widgets. """ self.p = parameters # scitools.ParameterInterface.Parameters instance self.p.endadd() if sort_widgets: self.sliders_sequence = self.p.sliders_sequence else: self.sliders_sequence = None if sort_widgets: self.entries_sequence = self.p.entries_sequence else: self.entries_sequence = None if sort_widgets: self.options_sequence = self.p.options_sequence else: self.options_sequence = None if sort_widgets: self.checkbuttons_sequence = self.p.checkbt_sequence else: self.checkbuttons_sequence = None if not sort_widgets: self.parameters_sequence = self.p.parameters_sequence else: self.parameters_sequence = None self.master = parent self.top = Tkinter.Frame(self.master) self.top.pack(expand=1, fill='both') # could be moved to pack method... self.top_columns = Tkinter.Frame(self.top) self.top_columns.pack(expand=1, fill='both') self.pane = pane if self.pane: self.top_pane = Pmw.PanedWidget(self.top_columns, orient='horizontal') ### hull_width=900, hull_height=600) self.top_pane.pack(expand=1, fill='both') if self.sliders_sequence is not None: # create a scrolled frame with a set of slider widgets # below each other: if self.pane: self.top_pane.add('sliders', min=340) parent = self.top_pane.pane('sliders') else: parent = self.top_columns self.sliders_frame = Pmw.ScrolledFrame(parent, usehullsize=1, hull_width=320, hull_height=height) self.sliders_frame.pack(side='left', fill='both', expand=1) self.sliders_frame = self.sliders_frame.interior() for obj in self.sliders_sequence: if obj.widget_type is None: obj.widget_type = 'slider' obj.parent = self.sliders_frame obj.make_widget() obj.widget.pack(side='top', padx=5, pady=3, fill='x', expand=1) if self.entries_sequence is not None or \ self.options_sequence is not None or \ self.checkbuttons_sequence is not None: if self.pane: self.top_pane.add('entries', min=175) parent = self.top_pane.pane('entries') else: parent = self.top_columns self.entries_frame = Pmw.ScrolledFrame(parent, usehullsize=1, hull_width=240, hull_height=height) self.entries_frame.pack(side='left', fill='both', expand=1) self.entries_frame = self.entries_frame.interior() widgets = [] if self.entries_sequence is not None: for obj in self.entries_sequence: if obj.widget_type is None: obj.widget_type = 'entry' obj.parent = self.entries_frame obj.make_widget() obj.widget.pack(side='top', padx=5, pady=3, fill='x', expand=1, anchor='w') widgets.append(obj.widget) # add option menus under the entries in the second column: if self.options_sequence is not None: for obj in self.options_sequence: if obj.widget_type is None: obj.widget_type = 'option' obj.parent = self.entries_frame obj.make_widget() obj.widget.pack(side='top', padx=5, pady=3, anchor='w') widgets.append(obj.widget) Pmw.alignlabels(widgets) # nice alignment # add checkbuttons under the options/entries in the second column: if self.checkbuttons_sequence is not None: for obj in self.checkbuttons_sequence: if obj.widget_type is None: obj.widget_type = 'checkbutton' obj.parent = self.entries_frame obj.make_widget() obj.widget.pack(side='top', padx=5, pady=3, anchor='w') if self.parameters_sequence is not None: if self.pane: self.top_pane.add('parameters', min=350) parent = self.top_pane.pane('parameters') else: parent = self.top_columns if height is None: height = min(len(self.parameters_sequence)*80, 700) parametersGUI(self.p, parent, pack_side='left', scrolled={'height':height, 'width':350}) # note: if we use Pmw.ScrolledCanvas, call canvas.resizescrollregion()
[docs] def make_buttonGUI(self, parent, buttons=[], logo=None, help=None): if self.pane: #self.top_pane.add('buttons', min=300) self.top_pane.add('buttons') parent = self.top_pane.pane('buttons') else: parent = self.top_columns self.button_frame = Tkinter.Frame(parent) self.button_frame.pack(side='left') # put a help button first: if type(help) is type(""): # description given? self.description = help Tkinter.Button(self.button_frame, text="Help", width=10, command=self._helpwindow).\ pack(side='top', padx=5, pady=3, anchor="n") if logo is not None: self.logo = Tkinter.PhotoImage(file=logo) Tkinter.Label(self.button_frame, image=self.logo).\ pack(side='top', pady=20) if buttons: for button_name, func in buttons: width = max(len(button_name), 10) Tkinter.Button(self.button_frame, text=button_name, width=width, command=func).\ pack(side='top', padx=5, pady=3) self.master.bind('<q>', self._quit) if self.pane: self.top_pane.setnaturalsize()
[docs] def make_curveplotGUI(self, parent, no_of_plotframes=1, placement='right', ): """ @param parent: parent (master) widget @param no_of_plotframes: no of graph areas @param placement: placement of the plot area ('right' or 'bottom') Example on creating three plot areas to the right in the window:: self.plot1, self.plot2, self.plot3 = \ self.someGUI.make_curveplotGUI(parent, 3, 'right') self.plot1 etc. holds Pmw.Blt.Graph widgets. Create a single plot area:: self.plot1 = self.someGUI.make_curveplotGUI(parent, 1, 'bottom') """ if placement == 'right': if self.pane: self.top_pane.add('plot', size=300) parent = self.top_pane.pane('plot') else: parent = self.top_columns self.plotframe = Tkinter.Frame(parent) self.plotframe.pack(side='left', expand=1, fill='both') # size of plot canvas: width=400; total_height=500 elif placement == 'bottom': self.plotframe = Tkinter.Frame(self.top) self.plotframe.pack(side='bottom', expand=1, fill='both') # size of plot canvas: width=None; total_height=no_of_plotframes*200 height=int(total_height/float(no_of_plotframes)) self.g = [] for i in range(no_of_plotframes): try: self.g.append(Pmw.Blt.Graph(self.plotframe, width=width,height=height)) except: print "Python is not linked with Blt"; sys.exit(1) # place the plot areas below each other: self.g[i].pack(side='top',expand=1, fill='both') # some dictionaries with self.g[i] as keys: self.data = {} # holds (x,y) Blt vectors in a graph self.identifier = {} # holds curve identifiers (numbers) for each graph for graph in self.g: self.identifier[graph] = 0 # when a new curve is drawn, the self.nsavecurves old ones # are still present self.nsavecurves = 2 self.curvecolors = ('red', 'blue', 'green', 'yellow', 'black') if self.pane: self.top_pane.setnaturalsize() return self.g
[docs] def load_curveplot(self, filename, graph, curvename=''): """ Load data from a two-column file into x and y Blt vectors. graph is a Pmw.Blt.Graph widget, normally returned from make_curveplotGUI. x, y = self.someGUI.load_curveplot('my.dat', self.plot2, curvename='measured data') One can convert x and y, which are plain Python lists, to NumPy arrays for further processing if desired. """ if isinstance(graph, (list,tuple)): if len(graph) != 1: raise TypeError( 'graph argument is a list of length %d>1, should be scalar' %\ len(graph)) else: graph = graph[0] f = open(filename, 'r') self.identifier[graph] += 1 # identifiers are integers id = self.identifier[graph] # current storage index in an array [0,..,self.nsavecurves+1] counter = id % (self.nsavecurves+1) # The Blt vectors cannot be local variables, because the plot # disappears when the vectors go out of scope. # Letting the user handle these objects results in more user # code. On the other hand, we do not know how many vectors # we need. Remedy: use a dict. with graph as key and # one (x,y) pair of Blt vectors if graph not in self.data: self.data[graph] = {} self.data[graph][counter] = {} self.data[graph][counter]['x'] = Pmw.Blt.Vector() self.data[graph][counter]['y'] = Pmw.Blt.Vector() for line in f: numbers = line.split() self.data[graph][counter]['x'].append(float(numbers[0])) self.data[graph][counter]['y'].append(float(numbers[1])) f.close() # remove an old curve (save the last self.nsavecurves curves): id_old = id - self.nsavecurves - 1 if graph.element_exists(str(id_old)): graph.element_delete(str(id_old)) # remove old curve # dash the old remaining curves: for i in range(max(id_old+1,1),id): graph.element_configure(str(i), linewidth=1) color = self.curvecolors[counter] graph.line_create(str(id), label='', xdata=self.data[graph][counter]['x'], ydata=self.data[graph][counter]['y'], linewidth=2, dashes='', symbol='', color=color) # drop label, use title instead graph.configure(title=curvename) self.master.update() return self.data[graph][counter]['x'],\ self.data[graph][counter]['y']
[docs] def update_curveplot(self, filename, graph): """Update Blt vectors with data from a two-column file.""" id = self.identifier[graph] counter = id % (self.nsavecurves+1) f = open(filename, 'r') lines = f.readlines() if len(lines) != len(self.data[graph][counter]['x']): print "Blt vector has length=%d, but %s has %d lines" % \ (len(self.data[graph][counter]['x']),len(lines)) for i in range(len(self.data[graph][counter]['x'])): self.data[graph][counter]['x'][i], \ self.data[graph][counter]['y'][i] = \ map(float, lines[i].split()) f.close()
def _quit(self, event=None): self.master.destroy() def _helpwindow(self): """ Launch a separate toplevel window with a scrolled text widget containing self.description. """ # read file into a text widget in a _separate_ window: self.filewindow = Tkinter.Toplevel(self.master) # new window lines = self.description.split('\n') nlines = min(len(lines),30) width = min(max([len(i) for i in lines]), 70) # max line width self.filetext = Pmw.ScrolledText(self.filewindow, borderframe=5, # a bit space around the text vscrollmode='dynamic', hscrollmode='dynamic', labelpos='n', label_text="Description", text_width=width, text_height=nlines, text_wrap='none') self.filetext.pack() self.filetext.insert('end', self.description) # add a quit button: Tkinter.Button(self.filewindow, text="Quit", command=self.filewindow.destroy).pack(pady=10)
[docs]class AutoSimVizCGI: """ Organize a set of form variables for input data. """
[docs] def __init__(self): return
[docs] def make(self, form, parameters, CGI_script, imagefile=None, ): """ Create an HTML page consisting of an optional image (specified by imagefile), a table of form variables (specified by parameters (scitools.ParameterInterface.Parameters)), and a "simulate and visualize" button. The resulting visualization part must be created after calling this function. Finally, the HTML page needs a footer (see the footer function). """ self.p = parameters self.p.endadd() s = """ <html><body bgcolor="white"> """ if imagefile is not None: s += """<img src="%s" align="right"><p>""" % imagefile s += """ <form action="%s" method="post"> <table> """ % CGI_script # should we have a help and/or dimension column? help = 0; unit = 0 for p in self.p.parameters_sequence: if p.unit is not None: unit = 1 if p.help is not None: help = 1 for p in self.p.parameters_sequence: s += '<tr>\n<td>%s</td><td>%s</td>' % \ (p.name, p.make_form_entry()) if unit: if p.unit is not None: s += '<td>%s</td>' % p.unit else: s += '<td></td>' # empty if help: if p.help is not None: s += '<td>(%s)</td>' % p.help else: s += '<td></td>' # empty s += '\n</tr>\n' s += """ </table><br> <input type="submit" value="simulate and visualize" name="sim"> </form> """ # perform simulation and visualization as next step #return s print s
[docs] def footer(self): """Write out HTML footer instructions.""" s = """\n</body></html>\n""" #return s print s
def _test1_Parameters(): d = {'A': 1.0, 'w': 0.2, 'func': 'siny', 'y0': 0.0} p = Parameters(interface='GUI', prm_dict=d) p['w'] = 0.1 p.add('tstop', 2.0, widget_type='slider', values=(0,10)) p.add('plot', False) d = p.get() print d print repr(p) p['plot'] = True for name in p: print 'p[%s]=%s' % (name, p[name]) return p def _test1_Parameters_wGUI(): parent = Tkinter.Tk() Pmw.initialise(parent) import scitools.misc scitools.misc.fontscheme1(parent) p = _test1_Parameters() parametersGUI(p, parent, scrolled=False) def get(): print p.get() Tkinter.Button(parent, text='Dump', command=get).pack(pady=10) Tkinter.Button(parent, text='Quit', command=parent.quit).pack(pady=10) parent.mainloop() if __name__ == '__main__': cmd = sys.argv[1] + '(' + ' '.join(sys.argv[2:]) + ')' print cmd exec(cmd) """ Notebook for selecting functions. """ import types from scitools.numpyutils import seq, wrap2callable, ndarray, pi from scitools.StringFunction import StringFunction
[docs]class FuncSpec: """ Specification of a function. Lists of such specifications can be fed to class FunctionSelector to form a notebook where each page is designed according to the contents of a FuncSpec object. """
[docs] def __init__(self, representation, name='', parameters=None, independent_variables=[], formula=None, image=None, function_object=None, vector = 0, description=None, xcoor=None, scrolled_frame=False, ): """ Arguments: @param representation: class Drawing, UserFunction, or StringFormula @param name: name of function @param parameters: parameters in the function, either dict or Parameters instance @param independent_variables: list/tuple of strings with the names of the indep. variables. @param formula: textual doc of function formula @param image: filename of GIF image (LaTeX) @param function_object: callable object for evaluating the function @param vector: 0: scalar function, >0: no of vector comp. @param description: more verbose description than formula @param xcoor: array of coordinates for drawing @param scrolled_frame: scrollbars in the notebook page, False or dict: {'width': 300, 'height':200} Examples: see test_FunctionSelector in TkGUI.py. """ self.name = name self.representation = representation if not self.name: raise ValueError('name keyword must be set when creating a '\ 'FuncSpec object') self.configure( parameters=parameters, independent_variables=independent_variables, formula=formula, image=image, function_object=function_object, vector=vector, description=description, xcoor=xcoor, scrolled_frame=scrolled_frame)
[docs] def configure(self, **kwargs): if 'parameters' in kwargs: self.parameters = kwargs['parameters'] if self.parameters is not None: if isinstance(self.parameters, dict): self.parameters = \ Parameters(interface='GUI', prm_dict=self.parameters) if not isinstance(self.parameters, Parameters): raise TypeError( 'parameters must be a dictionary or Parameters object, '\ 'not a %s' % type(self.parameters)) if 'independent_variables' in kwargs: self.independent_variables = kwargs['independent_variables'] if 'formula' in kwargs: self.formula = kwargs['formula'] if 'image' in kwargs: self.image = kwargs['image'] if 'function_object' in kwargs: self.function_object = kwargs['function_object'] if type(self.function_object) == types.ClassType: raise TypeError( 'class type, not instance, provided as '\ 'function_object for %s' % self.name) if 'vector' in kwargs: self.vector = kwargs['vector'] if 'description' in kwargs: self.description = kwargs['description'] if 'xcoor' in kwargs: self.xcoor = kwargs['xcoor'] if 'scrolled_frame' in kwargs: self.scrolled_frame = kwargs['scrolled_frame'] self.ok() # check validity of arguments
[docs] def ok(self): if not isinstance(self.independent_variables, (list, tuple)): raise TypeError( 'independent_variables must be list or tuple, not %s' % \ type(self.independent_variables)) if self.formula is not None: if not isinstance(self.formula, basestring): raise TypeError( 'formula must be string, not %s' % type(self.formula)) if self.image is not None: if not isinstance(self.image, basestring): raise TypeError( 'image must be string (filename), not %s' % \ type(self.image)) if not os.path.isfile(self.image): raise ValueError('file %s not found' % self.image) if not isinstance(self.vector, int): raise TypeError( 'vector must be int (0=scalar, >=1: no of vector comp.), '\ 'not %s' % type(self.vector)) if self.description is not None: if not isinstance(self.description, basestring): raise TypeError( 'description must be string, not %s' % \ type(self.description)) if self.xcoor is not None: if not isinstance(self.xcoor, ndarray): raise TypeError( 'xcoor must be a NumPy array, not %s' % type(self.xcoor)) if self.scrolled_frame != False: if not isinstance(self.scrolled_frame, dict): raise TypeError('scrolled_frame must be True or dict, '\ 'not %s' % type(self.scrolled_frame))
[docs] def get_independent_variables(self): if not self.independent_variables: raise ValueError('FuncSpec for "%s" has no list of independent '\ 'variables' % self.name) text = 'independent variable' if len(self.independent_variables) > 1: text += 's' text += ': ' + ', '.join(self.independent_variables) return text
[docs] def __repr__(self): args = [] for key in self.__dict__: if self.__dict__[key] is not None: args.append('%s=%s' % (key,self.__dict__[key])) return 'FuncSpec(' + ', '.join(args) + ')'
[docs]class StringFormula:
[docs] def __init__(self, parent, func_spec): self.fspec = func_spec self.master = parent self.top = Tkinter.Frame(parent, borderwidth=2) self.top.pack(side='top') # note that StringFunction works for scalar and vector fields! # just use [formula_x, formula_y] self.formula = Tkinter.StringVar() if self.fspec.formula is not None: self.formula.set(self.fspec.formula) self.widget = Pmw.EntryField(self.top, labelpos='n', label_text=self.fspec.get_independent_variables(), entry_width=15, entry_textvariable=self.formula) self.widget.pack(pady=5) if self.fspec.parameters: parametersGUI(self.fspec.parameters, self.top, scrolled=self.fspec.scrolled_frame)
[docs] def get(self): """Return function object.""" f = StringFunction(self.formula.get(), independent_variables=self.fspec.independent_variables) if self.fspec.parameters: # turn parameters object into dictionary and send # this as keyword arguments to StringFunction.set_parameters f.set_parameters(**self.fspec.parameters.get()) return wrap2callable(f)
[docs]class UserFunction:
[docs] def __init__(self, parent, func_spec): self.fspec = func_spec self.master = parent self.top = Tkinter.Frame(parent, borderwidth=2) self.top.pack(side='top') Tkinter.Label(self.top, text=self.fspec.get_independent_variables()).pack() if self.fspec.formula: width = min(len(self.fspec.formula)+5, 30) Tkinter.Label(self.top, text=self.fspec.formula, width=width).pack() else: print 'Warning: UserFunction, name=%s, has no formula!' % \ self.fspec.name Tkinter.Label(self.top, text='no function expression known').pack() if self.fspec.parameters: print self.fspec.parameters, type(self.fspec.parameters) Tkinter.Label(self.top, text='parameters: ' + ', '.join(self.fspec.parameters.keys())).pack() if self.fspec.image is not None: self.image = Tkinter.PhotoImage(file=self.fspec.image) Tkinter.Label(self.top, image=self.image).pack() # widgets for setting parameters: if self.fspec.parameters: parametersGUI(self.fspec.parameters, self.top, pack_side='top',scrolled=self.fspec.scrolled_frame)
[docs] def get(self): """Return function object.""" # extract parameter values from the GUI? if self.fspec.parameters: d = self.fspec.parameters.get() # dict of (name,value) pairs for name in d: try: f = self.fspec.function_object except: raise AttributeError( 'FuncSpec "%s" used in UserFunction has '\ 'no function_object set' % self.fspec.name) if hasattr(f, name): setattr(f, name, d[name]) else: raise NameError('expected parameter name %s '\ 'as attribute in function object '\ '\n(dir(function object)=%s)' % \ (name,dir(f))) return wrap2callable(self.fspec.function_object)
[docs]class Drawing(UserFunction):
[docs] def __init__(self, parent, func_spec): UserFunction.__init__(self, parent, func_spec) if self.fspec.xcoor is None: raise ValueError('want DrawFunction widget, but no xcoor info'\ ' in the FuncSpec object') self.drawing = DrawFunction(self.fspec.xcoor, self.top) self.drawing.pack(padx=10, pady=10)
[docs] def get(self): """Return function object.""" x, y = self.drawing.get() d = wrap2callable((x,y)) # The drawing function d may be combined with another # expression, forming a new function object. This functionality # is in a method 'embed' in self.func_spec.function_object. try: f = UserFunction.get(self) f.attach_func(d) d = f except: pass # no combination with other functions registered return d
[docs]class FunctionChoices: """ Notebook for various representations of a function. The representations appear as pages. Each page is realized as a UserFunction, StringFormula, or Drawing instance. """
[docs] def __init__(self, parent, func_spec_list): self.master = parent self.top = Tkinter.Frame(self.master, borderwidth=2) self.top.pack(expand=True, fill='both') self.nb = Pmw.NoteBook(self.top) self.func_spec_list = func_spec_list # list of FuncSpec objects # hold UserFunction, Drawing, or StringFormula objects, # one for each page (key is page name): self.page = {} for f in self.func_spec_list: # define a page: new_page = self.nb.add(f.name, tab_text=f.name) # group is a kind of frame widget with a solid border: group = Pmw.Group(new_page, tag_text=f.name) group.pack(fill='both', expand=1, padx=10, pady=10) # build contents in current page: self.page[f.name] = \ f.representation(group.interior(), f) self.nb.pack(padx=5, pady=5, fill='both', expand=1) self.nb.setnaturalsize()
[docs] def get(self): """ Return initialized function object corresponding to the currently selected notebook page. """ # get user-chosen page name: current = self.nb.getcurselection() # get corresponding function object (self.page[current] # is a UserFunction, Drawing, or StringFunction instance): f = self.page[current].get() #from scitools.misc import dump #dump(f, hide_nonpublic=False) return f, current
[docs]class FunctionSelector: """ Notebook with a collection of functions to be specified. Each function is represented by a FunctionChoices page. This page is again a notebook with pages corresponding to different ways of specifying a function: drawing, string formulas, ready-made formulas with free parameters, hardcoded Python functions etc. """
[docs] def __init__(self, parent): self.master = parent self.top = Tkinter.Frame(self.master, borderwidth=2) self.top.pack(expand=True, fill='both') self.nb = Pmw.NoteBook(self.top) self.page = {} # FunctionChoices widgets
[docs] def add(self, name, func_spec_list): new_page = self.nb.add(name, tab_text=name) group = Tkinter.Frame(new_page, borderwidth=2) group.pack(expand=True, fill='both') w = FunctionChoices(group, func_spec_list) self.page[name] = w
[docs] def pack(self, **kwargs): # pack notebook: self.nb.pack(padx=5, pady=5, fill='both', expand=1, **kwargs) self.nb.setnaturalsize()
[docs] def select(self, name, page): """Select page under the name tab.""" self.page[name].nb.selectpage(page)
[docs] def get(self, name): """ Return initialized function object corresponding to the page with the given name. """ return self.page[name].get() #from scitools.misc import dump #dump(f, hide_nonpublic=False)
[docs]def test_FunctionChoices(root): class MyFunc: def __init__(self, a, b): self.a = a; self.b = b def __call__(self, q, t): return self.a*q + self.b*t F = FuncSpec nb = [F(Drawing, name='k coeff', xcoor=seq(0,1,0.01)), F(StringFormula, name='velocity', parameters={'A': 1, 'B': 1, 'p': 1, 'q': 1}, formula='[-B*cos(q*y), A*sin(p*x)]', # vector field independent_variables=('x', 'y'), vector=2), F(UserFunction, name='bc', parameters=('a', 'b'), formula='a*q + b*t', independent_variables=('q', 't'), function_object=MyFunc(0,0)), ] print nb gui = FunctionChoices(root, nb) Tkinter.Button(root, text='get', # note that gui is local so obj=gui is needed to # remember the gui object... command=gui.get).pack(pady=5) root.mainloop()
def _test_FunctionSelector(root): """Two-level notebook. Top level: f, I, bc.""" s = FunctionSelector(root) class MovingSource1: """Function object: A*exp(-(x - x0 - sin(w*t))**2).""" def __init__(self, A, w, x0): self.A = A; self.w = w; self.x0 = x0 def __call__(self, x, t): return self.A*exp(-(x - self.x0 - sin(self.w*t))**2) def __str__(self): return 'A*exp(-(x - x0 - sin(w*t))**2)' def parameters(self): return {'A': self.A, 'w': self.w, 'x0': self.x0} def growing_source(x, t): A = 1; w = 0.1; x0 = 5 return A*(sin(w*t))**2*exp(-(x-x0)**2) class MovingSource2: """ As MovingSource1, but let the user specify (through a drawing, for instance) the spatial shape f: f(x - x0 - sin(w*t)). """ def __init__(self, w, x0): self.w = w; self.x0 = x0 self.spatial_shape = lambda x: exp(-x*x) def attach_func(self, spatial_shape): self.spatial_shape = spatial_shape def __call__(self, x, t): return self.spatial_shape(x - self.x0 - sin(self.w*t)) def __str__(self): return 'f(x - x0 - sin(w*t))' def parameters(self): return {'w': self.w, 'x0': self.x0} ms1 = MovingSource1(1, 1, 5) ms2 = MovingSource2(1, 5) F = FuncSpec # short form f = [F(UserFunction, name='moving source 1', independent_variables=('x', 't'), formula=str(ms1), function_object=ms1, parameters=ms1.parameters()), F(UserFunction, name='growing source 1', independent_variables=('x', 't'), formula='A*(sin(w*t))**2*exp(-(x-x0)**2); A=1, w=0.1', function_object=growing_source), F(Drawing, name='moving source 2', independent_variables=('x', 't'), description='spatial shape f(x) can be drawn', function_object=ms2, parameters=ms2.parameters(), formula=str(ms2), xcoor=seq(0,10,0.1),), F(StringFormula, name='growing source 2', parameters={'A': 1.0, 'w': 1.0, 'x0': 0}, formula='A*(sin(w*t))**2*exp(-(x-x0)**2)', independent_variables=('x', 't')), F(UserFunction, name='no source', independent_variables=('x', 't'), formula='f(x,t)=0', function_object=lambda x,t: 0), ] s.add('f', f) class GaussianBell: """Gaussian Bell at x0 with st.dev. sigma.""" def __init__(self, x0, sigma): self.x0 = x0; self.sigma = sigma def __call__(self, x): return exp(-0.5*((x-self.x0)/self.sigma)**2) def __str__(self): return 'exp(-0.5*((x-x0)/sigma)**2)' def parameters(self): return {'x0': self.x0, 'sigma': self.sigma} gb = GaussianBell(5,1) I = [F(UserFunction, name='localized disturbance', function_object=gb, independent_variables=['x'], parameters=gb.parameters()), F(Drawing, name='draw initial shape', independent_variables=['x'], xcoor=seq(0,10,0.1),) ] s.add('initial condition', I) class OscHalfPeriod: """Oscillate sin(w*t) half a period, then hold zero.""" def __init__(self, w): self.w = w def __call__(self, t): T = pi/self.w if t <= T: return sin(self.w*t) else: return 0.0 def __str__(self): return 'sin(w*t) for t<pi/w, otherwise 0' def parameters(self): return {'w': self.w} half_period = OscHalfPeriod(pi/2) bc = [F(UserFunction, name='1 period oscillation', independent_variables=('t',), function_object=half_period, formula=str(half_period), parameters=half_period.parameters()), F(UserFunction, name='fixed ends', independent_variables=('t',), function_object=lambda x: 0, formula='u=0 at the ends',), ] s.add('boundary conditions', bc) s.pack() def get(): f_func, page = s.get('f') I_func, page = s.get('initial condition') bc_func, page = s.get('boundary conditions') from scitools.misc import dump print 'f_func:' dump(f_func, hide_nonpublic=False) print 'I_func:' dump(I_func, hide_nonpublic=False) print 'bc_func:' dump(bc_func, hide_nonpublic=False) Tkinter.Button(root, text='get', command=get).pack(pady=5) root.mainloop() def _FunctionSelector_test(): root = Tkinter.Tk() Pmw.initialise(root) import scitools.misc scitools.misc.fontscheme6(root) root.title('FunctionSelector notebook demo') _test_FunctionSelector(root)
[docs]class FuncDependenceViz: """ Visualization of the shape of a function depends continuously on its parameters, and this class makes a graphical illustration of this dependence. """
[docs] def __init__(self, master, parameter_intervals={}, # interval for each prm functions={}, # functions to be plotted xmin=0.0, xmax=1.0, # x axis range resolution=101, # no of x evaluations width=500, height=400, # size of plot window viztool = 'Pmw.Blt.Graph', # or 'gnuplot' plot_update = 'after' # how slider movements # update the plots ): """ Define a set of functions depending on a set of parameters. This class creates a GUI where the parameters can be adjusted, and the effect on the function graphs can be seen immediately. """ import scitools.modulecheck as sm sm("TkGUI module:", 'Pmw', 'Tkinter', 'Gnuplot', 'numpy') Gnuplot = import_module('Gnuplot') self.Gnuplot = Gnuplot self.master = master self.top = Tkinter.Frame(master, borderwidth=2) self.top.pack() # could leave this pack for a pack class function self.p_intervals = parameter_intervals self.funcs = functions # f_i(x; p_1,...p_n) self.p = {} # values of the parameters self.slider_var = {} # Tkinter vars for slide.p for pname in self.p_intervals: # set parameter value to midpoint in interval: self.p[pname] = (self.p_intervals[pname][0] + \ self.p_intervals[pname][1])/2.0 self.slider_var[pname] = Tkinter.DoubleVar() self.slider_var[pname].set(self.p[pname]) # define the sliders: for pname in self.p_intervals: pmin = self.p_intervals[pname][0] pmax = self.p_intervals[pname][1] slider = Tkinter.Scale(self.top, orient='horizontal', from_=pmin, to=pmax, tickinterval=(pmax-pmin)/10.0, resolution=(pmax-pmin)/100.0, label=pname, font="helvetica 10 bold", length=width-100, variable=self.slider_var[pname]) slider.pack(side='top', pady=4) # we can update the plot according to slider # movements in two ways: during movement # (command= option) or after movement (event binding) if plot_update == 'after': slider.bind('<ButtonRelease-1>', self.visualize) else: slider.configure(command=self.visualize) # does not work: slider.bind('<B1-Motion>', self.visualize) # define a widget row where xmin/xmax and n can be adjusted: self.xmin = Tkinter.DoubleVar(); self.xmin.set(xmin) self.xmax = Tkinter.DoubleVar(); self.xmax.set(xmax) self.n = Tkinter.IntVar(); self.n.set(resolution) row = Tkinter.Frame(self.top, borderwidth=2) row.pack() Tkinter.Label(row, text="x min:").pack(side='left') Tkinter.Entry(row, textvariable=self.xmin, width=5, justify='right').pack(side='left') Tkinter.Label(row, text=" x max:").pack(side='left') Tkinter.Entry(row, textvariable=self.xmax, width=5, justify='right').pack(side='left') Tkinter.Label(row, text=" no of points:").pack(side='left') Tkinter.Entry(row, textvariable=self.n, width=3, justify='right').pack(side='left') # make graph widget or use a plotting program? try: # see if we can create a BLT graph successfully: self.g = Pmw.Blt.Graph(self.top, width=width, height=height) have_blt = 1 except: have_blt = 0 self.viztool = viztool # user-specified plotting tool print have_blt, viztool if have_blt and viztool == "Pmw.Blt.Graph": self.g.pack(expand=1, fill='both') else: # we do not have BLT or the user has not requested BLT: self.g = self.Gnuplot.Gnuplot(persist=1) self.dx = 0 # new vectors must be made if the x incr. changes self.make_vectors() # vectors for x and y values in plot # PostScript plot: Tkinter.Button(row, text="Postscript plot", command=self.psdump).pack(side='left',padx=5) # bind 'p' to dumping the plot in PostScript: # (must bind to master, not self.top) self.master.bind('<p>', self.psdump) self.master.bind('<q>', self.quit) # convenient
[docs] def psdump(self, event=None): import tkFileDialog fname = tkFileDialog.SaveAs( filetypes=[('psfiles','*.ps')], initialfile="tmp.ps", title="Save plot in PostScript format").show() if fname: if self.viztool == "gnuplot": self.g.hardcopy(filename=fname, enhanced=1, mode='eps', color=0, fontname='Times-Roman', fontsize=28) elif self.viztool == "Pmw.Blt.Graph": self.g.postscript_output(fileName=fname, decorations='no')
[docs] def quit(self, event=None): "kill plot window" self.master.destroy()
[docs] def make_vectors(self): "make x vector and a dictionary of y vectors" # self.x : vector of x coordinates # self.y[funcname] : vector of function values dx = (self.xmax.get() - self.xmin.get())/\ float(self.n.get() - 1) if dx != self.dx: self.dx = dx # x increment has changed, make new vectors: # add dx/2 to upper limit to ensure self.n entries: x = arange(self.xmin.get(), self.xmax.get()+dx/2, dx, float) if x.shape[0] != self.n.get(): raise IndexError("x has wrong length") self.x = x self.y = {} for funcname in self.funcs: self.y[funcname] = zeros(x.shape[0],float) if self.viztool == "Pmw.Blt.Graph": self.bind_vectors2BLTgraph() # fill the vectors with appropriate data for testing: self.fill_vectors()
[docs] def bind_vectors2BLTgraph(self): "bind vectors to the curves in the BLT graph" # each curve has its own color: colors = ['red','blue','green','black','grey', 'black','yellow','orange'] if len(self.funcs) > len(colors): print "Cannot handle more than %d functions"\ % len(self.funcs); sys.exit(1) color_counter = 0 for curvename in self.funcs: if self.g.element_exists(curvename): self.g.element_delete(curvename) self.g.line_create( curvename, # used as identifier xdata=tuple(self.x), # x coords ydata=tuple(self.y[curvename]), # y coords color=colors[color_counter], linewidth=1, dashes='', # number: dash, "": solid label=curvename, # legend symbol='', # no symbols at data points ) color_counter += 1
[docs] def visualize(self, var): for pname in self.p: self.p[pname] = self.slider_var[pname].get() self.make_vectors() self.fill_vectors() title = "" for pname in self.p: title += "%s=%g " % (pname,self.p[pname]) if self.viztool == "gnuplot": self.g.clear() self.g("set xrange [%g:%g]" % (self.xmin.get(), self.xmax.get())) self.g.title(title) # we do not launch the plot here plots = []; line_counter=1 for funcname in self.funcs: plots.append(self.Gnuplot.Data( self.x, self.y[funcname], with_="line %d" % line_counter)) line_counter += 1 self.g.plot(*tuple(plots)) elif self.viztool == "Pmw.Blt.Graph": # BLT graph commands: self.g.xaxis_configure(min=self.xmin.get(), max=self.xmax.get()) for curvename in self.funcs: self.g.element_configure( curvename, ydata=tuple(self.y[curvename])) self.g.update() self.g.configure(title=title)
[docs] def fill_vectors(self): for funcname in self.funcs: # slow loop over NumPy array... for i in range(self.n.get()): x = self.x[i] self.y[funcname][i] = \ self.funcs[funcname](x, self.p)
def _test_FuncDependenceViz(): import math p_intervals = {'mu': (0,8), 'sigma': (0,1), 'alpha': (0,1)} # recall that lambda is a reserved keyword in Python, # use lambda_ instead: def lognormal(x, lambda_, zeta): if x < 1.0E-9: f = 0.0 else: f = 1/(zeta*math.sqrt(2*math.pi)*x)*math.exp( -0.5*((math.log(x)-lambda_)/zeta)**2) return f def U(x, p): mu = p['mu']; sigma = p['sigma'] zeta = math.sqrt(math.log(1 + (sigma/mu)**2)) lambda_ = math.log(mu) - 0.5*0.5*zeta**2 return lognormal(x, lambda_, zeta) def F(x, p): mu = p['mu']; sigma = p['mu']; alpha = p['alpha'] zeta = math.sqrt(math.log(1 + (sigma/mu)**2)) lambda_ = math.log(mu) - 0.5*0.5*zeta**2 # response modification: lambda_ = math.log(alpha) + 2*lambda_ zeta = 2*zeta return lognormal(x, lambda_, zeta) f = { 'U': U, 'F': F } # function names and objects root = Tkinter.Tk() Pmw.initialise(root) try: viztool = sys.argv[1] except: viztool = 'gnuplot' # alternative: Pmw.Blt.Graph try: update = sys.argv[2] except: update = 'after' # alternative: arbitrary (cont. update) gui = FuncDependenceViz(root, p_intervals, f, xmin=0, xmax=8, resolution=40, viztool=viztool, plot_update=update) root.mainloop() def _test(): print 'Testing DrawFunction:' _test_DrawFunction() print 'Testing CanvasCoords:' _CanvasCoords_test() print 'Testing FunctionSelector:' _FunctionSelector_test() print 'Testing ParameterInterface:' _test1_Parameters() _test1_Parameters_wGUI() print 'Testing FuncDependenceViz:' _test_FuncDependenceViz() if __name__ == '__main__': _test()