source: branches/asap4casa3.1.0/python/asapfitter.py @ 1935

Last change on this file since 1935 was 1935, checked in by Takeshi Nakazato, 14 years ago

New Development: No

JIRA Issue: No

Ready for Test: Yes

Interface Changes: No

What Interface Changed: Please list interface changes

Test Programs: List test programs

Put in Release Notes: Yes/No?

Module(s): Module Names change impacts.

Description: Describe your changes here...

Minor bug fix to work asapfitter.store_fit() on polynomial fitting.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 23.0 KB
RevLine 
[113]1import _asap
[1826]2from asap.parameters import rcParams
[1862]3from asap.logging import asaplog, asaplog_post_dec
[1826]4from asap.utils import _n_bools, mask_and
[113]5
[1826]6
[113]7class fitter:
8    """
9    The fitting class for ASAP.
10    """
11    def __init__(self):
12        """
13        Create a fitter object. No state is set.
14        """
15        self.fitter = _asap.fitter()
16        self.x = None
17        self.y = None
18        self.mask = None
19        self.fitfunc = None
[515]20        self.fitfuncs = None
[113]21        self.fitted = False
22        self.data = None
[515]23        self.components = 0
24        self._fittedrow = 0
[113]25        self._p = None
[515]26        self._selection = None
[1391]27        self.uselinear = False
[113]28
29    def set_data(self, xdat, ydat, mask=None):
30        """
[158]31        Set the absissa and ordinate for the fit. Also set the mask
[113]32        indicationg valid points.
33        This can be used for data vectors retrieved from a scantable.
34        For scantable fitting use 'fitter.set_scan(scan, mask)'.
35        Parameters:
[158]36            xdat:    the abcissa values
[113]37            ydat:    the ordinate values
38            mask:    an optional mask
[723]39
[113]40        """
41        self.fitted = False
42        self.x = xdat
43        self.y = ydat
44        if mask == None:
[1295]45            self.mask = _n_bools(len(xdat), True)
[113]46        else:
47            self.mask = mask
48        return
49
[1862]50    @asaplog_post_dec
[113]51    def set_scan(self, thescan=None, mask=None):
52        """
53        Set the 'data' (a scantable) of the fitter.
54        Parameters:
55            thescan:     a scantable
[1420]56            mask:        a msk retrieved from the scantable
[113]57        """
58        if not thescan:
[723]59            msg = "Please give a correct scan"
[1859]60            raise TypeError(msg)
[113]61        self.fitted = False
62        self.data = thescan
[1075]63        self.mask = None
[113]64        if mask is None:
[1295]65            self.mask = _n_bools(self.data.nchan(), True)
[113]66        else:
67            self.mask = mask
68        return
69
[1862]70    @asaplog_post_dec
[113]71    def set_function(self, **kwargs):
72        """
73        Set the function to be fit.
74        Parameters:
[1589]75            poly:    use a polynomial of the order given with nonlinear least squares fit
[1391]76            lpoly:   use polynomial of the order given with linear least squares fit
[113]77            gauss:   fit the number of gaussian specified
[1819]78            lorentz: fit the number of lorentzian specified
[113]79        Example:
[1391]80            fitter.set_function(poly=3)  # will fit a 3rd order polynomial via nonlinear method
81            fitter.set_function(lpoly=3)  # will fit a 3rd order polynomial via linear method
[1819]82            fitter.set_function(gauss=2) # will fit two gaussians
83            fitter.set_function(lorentz=2) # will fit two lorentzians
[113]84        """
[723]85        #default poly order 0
[515]86        n=0
[113]87        if kwargs.has_key('poly'):
88            self.fitfunc = 'poly'
[1935]89            self.fitfuncs = ['poly']
[113]90            n = kwargs.get('poly')
[1935]91            self.components = [n+1]
[1589]92            self.uselinear = False
[1391]93        elif kwargs.has_key('lpoly'):
94            self.fitfunc = 'poly'
[1935]95            self.fitfuncs = ['lpoly']
[1391]96            n = kwargs.get('lpoly')
[1935]97            self.components = [n+1]
[1391]98            self.uselinear = True
[113]99        elif kwargs.has_key('gauss'):
100            n = kwargs.get('gauss')
101            self.fitfunc = 'gauss'
[515]102            self.fitfuncs = [ 'gauss' for i in range(n) ]
103            self.components = [ 3 for i in range(n) ]
[1589]104            self.uselinear = False
[1819]105        elif kwargs.has_key('lorentz'):
106            n = kwargs.get('lorentz')
107            self.fitfunc = 'lorentz'
108            self.fitfuncs = [ 'lorentz' for i in range(n) ]
109            self.components = [ 3 for i in range(n) ]
110            self.uselinear = False
[515]111        else:
[723]112            msg = "Invalid function type."
[1859]113            raise TypeError(msg)
[723]114
[113]115        self.fitter.setexpression(self.fitfunc,n)
[1232]116        self.fitted = False
[113]117        return
[723]118
[1862]119    @asaplog_post_dec
[1075]120    def fit(self, row=0, estimate=False):
[113]121        """
122        Execute the actual fitting process. All the state has to be set.
123        Parameters:
[1075]124            row:        specify the row in the scantable
125            estimate:   auto-compute an initial parameter set (default False)
126                        This can be used to compute estimates even if fit was
127                        called before.
[113]128        Example:
[515]129            s = scantable('myscan.asap')
130            s.set_cursor(thepol=1)        # select second pol
[113]131            f = fitter()
132            f.set_scan(s)
133            f.set_function(poly=0)
[723]134            f.fit(row=0)                  # fit first row
[113]135        """
136        if ((self.x is None or self.y is None) and self.data is None) \
137               or self.fitfunc is None:
[723]138            msg = "Fitter not yet initialised. Please set data & fit function"
[1859]139            raise RuntimeError(msg)
[723]140
[113]141        else:
142            if self.data is not None:
[515]143                self.x = self.data._getabcissa(row)
144                self.y = self.data._getspectrum(row)
[1536]145                self.mask = mask_and(self.mask, self.data._getmask(row))
[723]146                asaplog.push("Fitting:")
[943]147                i = row
[1536]148                out = "Scan[%d] Beam[%d] IF[%d] Pol[%d] Cycle[%d]" % (self.data.getscan(i),
149                                                                      self.data.getbeam(i),
150                                                                      self.data.getif(i),
[1589]151                                                                      self.data.getpol(i),
[1536]152                                                                      self.data.getcycle(i))
[1075]153                asaplog.push(out,False)
[515]154        self.fitter.setdata(self.x, self.y, self.mask)
[1819]155        if self.fitfunc == 'gauss' or self.fitfunc == 'lorentz':
[113]156            ps = self.fitter.getparameters()
[1075]157            if len(ps) == 0 or estimate:
[113]158                self.fitter.estimate()
[1859]159        fxdpar = list(self.fitter.getfixedparameters())
160        if len(fxdpar) and fxdpar.count(0) == 0:
161             raise RuntimeError,"No point fitting, if all parameters are fixed."
162        if self.uselinear:
163            converged = self.fitter.lfit()
164        else:
165            converged = self.fitter.fit()
166        if not converged:
167            raise RuntimeError,"Fit didn't converge."
[515]168        self._fittedrow = row
[113]169        self.fitted = True
170        return
171
[1232]172    def store_fit(self, filename=None):
[526]173        """
[1232]174        Save the fit parameters.
175        Parameters:
176            filename:    if specified save as an ASCII file, if None (default)
177                         store it in the scnatable
[526]178        """
[515]179        if self.fitted and self.data is not None:
180            pars = list(self.fitter.getparameters())
181            fixed = list(self.fitter.getfixedparameters())
[975]182            from asap.asapfit import asapfit
183            fit = asapfit()
184            fit.setparameters(pars)
185            fit.setfixedparameters(fixed)
186            fit.setfunctions(self.fitfuncs)
187            fit.setcomponents(self.components)
188            fit.setframeinfo(self.data._getcoordinfo())
[1232]189            if filename is not None:
190                import os
191                filename = os.path.expandvars(os.path.expanduser(filename))
192                if os.path.exists(filename):
193                    raise IOError("File '%s' exists." % filename)
194                fit.save(filename)
195            else:
196                self.data._addfit(fit,self._fittedrow)
[515]197
[1862]198    @asaplog_post_dec
[1017]199    def set_parameters(self,*args,**kwargs):
[526]200        """
201        Set the parameters to be fitted.
202        Parameters:
203              params:    a vector of parameters
204              fixed:     a vector of which parameters are to be held fixed
205                         (default is none)
[1927]206              component: in case of multiple gaussians/lorentzians,
207                         the index of the component
[1017]208        """
209        component = None
210        fixed = None
211        params = None
[1031]212
[1017]213        if len(args) and isinstance(args[0],dict):
214            kwargs = args[0]
215        if kwargs.has_key("fixed"): fixed = kwargs["fixed"]
216        if kwargs.has_key("params"): params = kwargs["params"]
217        if len(args) == 2 and isinstance(args[1], int):
218            component = args[1]
[515]219        if self.fitfunc is None:
[723]220            msg = "Please specify a fitting function first."
[1859]221            raise RuntimeError(msg)
[1819]222        if (self.fitfunc == "gauss" or self.fitfunc == 'lorentz') and component is not None:
[1017]223            if not self.fitted and sum(self.fitter.getparameters()) == 0:
[1295]224                pars = _n_bools(len(self.components)*3, False)
225                fxd = _n_bools(len(pars), False)
[515]226            else:
[723]227                pars = list(self.fitter.getparameters())
[515]228                fxd = list(self.fitter.getfixedparameters())
229            i = 3*component
230            pars[i:i+3] = params
231            fxd[i:i+3] = fixed
232            params = pars
[723]233            fixed = fxd
[113]234        self.fitter.setparameters(params)
235        if fixed is not None:
236            self.fitter.setfixedparameters(fixed)
237        return
[515]238
[1862]239    @asaplog_post_dec
[1217]240    def set_gauss_parameters(self, peak, centre, fwhm,
[1409]241                             peakfixed=0, centrefixed=0,
[1217]242                             fwhmfixed=0,
[515]243                             component=0):
[113]244        """
[515]245        Set the Parameters of a 'Gaussian' component, set with set_function.
246        Parameters:
[1232]247            peak, centre, fwhm:  The gaussian parameters
[515]248            peakfixed,
[1409]249            centrefixed,
[1217]250            fwhmfixed:           Optional parameters to indicate if
[515]251                                 the paramters should be held fixed during
252                                 the fitting process. The default is to keep
253                                 all parameters flexible.
[526]254            component:           The number of the component (Default is the
255                                 component 0)
[515]256        """
257        if self.fitfunc != "gauss":
[723]258            msg = "Function only operates on Gaussian components."
[1859]259            raise ValueError(msg)
[515]260        if 0 <= component < len(self.components):
[1217]261            d = {'params':[peak, centre, fwhm],
[1409]262                 'fixed':[peakfixed, centrefixed, fwhmfixed]}
[1017]263            self.set_parameters(d, component)
[515]264        else:
[723]265            msg = "Please select a valid  component."
[1859]266            raise ValueError(msg)
[723]267
[1862]268    @asaplog_post_dec
[1819]269    def set_lorentz_parameters(self, peak, centre, fwhm,
270                             peakfixed=0, centrefixed=0,
271                             fwhmfixed=0,
272                             component=0):
273        """
274        Set the Parameters of a 'Lorentzian' component, set with set_function.
275        Parameters:
[1927]276            peak, centre, fwhm:  The lorentzian parameters
[1819]277            peakfixed,
278            centrefixed,
279            fwhmfixed:           Optional parameters to indicate if
280                                 the paramters should be held fixed during
281                                 the fitting process. The default is to keep
282                                 all parameters flexible.
283            component:           The number of the component (Default is the
284                                 component 0)
285        """
286        if self.fitfunc != "lorentz":
287            msg = "Function only operates on Lorentzian components."
[1859]288            raise ValueError(msg)
[1819]289        if 0 <= component < len(self.components):
290            d = {'params':[peak, centre, fwhm],
291                 'fixed':[peakfixed, centrefixed, fwhmfixed]}
292            self.set_parameters(d, component)
293        else:
294            msg = "Please select a valid  component."
[1859]295            raise ValueError(msg)
[1819]296
[975]297    def get_area(self, component=None):
298        """
[1819]299        Return the area under the fitted gaussian/lorentzian component.
[975]300        Parameters:
[1819]301              component:   the gaussian/lorentzian component selection,
[975]302                           default (None) is the sum of all components
303        Note:
[1819]304              This will only work for gaussian/lorentzian fits.
[975]305        """
306        if not self.fitted: return
[1819]307        if self.fitfunc == "gauss" or self.fitfunc == "lorentz":
[975]308            pars = list(self.fitter.getparameters())
309            from math import log,pi,sqrt
[1819]310            if self.fitfunc == "gauss":
311                fac = sqrt(pi/log(16.0))
312            elif self.fitfunc == "lorentz":
313                fac = pi/2.0
[975]314            areas = []
315            for i in range(len(self.components)):
316                j = i*3
317                cpars = pars[j:j+3]
318                areas.append(fac * cpars[0] * cpars[2])
319        else:
320            return None
321        if component is not None:
322            return areas[component]
323        else:
324            return sum(areas)
325
[1862]326    @asaplog_post_dec
[1075]327    def get_errors(self, component=None):
[515]328        """
[1075]329        Return the errors in the parameters.
330        Parameters:
331            component:    get the errors for the specified component
332                          only, default is all components
333        """
334        if not self.fitted:
335            msg = "Not yet fitted."
[1859]336            raise RuntimeError(msg)
[1075]337        errs = list(self.fitter.geterrors())
338        cerrs = errs
339        if component is not None:
[1819]340            if self.fitfunc == "gauss" or self.fitfunc == "lorentz":
[1075]341                i = 3*component
342                if i < len(errs):
343                    cerrs = errs[i:i+3]
344        return cerrs
345
[1859]346
[1862]347    @asaplog_post_dec
[1075]348    def get_parameters(self, component=None, errors=False):
349        """
[113]350        Return the fit paramters.
[526]351        Parameters:
352             component:    get the parameters for the specified component
353                           only, default is all components
[113]354        """
355        if not self.fitted:
[723]356            msg = "Not yet fitted."
[1859]357            raise RuntimeError(msg)
[113]358        pars = list(self.fitter.getparameters())
359        fixed = list(self.fitter.getfixedparameters())
[1075]360        errs = list(self.fitter.geterrors())
[1039]361        area = []
[723]362        if component is not None:
[1819]363            if self.fitfunc == "gauss" or self.fitfunc == "lorentz":
[515]364                i = 3*component
365                cpars = pars[i:i+3]
366                cfixed = fixed[i:i+3]
[1075]367                cerrs = errs[i:i+3]
[1039]368                a = self.get_area(component)
369                area = [a for i in range(3)]
[515]370            else:
371                cpars = pars
[723]372                cfixed = fixed
[1075]373                cerrs = errs
[515]374        else:
375            cpars = pars
376            cfixed = fixed
[1075]377            cerrs = errs
[1819]378            if self.fitfunc == "gauss" or self.fitfunc == "lorentz":
[1039]379                for c in range(len(self.components)):
380                  a = self.get_area(c)
381                  area += [a for i in range(3)]
[1088]382        fpars = self._format_pars(cpars, cfixed, errors and cerrs, area)
[1859]383        asaplog.push(fpars)
[1075]384        return {'params':cpars, 'fixed':cfixed, 'formatted': fpars,
385                'errors':cerrs}
[723]386
[1075]387    def _format_pars(self, pars, fixed, errors, area):
[113]388        out = ''
389        if self.fitfunc == 'poly':
390            c = 0
[515]391            for i in range(len(pars)):
392                fix = ""
[1232]393                if len(fixed) and fixed[i]: fix = "(fixed)"
[1088]394                if errors :
395                    out += '  p%d%s= %3.6f (%1.6f),' % (c,fix,pars[i], errors[i])
396                else:
397                    out += '  p%d%s= %3.6f,' % (c,fix,pars[i])
[113]398                c+=1
[515]399            out = out[:-1]  # remove trailing ','
[1819]400        elif self.fitfunc == 'gauss' or self.fitfunc == 'lorentz':
[113]401            i = 0
402            c = 0
[515]403            aunit = ''
404            ounit = ''
[113]405            if self.data:
[515]406                aunit = self.data.get_unit()
407                ounit = self.data.get_fluxunit()
[113]408            while i < len(pars):
[1039]409                if len(area):
410                    out += '  %2d: peak = %3.3f %s , centre = %3.3f %s, FWHM = %3.3f %s\n      area = %3.3f %s %s\n' % (c,pars[i],ounit,pars[i+1],aunit,pars[i+2],aunit, area[i],ounit,aunit)
[1017]411                else:
412                    out += '  %2d: peak = %3.3f %s , centre = %3.3f %s, FWHM = %3.3f %s\n' % (c,pars[i],ounit,pars[i+1],aunit,pars[i+2],aunit,ounit,aunit)
[113]413                c+=1
414                i+=3
415        return out
[723]416
[1859]417
[1862]418    @asaplog_post_dec
[113]419    def get_estimate(self):
420        """
[515]421        Return the parameter estimates (for non-linear functions).
[113]422        """
423        pars = self.fitter.getestimate()
[943]424        fixed = self.fitter.getfixedparameters()
[1927]425        asaplog.push(self._format_pars(pars,fixed,None,None))
[113]426        return pars
427
[1862]428    @asaplog_post_dec
[113]429    def get_residual(self):
430        """
431        Return the residual of the fit.
432        """
433        if not self.fitted:
[723]434            msg = "Not yet fitted."
[1859]435            raise RuntimeError(msg)
[113]436        return self.fitter.getresidual()
437
[1862]438    @asaplog_post_dec
[113]439    def get_chi2(self):
440        """
441        Return chi^2.
442        """
443        if not self.fitted:
[723]444            msg = "Not yet fitted."
[1859]445            raise RuntimeError(msg)
[113]446        ch2 = self.fitter.getchi2()
[1859]447        asaplog.push( 'Chi^2 = %3.3f' % (ch2) )
[723]448        return ch2
[113]449
[1862]450    @asaplog_post_dec
[113]451    def get_fit(self):
452        """
453        Return the fitted ordinate values.
454        """
455        if not self.fitted:
[723]456            msg = "Not yet fitted."
[1859]457            raise RuntimeError(msg)
[113]458        return self.fitter.getfit()
459
[1862]460    @asaplog_post_dec
[113]461    def commit(self):
462        """
[526]463        Return a new scan where the fits have been commited (subtracted)
[113]464        """
465        if not self.fitted:
[723]466            msg = "Not yet fitted."
[1859]467            raise RuntimeError(msg)
[975]468        from asap import scantable
469        if not isinstance(self.data, scantable):
[723]470            msg = "Not a scantable"
[1859]471            raise TypeError(msg)
[113]472        scan = self.data.copy()
[259]473        scan._setspectrum(self.fitter.getresidual())
[1092]474        return scan
[113]475
[1862]476    @asaplog_post_dec
[1689]477    def plot(self, residual=False, components=None, plotparms=False,
478             filename=None):
[113]479        """
480        Plot the last fit.
481        Parameters:
482            residual:    an optional parameter indicating if the residual
483                         should be plotted (default 'False')
[526]484            components:  a list of components to plot, e.g [0,1],
485                         -1 plots the total fit. Default is to only
486                         plot the total fit.
487            plotparms:   Inidicates if the parameter values should be present
488                         on the plot
[113]489        """
490        if not self.fitted:
491            return
[723]492        if not self._p or self._p.is_dead:
493            if rcParams['plotter.gui']:
494                from asap.asaplotgui import asaplotgui as asaplot
495            else:
496                from asap.asaplot import asaplot
497            self._p = asaplot()
498        self._p.hold()
[113]499        self._p.clear()
[515]500        self._p.set_panels()
[652]501        self._p.palette(0)
[113]502        tlab = 'Spectrum'
[723]503        xlab = 'Abcissa'
[1017]504        ylab = 'Ordinate'
[1739]505        from numpy import ma,logical_not,logical_and,array
[1273]506        m = self.mask
[113]507        if self.data:
[515]508            tlab = self.data._getsourcename(self._fittedrow)
509            xlab = self.data._getabcissalabel(self._fittedrow)
[1273]510            m =  logical_and(self.mask,
[1306]511                             array(self.data._getmask(self._fittedrow),
512                                   copy=False))
[1589]513
[626]514            ylab = self.data._get_ordinate_label()
[515]515
[1075]516        colours = ["#777777","#dddddd","red","orange","purple","green","magenta", "cyan"]
[1819]517        nomask=True
518        for i in range(len(m)):
519            nomask = nomask and m[i]
520        label0='Masked Region'
521        label1='Spectrum'
522        if ( nomask ):
523            label0=label1
524        else:
525            y = ma.masked_array( self.y, mask = m )
526            self._p.palette(1,colours)
527            self._p.set_line( label = label1 )
528            self._p.plot( self.x, y )
[652]529        self._p.palette(0,colours)
[1819]530        self._p.set_line(label=label0)
[1273]531        y = ma.masked_array(self.y,mask=logical_not(m))
[1088]532        self._p.plot(self.x, y)
[113]533        if residual:
[1819]534            self._p.palette(7)
[515]535            self._p.set_line(label='Residual')
[1116]536            y = ma.masked_array(self.get_residual(),
[1273]537                                  mask=logical_not(m))
[1088]538            self._p.plot(self.x, y)
[652]539        self._p.palette(2)
[515]540        if components is not None:
541            cs = components
542            if isinstance(components,int): cs = [components]
[526]543            if plotparms:
[1031]544                self._p.text(0.15,0.15,str(self.get_parameters()['formatted']),size=8)
[515]545            n = len(self.components)
[652]546            self._p.palette(3)
[515]547            for c in cs:
548                if 0 <= c < n:
549                    lab = self.fitfuncs[c]+str(c)
550                    self._p.set_line(label=lab)
[1116]551                    y = ma.masked_array(self.fitter.evaluate(c),
[1273]552                                          mask=logical_not(m))
[1088]553
554                    self._p.plot(self.x, y)
[515]555                elif c == -1:
[652]556                    self._p.palette(2)
[515]557                    self._p.set_line(label="Total Fit")
[1116]558                    y = ma.masked_array(self.fitter.getfit(),
[1273]559                                          mask=logical_not(m))
[1088]560                    self._p.plot(self.x, y)
[515]561        else:
[652]562            self._p.palette(2)
[515]563            self._p.set_line(label='Fit')
[1116]564            y = ma.masked_array(self.fitter.getfit(),
[1273]565                                  mask=logical_not(m))
[1088]566            self._p.plot(self.x, y)
[723]567        xlim=[min(self.x),max(self.x)]
568        self._p.axes.set_xlim(xlim)
[113]569        self._p.set_axes('xlabel',xlab)
570        self._p.set_axes('ylabel',ylab)
571        self._p.set_axes('title',tlab)
572        self._p.release()
[723]573        if (not rcParams['plotter.gui']):
574            self._p.save(filename)
[113]575
[1862]576    @asaplog_post_dec
[1061]577    def auto_fit(self, insitu=None, plot=False):
[113]578        """
[515]579        Return a scan where the function is applied to all rows for
580        all Beams/IFs/Pols.
[723]581
[113]582        """
583        from asap import scantable
[515]584        if not isinstance(self.data, scantable) :
[723]585            msg = "Data is not a scantable"
[1859]586            raise TypeError(msg)
[259]587        if insitu is None: insitu = rcParams['insitu']
588        if not insitu:
589            scan = self.data.copy()
590        else:
591            scan = self.data
[880]592        rows = xrange(scan.nrow())
[1826]593        # Save parameters of baseline fits as a class attribute.
[1819]594        # NOTICE: This does not reflect changes in scantable!
595        if len(rows) > 0: self.blpars=[]
[876]596        asaplog.push("Fitting:")
597        for r in rows:
[1536]598            out = " Scan[%d] Beam[%d] IF[%d] Pol[%d] Cycle[%d]" % (scan.getscan(r),
599                                                                   scan.getbeam(r),
600                                                                   scan.getif(r),
[1589]601                                                                   scan.getpol(r),
[1536]602                                                                   scan.getcycle(r))
[880]603            asaplog.push(out, False)
[876]604            self.x = scan._getabcissa(r)
605            self.y = scan._getspectrum(r)
[1536]606            self.mask = mask_and(self.mask, scan._getmask(r))
[876]607            self.data = None
608            self.fit()
609            x = self.get_parameters()
[1819]610            fpar = self.get_parameters()
[1061]611            if plot:
612                self.plot(residual=True)
613                x = raw_input("Accept fit ([y]/n): ")
614                if x.upper() == 'N':
[1819]615                    self.blpars.append(None)
[1061]616                    continue
[880]617            scan._setspectrum(self.fitter.getresidual(), r)
[1819]618            self.blpars.append(fpar)
[1061]619        if plot:
620            self._p.unmap()
621            self._p = None
[876]622        return scan
Note: See TracBrowser for help on using the repository browser.