source: sasview/src/sas/sascalc/invariant/invariant.py @ a8ed421

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.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since a8ed421 was b699768, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 9 years ago

Initial commit of the refactored SasCalc? module.

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