1 | # This program is public domain |
---|
2 | """ |
---|
3 | Define a park fitting model. |
---|
4 | |
---|
5 | Usage |
---|
6 | ----- |
---|
7 | |
---|
8 | The simplest sort of fitting model is something like the following:: |
---|
9 | |
---|
10 | import numpy |
---|
11 | import park |
---|
12 | def G(x,mu,sigma): |
---|
13 | return numpy.exp(-0.5*(x-mu)**2/sigma**2) |
---|
14 | |
---|
15 | class Gauss(park.Model): |
---|
16 | parameters = ['center','width','scale'] |
---|
17 | def eval(self,x): |
---|
18 | return self.scale * G(x,self.center,self.width) |
---|
19 | |
---|
20 | It has a function which is evaluated at a series of x values and |
---|
21 | a set of adjustable parameters controlling the shape of f(x). |
---|
22 | |
---|
23 | You can check your module with something like the following:: |
---|
24 | |
---|
25 | $ ipython -pylab |
---|
26 | |
---|
27 | from gauss import Gauss |
---|
28 | |
---|
29 | g = Gauss(center=5,width=1,scale=10) |
---|
30 | x = asarray([1,2,3,4,5]) |
---|
31 | y = g(x) |
---|
32 | plot(x,y) |
---|
33 | |
---|
34 | This should produce a plot of the Gaussian peak. |
---|
35 | |
---|
36 | You will then want to try your model with some data. Create a file |
---|
37 | with some dummy data, such as gauss.dat:: |
---|
38 | |
---|
39 | # x y |
---|
40 | 4 2 |
---|
41 | 5 6 |
---|
42 | 6 7 |
---|
43 | 7 3 |
---|
44 | |
---|
45 | In order to match the model to data, you need to define a fitness |
---|
46 | object. This shows the difference between the model and the data, |
---|
47 | which you can then plot or sum to create a weighted chisq value:: |
---|
48 | |
---|
49 | f = park.Fitness(g, 'gauss.dat') |
---|
50 | plot(f.data.fit_x, f.residuals()) |
---|
51 | |
---|
52 | The data file can have up to four columns, either x,y or x,y,dy |
---|
53 | or x,dx,y,dy where x,y are the measurement points and values, |
---|
54 | dx is the instrument resolution in x and dy is the uncertainty |
---|
55 | in the measurement value y. You can pass any keyword arguments |
---|
56 | to data.load that are accepted by numpy.loadtxt. For example, |
---|
57 | you can reorder columns or skip rows. Additionally, you can modify |
---|
58 | data attributes directly x,y,dx,dy and calc_x. See help on park.Data1D |
---|
59 | for details. |
---|
60 | |
---|
61 | Once you have your model debugged you can use it in a fit:: |
---|
62 | |
---|
63 | g = Gauss(center=[3,5],scale=7.2,width=[1,3]) |
---|
64 | result = park.fit((g, 'gauss.dat')) |
---|
65 | result.plot() |
---|
66 | |
---|
67 | In this example, center and width are allowed to vary but scale is fixed. |
---|
68 | |
---|
69 | Existing models can be readily adapted to Park:: |
---|
70 | |
---|
71 | class Gauss(object): |
---|
72 | "Existing model" |
---|
73 | center,width,scale = 0,1,1 |
---|
74 | def __init__(self,**kw): |
---|
75 | for k,v in kw.items(): setattr(self,k,v) |
---|
76 | def eval(self,x): |
---|
77 | return self.scale *G(x,self.center,self.width) |
---|
78 | |
---|
79 | class GaussAdaptor(Gauss,Model): |
---|
80 | "PARK adaptor" |
---|
81 | parameters = ['center','width','scale'] |
---|
82 | def __init__(self,*args,**kw): |
---|
83 | Model.__init__(self,*args) |
---|
84 | Gauss.__init__(self,**kw) |
---|
85 | |
---|
86 | g = GaussAdaptor(center=[3,5],scale=7.2,width=[1,3]) |
---|
87 | result = park.fit((g, 'gauss.dat')) |
---|
88 | result.plot() |
---|
89 | |
---|
90 | Models can become much more complex than the ones described above, |
---|
91 | including multilevel models where fitting parameters can be added |
---|
92 | and removed dynamically. |
---|
93 | |
---|
94 | In many cases the park optimizer will need an adaptor for pre-existing |
---|
95 | models. The adaptor above relies on python properties to translate |
---|
96 | model.par access into model._par.get() and model._par.set() where _par |
---|
97 | is the internal name for par. This technique works for simple static |
---|
98 | models, but will not work well for sophisticated models which have, |
---|
99 | for example, a dynamic parameter set where the model parameters cannot |
---|
100 | be set as properties. A solution to this problem is to subclass the |
---|
101 | park.Parameter and override the value attribute as a property. |
---|
102 | |
---|
103 | Once models are defined they can be used in a variety of contexts, such |
---|
104 | as simultaneous fitting with constraints between the parameters. With |
---|
105 | some care in defining the model, computationally intensive fits can |
---|
106 | be distributed across multiple processors. We provide a simple user |
---|
107 | interface for interacting with the model parameters and managing fits. |
---|
108 | This can be extended with model specialized model editors which format |
---|
109 | the parameters in a sensible way for the model, or allow direct manipulation |
---|
110 | of the model structure. The underlying fitting engine can also be |
---|
111 | used directly from your own user interface. |
---|
112 | |
---|
113 | """ |
---|
114 | |
---|
115 | __all__ = ['Model'] |
---|
116 | |
---|
117 | import park |
---|
118 | from park.parameter import Parameter, ParameterSet |
---|
119 | from copy import copy, deepcopy |
---|
120 | |
---|
121 | |
---|
122 | class ParameterProperty(object): |
---|
123 | """ |
---|
124 | Attribute accessor for a named parameter. |
---|
125 | |
---|
126 | Objects of class ParameterProperty act similarly to normal property |
---|
127 | objects in that assignment to object.attr triggers the __set__ |
---|
128 | method of ParameterProperty and queries of the value object.attr |
---|
129 | triggers the __get__ method. These methods look up the actual |
---|
130 | parameter in the dictionary model.parameterset, delegating the |
---|
131 | the set/get methods of the underlying parameter object. |
---|
132 | |
---|
133 | For example:: |
---|
134 | |
---|
135 | model.P1 = 5 |
---|
136 | print model.P1 |
---|
137 | |
---|
138 | is equivalent to:: |
---|
139 | |
---|
140 | model.parameterset['P1'].set(5) |
---|
141 | print model.parameterset['P1'].get() |
---|
142 | |
---|
143 | To enable this behaviour, the model developer must assign a |
---|
144 | ParameterProperty object to a class attribute of the model. It |
---|
145 | must be a class attribute. Properties assigned as an instance |
---|
146 | attribute in the __init__ of the class will not be recognized |
---|
147 | as properties. |
---|
148 | |
---|
149 | An example model will look something like the following:: |
---|
150 | |
---|
151 | class MyModel: |
---|
152 | # A property must be assigned as a class attribute! |
---|
153 | P1 = ParameterProperty('P1') |
---|
154 | P2 = ParameterProperty('P2') |
---|
155 | def __init__(self, P1=None, P2=None): |
---|
156 | parP1 = Parameter('P1') |
---|
157 | if P1 is not None: parP1.set(P1) |
---|
158 | parP2 = Parameter('P2') |
---|
159 | if P2 is not None: parP2.set(P2) |
---|
160 | self.parameterset = { 'P1': parP1, 'P2': parP2 } |
---|
161 | |
---|
162 | This work is done implicitly by MetaModel, and by subclassing |
---|
163 | the class Model, the model developer does not ever need to |
---|
164 | use ParameterProperty. |
---|
165 | """ |
---|
166 | def __init__(self,name,**kw): |
---|
167 | self.name = name |
---|
168 | def __get__(self,instance,cls): |
---|
169 | return instance.parameterset[self.name].get() |
---|
170 | def __set__(self,instance,value): |
---|
171 | instance.parameterset[self.name].set(value) |
---|
172 | |
---|
173 | |
---|
174 | class MetaModel(type): |
---|
175 | """ |
---|
176 | Interpret parameters. |
---|
177 | |
---|
178 | This is a meta class, and the usual meta class rules apply. That is, |
---|
179 | the Model base class should be defined like:: |
---|
180 | |
---|
181 | class Model(object): |
---|
182 | __metaclass__ = MetaModel |
---|
183 | ... |
---|
184 | |
---|
185 | The MetaModel.__new__ method is called whenever a new Model |
---|
186 | class is created. The name of the model, its superclasses and |
---|
187 | its attributes are passed to MetaModel.__new__ which creates the |
---|
188 | actual class. It is not called for new instances of the model. |
---|
189 | |
---|
190 | MetaModel is designed to simplify the definition of parameters for |
---|
191 | the model. When processing the class, MetaModel defines |
---|
192 | parameters which is a list of names of all the parameters in the |
---|
193 | model, and parameterset, which is a dictionary of the actual |
---|
194 | parameters and any parameter sets in the model. |
---|
195 | |
---|
196 | Parameters can be defined using a list of names in the parameter |
---|
197 | attribute, or by defining the individual parameters as separate |
---|
198 | attributes of class Parameter. |
---|
199 | |
---|
200 | For example, the following defines parameters P1, P2, and P3:: |
---|
201 | |
---|
202 | class MyModel(Model): |
---|
203 | parameters = ['P1','P2'] |
---|
204 | P2 = Parameter(range=[0,inf]) |
---|
205 | P3 = Parameter() |
---|
206 | |
---|
207 | For each parameter, MetaModel will define a parameter accessor, |
---|
208 | and add the parameter definition to the parameter set. The accessor |
---|
209 | delegates query and assignment to the Parameter get/set methods. The |
---|
210 | attributes of the particular parameter instance can be |
---|
211 | adjusted using model.parameterset['name'].attribute = value. |
---|
212 | """ |
---|
213 | def __new__(cls, name, bases, vars): |
---|
214 | #print 'calling model meta',vars |
---|
215 | |
---|
216 | # Find all the parameters for the model. The listed parameters |
---|
217 | # are defined using:: |
---|
218 | # parameters = ['P1', 'P2', ...] |
---|
219 | # The remaining parameters are defined individually:: |
---|
220 | # P3 = Parameter() |
---|
221 | # P4 = Parameter() |
---|
222 | # The undefined parameters are those that are listed but not defined. |
---|
223 | listed = vars.get('parameters',[]) |
---|
224 | defined = [k for k,v in vars.items() if isinstance(v,Parameter)] |
---|
225 | undefined = [k for k in listed if k not in defined] |
---|
226 | #print listed,defined,undefined |
---|
227 | |
---|
228 | # Define a getter/setter for every parameter so that the user |
---|
229 | # can say model.name to access parameter name. |
---|
230 | # |
---|
231 | # Create a parameter object for every undefined parameter. |
---|
232 | # Check if the base class defines a default value, and use |
---|
233 | # that for the initial value. We don't want to do this for |
---|
234 | # parameters explicitly defined since the user may have |
---|
235 | # given them a default value already. |
---|
236 | # |
---|
237 | # For predefined parameters we must set the name explicitly. |
---|
238 | # This saves the user from having to use, e.g.:: |
---|
239 | # P1 = Parameter('P1') |
---|
240 | pars = [] |
---|
241 | for p in undefined: |
---|
242 | # Check if the attribute value is initialized in a base class |
---|
243 | for b in bases: |
---|
244 | if hasattr(b,p): |
---|
245 | value = getattr(b,p) |
---|
246 | break |
---|
247 | else: |
---|
248 | value = 0. |
---|
249 | #print "looking up value in base as",value |
---|
250 | pars.append(Parameter(name=p,value=value)) |
---|
251 | vars[p] = ParameterProperty(p) |
---|
252 | for p in defined: |
---|
253 | # Make sure parameter name is correct. Note that we are using |
---|
254 | # _name rather than .name to access the name attribute since |
---|
255 | # name is a read-only parameter. |
---|
256 | vars[p]._name = p |
---|
257 | pars.append(vars[p]) |
---|
258 | vars[p] = ParameterProperty(p) |
---|
259 | |
---|
260 | # Construct the class attributes 'parameters' and 'parameterset'. |
---|
261 | # Listed parameters are given first, with unlisted parameters |
---|
262 | # following alphabetically. For hierarchical parameter sets, |
---|
263 | # we also need to include the defined sets. |
---|
264 | unlisted = list(set(defined+undefined) - set(listed)) |
---|
265 | unlisted.sort() |
---|
266 | parameters = listed + unlisted |
---|
267 | parsets = [k for k,v in vars.items() if isinstance(v,ParameterSet)] |
---|
268 | vars['parameters'] = parameters |
---|
269 | vars['parameterset'] = ParameterSet(pars=pars+parsets) |
---|
270 | #print 'pars',vars['parameters'] |
---|
271 | #print 'parset',vars['parameterset'] |
---|
272 | |
---|
273 | # Return a new specialized instance of the class with parameters |
---|
274 | # and parameterset made explicit. |
---|
275 | return type.__new__(cls, name, bases, vars) |
---|
276 | |
---|
277 | class Model(object): |
---|
278 | """ |
---|
279 | Model definition. |
---|
280 | |
---|
281 | The model manages attribute access to the fitting parameters and |
---|
282 | also manages the dataset. |
---|
283 | |
---|
284 | derivatives ['p1','p2',...] |
---|
285 | List of parameters for which the model can calculate |
---|
286 | derivatives. The derivs |
---|
287 | The model function can compute the derivative with respect |
---|
288 | to this parameter. The function model.derivs(x,[p1,p2,...]) |
---|
289 | will return (f(x),df/dp1(x), ...). The parameters and their |
---|
290 | order are determined by the fitting engine. |
---|
291 | |
---|
292 | Note: This is a property of the model, not the fit. |
---|
293 | Numerical derivatives will be used if the parameter is |
---|
294 | used in an expression or if no analytic derivative is |
---|
295 | available for the parameter. Automatic differentiation |
---|
296 | on parameter expressions is possible, but beyond the scope |
---|
297 | of this project. |
---|
298 | |
---|
299 | eval(x) |
---|
300 | Evaluate the model at x. This must be defined by the subclass. |
---|
301 | |
---|
302 | eval_deriv(x,pars=[]) |
---|
303 | Evaluate the model and the derivatives at x. This must be |
---|
304 | defined by the subclass. |
---|
305 | |
---|
306 | parameters |
---|
307 | The names of the model parameters. If this is not provided, then |
---|
308 | the model will search the subclass for park.Parameter attributes |
---|
309 | and construct the list of names from that. Any parameters in the |
---|
310 | list not already defined as park.Parameter attributes will be |
---|
311 | defined as parameters with a default of 0. |
---|
312 | |
---|
313 | parameterset |
---|
314 | The set of parameters defined by the model. These are the |
---|
315 | parameters themselves, gathered into a park.ParameterSet. |
---|
316 | |
---|
317 | The minimum fittng model if you choose not to subclass park.Model |
---|
318 | requires parameterset and a residuals() method. |
---|
319 | """ |
---|
320 | __metaclass__ = MetaModel |
---|
321 | derivatives = [] |
---|
322 | def __init__(self,*args,**kw): |
---|
323 | """ |
---|
324 | Initialize the model. |
---|
325 | |
---|
326 | Model('name',P1=value, P2=Value, ...) |
---|
327 | |
---|
328 | When overriding __init__ in the subclass be sure to call |
---|
329 | Model.__init__(self, *args, **kw). This makes a private |
---|
330 | copy of the parameterset for the model and initializes |
---|
331 | any parameters set using keyword arguments. |
---|
332 | """ |
---|
333 | #print 'calling model init on',id(self) |
---|
334 | # To avoid trashing the Model.parname = Parameter() template |
---|
335 | # when we create an instance and accessor for the parameter |
---|
336 | # we need to make sure that we are using our own copy of the |
---|
337 | # model dictionary stored in vars |
---|
338 | if len(args)>1: raise TypeError("expect name as model parameter") |
---|
339 | name = args[0] if len(args)>=1 else 'unknown' |
---|
340 | self.parameterset = deepcopy(self.parameterset) |
---|
341 | for k,v in kw.items(): |
---|
342 | self.parameterset[k].set(v) |
---|
343 | self.parameterset._name = name |
---|
344 | #print 'done',id(self) |
---|
345 | |
---|
346 | def __call__(self, x, pars=[]): |
---|
347 | return self.eval(x) |
---|
348 | |
---|
349 | def eval(self, x): |
---|
350 | """ |
---|
351 | Evaluate the model at x. |
---|
352 | |
---|
353 | This method needs to be specialized in the model to evaluate the |
---|
354 | model function. Alternatively, the model can implement is own |
---|
355 | version of residuals which calculates the residuals directly |
---|
356 | instead of calling eval. |
---|
357 | """ |
---|
358 | raise NotImplementedError('Model needs to implement eval') |
---|
359 | |
---|
360 | def eval_derivs(self, x, pars=[]): |
---|
361 | """ |
---|
362 | Evaluate the model and derivatives wrt pars at x. |
---|
363 | |
---|
364 | pars is a list of the names of the parameters for which derivatives |
---|
365 | are desired. |
---|
366 | |
---|
367 | This method needs to be specialized in the model to evaluate the |
---|
368 | model function. Alternatively, the model can implement is own |
---|
369 | version of residuals which calculates the residuals directly |
---|
370 | instead of calling eval. |
---|
371 | """ |
---|
372 | raise NotImplementedError('Model needs to implement eval_derivs') |
---|
373 | |
---|
374 | def set(self, **kw): |
---|
375 | """ |
---|
376 | Set the initial value for a set of parameters. |
---|
377 | |
---|
378 | E.g., model.set(width=3,center=5) |
---|
379 | """ |
---|
380 | # This is a convenience funciton for the user. |
---|
381 | # |
---|
382 | for k,v in kw.items(): |
---|
383 | self.parameterset[k].set(v) |
---|
384 | |
---|
385 | |
---|
386 | def add_parameter(model, name, **kw): |
---|
387 | """ |
---|
388 | Add a parameter to an existing park model. |
---|
389 | |
---|
390 | Note: this may not work if it is operating on a BaseModel. |
---|
391 | """ |
---|
392 | setattr(model.__class__, name, park.model.ParameterProperty(name)) |
---|
393 | model.parameterset.append(park.Parameter(name=name, **kw)) |
---|
394 | |
---|
395 | |
---|
396 | def test(): |
---|
397 | # Gauss theory function |
---|
398 | import numpy |
---|
399 | eps,inf = numpy.finfo('d').eps,numpy.inf |
---|
400 | def G(x,mu,sigma): |
---|
401 | mu,sigma = numpy.asarray(mu),numpy.asarray(sigma) |
---|
402 | return numpy.exp(-0.5*(x-mu)**2/sigma**2) |
---|
403 | |
---|
404 | # === Minimal model === |
---|
405 | # just list the fitting parameters and define the function. |
---|
406 | class Gauss(Model): |
---|
407 | parameters = ['center','width','scale'] |
---|
408 | def eval(self,x): |
---|
409 | return self.scale * G(x,self.center,self.width) |
---|
410 | |
---|
411 | # Create a couple of models and make sure they don't conflict |
---|
412 | g1 = Gauss(center=5,width=1,scale=2) |
---|
413 | assert g1.center == 5 |
---|
414 | g2 = Gauss(center=6,width=1) |
---|
415 | assert g1.center == 5 |
---|
416 | assert g2.center == 6 |
---|
417 | assert g1(5) == 2 |
---|
418 | assert g2(6) == 0 |
---|
419 | |
---|
420 | # === explore parameters === |
---|
421 | # dedicated model using park parameters directly, and defining derivatives |
---|
422 | class Gauss(Model): |
---|
423 | center = Parameter(value=0, |
---|
424 | tip='Peak center') |
---|
425 | width = Parameter(value=1, |
---|
426 | limits=(eps,inf), |
---|
427 | tip='Peak width (1-sigma)') |
---|
428 | scale = Parameter(value=1, |
---|
429 | limits=(0,inf), |
---|
430 | tip='Peak scale (>0)') |
---|
431 | def eval(self,x): |
---|
432 | return self.scale * G(x,self.center,self.width) |
---|
433 | |
---|
434 | # Derivatives |
---|
435 | derivatives = ['center','width','scale'] |
---|
436 | def dscale(self, g, x): |
---|
437 | return g/self.scale |
---|
438 | def dcenter(self, g, x): |
---|
439 | return 2*(x-self.center)/self.width*g |
---|
440 | def dwidth(self, g, x): |
---|
441 | return 2*(x-self.center)/self.width**3*g |
---|
442 | dmap = dict(scale=dscale,center=dcenter,width=dwidth) |
---|
443 | def eval_derivs(self,x,pars=[]): |
---|
444 | """ |
---|
445 | Calculate function value and derivatives wrt to the parameters. |
---|
446 | |
---|
447 | pars is a list of parameter names, possibly consisting of any |
---|
448 | parameter with deriv=True. |
---|
449 | """ |
---|
450 | g = self.eval(x) |
---|
451 | dg = [self.dmap[p](self,g,x) for p in pars] |
---|
452 | return [g]+dg |
---|
453 | |
---|
454 | g1 = Gauss(center=5) |
---|
455 | g1.parameterset['center'].tip = 'This is the center' |
---|
456 | assert g1.center == 5 |
---|
457 | g2 = Gauss(center=6) |
---|
458 | assert g1.center == 5 |
---|
459 | assert g2.center == 6 |
---|
460 | assert g1(5) == 1 |
---|
461 | assert g2(6) == 1 |
---|
462 | |
---|
463 | # ====== Test wrapper ======= |
---|
464 | # delegate to existing model via inheritence |
---|
465 | class Gauss(object): |
---|
466 | """Pre-existing model""" |
---|
467 | center,width,scale = 0,1,1 |
---|
468 | def __init__(self,**kw): |
---|
469 | #print "calling BaseGauss init on",id(self) |
---|
470 | for k,v in kw.items(): setattr(self,k,v) |
---|
471 | #print "done",id(self) |
---|
472 | def eval(self,x): |
---|
473 | return self.scale *G(x,self.center,self.width) |
---|
474 | |
---|
475 | class GaussAdaptor(Gauss,Model): |
---|
476 | """PARK wrapper""" |
---|
477 | parameters = ['center','width','scale'] |
---|
478 | def __init__(self,*args,**kw): |
---|
479 | #print "calling Gauss init on",id(self) |
---|
480 | Model.__init__(self,*args) |
---|
481 | Gauss.__init__(self,**kw) |
---|
482 | #print "done",id(self) |
---|
483 | |
---|
484 | g1 = GaussAdaptor(center=5) |
---|
485 | g2 = GaussAdaptor(center=6) |
---|
486 | g3 = GaussAdaptor() |
---|
487 | assert g1.center == 5 |
---|
488 | assert g2.center == 6 |
---|
489 | assert g3.scale == 1 |
---|
490 | assert g1(5) == 1 |
---|
491 | assert g2(6) == 1 |
---|
492 | |
---|
493 | g4 = GaussAdaptor('M3',center=6) |
---|
494 | assert g4.parameterset.name == 'M3' |
---|
495 | |
---|
496 | # dedicated multilevel model using park parameters directly |
---|
497 | class MultiGauss(Model): |
---|
498 | def add(self,name,model): |
---|
499 | pass |
---|
500 | |
---|
501 | # wrapped model using park parameters indirectly |
---|
502 | class BaseMultiGauss(object): |
---|
503 | def __init__(self): |
---|
504 | self.models = [] |
---|
505 | def add(self,**kw): |
---|
506 | self.models.append(BaseGauss(**kw)) |
---|
507 | class WrapMultiGauss(BaseMultiGauss,Model): |
---|
508 | def __init__(self): |
---|
509 | pass |
---|
510 | |
---|
511 | if __name__ == "__main__": test() |
---|