Convert from "print ..." to "print(...)"
[hooke.git] / hooke / plugin / polymer_fit.py
index c3d715cac554771684db2673ecc8a4cc9054f52d..31036cd094266fc98832eda96f9d0b0227bda9bf 100644 (file)
@@ -1,25 +1,21 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2008-2010 Alberto Gomez-Casado
-#                         Fabrizio Benedetti
-#                         Massimo Sandal <devicerandom@gmail.com>
-#                         W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2010-2012 W. Trevor King <wking@tremily.us>
 #
 # This file is part of Hooke.
 #
-# Hooke is free software: you can redistribute it and/or modify it
-# under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
+# Hooke is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
 #
-# Hooke is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
-# Public License for more details.
+# Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
 #
-# You should have received a copy of the GNU Lesser General Public
-# License along with Hooke.  If not, see
-# <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
 
 """The ``polymer_fit`` module proviews :class:`PolymerFitPlugin` and
 serveral associated :class:`hooke.command.Command`\s for Fitting
@@ -28,6 +24,7 @@ velocity-clamp data to various polymer models (WLC, FJC, etc.).
 
 import copy
 from Queue import Queue
+import logging
 import StringIO
 import sys
 
@@ -40,6 +37,7 @@ from ..curve import Data
 from ..util.callback import is_iterable
 from ..util.fit import PoorFit, ModelFitter
 from ..util.peak import Peak
+from ..util.quickhull import qhull, points_inside_hull
 from ..util.si import join_data_label, split_data_label
 from . import Plugin, argument_to_setting
 from .curve import ColumnAccessCommand, ColumnAddingCommand
@@ -234,9 +232,9 @@ class FJC (ModelFitter):
     >>> outqueue = Queue()
     >>> L,a = model.fit(outqueue=outqueue)
     >>> fit_info = outqueue.get(block=False)
-    >>> print L
+    >>> print(L)
     3.5e-08
-    >>> print a
+    >>> print(a)
     2.5e-10
 
     Fit the example data with a one-parameter fit (`L`).  We introduce
@@ -244,9 +242,9 @@ class FJC (ModelFitter):
 
     >>> info['Kuhn length (m)'] = 2*a
     >>> model = FJC(d_data, info=info, rescale=True)
-    >>> L = model.fit(outqueue=outqueue)
+    >>> L, = model.fit(outqueue=outqueue)
     >>> fit_info = outqueue.get(block=False)
-    >>> print L  # doctest: +ELLIPSIS
+    >>> print(L)  # doctest: +ELLIPSIS
     3.199...e-08
     """
     def Lp(self, L):
@@ -340,11 +338,9 @@ class FJC (ModelFitter):
 
     def fit(self, *args, **kwargs):
         params = super(FJC, self).fit(*args, **kwargs)
-        if is_iterable(params):
-            params[0] = self.L(params[0])  # convert Lp -> L
+        params[0] = self.L(params[0])  # convert Lp -> L
+        if len(params) > 1:
             params[1] = abs(params[1])  # take the absolute value of `a`
-        else:  # params is a float
-            params = self.L(params)  # convert Lp -> L
         return params
 
     def guess_initial_params(self, outqueue=None):
@@ -497,9 +493,9 @@ class FJC_PEG (ModelFitter):
     >>> outqueue = Queue()
     >>> N,a = model.fit(outqueue=outqueue)
     >>> fit_info = outqueue.get(block=False)
-    >>> print N
+    >>> print(N)
     123.0
-    >>> print a
+    >>> print(a)
     7e-10
 
     Fit the example data with a one-parameter fit (`N`).  We introduce
@@ -507,9 +503,9 @@ class FJC_PEG (ModelFitter):
 
     >>> info['Kuhn length (m)'] = 2*kwargs['a']
     >>> model = FJC_PEG(d_data, info=info, rescale=True)
-    >>> N = model.fit(outqueue=outqueue)
+    >>> N, = model.fit(outqueue=outqueue)
     >>> fit_info = outqueue.get(block=False)
