source: sasview/park-1.2.1/park/model.py @ 2c63e80e

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 2c63e80e was 3570545, checked in by Mathieu Doucet <doucetm@…>, 13 years ago

Adding park Part 2

  • Property mode set to 100644
File size: 18.1 KB
Line 
1# This program is public domain
2"""
3Define a park fitting model.
4
5Usage
6-----
7
8The 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
20It has a function which is evaluated at a series of x values and
21a set of adjustable parameters controlling the shape of f(x).
22
23You 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
34This should produce a plot of the Gaussian peak.
35
36You will then want to try your model with some data.  Create a file
37with some dummy data, such as gauss.dat::
38
39    # x y
40    4 2
41    5 6
42    6 7
43    7 3
44
45In order to match the model to data, you need to define a fitness
46object.  This shows the difference between the model and the data,
47which 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
52The data file can have up to four columns, either x,y or x,y,dy
53or x,dx,y,dy where x,y are the measurement points and values,
54dx is the instrument resolution in x and dy is the uncertainty
55in the measurement value y.  You can pass any keyword arguments
56to data.load that are accepted by numpy.loadtxt.  For example,
57you can reorder columns or skip rows.  Additionally, you can modify
58data attributes directly x,y,dx,dy and calc_x.  See help on park.Data1D
59for details.
60
61Once 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
67In this example, center and width are allowed to vary but scale is fixed.
68
69Existing 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
90Models can become much more complex than the ones described above,
91including multilevel models where fitting parameters can be added
92and removed dynamically.
93
94In many cases the park optimizer will need an adaptor for pre-existing
95models.  The adaptor above relies on python properties to translate
96model.par access into model._par.get() and model._par.set() where _par
97is the internal name for par.  This technique works for simple static
98models, but will not work well for sophisticated models which have,
99for example, a dynamic parameter set where the model parameters cannot
100be set as properties.  A solution to this problem is to subclass the
101park.Parameter and override the value attribute as a property.
102
103Once models are defined they can be used in a variety of contexts, such
104as simultaneous fitting with constraints between the parameters.  With
105some care in defining the model, computationally intensive fits can
106be distributed across multiple processors.  We provide a simple user
107interface for interacting with the model parameters and managing fits.
108This can be extended with model specialized model editors which format
109the parameters in a sensible way for the model, or allow direct manipulation
110of the model structure.  The underlying fitting engine can also be
111used directly from your own user interface.
112
113"""
114
115__all__ = ['Model']
116
117import park
118from park.parameter import Parameter, ParameterSet
119from copy import copy, deepcopy
120
121
122class 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
174class 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
277class 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
386def 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
396def 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
511if __name__ == "__main__": test()
Note: See TracBrowser for help on using the repository browser.