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

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 a189869 was 3570545, checked in by Mathieu Doucet <doucetm@…>, 13 years ago

Adding park Part 2

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