1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
5 # Hooke is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later version.
10 # Hooke is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke. If not, see
17 # <http://www.gnu.org/licenses/>.
19 """Provide :class:`ModelFitter` to make arbitrary model fitting easy.
22 from numpy import arange, ndarray
23 from scipy.optimize import leastsq
26 class PoorFit (ValueError):
29 class ModelFitter (object):
30 """A convenient wrapper around :func:`scipy.optimize.leastsq`.
35 Deflection data to be analyzed for the contact position.
37 Store any extra information useful inside your overridden
43 >>> from pprint import pprint
44 >>> from Queue import Queue
47 You'll want to subclass `ModelFitter`, overriding at least
48 `.model` and potentially the parameter and scale guessing
51 >>> class LinearModel (ModelFitter):
52 ... '''Simple linear model.
54 ... Levenberg-Marquardt is not how you want to solve this problem
55 ... for real systems, but it's a simple test case.
57 ... def model(self, params):
58 ... '''A linear model.
62 ... .. math:: y = p_0 x + p_1
64 ... p = params # convenient alias
65 ... self._model_data[:] = p[0]*arange(len(self._data)) + p[1]
66 ... return self._model_data
67 ... def guess_initial_params(self, outqueue=None):
68 ... return [float(self._data[-1] - self._data[0])/len(self._data),
70 ... def guess_scale(self, params, outqueue=None):
71 ... slope_scale = params[0]/10.
72 ... if slope_scale == 0: # data is expected to be flat
73 ... slope_scale = float(self._data.max()-self._data.min())/len(self._data)
74 ... if slope_scale == 0: # data is completely flat
76 ... offset_scale = self._data.std()/10.0
77 ... if offset_scale == 0: # data is completely flat
79 ... return [slope_scale, offset_scale]
80 >>> data = 20*numpy.sin(arange(1000)) + 7.*arange(1000) - 33.0
81 >>> m = LinearModel(data)
82 >>> outqueue = Queue()
83 >>> slope,offset = m.fit(outqueue=outqueue)
84 >>> info = outqueue.get()
85 >>> pprint(info) # doctest: +ELLIPSIS, +REPORT_UDIFF
86 {'convergence flag': 2,
87 'covariance matrix': array([[ 1.199...e-08, -5.993...e-06],
88 [ -5.993...e-06, 3.994...e-03]]),
89 'fitted parameters': array([ 6.999..., -32.889...]),
90 'info': {'fjac': array([[...]]),
92 'ipvt': array([1, 2]),
94 'qtf': array([ 2.851...e-07, 1.992...e-06])},
95 'initial parameters': [6.992..., -33.0],
96 'message': 'The relative error between two consecutive iterates is at most 0.000...',
97 'scale': [0.699..., 202.071...]}
99 We round the outputs to protect the doctest against differences in
100 machine rounding during computation. We expect the values to be close
101 to the input settings (slope 7, offset -33).
103 >>> print '%.3f' % slope
105 >>> print '%.3f' % offset
108 The offset is a bit off because, the range is not a multiple of
111 def __init__(self, data, info=None):
112 self.set_data(data, info)
114 def set_data(self, data, info=None):
116 self._model_data = ndarray(shape=data.shape, dtype=data.dtype)
119 def model(self, params):
120 p = params # convenient alias
121 self._model_data[:] = arange(len(self._data))
122 raise NotImplementedError
124 def guess_initial_params(self, outqueue=None):
127 def guess_scale(self, params, outqueue=None):
130 def residual(self, params):
131 return self._data - self.model(params)
133 def fit(self, initial_params=None, scale=None, outqueue=None, **kwargs):
137 initial_params : iterable or None
138 Initial parameter values for residual minimization. If
139 `None`, they are estimated from the data using
140 :meth:`guess_initial_params`.
141 scale : iterable or None
142 Parameter length scales for residual minimization. If
143 `None`, they are estimated from the data using
145 outqueue : Queue or None
146 If given, will be used to output the data and fitted model
147 for user verification.
149 Any additional arguments are passed through to `leastsq`.
151 if initial_params == None:
152 initial_params = self.guess_initial_params(outqueue)
154 scale = self.guess_scale(initial_params, outqueue)
155 assert min(scale) > 0, scale
156 params,cov,info,mesg,ier = leastsq(
157 func=self.residual, x0=initial_params, full_output=True,
158 diag=scale, **kwargs)
161 'initial parameters': initial_params,
163 'fitted parameters': params,
164 'covariance matrix': cov,
167 'convergence flag': ier,