1 | # This program is public domain |
---|
2 | """ |
---|
3 | Fitting service interface. |
---|
4 | |
---|
5 | A fit consists of a set of models and a fitting engine. The models are |
---|
6 | collected in an assembly, which manages the parameter set and the |
---|
7 | constraints between them. The models themselves are tightly coupled |
---|
8 | to the data that they are modeling and the data is invisible to the fit. |
---|
9 | |
---|
10 | The fitting engine can use a variety of methods depending on model. |
---|
11 | |
---|
12 | |
---|
13 | Usage |
---|
14 | ===== |
---|
15 | |
---|
16 | The fitter can be run directly on the local machine:: |
---|
17 | |
---|
18 | import park |
---|
19 | M1 = park.models.Peaks(datafile=park.sampledata('peak.dat')) |
---|
20 | M1.add_peak('P1', 'gaussian', A=[4,6], mu=[0.2, 0.5], sigma=0.1) |
---|
21 | result = park.fit(models=[M1]) |
---|
22 | print result |
---|
23 | |
---|
24 | The default settings print results every time the fit improves, and |
---|
25 | print a global result when the fit is complete. This is a suitable |
---|
26 | interface for a fitting script. |
---|
27 | |
---|
28 | For larger fit jobs you will want to run the fit on a remote server. |
---|
29 | The model setup is identical, but the fit call is different:: |
---|
30 | |
---|
31 | service = park.FitService('server:port') |
---|
32 | result = park.fit(models=[M1], service=service) |
---|
33 | print result |
---|
34 | |
---|
35 | Again, the default settings print results every time the fit improves, |
---|
36 | and print a global result when the fit is complete. |
---|
37 | |
---|
38 | For long running fit jobs, you want to be able to disconnect from |
---|
39 | the server after submitting the job, and later reconnect to fetch |
---|
40 | the results. An additional email field will send notification by |
---|
41 | email when the fit starts and ends, and daily updates on the status |
---|
42 | of all fits:: |
---|
43 | |
---|
44 | service = park.FitService('server:port') |
---|
45 | service.notify(email='me@my.email.address',update='daily') |
---|
46 | fit = park.Fit(models=[M1]) |
---|
47 | id = service.submit_job(fit, jobname='peaks') |
---|
48 | print id |
---|
49 | |
---|
50 | The results can be retrieved either by id returned from the server, |
---|
51 | or by the given jobname:: |
---|
52 | |
---|
53 | import park |
---|
54 | service = park.FitService('server:port',user='userid') |
---|
55 | fitlist = service.retrieve('peaks') |
---|
56 | for fit in fitlist: |
---|
57 | print fit.summary() |
---|
58 | |
---|
59 | The fit itself is a complicated object, including the model, the |
---|
60 | optimizer, and the type of uncertainty analysis to perform. |
---|
61 | |
---|
62 | GUI Usage |
---|
63 | ========= |
---|
64 | |
---|
65 | When used from a graphical user interface, a different programming |
---|
66 | interface is needed. In this case, the user may want to watch |
---|
67 | the progress of the fit and perhaps stop it. Also, as fits can |
---|
68 | take some time to complete, the user would like to be able to |
---|
69 | set up additional fits and run them at the same time, switching |
---|
70 | between them as necessary to monitor progress. |
---|
71 | |
---|
72 | """ |
---|
73 | import time, thread |
---|
74 | |
---|
75 | import numpy |
---|
76 | |
---|
77 | import assembly, fitresult |
---|
78 | |
---|
79 | class Objective(object): |
---|
80 | """ |
---|
81 | Abstract interface to the fitness function for the park minimizer |
---|
82 | classes. |
---|
83 | |
---|
84 | Park provides a specific implementation `park.assembly.Assembly`. |
---|
85 | |
---|
86 | TODO: add a results() method to return model specific info to the |
---|
87 | TODO: fit handler. |
---|
88 | """ |
---|
89 | def residuals(self, p): |
---|
90 | """ |
---|
91 | Some fitters, notably Levenberg-Marquardt, operate directly on the |
---|
92 | residuals vector. If the individual residuals are not available, |
---|
93 | then LM cannot be used. |
---|
94 | |
---|
95 | This method is optional. |
---|
96 | """ |
---|
97 | raise NotImplementedError |
---|
98 | |
---|
99 | def residuals_deriv(self, p): |
---|
100 | """ |
---|
101 | Returns residuals and derivatives with respect to the given |
---|
102 | parameters. |
---|
103 | |
---|
104 | If these are unavailable in the model, then they can be approximated |
---|
105 | by numerical derivatives, though it is generally better to use a |
---|
106 | derivative free optimizer such as coliny or cobyla which can use the |
---|
107 | function evaluations more efficiently. In any case, your objective |
---|
108 | function is responsible for calculating these. |
---|
109 | |
---|
110 | This method is optional. |
---|
111 | """ |
---|
112 | raise NotImplementedError |
---|
113 | |
---|
114 | def fit_parameters(self): |
---|
115 | """ |
---|
116 | Returns a list of fit parameters. Each parameter has a name, |
---|
117 | an initial value and a range. |
---|
118 | |
---|
119 | See `park.fitresult.FitParameter` for an example. |
---|
120 | |
---|
121 | On each function evaluation a new parameter set will be passed |
---|
122 | to the fitter, with values in the same order as the list of |
---|
123 | parameters. |
---|
124 | """ |
---|
125 | raise NotImplementedError |
---|
126 | |
---|
127 | def __call__(self, p): |
---|
128 | """ |
---|
129 | Returns the objective value for parameter set p . |
---|
130 | """ |
---|
131 | raise NotImplementedError |
---|
132 | |
---|
133 | def abort(self): |
---|
134 | """ |
---|
135 | Halts the current function evaluation, and has it return inf. |
---|
136 | This will be called from a separate thread. If the function |
---|
137 | contains an expensive calculation, it should reset an abort |
---|
138 | flag before each evaluation and test it periodically. |
---|
139 | |
---|
140 | This method is optional. |
---|
141 | """ |
---|
142 | |
---|
143 | class Fitter(object): |
---|
144 | """Abstract interface for a fitness optimizer. |
---|
145 | |
---|
146 | A fitter has a single method, fit, which takes an objective |
---|
147 | function (`park.fit.Objective`) and a handler. |
---|
148 | |
---|
149 | For a concrete instance see `park.fitmc.FitMC`. |
---|
150 | """ |
---|
151 | def __init__(self, **kw): |
---|
152 | for k,v in kw.items(): |
---|
153 | if hasattr(self,k): |
---|
154 | setattr(self,k,v) |
---|
155 | else: |
---|
156 | raise AttributeError(k+" is not an attribute of "+self.__class__.__name__) |
---|
157 | |
---|
158 | def _threaded(self, fn, *args, **kw): |
---|
159 | thread.start_new_thread(fn,args,kw) |
---|
160 | |
---|
161 | |
---|
162 | def _fit(self, objective, x0, bounds): |
---|
163 | """ |
---|
164 | Run the actual fit in a separate thread |
---|
165 | |
---|
166 | Each cycle k of n: |
---|
167 | self.handler.progress(k,n) |
---|
168 | Each improvement: |
---|
169 | self.handler.result.update(x,fx,ncalls) |
---|
170 | self.handler.improvement() |
---|
171 | On completion (if not already performed): |
---|
172 | self.hander.result.update(x,fx,ncalls) |
---|
173 | self.handler.done |
---|
174 | self.handler.finalize() |
---|
175 | """ |
---|
176 | raise NotImplementedError |
---|
177 | |
---|
178 | def fit(self, fitness, handler): |
---|
179 | """ |
---|
180 | Global optimizer. |
---|
181 | |
---|
182 | This function should return immediately |
---|
183 | """ |
---|
184 | # Determine initial value and bounds |
---|
185 | pars = fitness.fit_parameters() |
---|
186 | bounds = numpy.array([p.range for p in pars]).T |
---|
187 | x0 = [p.value for p in pars] |
---|
188 | |
---|
189 | # Initialize the monitor and results. |
---|
190 | # Need to make our own copy of the fit results so that the |
---|
191 | # values don't get stomped on by the next fit iteration. |
---|
192 | handler.done = False |
---|
193 | self.handler = handler |
---|
194 | fitpars = [fitresult.FitParameter(pars[i].name,pars[i].range,v) |
---|
195 | for i,v in enumerate(x0)] |
---|
196 | handler.result = fitresult.FitResult(fitpars, 0, numpy.NaN) |
---|
197 | |
---|
198 | # Run the fit (fit should perform _progress and _improvement updates) |
---|
199 | # This function may return before the fit is complete. |
---|
200 | self._fit(fitness, x0, bounds) |
---|
201 | |
---|
202 | class FitJob(object): |
---|
203 | """ |
---|
204 | Fit job. |
---|
205 | |
---|
206 | This implements `park.job.Job`. |
---|
207 | """ |
---|
208 | def __init__(self, objective=None, fitter=None, handler=None): |
---|
209 | self.fitter = fitter |
---|
210 | self.objective = objective |
---|
211 | self.handler = handler |
---|
212 | def run(self): |
---|
213 | self.fitter.fit(self.objective, self.handler) |
---|
214 | |
---|
215 | class LocalQueue(object): |
---|
216 | """ |
---|
217 | Simple interface to the local job queue. Currently supports start and |
---|
218 | wait. Needs to support stop and status. Also, needs to be a proper queue, |
---|
219 | and needs to allow jobs to run in separate processes according to priority, |
---|
220 | etc. All the essentials of the remote queuing system without the remote |
---|
221 | calls. |
---|
222 | |
---|
223 | Unlike the remote queue, the local queue need not maintain persistence. |
---|
224 | """ |
---|
225 | running = False |
---|
226 | def start(self, job): |
---|
227 | self.job = job |
---|
228 | job.run() |
---|
229 | return id(job) |
---|
230 | |
---|
231 | def wait(self, interval=.1): |
---|
232 | """ |
---|
233 | Wait for the job to complete. This is used in scripts to impose |
---|
234 | a synchronous interface to the fitting service. |
---|
235 | """ |
---|
236 | while not self.job.handler.done: |
---|
237 | time.sleep(interval) |
---|
238 | return self.job.handler.result |
---|
239 | |
---|
240 | def fit(models=None, fitter=None, service=None, handler=None): |
---|
241 | """ |
---|
242 | Start a fit with a set of models. The model set must be |
---|
243 | in a form accepted by `park.assembly.Assembly`. |
---|
244 | |
---|
245 | This is a convenience function which sets up the default |
---|
246 | optimizer and uses the local fitting engine to do the work. |
---|
247 | Progress reports are printed as they are received. |
---|
248 | |
---|
249 | The choice of fitter, service and handler can be specified |
---|
250 | by the caller. |
---|
251 | |
---|
252 | The default fitter is FitMC, which is a monte carlo Nelder-Mead |
---|
253 | simplex local optimizer with 100 random start points. |
---|
254 | |
---|
255 | The default handler does nothing. Instead, ConsoleUpdate could |
---|
256 | be used to report progress during the fit. |
---|
257 | |
---|
258 | The default service is to run in a separate thread with FitThread. |
---|
259 | Note that this will change soon to run in a separate process on |
---|
260 | the local machine so that python's global interpreter lock does |
---|
261 | not interfere with parallelism. |
---|
262 | """ |
---|
263 | if models is None: raise RuntimeError('fit expected a list of models') |
---|
264 | if service is None: service = LocalQueue() |
---|
265 | if fitter is None: |
---|
266 | import fitmc |
---|
267 | fitter = fitmc.FitMC() |
---|
268 | if handler is None: handler = fitresult.FitHandler() |
---|
269 | |
---|
270 | objective = assembly.Assembly(models) if isinstance(models,list) else models |
---|
271 | job = FitJob(objective,fitter,handler) |
---|
272 | service.start(job) |
---|
273 | return service.wait() |
---|
274 | |
---|
275 | |
---|
276 | def assembly_example(): |
---|
277 | import park, time |
---|
278 | problem = park.assembly.example() |
---|
279 | #result = fit(problem) |
---|
280 | #result.print_summary() |
---|
281 | handler=fitresult.ConsoleUpdate(improvement_delta=0.1,progress_delta=1) |
---|
282 | #result = fit(problem, handler=handler) |
---|
283 | result = fit(problem) |
---|
284 | print "=== Fit complete ===" |
---|
285 | result.print_summary() |
---|
286 | print "=== Target values ===" |
---|
287 | print "M1: a=1, c=1.5" |
---|
288 | print "M2: a=2.5, c=3" |
---|
289 | |
---|
290 | if False: # Detailed results |
---|
291 | print "parameter vector",result.pvec |
---|
292 | problem(result.pvec) |
---|
293 | print "residuals",problem.residuals |
---|
294 | for k,m in enumerate(problem.parts): |
---|
295 | print "Model",k,"chisq",m.chisq,"weight",m.weight |
---|
296 | print "pars",m.fitness.model.a,m.fitness.model.c |
---|
297 | print "x",m.fitness.data.fit_x |
---|
298 | print "y",m.fitness.data.fit_y |
---|
299 | print "f(x)",m.fitness.data.fx |
---|
300 | print "(y-f(x))/dy",m.residuals |
---|
301 | |
---|
302 | |
---|
303 | def demo(fitter=None): |
---|
304 | """Multiple minima example""" |
---|
305 | import time, math |
---|
306 | class MultiMin(object): |
---|
307 | def fit_parameters(self): |
---|
308 | return [fitresult.FitParameter('x1',[-5,5],1)] |
---|
309 | def __call__(self, p): |
---|
310 | x=p[0] |
---|
311 | fx = x**2 + math.sin(2*math.pi*x+3*math.pi/2) |
---|
312 | return fx |
---|
313 | handler = fitresult.ConsoleUpdate() # Show updates on the console |
---|
314 | handler.progress_delta = 1 # Update user every second |
---|
315 | handler.improvement_delta = 0.1 # Show improvements almost immediately |
---|
316 | fitter.fit(MultiMin(), handler) |
---|
317 | while not handler.done: time.sleep(1) |
---|
318 | |
---|
319 | def demo2(fitter=None): |
---|
320 | import park, time |
---|
321 | problem = park.assembly.example() |
---|
322 | handler = fitresult.ConsoleUpdate() # Show updates on the console |
---|
323 | handler.progress_delta = 1 # Update user every second |
---|
324 | handler.improvement_delta = 1 # Show improvements at the same rate |
---|
325 | fitter.fit(problem, handler) |
---|
326 | while not handler.done: time.sleep(1) |
---|
327 | |
---|
328 | |
---|
329 | |
---|
330 | if __name__ == "__main__": |
---|
331 | #main() |
---|
332 | assembly_example() |
---|