source: sasview/src/sas/sascalc/corfunc/corfunc_calculator.py @ 033c14c

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 033c14c was 033c14c, checked in by lewis, 8 years ago

Implement extract parameters

  • Property mode set to 100644
File size: 9.3 KB
Line 
1"""
2This module implements corfunc
3"""
4import warnings
5import numpy as np
6from scipy.optimize import curve_fit
7from scipy.interpolate import interp1d
8from scipy.fftpack import dct
9from scipy.signal import argrelextrema
10from numpy.linalg import lstsq
11from sas.sascalc.dataloader.data_info import Data1D
12
13class CorfuncCalculator(object):
14
15    class _Interpolator(object):
16        """
17        Interpolates between curve f and curve g over the range start:stop and
18        caches the result of the function when it's called
19
20        :param f: The first curve to interpolate
21        :param g: The second curve to interpolate
22        :param start: The value at which to start the interpolation
23        :param stop: The value at which to stop the interpolation
24        """
25        def __init__(self, f, g, start, stop):
26            self.f = f
27            self.g = g
28            self.start = start
29            self.stop = stop
30            self._lastx = []
31            self._lasty = []
32
33        def __call__(self, x):
34            if self._lastx == [] or x.tolist() != self._lastx.tolist():
35                self._lasty = self._smoothed_function(x)
36                self._lastx = x
37            return self._lasty
38
39        def _smoothed_function(self,x):
40            ys = np.zeros(x.shape)
41            ys[x <= self.start] = self.f(x[x <= self.start])
42            ys[x >= self.stop] = self.g(x[x >= self.stop])
43            with warnings.catch_warnings():
44                # Ignore divide by zero error
45                warnings.simplefilter('ignore')
46                h = 1/(1+(x-self.stop)**2/(self.start-x)**2)
47            mask = np.logical_and(x > self.start, x < self.stop)
48            ys[mask] = h[mask]*self.g(x[mask])+(1-h[mask])*self.f(x[mask])
49            return ys
50
51
52    def __init__(self, data=None, lowerq=None, upperq=None, scale=1):
53        """
54        Initialize the class.
55
56        :param data: Data of the type DataLoader.Data1D
57        :param lowerq: The Q value to use as the boundary for
58            Guinier extrapolation
59        :param upperq: A tuple of the form (lower, upper).
60            Values between lower and upper will be used for Porod extrapolation
61        :param scale: Scaling factor for I(q)
62        """
63        self._data = None
64        self.set_data(data, scale)
65        self.lowerq = lowerq
66        self.upperq = upperq
67        self.background = 0
68
69    def set_data(self, data, scale=1):
70        """
71        Prepares the data for analysis
72
73        :return: new_data = data * scale - background
74        """
75        if data is None:
76            return
77        # Only process data of the class Data1D
78        if not issubclass(data.__class__, Data1D):
79            raise ValueError, "Data must be of the type DataLoader.Data1D"
80
81        # Prepare the data
82        new_data = Data1D(x=data.x, y=data.y)
83        new_data *= scale
84
85        # Ensure the errors are set correctly
86        if new_data.dy is None or len(new_data.x) != len(new_data.dy) or \
87            (min(new_data.dy) == 0 and max(new_data.dy) == 0):
88            new_data.dy = np.ones(len(new_data.x))
89
90        self._data = new_data
91
92    def compute_background(self, upperq=None):
93        """
94        Compute the background level from the Porod region of the data
95        """
96        if self._data is None: return 0
97        elif upperq is None and self.upperq is not None: upperq = self.upperq
98        elif upperq == 0: return 0
99        q = self._data.x
100        mask = np.logical_and(q > upperq[0], q < upperq[1])
101        _, _, bg = self._fit_porod(q[mask], self._data.y[mask])
102
103        return bg
104
105    def compute_extrapolation(self):
106        """
107        Extrapolate and interpolate scattering data
108
109        :return: The extrapolated data
110        """
111        q = self._data.x
112        iq = self._data.y
113
114        s2 = self._fit_data(q, iq)
115        qs = np.arange(0, q[-1]*100, (q[1]-q[0]))
116        iqs = s2(qs)
117
118        extrapolation = Data1D(qs, iqs)
119
120        return extrapolation
121
122    def compute_transform(self, extrapolation, background=None):
123        """
124        Transform an extrapolated scattering curve into a correlation function.
125
126        :param extrapolation: The extrapolated data
127        :param background: The background value (if not provided, previously
128            calculated value will be used)
129        :return: The transformed data
130        """
131        qs = extrapolation.x
132        iqs = extrapolation.y
133        q = self._data.x
134        if background is None: background = self.background
135
136        gamma = dct((iqs-background)*qs**2)
137        gamma = gamma / gamma.max()
138        xs = np.pi*np.arange(len(qs),dtype=np.float32)/(q[1]-q[0])/len(qs)
139
140        transform = Data1D(xs, gamma)
141
142        return transform
143
144    def extract_parameters(self, transformed_data):
145        """
146        Extract the interesting measurements from a correlation function
147        :param transformed_data: Fourier transformation of the
148            extrapolated data
149        """
150        # Calculate indexes of maxima and minima
151        x = transformed_data.x
152        y = transformed_data.y
153        maxs = argrelextrema(y, np.greater)[0]
154        mins = argrelextrema(y, np.less)[0]
155
156        # If there are no maxima, return None
157        if len(maxs) == 0:
158            return None
159
160        GammaMin = y[mins[0]]  # The value at the first minimum
161
162        ddy = (y[:-2]+y[2:]-2*y[1:-1])/(x[2:]-x[:-2])**2  # 2nd derivative of y
163        dy = (y[2:]-y[:-2])/(x[2:]-x[:-2])  # 1st derivative of y
164        # Find where the second derivative goes to zero
165        zeros = argrelextrema(np.abs(ddy), np.less)[0]
166        # locate the first inflection point
167        linear_point = zeros[0]
168        linear_point = int(mins[0]/10)
169
170        # Try to calculate slope around linear_point using 80 data points
171        lower = linear_point - 40
172        upper = linear_point + 40
173
174        # If too few data points to the left, use linear_point*2 data points
175        if lower < 0:
176            lower = 0
177            upper = linear_point * 2
178        # If too few to right, use 2*(dy.size - linear_point) data points
179        elif upper > len(dy):
180            upper = len(dy)
181            width = len(dy) - linear_point
182            lower = 2*linear_point - dy.size
183
184        m = np.mean(dy[lower:upper])  # Linear slope
185        b = y[1:-1][linear_point]-m*x[1:-1][linear_point]  # Linear intercept
186
187        Lc = (GammaMin-b)/# Hard block thickness
188
189        # Find the data points where the graph is linear to within 1%
190        mask = np.where(np.abs((y-(m*x+b))/y) < 0.01)[0]
191        if len(mask) == 0:  # Return garbage for bad fits
192            return garbage
193        dtr = x[mask[0]]  # Beginning of Linear Section
194        d0 = x[mask[-1]]  # End of Linear Section
195        GammaMax = y[mask[-1]]
196        A = -GammaMin/GammaMax  # Normalized depth of minimum
197
198        params = {
199            'max': x[maxs[0]],
200            'dtr': dtr,
201            'Lc': Lc,
202            'd0': d0,
203            'A': A,
204            'Lc/max': Lc/x[maxs[0]]
205        }
206
207        for key, val in params.iteritems():
208            params[key] = self._round_sig_figs(val, 6)
209
210        return params
211
212
213    def _porod(self, q, K, sigma, bg):
214        """Equation for the Porod region of the data"""
215        return bg + (K*q**(-4))*np.exp(-q**2*sigma**2)
216
217    def _fit_guinier(self, q, iq):
218        """Fit the Guinier region of the curve"""
219        A = np.vstack([q**2, np.ones(q.shape)]).T
220        return lstsq(A, np.log(iq))
221
222    def _fit_porod(self, q, iq):
223        """Fit the Porod region of the curve"""
224        fitp = curve_fit(lambda q, k, sig, bg: self._porod(q, k, sig, bg)*q**2,
225                         q, iq*q**2)[0]
226        k, sigma, bg = fitp
227        return k, sigma, bg
228
229    def _fit_data(self, q, iq):
230        """
231        Given a data set, extrapolate out to large q with Porod and
232        to q=0 with Guinier
233        """
234        mask = np.logical_and(q > self.upperq[0], q < self.upperq[1])
235
236        # Returns an array where the 1st and 2nd elements are the values of k
237        # and sigma for the best-fit Porod function
238        k, sigma, _ = self._fit_porod(q[mask], iq[mask])
239        bg = self.background
240
241        # Smooths between the best-fit porod function and the data to produce a
242        # better fitting curve
243        data = interp1d(q, iq)
244        s1 = self._Interpolator(data,
245            lambda x: self._porod(x, k, sigma, bg), self.upperq[0], q[-1])
246
247        mask = np.logical_and(q < self.lowerq, 0 < q)
248
249        # Returns parameters for the best-fit Guinier function
250        g = self._fit_guinier(q[mask], iq[mask])[0]
251
252        # Smooths between the best-fit Guinier function and the Porod curve
253        s2 = self._Interpolator((lambda x: (np.exp(g[1]+g[0]*x**2))), s1, q[0],
254            self.lowerq)
255
256        return s2
257
258    def _round_sig_figs(self, x, sigfigs):
259        """
260        Round a number to a given number of significant figures.
261
262        :param x: The value to round
263        :param sigfigs: How many significant figures to round to
264        :return rounded_str: x rounded to the given number of significant
265            figures, as a string
266        """
267        # Index of first significant digit
268        significant_digit = -int(np.floor(np.log10(np.abs(x))))
269        # Number of digits required for correct number of sig figs
270        digits = significant_digit + (sigfigs - 1)
271        rounded = np.round(x, decimals=digits)
272        rounded_str = "{1:.{0}f}".format(sigfigs -1  + significant_digit,
273            rounded)
274        return rounded_str
Note: See TracBrowser for help on using the repository browser.