-    >>> print N  # doctest: +ELLIPSIS
+    >>> print(N)  # doctest: +ELLIPSIS
     96.931...
     """
     def Lr(self, L):
@@ -613,11 +609,9 @@ class FJC_PEG (ModelFitter):
 
     def fit(self, *args, **kwargs):
         params = super(FJC_PEG, self).fit(*args, **kwargs)
-        if is_iterable(params):
-            params[0] = self.L(params[0])  # convert Nr -> N
+        params[0] = self.L(params[0])  # convert Nr -> N
+        if len(params) > 1:
             params[1] = abs(params[1])  # take the absolute value of `a`
-        else:  # params is a float
-            params = self.L(params)  # convert Nr -> N
         return params
 
     def guess_initial_params(self, outqueue=None):
@@ -716,9 +710,9 @@ class WLC (ModelFitter):
     >>> outqueue = Queue()
     >>> L,p = model.fit(outqueue=outqueue)
     >>> fit_info = outqueue.get(block=False)
-    >>> print L
+    >>> print(L)
     3.5e-08
-    >>> print p
+    >>> print(p)
     2.5e-10
 
     Fit the example data with a one-parameter fit (`L`).  We introduce
@@ -726,9 +720,9 @@ class WLC (ModelFitter):
 
     >>> info['persistence length (m)'] = 2*p
     >>> model = WLC(d_data, info=info, rescale=True)
-    >>> L = model.fit(outqueue=outqueue)
+    >>> L, = model.fit(outqueue=outqueue)
     >>> fit_info = outqueue.get(block=False)
-    >>> print L  # doctest: +ELLIPSIS
+    >>> print(L)  # doctest: +ELLIPSIS
     3.318...e-08
     """
     def Lp(self, L):
@@ -822,11 +816,9 @@ class WLC (ModelFitter):
 
     def fit(self, *args, **kwargs):
         params = super(WLC, self).fit(*args, **kwargs)
-        if is_iterable(params):
-            params[0] = self.L(params[0])  # convert Lp -> L
+        params[0] = self.L(params[0])  # convert Lp -> L
+        if len(params) > 1:
             params[1] = abs(params[1])  # take the absolute value of `p`
-        else:  # params is a float
-            params = self.L(params)  # convert Lp -> L
         return params
 
     def guess_initial_params(self, outqueue=None):
@@ -940,11 +932,18 @@ Name (without units) for storing the fit parameters in the `.info` dictionary.
                          help="""
 Indicies of points bounding the selected data.
 """.strip()),
