1
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
95 You can access model parameters directly::
96
97 M1 = Gauss('M1',center=[3,5],scale=7.2,width=[1,3])
98 print M1.center # Access the value for parameter 'center'
99 M1.center = 7 # Set the value for parameter 'center'
100 M1.center = [5,9] # Set the fit range for parameter 'center'
101 M1.center = "2*M1.scale" # Set parameter 'center' to an expression
102
103 Note that a strange side-effect of the convenience functions used to
104 access model parameters is that assignment statements do not act like
105 you expect. The following sets the initial value of the parameter to
106 7 then sets the range to [5,9]::
107
108 M1.center = 7 # Set the value for parameter 'center'
109 M1.center = [5,9] # Set the fit range for parameter 'center'
110
111 If these statements were reversed, the initial value would still be 7 and
112 the range [5,9] but the parameter would be set to 'fixed' rather than 'fitted'.
113 True global optimizers will not care what the initial value is, so from the
114 perspective of the fit this should not change the results. Most optimizers
115 will however use the initial value as part of the search.
116
117 Since we may choose to improve this interface in future, programmatic access
118 to models should go through parameter sets::
119
120 M1.parameterset['center'].value
121 M1.parameterset['center'].range
122 ...
123
124 Once models are defined they can be used in a variety of contexts, such
125 as simultaneous fitting with constraints between the parameters. With
126 some care in defining the model, computationally intensive fits can
127 be distributed across multiple processors. We provide a simple user
128 interface for interacting with the model parameters and managing fits.
129 This can be extended with model specialized model editors which format
130 the parameters in a sensible way for the model, or allow direct manipulation
131 of the model structure. The underlying fitting engine can also be
132 used directly from your own user interface.
133
134 For those situations where the properties are dynamic or programmatically
135 generated the meta class machinery which translates parameters into
136 properties is likely not going to be useful. In this case you can
137 inherit directly from park.model.BaseModel, or simply define the
138 required API by hand.
139 """
140
141 __all__ = ['Model']
142
143 import inspect
144 from copy import copy, deepcopy
145
146 import park
147 from park.modelling.parameter import Parameter, ParameterSet
148
149
151 """
152 Model definition.
153
154 The model manages attribute access to the fitting parameters and
155 also manages the dataset.
156
157 derivatives ['p1','p2',...]
158 List of parameters for which the model can calculate
159 derivatives. The eval_derivs() method will be called
160 to calculate the derivatives with respect to the parameters.
161 Parameters with no analytic derivatives available will be
162 approximated by numerical differentiation.
163
164 eval(x)
165 Evaluate the model at x.
166
167 eval_deriv(x,pars=['p1','p2',...])
168 Evaluate the model and the derivatives at x.
169
170 parameterset
171 The set of parameters defined by the model.
172
173 The minimum fitting model if you choose not to subclass park.BaseModel
174 requires parameterset and an eval() method.
175
176 Note that BaseModel is a simple class which
177 """
178 derivatives = []
179 parameters = []
180 parameterset = None
182 """
183 Initialize the model.
184
185 Model('name',P1=value, P2=Value, ...)
186
187 When overriding __init__ in the subclass be sure to call
188 Model.__init__(self, *args, **kw). This makes a private
189 copy of the parameterset for the model and initializes
190 any parameters set using keyword arguments.
191 """
192
193
194
195
196
197 if len(args)>1: raise TypeError("expect name as model parameter")
198 name = args[0] if len(args)>=1 else 'unknown'
199
200
201
202 self.parameterset = deepcopy(self.parameterset)
203 for k,v in kw.items():
204 self.parameterset[k].set(v)
205 self.parameterset._name = name
206
207
208 - def eval(self, *args):
209 """
210 Evaluate the model at x.
211
212 This method needs to be specialized in the model to evaluate the
213 model function.
214 """
215 raise NotImplementedError('Model needs to implement eval')
216
218 """
219 Evaluate the model and derivatives wrt pars at x.
220
221 pars is a list of the names of the parameters for which derivatives
222 are desired. The fitting engine may be working with a subset of the
223 parameters stored in the model, and the parameters may be requested
224 in a different order from what they were stored.
225
226 Returns (f(x), df/dp1(x), df/dp2(x), ...).
227
228 Note that the analytic derivatives need not be defined for all
229 parameters; numerical derivatives can be calculated when needed.
230 If when analytic derivatives are available, they may not be
231 used because the parameter in question is used as part of an
232 expression for other parameters. Automatic differentiation
233 on parameter expressions is possible, but beyond the scope
234 of this project.
235
236 This method needs to be specialized in the model to evaluate the
237 model function.
238 """
239 raise NotImplementedError('Model needs to implement eval_derivs')
240
241
242 - def set(self, **kw):
243 """
244 Set the initial value for a set of parameters.
245
246 E.g., model.set(width=3,center=5)
247 """
248 for k,v in kw.items():
249 self.parameterset[k].set(v)
250
252 """
253 Return the value for parameter p (by name or number).
254 """
255 return self.parameterset[p].get()
256
258 """
259 Set the value for parameter p (by name or number).
260 """
261 return self.parameterset[p].set(value)
262
264 """
265 Evaluate the model at x.
266 """
267 return self.eval(*args)
268
269
270
271
272
273
274
275
276
277
278
279
281 """
282 Attribute accessor for a named parameter.
283
284 Objects of class ParameterProperty act similarly to normal property
285 objects in that assignment to object.attr triggers the __set__
286 method of ParameterProperty and queries of the value object.attr
287 triggers the __get__ method. These methods look up the actual
288 parameter in the dictionary model.parameterset, delegating the
289 the set/get methods of the underlying parameter object.
290
291 For example::
292
293 model.P1 = 5
294 print model.P1
295
296 is equivalent to::
297
298 model.parameterset['P1'].set(5)
299 print model.parameterset['P1'].get()
300
301 To enable this behaviour, the model developer must assign a
302 ParameterProperty object to a class attribute of the model. It
303 must be a class attribute. Properties assigned as an instance
304 attribute in the __init__ of the class will not be recognized
305 as properties.
306
307 An example model will look something like the following::
308
309 class MyModel:
310 # A property must be assigned as a class attribute!
311 P1 = ParameterProperty('P1')
312 P2 = ParameterProperty('P2')
313 def __init__(self, P1=None, P2=None):
314 parP1 = Parameter('P1')
315 if P1 is not None: parP1.set(P1)
316 parP2 = Parameter('P2')
317 if P2 is not None: parP2.set(P2)
318 self.parameterset = { 'P1': parP1, 'P2': parP2 }
319
320 This work is done implicitly by MetaModel, and by subclassing
321 the class Model, the model developer does not ever need to
322 use ParameterProperty.
323 """
330
331
434
436 """
437 Model definition.
438
439 The model manages attribute access to the fitting parameters and
440 also manages the dataset.
441
442 derivatives ['p1','p2',...]
443 List of parameters for which the model can calculate
444 derivatives. The eval_derivs() method will be called
445 to calculate the derivatives with respect to the parameters.
446 Parameters with no analytic derivatives available will be
447 approximated by numerical differentiation.
448
449 eval(x)
450 Evaluate the model at x. This must be defined by the subclass.
451
452 eval_deriv(x,pars=[])
453 Evaluate the model and the derivatives at x. This must be
454 defined by the subclass.
455
456 parameters
457 The names of the model parameters. If this is not provided, then
458 the model will search the subclass for park.Parameter attributes
459 and construct the list of names from that. Any parameters in the
460 list not already defined as park.Parameter attributes will be
461 defined as parameters with a default of 0.
462
463 parameterset
464 The set of parameters defined by the model. These are the
465 parameters themselves, gathered into a park.ParameterSet.
466
467 The minimum fittng model if you choose not to subclass park.Model
468 requires parameterset and a residuals() method.
469 """
470 __metaclass__ = MetaModel
471
480
482 """
483 Find the keyword arguments associated with a function and return them
484 as parameters.
485 """
486 names,arg,kw,values = inspect.getargspec(fn)
487 if len(values)<1:
488 raise ValueError("Need keyword args to model function")
489 parameters = names[-len(values):]
490 eval_args = names[:len(names)-len(values)]
491 return [park.Parameter(k,value=v) for k,v in zip(parameters,values)]
492
494 """
495 Define a model based on a function. The fitting parameters should
496 be defined as keyword arguments to the function.
497 """
502
503 - def eval(self, *args):
506
507
509
510 import numpy
511 eps,inf = numpy.finfo('d').eps,numpy.inf
512 def G(x,mu,sigma):
513 mu,sigma = numpy.asarray(mu),numpy.asarray(sigma)
514 return numpy.exp(-0.5*(x-mu)**2/sigma**2)
515
516
517
518 class Gauss(Model):
519 parameters = ['center','width','scale']
520 def eval(self,x):
521 return self.scale * G(x,self.center,self.width)
522
523
524 g1 = Gauss(center=5,width=1,scale=2)
525 assert g1.center == 5
526 g2 = Gauss(center=6,width=1)
527 assert g1.center == 5
528 assert g2.center == 6
529 assert g1(5) == 2
530 assert g2(6) == 0
531
532
533
534 class Gauss(Model):
535 center = Parameter(value=0,
536 tip='Peak center')
537 width = Parameter(value=1,
538 limits=(eps,inf),
539 tip='Peak width (1-sigma)')
540 scale = Parameter(value=1,
541 limits=(0,inf),
542 tip='Peak scale (>0)')
543 def eval(self,x):
544 return self.scale * G(x,self.center,self.width)
545
546
547 derivatives = ['center','width','scale']
548 def dscale(self, g, x):
549 return g/self.scale
550 def dcenter(self, g, x):
551 return 2*(x-self.center)/self.width*g
552 def dwidth(self, g, x):
553 return 2*(x-self.center)/self.width**3*g
554 dmap = dict(scale=dscale,center=dcenter,width=dwidth)
555 def eval_derivs(self,x,pars=[]):
556 """
557 Calculate function value and derivatives wrt to the parameters.
558
559 pars is a list of parameter names, possibly consisting of any
560 parameter with deriv=True.
561 """
562 g = self.eval(x)
563 dg = [self.dmap[p](self,g,x) for p in pars]
564 return [g]+dg
565
566 g1 = Gauss(center=5)
567 g1.parameterset['center'].tip = 'This is the center'
568 assert g1.center == 5
569 g2 = Gauss(center=6)
570 assert g1.center == 5
571 assert g2.center == 6
572 assert g1(5) == 1
573 assert g2(6) == 1
574
575
576
577 class Gauss(object):
578 """Pre-existing model"""
579 center,width,scale = 0,1,1
580 def __init__(self,**kw):
581
582 for k,v in kw.items(): setattr(self,k,v)
583
584 def eval(self,x):
585 return self.scale *G(x,self.center,self.width)
586
587 class GaussAdaptor(Gauss,Model):
588 """PARK wrapper"""
589 parameters = ['center','width','scale']
590 def __init__(self,*args,**kw):
591
592 Model.__init__(self,*args)
593 Gauss.__init__(self,**kw)
594
595
596 g1 = GaussAdaptor(center=5)
597 g2 = GaussAdaptor(center=6)
598 g3 = GaussAdaptor()
599 assert g1.center == 5
600 assert g2.center == 6
601 assert g3.scale == 1
602 assert g1(5) == 1
603 assert g2(6) == 1
604
605 g4 = GaussAdaptor('M3',center=6)
606 assert g4.parameterset.name == 'M3'
607
608
609 def line(x, a=1, b=2): return a*x+b
610 linemodel = FunctionModel(line,a=5)
611 assert linemodel['a'] == 5
612 assert linemodel(3) == 5*3+2
613
614
615 class MultiGauss(Model):
616 def add(self,name,model):
617 pass
618
619
620 class BaseMultiGauss(object):
621 def __init__(self):
622 self.models = []
623 def add(self,**kw):
624 self.models.append(BaseGauss(**kw))
625 class WrapMultiGauss(BaseMultiGauss,Model):
626 def __init__(self):
627 pass
628
629 if __name__ == "__main__": test()
630