source: sasview/park-1.2.1/park/parameter.py @ a8d882a

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 a8d882a was fb7180c, checked in by pkienzle, 11 years ago

enable park constrained fit test

  • Property mode set to 100644
File size: 18.8 KB
Line 
1# This program is public domain
2"""
3Parameters and parameter sets.
4
5Parameter defines an individual parameter, and ParameterSet groups them
6into a hierarchy.
7
8Individual models need to provide a parameter set with the correct
9properties, either by using park.ParameterSet in their model definition,
10or by providing a wrapper which can translate assignment to parameter.value
11into the appropriate change in the wrapped model.  See wrapper.py for
12an example.
13"""
14
15__all__ = ['Parameter', 'ParameterSet']
16
17import math
18import numpy
19import expression       
20
21class Pnormal(object):
22    """
23    Negative log likelihood function for a parameter from a Gaussian
24    distribution.
25   
26    Given P(v;mu,sigma) = exp(-1/2 (mu-v)^2/sigma^2)/sqrt(2 pi sigma^2)
27    then -log(P) = 1/2 (mu-v)^2/sigma^2 + 1/2 log(2*pi*sigma^2)
28   
29    Assuming that parameter P is selected from a normal distribution,
30    then P.likelihood = Pnormal(mu,sigma)
31    """
32    def __init__(self, mean, std):
33        self.mean = mean
34        self.std = std
35        self.const = math.log(2*math.pi*std**2)/2
36    def __call__(self, value):
37        return ((value-self.mean)/self.std)**2/2 + self.const
38   
39inf = numpy.inf
40class Parameter(object):
41    """
42    A parameter is a box for communicating with the fitting service.
43    Parameters can have a number of properties,
44   
45    Parameters have a number of properties:
46   
47    name "string"
48        name of the parameter within the parameter set. 
49       
50        The name is read only.  You can rename a parameter but only
51        in the context of the parameter set which contains it, using
52        parameterset.rename(par,name).  This will change all expressions
53        containing the named parameter.
54   
55    path
56        dotted name of the parameter within the set of models.  The
57        dotted name is automatically generated by the parameter set
58        before expressions are parsed and evaluated.  There are
59        some operations on parameter sets (such as renaming the
60        layer containing a parameter) which will force an adjustment
61        of all the underlying parameter names, as well as any
62        expressions in which they are referenced.
63   
64    limits (low, high)
65        hard limits on the range of allowed parameter values, dictated
66        by physics rather than by knowledge of the particular system.
67        For example, thickness parameters would have limits (0,inf)
68        because negative thicknesses are unphysical.  These limits
69        are enforced when setting range for the fit.
70
71    units "string"
72        units for the parameter.  This should be a string, but
73        be parsable by whatever units package your application
74        supports.
75
76    tip "string"
77        parameter description, suitable for use in a tooltip
78   
79    value double
80        current value of the parameter, either fixed, fitted or computed
81   
82    range (low, high)
83        range of expected values for the parameter in the model
84   
85    expression "string"
86        expression for the parameter in the model.  This is a string
87        containing a formula for updating the parameter value based
88        on the values of other parameters in the system.  The expression
89        is ignored if 'calculated' is False.
90
91        Note: the list of symbols available to the expression evaluator
92        defaults to the contents of the math module.  The caller will be
93        able to override this within the fitting fitting class.
94   
95    status 'fixed'|'computed'|'fitted'
96        the parameter type.  Choose 'fixed' if the values is to
97        remain fixed throughout the fit, even if a range and an
98        expression have been specified.  Choose 'computed' if the
99        value of the parameter is to be computed each time the
100        parameters are updated.  Choose 'fitted' if an optimization
101        algorithm is supposed to search the parameter space.
102
103    likelihood
104        function to return the negative log likelihood of seeing a
105        particular parameter value.  2*likelihood(value) will be added
106        to the total cost function for the particular parameter set
107        during the fit.  This will be on par with the probabilty
108        of seeing the particular theory function given the observed
109        datapoints when performing the fit (the residual term is
110        closely related to the log likelihood of the normal distribution).
111       
112        Note: we are minimizing chi^2 = sum [ ((y-f(x;p))/dy)^2 ] rather
113        than  -log P = sum [ ((y-f(x;p))/dy)^2/2 + log(2 pi dy^2) ],
114        where P is the probability of seeing f(x;p) given y,dy as the
115        mean and standard deviation of a normal distribution.  Because
116        chi^2_p = - 2 * log P_p + constant, the minima of p are the same
117        for chi^2 and negative log likelihood.  However, to weight the
118        likelihood properly when adding likelihood values to chisq, we
119        need the extra factor of 2 mentioned above.  The usual statistical
120        implications of normalized chi^2 will of course be suspect, both
121        because the assumption of independence between the points in
122        chi^2 (which definitely do not hold for the new 'point' p_k), and
123        because of the additional 2 log(2 pi dp_k^2) constant, but given
124        the uncertainty in the estimate of the distribution parameters,
125        this is likely a minor point.
126    """
127    # Protect parameter name from user modification
128    _name = "unknown"
129    def _getname(self): return self._name
130    name = property(_getname,doc="parameter name")
131   
132    # Do checking on the parameter range to make sure the model stays physical
133    _range = (-inf,inf)
134    def _setrange(self, r):
135        if self.limits[0]<=r[0]<=r[1] <=self.limits[1]:
136            self._range = r
137        else:
138            raise ValueError("invalid range %s for %s"%(r,self.name))
139    def _getrange(self): return self._range
140    range = property(_getrange,_setrange,doc="parameter range")
141
142    path = ""
143    value = 0.
144    limits = (-inf,inf)
145    expression = ""
146    status = 'fixed' # fixed, computed or fitted
147    likelihood = None
148    units = ""
149    tip = "Fitting parameter"
150    deriv = False
151    def __init__(self, name="unknown", **kw):
152        self._name = name
153        for k,v in kw.items():
154            if hasattr(self,k): setattr(self,k,v)
155            else: raise AttributeError("Unknown attribute %s"%k)
156    def __str__(self): return self.path if self.path != '' else self.name
157    def __repr__(self): return "Parameter('%s')"%self.name
158    def summarize(self):
159        """
160        Return parameter range string.
161       
162        E.g.,  "       Gold .....|.... 5.2043 in [2,7]"
163        """
164        range = ['.']*10
165        lo,hi = self.range
166        portion = (self.value-lo)/(hi-lo)
167        if portion < 0: portion = 0.
168        elif portion >= 1: portion = 0.99999999
169        bar = math.floor(portion*len(range))
170        range[bar] = '|'
171        range = "".join(range)
172        return "%25s %s %g in [%g,%g]"  % (self.name,range,self.value,lo,hi)
173
174    def isfitted(self): return self.status == 'fitted'
175    def iscomputed(self): return self.status == 'computed'
176    def isfixed(self): return self.status == 'fixed'
177    def isrestrained(self): return self.likelihood is not None
178    def isfeasible(self, value):
179        """Return true if the value is in the range"""
180        return self._range[0] <= value <= self._range[1]
181    def setprefix(self, prefix):
182        """
183        Set the full path to the parameter as used in expressions involving
184        the parameter name.
185        """
186        self.path = prefix+self.name
187    def get(self):
188        """
189        Return the current value for a parameter.
190        """
191        return self.value
192    def set(self, value):
193        """
194        Set a parameter to a value, a range or an expression.  If it is a value,
195        the parameter will be fixed for the fit.  If it is a range, the value
196        will be varying for the fit.  If it is an expression, the parameter will
197        be calculated from the values of other parameters in the fit.
198
199        Raises ValueError if the value could not be interpreted.
200        """
201       
202        # Expression
203        if isinstance(value,basestring):
204            self.expression = value
205            self.status = 'computed'
206            return
207       
208        # Fixed value
209        if numpy.isscalar(value):
210            self.value = value
211            self.status = 'fixed'
212            return
213       
214        # Likelihood
215        if hasattr(value,'__call__'):
216            self.range = value.range
217            self.likelihood = value
218            self.status = 'fitted'
219            return
220       
221        # Range
222        try:
223            lo,hi = numpy.asarray(value)
224            if not numpy.isscalar(lo) or not numpy.isscalar(hi):
225                raise Exception
226            self.range = (lo,hi)
227            self.status = 'fitted'
228            return
229        except:
230            pass
231       
232        raise ValueError,\
233            "parameter %s expects value, expression or range: %s"%(self.name,value)
234
235class ParameterSet(list):
236    """
237    The set of parameters used to fit theory to data.
238   
239    ParameterSet forms a hierarchy of parameters.  The parameters themselves
240    are referred to by the path through the hierarchy, usually::
241   
242         fitname.component.parameter
243         
244    Though more or fewer levels are permitted.  Parameters are assumed to
245    have a unique label throughout the fit.  This is required so that
246    expressions tying the results of one fit to another can uniquely
247    reference a parameter.
248   
249    Attributes:
250   
251    name
252        the name of the parameter set
253    path
254        the full dotted name of the parameter set
255    context
256        a dictionary providing additional context for evaluating parameters;
257        Note that this namespace is shared with other theory functions, so
258        populate it carefully.
259    """
260    # Protect parameter set name from user modification
261    def _getname(self): return self._name
262    def _setname(self, name): 
263        raise NotImplementedError("parameter.name is protected; use fit.rename_parameter() to change it")
264    name = property(_getname,doc="parameter name")
265    path = ""
266    def __init__(self, name="unknown", pars=[]):
267        super(ParameterSet,self).__init__(pars)
268        self._name = name
269        self.context = {}
270
271    def _byname(self, parts):
272        """byname recursion function"""
273        if len(parts) == 1: return self
274        for p in self:
275            if parts[1] == p.name:
276                if len(parts) == 2:
277                    return p
278                elif isinstance(p, ParameterSet):
279                    return p._byname(parts[1:])
280                else:
281                    raise
282        return None
283
284    def byname(self, name):
285        """Lookup parameter from dotted path"""
286        parts = name.split('.')
287        if parts[0] == self.name:
288            p =  self._byname(name.split('.'))
289            if p: return p
290        raise KeyError("parameter %s not in parameter set"%name)
291   
292    def __getitem__(self, idx):
293        """Allow indexing by name or by number"""
294        if isinstance(idx,basestring):
295            for p in self:
296                if p.name == idx: 
297                    return p
298            raise KeyError("%s is not in the parameter set"%idx)
299        else:
300            return super(ParameterSet,self).__getitem__(idx)
301
302    def flatten(self):
303        """
304        Iterate over the elements in depth first order.
305        """
306        import park
307        L = []
308        for p in self:
309            # Yuck! I really only want to try flattening if p is a
310            # ParameterSet but it seems we have parameter.ParameterSet
311            # and park.parameter.ParameterSet as separate entities,
312            # depending on how things were included.  The solution is
313            # probably to force absolute include paths always.
314            try:
315                L += p.flatten()
316            except:
317                L.append(p)
318        return L
319        """
320        # Iterators are cute but too hard to use since you can
321        # only use them in a [p for p in generator()] once.
322        for p in self:
323            if isinstance(p, ParameterSet):
324                for subp in p.flatten():
325                    yield subp
326            else:
327                yield p
328        """
329
330    def _fixed(self):
331        """
332        Return the subset of the parameters which are fixed
333        """
334        return [p for p in self.flatten() if p.isfixed()]
335    fixed = property(_fixed,doc=_fixed.__doc__)
336
337    def _fitted(self):
338        """
339        Return the subset of the paramters which are varying
340        """
341        return [p for p in self.flatten() if p.isfitted()]
342    fitted = property(_fitted,doc=_fitted.__doc__)
343
344    def _computed(self):
345        """
346        Return the subset of the parameters which are calculated
347        """
348        return [p for p in self.flatten() if p.iscomputed()]
349    computed = property(_computed,doc=_computed.__doc__)
350
351    def _restrained(self):
352        """
353        Return the subset of the parameters which have a likelihood
354        function associated with them.
355        """
356        return [p for p in self.flatten() if p.isrestrained()]
357    restrained = property(_restrained,doc=_restrained.__doc__)
358
359    def setprefix(self,prefix=None):
360        """
361        Fill in the full path name for all the parameters in the tree.
362       
363        Note: this function must be called from the root parameter set
364        to build proper path names.
365       
366        This is required before converting parameter expressions into
367        function calls.
368        """
369        if prefix == None:
370            # We are called from root, so we don't have a path
371            self.path = ""
372            prefix = ""
373        else:
374            self.path = prefix+self.name
375            prefix = self.path+'.'
376        for p in self:
377            #print "setting prefix for",p,prefix
378            p.setprefix(prefix)
379
380    def rename(self, par, name):
381        """
382        Rename the parameter to something new.
383        Called from root of the parameter hierarchy, rename the particular
384        parameter object to something else.
385
386        This changes the internal name of the parameter, as well as all
387        expressions in which it occurs.  If the parameter is actually
388        a parameter set, then it renames all parameters in the set.
389        """
390       
391        # Must run from root for self.setprefix and self.computed to work
392        if self.path != "": 
393            raise RuntimeError,"rename must be called from root parameter set"
394
395        # Change the name of the node
396        par._name = name
397       
398        # Determine which nodes (self and children) are affected
399        if isinstance(par,ParameterSet):
400            changeset = par.flatten()
401        else:
402            changeset = [par]
403       
404        #  Map the old names into the new names
405        old = [p.path for p in changeset]
406        self.setprefix() # Reset the path names of all parameters
407        new = [p.path for p in changeset]
408        mapping = dict(zip(old,new))
409
410        # Perform the substitution into all of the expressions
411        exprs = self.computed
412        for p in exprs:
413            p.expression = expression.substitute(p.expression, mapping)
414
415    def gather_context(self):
416        """
417        Gather all additional symbols that can be used in expressions.
418       
419        For example, if reflectometry provides a volume fraction
420        function volfrac(rho1,rho2,frac) to compute densities, then
421        this function can be added as a context dictionary to the
422        reflectometry parameter set.  Note that there is no guarantee
423        which function will be used if the same function exists in
424        two separate contexts.
425        """
426        context = {} # Create a new dictionary
427        context.update(self.context)
428        for p in self:
429            if hasattr(p,'gather_context'):
430                context.update(p.gather_context())
431        return context
432
433
434def test():
435    # Check single parameter
436    a = Parameter('a')
437    assert a.name == 'a'
438    a.value = 5
439    assert a.value == 5
440    # Check the setters
441    a.set(7)
442    assert a.value == 7 and a.status == 'fixed' and a.isfixed()
443    a.set([3,5])
444    assert a.value == 7 and a.range[0] == 3 and a.range[1]==5 and a.isfitted()
445    a.set('3*M.b')
446    assert a.iscomputed() and a.expression == '3*M.b'
447
448    # Check limits
449    a.limits = (0,inf)
450    try: 
451        a.range = (-1,1)
452        raise Exception,"Failed to check range in limits"
453    except ValueError: pass # Correct failure
454   
455    # Check that we can't change name directly
456    try:
457        a.name = 'Q'
458        raise Exception,"Failed to protect name"
459    except AttributeError: pass # Correct failure
460
461    assert str(a) == 'a'  # Before setpath, just print name
462    a.setprefix('M.')
463    assert a.path == 'M.a'
464    assert str(a) == 'M.a'  # After setpath, print path
465    assert a.units == ''
466    a.units = 'kg'
467    assert a.units == 'kg'
468    assert repr(a) == "Parameter('a')"
469
470    # Check parameter set
471    b,c = Parameter('b'),Parameter('c')
472    M1 = ParameterSet('M1',[a,b,c])
473    assert M1[0] is a
474    assert M1['a'] is a
475    a.set(5) # one value
476    b.set([3,5])
477    c.set('3*M1.a')
478    assert M1.computed == [c]
479    assert M1.fitted == [b]
480    assert M1.fixed == [a]
481    d = Parameter('d')
482    M1.insert(0,d)
483    assert M1.fixed == [d,a]
484    e = Parameter('e')
485    M1.append(e)
486    assert M1.fixed == [d,a,e]
487
488    a2,b2,c2 = [Parameter(s) for s in ('a','b','c')]
489    a2.set(15)
490    M2 = ParameterSet('M2',[a2,b2,c2])
491    # Adjust parameter in set
492    b2.set([3,5])
493    assert M2.fitted == [b2]
494   
495    # Hierarchical parameter sets
496    r = Parameter('r')
497    root = ParameterSet('root',[M1,r,M2])
498    assert root.fixed == [d,a,e,r,a2,c2]
499    assert root.fitted == [b,b2]
500    assert root.computed == [c]
501    root.setprefix()
502    assert a2.path == "M2.a"
503    # Rename individual parameter
504    root.rename(a,'a1')
505    assert a.path == "M1.a1"
506    assert c.expression == "3*M1.a1"
507    # Rename parameter set
508    root.rename(M1,'m1')
509    assert c.path == "m1.c"
510    assert c.expression == "3*m1.a1"
511
512    # Integration test: parameter and expression working together.
513    fn = expression.build_eval(root.flatten(), root.gather_context())
514    #import dis; dis.dis(fn)
515    fn()
516    assert c.value == 3*a.value
517
518    # Test context
519    M2.context['plus2'] = lambda x: x+2
520    c2.set('plus2(M2.a)')
521    assert set(root.computed) == set([c,c2])
522    fn = expression.build_eval(root.flatten(), root.gather_context())
523    #print dis.dis(fn)
524    fn()
525    assert c2.value == a2.value+2
526
527    # Multilevel hierarchy
528    # Forming M3.a.x, M3.a.y, M3.b with M3.a.y = 2*M3.b+M3.a.x
529    x = Parameter('x'); x.set(15)
530    y = Parameter('y'); y.set('2*M3.b+M3.a.x')
531    b = Parameter('b'); b.set(10)
532    a = ParameterSet('a',[x,y])
533    M3 = ParameterSet('M3',[a,b])
534    root = ParameterSet('root',[M3])
535    root.setprefix()
536    fn = expression.build_eval(root.flatten(), root.gather_context())
537    #import dis; dis.dis(fn)
538    fn()
539    #print "y-value:",y.value
540    assert y.value == 35
541   
542
543
544
545if __name__ == "__main__": test()
Note: See TracBrowser for help on using the repository browser.