From: W. Trevor King Date: Sat, 8 May 2010 21:04:08 +0000 (-0400) Subject: Rework hooke.driver and hooke.driver.tutorial along the lines of hooke.plugin. X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=15e678479f3dcfd8ef6f4e2b3de7ca87dca89290;p=hooke.git Rework hooke.driver and hooke.driver.tutorial along the lines of hooke.plugin. Many more drivers to refactor, but this is good enough to get me going again with the core code. --- diff --git a/hooke/curve.py b/hooke/curve.py index 99c481b..5aeaeaa 100644 --- a/hooke/curve.py +++ b/hooke/curve.py @@ -1,8 +1,13 @@ +"""The curve module provides :class:`Curve` and :class:`Data` for storing +force curves. +""" + import os.path import numpy from .driver import NotRecognized + class Data (numpy.ndarray): """Stores a single, continuous data set. @@ -41,6 +46,20 @@ class Curve (object): For an approach/retract force spectroscopy experiment, the group would consist of the approach data and the retract data. Metadata would be the temperature, cantilever spring constant, etc. + + Two important :attr:`info` settings are `filetype` and + `experiment`. These are two strings that can be used by Hooke + commands/plugins to understand what they are looking at. + + * `.info['filetype']` should contain the name of the exact + filetype defined by the driver (so that filetype-speciofic + commands can know if they're dealing with the correct filetype). + * `.info['experiment']` should contain an instance of a + :class:`hooke.experiment.Experiment` subclass to identify the + experiment type. For example, various + :class:`hooke.driver.Driver`\s can read in force-clamp data, but + Hooke commands could like to know if they're looking at force + clamp data, regardless of their origin. """ def __init__(self, path): #the data dictionary contains: {name of data: list of data sets [{[x], [y]}] @@ -56,9 +75,8 @@ class Curve (object): the curve file (`.path`). """ for driver in drivers: - current_driver = driver(self.path) - if current_driver.is_me(): - self.driver = current_driver # remember the working driver + if driver.is_me(self.path): + self.driver = driver # remember the working driver return raise NotRecognized(self.path) diff --git a/hooke/driver/__init__.py b/hooke/driver/__init__.py index 4b9c176..6eed3f7 100644 --- a/hooke/driver/__init__.py +++ b/hooke/driver/__init__.py @@ -1,3 +1,31 @@ +"""The driver module provides :class:`Driver`\s for identifying and +reading data files. + +This allows Hooke to be data-file agnostic. Drivers for various +commercial force spectroscopy microscopes are provided, and it's easy +to write your own to handle your lab's specific format. +""" + +from ..config import Setting +from ..util.graph import Node, Graph + + +DRIVER_MODULES = [ +# ('csvdriver', True), +# ('hdf5', True), +# ('hemingclamp', True), +# ('jpk', True), +# ('mcs', True), +# ('mfp1dexport', True), +# ('mfp3d', True), +# ('picoforce', True), +# ('picoforcealt', True), + ('tutorial', True), +] +"""List of driver modules and whether they should be included by +default. TODO: autodiscovery +""" + class NotRecognized (ValueError): def __init__(self, path): msg = 'Not a recognizable curve format: %s' % self.path @@ -5,28 +33,89 @@ class NotRecognized (ValueError): self.path = path class Driver(object): - ''' - Base class for file format drivers. - - To be overridden - ''' - def __init__(self): - self.experiment = '' - self.filetype = '' - - def is_me(self): - ''' - This method must read the file and return True if the filetype can be managed by the driver, False if not. - ''' + """Base class for file format drivers. + + :attr:`name` identifies your driver, and should match the module + name. + """ + def __init__(self, name): + self.name = name + + def dependencies(self): + """Return a list of :class:`Driver`\s we require.""" + return [] + + def default_settings(self): + """Return a list of :class:`hooke.config.Setting`\s for any + configurable driver settings. + + The suggested section setting is:: + + Setting(section='%s driver' % self.name, help=self.__doc__) + """ + return [] + + def is_me(self, path): + """Read the file and return True if the filetype can be + managed by the driver. Otherwise return False. + """ return False - def close_all(self): - ''' - This method must close all the open files of the driver, explicitly. - ''' - return None + def read(self, path): + """Read data from `path` and return a + (:class:`hooke.curve.Data`, `info`) tuple. + + The `info` :class:`dict` must contain values for the keys: + 'filetype' and 'experiment'. See :class:`hooke.curve.Curve` + for details. + """ + raise NotImplementedError + +# Construct driver dependency graph and load default drivers. + +DRIVERS = {} +"""(name, instance) :class:`dict` of all possible :class:`Driver`\s. +""" + +for driver_modname,default_include in DRIVER_MODULES: + assert len([mod_name for mod_name,di in DRIVER_MODULES]) == 1, \ + 'Multiple %s entries in DRIVER_MODULES' % mod_name + this_mod = __import__(__name__, fromlist=[driver_modname]) + driver_mod = getattr(this_mod, driver_modname) + for objname in dir(driver_mod): + obj = getattr(driver_mod, objname) + try: + subclass = issubclass(obj, Driver) + except TypeError: + continue + if subclass == True and obj != Driver: + d = obj() + if d.name != driver_modname: + raise Exception('Driver name %s does not match module name %s' + % (d.name, driver_modname)) + DRIVERS[d.name] = d + +DRIVER_GRAPH = Graph([Node([DRIVERS[name] for name in d.dependencies()], + data=d) + for d in DRIVERS.values()]) +DRIVER_GRAPH.topological_sort() + - def default_plots(self): - dummy_default = PlotObject() - dummy_default.vectors.append([[[0]],[[0]]]) - return [dummy_default] +def default_settings(): + settings = [Setting( + 'drivers', help='Enable/disable default drivers.')] + for dnode in DRIVER_GRAPH: + driver = dnode.data + default_include = [di for mod_name,di in DRIVER_MODULES + if mod_name == driver.name][0] + help = driver.__doc__.split('\n', 1)[0] + settings.append(Setting( + section='drivers', + option=driver.name, + value=str(default_include), + help=help, + )) + for dnode in DRIVER_GRAPH: + driver = dnode.data + settings.extend(driver.default_settings()) + return settings diff --git a/hooke/driver/tutorial.py b/hooke/driver/tutorial.py new file mode 100644 index 0000000..7bdf6ce --- /dev/null +++ b/hooke/driver/tutorial.py @@ -0,0 +1,122 @@ +# Copyright (c) 2008 Massimo Sandal +# 2010 W. Trevor King + +"""Tutorial driver for Hooke. + +This example driver explains driver construction. +""" + +""" +Here we define a simple file format that is read by this driver. The +file format is as following:: + + TUTORIAL_FILE + PLOT1 + X1 + n1 <- ? + n2 + ... + nN + Y1 + n1 + n2 + ... + nN + X2 + n1 + n2 + .. + nN + Y2 + n1 + n2 + .. + nN + PLOT2 + X1 + ... + Y1 + ... + X2 + ... + Y2 + ... + END + +that is, two plots with two datasets each. +""" + +# The following are relative imports. See PEP 328 for details +# http://www.python.org/dev/peps/pep-0328/ +from .. import curve as curve # this module defines data containers. +from .. import experiment as experiment # this module defines expt. types +from ..config import Setting # configurable setting class +from . import Driver as Driver # this is the Driver base class + +# The driver must inherit from the parent +# :class:`hooke.driver.Driver` (which we have imported as `Driver`). +class TutorialDriver (Driver): + """Handle simple text data as an example Driver. + """ + def __init__(self): + """YOU MUST OVERRIDE Driver.__init__. + + Here you set a value for `name` to identify your driver. It + should match the module name. + """ + super(TutorialDriver, self).__init__(name='tutorial') + + def default_settings(self): + """Return a list of any configurable settings for your driver. + + If your driver does not have any configurable settings, there + is no need to override this method. + """ + section = '%s driver' % self.name + return [ + Setting(section=section, help=self.__doc__), + Setting(section=section, option='x units', value='nm', + help='Set the units used for the x data.'), + ] + + def is_me(self): + """YOU MUST OVERRIDE Driver.is_me. + + RETURNS: Boolean (`True` or `False`) + + This method is a heuristic that looks at the file content and + decides if the file can be opened by the driver itself. It + returns `True` if the file opened can be interpreted by the + current driver, `False` otherwise. Defining this method allows + Hooke to understand what kind of files we're looking at + automatically. + """ + + f = open(self.filename, 'r') + header = f.readline() # we only need the first line + f.close() + + """Our "magic fingerprint" is the TUTORIAL_FILE header. Of + course, depending on the data file, you can have interesting + headers, or patterns, etc. that you can use to guess the data + format. What matters is successful recognition and the boolean + (True/False) return. + """ + if header.startswith('TUTORIAL_FILE'): + return True + return False + + def read(self, path): + f = open(path,'r') # open the file for reading + """In this case, we have a data format that is just a list of + ASCII values, so we can just divide that in rows, and generate + a list with each item being a row. Of course if your data + files are binary, or follow a different approach, do whatever + you need. :) + """ + self.data = list(self.filedata) + f.close() # remember to close the file + + data = curve.Data() + info = {'filetype':'tutorial', 'experiment':'generic'} + return (data, info) diff --git a/hooke/driver/tutorialdriver.py b/hooke/driver/tutorialdriver.py deleted file mode 100644 index f30107e..0000000 --- a/hooke/driver/tutorialdriver.py +++ /dev/null @@ -1,206 +0,0 @@ -''' -tutorialdriver.py - -TUTORIAL DRIVER FOR HOOKE - -Example driver to teach how to write a driver for data types. -(c)Massimo Sandal 2008 -''' - -''' -Here we define a (fake) file format that is read by this driver. The file format is as following: - -TUTORIAL_FILE -PLOT1 -X1 -n1 -n2 -... -nN -Y1 -n1 -n2 -... -nN -X2 -n1 -n2 -.. -nN -Y2 -n1 -n2 -.. -nN -PLOT2 -X1 -... -Y1 -... -X2 -... -Y2 -... -END -that is, two plots with two datasets each. -''' - -from .. import curve as lhc #We need to import this library to define some essential data types - -class tutorialdriverDriver(lhc.Driver): - ''' - This is a *generic* driver, not a specific force spectroscopy driver. - See the written documentation to see what a force spectroscopy driver must be defined to take into account Hooke facilities. - - Our driver is a class with the name convention nameofthedriverDriver, where "nameofthedriver" is the filename. - The driver must inherit from the parent class lhc.Driver, so the syntax is - class nameofthedriverDriver(lhc.Driver) - ''' - def __init__(self, filename): - ''' - THIS METHOD MUST BE DEFINED. - The __init__ method MUST call the filename, so that it can open the file. - ''' - self.filename=filename #self.filename can always be useful, and should be defined - self.filedata = open(filename,'r') #We open the file - ''' - In this case, we have a data format that is just a list of ASCII values, so we can just divide that in rows, and generate a list - with each item being a row. - Of course if your data files are binary, or follow a different approach, do whatever you need. :) - ''' - self.data = list(self.filedata) - self.filedata.close() #remember to close the file - - '''These are two strings that can be used by Hooke commands/plugins to understand what they are looking at. They have no other - meaning. They have to be somehow defined however - commands often look for those variables. - - self.filetype should contain the name of the exact filetype defined by the driver (so that filetype-specific commands can know - if they're dealing with the correct filetype) - self.experiment should contain instead the type of data involved (for example, various drivers can be used for force-clamp experiments, - but hooke commands could like to know if we're looking at force clamp data, regardless of their origin, and not other - kinds of data) - - Of course, all other variables you like can be defined in the class. - ''' - self.filetype = 'tutorial' - self.experiment = 'generic' - - def is_me(self): - ''' - THIS METHOD MUST BE DEFINED. - RETURNS: Boolean (True or False) - This method must be an heuristic that looks at the file content and decides if the file can be opened by the driver itself. - It returns True if the file opened can be interpreted by the current driver, False otherwise. - Defining this method allows Hooke to understand what kind of files we're looking at automatically. - - We have to re-open/re-close the file here. - ''' - - myfile=open(self.filename, 'r') - headerline=myfile.readlines()[0] #we take the first line - myfile.close() - - ''' - Here, our "magic fingerprint" is the TUTORIAL_FILE header. Of course, depending on the data file, you can have interesting - headers, or patterns, etc. that you can use to guess the data format. What matters is successful recognizing, and returning - a boolean (True/False). - ''' - if headerline[:-1]=='TUTORIAL_FILE': #[:-1], otherwise the return character is included in the line - return True - else: - return False - - def _generate_vectors(self): - ''' - Here we parse the data and generate the raw vectors. This method has only to do with the peculiar file format here, so it's of - no big interest (I just wrote it to present a functional driver). - - Only thing to remember, it can be nice to call methods that are used only "internally" by the driver (or by plugins) with a - "_" prefix, so to have a visual remark. But it's just an advice. - ''' - vectors={'PLOT1':[[],[],[],[]] , 'PLOT2':[[],[],[],[]]} - positions={'X1':0,'Y1':1,'X2':2,'Y2':3} - whatplot='' - pos=0 - for item in self.data: - try: - num=float(item) - vectors[whatplot][pos].append(num) - except ValueError: - if item[:-1]=='PLOT1': - whatplot=item[:-1] - elif item[:-1]=='PLOT2': - whatplot=item[:-1] - elif item[0]=='X' or item[0]=='Y': - pos=positions[item[:-1]] - else: - pass - - return vectors - - def close_all(self): - ''' - THIS METHOD MUST BE DEFINED. - This method is a precaution method that is invoked when cycling to avoid eventually dangling open files. - In this method, all file objects defined in the driver must be closed. - ''' - self.filename.close() - - - def default_plots(self): - ''' - THIS METHOD MUST BE DEFINED. - RETURNS: [ lhc.PlotObject ] or [ lhc.PlotObject, lhc.PlotObject] - - This is the method that returns the plots to Hooke. - It must return a list with *one* or *two* PlotObjects. - - See the curve.py source code to see how PlotObjects are defined and work in detail. - ''' - gen_vectors=self._generate_vectors() - - #Here we create the main plot PlotObject and initialize its vectors. - main_plot=lhc.PlotObject() - main_plot.vectors=[] - #The same for the other plot. - other_plot=lhc.PlotObject() - other_plot.vectors=[] - - ''' - Now we fill the plot vectors with our data. - set 1 set 2 - The "correct" shape of the vector is [ [[x1,x2,x3...],[y1,y2,y3...]] , [[x1,x2,x3...],[y1,y2,y3...]] ], so we have to put stuff in this way into it. - - The add_set() method takes care of this , just use plot.add_set(x,y). - ''' - main_plot.add_set(gen_vectors['PLOT1'][0],gen_vectors['PLOT1'][1]) - main_plot.add_set(gen_vectors['PLOT1'][2],gen_vectors['PLOT1'][3]) - - other_plot.add_set(gen_vectors['PLOT2'][0],gen_vectors['PLOT2'][1]) - other_plot.add_set(gen_vectors['PLOT2'][2],gen_vectors['PLOT2'][3]) - - ''' - normalize_vectors() trims the vectors, so that if two x/y couples are of different lengths, the latest - points are trimmed (otherwise we have a python error). Always a good idea to run it, to avoid crashes. - ''' - main_plot.normalize_vectors() - other_plot.normalize_vectors() - - ''' - Here we define: - - units: [string, string], define the measure units of X and Y axes - - destination: 0/1 , defines where to plot the plot (0=top, 1=bottom), default=0 - - title: string , the plot title. - - for each plot. - Again, see curve.py comments for details. - ''' - main_plot.units=['unit of x','unit of y'] - main_plot.destination=0 - main_plot.title=self.filename+' main' - - other_plot.units=['unit of x','unit of y'] - other_plot.destination=1 - other_plot.title=self.filename+' other' - - return [main_plot, other_plot] diff --git a/hooke/experiment.py b/hooke/experiment.py new file mode 100644 index 0000000..e9bf27b --- /dev/null +++ b/hooke/experiment.py @@ -0,0 +1,19 @@ +"""Define :class:`Experiment` and assorted subclasses. + +This allows :class:`hooke.plugin.Plugin`\s to specify the types of +experiments they can handle. +""" + +class Experiment (object): + """Base class for experiment classification. + """ + pass + +class ForceClamp (Experiment): + """Constant force experiments. + """ + pass + +class VelocityClamp (Experiment): + """Constant piezo velocity experiments. + """ diff --git a/hooke/plugin/__init__.py b/hooke/plugin/__init__.py index 585969e..2444ac9 100644 --- a/hooke/plugin/__init__.py +++ b/hooke/plugin/__init__.py @@ -4,7 +4,6 @@ commands. All of the science happens in here. """ -import os.path import Queue as queue from ..config import Setting diff --git a/hooke/ui/gui/driver.py b/hooke/ui/gui/driver.py new file mode 100644 index 0000000..57d6b18 --- /dev/null +++ b/hooke/ui/gui/driver.py @@ -0,0 +1,90 @@ + + def _generate_vectors(self): + """ + Here we parse the data and generate the raw vectors. This + method has only to do with the peculiar file format here, so + it's of no big interest (I just wrote it to present a + functional driver). + + Only thing to remember, it can be nice to call methods that + are used only "internally" by the driver (or by plugins) with + a "_" prefix, so to have a visual remark. But it's just an + advice. + """ + vectors={'PLOT1':[[],[],[],[]] , 'PLOT2':[[],[],[],[]]} + positions={'X1':0,'Y1':1,'X2':2,'Y2':3} + whatplot='' + pos=0 + for item in self.data: + try: + num=float(item) + vectors[whatplot][pos].append(num) + except ValueError: + if item[:-1]=='PLOT1': + whatplot=item[:-1] + elif item[:-1]=='PLOT2': + whatplot=item[:-1] + elif item[0]=='X' or item[0]=='Y': + pos=positions[item[:-1]] + else: + pass + + return vectors + + def default_plots(self): + """ + THIS METHOD MUST BE DEFINED. + RETURNS: [ lhc.PlotObject ] or [ lhc.PlotObject, lhc.PlotObject] + + This is the method that returns the plots to Hooke. + It must return a list with *one* or *two* PlotObjects. + + See the curve.py source code to see how PlotObjects are defined and work in detail. + """ + gen_vectors=self._generate_vectors() + + #Here we create the main plot PlotObject and initialize its vectors. + main_plot=lhc.PlotObject() + main_plot.vectors=[] + #The same for the other plot. + other_plot=lhc.PlotObject() + other_plot.vectors=[] + + """ + Now we fill the plot vectors with our data. + set 1 set 2 + The "correct" shape of the vector is [ [[x1,x2,x3...],[y1,y2,y3...]] , [[x1,x2,x3...],[y1,y2,y3...]] ], so we have to put stuff in this way into it. + + The add_set() method takes care of this , just use plot.add_set(x,y). + """ + main_plot.add_set(gen_vectors['PLOT1'][0],gen_vectors['PLOT1'][1]) + main_plot.add_set(gen_vectors['PLOT1'][2],gen_vectors['PLOT1'][3]) + + other_plot.add_set(gen_vectors['PLOT2'][0],gen_vectors['PLOT2'][1]) + other_plot.add_set(gen_vectors['PLOT2'][2],gen_vectors['PLOT2'][3]) + + """ + normalize_vectors() trims the vectors, so that if two x/y couples are of different lengths, the latest + points are trimmed (otherwise we have a python error). Always a good idea to run it, to avoid crashes. + """ + main_plot.normalize_vectors() + other_plot.normalize_vectors() + + """ + Here we define: + - units: [string, string], define the measure units of X and Y axes + - destination: 0/1 , defines where to plot the plot (0=top, 1=bottom), default=0 + - title: string , the plot title. + + for each plot. + Again, see curve.py comments for details. + """ + main_plot.units=['unit of x','unit of y'] + main_plot.destination=0 + main_plot.title=self.filename+' main' + + other_plot.units=['unit of x','unit of y'] + other_plot.destination=1 + other_plot.title=self.filename+' other' + + return [main_plot, other_plot]