Moved contact point detection from plugin.fit -> plugin.vclamp.
authorW. Trevor King <wking@drexel.edu>
Wed, 2 Jun 2010 04:36:41 +0000 (00:36 -0400)
committerW. Trevor King <wking@drexel.edu>
Wed, 2 Jun 2010 04:36:41 +0000 (00:36 -0400)
I haven't translated the two algorithms brought over from fit yet, but
I have added my own bilinear fit approach:
  _find_contact_point_wtk

Also:
* vclamp.scale gets access to hooke for the command list and
  command._run(hooke, ...)
* vclamp.scale functionality broken out into SurfaceContactCommand and
  ForceCommand.
* 'surface z piezo (m)' -> 'surface distance (m)'
* Actually set params['curve'] for flatfilt and convfilt FilterCommand
  subclasses.  Previous implementation just banged away on the default
  curve, regardless of the curve option passed to the subclass' _run.

hooke/plugin/convfilt.py
hooke/plugin/fit.py
hooke/plugin/flatfilt.py
hooke/plugin/vclamp.py

index 7ca9861191f9ffc3acd3910fb4aa37cee82d9a23..4be6ea86705d4f3f70ee125f3efc9df5dacd2d24 100644 (file)
@@ -130,7 +130,7 @@ class ConvolutionPeaksCommand (Command):
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
-        z_data,d_data,params = self._setup(params)
+        z_data,d_data,params = self._setup(hooke, params)
         start_index = 0
         while z_data[start_index] < params['bind window']:
             start_index += 1
@@ -144,7 +144,7 @@ class ConvolutionPeaksCommand (Command):
             peak.index += start_index
         outqueue.put(peaks)
 
-    def _setup(self, params):
+    def _setup(self, hooke, params):
         """Setup `params` from config and return the z piezo and
         deflection arrays.
         """
@@ -152,9 +152,9 @@ class ConvolutionPeaksCommand (Command):
         if curve.info['experiment'] != VelocityClamp:
             raise Failure('%s operates on VelocityClamp experiments, not %s'
                           % (self.name, curve.info['experiment']))
-        for col in ['surface z piezo (m)', 'deflection (N)']:
+        for col in ['surface distance (m)', 'deflection (N)']:
             if col not in curve.data[0].info['columns']:
-                scale(curve)
+                scale(hooke, curve)
         data = None
         for block in curve.data:
             if block.info['name'].startswith('retract'):
@@ -162,7 +162,7 @@ class ConvolutionPeaksCommand (Command):
                 break
         if data == None:
             raise Failure('No retraction blocks in %s.' % curve)
-        z_data = data[:,data.info['columns'].index('surface z piezo (m)')]
+        z_data = data[:,data.info['columns'].index('surface distance (m)')]
         if 'flattened deflection (N)' in data.info['columns']:
             params['deflection column name'] = 'flattened deflection (N)'
         else:
@@ -204,6 +204,7 @@ class ConvolutionFilterCommand (FilterCommand):
         self.arguments.extend(plugin._arguments)
 
     def filter(self, curve, hooke, inqueue, outqueue, params):
