source: sasview/Invariant/invariant.py @ ded62ce

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 ded62ce was c75a8ed, checked in by Mathieu Doucet <doucetm@…>, 15 years ago

invariant: added unit tests for specific surface and volume fraction. Minor fixes.

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