Update ForceCommand to use flexible column names.
[hooke.git] / hooke / plugin / vclamp.py
index fcf49e8598c9c7a0cb1793716ae607bf5d132881..617f09d8034e80fecc8b7cd56893d3f915a9f8f7 100644 (file)
@@ -33,31 +33,50 @@ import scipy
 from ..command import Command, Argument, Failure, NullQueue
 from ..config import Setting
 from ..curve import Data
-from ..plugin import Builtin
+from ..plugin import Plugin
 from ..util.fit import PoorFit, ModelFitter
+from ..util.si import join_data_label, split_data_label
 from .curve import CurveArgument
 
 
-def scale(hooke, curve):
+def scale(hooke, curve, block=None):
+    """Run 'add block force array' on `block`.
+
+    Runs 'zero block surface contact point' first, if necessary.  Does
+    not run either command if the columns they add to the block are
+    already present.
+
+    If `block` is `None`, scale all blocks in `curve`.
+    """
     commands = hooke.commands
     contact = [c for c in hooke.commands
                if c.name == 'zero block surface contact point'][0]
     force = [c for c in hooke.commands if c.name == 'add block force array'][0]
+    cant_adjust = [c for c in hooke.commands
+               if c.name == 'add block cantilever adjusted extension array'][0]
     inqueue = None
     outqueue = NullQueue()
-    for i,block in enumerate(curve.data):
-        numpy.savetxt(open('curve.dat', 'w'), block, delimiter='\t')
-        params = {'curve':curve, 'block':i}
-        try:
-            contact._run(hooke, inqueue, outqueue, params)
-        except PoorFit, e:
-            raise PoorFit('Could not fit %s %s: %s'
-                          % (curve.path, i, str(e)))
-        force._run(hooke, inqueue, outqueue, params)
+    if block == None:
+        for i in range(len(curve.data)):
+            scale(hooke, curve, block=i)
+    else:
+        params = {'curve':curve, 'block':block}
+        b = curve.data[block]
+        if ('surface distance (m)' not in b.info['columns']
+            or 'surface deflection (m)' not in b.info['columns']):
+            try:
+                contact.run(hooke, inqueue, outqueue, **params)
+            except PoorFit, e:
+                raise PoorFit('Could not fit %s %s: %s'
+                              % (curve.path, block, str(e)))
+        if ('deflection (N)' not in b.info['columns']):
+            force.run(hooke, inqueue, outqueue, **params)
+        if ('cantilever adjusted extension (m)' not in b.info['columns']):
+            cant_adjust.run(hooke, inqueue, outqueue, **params)
     return curve
 
 class SurfacePositionModel (ModelFitter):
-    """
+    """Bilinear surface position model.
 
     The bilinear model is symmetric, but the parameter guessing and
     sanity checks assume the contact region occurs for lower indicies
@@ -78,9 +97,6 @@ class SurfacePositionModel (ModelFitter):
     should be enough data in the off-surface region that interactions
     due to proteins, etc. will not seriously skew the fit in the
     off-surface region.
-
-
-    We guess
     """
     def model(self, params):
         """A continuous, bilinear model.
@@ -150,7 +166,6 @@ class SurfacePositionModel (ModelFitter):
 
         Notes
         -----
-
         We guess offset scale (:math:`p_0`) as one tenth of the total
         deflection range, the kink scale (:math:`p_2`) as one tenth of
         the total index range, the initial (contact) slope scale
@@ -195,11 +210,12 @@ class SurfacePositionModel (ModelFitter):
                           % (params[3], self.info['guessed contact slope']))
         return params
 
-class VelocityClampPlugin (Builtin):
+class VelocityClampPlugin (Plugin):
     def __init__(self):
         super(VelocityClampPlugin, self).__init__(name='vclamp')
         self._commands = [
             SurfaceContactCommand(self), ForceCommand(self),
+            CantileverAdjustedExtensionCommand(self),
             ]
 
     def default_settings(self):
