source: sasview/src/sans/fit/ParkFitting.py @ 6fe5100

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 6fe5100 was 6fe5100, checked in by pkienzle, 10 years ago

Bumps first pass. Fitting works but no pretty pictures

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