From b17f75ebc880f78a43a5e434382625615ad7c34a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 3 Aug 2010 11:18:54 -0400 Subject: [PATCH] Bring hooke.plugin.tutorial up to date with Plugin/Command architecture. --- hooke/plugin/__init__.py | 3 +- hooke/plugin/tutorial.py | 703 ++++++++++++--------------------------- 2 files changed, 212 insertions(+), 494 deletions(-) diff --git a/hooke/plugin/__init__.py b/hooke/plugin/__init__.py index 54cce66..3d3efd9 100644 --- a/hooke/plugin/__init__.py +++ b/hooke/plugin/__init__.py @@ -48,7 +48,7 @@ PLUGIN_MODULES = [ # ('showconvoluted', True), # ('superimpose', True), # ('tccd', True), -# ('tutorial', True), + ('tutorial', True), ('vclamp', True), ] """List of plugin modules and whether they should be included by @@ -173,7 +173,6 @@ def load_graph(graph, config, include_section): except configparser.NoOptionError: include = True # non-optional include (e.g. a Builtin) enabled[item.name] = include - print item.name, include if include == True: for dependency in node: if enabled.get(dependency.data.name, None) != True: diff --git a/hooke/plugin/tutorial.py b/hooke/plugin/tutorial.py index 3fc9319..a126e2c 100644 --- a/hooke/plugin/tutorial.py +++ b/hooke/plugin/tutorial.py @@ -21,11 +21,17 @@ Hooke plugin, including description of main Hooke internals. """ +import logging +import StringIO + from numpy import arange from ..command import Command, Argument, Failure -from ..playlist import FilePlaylist -from ..plugin import Plugin +from ..config import Setting +from ..interaction import PointRequest, PointResponse +from ..util.si import ppSI, split_data_label +from . import Plugin +from .curve import CurveArgument class TutorialPlugin (Plugin): @@ -77,8 +83,8 @@ class TutorialPlugin (Plugin): # we instantiate them in a list here, and rely on # :meth:`hooke.plugin.Plugin.commands` to return copies of # that list. - self._commands = [] #CommandInit] - + self._commands = [DoNothingCommand(self), HookeInfoCommand(self), + PointInfoCommand(self),] def dependencies(self): """Return a list of names of :class:`hooke.plugin.Plugin`\s we @@ -89,497 +95,210 @@ class TutorialPlugin (Plugin): enabled if it is missing dependencies. """ return ['vclamp'] - #Here we initialize a local configuration variable; see plotmanip_absvalue() for explanation. - self.config['tutorial_absvalue']=0 - - def do_nothing(self,args): - ''' - This is a boring but working example of an actual Hooke command. - A Hooke command is a function of the xxxxCommands class, which is ALWAYS defined - this way: - - def do_nameofcommand(self,args) - - *do_ is needed to make Hooke understand this function is a command - *nameofcommand is how the command will be called in the Hooke command line. - *self is, well, self - *args is ALWAYS needed (otherwise Hooke will crash executing the command). We will see - later what args is. - - Note that if you now start Hooke with this plugin activated and you type in the Hooke command - line "help nothing" you will see this very text as output. So the help of a command is a - string comment below the function definition, like this one. - - Commands usually return None. - ''' - print 'I am a Hooke command. I do nothing.' - - def do_printargs(self,args): - ''' - This command prints the args you give to it. - args is always a string, that contains everything you write after the command. - So if you issue "mycommand blah blah 12345" args is "blah blah 12345". - - Again, args is needed in the definition even if your command does not use it. - ''' - print 'You gave me those args: '+args - - def help_tutorial(self): - ''' - This is a help function. - If you want a help function for something that is not a command, you can write a help - function like this. Calling "help tutorial" will execute this function. - ''' - print 'You called help_tutorial()' - - def do_environment(self,args): - ''' - This plugin contains a panoramic of the Hooke command line environment variables, - and prints their current value. - ''' - - '''self.current_list - TYPE: [ curve.HookeCurve ], len=variable - contains the actual playlist of Hooke curve objects. - Each HookeCurve object represents a reference to a data file. - We will see later in detail how do they work. - ''' - print 'current_list length:',len(self.current_list) - print 'current_list 0th:',self.current_list[0] - - '''self.pointer - TYPE: int - contains the index of - the current curve in the playlist - ''' - print 'pointer: ',self.pointer - - '''self.current - TYPE: curve.HookeCurve - contains the current curve displayed. - We will see later how it works. - ''' - print 'current:',self.current - - '''self.plots - TYPE: [ curve.PlotObject ], len=1,2 - contains the current default plots. - Each PlotObject contains all info needed to display - the plot: apart from the data vectors, the title, destination - etc. - Usually self.plots[0] is the default topmost plot, self.plots[1] is the - accessory bottom plot. - ''' - print 'plots:',self.plots - - '''self.config - TYPE: { string:anything } - contains the current Hooke configuration variables, in form of a dictionary. - ''' - print 'config:',self.config - - '''self.plotmanip - TYPE: [ function ] - Contains the ordered plot manipulation functions. - These functions are called to modify the default plot by default before it is plotted. - self.plots contains the plot passed through the plot manipulators. - We will see it better later. - *YOU SHOULD NEVER MODIFY THAT* - ''' - print 'plotmanip: ',self.plotmanip - - '''self.drivers - TYPE: [ class ] - Contains the plot reading drivers. - *YOU SHOULD NEVER MODIFY THAT* - ''' - print 'drivers: ',self.drivers - - '''self.frame - TYPE: wx.Frame - Contains the wx Frame of the GUI. - ***NEVER, EVER TOUCH THAT.*** - ''' - print 'frame: ',self.frame - - '''self.list_of_events - TYPE: { string:wx.Event } - Contains the wx.Events to communicate with the GUI. - Usually not necessary to use it, unless you want - to create a GUI plugin. - ''' - print 'list of events:',self.list_of_events - - '''self.events_from_gui - TYPE: Queue.Queue - Contains the Queue where data from the GUI is put. - Usually not necessary to use it, unless you want - to create a GUI plugin. - ''' - print 'events from gui:',self.events_from_gui - - '''self.playlist_saved - TYPE: Int (0/1) ; Boolean - Flag that tells if the playlist has been saved or not. - ''' - print 'playlist saved:',self.playlist_saved - - '''self.playlist_name - TYPE: string - Name of current playlist - ''' - print 'playlist name:',self.playlist_name - - '''self.notes_saved - TYPE: Int (0/1) ; Boolean - Flag that tells if the playlist has been saved or not. - ''' - print 'notes saved:',self.notes_saved - - - def do_myfirstplot(self,args): - ''' - In this function, we see how to create a PlotObject and send it to the screen. - ***Read the code of PlotObject in curve.py before!***. - ''' - - #We generate some interesting data to plot for this example. - xdata1=arange(-5,5,0.1) - xdata2=arange(-5,5,0.1) - ydata1=[item**2 for item in xdata1] - ydata2=[item**3 for item in xdata2] - - #Create the object. - #The PlotObject class lives in the curve library. - myplot=lhc.PlotObject() - ''' - The *data* of the plot live in the .vectors list. - - plot.vectors is a multidimensional array: - plot.vectors[0]=set1 - plot.vectors[1]=set2 - plot.vectors[2]=sett3 - etc. - - 2 curves in a x,y plot are: - [[[x1],[y1]],[[x2],[y2]]] - for example: - x1 y1 x2 y2 - [[[1,2,3,4],[10,20,30,40]],[[3,6,9,12],[30,60,90,120]]] - x1 = self.vectors[0][0] - y1 = self.vectors[0][1] - x2 = self.vectors[1][0] - y2 = self.vectors[1][1] - ''' - #Pour 0-th dataset into myplot: - myplot.add_set(xdata1,ydata1) - - #Pour 1-st dataset into myplot: - myplot.add_set(xdata2,ydata2) - - #Add units to x and y axes - #units=[string, string] - myplot.units=['x axis','y axis'] - - #Where do we want the plot? 0=top, 1=bottom - myplot.destination=1 - - '''Send it to the GUI. - Note that you *have* to encapsulate it into a list, so you - have to send [myplot], not simply myplot. - - You can also send more two plots at once - self.send_plot([plot1,plot2]) - ''' - self._send_plot([myplot]) - - - def do_myfirstscatter(self,args): - ''' - How to draw a scatter plot. - ''' - #We generate some interesting data to plot for this example. - xdata1=arange(-5,5,1) - xdata2=arange(-5,5,1) - ydata1=[item**2 for item in xdata1] - ydata2=[item**3 for item in xdata2] - - myplot=lhc.PlotObject() - myplot.add_set(xdata1,ydata1) - myplot.add_set(xdata2,ydata2) - - - #Add units to x and y axes - myplot.units=['x axis','y axis'] - - #Where do we want the plot? 0=top, 1=bottom - myplot.destination=1 - - '''None=standard line plot - 'scatter'=scatter plot - By default, the styles attribute is an empty list. If you - want to define a scatter plot, you must define all other - plots as None or 'scatter', depending on what you want. - - Here we define the second set to be plotted as scatter, - and the first to be plotted as line. - - Here we define also the colors to be the default Matplotlib colors - ''' - myplot.styles=[None,'scatter'] - myplot.colors=[None,None] - self._send_plot([myplot]) - - - def do_clickaround(self,args): - ''' - Here we click two points on the curve and take some parameters from the points - we have clicked. - ''' - - ''' - points = self._measure_N_points(N=Int, whatset=Int) - *N = number of points to measure(1...n) - *whatset = data set to measure (0,1...n) - *points = a list of ClickedPoint objects, one for each point requested - ''' - points=self._measure_N_points(N=2,whatset=1) - print 'You clicked the following points.' - - ''' - These are the absolute coordinates of the - point clicked. - [float, float] = x,y - ''' - print 'Absolute coordinates:' - print points[0].absolute_coords - print points[1].absolute_coords - print - - ''' - These are the coordinates of the points - clicked, remapped on the graph. - Hooke looks at the graph point which X - coordinate is next to the X coordinate of - the point measured, and uses that point - as the actual clicked point. - [float, float] = x,y - ''' - print 'Coordinates on the graph:' - print points[0].graph_coords - print points[1].graph_coords - print - - ''' - These are the indexes of the clicked points - on the dataset vector. - ''' - print 'Index of points on the graph:' - print points[0].index - print points[1].index - - - def help_thedifferentplots(self): - ''' - The *three* different default plots you should be familiar with - in Hooke. - - Each plot contains of course the respective data in their - vectors attribute, so here you learn also which data access for - each situation. - ''' - print ''' - 1. THE RAW, CURRENT PLOTS - - self.current - --- - Contains the current curve.HookeCurve container object. - A HookeCurve object defines only two default attributes: - - * self.current.path = string - The path of the current displayed curve - - * self.current.curve = curve.Driver - The curve object. This is not only generated by the driver, - this IS a driver instance in itself. - This means that in self.current.curve you can access the - specific driver APIs, if you know them. - - And defines only one method: - * self.current.identify() - Fills in the self.current.curve object. - See in the cycling tutorial. - - ***** - The REAL curve data actually lives in: - --- - * self.current.curve.default_plots() = [ libhooke.PlotObject ] - Contains the raw PlotObject-s, as "spitted out" by the driver, without any - intervention. - This is as close to the raw data as Hooke gets. - - One or two plots can be spit out; they are always enclosed in a list. - ***** - - Methods of self.current.curve are: - --- - - * self.current.curve.is_me() - (Used by identify() only.) - - * self.current.curve.close_all() - Closes all driver open files; see the cycling tutorial. - ''' - - print ''' - 2. THE PROCESSED, DEFAULT PLOT - - The plot that is spitted out by the driver is *not* the usual default plot - that is displayed by calling "plot" at the Hooke prompt. - - This is because the raw, driver-generated plot is usually *processed* by so called - *plot processing* functions. We will see in the tutorial how to define - them. - - For example, in force spectroscopy force curves, raw data are automatically corrected - for deflection. Other data can be, say, filtered by default. - - The default plots are accessible in - self.plots = [ libhooke.PlotObject ] - - self.plots[0] is usually the topmost plot - self.plots[1] is usually the bottom plot (if present) - ''' - - print ''' - 3. THE PLOT DISPLAYED RIGHT NOW. - - Sometimes the plots you are displaying *right now* is different from the previous - two. You may have a fit trace, you may have issued some command that spits out - a custom plot and you want to rework that, whatever. - - You can obtain in any moment the plot currently displayed by Hooke by issuing - - PlotObject = self._get_displayed_plot(dest) - * dest = Int (0/1) - dest=0 : top plot - dest=1 : bottom plot - ''' - - - def do_cycling(self,args): - ''' - Here we cycle through our playlist and print some info on the curves we find. - Cycling through the playlist needs a bit of care to avoid memory leaks and dangling - open files... - - Look at the source code for more information. - ''' - - def things_when_cycling(item): - ''' - We encapsulate here everything has to open the actual curve file. - By doing it all here, we avoid to do acrobacies when deleting objects etc. - in the main loop: we do the dirty stuff here. - ''' - - ''' - identify() - - This method looks for the correct driver in self.drivers to use; - and puts the curve content in the .curve attribute. - Basically, until identify() is called, the HookeCurve object - is just an empty shell. When identify() is called (usually by - the Hooke plot routine), the HookeCurve object is "filled" with - the actual curve. - ''' - - item.identify(self.drivers) - ''' - After the identify(), item.curve contains the curve, and item.curve.default_plots() behaves exactly like - self.current.curve.default_plots() -but for the given item. - ''' - itplot=item.curve.default_plots() - - print 'length of X1 vector:',len(itplot[0].vectors[0][0]) #just to show something + def default_settings(self): + """Return a list of :class:`hooke.config.Setting`\s for any + configurable plugin settings. + + The suggested section setting is:: + + Setting(section=self.setting_section, help=self.__doc__) + + You only need to worry about this if your plugin has some + "magic numbers" that the user may want to tweak, but that + won't be changing on a per-command basis. + + You should lead off the list of settings with the suggested + section setting mentioned above. + """ + return [ + # We disable help wrapping, since we've wrapped + # TutorialPlugin.__doc__ ourselves, and it's more than one + # paragraph (textwrap.fill, used in + # :meth:`hooke.config.Setting.write` only handles one + # paragraph at a time). + Setting(section=self.setting_section, help=self.__doc__, + wrap=False), + Setting(section=self.setting_section, option='favorite color', + value='orange', help='Your personal favorite color.'), + ] + + +# Define common or complicated arguments + +# Often, several commands in a plugin will use similar arguments. For +# example, many curves in the 'playlist' plugin need a playlist to act +# on. Rather than repeating an argument definition in several times, +# you can keep your code DRY (Don't Repeat Yourself) by defining the +# argument at the module level and referencing it during each command +# initialization. + +def color_callback(hooke, command, argument, value): + """If `argument` is `None`, default to the configured 'favorite color'. + + :class:`hooke.command.Argument`\s may have static defaults, but + for dynamic defaults, they use callback functions (like this one). + """ + if value != None: + return value + return command.plugin.config['favorite color'] + +ColorArgument = Argument( + name='color', type='string', callback=color_callback, + help="Pick a color, any color.") +# See :func:`hooke.ui.gui.panel.propertyeditor.prop_from_argument` for +# a situation where :attr:`type` is important. + + +class DoNothingCommand (Command): + """This is a boring but working example of an actual Hooke command. + + As for :class:`hooke.plugin.Plugin`\s, the class name is not + important, but :attr:`name` is. :attr:`name` is used (possibly + with some adjustment) as the name for accessing the command in the + various :class:`hooke.ui.UserInterface`\s. For example the + `'do nothing'` command can be run from the command line UI with:: + + hooke> do_nothing + + Note that if you now start Hooke with the command's plugin + activated and you type in the Hooke command line "help do_nothing" + you will see this very text as output. That is because we set + :attr:`_help` to this class' docstring on initialization. + """ + def __init__(self, plugin): + # See the comments in TutorialPlugin.__init__ for details + # about super() and the docstring of + # :class:`hooke.command.Command` for details on the __init__() + # arguments. + super(DoNothingCommand, self).__init__( + name='do nothing', + arguments=[ColorArgument], + help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + """This is where the command-specific magic will happen. + + If you haven't already, read the Architecture section of + :file:`doc/hacking.txt` (also available `online`_). It + explains the engine/UI setup in more detail. + + .. _online: + http://www.physics.drexel.edu/~wking/rsrch/hooke/hacking.html#architecture + + The return value (if any) of this method is ignored. You + should modify the :class:`hooke.hooke.Hooke` instance passed + in via `hooke` and/or return things via `outqueue`. `inqueue` + is only important if your command requires mid-command user + interaction. + + By the time this method is called, all the argument + preprocessing (callbacks, defaults, etc.) have already been + handled by :meth:`hooke.command.Command.run`. + """ + # On initialization, :class:`hooke.hooke.Hooke` sets up a + # logger to use for Hooke-related messages. Please use it + # instead of debugging 'print' calls, etc., as it is more + # configurable. + log = logging.getLogger('hooke') + log.debug('Watching %s paint dry' % params['color']) - ''' - The following three lines are a magic spell you HAVE to do - before closing the function. - (Otherwise you will be plagued by unpredicatable, system-dependent bugs.) - ''' - item.curve.close_all() #Avoid open files dangling - del item.curve #Avoid memory leaks - del item #Just be paranoid. Don't ask. +class HookeInfoCommand (Command): + """Get information about the :class:`hooke.hooke.Hooke` instance. + """ + def __init__(self, plugin): + super(HookeInfoCommand, self).__init__( + name='hooke info', + help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + outqueue.put('Hooke info:') + # hooke.config contains a :class:`hooke.config.HookeConfigParser` + # with the current hooke configuration settings. + config_file = StringIO.StringIO() + hooke.config.write(config_file) + outqueue.put('configuration:\n %s' + % '\n '.join(config_file.getvalue().splitlines())) + # hooke.plugins contains :class:`hooke.plugin.Plugin`\s defining + # :class:`hooke.command.Command`\s. + outqueue.put('plugins: %s' + % ', '.join([plugin.name for plugin in hooke.plugins])) + # hooke.drivers contains :class:`hooke.driver.Driver`\s for + # loading curves. + outqueue.put('drivers: %s' + % ', '.join([driver.name for driver in hooke.drivers])) + # hooke.playlists contains a + # :class:`hooke.playlist.NoteIndexList` of + # :class:`hooke.playlist.Playlist`\s. Each playlist may + # contain several :class:`hooke.curve.Curve`\s representing a + # grouped collection of data. + playlist = hooke.playlists.current() + if playlist == None: return + outqueue.put('current playlist: %s (%d of %d)' + % (playlist.name, + hooke.playlists.index(), + len(hooke.playlists))) + curve = playlist.current() + if curve == None: + return + outqueue.put('current curve: %s (%d of %d)' + % (curve.name, + playlist.index(), + len(playlist))) + +class PointInfoCommand (Command): + """Get information about two user-selected points. - c=0 - for item in self.current_list: - print 'Looking at curve ',c,'of',len(self.current_list) - things_when_cycling(item) - c+=1 - - return - - - - def plotmanip_absvalue(self, plot, current, customvalue=None): - ''' - This function defines a PLOT MANIPULATOR. - A plot manipulator is a function that takes a plot in input, does something to the plot - and returns the modified plot in output. - The function, once plugged, gets automatically called everytime self.plots is updated - - For example, in force spectroscopy force curves, raw data are automatically corrected - for deflection. Other data can be, say, filtered by default. - - To create and activate a plot manipulator you have to: - * Write a function (like this) which name starts with "plotmanip_" (just like commands - start with "do_") - * The function must support four arguments: - self : (as usual) - plot : a plot object - current : (usually not used, deprecated) - customvalue=None : a variable containing custom value(s) you need for your plot manipulators. - * The function must return a plot object. - * Add an entry in hooke.conf: if your function is "plotmanip_something" you will have - to add in the plotmanips section: example - - - - - - - - - Important: Plot manipulators are *in pipe*: each plot manipulator output becomes the input of the next one. - The order in hooke.conf *is the order* in which plot manipulators are connected, so in the example above - we have: - self.current.curve.default_plots() --> detriggerize --> correct --> median --> something --> self.plots - ''' - - ''' - Here we see what is in a configuration variable to enable/disable the plot manipulator as user will using - the Hooke "set" command. - Typing "set tutorial_absvalue 0" disables the plot manipulator; typing "set tutorial_absvalue 1" will enable it. - ''' - if not self.config['tutorial_absvalue']: - return plot - - #We do something to the plot, for demonstration's sake - #If we needed variables, we would have used customvalue. - plot.vectors[0][1]=[abs(i) for i in plot.vectors[0][1]] - plot.vectors[1][1]=[abs(i) for i in plot.vectors[1][1]] - - #Return the plot object. - return plot - - -#TODO IN TUTORIAL: -#how to add lines to an existing plot!! -#peaks -#configuration files -#gui plugins + Ordinarily a command that knew it would need user selected points + would declare an appropriate argument (see, for example, + :class:`hooke.plugin.cut.CutCommand`). However, here we find the + points via user-interaction to show how user interaction works. + """ + def __init__(self, plugin): + super(PointInfoCommand, self).__init__( + name='point info', + arguments=[ + CurveArgument, + Argument(name='block', type='int', default=0, + help=""" +Data block that points are selected from. For an approach/retract +force curve, `0` selects the approaching curve and `1` selects the +retracting curve. +""".strip()), + ], + help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + data = params['curve'].data[params['block']] + while True: + # Ask the user to select a point. + outqueue.put(PointRequest( + msg="Select a point", + curve=params['curve'], + block=params['block'])) + + # Get the user's response + result = inqueue.get() + if not isinstance(result, PointResponse): + inqueue.put(result) # put the message back in the queue + raise Failure( + 'expected a PointResponse instance but got %s.' + % type(result)) + point = result.value + + # Act on the response + if point == None: + break + values = [] + for column_name in data.info['columns']: + name,unit = split_data_label(column_name) + column_index = data.info['columns'].index(column_name) + value = data[point,column_index] + si_value = ppSI(value, unit, decimals=2) + values.append('%s: %s' % (name, si_value)) + + outqueue.put('selected point %d: %s' + % (point, ', '.join(values))) -- 2.26.2