source: sasview/src/sans/fit/ParkFitting.py @ 4de1fed

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 4de1fed was fb7180c, checked in by pkienzle, 10 years ago

enable park constrained fit test

  • Property mode set to 100644
File size: 21.4 KB
Line 
1
2
3
4"""
5ParkFitting module contains SansParameter,Model,Data
6FitArrange, ParkFit,Parameter classes.All listed classes work together
7to perform a simple fit with park optimizer.
8"""
9#import time
10import numpy
11import math
12from  numpy.linalg.linalg import LinAlgError
13#import park
14from park import fit
15from park import fitresult
16from  park.fitresult import FitParameter
17import park.simplex
18from park.assembly import Assembly
19from park.assembly import Part
20from park.fitmc import FitSimplex
21import park.fitmc
22from park.fit import Fitter
23from park.formatnum import format_uncertainty
24from sans.fit.AbstractFitEngine import FitEngine
25from sans.fit.AbstractFitEngine import FResult
26
27class SansParameter(park.Parameter):
28    """
29    SANS model parameters for use in the PARK fitting service.
30    The parameter attribute value is redirected to the underlying
31    parameter value in the SANS model.
32    """
33    def __init__(self, name, model, data):
34        """
35            :param name: the name of the model parameter
36            :param model: the sans model to wrap as a park model
37        """
38        park.Parameter.__init__(self, name)
39        self._model, self._name = model, name
40        self.data = data
41        self.model = model
42        #set the value for the parameter of the given name
43        self.set(model.getParam(name))
44
45    def _getvalue(self):
46        """
47        override the _getvalue of park parameter
48
49        :return value the parameter associates with self.name
50
51        """
52        return self._model.getParam(self.name)
53
54    def _setvalue(self, value):
55        """
56        override the _setvalue pf park parameter
57
58        :param value: the value to set on a given parameter
59
60        """
61        self._model.setParam(self.name, value)
62
63    value = property(_getvalue, _setvalue)
64
65    def _getrange(self):
66        """
67        Override _getrange of park parameter
68        return the range of parameter
69        """
70        #if not  self.name in self._model.getDispParamList():
71        lo, hi = self._model.details[self.name][1:3]
72        if lo is None: lo = -numpy.inf
73        if hi is None: hi = numpy.inf
74        if lo > hi:
75            raise ValueError, "wrong fit range for parameters"
76
77        return lo, hi
78
79    def get_name(self):
80        """
81        """
82        return self._getname()
83
84    def _setrange(self, r):
85        """
86        override _setrange of park parameter
87
88        :param r: the value of the range to set
89
90        """
91        self._model.details[self.name][1:3] = r
92    range = property(_getrange, _setrange)
93
94
95class ParkModel(park.Model):
96    """
97    PARK wrapper for SANS models.
98    """
99    def __init__(self, sans_model, sans_data=None, **kw):
100        """
101        :param sans_model: the sans model to wrap using park interface
102
103        """
104        park.Model.__init__(self, **kw)
105        self.model = sans_model
106        self.name = sans_model.name
107        self.data = sans_data
108        #list of parameters names
109        self.sansp = sans_model.getParamList()
110        #list of park parameter
111        self.parkp = [SansParameter(p, sans_model, sans_data) for p in self.sansp]
112        #list of parameter set
113        self.parameterset = park.ParameterSet(sans_model.name, pars=self.parkp)
114        self.pars = []
115
116    def get_params(self, fitparams):
117        """
118        return a list of value of paramter to fit
119
120        :param fitparams: list of paramaters name to fit
121
122        """
123        list_params = []
124        self.pars = fitparams
125        for item in fitparams:
126            for element in self.parkp:
127                if element.name == str(item):
128                    list_params.append(element.value)
129        return list_params
130
131    def set_params(self, paramlist, params):
132        """
133        Set value for parameters to fit
134
135        :param params: list of value for parameters to fit
136
137        """
138        try:
139            for i in range(len(self.parkp)):
140                for j in range(len(paramlist)):
141                    if self.parkp[i].name == paramlist[j]:
142                        self.parkp[i].value = params[j]
143                        self.model.setParam(self.parkp[i].name, params[j])
144        except:
145            raise
146
147    def eval(self, x):
148        """
149            Override eval method of park model.
150
151            :param x: the x value used to compute a function
152        """
153        try:
154            return self.model.evalDistribution(x)
155        except:
156            raise
157
158    def eval_derivs(self, x, pars=[]):
159        """
160        Evaluate the model and derivatives wrt pars at x.
161
162        pars is a list of the names of the parameters for which derivatives
163        are desired.
164
165        This method needs to be specialized in the model to evaluate the
166        model function.  Alternatively, the model can implement is own
167        version of residuals which calculates the residuals directly
168        instead of calling eval.
169        """
170        return []
171
172
173class SansFitResult(fitresult.FitResult):
174    def __init__(self, *args, **kwrds):
175        fitresult.FitResult.__init__(self, *args, **kwrds)
176        self.theory = None
177        self.inputs = []
178       
179class SansFitSimplex(FitSimplex):
180    """
181    Local minimizer using Nelder-Mead simplex algorithm.
182
183    Simplex is robust and derivative free, though not very efficient.
184
185    This class wraps the bounds contrained Nelder-Mead simplex
186    implementation for `park.simplex.simplex`.
187    """
188    radius = 0.05
189    """Size of the initial simplex; this is a portion between 0 and 1"""
190    xtol = 1
191    #xtol = 1e-4
192    """Stop when simplex vertices are within xtol of each other"""
193    ftol = 5e-5
194    """Stop when vertex values are within ftol of each other"""
195    maxiter = None
196    """Maximum number of iterations before fit terminates"""
197    def __init__(self, ftol=5e-5):
198        self.ftol = ftol
199       
200    def fit(self, fitness, x0):
201        """Run the fit"""
202        self.cancel = False
203        pars = fitness.fit_parameters()
204        bounds = numpy.array([p.range for p in pars]).T
205        result = park.simplex.simplex(fitness, x0, bounds=bounds,
206                                 radius=self.radius, xtol=self.xtol,
207                                 ftol=self.ftol, maxiter=self.maxiter,
208                                 abort_test=self._iscancelled)
209        #print "calls:",result.calls
210        #print "simplex returned",result.x,result.fx
211        # Need to make our own copy of the fit results so that the
212        # values don't get stomped on by the next fit iteration.
213        fitpars = [SansFitParameter(pars[i].name,pars[i].range,v, pars[i].model, pars[i].data)
214                   for i,v in enumerate(result.x)]
215        res = SansFitResult(fitpars, result.calls, result.fx)
216        res.inputs = [(pars[i].model, pars[i].data) for i,v in enumerate(result.x)]
217        # Compute the parameter uncertainties from the jacobian
218        res.calc_cov(fitness)
219        return res
220     
221class SansFitter(Fitter):
222    """
223    """
224    def fit(self, fitness, handler):
225        """
226        Global optimizer.
227
228        This function should return immediately
229        """
230        # Determine initial value and bounds
231        pars = fitness.fit_parameters()
232        bounds = numpy.array([p.range for p in pars]).T
233        x0 = [p.value for p in pars]
234
235        # Initialize the monitor and results.
236        # Need to make our own copy of the fit results so that the
237        # values don't get stomped on by the next fit iteration.
238        handler.done = False
239        self.handler = handler
240        fitpars = [SansFitParameter(pars[i].name, pars[i].range, v,
241                                     pars[i].model, pars[i].data)
242                   for i,v in enumerate(x0)]
243        handler.result = fitresult.FitResult(fitpars, 0, numpy.NaN)
244
245        # Run the fit (fit should perform _progress and _improvement updates)
246        # This function may return before the fit is complete.
247        self._fit(fitness, x0, bounds)
248       
249class SansFitMC(SansFitter):
250    """
251    Monte Carlo optimizer.
252
253    This implements `park.fit.Fitter`.
254    """
255    localfit = SansFitSimplex()
256    start_points = 10
257    def __init__(self, localfit, start_points=10):
258        self.localfit = localfit
259        self.start_points = start_points
260       
261    def _fit(self, objective, x0, bounds):
262        """
263        Run a monte carlo fit.
264
265        This procedure maps a local optimizer across a set of initial points.
266        """
267        try:
268            park.fitmc.fitmc(objective, x0, bounds, self.localfit,
269                             self.start_points, self.handler)
270        except:
271            raise ValueError, "Fit did not converge.\n"
272       
273class SansPart(Part):
274    """
275    Part of a fitting assembly.  Part holds the model itself and
276    associated data.  The part can be initialized with a fitness
277    object or with a pair (model,data) for the default fitness function.
278
279    fitness (Fitness)
280        object implementing the `park.assembly.Fitness` interface.  In
281        particular, fitness should provide a parameterset attribute
282        containing a ParameterSet and a residuals method returning a vector
283        of residuals.
284    weight (dimensionless)
285        weight for the model.  See comments in assembly.py for details.
286    isfitted (boolean)
287        True if the model residuals should be included in the fit.
288        The model parameters may still be used in parameter
289        expressions, but there will be no comparison to the data.
290    residuals (vector)
291        Residuals for the model if they have been calculated, or None
292    degrees_of_freedom
293        Number of residuals minus number of fitted parameters.
294        Degrees of freedom for individual models does not make
295        sense in the presence of expressions combining models,
296        particularly in the case where a model has many parameters
297        but no data or many computed parameters.  The degrees of
298        freedom for the model is set to be at least one.
299    chisq
300        sum(residuals**2); use chisq/degrees_of_freedom to
301        get the reduced chisq value.
302
303        Get/set the weight on the given model.
304
305        assembly.weight(3) returns the weight on model 3 (0-origin)
306        assembly.weight(3,0.5) sets the weight on model 3 (0-origin)
307    """
308
309    def __init__(self, fitness, weight=1., isfitted=True):
310        Part.__init__(self, fitness=fitness, weight=weight,
311                       isfitted=isfitted)
312       
313        self.model, self.data = fitness[0], fitness[1]
314
315class SansFitParameter(FitParameter):
316    """
317    Fit result for an individual parameter.
318    """
319    def __init__(self, name, range, value, model, data):
320        FitParameter.__init__(self, name, range, value)
321        self.model = model
322        self.data = data
323       
324    def summarize(self):
325        """
326        Return parameter range string.
327
328        E.g.,  "       Gold .....|.... 5.2043 in [2,7]"
329        """
330        bar = ['.']*10
331        lo,hi = self.range
332        if numpy.isfinite(lo)and numpy.isfinite(hi):
333            portion = (self.value-lo)/(hi-lo)
334            if portion < 0: portion = 0.
335            elif portion >= 1: portion = 0.99999999
336            barpos = int(math.floor(portion*len(bar)))
337            bar[barpos] = '|'
338        bar = "".join(bar)
339        lostr = "[%g"%lo if numpy.isfinite(lo) else "(-inf"
340        histr = "%g]"%hi if numpy.isfinite(hi) else "inf)"
341        valstr = format_uncertainty(self.value, self.stderr)
342        model_name = str(None)
343        if self.model is not None:
344            model_name = self.model.name
345        data_name = str(None)
346        if self.data is not None:
347            data_name = self.data.name
348           
349        return "%25s %s %s in %s,%s, %s, %s"  % (self.name,bar,valstr,lostr,histr, 
350                                                 model_name, data_name)
351    def __repr__(self):
352        #return "FitParameter('%s')"%self.name
353        return str(self.__class__)
354   
355class MyAssembly(Assembly):
356    def __init__(self, models, curr_thread=None):
357        """Build an assembly from a list of models."""
358        self.parts = []
359        for m in models:
360            self.parts.append(SansPart(m))
361        self.curr_thread = curr_thread
362        self.chisq = None
363        self._cancel = False
364        self.theory = None
365        self._reset()
366       
367    def fit_parameters(self):
368        """
369        Return an alphabetical list of the fitting parameters.
370
371        This function is called once at the beginning of a fit,
372        and serves as a convenient place to precalculate what
373        can be precalculated such as the set of fitting parameters
374        and the parameter expressions evaluator.
375        """
376        self.parameterset.setprefix()
377        self._fitparameters = self.parameterset.fitted
378        self._restraints = self.parameterset.restrained
379        pars = self.parameterset.flatten()
380        context = self.parameterset.gather_context()
381        self._fitexpression = park.expression.build_eval(pars,context)
382        #print "constraints",self._fitexpression.__doc__
383
384        self._fitparameters.sort(lambda a,b: cmp(a.path,b.path))
385        # Convert to fitparameter a object
386       
387        fitpars = [SansFitParameter(p.path,p.range,p.value, p.model, p.data)
388                   for p in self._fitparameters]
389        #print "fitpars", fitpars
390        return fitpars
391   
392    def extend_results_with_calculated_parameters(self, result):
393        """
394        Extend result from the fit with the calculated parameters.
395        """
396        calcpars = [SansFitParameter(p.path,p.range,p.value, p.model, p.data)
397                    for p in self.parameterset.computed]
398        result.parameters += calcpars
399        result.theory = self.theory
400
401    def eval(self):
402        """
403        Recalculate the theory functions, and from them, the
404        residuals and chisq.
405
406        :note: Call this after the parameters have been updated.
407        """
408        # Handle abort from a separate thread.
409        self._cancel = False
410        if self.curr_thread != None:
411            try:
412                self.curr_thread.isquit()
413            except:
414                self._cancel = True
415
416        # Evaluate the computed parameters
417        try:
418            self._fitexpression()
419        except NameError:
420            pass
421
422        # Check that the resulting parameters are in a feasible region.
423        if not self.isfeasible(): return numpy.inf
424
425        resid = []
426        k = len(self._fitparameters)
427        for m in self.parts:
428            # In order to support abort, need to be able to propagate an
429            # external abort signal from self.abort() into an abort signal
430            # for the particular model.  Can't see a way to do this which
431            # doesn't involve setting a state variable.
432            self._current_model = m
433            if self._cancel: return numpy.inf
434            if m.isfitted and m.weight != 0:
435                m.residuals, self.theory = m.fitness.residuals()
436                N = len(m.residuals)
437                m.degrees_of_freedom = N-k if N>k else 1
438                # dividing residuals by N in order to be consistent with Scipy
439                m.chisq = numpy.sum(m.residuals**2/N) 
440                resid.append(m.weight*m.residuals)
441        self.residuals = numpy.hstack(resid)
442        N = len(self.residuals)
443        self.degrees_of_freedom = N-k if N>k else 1
444        self.chisq = numpy.sum(self.residuals**2)
445        return self.chisq/self.degrees_of_freedom
446   
447class ParkFit(FitEngine):
448    """
449    ParkFit performs the Fit.This class can be used as follow:
450    #Do the fit Park
451    create an engine: engine = ParkFit()
452    Use data must be of type plottable
453    Use a sans model
454   
455    Add data with a dictionnary of FitArrangeList where Uid is a key and data
456    is saved in FitArrange object.
457    engine.set_data(data,Uid)
458   
459    Set model parameter "M1"= model.name add {model.parameter.name:value}.
460   
461    ..note::
462       Set_param() if used must always preceded set_model() for the fit to be performed.
463      ``engine.set_param( model,"M1", {'A':2,'B':4})``
464   
465    Add model with a dictionnary of FitArrangeList{} where Uid is a key
466    and model
467    is save in FitArrange object.
468    engine.set_model(model,Uid)
469   
470    engine.fit return chisqr,[model.parameter 1,2,..],[[err1....][..err2...]]
471    chisqr1, out1, cov1=engine.fit({model.parameter.name:value},qmin,qmax)
472   
473    ..note::
474        {model.parameter.name:value} is ignored in fit function since
475        the user should make sure to call set_param himself.
476       
477    """
478    def __init__(self):
479        """
480        Creates a dictionary (self.fitArrangeList={})of FitArrange elements
481        with Uid as keys
482        """
483        FitEngine.__init__(self)
484        self.fit_arrange_dict = {}
485        self.param_list = []
486       
487    def create_assembly(self, curr_thread, reset_flag=False):
488        """
489        Extract sansmodel and sansdata from
490        self.FitArrangelist ={Uid:FitArrange}
491        Create parkmodel and park data ,form a list couple of parkmodel
492        and parkdata
493        create an assembly self.problem=  park.Assembly([(parkmodel,parkdata)])
494        """
495        mylist = []
496        #listmodel = []
497        #i = 0
498        fitproblems = []
499        for fproblem in self.fit_arrange_dict.itervalues():
500            if fproblem.get_to_fit() == 1:
501                fitproblems.append(fproblem)
502        if len(fitproblems) == 0:
503            raise RuntimeError, "No Assembly scheduled for Park fitting."
504        for item in fitproblems:
505            model = item.get_model()
506            parkmodel = ParkModel(model.model, model.data)
507            if reset_flag:
508                # reset the initial value; useful for batch
509                for name in item.pars:
510                    ind = item.pars.index(name)
511                    parkmodel.model.setParam(name, item.vals[ind])
512
513            # set the constraints into the model
514            for p,v in model.constraints:
515                parkmodel.parameterset[str(p)].set(str(v))
516           
517            for p in parkmodel.parameterset:
518                ## does not allow status change for constraint parameters
519                if p.status != 'computed':
520                    if p.get_name() in item.pars:
521                        ## make parameters selected for
522                        #fit will be between boundaries
523                        p.set(p.range)         
524                    else:
525                        p.status = 'fixed'
526            data_list = item.get_data()
527            parkdata = data_list
528            fitness = (parkmodel, parkdata)
529            mylist.append(fitness)
530        self.problem = MyAssembly(models=mylist, curr_thread=curr_thread)
531       
532 
533    def fit(self, msg_q=None, 
534            q=None, handler=None, curr_thread=None, 
535            ftol=1.49012e-8, reset_flag=False):
536        """
537        Performs fit with park.fit module.It can  perform fit with one model
538        and a set of data, more than two fit of  one model and sets of data or
539        fit with more than two model associated with their set of data and
540        constraints
541       
542        :param pars: Dictionary of parameter names for the model and their
543            values.
544        :param qmin: The minimum value of data's range to be fit
545        :param qmax: The maximum value of data's range to be fit
546       
547        :note: all parameter are ignored most of the time.Are just there
548            to keep ScipyFit and ParkFit interface the same.
549           
550        :return: result.fitness Value of the goodness of fit metric
551        :return: result.pvec list of parameter with the best value
552            found during fitting
553        :return: result.cov Covariance matrix
554       
555        """
556        self.create_assembly(curr_thread=curr_thread, reset_flag=reset_flag)
557        localfit = SansFitSimplex()
558        localfit.ftol = ftol
559        localfit.xtol = 1e-6
560
561        # See `park.fitresult.FitHandler` for details.
562        fitter = SansFitMC(localfit=localfit, start_points=1)
563        if handler == None:
564            handler = fitresult.ConsoleUpdate(improvement_delta=0.1)
565       
566        result_list = []
567        try:
568            result = fit.fit(self.problem, fitter=fitter, handler=handler)
569            self.problem.extend_results_with_calculated_parameters(result)
570           
571        except LinAlgError:
572            raise ValueError, "SVD did not converge"
573   
574        for m in self.problem.parts:
575            residuals, theory = m.fitness.residuals()
576            small_result = FResult(model=m.model, data=m.data.sans_data)
577            small_result.fitter_id = self.fitter_id
578            small_result.theory = theory
579            small_result.residuals = residuals
580            small_result.pvec = []
581            small_result.cov = []
582            small_result.stderr = []
583            small_result.param_list = []
584            small_result.residuals = m.residuals
585            if result is not None:
586                for p in result.parameters:
587                    #if p.data.name == small_result.data.name and
588                    if p.model.name == small_result.model.name:
589                        small_result.index = m.data.idx
590                        small_result.fitness = result.fitness
591                        small_result.pvec.append(p.value)
592                        small_result.stderr.append(p.stderr)
593                        name_split = p.name.split('.')
594                        name = name_split[1].strip()
595                        if len(name_split) > 2:
596                            name += '.' + name_split[2].strip()
597                        small_result.param_list.append(name)
598                # normalize chisq by degrees of freedom
599                small_result.fitness /= len(small_result.residuals)-len(small_result.pvec)
600            result_list.append(small_result)   
601        if q != None:
602            q.put(result_list)
603            return q
604        return result_list
605       
Note: See TracBrowser for help on using the repository browser.