1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
5 # This file is part of Hooke.
7 # Hooke is free software: you can redistribute it and/or modify it under the
8 # terms of the GNU Lesser General Public License as published by the Free
9 # Software Foundation, either version 3 of the License, or (at your option) any
12 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with Hooke. If not, see <http://www.gnu.org/licenses/>.
20 """The ``polymer_fit`` module proviews :class:`PolymerFitPlugin` and
21 serveral associated :class:`hooke.command.Command`\s for Fitting
22 velocity-clamp data to various polymer models (WLC, FJC, etc.).
26 from Queue import Queue
32 from scipy.optimize import newton
34 from ..command import Command, Argument, Success, Failure
35 from ..config import Setting
36 from ..curve import Data
37 from ..util.callback import is_iterable
38 from ..util.fit import PoorFit, ModelFitter
39 from ..util.peak import Peak
40 from ..util.quickhull import qhull, points_inside_hull
41 from ..util.si import join_data_label, split_data_label
42 from . import Plugin, argument_to_setting
43 from .curve import ColumnAccessCommand, ColumnAddingCommand
46 kB = 1.3806503e-23 # Joules/Kelvin
50 """Hyperbolic cotangent.
54 >>> coth(1.19967874) # doctest: +ELLIPSIS
62 \coth(z) \equiv \frac{\exp(z) + \exp(-z)}{\exp(z) - \exp(-z)}
66 http://mathworld.wolfram.com/HyperbolicCotangent.html
68 return 1.0/numpy.tanh(z)
71 """Inverse hyperbolic cotangent.
75 >>> arccoth(1.19967874) # doctest: +ELLIPSIS
77 >>> arccoth(coth(numpy.pi)) # doctest: +ELLIPSIS
82 Inverting from the :func:`definition <coth>`.
85 z \equiv \atanh\left( \frac{1}{\coth(z)} \right)
87 return numpy.arctanh(1.0/z)
94 >>> Langevin(numpy.pi) # doctest: +ELLIPSIS
99 From `Wikipedia`_ or Hatfield [#]_
102 L(z) \equiv \coth(z) - \frac{1}{z}
105 http://en.wikipedia.org/wiki/Brillouin_and_Langevin_functions#Langevin_Function
107 .. [#]: J.W. Hatfield and S.R. Quake
108 "Dynamic Properties of an Extended Polymer in Solution."
109 Phys. Rev. Lett. 1999.
110 doi: `10.1103/PhysRevLett.82.3548 <http://dx.doi.org/10.1103/PhysRevLett.82.3548>`
112 return coth(z) - 1.0/z
114 def inverse_Langevin(z, extreme=1.0 - 1e-8):
115 """Inverse Langevin function.
119 z : float or array_like
120 object whose inverse Langevin will be returned
122 value (close to one) for which we assume the inverse is +/-
123 infinity. This avoids problems with Newton-Raphson
124 convergence in regions with low slope.
128 >>> inverse_Langevin(Langevin(numpy.pi)) # doctest: +ELLIPSIS
130 >>> inverse_Langevin(Langevin(numpy.array([1,2,3]))) # doctest: +ELLIPSIS
135 We approximate the inverse Langevin via Newton's method
136 (:func:`scipy.optimize.newton`). For the starting point, we take
137 the first three terms from the Taylor series (from `Wikipedia`_).
140 L^{-1}(z) = 3z + \frac{9}{5}z^3 + \frac{297}{175}z^5 + \dots
143 http://en.wikipedia.org/wiki/Brillouin_and_Langevin_functions#Langevin_Function
146 ret = numpy.ndarray(shape=z.shape, dtype=z.dtype)
147 for i,z_i in enumerate(z):
148 ret[i] = inverse_Langevin(z_i)
154 # Catch stdout since sometimes the newton function prints exit
155 # messages to stdout, e.g. "Tolerance of %s reached".
157 tmp_stdout = StringIO.StringIO()
158 sys.stdout = tmp_stdout
159 ret = newton(func=lambda x: Langevin(x)-z,
160 x0=3*z + 9./5.*z**3 + 297./175.*z**5,)
162 output = tmp_stdout.getvalue()
165 def FJC_fn(x_data, T, L, a):
166 """The freely jointed chain model.
171 x values for which the WLC tension should be calculated.
173 temperature in Kelvin.
175 contour length in meters.
177 Kuhn length in meters.
186 >>> FJC_fn(numpy.array([1e-9, 5e-9, 10e-9], dtype=numpy.float),
187 ... T=300, L=15e-9, a=2.5e-10) # doctest: +ELLIPSIS
188 array([ 3.322...-12, 1.78...e-11, 4.889...e-11])
192 The freely jointed chain model is
195 F(x) = \frac{k_B T}{a} L^{-1}\left( \frac{x}{L} \right)
197 where :math:`L^{-1}` is the :func:`inverse_Langevin`, :math:`a` is
198 the Kuhn length, and :math:`L` is the contour length [#]_. For
199 the inverse formulation (:math:`x(F)`), see Ray and Akhremitchev [#]_.
202 .. [#]: J.W. Hatfield and S.R. Quake
203 "Dynamic Properties of an Extended Polymer in Solution."
204 Phys. Rev. Lett. 1999.
205 doi: `10.1103/PhysRevLett.82.3548 <http://dx.doi.org/10.1103/PhysRevLett.82.3548>`
207 .. [#] C. Ray and B.B. Akhremitchev.
208 `"Fitting Force Curves by the Extended Freely Jointed Chain Model" <http://www.chem.duke.edu/~boris/research/force_spectroscopy/fit_efjc.pdf>`
210 return kB*T / a * inverse_Langevin(x_data/L)
212 class FJC (ModelFitter):
213 """Fit the data to a freely jointed chain.
217 Generate some example data
220 >>> L = 35e-9 # meters
221 >>> a = 2.5e-10 # meters
222 >>> x_data = numpy.linspace(10e-9, 30e-9, num=20)
223 >>> d_data = FJC_fn(x_data, T=T, L=L, a=a)
225 Fit the example data with a two-parameter fit (`L` and `a`).
228 ... 'temperature (K)': T,
229 ... 'x data (m)': x_data,
231 >>> model = FJC(d_data, info=info, rescale=True)
232 >>> outqueue = Queue()
233 >>> L,a = model.fit(outqueue=outqueue)
234 >>> fit_info = outqueue.get(block=False)
240 Fit the example data with a one-parameter fit (`L`). We introduce
241 some error in our fixed Kuhn length for fun.
243 >>> info['Kuhn length (m)'] = 2*a
244 >>> model = FJC(d_data, info=info, rescale=True)
245 >>> L, = model.fit(outqueue=outqueue)
246 >>> fit_info = outqueue.get(block=False)
247 >>> print L # doctest: +ELLIPSIS
251 """To avoid invalid values of `L`, we reparametrize with `Lp`.
256 contour length in meters
261 rescaled version of `L`.
265 >>> x_data = numpy.linspace(1e-9, 20e-9, num=10)
266 >>> model = FJC(data=x_data, info={'x data (m)':x_data})
267 >>> model.Lp(20e-9) # doctest: +ELLIPSIS
269 >>> model.Lp(19e-9) # doctest: +ELLIPSIS
271 >>> model.Lp(21e-9) # doctest: +ELLIPSIS
273 >>> model.Lp(100e-9) # doctest: +ELLIPSIS
278 The rescaling is designed to limit `L` to values strictly
279 greater than the maximum `x` data value, so we use
282 L = [\exp(L_p))+1] * x_\text{max}
284 which has the inverse
287 L_p = \ln(L/x_\text{max}-1)
289 This will obviously effect the interpretation of the fit's covariance matrix.
291 x_max = self.info['x data (m)'].max()
292 return numpy.log(L/x_max - 1)
295 """Inverse of :meth:`Lp`.
300 rescaled version of `L`
305 contour length in meters
309 >>> x_data = numpy.linspace(1e-9, 20e-9, num=10)
310 >>> model = FJC(data=x_data, info={'x data (m)':x_data})
311 >>> model.L(model.Lp(21e-9)) # doctest: +ELLIPSIS
313 >>> model.L(model.Lp(100e-9)) # doctest: +ELLIPSIS
316 x_max = self.info['x data (m)'].max()
317 return (numpy.exp(Lp)+1)*x_max
319 def model(self, params):
320 """Extract the relavant arguments and use :func:`FJC_fn`.
324 params : list of floats
325 `[Lp, a]`, where the presence of `a` is optional.
327 # setup convenient aliases
328 T = self.info['temperature (K)']
329 x_data = self.info['x data (m)']
330 L = self.L(params[0])
334 a = self.info['Kuhn length (m)']
336 self._model_data[:] = FJC_fn(x_data, T, L, a)
337 return self._model_data
339 def fit(self, *args, **kwargs):
340 params = super(FJC, self).fit(*args, **kwargs)
341 params[0] = self.L(params[0]) # convert Lp -> L
343 params[1] = abs(params[1]) # take the absolute value of `a`
346 def guess_initial_params(self, outqueue=None):
347 """Guess initial fitting parameters.
352 A guess at the reparameterized contour length in meters.
354 A guess at the Kuhn length in meters. If `.info` has a
355 setting for `'Kuhn length (m)'`, `a` is not returned.
357 x_max = self.info['x data (m)'].max()
358 a_given = 'Kuhn length (m)' in self.info
360 a = self.info['Kuhn length (m)']
370 def inverse_FJC_PEG_fn(F_data, T=300, N=1, k=150., Lp=3.58e-10, Lh=2.8e-10, dG=3., a=7e-10):
371 """Inverse poly(ethylene-glycol) adjusted extended FJC model.
375 >>> kwargs = {'T':300.0, 'N':1, 'k':150.0, 'Lp':3.58e-10, 'Lh':2.8e-10,
376 ... 'dG':3.0, 'a':7e-10}
377 >>> inverse_FJC_PEG_fn(F_data=200e-12, **kwargs) # doctest: +ELLIPSIS
382 The function is that proposed by F. Oesterhelt, et al. [#]_.
385 x(F) = N_\text{S} \cdot \left[
387 \frac{L_\text{planar}}{
388 \exp\left(-\Delta G/k_B T\right) + 1}
389 + \frac{L_\text{helical}}{
390 \exp\left(+\Delta G/k_B T\right) + 1}
392 \coth\left(\frac{Fa}{k_B T}\right)
395 + \frac{F}{K_\text{S}}
397 where :math:`N_\text{S}` is the number of segments,
398 :math:`K_\text{S}` is the segment elasticicty,
399 :math:`L_\text{planar}` is the ttt contour length,
400 :math:`L_\text{helical}` is the ttg contour length,
401 :math:`\Delta G` is the Gibbs free energy difference
402 :math:`G_\text{planar}-G_\text{helical}`,
403 :math:`a` is the Kuhn length,
404 and :math:`F` is the chain tension.
406 .. [#]: F. Oesterhelt, M. Rief, and H.E. Gaub.
407 "Single molecule force spectroscopy by AFM indicates helical
408 structure of poly(ethylene-glycol) in water."
409 New Journal of Physics. 1999.
410 doi: `10.1088/1367-2630/1/1/006 <http://dx.doi.org/10.1088/1367-2630/1/1/006>`
414 g = (dG - F_data*(Lp-Lh)) / kBT
416 return N * ((Lp/(numpy.exp(-g)+1) + Lh/(numpy.exp(+g)+1)) * (coth(z)-1./z)
419 def FJC_PEG_fn(x_data, T, N, k, Lp, Lh, dG, a):
420 """Poly(ethylene-glycol) adjusted extended FJC model.
424 z : float or array_like
425 object whose inverse Langevin will be returned
427 value (close to one) for which we assume the inverse is +/-
428 infinity. This avoids problems with Newton-Raphson
429 convergence in regions with low slope.
433 >>> kwargs = {'T':300.0, 'N':1, 'k':150.0, 'Lp':3.58e-10, 'Lh':2.8e-10,
434 ... 'dG':3.0, 'a':7e-10}
435 >>> FJC_PEG_fn(inverse_FJC_PEG_fn(1e-11, **kwargs), **kwargs) # doctest: +ELLIPSIS
437 >>> FJC_PEG_fn(inverse_FJC_PEG_fn(3.4e-10, **kwargs), **kwargs) # doctest: +ELLIPSIS
439 >>> FJC_PEG_fn(numpy.array([1e-10,2e-10,3e-10]), **kwargs) # doctest: +ELLIPSIS
440 array([ 5.207...e-12, 1.257...e-11, 3.636...e-11])
441 >>> kwargs['N'] = 123
442 >>> FJC_PEG_fn(numpy.array([1e-8,2e-8,3e-8]), **kwargs) # doctest: +ELLIPSIS
443 array([ 4.160...e-12, 9.302...e-12, 1.830...e-11])
447 We approximate the PEG adjusted eFJC via Newton's method
448 (:func:`scipy.optimize.newton`). For the starting point, we use
449 the standard FJC function with an averaged contour length.
451 kwargs = {'T':T, 'N':N, 'k':k, 'Lp':Lp, 'Lh':Lh, 'dG':dG, 'a':a}
452 if is_iterable(x_data):
453 ret = numpy.ndarray(shape=x_data.shape, dtype=x_data.dtype)
454 for i,x in enumerate(x_data):
455 ret[i] = FJC_PEG_fn(x, **kwargs)
459 # Approximate with the simple FJC_fn()
462 while guess == numpy.inf:
463 guess = FJC_fn(x_data, T=T, L=L, a=a)
466 fn = lambda f: inverse_FJC_PEG_fn(guess*abs(f), **kwargs) - x_data
467 ret = guess*abs(newton(func=fn, x0=1.0))
470 class FJC_PEG (ModelFitter):
471 """Fit the data to an poly(ethylene-glycol) adjusted extended FJC.
475 Generate some example data
477 >>> kwargs = {'T':300.0, 'N':123, 'k':150.0, 'Lp':3.58e-10, 'Lh':2.8e-10,
478 ... 'dG':3.0, 'a':7e-10}
479 >>> x_data = numpy.linspace(10e-9, 30e-9, num=20)
480 >>> d_data = FJC_PEG_fn(x_data, **kwargs)
482 Fit the example data with a two-parameter fit (`N` and `a`).
485 ... 'temperature (K)': kwargs['T'],
486 ... 'x data (m)': x_data,
487 ... 'section elasticity (N/m)': kwargs['k'],
488 ... 'planar section length (m)': kwargs['Lp'],
489 ... 'helical section length (m)': kwargs['Lh'],
490 ... 'Gibbs free energy difference (Gp - Gh) (kBT)': kwargs['dG'],
492 >>> model = FJC_PEG(d_data, info=info, rescale=True)
493 >>> outqueue = Queue()
494 >>> N,a = model.fit(outqueue=outqueue)
495 >>> fit_info = outqueue.get(block=False)
501 Fit the example data with a one-parameter fit (`N`). We introduce
502 some error in our fixed Kuhn length for fun.
504 >>> info['Kuhn length (m)'] = 2*kwargs['a']
505 >>> model = FJC_PEG(d_data, info=info, rescale=True)
506 >>> N, = model.fit(outqueue=outqueue)
507 >>> fit_info = outqueue.get(block=False)
508 >>> print N # doctest: +ELLIPSIS
512 """To avoid invalid values of `L`, we reparametrize with `Lr`.
517 contour length in meters
522 rescaled version of `L`.
526 >>> x_data = numpy.linspace(1e-9, 20e-9, num=10)
527 >>> model = FJC_PEG(data=x_data, info={'x data (m)':x_data})
528 >>> model.Lr(20e-9) # doctest: +ELLIPSIS
530 >>> model.Lr(19e-9) # doctest: +ELLIPSIS
532 >>> model.Lr(21e-9) # doctest: +ELLIPSIS
534 >>> model.Lr(100e-9) # doctest: +ELLIPSIS
539 The rescaling is designed to limit `L` to values strictly
540 greater than zero, so we use
543 L = \exp(L_p) * x_\text{max}
545 which has the inverse
548 L_p = \ln(L/x_\text{max})
550 This will obviously effect the interpretation of the fit's covariance matrix.
552 x_max = self.info['x data (m)'].max()
553 return numpy.log(L/x_max)
556 """Inverse of :meth:`Lr`.
561 rescaled version of `L`
566 contour length in meters
570 >>> x_data = numpy.linspace(1e-9, 20e-9, num=10)
571 >>> model = FJC_PEG(data=x_data, info={'x data (m)':x_data})
572 >>> model.L(model.Lr(21e-9)) # doctest: +ELLIPSIS
574 >>> model.L(model.Lr(100e-9)) # doctest: +ELLIPSIS
577 x_max = self.info['x data (m)'].max()
578 return numpy.exp(Lr)*x_max
582 'T': self.info['temperature (K)'],
583 'k': self.info['section elasticity (N/m)'],
584 'Lp': self.info['planar section length (m)'],
585 'Lh': self.info['helical section length (m)'],
586 'dG': self.info['Gibbs free energy difference (Gp - Gh) (kBT)'],
589 def model(self, params):
590 """Extract the relavant arguments and use :func:`FJC_PEG_fn`.
594 params : list of floats
595 `[N, a]`, where the presence of `a` is optional.
597 # setup convenient aliases
598 T = self.info['temperature (K)']
599 x_data = self.info['x data (m)']
600 N = self.L(params[0])
604 a = self.info['Kuhn length (m)']
605 kwargs = self._kwargs()
607 self._model_data[:] = FJC_PEG_fn(x_data, N=N, a=a, **kwargs)
608 return self._model_data
610 def fit(self, *args, **kwargs):
611 params = super(FJC_PEG, self).fit(*args, **kwargs)
612 params[0] = self.L(params[0]) # convert Nr -> N
614 params[1] = abs(params[1]) # take the absolute value of `a`
617 def guess_initial_params(self, outqueue=None):
618 """Guess initial fitting parameters.
623 A guess at the section count.
625 A guess at the Kuhn length in meters. If `.info` has a
626 setting for `'Kuhn length (m)'`, `a` is not returned.
628 x_max = self.info['x data (m)'].max()
629 a_given = 'Kuhn length (m)' in self.info
631 a = self.info['Kuhn length (m)']
634 f_max = self._data.max()
635 kwargs = self._kwargs()
637 x_section = inverse_FJC_PEG_fn(F_data=f_max, **kwargs)
638 N = x_max / x_section;
645 def WLC_fn(x_data, T, L, p):
646 """The worm like chain interpolation model.
651 x values for which the WLC tension should be calculated.
653 temperature in Kelvin.
655 contour length in meters.
657 persistence length in meters.
666 >>> WLC_fn(numpy.array([1e-9, 5e-9, 10e-9], dtype=numpy.float),
667 ... T=300, L=15e-9, p=2.5e-10) # doctest: +ELLIPSIS
668 array([ 1.717...e-12, 1.070...e-11, 4.418...e-11])
672 The function is the simple polynomial worm like chain
673 interpolation forumula proposed by C. Bustamante, et al. [#]_.
676 F(x) = \frac{k_B T}{p} \left[
677 \frac{1}{4}\left( \frac{1}{(1-x/L)^2} - 1 \right)
681 .. [#] C. Bustamante, J.F. Marko, E.D. Siggia, and S.Smith.
682 "Entropic elasticity of lambda-phage DNA."
684 doi: `10.1126/science.8079175 <http://dx.doi.org/10.1126/science.8079175>`
687 scaled_data = x_data / L
688 return a * (0.25*((1-scaled_data)**-2 - 1) + scaled_data)
690 class WLC (ModelFitter):
691 """Fit the data to a worm like chain.
695 Generate some example data
698 >>> L = 35e-9 # meters
699 >>> p = 2.5e-10 # meters
700 >>> x_data = numpy.linspace(10e-9, 30e-9, num=20)
701 >>> d_data = WLC_fn(x_data, T=T, L=L, p=p)
703 Fit the example data with a two-parameter fit (`L` and `p`).
706 ... 'temperature (K)': T,
707 ... 'x data (m)': x_data,
709 >>> model = WLC(d_data, info=info, rescale=True)
710 >>> outqueue = Queue()
711 >>> L,p = model.fit(outqueue=outqueue)
712 >>> fit_info = outqueue.get(block=False)
718 Fit the example data with a one-parameter fit (`L`). We introduce
719 some error in our fixed persistence length for fun.
721 >>> info['persistence length (m)'] = 2*p
722 >>> model = WLC(d_data, info=info, rescale=True)
723 >>> L, = model.fit(outqueue=outqueue)
724 >>> fit_info = outqueue.get(block=False)
725 >>> print L # doctest: +ELLIPSIS
729 """To avoid invalid values of `L`, we reparametrize with `Lp`.
734 contour length in meters
739 rescaled version of `L`.
743 >>> x_data = numpy.linspace(1e-9, 20e-9, num=10)
744 >>> model = WLC(data=x_data, info={'x data (m)':x_data})
745 >>> model.Lp(20e-9) # doctest: +ELLIPSIS
747 >>> model.Lp(19e-9) # doctest: +ELLIPSIS
749 >>> model.Lp(21e-9) # doctest: +ELLIPSIS
751 >>> model.Lp(100e-9) # doctest: +ELLIPSIS
756 The rescaling is designed to limit `L` to values strictly
757 greater than the maximum `x` data value, so we use
760 L = [\exp(L_p))+1] * x_\text{max}
762 which has the inverse
765 L_p = \ln(L/x_\text{max}-1)
767 This will obviously effect the interpretation of the fit's covariance matrix.
769 x_max = self.info['x data (m)'].max()
770 return numpy.log(L/x_max - 1)
773 """Inverse of :meth:`Lp`.
778 rescaled version of `L`
783 contour length in meters
787 >>> x_data = numpy.linspace(1e-9, 20e-9, num=10)
788 >>> model = WLC(data=x_data, info={'x data (m)':x_data})
789 >>> model.L(model.Lp(21e-9)) # doctest: +ELLIPSIS
791 >>> model.L(model.Lp(100e-9)) # doctest: +ELLIPSIS
794 x_max = self.info['x data (m)'].max()
795 return (numpy.exp(Lp)+1)*x_max
797 def model(self, params):
798 """Extract the relavant arguments and use :func:`WLC_fn`.
802 params : list of floats
803 `[Lp, p]`, where the presence of `p` is optional.
805 # setup convenient aliases
806 T = self.info['temperature (K)']
807 x_data = self.info['x data (m)']
808 L = self.L(params[0])
812 p = self.info['persistence length (m)']
814 self._model_data[:] = WLC_fn(x_data, T, L, p)
815 return self._model_data
817 def fit(self, *args, **kwargs):
818 params = super(WLC, self).fit(*args, **kwargs)
819 params[0] = self.L(params[0]) # convert Lp -> L
821 params[1] = abs(params[1]) # take the absolute value of `p`
824 def guess_initial_params(self, outqueue=None):
825 """Guess initial fitting parameters.
830 A guess at the reparameterized contour length in meters.
832 A guess at the persistence length in meters. If `.info`
833 has a setting for `'persistence length (m)'`, `p` is not
836 x_max = self.info['x data (m)'].max()
837 p_given = 'persistence length (m)' in self.info
839 p = self.info['persistence length (m)']
849 class PolymerFitPlugin (Plugin):
850 """Polymer model (WLC, FJC, etc.) fitting.
853 super(PolymerFitPlugin, self).__init__(name='polymer_fit')
854 self._arguments = [ # For Command initialization
855 Argument('polymer model', default='WLC', help="""
856 Select the default polymer model for 'polymer fit'. See the
857 documentation for descriptions of available algorithms.
859 Argument('FJC Kuhn length', type='float', default=4e-10,
860 help='Kuhn length in meters'),
861 Argument('FJC_PEG Kuhn length', type='float', default=4e-10,
862 help='Kuhn length in meters'),
863 Argument('FJC_PEG elasticity', type='float', default=150.0,
864 help='Elasticity of a PEG segment in Newtons per meter.'),
865 Argument('FJC_PEG delta G', type='float', default=3.0, help="""
866 Gibbs free energy difference between trans-trans-trans (ttt) and
867 trans-trans-gauche (ttg) PEG states in units of kBT.
869 Argument('FJC_PEG L_helical', type='float', default=2.8e-10,
870 help='Contour length of PEG in the ttg state.'),
871 Argument('FJC_PEG L_planar', type='float', default=3.58e-10,
872 help='Contour length of PEG in the ttt state.'),
873 Argument('WLC persistence length', type='float', default=4e-10,
874 help='Persistence length in meters'),
877 Setting(section=self.setting_section, help=self.__doc__)]
878 for argument in self._arguments:
879 self._settings.append(argument_to_setting(
880 self.setting_section, argument))
881 argument.default = None # if argument isn't given, use the config.
882 self._input_columns = [
883 ('distance column', 'cantilever adjusted extension (m)',
885 Name of the column to use as the surface position input.
887 ('deflection column', 'deflection (N)',
889 Name of the column to use as the deflection input.
893 PolymerFitCommand(self), PolymerFitPeaksCommand(self),
894 TranslateFlatPeaksCommand(self),
897 def dependencies(self):
900 def default_settings(self):
901 return self._settings
904 class PolymerFitCommand (ColumnAddingCommand):
905 """Polymer model (WLC, FJC, etc.) fitting.
907 Fits an entropic elasticity function to a given chunk of the
908 curve. Fit quality compares the residual with the thermal noise
909 (a good fit should be 1 or less).
911 Because the models are complicated and varied, you should
912 configure the command by setting configuration options instead of
913 using command arguments. TODO: argument_to_setting()
915 def __init__(self, plugin):
916 super(PolymerFitCommand, self).__init__(
918 columns=plugin._input_columns,
920 ('output tension column', 'polymer tension',
922 Name of the column (without units) to use as the polymer tension output.
926 Argument(name='fit parameters info name', type='string',
927 default='polymer fit',
929 Name (without units) for storing the fit parameters in the `.info` dictionary.
931 Argument(name='bounds', type='point', optional=False, count=2,
933 Indicies of points bounding the selected data.
935 Argument(name='peak extraction method', default='convex hull',
937 'Select the method used to extract the peak '
938 'deflection from the fitted model. `convex hull`, '
939 '`peak data`, or `peak model`.')),
940 ] + plugin._arguments,
941 help=self.__doc__, plugin=plugin)
943 def _run(self, hooke, inqueue, outqueue, params):
944 log = logging.getLogger('hooke')
945 params = self._setup_params(hooke, params)
946 data = self._block(hooke, params)
947 model = params['polymer model']
948 dist_data = self._get_column(
949 hooke=hooke, params=params, column_name='distance column')
950 def_data = self._get_column(
951 hooke=hooke, params=params, column_name='deflection column')
952 start,stop = params['bounds']
953 tension_data,ps = self.fit_polymer_model(
954 params, dist_data, def_data, start, stop, outqueue)
955 if params['peak extraction method'] == 'convex hull':
956 peak_def,hull = self._convex_hull_deflection(
957 distance_data=dist_data, deflection_data=def_data,
958 deflection_model=tension_data, start=start, stop=stop,
960 elif params['peak extraction method'] == 'peak data':
961 peak_def = numpy.nanmax(def_data[start:stop])
962 elif params['peak extraction method'] == 'peak model':
963 peak_def = numpy.nanmax(tension_data[start:stop])
965 raise ValueError(params['peak extraction method'])
966 log.debug('{}: {}'.format(params['peak extraction method'], peak_def))
967 ps['peak deflection'] = peak_def
968 ps['peak extraction method'] = params['peak extraction method']
970 data.info[params['fit parameters info name']] = ps
971 self._set_column(hooke=hooke, params=params,
972 column_name='output tension column',
975 def _setup_params(self, hooke, params):
976 for key,value in params.items():
977 if value == None: # Use configured default value.
978 params[key] = self.plugin.config[key]
979 name,def_unit = split_data_label(params['deflection column'])
980 params['output tension column'] = join_data_label(
981 params['output tension column'], def_unit)
984 def fit_polymer_model(self, params, dist_data, def_data, start, stop,
986 """Railyard for the `fit_*_model` family.
988 Uses the `polymer model` configuration setting to call the
989 appropriate backend algorithm.
991 fn = getattr(self, 'fit_%s_model'
992 % params['polymer model'].replace('-','_'))
993 return fn(params, dist_data, def_data, start, stop, outqueue)
995 def fit_FJC_model(self, params, dist_data, def_data, start, stop,
997 """Fit the data with :class:`FJC`.
1000 'temperature (K)': self.plugin.config['temperature'],
1001 'x data (m)': dist_data[start:stop],
1003 if True: # TODO: optionally free persistence length
1004 info['Kuhn length (m)'] = (
1005 params['FJC Kuhn length'])
1006 model = FJC(def_data[start:stop], info=info, rescale=True)
1008 params = model.fit(outqueue=queue)
1009 if True: # TODO: if Kuhn length fixed
1010 a = info['Kuhn length (m)']
1014 T = info['temperature (K)']
1015 fit_info = queue.get(block=False)
1016 f_data = numpy.ones(dist_data.shape, dtype=dist_data.dtype) * numpy.nan
1017 f_data[start:stop] = FJC_fn(dist_data[start:stop], T=T, L=L, a=a)
1018 return [f_data, fit_info]
1020 def fit_FJC_PEG_model(self, params, dist_data, def_data, start, stop,
1022 """Fit the data with :class:`FJC_PEG`.
1025 'temperature (K)': self.plugin.config['temperature'],
1026 'x data (m)': dist_data[start:stop],
1029 if True: # TODO: optionally free persistence length
1030 info['Kuhn length (m)'] = (
1031 params['FJC Kuhn length'])
1032 model = FJC_PEG(def_data[start:stop], info=info, rescale=True)
1034 params = model.fit(outqueue=queue)
1035 if True: # TODO: if Kuhn length fixed
1036 a = info['Kuhn length (m)']
1040 T = info['temperature (K)']
1041 fit_info = queue.get(block=False)
1042 f_data = numpy.ones(dist_data.shape, dtype=dist_data.dtype) * numpy.nan
1043 f_data[start:stop] = FJC_PEG_fn(dist_data[start:stop], **kwargs)
1044 return [f_data, fit_info]
1046 def fit_WLC_model(self, params, dist_data, def_data, start, stop,
1048 """Fit the data with :class:`WLC`.
1051 'temperature (K)': self.plugin.config['temperature'],
1052 'x data (m)': dist_data[start:stop],
1054 if True: # TODO: optionally free persistence length
1055 info['persistence length (m)'] = (
1056 params['WLC persistence length'])
1057 model = WLC(def_data[start:stop], info=info, rescale=True)
1059 params = model.fit(outqueue=queue)
1060 if True: # TODO: if persistence length fixed
1061 p = info['persistence length (m)']
1064 info['persistence length (m)'] = p
1066 info['contour length (m)'] = L
1067 T = info['temperature (K)']
1068 fit_info = queue.get(block=False)
1069 info['fit'] = fit_info
1070 info.pop('x data (m)')
1071 f_data = numpy.ones(dist_data.shape, dtype=dist_data.dtype) * numpy.nan
1072 f_data[start:stop] = WLC_fn(dist_data[start:stop], T=T, L=L, p=p)
1073 return [f_data, info]
1075 def _convex_hull_deflection(self, distance_data, deflection_data,
1076 deflection_model, start, stop, outqueue=None):
1077 """Return the last model deflection point inside the data hull.
1079 If the `stop` point is a bit past it's ideal location, the
1080 rapid rise of some polymer models can lead to artificially
1081 high peak deflections if the largest distance value is plugged
1082 into the polymer model directly. As a more robust estimation
1083 of the peak deflection, we calculate the convex hull
1084 surrounding the distance-deflection data and return the last
1085 model deflection point that is inside that hull.
1087 distance_data = distance_data[start:stop]
1088 deflection_data = deflection_data[start:stop]
1089 deflection_model = deflection_model[start:stop]
1093 [[x,y] for x,y in zip(distance_data, deflection_data)])
1094 model = numpy.array(
1095 [[x,y] for x,y in zip(distance_data, deflection_model)])
1097 inside = points_inside_hull(hull, model)
1098 # distance data is not necessarily monatonic
1099 index_inside = [j for j,i in enumerate(inside) if i is True]
1100 distance_inside = [(distance_data[i],i) for i in index_inside]
1102 peak_dist,peak_index = max(distance_inside)
1103 unfold = deflection_model[peak_index]
1104 else: # all model points are outside the hull?!
1106 return (unfold, hull)
1109 class PolymerFitPeaksCommand (ColumnAccessCommand):
1110 """Polymer model (WLC, FJC, etc.) fitting.
1112 Use :class:`PolymerFitCommand` to fit the each peak in a list of
1113 previously determined peaks.
1115 def __init__(self, plugin):
1116 super(PolymerFitPeaksCommand, self).__init__(
1117 name='polymer fit peaks',
1118 columns=plugin._input_columns,
1120 Argument(name='peak info name', type='string',
1121 default='polymer peaks',
1123 Name for the list of peaks in the `.info` dictionary.
1125 Argument(name='peak index', type='int', count=-1, default=None,
1127 Index of the selected peak in the list of peaks. Use `None` to fit all peaks.
1129 ] + plugin._arguments,
1130 help=self.__doc__, plugin=plugin)
1132 def _run(self, hooke, inqueue, outqueue, params):
1133 data = self._block(hooke, params)
1134 fit_command = hooke.command_by_name['polymer fit']
1137 curve = params['curve']
1138 params['curve'] = None
1139 p = copy.deepcopy(params)
1140 p['curve'] = params['curve']
1141 del(p['peak info name'])
1142 del(p['peak index'])
1143 for i,peak in enumerate(data.info[params['peak info name']]):
1144 if params['peak index'] == None or i in params['peak index']:
1145 p['bounds'] = [peak.index, peak.post_index()]
1146 p['output tension column'] = peak.name
1147 p['fit parameters info name'] = peak.name
1148 fit_command.run(hooke, inq, outq, **p)
1150 if not isinstance(ret, Success):
1154 class TranslateFlatPeaksCommand (ColumnAccessCommand):
1155 """Translate flat filter peaks into polymer peaks for fitting.
1157 Use :class:`~hooke.plugin.flatfilt.FlatPeaksCommand` creates a
1158 list of peaks for regions with large derivatives. For velocity
1159 clamp measurements, these regions are usually the rebound phase
1160 after a protein domain unfolds, the cantilever detaches, etc.
1161 Because these features occur after the polymer loading phase, we
1162 need to shift the selected regions back to align them with the
1163 polymer loading regions.
1165 def __init__(self, plugin):
1166 super(TranslateFlatPeaksCommand, self).__init__(
1167 name='flat peaks to polymer peaks',
1168 columns=plugin._input_columns,
1170 Argument(name='input peak info name', type='string',
1171 default='flat filter peaks',
1173 Name for the input peaks in the `.info` dictionary.
1175 Argument(name='output peak info name', type='string',
1176 default='polymer peaks',
1178 Name for the ouptput peaks in the `.info` dictionary.
1180 Argument(name='end offset', type='int', default=-1,
1182 Number of points between the end of a new peak and the start of the old.
1184 Argument(name='start fraction', type='float', default=0.2,
1186 Place the start of the new peak at `start_fraction` from the end of
1187 the previous old peak to the end of the new peak. Because the first
1188 new peak will have no previous old peak, it uses a distance of zero
1191 ] + plugin._arguments,
1192 help=self.__doc__, plugin=plugin)
1194 def _run(self, hooke, inqueue, outqueue, params):
1195 data = self._block(hooke, params)
1196 dist_data = self._get_column(
1197 hooke=hooke, params=params, column_name='distance column')
1198 def_data = self._get_column(
1199 hooke=hooke, params=params, column_name='deflection column')
1200 previous_old_stop = numpy.absolute(dist_data).argmin()
1203 peaks = data.info[params['input peak info name']]
1205 raise Failure('No value for %s in %s.info (%s): %s'
1206 % (params['input peak info name'], data.info['name'],
1207 sorted(data.info.keys()), e))
1208 for i,peak in enumerate(peaks):
1209 next_old_start = peak.index
1210 stop = next_old_start + params['end offset']
1212 dist_data[previous_old_stop]
1213 + params['start fraction']*(
1214 dist_data[stop] - dist_data[previous_old_stop]))
1215 start = numpy.absolute(dist_data - dist_start).argmin()
1216 p = Peak('polymer peak %d' % i,
1218 values=def_data[start:stop])
1220 previous_old_stop = peak.post_index()
1221 data.info[params['output peak info name']] = new
1225 # def dist2fit(self):
1226 # '''Calculates the average distance from data to fit, scaled by
1227 # the standard deviation of the free cantilever area (thermal