@@ -215,14 +231,6 @@ class VelocityClampPlugin (Builtin):
 class SurfaceContactCommand (Command):
     """Automatically determine a block's surface contact point.
 
-    Uses the block's `z piezo (m)` and `deflection (m)` arrays.
-    Stores the contact parameters in `block.info`'s `surface distance
-    offset (m)` and `surface deflection offset (m)`.  Model-specific
-    fitting information is stored in `surface detection parameters`.
-
-    The adjusted data columns `surface distance (m)` and `surface
-    adjusted deflection (m)` are also added to the block.
-
     You can select the contact point algorithm with the creatively
     named `surface contact point algorithm` configuration setting.
     Currently available options are:
@@ -241,12 +249,47 @@ class SurfaceContactCommand (Command):
 Data block for which the force should be calculated.  For an
 approach/retract force curve, `0` selects the approaching curve and `1`
 selects the retracting curve.
+""".strip()),
+                Argument(name='input distance column', type='string',
+                         default='z piezo (m)',
+                         help="""
+Name of the column to use as the surface positioning input.
+""".strip()),
+                Argument(name='input deflection column', type='string',
+                         default='deflection (m)',
+                         help="""
+Name of the column to use as the deflection input.
+""".strip()),
+                Argument(name='output distance column', type='string',
+                         default='surface distance',
+                         help="""
+Name of the column (without units) to use as the surface positioning output.
+""".strip()),
+                Argument(name='output deflection column', type='string',
+                         default='surface deflection',
+                         help="""
+Name of the column (without units) to use as the deflection output.
+""".strip()),
+                Argument(name='distance info name', type='string',
+                         default='surface distance offset',
+                         help="""
+Name (without units) for storing the distance offset in the `.info` dictionary.
+""".strip()),
+                Argument(name='deflection info name', type='string',
+                         default='surface deflection offset',
+                         help="""
+Name (without units) for storing the deflection offset in the `.info` dictionary.
+""".strip()),
+                Argument(name='fit parameters info name', type='string',
+                         default='surface deflection offset',
+                         help="""
+Name (without units) for storing the deflection offset in the `.info` dictionary.
 """.strip()),
                 ],
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
-        data = params['curve'].data[int(params['block'])] # HACK, int() should be handled by ui
+        data = params['curve'].data[params['block']]
         # HACK? rely on params['curve'] being bound to the local hooke
         # playlist (i.e. not a copy, as you would get by passing a
         # curve through the queue).  Ugh.  Stupid queues.  As an
@@ -255,20 +298,27 @@ selects the retracting curve.
         new = Data((data.shape[0], data.shape[1]+2), dtype=data.dtype)
         new.info = copy.deepcopy(data.info)
         new[:,:-2] = data
-        new.info['columns'].extend(
-            ['surface distance (m)', 'surface adjusted deflection (m)'])
-        z_data = data[:,data.info['columns'].index('z piezo (m)')]
-        d_data = data[:,data.info['columns'].index('deflection (m)')]
-        i,deflection_offset,ps = self.find_contact_point(
-            params['curve'], z_data, d_data, outqueue)
-        surface_offset = z_data[i]
-        new.info['surface distance offset (m)'] = surface_offset
-        new.info['surface deflection offset (m)'] = deflection_offset
-        new.info['surface detection parameters'] = ps
-        new[:,-2] = z_data - surface_offset
-        new[:,-1] = d_data - deflection_offset
-        data = params['curve'].data[int(params['block'])] # HACK, int() should be handled by ui
-        params['curve'].data[int(params['block'])] = new # HACK, int() should be handled by ui
+        name,dist_units = split_data_label(params['input distance column'])
+        name,def_units = split_data_label(params['input deflection column'])
+        new.info['columns'].extend([
+                join_data_label(params['output distance column'], dist_units),
+                join_data_label(params['output deflection column'], def_units),
+                ])
+        dist_data = data[:,data.info['columns'].index(
+                params['input distance column'])]
+        def_data = data[:,data.info['columns'].index(
+                params['input deflection column'])]
+        i,def_offset,ps = self.find_contact_point(
+            params['curve'], dist_data, def_data, outqueue)
+        dist_offset = dist_data[i]
+        new.info[join_data_label(params['distance info name'], dist_units
+                                 )] = dist_offset
+        new.info[join_data_label(params['deflection info name'], def_units
+                                 )] = def_offset
+        new.info[params['fit parameters info name']] = ps
+        new[:,-2] = dist_data - dist_offset
+        new[:,-1] = def_data - def_offset
+        params['curve'].data[params['block']] = new
 
     def find_contact_point(self, curve, z_data, d_data, outqueue=None):
         """Railyard for the `find_contact_point_*` family.
@@ -412,11 +462,9 @@ selects the retracting curve.
             surface_index = len(d_data)-1-surface_index
         return (numpy.round(surface_index), deflection_offset, info)
 
-class ForceCommand (Command):
-    """Calculate a block's `deflection (N)` array.
 
