test/data/vclamp_jpk/README: Document sample versions
[hooke.git] / hooke / util / fit.py
index 13606773be4417a02d9384b7ff0bfee0648929e6..f0bf32c9f915595ab859befd6adeb5147719554f 100644 (file)
@@ -1,27 +1,31 @@
-# Copyright (C) 2010 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/>.
 
 """Provide :class:`ModelFitter` to make arbitrary model fitting easy.
 """
 
 from numpy import arange, ndarray
+from scipy import __version__ as _scipy_version
 from scipy.optimize import leastsq
-import scipy.optimize
+
+_strings = _scipy_version.split('.')
+# Don't convert third string to an integer in case of (for example) '0.7.2rc3'
+_SCIPY_VERSION = (int(_strings[0]), int(_strings[1]), _strings[2])
+del _strings
 
 
 class PoorFit (ValueError):
@@ -89,10 +93,10 @@ class ModelFitter (object):
     >>> outqueue = Queue()
     >>> slope,offset = m.fit(outqueue=outqueue)
     >>> info = outqueue.get(block=False)
-    >>> pprint(info)  # doctest: +ELLIPSIS, +REPORT_UDIFF
+    >>> pprint(info)  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF
     {'active fitted parameters': array([  6.999..., -32.889...]),
      'active parameters': array([  6.999..., -32.889...]),
-     'convergence flag': 2,
+     'convergence flag': ...,
      'covariance matrix': array([[  1.199...e-08,  -5.993...e-06],
            [ -5.993...e-06,   3.994...e-03]]),
      'data scale factor': 1.0,
@@ -101,9 +105,9 @@ class ModelFitter (object):
               'fvec': array([...]),
               'ipvt': array([1, 2]),
               'nfev': 7,
-              'qtf': array([  2.851...e-07,   1.992...e-06])},
+              'qtf': array([...])},
      'initial parameters': [6.992..., -33.0],
-     'message': 'The relative error between two consecutive iterates is at most 0.000...',
+     'message': '...relative error between two consecutive iterates is at most 0.000...',
      'rescaled': False,
      'scale': [0.100..., 6.123...]}
 
@@ -111,9 +115,9 @@ class ModelFitter (object):
     machine rounding during computation.  We expect the values to be close
     to the input settings (slope 7, offset -33).
 
-    >>> print '%.3f' % slope
+    >>> print('{:.3f}'.format(slope))
     7.000
-    >>> print '%.3f' % offset
+    >>> print('{:.3f}'.format(offset))
     -32.890
 
     The offset is a bit off because, the range is not a multiple of
@@ -124,10 +128,29 @@ class ModelFitter (object):
     >>> m = LinearModel(data, rescale=True)
     >>> outqueue = Queue()
     >>> slope,offset = m.fit(outqueue=outqueue)
-    >>> print '%.3f' % slope
+    >>> print('{:.3f}'.format(slope))
     7.000
-    >>> print '%.3f' % offset
+    >>> print('{:.3f}'.format(offset))
     -32.890
+
+    Test single-parameter models:
+
+    >>> class SingleParameterModel (LinearModel):
+    ...     '''Simple linear model.
+    ...     '''
+    ...     def model(self, params):
+    ...         return super(SingleParameterModel, self).model([params[0], 0.])
+    ...     def guess_initial_params(self, outqueue=None):
+    ...         return super(SingleParameterModel, self
+    ...             ).guess_initial_params(outqueue)[:1]
+    ...     def guess_scale(self, params, outqueue=None):
+    ...         return super(SingleParameterModel, self
+    ...             ).guess_scale([params[0], 0.], outqueue)[:1]
+    >>> data = 20*numpy.sin(arange(1000)) + 7.*arange(1000)
+    >>> m = SingleParameterModel(data)
+    >>> slope, = m.fit(outqueue=outqueue)
+    >>> print('{:.3f}'.format(slope))
+    7.000
     """
     def __init__(self, *args, **kwargs):
         self.set_data(*args, **kwargs)
@@ -187,6 +210,12 @@ class ModelFitter (object):
         if self._rescale == True:
             params = [p*s for p,s in zip(params, self._param_scale_factors)]
         residual = self._data - self.model(params)
+        if False:  # fit debugging
+            if not hasattr(self, '_i_'):
+                self._i_ = 0
+            self._data.tofile('data.%d' % self._i_, sep='\n')
+            self.model(params).tofile('model.%d' % self._i_, sep='\n')
+            self._i_ += 1
         if self._rescale == True:
             residual /= self._data_scale_factor
         return residual
@@ -227,13 +256,13 @@ class ModelFitter (object):
         params,cov,info,mesg,ier = leastsq(
             func=self.residual, x0=active_params, full_output=True,
             diag=scale, **kwargs)
+        if len(initial_params) == 1 and _SCIPY_VERSION < (0, 8, '0'):
+            # params is a float for scipy < 0.8.0.  Convert to list.
+            params = [params]
         if self._rescale == True:
             active_params = params
-            if len(initial_params) == 1:  # params is a float
-                params = params * self._param_scale_factors[0]
-            else:
-                params = [p*s for p,s in zip(params,
-                                             self._param_scale_factors)]
+            params = [p*s for p,s in zip(params,
+                                         self._param_scale_factors)]
         else:
             active_params = params
         if outqueue != None:
@@ -256,7 +285,7 @@ class ModelFitter (object):
 #        def dist(px,py,linex,liney):
 #            distancesx=scipy.array([(px-x)**2 for x in linex])
 #            minindex=numpy.argmin(distancesx)
-#            print px, linex[0], linex[-1]
+#            print(px, linex[0], linex[-1])
 #            return (py-liney[minindex])**2
 #
 #