source: sasview/Invariant/invariant.py @ 210ff4f

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 210ff4f was 16f60cb, checked in by Jae Cho <jhjcho@…>, 14 years ago

fixed minor bug toward unit_test

  • Property mode set to 100644
File size: 35.1 KB
RevLine 
[a45622a]1#####################################################################
2#This software was developed by the University of Tennessee as part of the
3#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
4#project funded by the US National Science Foundation.
5#See the license text in license.txt
6#copyright 2010, University of Tennessee
7######################################################################
[2e94cbde]8
9"""
[a45622a]10This module implements invariant and its related computations.
[2e94cbde]11
[a45622a]12:author: Gervaise B. Alina/UTK
13:author: Mathieu Doucet/UTK
14:author: Jae Cho/UTK
[c75a8ed]15
[75047cf]16"""
17import math 
[ef9ed58]18import numpy
[75047cf]19
[46d50ca]20from DataLoader.data_info import Data1D as LoaderData1D
[75047cf]21
[b6666d4]22# The minimum q-value to be used when extrapolating
23Q_MINIMUM  = 1e-5
24
25# The maximum q-value to be used when extrapolating
26Q_MAXIMUM  = 10
27
28# Number of steps in the extrapolation
29INTEGRATION_NSTEPS = 1000
30
[59a41066]31class Transform(object):
[ef9ed58]32    """
[a45622a]33    Define interface that need to compute a function or an inverse
34    function given some x, y
[59a41066]35    """
[76c1727]36   
37    def linearize_data(self, data):
[59a41066]38        """
[a45622a]39        Linearize data so that a linear fit can be performed.
40        Filter out the data that can't be transformed.
41       
42        :param data: LoadData1D instance
43       
[76c1727]44        """
45        # Check that the vector lengths are equal
46        assert(len(data.x)==len(data.y))
47        if data.dy is not None:
48            assert(len(data.x)==len(data.dy))
49            dy = data.dy
50        else:
[bdd162f]51            dy = numpy.ones(len(data.y))
[76c1727]52           
53        # Transform the data
[bdd162f]54        data_points = zip(data.x, data.y, dy)
55
[76c1727]56        output_points = [(self.linearize_q_value(p[0]),
57                          math.log(p[1]),
[bdd162f]58                          p[2]/p[1]) for p in data_points if p[0]>0 and p[1]>0 and p[2]>0]
[76c1727]59       
60        x_out, y_out, dy_out = zip(*output_points)
61       
[bdd162f]62        # Create Data1D object
[76c1727]63        x_out = numpy.asarray(x_out)
64        y_out = numpy.asarray(y_out)
65        dy_out = numpy.asarray(dy_out)
[bdd162f]66        linear_data = LoaderData1D(x=x_out, y=y_out, dy=dy_out)
[76c1727]67       
68        return linear_data
[bdd162f]69   
70    def get_allowed_bins(self, data):
[76c1727]71        """
[a45622a]72        Goes through the data points and returns a list of boolean values
73        to indicate whether each points is allowed by the model or not.
74       
75        :param data: Data1D object
[59a41066]76        """
[bdd162f]77        return [p[0]>0 and p[1]>0 and p[2]>0 for p in zip(data.x, data.y, data.dy)]
78       
[aafa962]79    def linearize_q_value(self, value):
[59a41066]80        """
[a45622a]81        Transform the input q-value for linearization
[59a41066]82        """
[82703a1]83        return NotImplemented
[aafa962]84
[bdd162f]85    def extract_model_parameters(self, constant, slope, dconstant=0, dslope=0):
[59a41066]86        """
[a45622a]87        set private member
[59a41066]88        """
[82703a1]89        return NotImplemented
[aafa962]90     
91    def evaluate_model(self, x):
[59a41066]92        """
[a45622a]93        Returns an array f(x) values where f is the Transform function.
[59a41066]94        """
[82703a1]95        return NotImplemented
96   
[bdd162f]97    def evaluate_model_errors(self, x):
98        """
[a45622a]99        Returns an array of I(q) errors
[bdd162f]100        """
101        return NotImplemented
102   
[59a41066]103class Guinier(Transform):
104    """
[a45622a]105    class of type Transform that performs operations related to guinier
106    function
[59a41066]107    """
108    def __init__(self, scale=1, radius=60):
109        Transform.__init__(self)
110        self.scale = scale
111        self.radius = radius
[bdd162f]112        ## Uncertainty of scale parameter
113        self.dscale  = 0
114        ## Unvertainty of radius parameter
115        self.dradius = 0
[76c1727]116       
[aafa962]117    def linearize_q_value(self, value):
[82703a1]118        """
[a45622a]119        Transform the input q-value for linearization
120       
121        :param value: q-value
122       
123        :return: q*q
[82703a1]124        """
125        return value * value
[59a41066]126   
[bdd162f]127    def extract_model_parameters(self, constant, slope, dconstant=0, dslope=0):
[a45622a]128        """
129            assign new value to the scale and the radius
[82703a1]130        """
[bdd162f]131        self.scale = math.exp(constant)
132        self.radius = math.sqrt(-3 * slope)
133       
134        # Errors
135        self.dscale = math.exp(constant)*dconstant
136        self.dradius = -3.0/2.0/math.sqrt(-3 * slope)*dslope
[59a41066]137       
[c4f52e3]138        return [self.radius, self.scale], [self.dradius, self.dscale]
139       
[aafa962]140    def evaluate_model(self, x):
[59a41066]141        """
[a45622a]142        return F(x)= scale* e-((radius*x)**2/3)
[59a41066]143        """
144        return self._guinier(x)
[aafa962]145             
[bdd162f]146    def evaluate_model_errors(self, x):
147        """
[a45622a]148        Returns the error on I(q) for the given array of q-values
149       
150        :param x: array of q-values
[bdd162f]151        """
152        p1 = numpy.array([self.dscale * math.exp(-((self.radius * q)**2/3)) for q in x])
153        p2 = numpy.array([self.scale * math.exp(-((self.radius * q)**2/3)) * (-(q**2/3)) * 2 * self.radius * self.dradius for q in x])
154        diq2 = p1*p1 + p2*p2       
155        return numpy.array( [math.sqrt(err) for err in diq2] )
156             
[59a41066]157    def _guinier(self, x):
158        """
[a45622a]159        Retrive the guinier function after apply an inverse guinier function
160        to x
161        Compute a F(x) = scale* e-((radius*x)**2/3).
162       
163        :param x: a vector of q values
164        :param scale: the scale value
165        :param radius: the guinier radius value
166       
167        :return: F(x)
[59a41066]168        """   
169        # transform the radius of coming from the inverse guinier function to a
170        # a radius of a guinier function
171        if self.radius <= 0:
172            raise ValueError("Rg expected positive value, but got %s"%self.radius) 
173        value = numpy.array([math.exp(-((self.radius * i)**2/3)) for i in x ]) 
174        return self.scale * value
175
176class PowerLaw(Transform):
177    """
[a45622a]178    class of type transform that perform operation related to power_law
179    function
[ef9ed58]180    """
[59a41066]181    def __init__(self, scale=1, power=4):
182        Transform.__init__(self)
183        self.scale = scale
184        self.power = power
[76c1727]185   
[aafa962]186    def linearize_q_value(self, value):
187        """
[a45622a]188        Transform the input q-value for linearization
189       
190        :param value: q-value
191       
192        :return: log(q)
[aafa962]193        """
194        return math.log(value)
195   
[bdd162f]196    def extract_model_parameters(self, constant, slope, dconstant=0, dslope=0):
[82703a1]197        """
[a45622a]198        Assign new value to the scale and the power
[82703a1]199        """
[bdd162f]200        self.power = -slope
201        self.scale = math.exp(constant)
202       
203        # Errors
204        self.dscale = math.exp(constant)*dconstant
[c4f52e3]205        self.dpower = -dslope
206       
207        return [self.power, self.scale], [self.dpower, self.dscale]
[82703a1]208       
[aafa962]209    def evaluate_model(self, x):
[59a41066]210        """
[a45622a]211        given a scale and a radius transform x, y using a power_law
212        function
[59a41066]213        """
214        return self._power_law(x)
[bdd162f]215   
216    def evaluate_model_errors(self, x):
217        """
[a45622a]218        Returns the error on I(q) for the given array of q-values
219        :param x: array of q-values
[bdd162f]220        """
221        p1 = numpy.array([self.dscale * math.pow(q, -self.power) for q in x])
[c4f52e3]222        p2 = numpy.array([self.scale * self.power * math.pow(q, -self.power-1) * self.dpower for q in x])
[bdd162f]223        diq2 = p1*p1 + p2*p2       
224        return numpy.array( [math.sqrt(err) for err in diq2] )
[59a41066]225       
226    def _power_law(self, x):
227        """
[a45622a]228        F(x) = scale* (x)^(-power)
229            when power= 4. the model is porod
230            else power_law
231        The model has three parameters: ::
232            1. x: a vector of q values
233            2. power: power of the function
234            3. scale : scale factor value
235       
236        :param x: array
237        :return: F(x)
[59a41066]238        """
239        if self.power <= 0:
[82703a1]240            raise ValueError("Power_law function expected positive power, but got %s"%self.power)
[59a41066]241        if self.scale <= 0:
242            raise ValueError("scale expected positive value, but got %s"%self.scale) 
243       
244        value = numpy.array([ math.pow(i, -self.power) for i in x ]) 
245        return self.scale * value
[ef9ed58]246
[59a41066]247class Extrapolator:
[75047cf]248    """
[a45622a]249    Extrapolate I(q) distribution using a given model
[75047cf]250    """
[bdd162f]251    def __init__(self, data, model=None):
[75047cf]252        """
[a45622a]253        Determine a and b given a linear equation y = ax + b
254       
255        If a model is given, it will be used to linearize the data before
256        the extrapolation is performed. If None, a simple linear fit will be done.
257       
258        :param data: data containing x and y  such as  y = ax + b
259        :param model: optional Transform object
[75047cf]260        """
261        self.data  = data
[bdd162f]262        self.model = model
[59a41066]263       
[2a11d09]264        # Set qmin as the lowest non-zero value
265        self.qmin = Q_MINIMUM
266        for q_value in self.data.x:
[59a41066]267            if q_value > 0: 
[2a11d09]268                self.qmin = q_value
269                break
270        self.qmax = max(self.data.x)
[bdd162f]271             
272    def fit(self, power=None, qmin=None, qmax=None):
[75047cf]273        """
[a45622a]274        Fit data for y = ax + b  return a and b
275       
276        :param power: a fixed, otherwise None
277        :param qmin: Minimum Q-value
278        :param qmax: Maximum Q-value
[75047cf]279        """
[bdd162f]280        if qmin is None:
281            qmin = self.qmin
282        if qmax is None:
283            qmax = self.qmax
284           
285        # Identify the bin range for the fit
286        idx = (self.data.x >= qmin) & (self.data.x <= qmax)
287       
[437a9f0]288        fx = numpy.zeros(len(self.data.x))
[59a41066]289
[bdd162f]290        # Uncertainty
291        if type(self.data.dy)==numpy.ndarray and len(self.data.dy)==len(self.data.x):
[3082632]292            sigma = self.data.dy
[2a11d09]293        else:
294            sigma = numpy.ones(len(self.data.x))
[59a41066]295           
[9b6497bb]296        # Compute theory data f(x)
[bdd162f]297        fx[idx] = self.data.y[idx]
[472b11c]298       
[bdd162f]299        # Linearize the data
300        if self.model is not None:
301            linearized_data = self.model.linearize_data(LoaderData1D(self.data.x[idx],
302                                                                fx[idx],
303                                                                dy = sigma[idx]))
304        else:
305            linearized_data = LoaderData1D(self.data.x[idx],
306                                           fx[idx],
307                                           dy = sigma[idx])
[59a41066]308       
[472b11c]309        ##power is given only for function = power_law   
310        if power != None:
[bdd162f]311            sigma2 = linearized_data.dy * linearized_data.dy
[eb62193]312            a = -(power)
[bdd162f]313            b = (numpy.sum(linearized_data.y/sigma2) \
314                 - a*numpy.sum(linearized_data.x/sigma2))/numpy.sum(1.0/sigma2)
315           
[82703a1]316           
[bdd162f]317            deltas = linearized_data.x*a+numpy.ones(len(linearized_data.x))*b-linearized_data.y
318            residuals = numpy.sum(deltas*deltas/sigma2)
319           
320            err = math.fabs(residuals) / numpy.sum(1.0/sigma2)
321            return [a, b], [0, math.sqrt(err)]
322        else:
323            A = numpy.vstack([ linearized_data.x/linearized_data.dy,
324                               1.0/linearized_data.dy]).T       
325            (p, residuals, rank, s) = numpy.linalg.lstsq(A, linearized_data.y/linearized_data.dy)
326           
327            # Get the covariance matrix, defined as inv_cov = a_transposed * a
328            err = numpy.zeros(2)
329            try:
330                inv_cov = numpy.dot(A.transpose(), A)
331                cov = numpy.linalg.pinv(inv_cov)
332                err_matrix = math.fabs(residuals) * cov
333                err = [math.sqrt(err_matrix[0][0]), math.sqrt(err_matrix[1][1])]
334            except:
335                err = [-1.0, -1.0]
336               
337            return p, err
[aafa962]338       
[b6666d4]339
[75047cf]340class InvariantCalculator(object):
341    """
[a45622a]342    Compute invariant if data is given.
343    Can provide volume fraction and surface area if the user provides
344    Porod constant  and contrast values.
345   
346    :precondition:  the user must send a data of type DataLoader.Data1D
347                    the user provide background and scale values.
348                   
349    :note: Some computations depends on each others.
[75047cf]350    """
351    def __init__(self, data, background=0, scale=1 ):
352        """
[a45622a]353        Initialize variables.
354       
355        :param data: data must be of type DataLoader.Data1D
356        :param background: Background value. The data will be corrected before processing
357        :param scale: Scaling factor for I(q). The data will be corrected before processing
[75047cf]358        """
[b6666d4]359        # Background and scale should be private data member if the only way to
360        # change them are by instantiating a new object.
361        self._background = background
362        self._scale = scale
[7182d96]363        # slit height for smeared data
364        self._smeared = None
[b6666d4]365        # The data should be private
366        self._data = self._get_data(data)
[7182d96]367        # get the dxl if the data is smeared: This is done only once on init.
[16f60cb]368        if self._data.dxl != None and self._data.dxl.all() >0:
[7182d96]369            # assumes constant dxl
[16f60cb]370            self._smeared = self._data.dxl[0]
[f31ab59]371     
[b6666d4]372        # Since there are multiple variants of Q*, you should force the
373        # user to use the get method and keep Q* a private data member
374        self._qstar = None
375       
376        # You should keep the error on Q* so you can reuse it without
377        # recomputing the whole thing.
378        self._qstar_err = 0
[75047cf]379       
[b6666d4]380        # Extrapolation parameters
381        self._low_extrapolation_npts = 4
[59a41066]382        self._low_extrapolation_function = Guinier()
[4e80ae0]383        self._low_extrapolation_power = None
[90e5ca1]384        self._low_extrapolation_power_fitted = None
[ef9ed58]385   
[b6666d4]386        self._high_extrapolation_npts = 4
[59a41066]387        self._high_extrapolation_function = PowerLaw()
[4e80ae0]388        self._high_extrapolation_power = None
[90e5ca1]389        self._high_extrapolation_power_fitted = None
[75047cf]390       
[2e94cbde]391        # Extrapolation range
392        self._low_q_limit = Q_MINIMUM
393       
[b6666d4]394    def _get_data(self, data):
[75047cf]395        """
[a45622a]396        :note: this function must be call before computing any type
397         of invariant
398         
399        :return: new data = self._scale *data - self._background
[75047cf]400        """
[b6666d4]401        if not issubclass(data.__class__, LoaderData1D):
402            #Process only data that inherited from DataLoader.Data_info.Data1D
403            raise ValueError,"Data must be of type DataLoader.Data1D"
[6d55d81]404        #from copy import deepcopy
[82703a1]405        new_data = (self._scale * data) - self._background   
[bdd162f]406       
407        # Check that the vector lengths are equal
408        assert(len(new_data.x)==len(new_data.y))
409       
410        # Verify that the errors are set correctly
411        if new_data.dy is None or len(new_data.x) != len(new_data.dy) or \
412            (min(new_data.dy)==0 and max(new_data.dy)==0):
413            new_data.dy = numpy.ones(len(new_data.x)) 
[82703a1]414        return  new_data
415     
[c4f52e3]416    def _fit(self, model, qmin=Q_MINIMUM, qmax=Q_MAXIMUM, power=None):
[75047cf]417        """
[a45622a]418        fit data with function using
419        data = self._get_data()
420        fx = Functor(data , function)
421        y = data.y
422        slope, constant = linalg.lstsq(y,fx)
423       
424        :param qmin: data first q value to consider during the fit
425        :param qmax: data last q value to consider during the fit
426        :param power : power value to consider for power-law
427        :param function: the function to use during the fit
428       
429        :return a: the scale of the function
430        :return b: the other parameter of the function for guinier will be radius
431                for power_law will be the power value
[ef9ed58]432        """
[bdd162f]433        extrapolator = Extrapolator(data=self._data, model=model)
434        p, dp = extrapolator.fit(power=power, qmin=qmin, qmax=qmax) 
[82703a1]435       
[bdd162f]436        return model.extract_model_parameters(constant=p[1], slope=p[0], dconstant=dp[1], dslope=dp[0])
[ef9ed58]437   
438    def _get_qstar(self, data):
[75047cf]439        """
[a45622a]440        Compute invariant for pinhole data.
441        This invariant is given by: ::
442   
443            q_star = x0**2 *y0 *dx0 +x1**2 *y1 *dx1
[7182d96]444                        + ..+ xn**2 *yn *dxn    for non smeared data
445                       
446            q_star = dxl0 *x0 *y0 *dx0 +dxl1 *x1 *y1 *dx1
447                        + ..+ dlxn *xn *yn *dxn    for smeared data
[a45622a]448                       
[669aaf9]449            where n >= len(data.x)-1
[7182d96]450            dxl = slit height dQl
[75047cf]451            dxi = 1/2*(xi+1 - xi) + (xi - xi-1)
[3bb37ef]452            dx0 = (x1 - x0)/2
453            dxn = (xn - xn-1)/2
[a45622a]454           
455        :param data: the data to use to compute invariant.
456       
457        :return q_star: invariant value for pinhole data. q_star > 0
[75047cf]458        """
[ef9ed58]459        if len(data.x) <= 1 or len(data.y) <= 1 or len(data.x)!= len(data.y):
460            msg =  "Length x and y must be equal"
461            msg += " and greater than 1; got x=%s, y=%s"%(len(data.x), len(data.y))
462            raise ValueError, msg
463        else:
[7182d96]464            # Take care of smeared data
465            if self._smeared is None:
466                gx = data.x * data.x
467            # assumes that len(x) == len(dxl).
468            else:               
469                gx = data.dxl * data.x
470               
[ef9ed58]471            n = len(data.x)- 1
472            #compute the first delta q
[3bb37ef]473            dx0 = (data.x[1] - data.x[0])/2
[ef9ed58]474            #compute the last delta q
[3bb37ef]475            dxn = (data.x[n] - data.x[n-1])/2
[ef9ed58]476            sum = 0
[7182d96]477            sum += gx[0] * data.y[0] * dx0
478            sum += gx[n] * data.y[n] * dxn
[ef9ed58]479           
480            if len(data.x) == 2:
481                return sum
482            else:
483                #iterate between for element different from the first and the last
484                for i in xrange(1, n-1):
485                    dxi = (data.x[i+1] - data.x[i-1])/2
[7182d96]486                    sum += gx[i] * data.y[i] * dxi
[ef9ed58]487                return sum
488           
[bdd162f]489    def _get_qstar_uncertainty(self, data):
[75047cf]490        """
[a45622a]491        Compute invariant uncertainty with with pinhole data.
492        This uncertainty is given as follow: ::
493       
494           dq_star = math.sqrt[(x0**2*(dy0)*dx0)**2 +
495                (x1**2 *(dy1)*dx1)**2 + ..+ (xn**2 *(dyn)*dxn)**2 ]
496        where n >= len(data.x)-1
497        dxi = 1/2*(xi+1 - xi) + (xi - xi-1)
498        dx0 = (x1 - x0)/2
499        dxn = (xn - xn-1)/2
500        dyn: error on dy
501       
502        :param data:
503        :note: if data doesn't contain dy assume dy= math.sqrt(data.y)
[bdd162f]504        """         
[ef9ed58]505        if len(data.x) <= 1 or len(data.y) <= 1 or \
[bdd162f]506            len(data.x) != len(data.y) or \
507            (data.dy is not None and (len(data.dy) != len(data.y))):
[ef9ed58]508            msg = "Length of data.x and data.y must be equal"
509            msg += " and greater than 1; got x=%s, y=%s"%(len(data.x),
510                                                         len(data.y))
511            raise ValueError, msg
512        else:
513            #Create error for data without dy error
[bdd162f]514            if data.dy is None:
[ef9ed58]515                dy = math.sqrt(y) 
516            else:
517                dy = data.dy
[7182d96]518            # Take care of smeared data
519            if self._smeared is None:
520                gx = data.x * data.x
521            # assumes that len(x) == len(dxl).
522            else:
523                gx = data.dxl * data.x
524 
[ef9ed58]525            n = len(data.x) - 1
526            #compute the first delta
[3bb37ef]527            dx0 = (data.x[1] - data.x[0])/2
[ef9ed58]528            #compute the last delta
[3bb37ef]529            dxn= (data.x[n] - data.x[n-1])/2
[ef9ed58]530            sum = 0
[7182d96]531            sum += (gx[0] * dy[0] * dx0)**2
532            sum += (gx[n] * dy[n] * dxn)**2
[ef9ed58]533            if len(data.x) == 2:
534                return math.sqrt(sum)
535            else:
536                #iterate between for element different from the first and the last
537                for i in xrange(1, n-1):
538                    dxi = (data.x[i+1] - data.x[i-1])/2
[7182d96]539                    sum += (gx[i] * dy[i] * dxi)**2
[ef9ed58]540                return math.sqrt(sum)
[b6666d4]541       
[aafa962]542    def _get_extrapolated_data(self, model, npts=INTEGRATION_NSTEPS,
543                              q_start=Q_MINIMUM, q_end=Q_MAXIMUM):
544        """
[a45622a]545        :return: extrapolate data create from data
[aafa962]546        """
547        #create new Data1D to compute the invariant
548        q = numpy.linspace(start=q_start,
[bdd162f]549                           stop=q_end,
550                           num=npts,
551                           endpoint=True)
[aafa962]552        iq = model.evaluate_model(q)
[bdd162f]553        diq = model.evaluate_model_errors(q)
[aafa962]554         
[bdd162f]555        result_data = LoaderData1D(x=q, y=iq, dy=diq)
[7182d96]556        if self._smeared != None:
557            result_data.dxl = self._smeared * numpy.ones(len(q))
[aafa962]558        return result_data
[76c1727]559   
[6d55d81]560    def get_data(self):
561        """
[a45622a]562        :return: self._data
[6d55d81]563        """
564        return self._data
565   
[90e5ca1]566    def get_extrapolation_power(self, range='high'):
567        """
[a45622a]568        :return: the fitted power for power law function for a given extrapolation range
[90e5ca1]569        """
570        if range == 'low':
571            return self._low_extrapolation_power_fitted
572        return self._high_extrapolation_power_fitted
573   
[bdd162f]574    def get_qstar_low(self):
[75047cf]575        """
[a45622a]576        Compute the invariant for extrapolated data at low q range.
577       
578        Implementation:
579            data = self._get_extra_data_low()
580            return self._get_qstar()
[b6666d4]581           
[a45622a]582        :return q_star: the invariant for data extrapolated at low q.
[75047cf]583        """
[aafa962]584        # Data boundaries for fitting
[ef9ed58]585        qmin = self._data.x[0]
[437a9f0]586        qmax = self._data.x[self._low_extrapolation_npts - 1]
[aafa962]587       
[2a11d09]588        # Extrapolate the low-Q data
[c4f52e3]589        p, dp = self._fit(model=self._low_extrapolation_function,
590                              qmin=qmin,
591                          qmax=qmax,
592                          power=self._low_extrapolation_power)
593        self._low_extrapolation_power_fitted = p[0]
[bdd162f]594       
595        # Distribution starting point
[2e94cbde]596        self._low_q_limit = Q_MINIMUM
[3bb37ef]597        if Q_MINIMUM >= qmin:
[2e94cbde]598            self._low_q_limit = qmin/10
[82703a1]599       
[bdd162f]600        data = self._get_extrapolated_data(model=self._low_extrapolation_function,
[aafa962]601                                               npts=INTEGRATION_NSTEPS,
[2e94cbde]602                                               q_start=self._low_q_limit, q_end=qmin)
[bdd162f]603       
604        # Systematic error
605        # If we have smearing, the shape of the I(q) distribution at low Q will
606        # may not be a Guinier or simple power law. The following is a conservative
607        # estimation for the systematic error.
[2e94cbde]608        err = qmin*qmin*math.fabs((qmin-self._low_q_limit)*(data.y[0] - data.y[INTEGRATION_NSTEPS-1]))
[bdd162f]609        return self._get_qstar(data), self._get_qstar_uncertainty(data)+err
610       
611    def get_qstar_high(self):
[75047cf]612        """
[a45622a]613        Compute the invariant for extrapolated data at high q range.
614       
615        Implementation:
616            data = self._get_extra_data_high()
617            return self._get_qstar()
[75047cf]618           
[a45622a]619        :return q_star: the invariant for data extrapolated at high q.
[75047cf]620        """
[82703a1]621        # Data boundaries for fitting
[ef9ed58]622        x_len = len(self._data.x) - 1
[82703a1]623        qmin = self._data.x[x_len - (self._high_extrapolation_npts - 1)]
[ef9ed58]624        qmax = self._data.x[x_len]
625       
[59a41066]626        # fit the data with a model to get the appropriate parameters
[c4f52e3]627        p, dp = self._fit(model=self._high_extrapolation_function,
628                          qmin=qmin,
629                          qmax=qmax,
630                          power=self._high_extrapolation_power)
631        self._high_extrapolation_power_fitted = p[0]
[aafa962]632       
[ef9ed58]633        #create new Data1D to compute the invariant
[bdd162f]634        data = self._get_extrapolated_data(model=self._high_extrapolation_function,
635                                           npts=INTEGRATION_NSTEPS,
[2e94cbde]636                                           q_start=qmax, q_end=Q_MAXIMUM)       
[76c1727]637       
[bdd162f]638        return self._get_qstar(data), self._get_qstar_uncertainty(data)
[76c1727]639   
[c75a8ed]640    def get_extra_data_low(self, npts_in=None, q_start=None, npts=20):
[2e94cbde]641        """
[a45622a]642        Returns the extrapolated data used for the loew-Q invariant calculation.
643        By default, the distribution will cover the data points used for the
644        extrapolation. The number of overlap points is a parameter (npts_in).
645        By default, the maximum q-value of the distribution will be 
646        the minimum q-value used when extrapolating for the purpose of the
647        invariant calculation.
648       
649        :param npts_in: number of data points for which the extrapolated data overlap
650        :param q_start: is the minimum value to uses for extrapolated data
651        :param npts: the number of points in the extrapolated distribution
[1702180]652           
[76c1727]653        """
[2e94cbde]654        # Get extrapolation range
655        if q_start is None:
656            q_start = self._low_q_limit
[bdd162f]657           
[2e94cbde]658        if npts_in is None:
[76c1727]659            npts_in = self._low_extrapolation_npts
[2e94cbde]660        q_end = self._data.x[max(0, npts_in-1)]
[76c1727]661       
[2e94cbde]662        if q_start >= q_end:
663            return numpy.zeros(0), numpy.zeros(0)
664
665        return self._get_extrapolated_data(model=self._low_extrapolation_function,
[c75a8ed]666                                           npts=npts,
[2e94cbde]667                                           q_start=q_start, q_end=q_end)
[76c1727]668         
[2e94cbde]669    def get_extra_data_high(self, npts_in=None, q_end=Q_MAXIMUM, npts=20):
670        """
[a45622a]671        Returns the extrapolated data used for the high-Q invariant calculation.
672        By default, the distribution will cover the data points used for the
673        extrapolation. The number of overlap points is a parameter (npts_in).
674        By default, the maximum q-value of the distribution will be Q_MAXIMUM,
675        the maximum q-value used when extrapolating for the purpose of the
676        invariant calculation.
677       
678        :param npts_in: number of data points for which the extrapolated data overlap
679        :param q_end: is the maximum value to uses for extrapolated data
680        :param npts: the number of points in the extrapolated distribution
[76c1727]681        """
[2e94cbde]682        # Get extrapolation range
683        if npts_in is None:
[76c1727]684            npts_in = self._high_extrapolation_npts
[c75a8ed]685        _npts = len(self._data.x)
686        q_start = self._data.x[min(_npts, _npts-npts_in)]
[76c1727]687       
[2e94cbde]688        if q_start >= q_end:
689            return numpy.zeros(0), numpy.zeros(0)
690       
691        return self._get_extrapolated_data(model=self._high_extrapolation_function,
692                                           npts=npts,
693                                           q_start=q_start, q_end=q_end)
[4e80ae0]694     
695    def set_extrapolation(self, range, npts=4, function=None, power=None):
696        """
[a45622a]697        Set the extrapolation parameters for the high or low Q-range.
698        Note that this does not turn extrapolation on or off.
699       
700        :param range: a keyword set the type of extrapolation . type string
701        :param npts: the numbers of q points of data to consider for extrapolation
702        :param function: a keyword to select the function to use for extrapolation.
703            of type string.
704        :param power: an power to apply power_low function
[4e80ae0]705               
706        """
707        range = range.lower()
708        if range not in ['high', 'low']:
709            raise ValueError, "Extrapolation range should be 'high' or 'low'"
710        function = function.lower()
711        if function not in ['power_law', 'guinier']:
712            raise ValueError, "Extrapolation function should be 'guinier' or 'power_law'"
713       
714        if range == 'high':
715            if function != 'power_law':
716                raise ValueError, "Extrapolation only allows a power law at high Q"
717            self._high_extrapolation_npts  = npts
718            self._high_extrapolation_power = power
[90e5ca1]719            self._high_extrapolation_power_fitted = power
[4e80ae0]720        else:
721            if function == 'power_law':
[59a41066]722                self._low_extrapolation_function = PowerLaw()
[4e80ae0]723            else:
[59a41066]724                self._low_extrapolation_function = Guinier()
[4e80ae0]725            self._low_extrapolation_npts  = npts
726            self._low_extrapolation_power = power
[90e5ca1]727            self._low_extrapolation_power_fitted = power
[4e80ae0]728       
729    def get_qstar(self, extrapolation=None):
730        """
[a45622a]731        Compute the invariant of the local copy of data.
732       
733        :param extrapolation: string to apply optional extrapolation
[4e80ae0]734           
[a45622a]735        :return q_star: invariant of the data within data's q range
736       
737        :warning: When using setting data to Data1D , the user is responsible of
[4e80ae0]738            checking that the scale and the background are properly apply to the data
[a45622a]739       
[4e80ae0]740        """
[bdd162f]741        self._qstar = self._get_qstar(self._data)
742        self._qstar_err = self._get_qstar_uncertainty(self._data)
[4e80ae0]743       
744        if extrapolation is None:
745            return self._qstar
[bdd162f]746       
747        # Compute invariant plus invariant of extrapolated data
[4e80ae0]748        extrapolation = extrapolation.lower()   
749        if extrapolation == "low":
[bdd162f]750            qs_low, dqs_low = self.get_qstar_low()
751            qs_hi, dqs_hi   = 0, 0
752           
[4e80ae0]753        elif extrapolation == "high":
[bdd162f]754            qs_low, dqs_low = 0, 0
755            qs_hi, dqs_hi   = self.get_qstar_high()
756           
[4e80ae0]757        elif extrapolation == "both":
[bdd162f]758            qs_low, dqs_low = self.get_qstar_low()
759            qs_hi, dqs_hi   = self.get_qstar_high()
760           
761        self._qstar     += qs_low + qs_hi
762        self._qstar_err = math.sqrt(self._qstar_err*self._qstar_err \
763                                    + dqs_low*dqs_low + dqs_hi*dqs_hi)
764       
765        return self._qstar
[4e80ae0]766       
[bdd162f]767    def get_surface(self, contrast, porod_const, extrapolation=None):
[4e80ae0]768        """
[a45622a]769        Compute the specific surface from the data.
770       
771        Implementation::
772       
773          V =  self.get_volume_fraction(contrast, extrapolation)
774   
775          Compute the surface given by:
776            surface = (2*pi *V(1- V)*porod_const)/ q_star
777           
778        :param contrast: contrast value to compute the volume
779        :param porod_const: Porod constant to compute the surface
780        :param extrapolation: string to apply optional extrapolation
[4e80ae0]781       
[a45622a]782        :return: specific surface
[4e80ae0]783        """
784        # Compute the volume
[bdd162f]785        volume = self.get_volume_fraction(contrast, extrapolation)
[4e80ae0]786        return 2 * math.pi * volume *(1 - volume) * float(porod_const)/self._qstar
787       
[bdd162f]788    def get_volume_fraction(self, contrast, extrapolation=None):
[4e80ae0]789        """
[a45622a]790        Compute volume fraction is deduced as follow: ::
791       
[4e80ae0]792            q_star = 2*(pi*contrast)**2* volume( 1- volume)
793            for k = 10^(-8)*q_star/(2*(pi*|contrast|)**2)
794            we get 2 values of volume:
795                 with   1 - 4 * k >= 0
796                 volume1 = (1- sqrt(1- 4*k))/2
797                 volume2 = (1+ sqrt(1- 4*k))/2
798           
799            q_star: the invariant value included extrapolation is applied
800                         unit  1/A^(3)*1/cm
801                    q_star = self.get_qstar()
802                   
[bdd162f]803            the result returned will be 0 <= volume <= 1
[a45622a]804       
805        :param contrast: contrast value provides by the user of type float.
806                 contrast unit is 1/A^(2)= 10^(16)cm^(2)
807        :param extrapolation: string to apply optional extrapolation
808       
809        :return: volume fraction
810       
811        :note: volume fraction must have no unit
[4e80ae0]812        """
[bdd162f]813        if contrast <= 0:
814            raise ValueError, "The contrast parameter must be greater than zero" 
[4e80ae0]815       
[bdd162f]816        # Make sure Q star is up to date
817        self.get_qstar(extrapolation)
[4e80ae0]818       
[bdd162f]819        if self._qstar <= 0:
820            raise RuntimeError, "Invalid invariant: Invariant Q* must be greater than zero"
[4e80ae0]821       
822        # Compute intermediate constant
823        k =  1.e-8 * self._qstar/(2 * (math.pi * math.fabs(float(contrast)))**2)
[bdd162f]824        # Check discriminant value
[4e80ae0]825        discrim = 1 - 4 * k
826       
827        # Compute volume fraction
828        if discrim < 0:
[bdd162f]829            raise RuntimeError, "Could not compute the volume fraction: negative discriminant"
[4e80ae0]830        elif discrim == 0:
831            return 1/2
832        else:
833            volume1 = 0.5 * (1 - math.sqrt(discrim))
834            volume2 = 0.5 * (1 + math.sqrt(discrim))
835           
836            if 0 <= volume1 and volume1 <= 1:
837                return volume1
838            elif 0 <= volume2 and volume2 <= 1: 
839                return volume2
[bdd162f]840            raise RuntimeError, "Could not compute the volume fraction: inconsistent results"
[ef9ed58]841   
842    def get_qstar_with_error(self, extrapolation=None):
[75047cf]843        """
[a45622a]844        Compute the invariant uncertainty.
845        This uncertainty computation depends on whether or not the data is
846        smeared.
847       
848        :param extrapolation: string to apply optional extrapolation
849       
850        :return: invariant, the invariant uncertainty
[bdd162f]851        """   
852        self.get_qstar(extrapolation)
[ef9ed58]853        return self._qstar, self._qstar_err
854   
[bdd162f]855    def get_volume_fraction_with_error(self, contrast, extrapolation=None):
[75047cf]856        """
[a45622a]857        Compute uncertainty on volume value as well as the volume fraction
858        This uncertainty is given by the following equation: ::
859       
[75047cf]860            dV = 0.5 * (4*k* dq_star) /(2* math.sqrt(1-k* q_star))
861                                 
[437a9f0]862            for k = 10^(-8)*q_star/(2*(pi*|contrast|)**2)
[2cce133]863           
[75047cf]864            q_star: the invariant value including extrapolated value if existing
865            dq_star: the invariant uncertainty
866            dV: the volume uncertainty
[a45622a]867       
868        The uncertainty will be set to -1 if it can't be computed.
869       
870        :param contrast: contrast value
871        :param extrapolation: string to apply optional extrapolation
872       
873        :return: V, dV = volume fraction, error on volume fraction
[75047cf]874        """
[bdd162f]875        volume = self.get_volume_fraction(contrast, extrapolation)
[75047cf]876       
[bdd162f]877        # Compute error
[437a9f0]878        k =  1.e-8 * self._qstar /(2 * (math.pi* math.fabs(float(contrast)))**2)
[bdd162f]879        # Check value inside the sqrt function
[ef9ed58]880        value = 1 - k * self._qstar
[bad9ae2]881        if (value) <= 0:
[bdd162f]882            uncertainty = -1
[ef9ed58]883        # Compute uncertainty
[bdd162f]884        uncertainty = math.fabs((0.5 * 4 * k * self._qstar_err)/(2 * math.sqrt(1 - k * self._qstar)))
[ef9ed58]885       
[bdd162f]886        return volume, uncertainty
[ef9ed58]887   
[bdd162f]888    def get_surface_with_error(self, contrast, porod_const, extrapolation=None):
[75047cf]889        """
[a45622a]890        Compute uncertainty of the surface value as well as the surface value.
891        The uncertainty is given as follow: ::
892       
[75047cf]893            dS = porod_const *2*pi[( dV -2*V*dV)/q_star
894                 + dq_star(v-v**2)
895                 
[bdd162f]896            q_star: the invariant value
[75047cf]897            dq_star: the invariant uncertainty
898            V: the volume fraction value
899            dV: the volume uncertainty
[a45622a]900       
901        :param contrast: contrast value
902        :param porod_const: porod constant value
903        :param extrapolation: string to apply optional extrapolation
904       
905        :return S, dS: the surface, with its uncertainty
[75047cf]906        """
[bdd162f]907        # We get the volume fraction, with error
908        #   get_volume_fraction_with_error calls get_volume_fraction
909        #   get_volume_fraction calls get_qstar
910        #   which computes Qstar and dQstar
911        v, dv = self.get_volume_fraction_with_error(contrast, extrapolation)
912
[c75a8ed]913        s = self.get_surface(contrast=contrast, porod_const=porod_const, 
914                             extrapolation=extrapolation)
[ef9ed58]915        ds = porod_const * 2 * math.pi * (( dv - 2 * v * dv)/ self._qstar\
916                 + self._qstar_err * ( v - v**2))
[bdd162f]917
[ef9ed58]918        return s, ds
Note: See TracBrowser for help on using the repository browser.