+                Argument(name='peak extraction method', default='convex hull',
+                         help=(
+                        'Select the method used to extract the peak '
+                        'deflection from the fitted model.  `convex hull`, '
+                        '`peak data`, or `peak model`.')),
                 ] + plugin._arguments,
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
-        params = self.__setup_params(hooke, params)
+        self._add_to_command_stack(params)
+        log = logging.getLogger('hooke')
+        params = self._setup_params(hooke, params)
         data = self._block(hooke, params)
         model = params['polymer model']
         dist_data = self._get_column(
@@ -954,13 +953,27 @@ Indicies of points bounding the selected data.
         start,stop = params['bounds']
         tension_data,ps = self.fit_polymer_model(
             params, dist_data, def_data, start, stop, outqueue)
+        if params['peak extraction method'] == 'convex hull':
+            peak_def,hull = self._convex_hull_deflection(
+                distance_data=dist_data, deflection_data=def_data,
+                deflection_model=tension_data, start=start, stop=stop,
+                outqueue=outqueue)
+        elif params['peak extraction method'] == 'peak data':
+            peak_def = numpy.nanmax(def_data[start:stop])
+        elif params['peak extraction method'] == 'peak model':
+            peak_def = numpy.nanmax(tension_data[start:stop])
+        else:
+            raise ValueError(params['peak extraction method'])
+        log.debug('{}: {}'.format(params['peak extraction method'], peak_def))
+        ps['peak deflection'] = peak_def
+        ps['peak extraction method'] = params['peak extraction method']
+        ps['model'] = model
         data.info[params['fit parameters info name']] = ps
-        data.info[params['fit parameters info name']]['model'] = model
         self._set_column(hooke=hooke, params=params,
                          column_name='output tension column',
                          values=tension_data)
 
-    def __setup_params(self, hooke, params):
+    def _setup_params(self, hooke, params):
         for key,value in params.items():
             if value == None:  # Use configured default value.
                 params[key] = self.plugin.config[key]
@@ -995,7 +1008,6 @@ Indicies of points bounding the selected data.
         queue = Queue()
         params = model.fit(outqueue=queue)
         if True:  # TODO: if Kuhn length fixed
-            params = [params]
             a = info['Kuhn length (m)']
         else:
             a = params[1]
@@ -1022,7 +1034,6 @@ Indicies of points bounding the selected data.
         queue = Queue()
         params = model.fit(outqueue=queue)
         if True:  # TODO: if Kuhn length fixed
-            params = [params]
             a = info['Kuhn length (m)']
         else:
             a = params[1]
@@ -1048,7 +1059,6 @@ Indicies of points bounding the selected data.
         queue = Queue()
         params = model.fit(outqueue=queue)
         if True:  # TODO: if persistence length fixed
-            params = [params]
             p = info['persistence length (m)']
         else:
             p = params[1]
@@ -1063,6 +1073,39 @@ Indicies of points bounding the selected data.
         f_data[start:stop] = WLC_fn(dist_data[start:stop], T=T, L=L, p=p)
         return [f_data, info]
 
+    def _convex_hull_deflection(self, distance_data, deflection_data,
+                                deflection_model, start, stop, outqueue=None):
+        """Return the last model deflection point inside the data hull.
+
+        If the `stop` point is a bit past it's ideal location, the
+        rapid rise of some polymer models can lead to artificially
+        high peak deflections if the largest distance value is plugged
+        into the polymer model directly.  As a more robust estimation
+        of the peak deflection, we calculate the convex hull
+        surrounding the distance-deflection data and return the last
+        model deflection point that is inside that hull.
+        """
+        distance_data = distance_data[start:stop]
+        deflection_data = deflection_data[start:stop]
+        deflection_model = deflection_model[start:stop]
+        unfolds = []
+        hulls = []
+        data = numpy.array(
+            [[x,y] for x,y in zip(distance_data, deflection_data)])
+        model = numpy.array(
+            [[x,y] for x,y in zip(distance_data, deflection_model)])
+        hull = qhull(data)
+        inside = points_inside_hull(hull, model)
+        # distance data is not necessarily monatonic
+        index_inside = [j for j,i in enumerate(inside) if i is True]
+        distance_inside = [(distance_data[i],i) for i in index_inside]
+        if distance_inside:
+            peak_dist,peak_index = max(distance_inside)
+            unfold = deflection_model[peak_index]
+        else:  # all model points are outside the hull?!
+            unfold = None
+        return (unfold, hull)
+
 
 class PolymerFitPeaksCommand (ColumnAccessCommand):
     """Polymer model (WLC, FJC, etc.) fitting.
@@ -1088,9 +1131,9 @@ Index of the selected peak in the list of peaks.  Use `None` to fit all peaks.
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
+        self._add_to_command_stack(params)
         data = self._block(hooke, params)
-        fit_command = [c for c in hooke.commands
-                       if c.name=='polymer fit'][0]
+        fit_command = hooke.command_by_name['polymer fit']
         inq = Queue()
         outq = Queue()
         curve = params['curve']
@@ -1104,6 +1147,7 @@ Index of the selected peak in the list of peaks.  Use `None` to fit all peaks.
                 p['bounds'] = [peak.index, peak.post_index()]
                 p['output tension column'] = peak.name
                 p['fit parameters info name'] = peak.name
+                p['stack'] = False
                 fit_command.run(hooke, inq, outq, **p)
             ret = outq.get()
             if not isinstance(ret, Success):
@@ -1151,6 +1195,7 @@ instead.
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
+        self._add_to_command_stack(params)
         data = self._block(hooke, params)
         dist_data = self._get_column(
             hooke=hooke, params=params, column_name='distance column')