-    Uses the block's `deflection (m)` array and `spring constant
-    (N/m)`.
+class ForceCommand (Command):
+    """Convert a deflection column from meters to newtons.
     """
     def __init__(self, plugin):
         super(ForceCommand, self).__init__(
@@ -428,12 +476,27 @@ class ForceCommand (Command):
 Data block for which the force should be calculated.  For an
 approach/retract force curve, `0` selects the approaching curve and `1`
 selects the retracting curve.
+""".strip()),
+                Argument(name='input deflection column', type='string',
+                         default='surface deflection (m)',
+                         help="""
+Name of the column to use as the deflection input.
+""".strip()),
+                Argument(name='output deflection column', type='string',
+                         default='deflection',
+                         help="""
+Name of the column (without units) to use as the deflection output.
+""".strip()),
+                Argument(name='spring constant info name', type='string',
+                         default='spring constant (N/m)',
+                         help="""
+Name of the spring constant in the `.info` dictionary.
 """.strip()),
                 ],
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
-        data = params['curve'].data[int(params['block'])] # HACK, int() should be handled by ui
+        data = params['curve'].data[params['block']]
         # HACK? rely on params['curve'] being bound to the local hooke
         # playlist (i.e. not a copy, as you would get by passing a
         # curve through the queue).  Ugh.  Stupid queues.  As an
@@ -442,32 +505,52 @@ selects the retracting curve.
         new = Data((data.shape[0], data.shape[1]+1), dtype=data.dtype)
         new.info = copy.deepcopy(data.info)
         new[:,:-1] = data
-        new.info['columns'].append('deflection (N)')
-        d_data = data[:,data.info['columns'].index('surface adjusted deflection (m)')]
-        new[:,-1] = d_data * data.info['spring constant (N/m)']
-        params['curve'].data[int(params['block'])] = new # HACK, int() should be handled by ui
+        new.info['columns'].append(
+            join_data_label(params['output deflection column'], 'N'))
+        d_data = data[:,data.info['columns'].index(
+                params['input deflection column'])]
+        new[:,-1] = d_data * data.info[params['spring constant info name']]
+        params['curve'].data[params['block']] = new
 
 
-class generalvclampCommands(object):
+class CantileverAdjustedExtensionCommand (Command):
+    """Calculate a block's `cantilever adjusted extension (m)` array.
 
-    def do_subtplot(self, args):
-        '''
-        SUBTPLOT
-        (procplots.py plugin)
-        Plots the difference between ret and ext current curve
-        -------
-        Syntax: subtplot
-        '''
-        #FIXME: sub_filter and sub_order must be args
+    Uses the block's `deflection (m)` and `surface distance offset (m)`
+    arrays and `spring constant (N/m)`.
+    """
+    def __init__(self, plugin):
+        super(CantileverAdjustedExtensionCommand, self).__init__(
+            name='add block cantilever adjusted extension array',
+            arguments=[
+                CurveArgument,
+                Argument(name='block', aliases=['set'], type='int', default=0,
+                         help="""
+Data block for which the adjusted extension should be calculated.  For
+an approach/retract force curve, `0` selects the approaching curve and
+`1` selects the retracting curve.
+""".strip()),
+                ],
+            help=self.__doc__, plugin=plugin)
 
-        if len(self.plots[0].vectors) != 2:
-            print 'This command only works on a curve with two different plots.'
-            pass
+    def _run(self, hooke, inqueue, outqueue, params):
+        data = params['curve'].data[params['block']]
+        # HACK? rely on params['curve'] being bound to the local hooke
+        # playlist (i.e. not a copy, as you would get by passing a
+        # curve through the queue).  Ugh.  Stupid queues.  As an
+        # alternative, we could pass lookup information through the
+        # queue.
+        new = Data((data.shape[0], data.shape[1]+1), dtype=data.dtype)
+        new.info = copy.deepcopy(data.info)
+        new[:,:-1] = data
+        new.info['columns'].append('cantilever adjusted extension (m)')
+        z_data = data[:,data.info['columns'].index('surface distance (m)')]
+        d_data = data[:,data.info['columns'].index('deflection (N)')]
+        new[:,-1] = z_data - d_data / data.info['spring constant (N/m)']
+        params['curve'].data[params['block']] = new
 
-        outplot=self.subtract_curves(sub_order=1)
 
-        plot_graph=self.list_of_events['plot_graph']
-        wx.PostEvent(self.frame,plot_graph(plots=[outplot]))
+class generalvclampCommands(object):
 
     def _plug_init(self):
         self.basecurrent=None