Rework hooke.driver and hooke.driver.tutorial along the lines of hooke.plugin.
authorW. Trevor King <wking@drexel.edu>
Sat, 8 May 2010 21:04:08 +0000 (17:04 -0400)
committerW. Trevor King <wking@drexel.edu>
Sat, 8 May 2010 21:04:08 +0000 (17:04 -0400)
Many more drivers to refactor, but this is good enough to get me going
again with the core code.

hooke/curve.py
hooke/driver/__init__.py
hooke/driver/tutorial.py [new file with mode: 0644]
hooke/driver/tutorialdriver.py [deleted file]
hooke/experiment.py [new file with mode: 0644]
hooke/plugin/__init__.py
hooke/ui/gui/driver.py [new file with mode: 0644]

index 99c481bfb86bf4ce0a2bca9f4c517f910d527ec0..5aeaeaac887f3bc3cc26f0b8ead8a73098deb7ee 100644 (file)
@@ -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)
 
index 4b9c17616182e24c225503a135aaa63d06b17567..6eed3f70c0f77687004eef10d25c36a9fc90f403 100644 (file)
@@ -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 (file)
index 0000000..7bdf6ce
--- /dev/null
@@ -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 (file)
index f30107e..0000000
+++ /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 (file)
index 0000000..e9bf27b
--- /dev/null
@@ -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.
+    """
index 585969ea7f04fc5cf3a6386cab569ff193826f50..2444ac9221089403323b417d8b5231738cb99f36 100644 (file)
@@ -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 (file)
index 0000000..57d6b18
--- /dev/null
@@ -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]