source: sasview/src/sas/models/qsmearing.py @ a3f125f0

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 a3f125f0 was a3f125f0, checked in by Paul Kienzle <pkienzle@…>, 9 years ago

refactor resolution calculation to enable sasmodels resolution calcuator for pinhole smearing, but don't enable it yet

  • Property mode set to 100644
File size: 23.2 KB
Line 
1"""
2    Handle Q smearing
3"""
4#####################################################################
5#This software was developed by the University of Tennessee as part of the
6#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
7#project funded by the US National Science Foundation.
8#See the license text in license.txt
9#copyright 2008, University of Tennessee
10######################################################################
11import numpy
12import math
13import logging
14import sys
15import sas.models.sas_extension.smearer as smearer
16from sas.models.smearing_2d import Smearer2D
17
18def smear_selection(data1D, model = None):
19    """
20    Creates the right type of smearer according
21    to the data.
22
23    The canSAS format has a rule that either
24    slit smearing data OR resolution smearing data
25    is available.
26   
27    For the present purpose, we choose the one that
28    has none-zero data. If both slit and resolution
29    smearing arrays are filled with good data
30    (which should not happen), then we choose the
31    resolution smearing data.
32   
33    :param data1D: Data1D object
34    :param model: sas.model instance
35    """
36    # Sanity check. If we are not dealing with a SAS Data1D
37    # object, just return None
38    if  data1D.__class__.__name__ not in ['Data1D', 'Theory1D']:
39        if data1D == None:
40            return None
41        elif data1D.dqx_data == None or data1D.dqy_data == None:
42            return None
43        return Smearer2D(data1D)
44   
45    if  not hasattr(data1D, "dx") and not hasattr(data1D, "dxl")\
46         and not hasattr(data1D, "dxw"):
47        return None
48   
49    # Look for resolution smearing data
50    _found_resolution = False
51    if data1D.dx is not None and len(data1D.dx) == len(data1D.x):
52       
53        # Check that we have non-zero data
54        if data1D.dx[0] > 0.0:
55            _found_resolution = True
56            #print "_found_resolution",_found_resolution
57            #print "data1D.dx[0]",data1D.dx[0],data1D.dxl[0]
58    # If we found resolution smearing data, return a QSmearer
59    if _found_resolution == True:
60        return QSmearer(data1D, model)
61        #return pinhole_smear(data1D, model)
62
63    # Look for slit smearing data
64    _found_slit = False
65    if data1D.dxl is not None and len(data1D.dxl) == len(data1D.x) \
66        and data1D.dxw is not None and len(data1D.dxw) == len(data1D.x):
67       
68        # Check that we have non-zero data
69        if data1D.dxl[0] > 0.0 or data1D.dxw[0] > 0.0:
70            _found_slit = True
71       
72        # Sanity check: all data should be the same as a function of Q
73        for item in data1D.dxl:
74            if data1D.dxl[0] != item:
75                _found_resolution = False
76                break
77           
78        for item in data1D.dxw:
79            if data1D.dxw[0] != item:
80                _found_resolution = False
81                break
82    # If we found slit smearing data, return a slit smearer
83    if _found_slit == True:
84        #return SlitSmearer(data1D, model)
85        return slit_smear(data1D, model)
86    return None
87           
88
89class _BaseSmearer(object):
90    """
91        Base class for smearers
92    """
93    def __init__(self):
94        self.nbins = 0
95        self.nbins_low = 0
96        self.nbins_high = 0
97        self._weights = None
98        ## Internal flag to keep track of C++ smearer initialization
99        self._init_complete = False
100        self._smearer = None
101        self.model = None
102        self.min = None
103        self.max = None
104        self.qvalues = []
105       
106    def __deepcopy__(self, memo=None):
107        """
108        Return a valid copy of self.
109        Avoid copying the _smearer C object and force a matrix recompute
110        when the copy is used. 
111        """
112        result = _BaseSmearer()
113        result.nbins = self.nbins
114        return result
115
116    def _compute_matrix(self):
117        """
118            Place holder for matrix computation
119        """
120        return NotImplemented
121
122    def get_unsmeared_range(self, q_min=None, q_max=None):
123        """
124            Place holder for method returning unsmeared range
125        """
126        return NotImplemented
127   
128    def get_bin_range(self, q_min=None, q_max=None):
129        """
130       
131        :param q_min: minimum q-value to smear
132        :param q_max: maximum q-value to smear
133         
134        """
135        # If this is the first time we call for smearing,
136        # initialize the C++ smearer object first
137        if not self._init_complete:
138            self._initialize_smearer()
139        if q_min == None:
140            q_min = self.min
141        if q_max == None:
142            q_max = self.max
143
144        _qmin_unsmeared, _qmax_unsmeared = self.get_unsmeared_range(q_min,
145                                                                     q_max)
146        _first_bin = None
147        _last_bin  = None
148
149        #step = (self.max - self.min) / (self.nbins - 1.0)
150        # Find the first and last bin number in all extrapolated and real data
151        try:
152            for i in range(self.nbins):
153                q_i = smearer.get_q(self._smearer, i)
154                if (q_i >= _qmin_unsmeared) and (q_i <= _qmax_unsmeared):
155                    # Identify first and last bin
156                    if _first_bin is None:
157                        _first_bin = i
158                    else:
159                        _last_bin  = i
160        except:
161            msg = "_BaseSmearer.get_bin_range: "
162            msg += " error getting range\n  %s" % sys.exc_value
163            raise RuntimeError, msg
164   
165        #  Find the first and last bin number only in the real data
166        _first_bin, _last_bin = self._get_unextrapolated_bin( \
167                                        _first_bin, _last_bin)
168
169        return _first_bin, _last_bin
170
171    def __call__(self, iq_in, first_bin = 0, last_bin = None):
172        """
173        Perform smearing
174        """
175        # If this is the first time we call for smearing,
176        # initialize the C++ smearer object first
177        if not self._init_complete:
178            self._initialize_smearer()
179
180        if last_bin is None or last_bin >= len(iq_in):
181            last_bin = len(iq_in) - 1
182        # Check that the first bin is positive
183        if first_bin < 0:
184            first_bin = 0
185
186        # With a model given, compute I for the extrapolated points and append
187        # to the iq_in
188        iq_in_temp = iq_in
189        if self.model != None:
190            temp_first, temp_last = self._get_extrapolated_bin( \
191                                                        first_bin, last_bin)
192            if self.nbins_low > 0:
193                iq_in_low = self.model.evalDistribution( \
194                                    numpy.fabs(self.qvalues[0:self.nbins_low]))
195            iq_in_high = self.model.evalDistribution( \
196                                            self.qvalues[(len(self.qvalues) - \
197                                            self.nbins_high - 1):])
198            # Todo: find out who is sending iq[last_poin] = 0.
199            if iq_in[len(iq_in) - 1] == 0:
200                iq_in[len(iq_in) - 1] = iq_in_high[0]
201            # Append the extrapolated points to the data points
202            if self.nbins_low > 0:
203                iq_in_temp = numpy.append(iq_in_low, iq_in)
204            if self.nbins_high > 0:
205                iq_in_temp = numpy.append(iq_in_temp, iq_in_high[1:])
206        else:
207            temp_first = first_bin
208            temp_last = last_bin
209            #iq_in_temp = iq_in
210
211        # Sanity check
212        if len(iq_in_temp) != self.nbins:
213            msg = "Invalid I(q) vector: inconsistent array "
214            msg += " length %d != %s" % (len(iq_in_temp), str(self.nbins))
215            raise RuntimeError, msg
216
217        # Storage for smeared I(q)   
218        iq_out = numpy.zeros(self.nbins)
219
220        smear_output = smearer.smear(self._smearer, iq_in_temp, iq_out,
221                                      #0, self.nbins - 1)
222                                      temp_first, temp_last)
223                                      #first_bin, last_bin)
224        if smear_output < 0:
225            msg = "_BaseSmearer: could not smear, code = %g" % smear_output
226            raise RuntimeError, msg
227
228        temp_first = first_bin + self.nbins_low
229        temp_last = self.nbins - self.nbins_high
230        out = iq_out[temp_first: temp_last]
231
232        return out
233   
234    def _initialize_smearer(self):
235        """
236            Place holder for initializing data smearer
237        """
238        return NotImplemented
239           
240   
241    def _get_unextrapolated_bin(self, first_bin = 0, last_bin = 0):
242        """
243        Get unextrapolated first bin and the last bin
244       
245        : param first_bin: extrapolated first_bin
246        : param last_bin: extrapolated last_bin
247       
248        : return fist_bin, last_bin: unextrapolated first and last bin
249        """
250        #  For first bin
251        if first_bin <= self.nbins_low:
252            first_bin = 0
253        else:
254            first_bin = first_bin - self.nbins_low
255        # For last bin
256        if last_bin >= (self.nbins - self.nbins_high):
257            last_bin  = self.nbins - (self.nbins_high + self.nbins_low + 1)
258        elif last_bin >= self.nbins_low:
259            last_bin = last_bin - self.nbins_low
260        else:
261            last_bin = 0
262        return first_bin, last_bin
263   
264    def _get_extrapolated_bin(self, first_bin = 0, last_bin = 0):
265        """
266        Get extrapolated first bin and the last bin
267       
268        : param first_bin: unextrapolated first_bin
269        : param last_bin: unextrapolated last_bin
270       
271        : return first_bin, last_bin: extrapolated first and last bin
272        """
273        #  For the first bin
274        # In the case that needs low extrapolation data
275        first_bin = 0
276        # For last bin
277        if last_bin >= self.nbins - (self.nbins_high + self.nbins_low + 1):
278            # In the case that needs higher q extrapolation data
279            last_bin = self.nbins - 1
280        else:
281            # In the case that doesn't need higher q extrapolation data
282            last_bin += self.nbins_low
283
284        return first_bin, last_bin
285       
286class _SlitSmearer(_BaseSmearer):
287    """
288    Slit smearing for I(q) array
289    """
290   
291    def __init__(self, nbins=None, width=None, height=None, min=None, max=None):
292        """
293        Initialization
294           
295        :param iq: I(q) array [cm-1]
296        :param width: slit width [A-1]
297        :param height: slit height [A-1]
298        :param min: Q_min [A-1]
299        :param max: Q_max [A-1]
300       
301        """
302        _BaseSmearer.__init__(self)
303        ## Slit width in Q units
304        self.width  = width
305        ## Slit height in Q units
306        self.height = height
307        ## Q_min (Min Q-value for I(q))
308        self.min    = min
309        ## Q_max (Max Q_value for I(q))
310        self.max    = max
311        ## Number of Q bins
312        self.nbins  = nbins
313        ## Number of points used in the smearing computation
314        self.npts   = 3000
315        ## Smearing matrix
316        self._weights = None
317        self.qvalues  = None
318       
319    def _initialize_smearer(self):
320        """
321        Initialize the C++ smearer object.
322        This method HAS to be called before smearing
323        """
324        #self._smearer = smearer.new_slit_smearer(self.width,
325        # self.height, self.min, self.max, self.nbins)
326        self._smearer = smearer.new_slit_smearer_with_q(self.width, 
327                                                    self.height, self.qvalues)
328        self._init_complete = True
329
330    def get_unsmeared_range(self, q_min, q_max):
331        """
332        Determine the range needed in unsmeared-Q to cover
333        the smeared Q range
334        """
335        # Range used for input to smearing
336        _qmin_unsmeared = q_min
337        _qmax_unsmeared = q_max
338        try:
339            _qmin_unsmeared = self.min
340            _qmax_unsmeared = self.max
341        except:
342            logging.error("_SlitSmearer.get_bin_range: %s" % sys.exc_value)
343        return _qmin_unsmeared, _qmax_unsmeared
344
345class SlitSmearer(_SlitSmearer):
346    """
347    Adaptor for slit smearing class and SAS data
348    """
349    def __init__(self, data1D, model = None):
350        """
351        Assumption: equally spaced bins of increasing q-values.
352       
353        :param data1D: data used to set the smearing parameters
354        """
355        # Initialization from parent class
356        super(SlitSmearer, self).__init__()
357       
358        ## Slit width
359        self.width = 0
360        self.nbins_low = 0
361        self.nbins_high = 0
362        self.model = model
363        if data1D.dxw is not None and len(data1D.dxw) == len(data1D.x):
364            self.width = data1D.dxw[0]
365            # Sanity check
366            for value in data1D.dxw:
367                if value != self.width:
368                    msg = "Slit smearing parameters must "
369                    msg += " be the same for all data"
370                    raise RuntimeError, msg
371        ## Slit height
372        self.height = 0
373        if data1D.dxl is not None and len(data1D.dxl) == len(data1D.x):
374            self.height = data1D.dxl[0]
375            # Sanity check
376            for value in data1D.dxl:
377                if value != self.height:
378                    msg = "Slit smearing parameters must be"
379                    msg += " the same for all data"
380                    raise RuntimeError, msg
381        # If a model is given, get the q extrapolation
382        if self.model == None:
383            data1d_x = data1D.x
384        else:
385            # Take larger sigma
386            if self.height > self.width:
387                # The denominator (2.0) covers all the possible w^2 + h^2 range
388                sigma_in = data1D.dxl / 2.0
389            elif self.width > 0:
390                sigma_in = data1D.dxw / 2.0
391            else:
392                sigma_in = []
393
394            self.nbins_low, self.nbins_high, _, data1d_x = \
395                                get_qextrapolate(sigma_in, data1D.x)
396
397        ## Number of Q bins
398        self.nbins = len(data1d_x)
399        ## Minimum Q
400        self.min = min(data1d_x)
401        ## Maximum
402        self.max = max(data1d_x)
403        ## Q-values
404        self.qvalues = data1d_x
405       
406
407class _QSmearer(_BaseSmearer):
408    """
409    Perform Gaussian Q smearing
410    """
411       
412    def __init__(self, nbins=None, width=None, min=None, max=None):
413        """
414        Initialization
415       
416        :param nbins: number of Q bins
417        :param width: array standard deviation in Q [A-1]
418        :param min: Q_min [A-1]
419        :param max: Q_max [A-1]
420        """
421        _BaseSmearer.__init__(self)
422        ## Standard deviation in Q [A-1]
423        self.width = width
424        ## Q_min (Min Q-value for I(q))
425        self.min = min
426        ## Q_max (Max Q_value for I(q))
427        self.max = max
428        ## Number of Q bins
429        self.nbins = nbins
430        ## Smearing matrix
431        self._weights = None
432        self.qvalues  = None
433       
434    def _initialize_smearer(self):
435        """
436        Initialize the C++ smearer object.
437        This method HAS to be called before smearing
438        """
439        #self._smearer = smearer.new_q_smearer(numpy.asarray(self.width),
440        # self.min, self.max, self.nbins)
441        self._smearer = smearer.new_q_smearer_with_q(numpy.asarray(self.width),
442                                                      self.qvalues)
443        self._init_complete = True
444       
445    def get_unsmeared_range(self, q_min, q_max):
446        """
447        Determine the range needed in unsmeared-Q to cover
448        the smeared Q range
449        Take 3 sigmas as the offset between smeared and unsmeared space
450        """
451        # Range used for input to smearing
452        _qmin_unsmeared = q_min
453        _qmax_unsmeared = q_max
454        try:
455            offset = 3.0 * max(self.width)
456            _qmin_unsmeared = self.min#max([self.min, q_min - offset])
457            _qmax_unsmeared = self.max#min([self.max, q_max + offset])
458        except:
459            logging.error("_QSmearer.get_bin_range: %s" % sys.exc_value)
460        return _qmin_unsmeared, _qmax_unsmeared
461       
462   
463class QSmearer(_QSmearer):
464    """
465    Adaptor for Gaussian Q smearing class and SAS data
466    """
467    def __init__(self, data1D, model = None):
468        """
469        Assumption: equally spaced bins of increasing q-values.
470       
471        :param data1D: data used to set the smearing parameters
472        """
473        # Initialization from parent class
474        super(QSmearer, self).__init__()
475        data1d_x = []
476        self.nbins_low = 0
477        self.nbins_high = 0
478        self.model = model
479        ## Resolution
480        #self.width = numpy.zeros(len(data1D.x))
481        if data1D.dx is not None and len(data1D.dx) == len(data1D.x):
482            self.width = data1D.dx
483       
484        if self.model == None:
485            data1d_x = data1D.x
486        else:
487            self.nbins_low, self.nbins_high, self.width, data1d_x = \
488                                get_qextrapolate(self.width, data1D.x)
489
490        ## Number of Q bins
491        self.nbins = len(data1d_x)
492        ## Minimum Q
493        self.min = min(data1d_x)
494        ## Maximum
495        self.max = max(data1d_x)
496        ## Q-values
497        self.qvalues = data1d_x
498
499       
500def get_qextrapolate(width, data_x):
501    """
502    Make fake data_x points extrapolated outside of the data_x points
503   
504    :param width: array of std of q resolution
505    :param Data1D.x: Data1D.x array
506   
507    :return new_width, data_x_ext: extrapolated width array and x array
508   
509    :assumption1: data_x is ordered from lower q to higher q
510    :assumption2: len(data) = len(width)
511    :assumption3: the distance between the data points is more compact than the size of width
512    :Todo1: Make sure that the assumptions are correct for Data1D
513    :Todo2: This fixes the edge problem in Qsmearer but still needs to make smearer interface
514    """
515    # Length of the width
516    length = len(width)
517    width_low = math.fabs(width[0])   
518    width_high = math.fabs(width[length -1])
519    nbins_low = 0.0 
520    nbins_high = 0.0
521    # Compare width(dQ) to the data bin size and take smaller one as the bin
522    # size of the extrapolation; this will correct some weird behavior
523    # at the edge: This method was out (commented)
524    # because it becomes very expansive when
525    # bin size is very small comparing to the width.
526    # Now on, we will just give the bin size of the extrapolated points
527    # based on the width.
528    # Find bin sizes
529    #bin_size_low = math.fabs(data_x[1] - data_x[0])
530    #bin_size_high = math.fabs(data_x[length - 1] - data_x[length - 2])
531    # Let's set the bin size 1/3 of the width(sigma), it is good as long as
532    # the scattering is monotonous.
533    #if width_low < (bin_size_low):
534    bin_size_low = width_low / 10.0
535    #if width_high < (bin_size_high):
536    bin_size_high = width_high / 10.0
537       
538    # Number of q points required below the 1st data point in order to extend
539    # them 3 times of the width (std)
540    if width_low > 0.0:
541        nbins_low = math.ceil(3.0 * width_low / bin_size_low)
542    # Number of q points required above the last data point
543    if width_high > 0.0:
544        nbins_high = math.ceil(3.0 * width_high / bin_size_high)
545    # Make null q points       
546    extra_low = numpy.zeros(nbins_low)
547    extra_high = numpy.zeros(nbins_high)
548    # Give extrapolated values
549    ind = 0
550    qvalue = data_x[0] - bin_size_low
551    #if qvalue > 0:
552    while(ind < nbins_low):
553        extra_low[nbins_low - (ind + 1)] = qvalue
554        qvalue -= bin_size_low
555        ind += 1
556        #if qvalue <= 0:
557        #    break
558    # Redefine nbins_low
559    nbins_low = ind
560    # Reset ind for another extrapolation
561    ind = 0
562    qvalue = data_x[length -1] + bin_size_high
563    while(ind < nbins_high):
564        extra_high[ind] = qvalue
565        qvalue += bin_size_high
566        ind += 1
567    # Make a new qx array
568    if nbins_low > 0: 
569        data_x_ext = numpy.append(extra_low, data_x)
570    else:
571        data_x_ext = data_x
572    data_x_ext = numpy.append(data_x_ext, extra_high)
573   
574    # Redefine extra_low and high based on corrected nbins 
575    # And note that it is not necessary for extra_width to be a non-zero
576    if nbins_low > 0:     
577        extra_low = numpy.zeros(nbins_low)
578    extra_high = numpy.zeros(nbins_high) 
579    # Make new width array
580    new_width = numpy.append(extra_low, width)
581    new_width = numpy.append(new_width, extra_high)
582   
583    # nbins corrections due to the negative q value
584    nbins_low = nbins_low - len(data_x_ext[data_x_ext <= 0])
585    return nbins_low, nbins_high, \
586             new_width[data_x_ext > 0], data_x_ext[data_x_ext > 0]
587
588
589
590from .resolution import Slit1D, Pinhole1D
591class PySmear(object):
592    """
593    Wrapper for pure python sasmodels resolution functions.
594    """
595    def __init__(self, resolution, model):
596        self.model = model
597        self.resolution = resolution
598        self.offset = numpy.searchsorted(self.resolution.q_calc, self.resolution.q[0])
599
600    def apply(self, iq_in, first_bin=0, last_bin=None):
601        """
602        Apply the resolution function to the data.
603
604        Note that this is called with iq_in matching data.x, but with
605        iq_in[first_bin:last_bin] set to theory values for these bins,
606        and the remainder left undefined.  The first_bin, last_bin values
607        should be those returned from get_bin_range.
608
609        The returned value is of the same length as iq_in, with the range
610        first_bin:last_bin set to the resolution smeared values.
611        """
612        if last_bin is None: last_bin = len(iq_in)
613        start, end = first_bin + self.offset, last_bin + self.offset
614        q_calc = self.resolution.q_calc
615        iq_calc = numpy.empty_like(q_calc)
616        if start > 0:
617            iq_calc[:start] = self.model.evalDistribution(q_calc[:start])
618        if end+1 < len(q_calc):
619            iq_calc[end+1:] = self.model.evalDistribution(q_calc[end+1:])
620        iq_calc[start:end+1] = iq_in[first_bin:last_bin+1]
621        smeared = self.resolution.apply(iq_calc)
622        return smeared
623    __call__ = apply
624
625    def get_bin_range(self, q_min=None, q_max=None):
626        """
627        For a given q_min, q_max, find the corresponding indices in the data.
628
629        Returns first, last.
630
631        Note that these are indexes into q from the data, not the q_calc
632        needed by the resolution function.  Note also that these are the
633        indices, not the range limits.  That is, the complete range will be
634        q[first:last+1].
635        """
636        q = self.resolution.q
637        first = numpy.searchsorted(q, q_min)
638        last = numpy.searchsorted(q, q_max)
639        return first, min(last,len(q)-1)
640
641def slit_smear(data, model=None):
642    q = data.x
643    width = data.dxw if data.dxw is not None else 0
644    height = data.dxl if data.dxl is not None else 0
645    # TODO: width and height seem to be reversed
646    return PySmear(Slit1D(q, height, width), model)
647
648def pinhole_smear(data, model=None):
649    q = data.x
650    width = data.dx if data.dx is not None else 0
651    return PySmear(Pinhole1D(q, width), model)
Note: See TracBrowser for help on using the repository browser.