+        params['curve'] = curve
         inq = Queue()
         outq = Queue()
         conv_command = [c for c in self.hooke.commands
index 1718c1ae0387c468d7a6ad2fa182126c3b02a6b4..7ea27262aa9df338751e7881d6ceeb1cc658bdf8 100644 (file)
@@ -778,171 +778,3 @@ class fitCommands(object):
             fitplot.colors+=[None,None]
 
         self._send_plot([fitplot])
-
-
-
-    #----------
-
-
-    def find_contact_point(self,plot=False):
-        '''
-        Finds the contact point on the curve.
-
-        The current algorithm (thanks to Francesco Musiani, francesco.musiani@unibo.it and Massimo Sandal) is:
-        - take care of the PicoForce trigger bug - exclude retraction portions with too high standard deviation
-        - fit the second half of the retraction curve to a line
-        - if the fit is not almost horizontal, take a smaller chunk and repeat
-        - otherwise, we have something horizontal
-        - so take the average of horizontal points and use it as a baseline
-
-        Then, start from the rise of the retraction curve and look at the first point below the
-        baseline.
-
-        FIXME: should be moved, probably to generalvclamp.py
-        '''
-
-        if not plot:
-            plot=self.plots[0]
-
-        outplot=self.subtract_curves(1)
-        xret=outplot.vectors[1][0]
-        ydiff=outplot.vectors[1][1]
-
-        xext=plot.vectors[0][0]
-        yext=plot.vectors[0][1]
-        xret2=plot.vectors[1][0]
-        yret=plot.vectors[1][1]
-
-        #taking care of the picoforce trigger bug: we exclude portions of the curve that have too much
-        #standard deviation. yes, a lot of magic is here.
-        monster=True
-        monlength=len(xret)-int(len(xret)/20)
-        finalength=len(xret)
-        while monster:
-            monchunk=scipy.array(ydiff[monlength:finalength])
-            if abs(np.std(monchunk)) < 2e-10:
-                monster=False
-            else: #move away from the monster
-                monlength-=int(len(xret)/50)
-                finalength-=int(len(xret)/50)
-
-
-        #take half of the thing
-        endlength=int(len(xret)/2)
-
-        ok=False
-
-        while not ok:
-            xchunk=yext[endlength:monlength]
-            ychunk=yext[endlength:monlength]
-            regr=scipy.stats.linregress(xchunk,ychunk)[0:2]
-            #we stop if we found an almost-horizontal fit or if we're going too short...
-            #FIXME: 0.1 and 6 here are "magic numbers" (although reasonable)
-            if (abs(regr[1]) > 0.1) and ( endlength < len(xret)-int(len(xret)/6) ) :
-                endlength+=10
-            else:
-                ok=True
-
-
-        ymean=np.mean(ychunk) #baseline
-
-        index=0
-        point = ymean+1
-
-        #find the first point below the calculated baseline
-        while point > ymean:
-            try:
-                point=yret[index]
-                index+=1
-            except IndexError:
-                #The algorithm didn't find anything below the baseline! It should NEVER happen
-                index=0
-                return index
-
-        return index
-
-
-
-    def find_contact_point2(self, debug=False):
-        '''
-        TO BE DEVELOPED IN THE FUTURE
-        Finds the contact point on the curve.
-
-        FIXME: should be moved, probably to generalvclamp.py
-        '''
-
-        #raw_plot=self.current.curve.default_plots()[0]
-        raw_plot=self.plots[0]
-        '''xext=self.plots[0].vectors[0][0]
-        yext=self.plots[0].vectors[0][1]
-        xret2=self.plots[0].vectors[1][0]
-        yret=self.plots[0].vectors[1][1]
-        '''
-        xext=raw_plot.vectors[0][0]
-        yext=raw_plot.vectors[0][1]
-        xret2=raw_plot.vectors[1][0]
-        yret=raw_plot.vectors[1][1]
-
-        first_point=[xext[0], yext[0]]
-        last_point=[xext[-1], yext[-1]]
-
-        #regr=scipy.polyfit(first_point, last_point,1)[0:2]
-        diffx=abs(first_point[0]-last_point[0])
-        diffy=abs(first_point[1]-last_point[1])
-
-        #using polyfit results in numerical errors. good old algebra.
-        a=diffy/diffx
-        b=first_point[1]-(a*first_point[0])
-        baseline=scipy.polyval((a,b), xext)
-
-        ysub=[item-basitem for item,basitem in zip(yext,baseline)]
-
-        contact=ysub.index(min(ysub))
-
-        return xext,ysub,contact
-
-        #now, exploit a ClickedPoint instance to calculate index...
-        dummy=ClickedPoint()
-        dummy.absolute_coords=(x_intercept,y_intercept)
-        dummy.find_graph_coords(xret2,yret)
-
-        if debug:
-            return dummy.index, regr, regr_contact
-        else:
-            return dummy.index
-
-
-
-    def x_do_contact(self,args):
-        '''
-        DEBUG COMMAND to be activated in the future
-        '''
-        xext,ysub,contact=self.find_contact_point2(debug=True)
-
-        contact_plot=self.plots[0]
-        contact_plot.add_set(xext,ysub)
-        contact_plot.add_set([xext[contact]],[self.plots[0].vectors[0][1][contact]])
-        #contact_plot.add_set([first_point[0]],[first_point[1]])
-        #contact_plot.add_set([last_point[0]],[last_point[1]])
-        contact_plot.styles=[None,None,None,'scatter']
-        self._send_plot([contact_plot])
-        return
-
-
-        index,regr,regr_contact=self.find_contact_point2(debug=True)
-        print regr
-        print regr_contact
-        raw_plot=self.current.curve.default_plots()[0]
-        xret=raw_plot.vectors[0][0]
-        #nc_line=[(item*regr[0])+regr[1] for item in x_nc]
-        nc_line=scipy.polyval(regr,xret)
-        c_line=scipy.polyval(regr_contact,xret)
-
-
-        contact_plot=self.current.curve.default_plots()[0]
-        contact_plot.add_set(xret, nc_line)
-        contact_plot.add_set(xret, c_line)
-        contact_plot.styles=[None,None,None,None]
-        #contact_plot.styles.append(None)
-        contact_plot.destination=1
-        self._send_plot([contact_plot])
index 1a4997218771a3ea47c40003f76243c0370c04bb..1637ce3f6ed12f139e41d2316921f56b05b3dca1 100644 (file)
@@ -121,7 +121,7 @@ class FlatPeaksCommand (Command):
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
-        z_data,d_data,params = self._setup(params)
+        z_data,d_data,params = self._setup(hooke, params)
         start_index = 0
         while (start_index < len(z_data)
                and z_data[start_index] < params['blind window']):
@@ -135,7 +135,7 @@ class FlatPeaksCommand (Command):
             peak.index += start_index
         outqueue.put(peaks)
 
-    def _setup(self, params):
+    def _setup(self, hooke, params):
         """Setup `params` from config and return the z piezo and
         deflection arrays.
         """
@@ -143,17 +143,17 @@ class FlatPeaksCommand (Command):
         if curve.info['experiment'] != VelocityClamp:
             raise Failure('%s operates on VelocityClamp experiments, not %s'
                           % (self.name, curve.info['experiment']))
-        for col in ['surface z piezo (m)', 'deflection (N)']:
+        for col in ['surface distance (m)', 'deflection (N)']:
             if col not in curve.data[0].info['columns']:
-                scale(curve)
+                scale(hooke, curve)
         data = None
-        for block in curve.data:
+        for i,block in enumerate(curve.data):
             if block.info['name'].startswith('retract'):
                 data = block
                 break
         if data == None:
             raise Failure('No retraction blocks in %s.' % curve)
-        z_data = data[:,data.info['columns'].index('surface z piezo (m)')]
+        z_data = data[:,data.info['columns'].index('surface distance (m)')]
         if 'flattened deflection (N)' in data.info['columns']:
             params['deflection column name'] = 'flattened deflection (N)'
         else:
@@ -197,6 +197,7 @@ class FlatFilterCommand (FilterCommand):
         self.arguments.extend(plugin._arguments)
 
     def filter(self, curve, hooke, inqueue, outqueue, params):
+        params['curve'] = curve
         inq = Queue()
         outq = Queue()
         filt_command = [c for c in hooke.commands
index 54498e4d5e8601bb211851805b61935d6a26c04a..2ab03727a7376387081c9c5d37a4d771ff90dd6f 100644 (file)
@@ -27,35 +27,426 @@ common velocity clamp analysis tasks.
 
 import copy
 
-from ..command import Command, Argument, Failure
+import numpy
+
+from ..command import Command, Argument, Failure, NullQueue
+from ..config import Setting
 from ..curve import Data
 from ..plugin import Builtin
+from ..util.fit import PoorFit, ModelFitter
+from .curve import CurveArgument
 
 
-# Utility functions
-
-def scale(curve):
+def scale(hooke, 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]
+    inqueue = None
+    outqueue = NullQueue()
     for i,block in enumerate(curve.data):
-        data = Data((block.shape[0], block.shape[1]+2), dtype=block.dtype)
-        data.info = copy.deepcopy(block.info)
-        data[:,:-2] = block
-        data.info['columns'].extend(['surface z piezo (m)', 'deflection (N)'])
-        z_data = data[:,data.info['columns'].index('z piezo (m)')]
-        d_data = data[:,data.info['columns'].index('deflection (m)')]
-        surface_offset = min(z_data) # TODO
-        data.info['surface offset (m)'] = surface_offset
-        data[:,-2] = z_data - surface_offset
-        data[:,-1] = d_data * data.info['spring constant (N/m)']
-        curve.data[i] = data
+        params = {'curve':curve, 'block':i}
+        contact._run(hooke, inqueue, outqueue, params)
+        force._run(hooke, inqueue, outqueue, params)
     return curve
 
+class SurfacePositionModel (ModelFitter):
+    """
+
+    The bilinear model is symmetric, but the parameter guessing and
+    sanity checks assume the contact region occurs for lower indicies
+    ("left of") the non-contact region.  We also assume that
+    tip-surface attractions produce positive deflections.
+
+    Notes
+    -----
+    Algorithm borrowed from WTK's `piezo package`_, specifically
+    from :func:`piezo.z_piezo_utils.analyzeSurfPosData`.
+
+    .. _piezo package:
+      http://www.physics.drexel.edu/~wking/code/git/git.php?p=piezo.git
+
+    Fits the data to the bilinear :method:`model`.
+
+    In order for this model to produce a satisfactory fit, there
+    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.
+
+        Notes
+        -----
+        .. math::
+    
+          y = \begin{cases}
+            p_0 + p_1 x                 & \text{if $x <= p_2$}, \\
+            p_0 + p_1 p_2 + p_3 (x-p_2) & \text{if $x >= p_2$}.
+              \end{cases}
+    
+        Where :math:`p_0` is a vertical offset, :math:`p_1` is the slope
+        of the first region, :math:`p_2` is the transition location, and
+        :math:`p_3` is the slope of the second region.
+        """
+        p = params  # convenient alias
+        if self.info.get('force zero non-contact slope', None) == True:
+            p = list(p)
+            p.append(0.)  # restore the non-contact slope parameter
+        r2 = numpy.round(p[2])
+        if r2 >= 1:
+            self._model_data[:r2] = p[0] + p[1] * numpy.arange(r2)
+        if r2 < len(self._data)-1:
+            self._model_data[r2:] = p[0] + p[1]*p[2] + p[3] * numpy.arange(len(self._data)-r2)
+        return self._model_data
+
+    def set_data(self, data, info=None):
+        super(SurfacePositionModel, self).set_data(data, info)
+        if info == None:
+            info = {}
+        self.info = info
+        self.info['min position'] = 0
+        self.info['max position'] = len(data)
+        self.info['max deflection'] = data.max()
+        self.info['min deflection'] = data.min()
+        self.info['position range'] = self.info['max position'] - self.info['min position']
+        self.info['deflection range'] = self.info['max deflection'] - self.info['min deflection']
+
+    def guess_initial_params(self, outqueue=None):
+        """Guess the initial parameters.
+
+        Notes
+        -----
+        We guess initial parameters such that the offset (:math:`p_1`)
+        matches the minimum deflection, the kink (:math:`p_2`) occurs in
+        the middle of the data, the initial (contact) slope (:math:`p_0`)
+        produces the maximum deflection at the left-most point, and the
+        final (non-contact) slope (:math:`p_3`) is zero.
+        """
+        left_offset = self.info['max deflection']
+        left_slope = 2*(-self.info['deflection range']
+                         /self.info['position range'])
+        kink_position = (self.info['max position']
+                         +self.info['min position'])/2.0
+        right_slope = 0
+        self.info['guessed contact slope'] = left_slope
+        params = [left_offset, left_slope, kink_position, right_slope]
+        if self.info.get('force zero non-contact slope', None) == True:
+            params = params[:-1]
+        return params
+
+    def guess_scale(self, params, outqueue=None):
+        """Guess the parameter scales.
+
+        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
+        (:math:`p_1`) as one tenth of the contact slope estimation,
+        and the final (non-contact) slope scale (:math:`p_3`) is as
+        one tenth of the initial slope scale.
+        """
+        offset_scale = self.info['deflection range']/10.
+        left_slope_scale = abs(params[1])/10.
+        kink_scale = self.info['position range']/10.
+        right_slope_scale = left_slope_scale/10.
+        scale = [offset_scale, left_slope_scale, kink_scale, right_slope_scale]
+        if self.info.get('force zero non-contact slope', None) == True:
+            scale = scale[:-1]
+        return scale
+
+    def fit(self, *args, **kwargs):
+        self.info['guessed contact slope'] = None
+        params = super(SurfacePositionModel, self).fit(*args, **kwargs)
+        if self.info.get('force zero non-contact slope', None) == True:
+            params = list(params)
+            params.append(0.)  # restore the non-contact slope parameter
+
+        # check that the fit is reasonable, see the :meth:`model` docstring
+        # for parameter descriptions.
+        if abs(params[3]*10) > abs(params[1]) :
+            raise PoorFit('Slope in non-contact region, or no slope in contact')
+        if params[2] < self.info['min position']+0.02*self.info['position range']:
+            raise poorFit(
+                'No kink (kink %g less than %g, need more space to left)'
+                % (params[2],
+                   self.info['min position']+0.02*self.info['position range']))
+        if params[2] > self.info['max position']-0.02*self.info['position range']:
+            raise poorFit(
+                'No kink (kink %g more than %g, need more space to right)'
+                % (params[2],
+                   self.info['max position']-0.02*self.info['position range']))
+        if (self.info['guessed contact slope'] != None
+            and params[3] < 0.5 * self.info['guessed contact slope']):
+            raise PoorFit('Too far')
+        return params
 
 class VelocityClampPlugin (Builtin):
     def __init__(self):
         super(VelocityClampPlugin, self).__init__(name='vclamp')
-        self._commands = []
-#            NextCommand(self), PreviousCommand(self), JumpCommand(self),
-#            ]
+        self._commands = [
+            SurfaceContactCommand(self), ForceCommand(self),
+            ]
+
+    def default_settings(self):
+        return [
+            Setting(section=self.setting_section, help=self.__doc__),
+            Setting(section=self.setting_section,
+                    option='surface contact point algorithm',
+                    value='wtk',
+                    help='Select the surface contact point algorithm.  See the documentation for descriptions of available algorithms.')
+            ]
+
+
+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.
+    """
+    def __init__(self, plugin):
+        super(SurfaceContactCommand, self).__init__(
+            name='zero block surface contact point',
+            arguments=[
+                CurveArgument,
+                Argument(name='block', aliases=['set'], type='int', default=0,
+                         help="""
+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()),
+                ],
+            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
+        # 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]+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(
+            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
+
+    def _find_contact_point(self, z_data, d_data, outqueue=None):
+        fn = getattr(self, '_find_contact_point_%s'
+                     % self.plugin.config['surface contact point algorithm'])
+        return fn(z_data, d_data, outqueue)
+
+    def _find_contact_point_fmms(self, z_data, d_data, outqueue=None):
+        """Algorithm by Francesco Musiani and Massimo Sandal.
+
+        Notes
+        -----
+        Algorithm:
+
+        * take care of the PicoForce trigger bug - exclude retraction portions with too high standard deviation
+        * fit the second half of the retraction curve to a line
+        * if the fit is not almost horizontal, take a smaller chunk and repeat
+        * otherwise, we have something horizontal
+        * so take the average of horizontal points and use it as a baseline
+
+        Then, start from the rise of the retraction curve and look at
+        the first point below the baseline.
+        """
+        if not plot:
+            plot=self.plots[0]
+
+        outplot=self.subtract_curves(1)
+        xret=outplot.vectors[1][0]
+        ydiff=outplot.vectors[1][1]
+
+        xext=plot.vectors[0][0]
+        yext=plot.vectors[0][1]
+        xret2=plot.vectors[1][0]
+        yret=plot.vectors[1][1]
+
+        #taking care of the picoforce trigger bug: we exclude portions of the curve that have too much
+        #standard deviation. yes, a lot of magic is here.
+        monster=True
+        monlength=len(xret)-int(len(xret)/20)
+        finalength=len(xret)
+        while monster:
+            monchunk=scipy.array(ydiff[monlength:finalength])
+            if abs(np.std(monchunk)) < 2e-10:
+                monster=False
+            else: #move away from the monster
+                monlength-=int(len(xret)/50)
+                finalength-=int(len(xret)/50)
+
+
+        #take half of the thing
+        endlength=int(len(xret)/2)
+
+        ok=False
+
+        while not ok:
+            xchunk=yext[endlength:monlength]
+            ychunk=yext[endlength:monlength]
+            regr=scipy.stats.linregress(xchunk,ychunk)[0:2]
+            #we stop if we found an almost-horizontal fit or if we're going too short...
+            #FIXME: 0.1 and 6 here are "magic numbers" (although reasonable)
+            if (abs(regr[1]) > 0.1) and ( endlength < len(xret)-int(len(xret)/6) ) :
+                endlength+=10
+            else:
+                ok=True
+
+
+        ymean=np.mean(ychunk) #baseline
+
+        index=0
+        point = ymean+1
+
+        #find the first point below the calculated baseline
+        while point > ymean:
+            try:
+                point=yret[index]
+                index+=1
+            except IndexError:
+                #The algorithm didn't find anything below the baseline! It should NEVER happen
+                index=0
+                return index
+
+        return index
+
+    def _find_contact_point_ms(self, z_data, d_data, outqueue=None):
+        """Algorithm by Massimo Sandal.
+
+        Notes
+        -----
+        WTK: At least the commits are by Massimo, and I see no notes
+        attributing the algorithm to anyone else.
+
+        Algorithm:
+
+        * ?
+        """
+        #raw_plot=self.current.curve.default_plots()[0]
+        raw_plot=self.plots[0]
+        '''xext=self.plots[0].vectors[0][0]
+        yext=self.plots[0].vectors[0][1]
+        xret2=self.plots[0].vectors[1][0]
+        yret=self.plots[0].vectors[1][1]
+        '''
+        xext=raw_plot.vectors[0][0]
+        yext=raw_plot.vectors[0][1]
+        xret2=raw_plot.vectors[1][0]
+        yret=raw_plot.vectors[1][1]
+
+        first_point=[xext[0], yext[0]]
+        last_point=[xext[-1], yext[-1]]
+
+        #regr=scipy.polyfit(first_point, last_point,1)[0:2]
+        diffx=abs(first_point[0]-last_point[0])
+        diffy=abs(first_point[1]-last_point[1])
+
+        #using polyfit results in numerical errors. good old algebra.
+        a=diffy/diffx
+        b=first_point[1]-(a*first_point[0])
+        baseline=scipy.polyval((a,b), xext)
+
+        ysub=[item-basitem for item,basitem in zip(yext,baseline)]
+
+        contact=ysub.index(min(ysub))
+
+        return xext,ysub,contact
+
+        #now, exploit a ClickedPoint instance to calculate index...
+        dummy=ClickedPoint()
+        dummy.absolute_coords=(x_intercept,y_intercept)
+        dummy.find_graph_coords(xret2,yret)
+
+        if debug:
+            return dummy.index, regr, regr_contact
+        else:
+            return dummy.index
+
+    def _find_contact_point_wtk(self, z_data, d_data, outqueue=None):
+        """Algorithm by W. Trevor King.
+
+        Notes
+        -----
+        Uses :func:`analyze_surf_pos_data_wtk` internally.
+        """
+        reverse = z_data[0] > z_data[-1]
+        if reverse == True:    # approaching, contact region on the right
+            d_data = d_data[::-1]
+        s = SurfacePositionModel(d_data)
+        s.info['force zero non-contact slope'] = True
+        offset,contact_slope,surface_index,non_contact_slope = s.fit(
+            outqueue=outqueue)
+        info = {
+            'offset': offset,
+            'contact slope': contact_slope,
+            'surface index': surface_index,
+            'non-contact slope': non_contact_slope,
+            'reversed': reverse,
+            }
+        deflection_offset = offset + contact_slope*surface_index,
+        if reverse == True:
+            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)`.
+    """
+    def __init__(self, plugin):
+        super(ForceCommand, self).__init__(
+            name='add block force array',
+            arguments=[
+                CurveArgument,
+                Argument(name='block', aliases=['set'], type='int', default=0,
+                         help="""
+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()),
+                ],
+            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
+        # 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('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
+
 
 class generalvclampCommands(object):
 
@@ -452,7 +843,6 @@ class generalvclampCommands(object):
         return contact_point, contact_point_index
 
 
-
     def baseline_points(self,peak_location, displayed_plot):
         clicks=self.config['baseline_clicks']
         if clicks==0: