source: sasview/src/sans/fit/ParkFitting.py @ 9d6d5ba

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

code cleanup

  • Property mode set to 100644
File size: 16.9 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 SansFitResult(fitresult.FitResult):
28    def __init__(self, *args, **kwrds):
29        fitresult.FitResult.__init__(self, *args, **kwrds)
30        self.theory = None
31        self.inputs = []
32       
33class SansFitSimplex(FitSimplex):
34    """
35    Local minimizer using Nelder-Mead simplex algorithm.
36
37    Simplex is robust and derivative free, though not very efficient.
38
39    This class wraps the bounds contrained Nelder-Mead simplex
40    implementation for `park.simplex.simplex`.
41    """
42    radius = 0.05
43    """Size of the initial simplex; this is a portion between 0 and 1"""
44    xtol = 1
45    #xtol = 1e-4
46    """Stop when simplex vertices are within xtol of each other"""
47    ftol = 5e-5
48    """Stop when vertex values are within ftol of each other"""
49    maxiter = None
50    """Maximum number of iterations before fit terminates"""
51    def __init__(self, ftol=5e-5):
52        self.ftol = ftol
53       
54    def fit(self, fitness, x0):
55        """Run the fit"""
56        self.cancel = False
57        pars = fitness.fit_parameters()
58        bounds = numpy.array([p.range for p in pars]).T
59        result = park.simplex.simplex(fitness, x0, bounds=bounds,
60                                 radius=self.radius, xtol=self.xtol,
61                                 ftol=self.ftol, maxiter=self.maxiter,
62                                 abort_test=self._iscancelled)
63        #print "calls:",result.calls
64        #print "simplex returned",result.x,result.fx
65        # Need to make our own copy of the fit results so that the
66        # values don't get stomped on by the next fit iteration.
67        fitpars = [SansFitParameter(pars[i].name,pars[i].range,v, pars[i].model, pars[i].data)
68                   for i,v in enumerate(result.x)]
69        res = SansFitResult(fitpars, result.calls, result.fx)
70        res.inputs = [(pars[i].model, pars[i].data) for i,v in enumerate(result.x)]
71        # Compute the parameter uncertainties from the jacobian
72        res.calc_cov(fitness)
73        return res
74     
75class SansFitter(Fitter):
76    """
77    """
78    def fit(self, fitness, handler):
79        """
80        Global optimizer.
81
82        This function should return immediately
83        """
84        # Determine initial value and bounds
85        pars = fitness.fit_parameters()
86        bounds = numpy.array([p.range for p in pars]).T
87        x0 = [p.value for p in pars]
88
89        # Initialize the monitor and results.
90        # Need to make our own copy of the fit results so that the
91        # values don't get stomped on by the next fit iteration.
92        handler.done = False
93        self.handler = handler
94        fitpars = [SansFitParameter(pars[i].name, pars[i].range, v,
95                                     pars[i].model, pars[i].data)
96                   for i,v in enumerate(x0)]
97        handler.result = fitresult.FitResult(fitpars, 0, numpy.NaN)
98
99        # Run the fit (fit should perform _progress and _improvement updates)
100        # This function may return before the fit is complete.
101        self._fit(fitness, x0, bounds)
102       
103class SansFitMC(SansFitter):
104    """
105    Monte Carlo optimizer.
106
107    This implements `park.fit.Fitter`.
108    """
109    localfit = SansFitSimplex()
110    start_points = 10
111    def __init__(self, localfit, start_points=10):
112        self.localfit = localfit
113        self.start_points = start_points
114       
115    def _fit(self, objective, x0, bounds):
116        """
117        Run a monte carlo fit.
118
119        This procedure maps a local optimizer across a set of initial points.
120        """
121        try:
122            park.fitmc.fitmc(objective, x0, bounds, self.localfit,
123                             self.start_points, self.handler)
124        except:
125            raise ValueError, "Fit did not converge.\n"
126       
127class SansPart(Part):
128    """
129    Part of a fitting assembly.  Part holds the model itself and
130    associated data.  The part can be initialized with a fitness
131    object or with a pair (model,data) for the default fitness function.
132
133    fitness (Fitness)
134        object implementing the `park.assembly.Fitness` interface.  In
135        particular, fitness should provide a parameterset attribute
136        containing a ParameterSet and a residuals method returning a vector
137        of residuals.
138    weight (dimensionless)
139        weight for the model.  See comments in assembly.py for details.
140    isfitted (boolean)
141        True if the model residuals should be included in the fit.
142        The model parameters may still be used in parameter
143        expressions, but there will be no comparison to the data.
144    residuals (vector)
145        Residuals for the model if they have been calculated, or None
146    degrees_of_freedom
147        Number of residuals minus number of fitted parameters.
148        Degrees of freedom for individual models does not make
149        sense in the presence of expressions combining models,
150        particularly in the case where a model has many parameters
151        but no data or many computed parameters.  The degrees of
152        freedom for the model is set to be at least one.
153    chisq
154        sum(residuals**2); use chisq/degrees_of_freedom to
155        get the reduced chisq value.
156
157        Get/set the weight on the given model.
158
159        assembly.weight(3) returns the weight on model 3 (0-origin)
160        assembly.weight(3,0.5) sets the weight on model 3 (0-origin)
161    """
162
163    def __init__(self, fitness, weight=1., isfitted=True):
164        Part.__init__(self, fitness=fitness, weight=weight,
165                       isfitted=isfitted)
166       
167        self.model, self.data = fitness[0], fitness[1]
168
169class SansFitParameter(FitParameter):
170    """
171    Fit result for an individual parameter.
172    """
173    def __init__(self, name, range, value, model, data):
174        FitParameter.__init__(self, name, range, value)
175        self.model = model
176        self.data = data
177       
178    def summarize(self):
179        """
180        Return parameter range string.
181
182        E.g.,  "       Gold .....|.... 5.2043 in [2,7]"
183        """
184        bar = ['.']*10
185        lo,hi = self.range
186        if numpy.isfinite(lo)and numpy.isfinite(hi):
187            portion = (self.value-lo)/(hi-lo)
188            if portion < 0: portion = 0.
189            elif portion >= 1: portion = 0.99999999
190            barpos = int(math.floor(portion*len(bar)))
191            bar[barpos] = '|'
192        bar = "".join(bar)
193        lostr = "[%g"%lo if numpy.isfinite(lo) else "(-inf"
194        histr = "%g]"%hi if numpy.isfinite(hi) else "inf)"
195        valstr = format_uncertainty(self.value, self.stderr)
196        model_name = str(None)
197        if self.model is not None:
198            model_name = self.model.name
199        data_name = str(None)
200        if self.data is not None:
201            data_name = self.data.name
202           
203        return "%25s %s %s in %s,%s, %s, %s"  % (self.name,bar,valstr,lostr,histr, 
204                                                 model_name, data_name)
205    def __repr__(self):
206        #return "FitParameter('%s')"%self.name
207        return str(self.__class__)
208   
209class MyAssembly(Assembly):
210    def __init__(self, models, curr_thread=None):
211        """Build an assembly from a list of models."""
212        self.parts = []
213        for m in models:
214            self.parts.append(SansPart(m))
215        self.curr_thread = curr_thread
216        self.chisq = None
217        self._cancel = False
218        self.theory = None
219        self._reset()
220       
221    def fit_parameters(self):
222        """
223        Return an alphabetical list of the fitting parameters.
224
225        This function is called once at the beginning of a fit,
226        and serves as a convenient place to precalculate what
227        can be precalculated such as the set of fitting parameters
228        and the parameter expressions evaluator.
229        """
230        self.parameterset.setprefix()
231        self._fitparameters = self.parameterset.fitted
232        self._restraints = self.parameterset.restrained
233        pars = self.parameterset.flatten()
234        context = self.parameterset.gather_context()
235        self._fitexpression = park.expression.build_eval(pars,context)
236        #print "constraints",self._fitexpression.__doc__
237
238        self._fitparameters.sort(lambda a,b: cmp(a.path,b.path))
239        # Convert to fitparameter a object
240       
241        fitpars = [SansFitParameter(p.path,p.range,p.value, p.model, p.data)
242                   for p in self._fitparameters]
243        #print "fitpars", fitpars
244        return fitpars
245   
246    def all_results(self, result):
247        """
248        Extend result from the fit with the calculated parameters.
249        """
250        calcpars = [SansFitParameter(p.path,p.range,p.value, p.model, p.data)
251                    for p in self.parameterset.computed]
252        result.parameters += calcpars
253        result.theory = self.theory
254
255    def eval(self):
256        """
257        Recalculate the theory functions, and from them, the
258        residuals and chisq.
259
260        :note: Call this after the parameters have been updated.
261        """
262        # Handle abort from a separate thread.
263        self._cancel = False
264        if self.curr_thread != None:
265            try:
266                self.curr_thread.isquit()
267            except:
268                self._cancel = True
269
270        # Evaluate the computed parameters
271        try:
272            self._fitexpression()
273        except NameError:
274            pass
275
276        # Check that the resulting parameters are in a feasible region.
277        if not self.isfeasible(): return numpy.inf
278
279        resid = []
280        k = len(self._fitparameters)
281        for m in self.parts:
282            # In order to support abort, need to be able to propagate an
283            # external abort signal from self.abort() into an abort signal
284            # for the particular model.  Can't see a way to do this which
285            # doesn't involve setting a state variable.
286            self._current_model = m
287            if self._cancel: return numpy.inf
288            if m.isfitted and m.weight != 0:
289                m.residuals, self.theory = m.fitness.residuals()
290                N = len(m.residuals)
291                m.degrees_of_freedom = N-k if N>k else 1
292                # dividing residuals by N in order to be consistent with Scipy
293                m.chisq = numpy.sum(m.residuals**2/N) 
294                resid.append(m.weight*m.residuals/math.sqrt(N))
295        self.residuals = numpy.hstack(resid)
296        N = len(self.residuals)
297        self.degrees_of_freedom = N-k if N>k else 1
298        self.chisq = numpy.sum(self.residuals**2)
299        return self.chisq
300   
301class ParkFit(FitEngine):
302    """
303    ParkFit performs the Fit.This class can be used as follow:
304    #Do the fit Park
305    create an engine: engine = ParkFit()
306    Use data must be of type plottable
307    Use a sans model
308   
309    Add data with a dictionnary of FitArrangeList where Uid is a key and data
310    is saved in FitArrange object.
311    engine.set_data(data,Uid)
312   
313    Set model parameter "M1"= model.name add {model.parameter.name:value}.
314   
315    ..note::
316       Set_param() if used must always preceded set_model() for the fit to be performed.
317      ``engine.set_param( model,"M1", {'A':2,'B':4})``
318   
319    Add model with a dictionnary of FitArrangeList{} where Uid is a key
320    and model
321    is save in FitArrange object.
322    engine.set_model(model,Uid)
323   
324    engine.fit return chisqr,[model.parameter 1,2,..],[[err1....][..err2...]]
325    chisqr1, out1, cov1=engine.fit({model.parameter.name:value},qmin,qmax)
326   
327    ..note::
328        {model.parameter.name:value} is ignored in fit function since
329        the user should make sure to call set_param himself.
330       
331    """
332    def __init__(self):
333        """
334        Creates a dictionary (self.fitArrangeList={})of FitArrange elements
335        with Uid as keys
336        """
337        FitEngine.__init__(self)
338        self.fit_arrange_dict = {}
339        self.param_list = []
340       
341    def create_assembly(self, curr_thread, reset_flag=False):
342        """
343        Extract sansmodel and sansdata from
344        self.FitArrangelist ={Uid:FitArrange}
345        Create parkmodel and park data ,form a list couple of parkmodel
346        and parkdata
347        create an assembly self.problem=  park.Assembly([(parkmodel,parkdata)])
348        """
349        mylist = []
350        #listmodel = []
351        #i = 0
352        fitproblems = []
353        for fproblem in self.fit_arrange_dict.itervalues():
354            if fproblem.get_to_fit() == 1:
355                fitproblems.append(fproblem)
356        if len(fitproblems) == 0: 
357            raise RuntimeError, "No Assembly scheduled for Park fitting."
358            return
359        for item in fitproblems:
360            parkmodel = item.get_model()
361            if reset_flag:
362                # reset the initial value; useful for batch
363                for name in item.pars:
364                    ind = item.pars.index(name)
365                    parkmodel.model.setParam(name, item.vals[ind])
366           
367            for p in parkmodel.parameterset:
368                ## does not allow status change for constraint parameters
369                if p.status != 'computed':
370                    if p.get_name()in item.pars:
371                        ## make parameters selected for
372                        #fit will be between boundaries
373                        p.set(p.range)         
374                    else:
375                        p.status = 'fixed'
376            data_list = item.get_data()
377            parkdata = data_list
378            fitness = (parkmodel, parkdata)
379            mylist.append(fitness)
380        self.problem = MyAssembly(models=mylist, curr_thread=curr_thread)
381       
382 
383    def fit(self, msg_q=None, 
384            q=None, handler=None, curr_thread=None, 
385                                        ftol=1.49012e-8, reset_flag=False):
386        """
387        Performs fit with park.fit module.It can  perform fit with one model
388        and a set of data, more than two fit of  one model and sets of data or
389        fit with more than two model associated with their set of data and
390        constraints
391       
392        :param pars: Dictionary of parameter names for the model and their
393            values.
394        :param qmin: The minimum value of data's range to be fit
395        :param qmax: The maximum value of data's range to be fit
396       
397        :note: all parameter are ignored most of the time.Are just there
398            to keep ScipyFit and ParkFit interface the same.
399           
400        :return: result.fitness Value of the goodness of fit metric
401        :return: result.pvec list of parameter with the best value
402            found during fitting
403        :return: result.cov Covariance matrix
404       
405        """
406        self.create_assembly(curr_thread=curr_thread, reset_flag=reset_flag)
407        localfit = SansFitSimplex()
408        localfit.ftol = ftol
409       
410        # See `park.fitresult.FitHandler` for details.
411        fitter = SansFitMC(localfit=localfit, start_points=1)
412        if handler == None:
413            handler = fitresult.ConsoleUpdate(improvement_delta=0.1)
414       
415        result_list = []
416        try:
417            result = fit.fit(self.problem, fitter=fitter, handler=handler)
418            self.problem.all_results(result)
419           
420        except LinAlgError:
421            raise ValueError, "SVD did not converge"
422   
423        for m in self.problem.parts:
424            residuals, theory = m.fitness.residuals()
425            small_result = FResult(model=m.model, data=m.data.sans_data)
426            small_result.fitter_id = self.fitter_id
427            small_result.theory = theory
428            small_result.residuals = residuals
429            small_result.pvec = []
430            small_result.cov = []
431            small_result.stderr = []
432            small_result.param_list = []
433            small_result.residuals = m.residuals
434            if result is not None:
435                for p in result.parameters:
436                    if p.data.name == small_result.data.name and \
437                            p.model.name == small_result.model.name:
438                        small_result.index = m.data.idx
439                        small_result.fitness = result.fitness
440                        small_result.pvec.append(p.value)
441                        small_result.stderr.append(p.stderr)
442                        name_split = p.name.split('.')
443                        name = name_split[1].strip()
444                        if len(name_split) > 2:
445                            name += '.' + name_split[2].strip()
446                        small_result.param_list.append(name)
447            result_list.append(small_result)   
448        if q != None:
449            q.put(result_list)
450            return q
451        return result_list
452       
Note: See TracBrowser for help on using the repository browser.